diff --git a/drizzle/0008_harsh_overlord.sql b/drizzle/0008_harsh_overlord.sql new file mode 100644 index 000000000..fd4d83682 --- /dev/null +++ b/drizzle/0008_harsh_overlord.sql @@ -0,0 +1,10 @@ +CREATE TABLE `prompt_templates` ( + `id` text PRIMARY KEY NOT NULL, + `name` text NOT NULL, + `text` text NOT NULL, + `sort_order` integer DEFAULT 0 NOT NULL, + `created_at` text DEFAULT CURRENT_TIMESTAMP NOT NULL, + `updated_at` text DEFAULT CURRENT_TIMESTAMP NOT NULL +); +--> statement-breakpoint +CREATE INDEX `idx_prompt_templates_sort_order` ON `prompt_templates` (`sort_order`); \ No newline at end of file diff --git a/drizzle/meta/0008_snapshot.json b/drizzle/meta/0008_snapshot.json new file mode 100644 index 000000000..f2ddc7361 --- /dev/null +++ b/drizzle/meta/0008_snapshot.json @@ -0,0 +1,1351 @@ +{ + "version": "6", + "dialect": "sqlite", + "id": "16c733f3-096d-4755-ad6a-c0e7fc0ac08d", + "prevId": "68b5cc95-67aa-4078-94fa-c10caad8d9eb", + "tables": { + "app_secrets": { + "name": "app_secrets", + "columns": { + "key": { + "name": "key", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "secret": { + "name": "secret", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "idx_app_secrets_key": { + "name": "idx_app_secrets_key", + "columns": ["key"], + "isUnique": true + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + }, + "app_settings": { + "name": "app_settings", + "columns": { + "key": { + "name": "key", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "value": { + "name": "value", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "idx_app_settings_key": { + "name": "idx_app_settings_key", + "columns": ["key"], + "isUnique": true + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + }, + "conversations": { + "name": "conversations", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "project_id": { + "name": "project_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "task_id": { + "name": "task_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "provider": { + "name": "provider", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "config": { + "name": "config", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "idx_conversations_task_id": { + "name": "idx_conversations_task_id", + "columns": ["task_id"], + "isUnique": false + } + }, + "foreignKeys": { + "conversations_project_id_projects_id_fk": { + "name": "conversations_project_id_projects_id_fk", + "tableFrom": "conversations", + "tableTo": "projects", + "columnsFrom": ["project_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "conversations_task_id_tasks_id_fk": { + "name": "conversations_task_id_tasks_id_fk", + "tableFrom": "conversations", + "tableTo": "tasks", + "columnsFrom": ["task_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + }, + "editor_buffers": { + "name": "editor_buffers", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "project_id": { + "name": "project_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "file_path": { + "name": "file_path", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "content": { + "name": "content", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "idx_editor_buffers_workspace_file": { + "name": "idx_editor_buffers_workspace_file", + "columns": ["workspace_id", "file_path"], + "isUnique": false + } + }, + "foreignKeys": { + "editor_buffers_project_id_projects_id_fk": { + "name": "editor_buffers_project_id_projects_id_fk", + "tableFrom": "editor_buffers", + "tableTo": "projects", + "columnsFrom": ["project_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + }, + "kv": { + "name": "kv", + "columns": { + "key": { + "name": "key", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "value": { + "name": "value", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "idx_kv_key": { + "name": "idx_kv_key", + "columns": ["key"], + "isUnique": true + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + }, + "messages": { + "name": "messages", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "conversation_id": { + "name": "conversation_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "content": { + "name": "content", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "sender": { + "name": "sender", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "timestamp": { + "name": "timestamp", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + }, + "metadata": { + "name": "metadata", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": { + "idx_messages_conversation_id": { + "name": "idx_messages_conversation_id", + "columns": ["conversation_id"], + "isUnique": false + }, + "idx_messages_timestamp": { + "name": "idx_messages_timestamp", + "columns": ["timestamp"], + "isUnique": false + } + }, + "foreignKeys": { + "messages_conversation_id_conversations_id_fk": { + "name": "messages_conversation_id_conversations_id_fk", + "tableFrom": "messages", + "tableTo": "conversations", + "columnsFrom": ["conversation_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + }, + "project_remotes": { + "name": "project_remotes", + "columns": { + "project_id": { + "name": "project_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "remote_name": { + "name": "remote_name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "remote_url": { + "name": "remote_url", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": { + "project_remotes_project_id_projects_id_fk": { + "name": "project_remotes_project_id_projects_id_fk", + "tableFrom": "project_remotes", + "tableTo": "projects", + "columnsFrom": ["project_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "project_remotes_project_id_remote_name_pk": { + "columns": ["project_id", "remote_name"], + "name": "project_remotes_project_id_remote_name_pk" + } + }, + "uniqueConstraints": {} + }, + "projects": { + "name": "projects", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "path": { + "name": "path", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "workspace_provider": { + "name": "workspace_provider", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'local'" + }, + "base_ref": { + "name": "base_ref", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "ssh_connection_id": { + "name": "ssh_connection_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "idx_projects_path": { + "name": "idx_projects_path", + "columns": ["path"], + "isUnique": true + }, + "idx_projects_ssh_connection_id": { + "name": "idx_projects_ssh_connection_id", + "columns": ["ssh_connection_id"], + "isUnique": false + } + }, + "foreignKeys": { + "projects_ssh_connection_id_ssh_connections_id_fk": { + "name": "projects_ssh_connection_id_ssh_connections_id_fk", + "tableFrom": "projects", + "tableTo": "ssh_connections", + "columnsFrom": ["ssh_connection_id"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + }, + "prompt_templates": { + "name": "prompt_templates", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "text": { + "name": "text", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "sort_order": { + "name": "sort_order", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "idx_prompt_templates_sort_order": { + "name": "idx_prompt_templates_sort_order", + "columns": ["sort_order"], + "isUnique": false + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + }, + "pull_request_assignees": { + "name": "pull_request_assignees", + "columns": { + "pull_request_url": { + "name": "pull_request_url", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "idx_pra_pull_request_url": { + "name": "idx_pra_pull_request_url", + "columns": ["pull_request_url"], + "isUnique": false + }, + "idx_pra_user_id": { + "name": "idx_pra_user_id", + "columns": ["user_id"], + "isUnique": false + } + }, + "foreignKeys": { + "pull_request_assignees_pull_request_url_pull_requests_url_fk": { + "name": "pull_request_assignees_pull_request_url_pull_requests_url_fk", + "tableFrom": "pull_request_assignees", + "tableTo": "pull_requests", + "columnsFrom": ["pull_request_url"], + "columnsTo": ["url"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "pull_request_assignees_user_id_pull_request_users_user_id_fk": { + "name": "pull_request_assignees_user_id_pull_request_users_user_id_fk", + "tableFrom": "pull_request_assignees", + "tableTo": "pull_request_users", + "columnsFrom": ["user_id"], + "columnsTo": ["user_id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "pull_request_assignees_pull_request_url_user_id_pk": { + "columns": ["pull_request_url", "user_id"], + "name": "pull_request_assignees_pull_request_url_user_id_pk" + } + }, + "uniqueConstraints": {} + }, + "pull_request_checks": { + "name": "pull_request_checks", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "pull_request_url": { + "name": "pull_request_url", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "commit_sha": { + "name": "commit_sha", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "conclusion": { + "name": "conclusion", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "details_url": { + "name": "details_url", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "started_at": { + "name": "started_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "completed_at": { + "name": "completed_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "workflow_name": { + "name": "workflow_name", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "app_name": { + "name": "app_name", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "app_logo_url": { + "name": "app_logo_url", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": { + "idx_prc_pull_request_url": { + "name": "idx_prc_pull_request_url", + "columns": ["pull_request_url"], + "isUnique": false + } + }, + "foreignKeys": { + "pull_request_checks_pull_request_url_pull_requests_url_fk": { + "name": "pull_request_checks_pull_request_url_pull_requests_url_fk", + "tableFrom": "pull_request_checks", + "tableTo": "pull_requests", + "columnsFrom": ["pull_request_url"], + "columnsTo": ["url"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + }, + "pull_request_labels": { + "name": "pull_request_labels", + "columns": { + "pull_request_id": { + "name": "pull_request_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "color": { + "name": "color", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": { + "idx_prl_name": { + "name": "idx_prl_name", + "columns": ["name"], + "isUnique": false + } + }, + "foreignKeys": { + "pull_request_labels_pull_request_id_pull_requests_url_fk": { + "name": "pull_request_labels_pull_request_id_pull_requests_url_fk", + "tableFrom": "pull_request_labels", + "tableTo": "pull_requests", + "columnsFrom": ["pull_request_id"], + "columnsTo": ["url"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "pull_request_labels_pull_request_id_name_pk": { + "columns": ["pull_request_id", "name"], + "name": "pull_request_labels_pull_request_id_name_pk" + } + }, + "uniqueConstraints": {} + }, + "pull_request_users": { + "name": "pull_request_users", + "columns": { + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "user_name": { + "name": "user_name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "display_name": { + "name": "display_name", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "avatar_url": { + "name": "avatar_url", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "url": { + "name": "url", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "user_updated_at": { + "name": "user_updated_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "user_created_at": { + "name": "user_created_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + }, + "pull_requests": { + "name": "pull_requests", + "columns": { + "url": { + "name": "url", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "provider": { + "name": "provider", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'github'" + }, + "repository_url": { + "name": "repository_url", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "base_ref_name": { + "name": "base_ref_name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "base_ref_oid": { + "name": "base_ref_oid", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "head_repository_url": { + "name": "head_repository_url", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "head_ref_name": { + "name": "head_ref_name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "head_ref_oid": { + "name": "head_ref_oid", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "identifier": { + "name": "identifier", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'open'" + }, + "is_draft": { + "name": "is_draft", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "author_user_id": { + "name": "author_user_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "additions": { + "name": "additions", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "deletions": { + "name": "deletions", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "changed_files": { + "name": "changed_files", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "commit_count": { + "name": "commit_count", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "mergeable_status": { + "name": "mergeable_status", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "merge_state_status": { + "name": "merge_state_status", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "review_decision": { + "name": "review_decision", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "pull_request_created_at": { + "name": "pull_request_created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + }, + "pull_request_updated_at": { + "name": "pull_request_updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "idx_pull_requests_url": { + "name": "idx_pull_requests_url", + "columns": ["url"], + "isUnique": true + }, + "idx_pull_requests_repository_url": { + "name": "idx_pull_requests_repository_url", + "columns": ["repository_url"], + "isUnique": false + }, + "idx_pull_requests_head_repository_url": { + "name": "idx_pull_requests_head_repository_url", + "columns": ["head_repository_url"], + "isUnique": false + } + }, + "foreignKeys": { + "pull_requests_author_user_id_pull_request_users_user_id_fk": { + "name": "pull_requests_author_user_id_pull_request_users_user_id_fk", + "tableFrom": "pull_requests", + "tableTo": "pull_request_users", + "columnsFrom": ["author_user_id"], + "columnsTo": ["user_id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + }, + "ssh_connections": { + "name": "ssh_connections", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "host": { + "name": "host", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "port": { + "name": "port", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 22 + }, + "username": { + "name": "username", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "auth_type": { + "name": "auth_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'agent'" + }, + "private_key_path": { + "name": "private_key_path", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "use_agent": { + "name": "use_agent", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "metadata": { + "name": "metadata", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "idx_ssh_connections_name": { + "name": "idx_ssh_connections_name", + "columns": ["name"], + "isUnique": true + }, + "idx_ssh_connections_host": { + "name": "idx_ssh_connections_host", + "columns": ["host"], + "isUnique": false + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + }, + "tasks": { + "name": "tasks", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "project_id": { + "name": "project_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "source_branch": { + "name": "source_branch", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "task_branch": { + "name": "task_branch", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "linked_issue": { + "name": "linked_issue", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "archived_at": { + "name": "archived_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + }, + "last_interacted_at": { + "name": "last_interacted_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "status_changed_at": { + "name": "status_changed_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + }, + "is_pinned": { + "name": "is_pinned", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "workspace_provider": { + "name": "workspace_provider", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "workspace_provider_data": { + "name": "workspace_provider_data", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": { + "idx_tasks_project_id": { + "name": "idx_tasks_project_id", + "columns": ["project_id"], + "isUnique": false + } + }, + "foreignKeys": { + "tasks_project_id_projects_id_fk": { + "name": "tasks_project_id_projects_id_fk", + "tableFrom": "tasks", + "tableTo": "projects", + "columnsFrom": ["project_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + }, + "terminals": { + "name": "terminals", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "project_id": { + "name": "project_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "task_id": { + "name": "task_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "ssh": { + "name": "ssh", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "idx_terminals_task_id": { + "name": "idx_terminals_task_id", + "columns": ["task_id"], + "isUnique": false + } + }, + "foreignKeys": { + "terminals_project_id_projects_id_fk": { + "name": "terminals_project_id_projects_id_fk", + "tableFrom": "terminals", + "tableTo": "projects", + "columnsFrom": ["project_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "terminals_task_id_tasks_id_fk": { + "name": "terminals_task_id_tasks_id_fk", + "tableFrom": "terminals", + "tableTo": "tasks", + "columnsFrom": ["task_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + } + }, + "enums": {}, + "_meta": { + "schemas": {}, + "tables": {}, + "columns": {} + }, + "internal": { + "indexes": {} + } +} diff --git a/drizzle/meta/_journal.json b/drizzle/meta/_journal.json index 236853131..fcfcbb433 100644 --- a/drizzle/meta/_journal.json +++ b/drizzle/meta/_journal.json @@ -57,6 +57,13 @@ "when": 1777399834705, "tag": "0007_bent_spitfire", "breakpoints": true + }, + { + "idx": 8, + "version": "6", + "when": 1777551942949, + "tag": "0008_harsh_overlord", + "breakpoints": true } ] } diff --git a/src/main/core/prompt-templates/controller.ts b/src/main/core/prompt-templates/controller.ts new file mode 100644 index 000000000..40ad6763a --- /dev/null +++ b/src/main/core/prompt-templates/controller.ts @@ -0,0 +1,26 @@ +import { createRPCController } from '@shared/ipc/rpc'; +import type { + CreatePromptTemplateInput, + PromptTemplate, + UpdatePromptTemplateInput, +} from '@shared/prompt-templates'; +import { db } from '@main/db/client'; +import { PromptTemplateService } from './service'; + +const promptTemplateService = new PromptTemplateService(db); + +export const promptTemplatesController = createRPCController({ + list: (): Promise => promptTemplateService.list(), + + getById: (id: string): Promise => promptTemplateService.getById(id), + + create: (input: CreatePromptTemplateInput): Promise => + promptTemplateService.create(input), + + update: (id: string, input: UpdatePromptTemplateInput): Promise => + promptTemplateService.update(id, input), + + delete: (id: string): Promise => promptTemplateService.delete(id), + + reorder: (ids: string[]): Promise => promptTemplateService.reorder(ids), +}); diff --git a/src/main/core/prompt-templates/service.test.ts b/src/main/core/prompt-templates/service.test.ts new file mode 100644 index 000000000..a8238bfd9 --- /dev/null +++ b/src/main/core/prompt-templates/service.test.ts @@ -0,0 +1,123 @@ +import Database from 'better-sqlite3'; +import { drizzle } from 'drizzle-orm/better-sqlite3'; +import { beforeEach, describe, expect, it } from 'vitest'; +import * as schema from '@main/db/schema'; +import { PromptTemplateService } from './service'; + +function createTestDb() { + const sqlite = new Database(':memory:'); + const db = drizzle(sqlite, { schema }); + // Create the prompt_templates table manually since we're not running migrations + sqlite.exec(` + CREATE TABLE prompt_templates ( + id TEXT PRIMARY KEY NOT NULL, + name TEXT NOT NULL, + text TEXT NOT NULL, + sort_order INTEGER DEFAULT 0 NOT NULL, + created_at TEXT DEFAULT CURRENT_TIMESTAMP NOT NULL, + updated_at TEXT DEFAULT CURRENT_TIMESTAMP NOT NULL + ); + CREATE INDEX idx_prompt_templates_sort_order ON prompt_templates (sort_order); + `); + return db; +} + +describe('PromptTemplateService', () => { + let service: PromptTemplateService; + + beforeEach(() => { + const db = createTestDb(); + service = new PromptTemplateService(db); + }); + + it('lists templates ordered by sortOrder', async () => { + const t1 = await service.create({ name: 'A', text: 'text A' }); + const t2 = await service.create({ name: 'B', text: 'text B' }); + await service.reorder([t2.id, t1.id]); + + const list = await service.list(); + expect(list.map((t) => t.name)).toEqual(['B', 'A']); + }); + + it('creates a template with incremental sortOrder', async () => { + const t1 = await service.create({ name: 'First', text: 'hello' }); + const t2 = await service.create({ name: 'Second', text: 'world' }); + expect(t1.sortOrder).toBe(0); + expect(t2.sortOrder).toBe(1); + }); + + it('rejects empty name', async () => { + await expect(service.create({ name: ' ', text: 'valid' })).rejects.toThrow( + 'Template name is required' + ); + }); + + it('rejects empty text', async () => { + await expect(service.create({ name: 'valid', text: ' ' })).rejects.toThrow( + 'Template text is required' + ); + }); + + it('rejects name over 64 chars', async () => { + await expect(service.create({ name: 'x'.repeat(65), text: 'valid' })).rejects.toThrow( + 'Template name must be 64 characters or fewer' + ); + }); + + it('rejects text over 4000 chars', async () => { + await expect(service.create({ name: 'valid', text: 'x'.repeat(4001) })).rejects.toThrow( + 'Template text must be 4000 characters or fewer' + ); + }); + + it('updates a template', async () => { + const t = await service.create({ name: 'Old', text: 'old text' }); + const updated = await service.update(t.id, { name: 'New', text: 'new text' }); + expect(updated.name).toBe('New'); + expect(updated.text).toBe('new text'); + }); + + it('throws when updating non-existent template', async () => { + await expect(service.update('nonexistent', { name: 'X' })).rejects.toThrow( + 'Prompt template nonexistent not found' + ); + }); + + it('deletes a template', async () => { + const t = await service.create({ name: 'ToDelete', text: 'bye' }); + await service.delete(t.id); + const found = await service.getById(t.id); + expect(found).toBeNull(); + }); + + it('reorders templates', async () => { + const t1 = await service.create({ name: '1', text: 'a' }); + const t2 = await service.create({ name: '2', text: 'b' }); + const t3 = await service.create({ name: '3', text: 'c' }); + await service.reorder([t3.id, t1.id, t2.id]); + + const list = await service.list(); + expect(list.map((t) => t.name)).toEqual(['3', '1', '2']); + expect(list[0]!.sortOrder).toBe(0); + expect(list[1]!.sortOrder).toBe(1); + expect(list[2]!.sortOrder).toBe(2); + }); + + it('rejects partial reorder lists', async () => { + const t1 = await service.create({ name: '1', text: 'a' }); + await service.create({ name: '2', text: 'b' }); + + await expect(service.reorder([t1.id])).rejects.toThrow( + 'Reorder request must include all prompt templates' + ); + }); + + it('rejects duplicate ids in reorder list', async () => { + const t1 = await service.create({ name: '1', text: 'a' }); + await service.create({ name: '2', text: 'b' }); + + await expect(service.reorder([t1.id, t1.id])).rejects.toThrow( + 'Reorder request contains duplicate template IDs' + ); + }); +}); diff --git a/src/main/core/prompt-templates/service.ts b/src/main/core/prompt-templates/service.ts new file mode 100644 index 000000000..9ddc93e01 --- /dev/null +++ b/src/main/core/prompt-templates/service.ts @@ -0,0 +1,155 @@ +import { randomUUID } from 'node:crypto'; +import { count, desc, eq, inArray } from 'drizzle-orm'; +import { + MAX_PROMPT_TEMPLATES, + type CreatePromptTemplateInput, + type PromptTemplate, + type UpdatePromptTemplateInput, +} from '@shared/prompt-templates'; +import type { AppDb } from '@main/db/client'; +import { promptTemplates } from '@main/db/schema'; + +const MAX_NAME_LENGTH = 64; +const MAX_TEXT_LENGTH = 4000; + +function validateName(name: string): void { + const trimmed = name.trim(); + if (!trimmed) throw new Error('Template name is required'); + if (trimmed.length > MAX_NAME_LENGTH) + throw new Error(`Template name must be ${MAX_NAME_LENGTH} characters or fewer`); +} + +function validateText(text: string): void { + const trimmed = text.trim(); + if (!trimmed) throw new Error('Template text is required'); + if (trimmed.length > MAX_TEXT_LENGTH) + throw new Error(`Template text must be ${MAX_TEXT_LENGTH} characters or fewer`); +} + +function toModel(row: typeof promptTemplates.$inferSelect): PromptTemplate { + return { + id: row.id, + name: row.name, + text: row.text, + sortOrder: row.sortOrder, + createdAt: row.createdAt, + updatedAt: row.updatedAt, + }; +} + +export class PromptTemplateService { + private db: AppDb; + + constructor(database: AppDb) { + this.db = database; + } + + async list(): Promise { + const rows = await this.db + .select() + .from(promptTemplates) + .orderBy(promptTemplates.sortOrder, promptTemplates.createdAt); + return rows.map(toModel); + } + + async getById(id: string): Promise { + const [row] = await this.db + .select() + .from(promptTemplates) + .where(eq(promptTemplates.id, id)) + .limit(1); + return row ? toModel(row) : null; + } + + async create(input: CreatePromptTemplateInput): Promise { + validateName(input.name); + validateText(input.text); + + const [countRow] = await this.db.select({ value: count() }).from(promptTemplates); + if (countRow.value >= MAX_PROMPT_TEMPLATES) { + throw new Error(`You can create up to ${MAX_PROMPT_TEMPLATES} templates`); + } + + const [maxRow] = await this.db + .select({ sortOrder: promptTemplates.sortOrder }) + .from(promptTemplates) + .orderBy(desc(promptTemplates.sortOrder)) + .limit(1); + const nextSortOrder = (maxRow?.sortOrder ?? -1) + 1; + + const id = randomUUID(); + const now = new Date().toISOString(); + const insertData = { + id, + name: input.name.trim(), + text: input.text.trim(), + sortOrder: nextSortOrder, + createdAt: now, + updatedAt: now, + }; + + await this.db.insert(promptTemplates).values(insertData); + return toModel(insertData); + } + + async update(id: string, input: UpdatePromptTemplateInput): Promise { + const existing = await this.getById(id); + if (!existing) throw new Error(`Prompt template ${id} not found`); + + if (input.name !== undefined) validateName(input.name); + if (input.text !== undefined) validateText(input.text); + + const updates: Partial = {}; + if (input.name !== undefined) updates.name = input.name.trim(); + if (input.text !== undefined) updates.text = input.text.trim(); + if (input.sortOrder !== undefined) updates.sortOrder = input.sortOrder; + updates.updatedAt = new Date().toISOString(); + + await this.db.update(promptTemplates).set(updates).where(eq(promptTemplates.id, id)); + + return { + ...existing, + name: updates.name ?? existing.name, + text: updates.text ?? existing.text, + sortOrder: updates.sortOrder ?? existing.sortOrder, + updatedAt: updates.updatedAt ?? existing.updatedAt, + }; + } + + async delete(id: string): Promise { + await this.db.delete(promptTemplates).where(eq(promptTemplates.id, id)); + } + + async reorder(ids: string[]): Promise { + const [countRow] = await this.db.select({ value: count() }).from(promptTemplates); + const totalTemplates = countRow.value; + if (totalTemplates === 0 && ids.length === 0) return; + + if (ids.length !== totalTemplates) { + throw new Error('Reorder request must include all prompt templates'); + } + + if (new Set(ids).size !== ids.length) { + throw new Error('Reorder request contains duplicate template IDs'); + } + + const existingRows = await this.db + .select({ id: promptTemplates.id }) + .from(promptTemplates) + .where(inArray(promptTemplates.id, ids)); + + if (existingRows.length !== ids.length) { + throw new Error('One or more prompt templates were not found'); + } + + const now = new Date().toISOString(); + this.db.transaction((tx) => { + for (let i = 0; i < ids.length; i++) { + tx.update(promptTemplates) + .set({ sortOrder: i, updatedAt: now }) + .where(eq(promptTemplates.id, ids[i])) + .run(); + } + }); + } +} diff --git a/src/main/db/schema.ts b/src/main/db/schema.ts index 2de8e420b..af8d19e6c 100644 --- a/src/main/db/schema.ts +++ b/src/main/db/schema.ts @@ -86,6 +86,25 @@ export const appSettings = sqliteTable( }) ); +export const promptTemplates = sqliteTable( + 'prompt_templates', + { + id: text('id').primaryKey(), + name: text('name').notNull(), + text: text('text').notNull(), + sortOrder: integer('sort_order').notNull().default(0), + createdAt: text('created_at') + .notNull() + .default(sql`CURRENT_TIMESTAMP`), + updatedAt: text('updated_at') + .notNull() + .default(sql`CURRENT_TIMESTAMP`), + }, + (table) => ({ + sortOrderIdx: index('idx_prompt_templates_sort_order').on(table.sortOrder), + }) +); + export const tasks = sqliteTable( 'tasks', { @@ -391,6 +410,8 @@ export const messagesRelations = relations(messages, ({ one }) => ({ export type SshConnectionRow = typeof sshConnections.$inferSelect; export type SshConnectionInsert = typeof sshConnections.$inferInsert; +export type PromptTemplateRow = typeof promptTemplates.$inferSelect; +export type PromptTemplateInsert = typeof promptTemplates.$inferInsert; export type ProjectRow = typeof projects.$inferSelect; export type TaskRow = typeof tasks.$inferSelect; export type ConversationRow = typeof conversations.$inferSelect; diff --git a/src/main/index.ts b/src/main/index.ts index 93410235e..603f3fc90 100644 --- a/src/main/index.ts +++ b/src/main/index.ts @@ -3,6 +3,7 @@ import { app, BrowserWindow, dialog, ipcMain } from 'electron'; import dockIcon from '@/assets/images/emdash/icon-dock.png?asset'; import { PRODUCT_NAME } from '@shared/app-identity'; import { registerRPCRouter } from '@shared/ipc/rpc'; +import { STARTER_PROMPT_TEMPLATES } from '@shared/prompt-templates'; import { setupApplicationMenu } from './app/menu'; import { registerAppScheme, setupAppProtocol } from './app/protocol'; import { createMainWindow } from './app/window'; @@ -15,9 +16,11 @@ import { editorBufferService } from './core/editor/editor-buffer-service'; import { gitWatcherRegistry } from './core/git/git-watcher-registry'; import { githubConnectionService } from './core/github/services/github-connection-service'; import { projectManager } from './core/projects/project-manager'; +import { PromptTemplateService } from './core/prompt-templates/service'; import { prSyncScheduler } from './core/pull-requests/pr-sync-scheduler'; import { appSettingsService } from './core/settings/settings-service'; import { updateService } from './core/updates/update-service'; +import { db } from './db/client'; import { initializeDatabase } from './db/initialize'; import { log } from './lib/logger'; import * as telemetry from './lib/telemetry'; @@ -28,6 +31,32 @@ if (process.platform === 'linux') { app.commandLine.appendSwitch('ozone-platform-hint', 'auto'); } +async function migrateReviewPromptToTemplates(): Promise { + const service = new PromptTemplateService(db); + const existing = await service.list(); + if (existing.length > 0) return; + + // Migrate old reviewPrompt if it exists and is non-empty + let migrated = false; + try { + const reviewPrompt = await appSettingsService.get('reviewPrompt'); + const text = (reviewPrompt ?? '').trim(); + if (text) { + await service.create({ name: 'Review prompt', text }); + migrated = true; + } + } catch { + // If reviewPrompt setting doesn't exist or fails, skip silently + } + + // If nothing was migrated, seed the default starter templates + if (!migrated) { + for (const template of STARTER_PROMPT_TEMPLATES) { + await service.create(template); + } + } +} + registerAppScheme(); app.setName(PRODUCT_NAME); @@ -90,6 +119,7 @@ app.whenReady().then(async () => { prSyncScheduler.initialize(); appService.initialize(); await appSettingsService.initialize(); + await migrateReviewPromptToTemplates(); agentHookService.initialize().catch((e) => { log.error('Failed to start agent event service:', e); diff --git a/src/main/rpc.ts b/src/main/rpc.ts index 620286047..a85ced77a 100644 --- a/src/main/rpc.ts +++ b/src/main/rpc.ts @@ -15,6 +15,7 @@ import { linearController } from './core/linear/controller'; import { mcpController } from './core/mcp/controller'; import { plainController } from './core/plain/controller'; import { projectController } from './core/projects/controller'; +import { promptTemplatesController } from './core/prompt-templates/controller'; import { ptyController } from './core/pty/controller'; import { pullRequestController } from './core/pull-requests/controller'; import { repositoryController } from './core/repository/controller'; @@ -49,6 +50,7 @@ export const rpcRouter = createRPCRouter({ skills: skillsController, ssh: sshController, projects: projectController, + promptTemplates: promptTemplatesController, tasks: taskController, conversations: conversationController, terminals: terminalsController, diff --git a/src/renderer/features/settings/components/PromptTemplatesSettingsCard.tsx b/src/renderer/features/settings/components/PromptTemplatesSettingsCard.tsx new file mode 100644 index 000000000..d43bed03e --- /dev/null +++ b/src/renderer/features/settings/components/PromptTemplatesSettingsCard.tsx @@ -0,0 +1,352 @@ +import { ArrowDown, ArrowUp, Copy, FileSearch, Pencil, Plus, Trash2, X } from 'lucide-react'; +import { useState } from 'react'; +import { + MAX_PROMPT_TEMPLATES, + STARTER_PROMPT_TEMPLATES, + type PromptTemplate, +} from '@shared/prompt-templates'; +import { usePromptTemplates } from '@renderer/features/settings/use-prompt-templates'; +import { useToast } from '@renderer/lib/hooks/use-toast'; +import { Button } from '@renderer/lib/ui/button'; +import { Input } from '@renderer/lib/ui/input'; +import { Textarea } from '@renderer/lib/ui/textarea'; + +const MAX_NAME_LENGTH = 64; +const MAX_TEXT_LENGTH = 4000; + +function PromptTemplateEditor({ + template, + onSave, + onCancel, + isSaving, +}: { + template?: PromptTemplate; + onSave: (name: string, text: string) => void; + onCancel: () => void; + isSaving: boolean; +}) { + const [name, setName] = useState(template?.name ?? ''); + const [text, setText] = useState(template?.text ?? ''); + const canSave = name.trim().length > 0 && text.trim().length > 0 && !isSaving; + + return ( +
+ setName(e.target.value)} + disabled={isSaving} + className="text-sm" + maxLength={MAX_NAME_LENGTH} + /> +
+