From e89076148a577e1b279d0191c7699011488433ce Mon Sep 17 00:00:00 2001 From: Dotta <34892728+cryppadotta@users.noreply.github.com> Date: Tue, 14 Apr 2026 12:57:11 -0500 Subject: [PATCH] [codex] Improve workspace runtime and navigation ergonomics (#3680) ## Thinking Path > - Paperclip orchestrates AI agents for zero-human companies > - That operator experience depends not just on issue chat, but also on how workspaces, inbox groups, and navigation state behave over long-running sessions > - The current branch included a separate cluster of workspace-runtime controls, inbox grouping, sidebar ordering, and worktree lifecycle fixes > - Those changes cross server, shared contracts, database state, and UI navigation, but they still form one coherent operator workflow area > - This pull request isolates the workspace/runtime and navigation ergonomics work into one standalone branch > - The benefit is better workspace recovery and navigation persistence without forcing reviewers through the unrelated issue-detail/chat work ## What Changed - Improved execution workspace and project workspace controls, request wiring, layout, and JSON editor ergonomics - Hardened linked worktree reuse/startup behavior and documented the `worktree repair` flow for recovering linked worktrees safely - Added inbox workspace grouping, mobile collapse, archive undo, keyboard navigation, shared group-header styling, and persisted collapsed-group behavior - Added persistent sidebar order preferences with the supporting DB migration, shared/server contracts, routes, services, hooks, and UI integration - Scoped issue-list preferences by context and added targeted UI/server tests for workspace controls, inbox behavior, sidebar preferences, and worktree validation ## Verification - `pnpm vitest run server/src/__tests__/sidebar-preferences-routes.test.ts ui/src/pages/Inbox.test.tsx ui/src/components/ProjectWorkspaceSummaryCard.test.tsx ui/src/components/WorkspaceRuntimeControls.test.tsx ui/src/api/workspace-runtime-control.test.ts` - `server/src/__tests__/workspace-runtime.test.ts` was attempted, but the embedded Postgres suite self-skipped/hung on this host after reporting an init-script issue, so it is not counted as a local pass here ## Risks - Medium: this branch includes migration-backed preference storage plus worktree/runtime behavior, so merge review should pay attention to state persistence and worktree recovery semantics - The sidebar preference migration is standalone, but it should still be watched for conflicts if another migration lands first ## Model Used - OpenAI Codex coding agent (GPT-5-class runtime in Codex CLI; exact deployed model ID is not exposed in this environment), reasoning enabled, tool use and local code execution enabled ## Checklist - [x] I have included a thinking path that traces from project context to this change - [x] I have specified the model used (with version and capability details) - [ ] I have run tests locally and they pass - [x] I have added or updated tests where applicable - [ ] If this change affects the UI, I have included before/after screenshots - [x] I have updated relevant documentation to reflect my changes - [x] I have considered and documented any risks above - [x] I will address all Greptile and reviewer comments before requesting merge --------- Co-authored-by: Paperclip --- doc/DEVELOPING.md | 29 +- ...ecution-workspaces-and-runtime-services.md | 28 +- .../src/migrations/0056_spooky_ultragirl.sql | 22 + .../db/src/migrations/meta/0056_snapshot.json | 13388 ++++++++++++++++ packages/db/src/migrations/meta/_journal.json | 7 + .../company_user_sidebar_preferences.ts | 22 + packages/db/src/schema/index.ts | 2 + .../db/src/schema/user_sidebar_preferences.ts | 15 + packages/shared/src/api.ts | 1 + packages/shared/src/index.ts | 20 + packages/shared/src/types/index.ts | 5 + .../shared/src/types/sidebar-preferences.ts | 4 + .../shared/src/types/workspace-runtime.ts | 30 + .../src/validators/execution-workspace.ts | 8 + packages/shared/src/validators/index.ts | 5 + packages/shared/src/validators/project.ts | 1 + .../src/validators/sidebar-preferences.ts | 14 + .../shared/src/workspace-commands.test.ts | 56 + packages/shared/src/workspace-commands.ts | 204 + scripts/provision-worktree.sh | 69 +- .../execution-workspaces-service.test.ts | 2 + .../sidebar-preferences-routes.test.ts | 166 + .../src/__tests__/workspace-runtime.test.ts | 662 +- server/src/app.ts | 2 + server/src/routes/execution-workspaces.ts | 230 +- server/src/routes/index.ts | 1 + server/src/routes/projects.ts | 156 +- server/src/routes/sidebar-preferences.ts | 71 + server/src/services/execution-workspaces.ts | 15 + server/src/services/index.ts | 1 + .../project-workspace-runtime-config.ts | 15 +- server/src/services/sidebar-preferences.ts | 97 + server/src/services/workspace-runtime.ts | 493 +- ui/src/App.tsx | 2 + ui/src/api/execution-workspaces.ts | 25 +- ui/src/api/index.ts | 1 + ui/src/api/projects.ts | 22 +- ui/src/api/sidebarPreferences.ts | 12 + ui/src/api/workspace-runtime-control.test.ts | 28 + ui/src/api/workspace-runtime-control.ts | 11 + ui/src/components/CompanyRail.tsx | 107 +- .../components/IssueFiltersPopover.test.tsx | 83 + ui/src/components/IssueFiltersPopover.tsx | 75 +- ui/src/components/IssueGroupHeader.tsx | 48 + ui/src/components/IssuesList.test.tsx | 39 +- ui/src/components/IssuesList.tsx | 118 +- .../ProjectWorkspaceSummaryCard.test.tsx | 192 + .../ProjectWorkspaceSummaryCard.tsx | 230 + ui/src/components/SidebarProjects.tsx | 6 +- .../WorkspaceRuntimeControls.test.tsx | 269 + .../components/WorkspaceRuntimeControls.tsx | 439 + ui/src/hooks/useCompanyOrder.ts | 100 + ui/src/hooks/useProjectOrder.ts | 84 +- ui/src/lib/inbox.test.ts | 167 + ui/src/lib/inbox.ts | 279 +- ui/src/lib/keyboardShortcuts.test.ts | 31 + ui/src/lib/keyboardShortcuts.ts | 28 + ui/src/lib/queryKeys.ts | 5 + ui/src/pages/CompanyImport.tsx | 8 +- ui/src/pages/ExecutionWorkspaceDetail.tsx | 813 +- ui/src/pages/Inbox.test.tsx | 51 +- ui/src/pages/Inbox.tsx | 199 +- ui/src/pages/ProjectDetail.tsx | 177 +- ui/src/pages/ProjectWorkspaceDetail.tsx | 149 +- 64 files changed, 18576 insertions(+), 1063 deletions(-) create mode 100644 packages/db/src/migrations/0056_spooky_ultragirl.sql create mode 100644 packages/db/src/migrations/meta/0056_snapshot.json create mode 100644 packages/db/src/schema/company_user_sidebar_preferences.ts create mode 100644 packages/db/src/schema/user_sidebar_preferences.ts create mode 100644 packages/shared/src/types/sidebar-preferences.ts create mode 100644 packages/shared/src/validators/sidebar-preferences.ts create mode 100644 packages/shared/src/workspace-commands.test.ts create mode 100644 packages/shared/src/workspace-commands.ts create mode 100644 server/src/__tests__/sidebar-preferences-routes.test.ts create mode 100644 server/src/routes/sidebar-preferences.ts create mode 100644 server/src/services/sidebar-preferences.ts create mode 100644 ui/src/api/sidebarPreferences.ts create mode 100644 ui/src/api/workspace-runtime-control.test.ts create mode 100644 ui/src/api/workspace-runtime-control.ts create mode 100644 ui/src/components/IssueFiltersPopover.test.tsx create mode 100644 ui/src/components/IssueGroupHeader.tsx create mode 100644 ui/src/components/ProjectWorkspaceSummaryCard.test.tsx create mode 100644 ui/src/components/ProjectWorkspaceSummaryCard.tsx create mode 100644 ui/src/components/WorkspaceRuntimeControls.test.tsx create mode 100644 ui/src/components/WorkspaceRuntimeControls.tsx create mode 100644 ui/src/hooks/useCompanyOrder.ts diff --git a/doc/DEVELOPING.md b/doc/DEVELOPING.md index f86ab47b..7fc68c0e 100644 --- a/doc/DEVELOPING.md +++ b/doc/DEVELOPING.md @@ -239,7 +239,7 @@ paperclipai worktree init --force Repair an already-created repo-managed worktree and reseed its isolated instance from the main default install: ```sh -cd ~/.paperclip/worktrees/PAP-884-ai-commits-component +cd /path/to/paperclip/.paperclip/worktrees/PAP-884-ai-commits-component pnpm paperclipai worktree init --force --seed-mode minimal \ --name PAP-884-ai-commits-component \ --from-config ~/.paperclip/instances/default/config.json @@ -247,6 +247,33 @@ pnpm paperclipai worktree init --force --seed-mode minimal \ That rewrites the worktree-local `.paperclip/config.json` + `.paperclip/.env`, recreates the isolated instance under `~/.paperclip-worktrees/instances//`, and preserves the git worktree contents themselves. +For an already-created worktree where you want the CLI to decide whether to rebuild missing worktree metadata or just reseed the isolated DB, use `worktree repair`. + +**`pnpm paperclipai worktree repair [options]`** — Repair the current linked worktree by default, or create/repair a named linked worktree under `.paperclip/worktrees/` when `--branch` is provided. The command never targets the primary checkout unless you explicitly pass `--branch`. + +| Option | Description | +|---|---| +| `--branch ` | Existing branch/worktree selector to repair, or a branch name to create under `.paperclip/worktrees` | +| `--home ` | Home root for worktree instances (default: `~/.paperclip-worktrees`) | +| `--from-config ` | Source config.json to seed from | +| `--from-data-dir ` | Source `PAPERCLIP_HOME` used when deriving the source config | +| `--from-instance ` | Source instance id when deriving the source config (default: `default`) | +| `--seed-mode ` | Seed profile: `minimal` or `full` (default: `minimal`) | +| `--no-seed` | Repair metadata only when bootstrapping a missing worktree config | +| `--allow-live-target` | Override the guard that requires the target worktree DB to be stopped first | + +Examples: + +```sh +# From inside a linked worktree, rebuild missing .paperclip metadata and reseed it from the default instance. +cd /path/to/paperclip/.paperclip/worktrees/PAP-1132-assistant-ui-pap-1131-make-issues-comments-be-like-a-chat +pnpm paperclipai worktree repair + +# From the primary checkout, create or repair a linked worktree for a branch under .paperclip/worktrees/. +cd /path/to/paperclip +pnpm paperclipai worktree repair --branch PAP-1132-assistant-ui-pap-1131-make-issues-comments-be-like-a-chat +``` + For an already-created worktree where you want to keep the existing repo-local config/env and only overwrite the isolated database, use `worktree reseed` instead. Stop the target worktree's Paperclip server first so the command can replace the DB safely. **`pnpm paperclipai worktree reseed [options]`** — Re-seed an existing worktree-local instance from another Paperclip instance or worktree while preserving the target worktree's current config, ports, and instance identity. diff --git a/docs/guides/board-operator/execution-workspaces-and-runtime-services.md b/docs/guides/board-operator/execution-workspaces-and-runtime-services.md index 285d701a..cd8a31c8 100644 --- a/docs/guides/board-operator/execution-workspaces-and-runtime-services.md +++ b/docs/guides/board-operator/execution-workspaces-and-runtime-services.md @@ -5,22 +5,28 @@ summary: How project runtime configuration, execution workspaces, and issue runs This guide documents the intended runtime model for projects, execution workspaces, and issue runs in Paperclip. +Paperclip now presents this as a workspace-command model: + +- `Services` are long-running commands that stay supervised. +- `Jobs` are one-shot commands that run once and exit. +- Raw runtime JSON is still available for advanced config, but it is no longer the primary mental model. + ## Project runtime configuration You can define how to run a project on the project workspace itself. -- Project workspace runtime config describes how to run services for that project checkout. +- Project workspace runtime config describes the services and jobs available for that project checkout. - This is the default runtime configuration that child execution workspaces may inherit. - Defining the config does not start anything by itself. ## Manual runtime control -Runtime services are manually controlled from the UI. +Workspace commands are manually controlled from the UI. -- Project workspace runtime services are started and stopped from the project workspace UI. -- Execution workspace runtime services are started and stopped from the execution workspace UI. -- Paperclip does not automatically start or stop these runtime services as part of issue execution. -- Paperclip also does not automatically restart workspace runtime services on server boot. +- Project workspace services are started and stopped from the project workspace UI, and project jobs can be run on demand there. +- Execution workspace services are started and stopped from the execution workspace UI, and execution-workspace jobs can be run on demand there. +- Paperclip does not automatically start or stop these workspace services as part of issue execution. +- Paperclip also does not automatically restart workspace services on server boot. ## Execution workspace inheritance @@ -29,7 +35,7 @@ Execution workspaces isolate code and runtime state from the project primary wor - An isolated execution workspace has its own checkout path, branch, and local runtime instance. - The runtime configuration may inherit from the linked project workspace by default. - The execution workspace may override that runtime configuration with its own workspace-specific settings. -- The inherited configuration answers "how to run the service", but the running process is still specific to that execution workspace. +- The inherited configuration answers "which commands exist and how to run them", but any running service process is still specific to that execution workspace. ## Issues and execution workspaces @@ -38,7 +44,7 @@ Issues are attached to execution workspace behavior, not to automatic runtime ma - An issue may create a new execution workspace when you choose an isolated workspace mode. - An issue may reuse an existing execution workspace when you choose reuse. - Multiple issues may intentionally share one execution workspace so they can work against the same branch and running runtime services. -- Assigning or running an issue does not automatically start or stop runtime services for that workspace. +- Assigning or running an issue does not automatically start or stop workspace services for that workspace. ## Execution workspace lifecycle @@ -62,7 +68,7 @@ Heartbeat still resolves a workspace for the run, but that is about code locatio With the current implementation: -- Project workspace runtime config is the fallback for execution workspace UI controls. +- Project workspace command config is the fallback for execution workspace UI controls. - Execution workspace runtime overrides are stored on the execution workspace. -- Heartbeat runs do not auto-start workspace runtime services. -- Server startup does not auto-restart workspace runtime services. +- Heartbeat runs do not auto-start workspace services. +- Server startup does not auto-restart workspace services. diff --git a/packages/db/src/migrations/0056_spooky_ultragirl.sql b/packages/db/src/migrations/0056_spooky_ultragirl.sql new file mode 100644 index 00000000..e8492176 --- /dev/null +++ b/packages/db/src/migrations/0056_spooky_ultragirl.sql @@ -0,0 +1,22 @@ +CREATE TABLE "company_user_sidebar_preferences" ( + "id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL, + "company_id" uuid NOT NULL, + "user_id" text NOT NULL, + "project_order" jsonb DEFAULT '[]'::jsonb NOT NULL, + "created_at" timestamp with time zone DEFAULT now() NOT NULL, + "updated_at" timestamp with time zone DEFAULT now() NOT NULL +); +--> statement-breakpoint +CREATE TABLE "user_sidebar_preferences" ( + "id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL, + "user_id" text NOT NULL, + "company_order" jsonb DEFAULT '[]'::jsonb NOT NULL, + "created_at" timestamp with time zone DEFAULT now() NOT NULL, + "updated_at" timestamp with time zone DEFAULT now() NOT NULL +); +--> statement-breakpoint +ALTER TABLE "company_user_sidebar_preferences" ADD CONSTRAINT "company_user_sidebar_preferences_company_id_companies_id_fk" FOREIGN KEY ("company_id") REFERENCES "public"."companies"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint +CREATE INDEX "company_user_sidebar_preferences_company_idx" ON "company_user_sidebar_preferences" USING btree ("company_id");--> statement-breakpoint +CREATE INDEX "company_user_sidebar_preferences_user_idx" ON "company_user_sidebar_preferences" USING btree ("user_id");--> statement-breakpoint +CREATE UNIQUE INDEX "company_user_sidebar_preferences_company_user_uq" ON "company_user_sidebar_preferences" USING btree ("company_id","user_id");--> statement-breakpoint +CREATE UNIQUE INDEX "user_sidebar_preferences_user_uq" ON "user_sidebar_preferences" USING btree ("user_id"); \ No newline at end of file diff --git a/packages/db/src/migrations/meta/0056_snapshot.json b/packages/db/src/migrations/meta/0056_snapshot.json new file mode 100644 index 00000000..6db5f80b --- /dev/null +++ b/packages/db/src/migrations/meta/0056_snapshot.json @@ -0,0 +1,13388 @@ +{ + "id": "5b9211ec-73c0-4825-bdf7-08ba60b6915c", + "prevId": "dfe6d9fd-7969-4f13-a701-1fcc62bf0015", + "version": "7", + "dialect": "postgresql", + "tables": { + "public.activity_log": { + "name": "activity_log", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "company_id": { + "name": "company_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "actor_type": { + "name": "actor_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'system'" + }, + "actor_id": { + "name": "actor_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "action": { + "name": "action", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "entity_type": { + "name": "entity_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "entity_id": { + "name": "entity_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "agent_id": { + "name": "agent_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "run_id": { + "name": "run_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "details": { + "name": "details", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "activity_log_company_created_idx": { + "name": "activity_log_company_created_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "activity_log_run_id_idx": { + "name": "activity_log_run_id_idx", + "columns": [ + { + "expression": "run_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "activity_log_entity_type_id_idx": { + "name": "activity_log_entity_type_id_idx", + "columns": [ + { + "expression": "entity_type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "entity_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "activity_log_company_id_companies_id_fk": { + "name": "activity_log_company_id_companies_id_fk", + "tableFrom": "activity_log", + "tableTo": "companies", + "columnsFrom": [ + "company_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "activity_log_agent_id_agents_id_fk": { + "name": "activity_log_agent_id_agents_id_fk", + "tableFrom": "activity_log", + "tableTo": "agents", + "columnsFrom": [ + "agent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "activity_log_run_id_heartbeat_runs_id_fk": { + "name": "activity_log_run_id_heartbeat_runs_id_fk", + "tableFrom": "activity_log", + "tableTo": "heartbeat_runs", + "columnsFrom": [ + "run_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.agent_api_keys": { + "name": "agent_api_keys", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "agent_id": { + "name": "agent_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "company_id": { + "name": "company_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "key_hash": { + "name": "key_hash", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "last_used_at": { + "name": "last_used_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "revoked_at": { + "name": "revoked_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "agent_api_keys_key_hash_idx": { + "name": "agent_api_keys_key_hash_idx", + "columns": [ + { + "expression": "key_hash", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "agent_api_keys_company_agent_idx": { + "name": "agent_api_keys_company_agent_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "agent_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "agent_api_keys_agent_id_agents_id_fk": { + "name": "agent_api_keys_agent_id_agents_id_fk", + "tableFrom": "agent_api_keys", + "tableTo": "agents", + "columnsFrom": [ + "agent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "agent_api_keys_company_id_companies_id_fk": { + "name": "agent_api_keys_company_id_companies_id_fk", + "tableFrom": "agent_api_keys", + "tableTo": "companies", + "columnsFrom": [ + "company_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.agent_config_revisions": { + "name": "agent_config_revisions", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "company_id": { + "name": "company_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "agent_id": { + "name": "agent_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "created_by_agent_id": { + "name": "created_by_agent_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "created_by_user_id": { + "name": "created_by_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "source": { + "name": "source", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'patch'" + }, + "rolled_back_from_revision_id": { + "name": "rolled_back_from_revision_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "changed_keys": { + "name": "changed_keys", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + }, + "before_config": { + "name": "before_config", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "after_config": { + "name": "after_config", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "agent_config_revisions_company_agent_created_idx": { + "name": "agent_config_revisions_company_agent_created_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "agent_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "agent_config_revisions_agent_created_idx": { + "name": "agent_config_revisions_agent_created_idx", + "columns": [ + { + "expression": "agent_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "agent_config_revisions_company_id_companies_id_fk": { + "name": "agent_config_revisions_company_id_companies_id_fk", + "tableFrom": "agent_config_revisions", + "tableTo": "companies", + "columnsFrom": [ + "company_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "agent_config_revisions_agent_id_agents_id_fk": { + "name": "agent_config_revisions_agent_id_agents_id_fk", + "tableFrom": "agent_config_revisions", + "tableTo": "agents", + "columnsFrom": [ + "agent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "agent_config_revisions_created_by_agent_id_agents_id_fk": { + "name": "agent_config_revisions_created_by_agent_id_agents_id_fk", + "tableFrom": "agent_config_revisions", + "tableTo": "agents", + "columnsFrom": [ + "created_by_agent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.agent_runtime_state": { + "name": "agent_runtime_state", + "schema": "", + "columns": { + "agent_id": { + "name": "agent_id", + "type": "uuid", + "primaryKey": true, + "notNull": true + }, + "company_id": { + "name": "company_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "adapter_type": { + "name": "adapter_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "session_id": { + "name": "session_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "state_json": { + "name": "state_json", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'::jsonb" + }, + "last_run_id": { + "name": "last_run_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "last_run_status": { + "name": "last_run_status", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "total_input_tokens": { + "name": "total_input_tokens", + "type": "bigint", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "total_output_tokens": { + "name": "total_output_tokens", + "type": "bigint", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "total_cached_input_tokens": { + "name": "total_cached_input_tokens", + "type": "bigint", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "total_cost_cents": { + "name": "total_cost_cents", + "type": "bigint", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "last_error": { + "name": "last_error", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "agent_runtime_state_company_agent_idx": { + "name": "agent_runtime_state_company_agent_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "agent_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "agent_runtime_state_company_updated_idx": { + "name": "agent_runtime_state_company_updated_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "updated_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "agent_runtime_state_agent_id_agents_id_fk": { + "name": "agent_runtime_state_agent_id_agents_id_fk", + "tableFrom": "agent_runtime_state", + "tableTo": "agents", + "columnsFrom": [ + "agent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "agent_runtime_state_company_id_companies_id_fk": { + "name": "agent_runtime_state_company_id_companies_id_fk", + "tableFrom": "agent_runtime_state", + "tableTo": "companies", + "columnsFrom": [ + "company_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.agent_task_sessions": { + "name": "agent_task_sessions", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "company_id": { + "name": "company_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "agent_id": { + "name": "agent_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "adapter_type": { + "name": "adapter_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "task_key": { + "name": "task_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "session_params_json": { + "name": "session_params_json", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "session_display_id": { + "name": "session_display_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "last_run_id": { + "name": "last_run_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "last_error": { + "name": "last_error", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "agent_task_sessions_company_agent_adapter_task_uniq": { + "name": "agent_task_sessions_company_agent_adapter_task_uniq", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "agent_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "adapter_type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "task_key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "agent_task_sessions_company_agent_updated_idx": { + "name": "agent_task_sessions_company_agent_updated_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "agent_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "updated_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "agent_task_sessions_company_task_updated_idx": { + "name": "agent_task_sessions_company_task_updated_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "task_key", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "updated_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "agent_task_sessions_company_id_companies_id_fk": { + "name": "agent_task_sessions_company_id_companies_id_fk", + "tableFrom": "agent_task_sessions", + "tableTo": "companies", + "columnsFrom": [ + "company_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "agent_task_sessions_agent_id_agents_id_fk": { + "name": "agent_task_sessions_agent_id_agents_id_fk", + "tableFrom": "agent_task_sessions", + "tableTo": "agents", + "columnsFrom": [ + "agent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "agent_task_sessions_last_run_id_heartbeat_runs_id_fk": { + "name": "agent_task_sessions_last_run_id_heartbeat_runs_id_fk", + "tableFrom": "agent_task_sessions", + "tableTo": "heartbeat_runs", + "columnsFrom": [ + "last_run_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.agent_wakeup_requests": { + "name": "agent_wakeup_requests", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "company_id": { + "name": "company_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "agent_id": { + "name": "agent_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "source": { + "name": "source", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "trigger_detail": { + "name": "trigger_detail", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "reason": { + "name": "reason", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "payload": { + "name": "payload", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'queued'" + }, + "coalesced_count": { + "name": "coalesced_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "requested_by_actor_type": { + "name": "requested_by_actor_type", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "requested_by_actor_id": { + "name": "requested_by_actor_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "idempotency_key": { + "name": "idempotency_key", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "run_id": { + "name": "run_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "requested_at": { + "name": "requested_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "claimed_at": { + "name": "claimed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "finished_at": { + "name": "finished_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "error": { + "name": "error", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "agent_wakeup_requests_company_agent_status_idx": { + "name": "agent_wakeup_requests_company_agent_status_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "agent_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "agent_wakeup_requests_company_requested_idx": { + "name": "agent_wakeup_requests_company_requested_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "requested_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "agent_wakeup_requests_agent_requested_idx": { + "name": "agent_wakeup_requests_agent_requested_idx", + "columns": [ + { + "expression": "agent_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "requested_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "agent_wakeup_requests_company_id_companies_id_fk": { + "name": "agent_wakeup_requests_company_id_companies_id_fk", + "tableFrom": "agent_wakeup_requests", + "tableTo": "companies", + "columnsFrom": [ + "company_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "agent_wakeup_requests_agent_id_agents_id_fk": { + "name": "agent_wakeup_requests_agent_id_agents_id_fk", + "tableFrom": "agent_wakeup_requests", + "tableTo": "agents", + "columnsFrom": [ + "agent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.agents": { + "name": "agents", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "company_id": { + "name": "company_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "role": { + "name": "role", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'general'" + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "icon": { + "name": "icon", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'idle'" + }, + "reports_to": { + "name": "reports_to", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "capabilities": { + "name": "capabilities", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "adapter_type": { + "name": "adapter_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'process'" + }, + "adapter_config": { + "name": "adapter_config", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'::jsonb" + }, + "runtime_config": { + "name": "runtime_config", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'::jsonb" + }, + "budget_monthly_cents": { + "name": "budget_monthly_cents", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "spent_monthly_cents": { + "name": "spent_monthly_cents", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "pause_reason": { + "name": "pause_reason", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "paused_at": { + "name": "paused_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "permissions": { + "name": "permissions", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'::jsonb" + }, + "last_heartbeat_at": { + "name": "last_heartbeat_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "metadata": { + "name": "metadata", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "agents_company_status_idx": { + "name": "agents_company_status_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "agents_company_reports_to_idx": { + "name": "agents_company_reports_to_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "reports_to", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "agents_company_id_companies_id_fk": { + "name": "agents_company_id_companies_id_fk", + "tableFrom": "agents", + "tableTo": "companies", + "columnsFrom": [ + "company_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "agents_reports_to_agents_id_fk": { + "name": "agents_reports_to_agents_id_fk", + "tableFrom": "agents", + "tableTo": "agents", + "columnsFrom": [ + "reports_to" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.approval_comments": { + "name": "approval_comments", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "company_id": { + "name": "company_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "approval_id": { + "name": "approval_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "author_agent_id": { + "name": "author_agent_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "author_user_id": { + "name": "author_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "body": { + "name": "body", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "approval_comments_company_idx": { + "name": "approval_comments_company_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "approval_comments_approval_idx": { + "name": "approval_comments_approval_idx", + "columns": [ + { + "expression": "approval_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "approval_comments_approval_created_idx": { + "name": "approval_comments_approval_created_idx", + "columns": [ + { + "expression": "approval_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "approval_comments_company_id_companies_id_fk": { + "name": "approval_comments_company_id_companies_id_fk", + "tableFrom": "approval_comments", + "tableTo": "companies", + "columnsFrom": [ + "company_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "approval_comments_approval_id_approvals_id_fk": { + "name": "approval_comments_approval_id_approvals_id_fk", + "tableFrom": "approval_comments", + "tableTo": "approvals", + "columnsFrom": [ + "approval_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "approval_comments_author_agent_id_agents_id_fk": { + "name": "approval_comments_author_agent_id_agents_id_fk", + "tableFrom": "approval_comments", + "tableTo": "agents", + "columnsFrom": [ + "author_agent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.approvals": { + "name": "approvals", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "company_id": { + "name": "company_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "requested_by_agent_id": { + "name": "requested_by_agent_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "requested_by_user_id": { + "name": "requested_by_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "payload": { + "name": "payload", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "decision_note": { + "name": "decision_note", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "decided_by_user_id": { + "name": "decided_by_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "decided_at": { + "name": "decided_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "approvals_company_status_type_idx": { + "name": "approvals_company_status_type_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "type", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "approvals_company_id_companies_id_fk": { + "name": "approvals_company_id_companies_id_fk", + "tableFrom": "approvals", + "tableTo": "companies", + "columnsFrom": [ + "company_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "approvals_requested_by_agent_id_agents_id_fk": { + "name": "approvals_requested_by_agent_id_agents_id_fk", + "tableFrom": "approvals", + "tableTo": "agents", + "columnsFrom": [ + "requested_by_agent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.assets": { + "name": "assets", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "company_id": { + "name": "company_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "provider": { + "name": "provider", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "object_key": { + "name": "object_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "content_type": { + "name": "content_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "byte_size": { + "name": "byte_size", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "sha256": { + "name": "sha256", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "original_filename": { + "name": "original_filename", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_by_agent_id": { + "name": "created_by_agent_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "created_by_user_id": { + "name": "created_by_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "assets_company_created_idx": { + "name": "assets_company_created_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "assets_company_provider_idx": { + "name": "assets_company_provider_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "provider", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "assets_company_object_key_uq": { + "name": "assets_company_object_key_uq", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "object_key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "assets_company_id_companies_id_fk": { + "name": "assets_company_id_companies_id_fk", + "tableFrom": "assets", + "tableTo": "companies", + "columnsFrom": [ + "company_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "assets_created_by_agent_id_agents_id_fk": { + "name": "assets_created_by_agent_id_agents_id_fk", + "tableFrom": "assets", + "tableTo": "agents", + "columnsFrom": [ + "created_by_agent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.account": { + "name": "account", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "account_id": { + "name": "account_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "provider_id": { + "name": "provider_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "access_token": { + "name": "access_token", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "refresh_token": { + "name": "refresh_token", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "id_token": { + "name": "id_token", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "access_token_expires_at": { + "name": "access_token_expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "refresh_token_expires_at": { + "name": "refresh_token_expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "scope": { + "name": "scope", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "password": { + "name": "password", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "account_user_id_user_id_fk": { + "name": "account_user_id_user_id_fk", + "tableFrom": "account", + "tableTo": "user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.session": { + "name": "session", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "token": { + "name": "token", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "ip_address": { + "name": "ip_address", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "user_agent": { + "name": "user_agent", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "session_user_id_user_id_fk": { + "name": "session_user_id_user_id_fk", + "tableFrom": "session", + "tableTo": "user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.user": { + "name": "user", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "email_verified": { + "name": "email_verified", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "image": { + "name": "image", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.verification": { + "name": "verification", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "identifier": { + "name": "identifier", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "value": { + "name": "value", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.board_api_keys": { + "name": "board_api_keys", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "key_hash": { + "name": "key_hash", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "last_used_at": { + "name": "last_used_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "revoked_at": { + "name": "revoked_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "board_api_keys_key_hash_idx": { + "name": "board_api_keys_key_hash_idx", + "columns": [ + { + "expression": "key_hash", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "board_api_keys_user_idx": { + "name": "board_api_keys_user_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "board_api_keys_user_id_user_id_fk": { + "name": "board_api_keys_user_id_user_id_fk", + "tableFrom": "board_api_keys", + "tableTo": "user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.budget_incidents": { + "name": "budget_incidents", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "company_id": { + "name": "company_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "policy_id": { + "name": "policy_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "scope_type": { + "name": "scope_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "scope_id": { + "name": "scope_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "metric": { + "name": "metric", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "window_kind": { + "name": "window_kind", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "window_start": { + "name": "window_start", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "window_end": { + "name": "window_end", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "threshold_type": { + "name": "threshold_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "amount_limit": { + "name": "amount_limit", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "amount_observed": { + "name": "amount_observed", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'open'" + }, + "approval_id": { + "name": "approval_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "resolved_at": { + "name": "resolved_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "budget_incidents_company_status_idx": { + "name": "budget_incidents_company_status_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "budget_incidents_company_scope_idx": { + "name": "budget_incidents_company_scope_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "scope_type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "scope_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "budget_incidents_policy_window_threshold_idx": { + "name": "budget_incidents_policy_window_threshold_idx", + "columns": [ + { + "expression": "policy_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "window_start", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "threshold_type", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"budget_incidents\".\"status\" <> 'dismissed'", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "budget_incidents_company_id_companies_id_fk": { + "name": "budget_incidents_company_id_companies_id_fk", + "tableFrom": "budget_incidents", + "tableTo": "companies", + "columnsFrom": [ + "company_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "budget_incidents_policy_id_budget_policies_id_fk": { + "name": "budget_incidents_policy_id_budget_policies_id_fk", + "tableFrom": "budget_incidents", + "tableTo": "budget_policies", + "columnsFrom": [ + "policy_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "budget_incidents_approval_id_approvals_id_fk": { + "name": "budget_incidents_approval_id_approvals_id_fk", + "tableFrom": "budget_incidents", + "tableTo": "approvals", + "columnsFrom": [ + "approval_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.budget_policies": { + "name": "budget_policies", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "company_id": { + "name": "company_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "scope_type": { + "name": "scope_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "scope_id": { + "name": "scope_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "metric": { + "name": "metric", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'billed_cents'" + }, + "window_kind": { + "name": "window_kind", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "amount": { + "name": "amount", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "warn_percent": { + "name": "warn_percent", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 80 + }, + "hard_stop_enabled": { + "name": "hard_stop_enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "notify_enabled": { + "name": "notify_enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "is_active": { + "name": "is_active", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "created_by_user_id": { + "name": "created_by_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "updated_by_user_id": { + "name": "updated_by_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "budget_policies_company_scope_active_idx": { + "name": "budget_policies_company_scope_active_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "scope_type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "scope_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "is_active", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "budget_policies_company_window_idx": { + "name": "budget_policies_company_window_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "window_kind", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "metric", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "budget_policies_company_scope_metric_unique_idx": { + "name": "budget_policies_company_scope_metric_unique_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "scope_type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "scope_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "metric", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "window_kind", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "budget_policies_company_id_companies_id_fk": { + "name": "budget_policies_company_id_companies_id_fk", + "tableFrom": "budget_policies", + "tableTo": "companies", + "columnsFrom": [ + "company_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.cli_auth_challenges": { + "name": "cli_auth_challenges", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "secret_hash": { + "name": "secret_hash", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "command": { + "name": "command", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "client_name": { + "name": "client_name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "requested_access": { + "name": "requested_access", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'board'" + }, + "requested_company_id": { + "name": "requested_company_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "pending_key_hash": { + "name": "pending_key_hash", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "pending_key_name": { + "name": "pending_key_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "approved_by_user_id": { + "name": "approved_by_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "board_api_key_id": { + "name": "board_api_key_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "approved_at": { + "name": "approved_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "cancelled_at": { + "name": "cancelled_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "cli_auth_challenges_secret_hash_idx": { + "name": "cli_auth_challenges_secret_hash_idx", + "columns": [ + { + "expression": "secret_hash", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "cli_auth_challenges_approved_by_idx": { + "name": "cli_auth_challenges_approved_by_idx", + "columns": [ + { + "expression": "approved_by_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "cli_auth_challenges_requested_company_idx": { + "name": "cli_auth_challenges_requested_company_idx", + "columns": [ + { + "expression": "requested_company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "cli_auth_challenges_requested_company_id_companies_id_fk": { + "name": "cli_auth_challenges_requested_company_id_companies_id_fk", + "tableFrom": "cli_auth_challenges", + "tableTo": "companies", + "columnsFrom": [ + "requested_company_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "cli_auth_challenges_approved_by_user_id_user_id_fk": { + "name": "cli_auth_challenges_approved_by_user_id_user_id_fk", + "tableFrom": "cli_auth_challenges", + "tableTo": "user", + "columnsFrom": [ + "approved_by_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "cli_auth_challenges_board_api_key_id_board_api_keys_id_fk": { + "name": "cli_auth_challenges_board_api_key_id_board_api_keys_id_fk", + "tableFrom": "cli_auth_challenges", + "tableTo": "board_api_keys", + "columnsFrom": [ + "board_api_key_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.companies": { + "name": "companies", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'active'" + }, + "pause_reason": { + "name": "pause_reason", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "paused_at": { + "name": "paused_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "issue_prefix": { + "name": "issue_prefix", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'PAP'" + }, + "issue_counter": { + "name": "issue_counter", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "budget_monthly_cents": { + "name": "budget_monthly_cents", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "spent_monthly_cents": { + "name": "spent_monthly_cents", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "require_board_approval_for_new_agents": { + "name": "require_board_approval_for_new_agents", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "feedback_data_sharing_enabled": { + "name": "feedback_data_sharing_enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "feedback_data_sharing_consent_at": { + "name": "feedback_data_sharing_consent_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "feedback_data_sharing_consent_by_user_id": { + "name": "feedback_data_sharing_consent_by_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "feedback_data_sharing_terms_version": { + "name": "feedback_data_sharing_terms_version", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "brand_color": { + "name": "brand_color", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "companies_issue_prefix_idx": { + "name": "companies_issue_prefix_idx", + "columns": [ + { + "expression": "issue_prefix", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.company_logos": { + "name": "company_logos", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "company_id": { + "name": "company_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "asset_id": { + "name": "asset_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "company_logos_company_uq": { + "name": "company_logos_company_uq", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "company_logos_asset_uq": { + "name": "company_logos_asset_uq", + "columns": [ + { + "expression": "asset_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "company_logos_company_id_companies_id_fk": { + "name": "company_logos_company_id_companies_id_fk", + "tableFrom": "company_logos", + "tableTo": "companies", + "columnsFrom": [ + "company_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "company_logos_asset_id_assets_id_fk": { + "name": "company_logos_asset_id_assets_id_fk", + "tableFrom": "company_logos", + "tableTo": "assets", + "columnsFrom": [ + "asset_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.company_memberships": { + "name": "company_memberships", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "company_id": { + "name": "company_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "principal_type": { + "name": "principal_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "principal_id": { + "name": "principal_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'active'" + }, + "membership_role": { + "name": "membership_role", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "company_memberships_company_principal_unique_idx": { + "name": "company_memberships_company_principal_unique_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "principal_type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "principal_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "company_memberships_principal_status_idx": { + "name": "company_memberships_principal_status_idx", + "columns": [ + { + "expression": "principal_type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "principal_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "company_memberships_company_status_idx": { + "name": "company_memberships_company_status_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "company_memberships_company_id_companies_id_fk": { + "name": "company_memberships_company_id_companies_id_fk", + "tableFrom": "company_memberships", + "tableTo": "companies", + "columnsFrom": [ + "company_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.company_secret_versions": { + "name": "company_secret_versions", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "secret_id": { + "name": "secret_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "version": { + "name": "version", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "material": { + "name": "material", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "value_sha256": { + "name": "value_sha256", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_by_agent_id": { + "name": "created_by_agent_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "created_by_user_id": { + "name": "created_by_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "revoked_at": { + "name": "revoked_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "company_secret_versions_secret_idx": { + "name": "company_secret_versions_secret_idx", + "columns": [ + { + "expression": "secret_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "company_secret_versions_value_sha256_idx": { + "name": "company_secret_versions_value_sha256_idx", + "columns": [ + { + "expression": "value_sha256", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "company_secret_versions_secret_version_uq": { + "name": "company_secret_versions_secret_version_uq", + "columns": [ + { + "expression": "secret_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "version", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "company_secret_versions_secret_id_company_secrets_id_fk": { + "name": "company_secret_versions_secret_id_company_secrets_id_fk", + "tableFrom": "company_secret_versions", + "tableTo": "company_secrets", + "columnsFrom": [ + "secret_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "company_secret_versions_created_by_agent_id_agents_id_fk": { + "name": "company_secret_versions_created_by_agent_id_agents_id_fk", + "tableFrom": "company_secret_versions", + "tableTo": "agents", + "columnsFrom": [ + "created_by_agent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.company_secrets": { + "name": "company_secrets", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "company_id": { + "name": "company_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "provider": { + "name": "provider", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'local_encrypted'" + }, + "external_ref": { + "name": "external_ref", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "latest_version": { + "name": "latest_version", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 1 + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_by_agent_id": { + "name": "created_by_agent_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "created_by_user_id": { + "name": "created_by_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "company_secrets_company_idx": { + "name": "company_secrets_company_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "company_secrets_company_provider_idx": { + "name": "company_secrets_company_provider_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "provider", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "company_secrets_company_name_uq": { + "name": "company_secrets_company_name_uq", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "name", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "company_secrets_company_id_companies_id_fk": { + "name": "company_secrets_company_id_companies_id_fk", + "tableFrom": "company_secrets", + "tableTo": "companies", + "columnsFrom": [ + "company_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "company_secrets_created_by_agent_id_agents_id_fk": { + "name": "company_secrets_created_by_agent_id_agents_id_fk", + "tableFrom": "company_secrets", + "tableTo": "agents", + "columnsFrom": [ + "created_by_agent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.company_skills": { + "name": "company_skills", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "company_id": { + "name": "company_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "key": { + "name": "key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "slug": { + "name": "slug", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "markdown": { + "name": "markdown", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "source_type": { + "name": "source_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'local_path'" + }, + "source_locator": { + "name": "source_locator", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "source_ref": { + "name": "source_ref", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "trust_level": { + "name": "trust_level", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'markdown_only'" + }, + "compatibility": { + "name": "compatibility", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'compatible'" + }, + "file_inventory": { + "name": "file_inventory", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + }, + "metadata": { + "name": "metadata", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "company_skills_company_key_idx": { + "name": "company_skills_company_key_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "company_skills_company_name_idx": { + "name": "company_skills_company_name_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "name", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "company_skills_company_id_companies_id_fk": { + "name": "company_skills_company_id_companies_id_fk", + "tableFrom": "company_skills", + "tableTo": "companies", + "columnsFrom": [ + "company_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.company_user_sidebar_preferences": { + "name": "company_user_sidebar_preferences", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "company_id": { + "name": "company_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "project_order": { + "name": "project_order", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "company_user_sidebar_preferences_company_idx": { + "name": "company_user_sidebar_preferences_company_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "company_user_sidebar_preferences_user_idx": { + "name": "company_user_sidebar_preferences_user_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "company_user_sidebar_preferences_company_user_uq": { + "name": "company_user_sidebar_preferences_company_user_uq", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "company_user_sidebar_preferences_company_id_companies_id_fk": { + "name": "company_user_sidebar_preferences_company_id_companies_id_fk", + "tableFrom": "company_user_sidebar_preferences", + "tableTo": "companies", + "columnsFrom": [ + "company_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.cost_events": { + "name": "cost_events", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "company_id": { + "name": "company_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "agent_id": { + "name": "agent_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "issue_id": { + "name": "issue_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "project_id": { + "name": "project_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "goal_id": { + "name": "goal_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "heartbeat_run_id": { + "name": "heartbeat_run_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "billing_code": { + "name": "billing_code", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "provider": { + "name": "provider", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "biller": { + "name": "biller", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'unknown'" + }, + "billing_type": { + "name": "billing_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'unknown'" + }, + "model": { + "name": "model", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "input_tokens": { + "name": "input_tokens", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "cached_input_tokens": { + "name": "cached_input_tokens", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "output_tokens": { + "name": "output_tokens", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "cost_cents": { + "name": "cost_cents", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "occurred_at": { + "name": "occurred_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "cost_events_company_occurred_idx": { + "name": "cost_events_company_occurred_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "occurred_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "cost_events_company_agent_occurred_idx": { + "name": "cost_events_company_agent_occurred_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "agent_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "occurred_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "cost_events_company_provider_occurred_idx": { + "name": "cost_events_company_provider_occurred_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "provider", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "occurred_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "cost_events_company_biller_occurred_idx": { + "name": "cost_events_company_biller_occurred_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "biller", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "occurred_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "cost_events_company_heartbeat_run_idx": { + "name": "cost_events_company_heartbeat_run_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "heartbeat_run_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "cost_events_company_id_companies_id_fk": { + "name": "cost_events_company_id_companies_id_fk", + "tableFrom": "cost_events", + "tableTo": "companies", + "columnsFrom": [ + "company_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "cost_events_agent_id_agents_id_fk": { + "name": "cost_events_agent_id_agents_id_fk", + "tableFrom": "cost_events", + "tableTo": "agents", + "columnsFrom": [ + "agent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "cost_events_issue_id_issues_id_fk": { + "name": "cost_events_issue_id_issues_id_fk", + "tableFrom": "cost_events", + "tableTo": "issues", + "columnsFrom": [ + "issue_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "cost_events_project_id_projects_id_fk": { + "name": "cost_events_project_id_projects_id_fk", + "tableFrom": "cost_events", + "tableTo": "projects", + "columnsFrom": [ + "project_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "cost_events_goal_id_goals_id_fk": { + "name": "cost_events_goal_id_goals_id_fk", + "tableFrom": "cost_events", + "tableTo": "goals", + "columnsFrom": [ + "goal_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "cost_events_heartbeat_run_id_heartbeat_runs_id_fk": { + "name": "cost_events_heartbeat_run_id_heartbeat_runs_id_fk", + "tableFrom": "cost_events", + "tableTo": "heartbeat_runs", + "columnsFrom": [ + "heartbeat_run_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.document_revisions": { + "name": "document_revisions", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "company_id": { + "name": "company_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "document_id": { + "name": "document_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "revision_number": { + "name": "revision_number", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "format": { + "name": "format", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'markdown'" + }, + "body": { + "name": "body", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "change_summary": { + "name": "change_summary", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_by_agent_id": { + "name": "created_by_agent_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "created_by_user_id": { + "name": "created_by_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_by_run_id": { + "name": "created_by_run_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "document_revisions_document_revision_uq": { + "name": "document_revisions_document_revision_uq", + "columns": [ + { + "expression": "document_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "revision_number", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "document_revisions_company_document_created_idx": { + "name": "document_revisions_company_document_created_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "document_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "document_revisions_company_id_companies_id_fk": { + "name": "document_revisions_company_id_companies_id_fk", + "tableFrom": "document_revisions", + "tableTo": "companies", + "columnsFrom": [ + "company_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "document_revisions_document_id_documents_id_fk": { + "name": "document_revisions_document_id_documents_id_fk", + "tableFrom": "document_revisions", + "tableTo": "documents", + "columnsFrom": [ + "document_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "document_revisions_created_by_agent_id_agents_id_fk": { + "name": "document_revisions_created_by_agent_id_agents_id_fk", + "tableFrom": "document_revisions", + "tableTo": "agents", + "columnsFrom": [ + "created_by_agent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "document_revisions_created_by_run_id_heartbeat_runs_id_fk": { + "name": "document_revisions_created_by_run_id_heartbeat_runs_id_fk", + "tableFrom": "document_revisions", + "tableTo": "heartbeat_runs", + "columnsFrom": [ + "created_by_run_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.documents": { + "name": "documents", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "company_id": { + "name": "company_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "format": { + "name": "format", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'markdown'" + }, + "latest_body": { + "name": "latest_body", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "latest_revision_id": { + "name": "latest_revision_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "latest_revision_number": { + "name": "latest_revision_number", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 1 + }, + "created_by_agent_id": { + "name": "created_by_agent_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "created_by_user_id": { + "name": "created_by_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "updated_by_agent_id": { + "name": "updated_by_agent_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "updated_by_user_id": { + "name": "updated_by_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "documents_company_updated_idx": { + "name": "documents_company_updated_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "updated_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "documents_company_created_idx": { + "name": "documents_company_created_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "documents_company_id_companies_id_fk": { + "name": "documents_company_id_companies_id_fk", + "tableFrom": "documents", + "tableTo": "companies", + "columnsFrom": [ + "company_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "documents_created_by_agent_id_agents_id_fk": { + "name": "documents_created_by_agent_id_agents_id_fk", + "tableFrom": "documents", + "tableTo": "agents", + "columnsFrom": [ + "created_by_agent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "documents_updated_by_agent_id_agents_id_fk": { + "name": "documents_updated_by_agent_id_agents_id_fk", + "tableFrom": "documents", + "tableTo": "agents", + "columnsFrom": [ + "updated_by_agent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.execution_workspaces": { + "name": "execution_workspaces", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "company_id": { + "name": "company_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "project_id": { + "name": "project_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "project_workspace_id": { + "name": "project_workspace_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "source_issue_id": { + "name": "source_issue_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "mode": { + "name": "mode", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "strategy_type": { + "name": "strategy_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'active'" + }, + "cwd": { + "name": "cwd", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "repo_url": { + "name": "repo_url", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "base_ref": { + "name": "base_ref", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "branch_name": { + "name": "branch_name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "provider_type": { + "name": "provider_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'local_fs'" + }, + "provider_ref": { + "name": "provider_ref", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "derived_from_execution_workspace_id": { + "name": "derived_from_execution_workspace_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "last_used_at": { + "name": "last_used_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "opened_at": { + "name": "opened_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "closed_at": { + "name": "closed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "cleanup_eligible_at": { + "name": "cleanup_eligible_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "cleanup_reason": { + "name": "cleanup_reason", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "metadata": { + "name": "metadata", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "execution_workspaces_company_project_status_idx": { + "name": "execution_workspaces_company_project_status_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "project_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "execution_workspaces_company_project_workspace_status_idx": { + "name": "execution_workspaces_company_project_workspace_status_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "project_workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "execution_workspaces_company_source_issue_idx": { + "name": "execution_workspaces_company_source_issue_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "source_issue_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "execution_workspaces_company_last_used_idx": { + "name": "execution_workspaces_company_last_used_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "last_used_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "execution_workspaces_company_branch_idx": { + "name": "execution_workspaces_company_branch_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "branch_name", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "execution_workspaces_company_id_companies_id_fk": { + "name": "execution_workspaces_company_id_companies_id_fk", + "tableFrom": "execution_workspaces", + "tableTo": "companies", + "columnsFrom": [ + "company_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "execution_workspaces_project_id_projects_id_fk": { + "name": "execution_workspaces_project_id_projects_id_fk", + "tableFrom": "execution_workspaces", + "tableTo": "projects", + "columnsFrom": [ + "project_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "execution_workspaces_project_workspace_id_project_workspaces_id_fk": { + "name": "execution_workspaces_project_workspace_id_project_workspaces_id_fk", + "tableFrom": "execution_workspaces", + "tableTo": "project_workspaces", + "columnsFrom": [ + "project_workspace_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "execution_workspaces_source_issue_id_issues_id_fk": { + "name": "execution_workspaces_source_issue_id_issues_id_fk", + "tableFrom": "execution_workspaces", + "tableTo": "issues", + "columnsFrom": [ + "source_issue_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "execution_workspaces_derived_from_execution_workspace_id_execution_workspaces_id_fk": { + "name": "execution_workspaces_derived_from_execution_workspace_id_execution_workspaces_id_fk", + "tableFrom": "execution_workspaces", + "tableTo": "execution_workspaces", + "columnsFrom": [ + "derived_from_execution_workspace_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.feedback_exports": { + "name": "feedback_exports", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "company_id": { + "name": "company_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "feedback_vote_id": { + "name": "feedback_vote_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "issue_id": { + "name": "issue_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "project_id": { + "name": "project_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "author_user_id": { + "name": "author_user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "target_type": { + "name": "target_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "target_id": { + "name": "target_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "vote": { + "name": "vote", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'local_only'" + }, + "destination": { + "name": "destination", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "export_id": { + "name": "export_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "consent_version": { + "name": "consent_version", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "schema_version": { + "name": "schema_version", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'paperclip-feedback-envelope-v2'" + }, + "bundle_version": { + "name": "bundle_version", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'paperclip-feedback-bundle-v2'" + }, + "payload_version": { + "name": "payload_version", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'paperclip-feedback-v1'" + }, + "payload_digest": { + "name": "payload_digest", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "payload_snapshot": { + "name": "payload_snapshot", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "target_summary": { + "name": "target_summary", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "redaction_summary": { + "name": "redaction_summary", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "attempt_count": { + "name": "attempt_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "last_attempted_at": { + "name": "last_attempted_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "exported_at": { + "name": "exported_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "failure_reason": { + "name": "failure_reason", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "feedback_exports_feedback_vote_idx": { + "name": "feedback_exports_feedback_vote_idx", + "columns": [ + { + "expression": "feedback_vote_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "feedback_exports_company_created_idx": { + "name": "feedback_exports_company_created_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "feedback_exports_company_status_idx": { + "name": "feedback_exports_company_status_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "feedback_exports_company_issue_idx": { + "name": "feedback_exports_company_issue_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "issue_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "feedback_exports_company_project_idx": { + "name": "feedback_exports_company_project_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "project_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "feedback_exports_company_author_idx": { + "name": "feedback_exports_company_author_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "author_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "feedback_exports_company_id_companies_id_fk": { + "name": "feedback_exports_company_id_companies_id_fk", + "tableFrom": "feedback_exports", + "tableTo": "companies", + "columnsFrom": [ + "company_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "feedback_exports_feedback_vote_id_feedback_votes_id_fk": { + "name": "feedback_exports_feedback_vote_id_feedback_votes_id_fk", + "tableFrom": "feedback_exports", + "tableTo": "feedback_votes", + "columnsFrom": [ + "feedback_vote_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "feedback_exports_issue_id_issues_id_fk": { + "name": "feedback_exports_issue_id_issues_id_fk", + "tableFrom": "feedback_exports", + "tableTo": "issues", + "columnsFrom": [ + "issue_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "feedback_exports_project_id_projects_id_fk": { + "name": "feedback_exports_project_id_projects_id_fk", + "tableFrom": "feedback_exports", + "tableTo": "projects", + "columnsFrom": [ + "project_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.feedback_votes": { + "name": "feedback_votes", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "company_id": { + "name": "company_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "issue_id": { + "name": "issue_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "target_type": { + "name": "target_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "target_id": { + "name": "target_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "author_user_id": { + "name": "author_user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "vote": { + "name": "vote", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "reason": { + "name": "reason", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "shared_with_labs": { + "name": "shared_with_labs", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "shared_at": { + "name": "shared_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "consent_version": { + "name": "consent_version", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "redaction_summary": { + "name": "redaction_summary", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "feedback_votes_company_issue_idx": { + "name": "feedback_votes_company_issue_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "issue_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "feedback_votes_issue_target_idx": { + "name": "feedback_votes_issue_target_idx", + "columns": [ + { + "expression": "issue_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "target_type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "target_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "feedback_votes_author_idx": { + "name": "feedback_votes_author_idx", + "columns": [ + { + "expression": "author_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "feedback_votes_company_target_author_idx": { + "name": "feedback_votes_company_target_author_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "target_type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "target_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "author_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "feedback_votes_company_id_companies_id_fk": { + "name": "feedback_votes_company_id_companies_id_fk", + "tableFrom": "feedback_votes", + "tableTo": "companies", + "columnsFrom": [ + "company_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "feedback_votes_issue_id_issues_id_fk": { + "name": "feedback_votes_issue_id_issues_id_fk", + "tableFrom": "feedback_votes", + "tableTo": "issues", + "columnsFrom": [ + "issue_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.finance_events": { + "name": "finance_events", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "company_id": { + "name": "company_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "agent_id": { + "name": "agent_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "issue_id": { + "name": "issue_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "project_id": { + "name": "project_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "goal_id": { + "name": "goal_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "heartbeat_run_id": { + "name": "heartbeat_run_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "cost_event_id": { + "name": "cost_event_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "billing_code": { + "name": "billing_code", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "event_kind": { + "name": "event_kind", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "direction": { + "name": "direction", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'debit'" + }, + "biller": { + "name": "biller", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "provider": { + "name": "provider", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "execution_adapter_type": { + "name": "execution_adapter_type", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "pricing_tier": { + "name": "pricing_tier", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "region": { + "name": "region", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "model": { + "name": "model", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "quantity": { + "name": "quantity", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "unit": { + "name": "unit", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "amount_cents": { + "name": "amount_cents", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "currency": { + "name": "currency", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'USD'" + }, + "estimated": { + "name": "estimated", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "external_invoice_id": { + "name": "external_invoice_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "metadata_json": { + "name": "metadata_json", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "occurred_at": { + "name": "occurred_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "finance_events_company_occurred_idx": { + "name": "finance_events_company_occurred_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "occurred_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "finance_events_company_biller_occurred_idx": { + "name": "finance_events_company_biller_occurred_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "biller", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "occurred_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "finance_events_company_kind_occurred_idx": { + "name": "finance_events_company_kind_occurred_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "event_kind", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "occurred_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "finance_events_company_direction_occurred_idx": { + "name": "finance_events_company_direction_occurred_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "direction", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "occurred_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "finance_events_company_heartbeat_run_idx": { + "name": "finance_events_company_heartbeat_run_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "heartbeat_run_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "finance_events_company_cost_event_idx": { + "name": "finance_events_company_cost_event_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "cost_event_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "finance_events_company_id_companies_id_fk": { + "name": "finance_events_company_id_companies_id_fk", + "tableFrom": "finance_events", + "tableTo": "companies", + "columnsFrom": [ + "company_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "finance_events_agent_id_agents_id_fk": { + "name": "finance_events_agent_id_agents_id_fk", + "tableFrom": "finance_events", + "tableTo": "agents", + "columnsFrom": [ + "agent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "finance_events_issue_id_issues_id_fk": { + "name": "finance_events_issue_id_issues_id_fk", + "tableFrom": "finance_events", + "tableTo": "issues", + "columnsFrom": [ + "issue_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "finance_events_project_id_projects_id_fk": { + "name": "finance_events_project_id_projects_id_fk", + "tableFrom": "finance_events", + "tableTo": "projects", + "columnsFrom": [ + "project_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "finance_events_goal_id_goals_id_fk": { + "name": "finance_events_goal_id_goals_id_fk", + "tableFrom": "finance_events", + "tableTo": "goals", + "columnsFrom": [ + "goal_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "finance_events_heartbeat_run_id_heartbeat_runs_id_fk": { + "name": "finance_events_heartbeat_run_id_heartbeat_runs_id_fk", + "tableFrom": "finance_events", + "tableTo": "heartbeat_runs", + "columnsFrom": [ + "heartbeat_run_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "finance_events_cost_event_id_cost_events_id_fk": { + "name": "finance_events_cost_event_id_cost_events_id_fk", + "tableFrom": "finance_events", + "tableTo": "cost_events", + "columnsFrom": [ + "cost_event_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.goals": { + "name": "goals", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "company_id": { + "name": "company_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "level": { + "name": "level", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'task'" + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'planned'" + }, + "parent_id": { + "name": "parent_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "owner_agent_id": { + "name": "owner_agent_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "goals_company_idx": { + "name": "goals_company_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "goals_company_id_companies_id_fk": { + "name": "goals_company_id_companies_id_fk", + "tableFrom": "goals", + "tableTo": "companies", + "columnsFrom": [ + "company_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "goals_parent_id_goals_id_fk": { + "name": "goals_parent_id_goals_id_fk", + "tableFrom": "goals", + "tableTo": "goals", + "columnsFrom": [ + "parent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "goals_owner_agent_id_agents_id_fk": { + "name": "goals_owner_agent_id_agents_id_fk", + "tableFrom": "goals", + "tableTo": "agents", + "columnsFrom": [ + "owner_agent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.heartbeat_run_events": { + "name": "heartbeat_run_events", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "bigserial", + "primaryKey": true, + "notNull": true + }, + "company_id": { + "name": "company_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "run_id": { + "name": "run_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "agent_id": { + "name": "agent_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "seq": { + "name": "seq", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "event_type": { + "name": "event_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "stream": { + "name": "stream", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "level": { + "name": "level", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "color": { + "name": "color", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "message": { + "name": "message", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "payload": { + "name": "payload", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "heartbeat_run_events_run_seq_idx": { + "name": "heartbeat_run_events_run_seq_idx", + "columns": [ + { + "expression": "run_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "seq", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "heartbeat_run_events_company_run_idx": { + "name": "heartbeat_run_events_company_run_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "run_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "heartbeat_run_events_company_created_idx": { + "name": "heartbeat_run_events_company_created_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "heartbeat_run_events_company_id_companies_id_fk": { + "name": "heartbeat_run_events_company_id_companies_id_fk", + "tableFrom": "heartbeat_run_events", + "tableTo": "companies", + "columnsFrom": [ + "company_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "heartbeat_run_events_run_id_heartbeat_runs_id_fk": { + "name": "heartbeat_run_events_run_id_heartbeat_runs_id_fk", + "tableFrom": "heartbeat_run_events", + "tableTo": "heartbeat_runs", + "columnsFrom": [ + "run_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "heartbeat_run_events_agent_id_agents_id_fk": { + "name": "heartbeat_run_events_agent_id_agents_id_fk", + "tableFrom": "heartbeat_run_events", + "tableTo": "agents", + "columnsFrom": [ + "agent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.heartbeat_runs": { + "name": "heartbeat_runs", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "company_id": { + "name": "company_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "agent_id": { + "name": "agent_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "invocation_source": { + "name": "invocation_source", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'on_demand'" + }, + "trigger_detail": { + "name": "trigger_detail", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'queued'" + }, + "started_at": { + "name": "started_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "finished_at": { + "name": "finished_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "error": { + "name": "error", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "wakeup_request_id": { + "name": "wakeup_request_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "exit_code": { + "name": "exit_code", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "signal": { + "name": "signal", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "usage_json": { + "name": "usage_json", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "result_json": { + "name": "result_json", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "session_id_before": { + "name": "session_id_before", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "session_id_after": { + "name": "session_id_after", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "log_store": { + "name": "log_store", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "log_ref": { + "name": "log_ref", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "log_bytes": { + "name": "log_bytes", + "type": "bigint", + "primaryKey": false, + "notNull": false + }, + "log_sha256": { + "name": "log_sha256", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "log_compressed": { + "name": "log_compressed", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "stdout_excerpt": { + "name": "stdout_excerpt", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "stderr_excerpt": { + "name": "stderr_excerpt", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "error_code": { + "name": "error_code", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "external_run_id": { + "name": "external_run_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "process_pid": { + "name": "process_pid", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "process_group_id": { + "name": "process_group_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "process_started_at": { + "name": "process_started_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "retry_of_run_id": { + "name": "retry_of_run_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "process_loss_retry_count": { + "name": "process_loss_retry_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "issue_comment_status": { + "name": "issue_comment_status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'not_applicable'" + }, + "issue_comment_satisfied_by_comment_id": { + "name": "issue_comment_satisfied_by_comment_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "issue_comment_retry_queued_at": { + "name": "issue_comment_retry_queued_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "context_snapshot": { + "name": "context_snapshot", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "heartbeat_runs_company_agent_started_idx": { + "name": "heartbeat_runs_company_agent_started_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "agent_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "started_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "heartbeat_runs_company_id_companies_id_fk": { + "name": "heartbeat_runs_company_id_companies_id_fk", + "tableFrom": "heartbeat_runs", + "tableTo": "companies", + "columnsFrom": [ + "company_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "heartbeat_runs_agent_id_agents_id_fk": { + "name": "heartbeat_runs_agent_id_agents_id_fk", + "tableFrom": "heartbeat_runs", + "tableTo": "agents", + "columnsFrom": [ + "agent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "heartbeat_runs_wakeup_request_id_agent_wakeup_requests_id_fk": { + "name": "heartbeat_runs_wakeup_request_id_agent_wakeup_requests_id_fk", + "tableFrom": "heartbeat_runs", + "tableTo": "agent_wakeup_requests", + "columnsFrom": [ + "wakeup_request_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "heartbeat_runs_retry_of_run_id_heartbeat_runs_id_fk": { + "name": "heartbeat_runs_retry_of_run_id_heartbeat_runs_id_fk", + "tableFrom": "heartbeat_runs", + "tableTo": "heartbeat_runs", + "columnsFrom": [ + "retry_of_run_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.inbox_dismissals": { + "name": "inbox_dismissals", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "company_id": { + "name": "company_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "item_key": { + "name": "item_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "dismissed_at": { + "name": "dismissed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "inbox_dismissals_company_user_idx": { + "name": "inbox_dismissals_company_user_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "inbox_dismissals_company_item_idx": { + "name": "inbox_dismissals_company_item_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "item_key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "inbox_dismissals_company_user_item_idx": { + "name": "inbox_dismissals_company_user_item_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "item_key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "inbox_dismissals_company_id_companies_id_fk": { + "name": "inbox_dismissals_company_id_companies_id_fk", + "tableFrom": "inbox_dismissals", + "tableTo": "companies", + "columnsFrom": [ + "company_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.instance_settings": { + "name": "instance_settings", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "singleton_key": { + "name": "singleton_key", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'default'" + }, + "general": { + "name": "general", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'::jsonb" + }, + "experimental": { + "name": "experimental", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'::jsonb" + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "instance_settings_singleton_key_idx": { + "name": "instance_settings_singleton_key_idx", + "columns": [ + { + "expression": "singleton_key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.instance_user_roles": { + "name": "instance_user_roles", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "role": { + "name": "role", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'instance_admin'" + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "instance_user_roles_user_role_unique_idx": { + "name": "instance_user_roles_user_role_unique_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "role", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "instance_user_roles_role_idx": { + "name": "instance_user_roles_role_idx", + "columns": [ + { + "expression": "role", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.invites": { + "name": "invites", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "company_id": { + "name": "company_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "invite_type": { + "name": "invite_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'company_join'" + }, + "token_hash": { + "name": "token_hash", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "allowed_join_types": { + "name": "allowed_join_types", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'both'" + }, + "defaults_payload": { + "name": "defaults_payload", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "invited_by_user_id": { + "name": "invited_by_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "revoked_at": { + "name": "revoked_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "accepted_at": { + "name": "accepted_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "invites_token_hash_unique_idx": { + "name": "invites_token_hash_unique_idx", + "columns": [ + { + "expression": "token_hash", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "invites_company_invite_state_idx": { + "name": "invites_company_invite_state_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "invite_type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "revoked_at", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "expires_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "invites_company_id_companies_id_fk": { + "name": "invites_company_id_companies_id_fk", + "tableFrom": "invites", + "tableTo": "companies", + "columnsFrom": [ + "company_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.issue_approvals": { + "name": "issue_approvals", + "schema": "", + "columns": { + "company_id": { + "name": "company_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "issue_id": { + "name": "issue_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "approval_id": { + "name": "approval_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "linked_by_agent_id": { + "name": "linked_by_agent_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "linked_by_user_id": { + "name": "linked_by_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "issue_approvals_issue_idx": { + "name": "issue_approvals_issue_idx", + "columns": [ + { + "expression": "issue_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "issue_approvals_approval_idx": { + "name": "issue_approvals_approval_idx", + "columns": [ + { + "expression": "approval_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "issue_approvals_company_idx": { + "name": "issue_approvals_company_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "issue_approvals_company_id_companies_id_fk": { + "name": "issue_approvals_company_id_companies_id_fk", + "tableFrom": "issue_approvals", + "tableTo": "companies", + "columnsFrom": [ + "company_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "issue_approvals_issue_id_issues_id_fk": { + "name": "issue_approvals_issue_id_issues_id_fk", + "tableFrom": "issue_approvals", + "tableTo": "issues", + "columnsFrom": [ + "issue_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "issue_approvals_approval_id_approvals_id_fk": { + "name": "issue_approvals_approval_id_approvals_id_fk", + "tableFrom": "issue_approvals", + "tableTo": "approvals", + "columnsFrom": [ + "approval_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "issue_approvals_linked_by_agent_id_agents_id_fk": { + "name": "issue_approvals_linked_by_agent_id_agents_id_fk", + "tableFrom": "issue_approvals", + "tableTo": "agents", + "columnsFrom": [ + "linked_by_agent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "issue_approvals_pk": { + "name": "issue_approvals_pk", + "columns": [ + "issue_id", + "approval_id" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.issue_attachments": { + "name": "issue_attachments", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "company_id": { + "name": "company_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "issue_id": { + "name": "issue_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "asset_id": { + "name": "asset_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "issue_comment_id": { + "name": "issue_comment_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "issue_attachments_company_issue_idx": { + "name": "issue_attachments_company_issue_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "issue_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "issue_attachments_issue_comment_idx": { + "name": "issue_attachments_issue_comment_idx", + "columns": [ + { + "expression": "issue_comment_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "issue_attachments_asset_uq": { + "name": "issue_attachments_asset_uq", + "columns": [ + { + "expression": "asset_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "issue_attachments_company_id_companies_id_fk": { + "name": "issue_attachments_company_id_companies_id_fk", + "tableFrom": "issue_attachments", + "tableTo": "companies", + "columnsFrom": [ + "company_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "issue_attachments_issue_id_issues_id_fk": { + "name": "issue_attachments_issue_id_issues_id_fk", + "tableFrom": "issue_attachments", + "tableTo": "issues", + "columnsFrom": [ + "issue_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "issue_attachments_asset_id_assets_id_fk": { + "name": "issue_attachments_asset_id_assets_id_fk", + "tableFrom": "issue_attachments", + "tableTo": "assets", + "columnsFrom": [ + "asset_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "issue_attachments_issue_comment_id_issue_comments_id_fk": { + "name": "issue_attachments_issue_comment_id_issue_comments_id_fk", + "tableFrom": "issue_attachments", + "tableTo": "issue_comments", + "columnsFrom": [ + "issue_comment_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.issue_comments": { + "name": "issue_comments", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "company_id": { + "name": "company_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "issue_id": { + "name": "issue_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "author_agent_id": { + "name": "author_agent_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "author_user_id": { + "name": "author_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_by_run_id": { + "name": "created_by_run_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "body": { + "name": "body", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "issue_comments_issue_idx": { + "name": "issue_comments_issue_idx", + "columns": [ + { + "expression": "issue_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "issue_comments_company_idx": { + "name": "issue_comments_company_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "issue_comments_company_issue_created_at_idx": { + "name": "issue_comments_company_issue_created_at_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "issue_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "issue_comments_company_author_issue_created_at_idx": { + "name": "issue_comments_company_author_issue_created_at_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "author_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "issue_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "issue_comments_body_search_idx": { + "name": "issue_comments_body_search_idx", + "columns": [ + { + "expression": "body", + "isExpression": false, + "asc": true, + "nulls": "last", + "opclass": "gin_trgm_ops" + } + ], + "isUnique": false, + "concurrently": false, + "method": "gin", + "with": {} + } + }, + "foreignKeys": { + "issue_comments_company_id_companies_id_fk": { + "name": "issue_comments_company_id_companies_id_fk", + "tableFrom": "issue_comments", + "tableTo": "companies", + "columnsFrom": [ + "company_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "issue_comments_issue_id_issues_id_fk": { + "name": "issue_comments_issue_id_issues_id_fk", + "tableFrom": "issue_comments", + "tableTo": "issues", + "columnsFrom": [ + "issue_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "issue_comments_author_agent_id_agents_id_fk": { + "name": "issue_comments_author_agent_id_agents_id_fk", + "tableFrom": "issue_comments", + "tableTo": "agents", + "columnsFrom": [ + "author_agent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "issue_comments_created_by_run_id_heartbeat_runs_id_fk": { + "name": "issue_comments_created_by_run_id_heartbeat_runs_id_fk", + "tableFrom": "issue_comments", + "tableTo": "heartbeat_runs", + "columnsFrom": [ + "created_by_run_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.issue_documents": { + "name": "issue_documents", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "company_id": { + "name": "company_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "issue_id": { + "name": "issue_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "document_id": { + "name": "document_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "key": { + "name": "key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "issue_documents_company_issue_key_uq": { + "name": "issue_documents_company_issue_key_uq", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "issue_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "issue_documents_document_uq": { + "name": "issue_documents_document_uq", + "columns": [ + { + "expression": "document_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "issue_documents_company_issue_updated_idx": { + "name": "issue_documents_company_issue_updated_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "issue_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "updated_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "issue_documents_company_id_companies_id_fk": { + "name": "issue_documents_company_id_companies_id_fk", + "tableFrom": "issue_documents", + "tableTo": "companies", + "columnsFrom": [ + "company_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "issue_documents_issue_id_issues_id_fk": { + "name": "issue_documents_issue_id_issues_id_fk", + "tableFrom": "issue_documents", + "tableTo": "issues", + "columnsFrom": [ + "issue_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "issue_documents_document_id_documents_id_fk": { + "name": "issue_documents_document_id_documents_id_fk", + "tableFrom": "issue_documents", + "tableTo": "documents", + "columnsFrom": [ + "document_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.issue_execution_decisions": { + "name": "issue_execution_decisions", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "company_id": { + "name": "company_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "issue_id": { + "name": "issue_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "stage_id": { + "name": "stage_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "stage_type": { + "name": "stage_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "actor_agent_id": { + "name": "actor_agent_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "actor_user_id": { + "name": "actor_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "outcome": { + "name": "outcome", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "body": { + "name": "body", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_by_run_id": { + "name": "created_by_run_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "issue_execution_decisions_company_issue_idx": { + "name": "issue_execution_decisions_company_issue_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "issue_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "issue_execution_decisions_stage_idx": { + "name": "issue_execution_decisions_stage_idx", + "columns": [ + { + "expression": "issue_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "stage_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "issue_execution_decisions_company_id_companies_id_fk": { + "name": "issue_execution_decisions_company_id_companies_id_fk", + "tableFrom": "issue_execution_decisions", + "tableTo": "companies", + "columnsFrom": [ + "company_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "issue_execution_decisions_issue_id_issues_id_fk": { + "name": "issue_execution_decisions_issue_id_issues_id_fk", + "tableFrom": "issue_execution_decisions", + "tableTo": "issues", + "columnsFrom": [ + "issue_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "issue_execution_decisions_actor_agent_id_agents_id_fk": { + "name": "issue_execution_decisions_actor_agent_id_agents_id_fk", + "tableFrom": "issue_execution_decisions", + "tableTo": "agents", + "columnsFrom": [ + "actor_agent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "issue_execution_decisions_created_by_run_id_heartbeat_runs_id_fk": { + "name": "issue_execution_decisions_created_by_run_id_heartbeat_runs_id_fk", + "tableFrom": "issue_execution_decisions", + "tableTo": "heartbeat_runs", + "columnsFrom": [ + "created_by_run_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.issue_inbox_archives": { + "name": "issue_inbox_archives", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "company_id": { + "name": "company_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "issue_id": { + "name": "issue_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "archived_at": { + "name": "archived_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "issue_inbox_archives_company_issue_idx": { + "name": "issue_inbox_archives_company_issue_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "issue_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "issue_inbox_archives_company_user_idx": { + "name": "issue_inbox_archives_company_user_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "issue_inbox_archives_company_issue_user_idx": { + "name": "issue_inbox_archives_company_issue_user_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "issue_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "issue_inbox_archives_company_id_companies_id_fk": { + "name": "issue_inbox_archives_company_id_companies_id_fk", + "tableFrom": "issue_inbox_archives", + "tableTo": "companies", + "columnsFrom": [ + "company_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "issue_inbox_archives_issue_id_issues_id_fk": { + "name": "issue_inbox_archives_issue_id_issues_id_fk", + "tableFrom": "issue_inbox_archives", + "tableTo": "issues", + "columnsFrom": [ + "issue_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.issue_labels": { + "name": "issue_labels", + "schema": "", + "columns": { + "issue_id": { + "name": "issue_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "label_id": { + "name": "label_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "company_id": { + "name": "company_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "issue_labels_issue_idx": { + "name": "issue_labels_issue_idx", + "columns": [ + { + "expression": "issue_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "issue_labels_label_idx": { + "name": "issue_labels_label_idx", + "columns": [ + { + "expression": "label_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "issue_labels_company_idx": { + "name": "issue_labels_company_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "issue_labels_issue_id_issues_id_fk": { + "name": "issue_labels_issue_id_issues_id_fk", + "tableFrom": "issue_labels", + "tableTo": "issues", + "columnsFrom": [ + "issue_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "issue_labels_label_id_labels_id_fk": { + "name": "issue_labels_label_id_labels_id_fk", + "tableFrom": "issue_labels", + "tableTo": "labels", + "columnsFrom": [ + "label_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "issue_labels_company_id_companies_id_fk": { + "name": "issue_labels_company_id_companies_id_fk", + "tableFrom": "issue_labels", + "tableTo": "companies", + "columnsFrom": [ + "company_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "issue_labels_pk": { + "name": "issue_labels_pk", + "columns": [ + "issue_id", + "label_id" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.issue_read_states": { + "name": "issue_read_states", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "company_id": { + "name": "company_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "issue_id": { + "name": "issue_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "last_read_at": { + "name": "last_read_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "issue_read_states_company_issue_idx": { + "name": "issue_read_states_company_issue_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "issue_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "issue_read_states_company_user_idx": { + "name": "issue_read_states_company_user_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "issue_read_states_company_issue_user_idx": { + "name": "issue_read_states_company_issue_user_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "issue_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "issue_read_states_company_id_companies_id_fk": { + "name": "issue_read_states_company_id_companies_id_fk", + "tableFrom": "issue_read_states", + "tableTo": "companies", + "columnsFrom": [ + "company_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "issue_read_states_issue_id_issues_id_fk": { + "name": "issue_read_states_issue_id_issues_id_fk", + "tableFrom": "issue_read_states", + "tableTo": "issues", + "columnsFrom": [ + "issue_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.issue_relations": { + "name": "issue_relations", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "company_id": { + "name": "company_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "issue_id": { + "name": "issue_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "related_issue_id": { + "name": "related_issue_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_by_agent_id": { + "name": "created_by_agent_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "created_by_user_id": { + "name": "created_by_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "issue_relations_company_issue_idx": { + "name": "issue_relations_company_issue_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "issue_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "issue_relations_company_related_issue_idx": { + "name": "issue_relations_company_related_issue_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "related_issue_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "issue_relations_company_type_idx": { + "name": "issue_relations_company_type_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "type", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "issue_relations_company_edge_uq": { + "name": "issue_relations_company_edge_uq", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "issue_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "related_issue_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "type", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "issue_relations_company_id_companies_id_fk": { + "name": "issue_relations_company_id_companies_id_fk", + "tableFrom": "issue_relations", + "tableTo": "companies", + "columnsFrom": [ + "company_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "issue_relations_issue_id_issues_id_fk": { + "name": "issue_relations_issue_id_issues_id_fk", + "tableFrom": "issue_relations", + "tableTo": "issues", + "columnsFrom": [ + "issue_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "issue_relations_related_issue_id_issues_id_fk": { + "name": "issue_relations_related_issue_id_issues_id_fk", + "tableFrom": "issue_relations", + "tableTo": "issues", + "columnsFrom": [ + "related_issue_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "issue_relations_created_by_agent_id_agents_id_fk": { + "name": "issue_relations_created_by_agent_id_agents_id_fk", + "tableFrom": "issue_relations", + "tableTo": "agents", + "columnsFrom": [ + "created_by_agent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.issue_work_products": { + "name": "issue_work_products", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "company_id": { + "name": "company_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "project_id": { + "name": "project_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "issue_id": { + "name": "issue_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "execution_workspace_id": { + "name": "execution_workspace_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "runtime_service_id": { + "name": "runtime_service_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "provider": { + "name": "provider", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "external_id": { + "name": "external_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "url": { + "name": "url", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "review_state": { + "name": "review_state", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'none'" + }, + "is_primary": { + "name": "is_primary", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "health_status": { + "name": "health_status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'unknown'" + }, + "summary": { + "name": "summary", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "metadata": { + "name": "metadata", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "created_by_run_id": { + "name": "created_by_run_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "issue_work_products_company_issue_type_idx": { + "name": "issue_work_products_company_issue_type_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "issue_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "type", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "issue_work_products_company_execution_workspace_type_idx": { + "name": "issue_work_products_company_execution_workspace_type_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "execution_workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "type", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "issue_work_products_company_provider_external_id_idx": { + "name": "issue_work_products_company_provider_external_id_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "provider", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "external_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "issue_work_products_company_updated_idx": { + "name": "issue_work_products_company_updated_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "updated_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "issue_work_products_company_id_companies_id_fk": { + "name": "issue_work_products_company_id_companies_id_fk", + "tableFrom": "issue_work_products", + "tableTo": "companies", + "columnsFrom": [ + "company_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "issue_work_products_project_id_projects_id_fk": { + "name": "issue_work_products_project_id_projects_id_fk", + "tableFrom": "issue_work_products", + "tableTo": "projects", + "columnsFrom": [ + "project_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "issue_work_products_issue_id_issues_id_fk": { + "name": "issue_work_products_issue_id_issues_id_fk", + "tableFrom": "issue_work_products", + "tableTo": "issues", + "columnsFrom": [ + "issue_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "issue_work_products_execution_workspace_id_execution_workspaces_id_fk": { + "name": "issue_work_products_execution_workspace_id_execution_workspaces_id_fk", + "tableFrom": "issue_work_products", + "tableTo": "execution_workspaces", + "columnsFrom": [ + "execution_workspace_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "issue_work_products_runtime_service_id_workspace_runtime_services_id_fk": { + "name": "issue_work_products_runtime_service_id_workspace_runtime_services_id_fk", + "tableFrom": "issue_work_products", + "tableTo": "workspace_runtime_services", + "columnsFrom": [ + "runtime_service_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "issue_work_products_created_by_run_id_heartbeat_runs_id_fk": { + "name": "issue_work_products_created_by_run_id_heartbeat_runs_id_fk", + "tableFrom": "issue_work_products", + "tableTo": "heartbeat_runs", + "columnsFrom": [ + "created_by_run_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.issues": { + "name": "issues", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "company_id": { + "name": "company_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "project_id": { + "name": "project_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "project_workspace_id": { + "name": "project_workspace_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "goal_id": { + "name": "goal_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "parent_id": { + "name": "parent_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'backlog'" + }, + "priority": { + "name": "priority", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'medium'" + }, + "assignee_agent_id": { + "name": "assignee_agent_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "assignee_user_id": { + "name": "assignee_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "checkout_run_id": { + "name": "checkout_run_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "execution_run_id": { + "name": "execution_run_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "execution_agent_name_key": { + "name": "execution_agent_name_key", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "execution_locked_at": { + "name": "execution_locked_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_by_agent_id": { + "name": "created_by_agent_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "created_by_user_id": { + "name": "created_by_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "issue_number": { + "name": "issue_number", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "identifier": { + "name": "identifier", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "origin_kind": { + "name": "origin_kind", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'manual'" + }, + "origin_id": { + "name": "origin_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "origin_run_id": { + "name": "origin_run_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "request_depth": { + "name": "request_depth", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "billing_code": { + "name": "billing_code", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "assignee_adapter_overrides": { + "name": "assignee_adapter_overrides", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "execution_policy": { + "name": "execution_policy", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "execution_state": { + "name": "execution_state", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "execution_workspace_id": { + "name": "execution_workspace_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "execution_workspace_preference": { + "name": "execution_workspace_preference", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "execution_workspace_settings": { + "name": "execution_workspace_settings", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "started_at": { + "name": "started_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "completed_at": { + "name": "completed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "cancelled_at": { + "name": "cancelled_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "hidden_at": { + "name": "hidden_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "issues_company_status_idx": { + "name": "issues_company_status_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "issues_company_assignee_status_idx": { + "name": "issues_company_assignee_status_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "assignee_agent_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "issues_company_assignee_user_status_idx": { + "name": "issues_company_assignee_user_status_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "assignee_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "issues_company_parent_idx": { + "name": "issues_company_parent_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "parent_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "issues_company_project_idx": { + "name": "issues_company_project_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "project_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "issues_company_origin_idx": { + "name": "issues_company_origin_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "origin_kind", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "origin_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "issues_company_project_workspace_idx": { + "name": "issues_company_project_workspace_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "project_workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "issues_company_execution_workspace_idx": { + "name": "issues_company_execution_workspace_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "execution_workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "issues_identifier_idx": { + "name": "issues_identifier_idx", + "columns": [ + { + "expression": "identifier", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "issues_title_search_idx": { + "name": "issues_title_search_idx", + "columns": [ + { + "expression": "title", + "isExpression": false, + "asc": true, + "nulls": "last", + "opclass": "gin_trgm_ops" + } + ], + "isUnique": false, + "concurrently": false, + "method": "gin", + "with": {} + }, + "issues_identifier_search_idx": { + "name": "issues_identifier_search_idx", + "columns": [ + { + "expression": "identifier", + "isExpression": false, + "asc": true, + "nulls": "last", + "opclass": "gin_trgm_ops" + } + ], + "isUnique": false, + "concurrently": false, + "method": "gin", + "with": {} + }, + "issues_description_search_idx": { + "name": "issues_description_search_idx", + "columns": [ + { + "expression": "description", + "isExpression": false, + "asc": true, + "nulls": "last", + "opclass": "gin_trgm_ops" + } + ], + "isUnique": false, + "concurrently": false, + "method": "gin", + "with": {} + }, + "issues_open_routine_execution_uq": { + "name": "issues_open_routine_execution_uq", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "origin_kind", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "origin_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"issues\".\"origin_kind\" = 'routine_execution'\n and \"issues\".\"origin_id\" is not null\n and \"issues\".\"hidden_at\" is null\n and \"issues\".\"execution_run_id\" is not null\n and \"issues\".\"status\" in ('backlog', 'todo', 'in_progress', 'in_review', 'blocked')", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "issues_company_id_companies_id_fk": { + "name": "issues_company_id_companies_id_fk", + "tableFrom": "issues", + "tableTo": "companies", + "columnsFrom": [ + "company_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "issues_project_id_projects_id_fk": { + "name": "issues_project_id_projects_id_fk", + "tableFrom": "issues", + "tableTo": "projects", + "columnsFrom": [ + "project_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "issues_project_workspace_id_project_workspaces_id_fk": { + "name": "issues_project_workspace_id_project_workspaces_id_fk", + "tableFrom": "issues", + "tableTo": "project_workspaces", + "columnsFrom": [ + "project_workspace_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "issues_goal_id_goals_id_fk": { + "name": "issues_goal_id_goals_id_fk", + "tableFrom": "issues", + "tableTo": "goals", + "columnsFrom": [ + "goal_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "issues_parent_id_issues_id_fk": { + "name": "issues_parent_id_issues_id_fk", + "tableFrom": "issues", + "tableTo": "issues", + "columnsFrom": [ + "parent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "issues_assignee_agent_id_agents_id_fk": { + "name": "issues_assignee_agent_id_agents_id_fk", + "tableFrom": "issues", + "tableTo": "agents", + "columnsFrom": [ + "assignee_agent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "issues_checkout_run_id_heartbeat_runs_id_fk": { + "name": "issues_checkout_run_id_heartbeat_runs_id_fk", + "tableFrom": "issues", + "tableTo": "heartbeat_runs", + "columnsFrom": [ + "checkout_run_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "issues_execution_run_id_heartbeat_runs_id_fk": { + "name": "issues_execution_run_id_heartbeat_runs_id_fk", + "tableFrom": "issues", + "tableTo": "heartbeat_runs", + "columnsFrom": [ + "execution_run_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "issues_created_by_agent_id_agents_id_fk": { + "name": "issues_created_by_agent_id_agents_id_fk", + "tableFrom": "issues", + "tableTo": "agents", + "columnsFrom": [ + "created_by_agent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "issues_execution_workspace_id_execution_workspaces_id_fk": { + "name": "issues_execution_workspace_id_execution_workspaces_id_fk", + "tableFrom": "issues", + "tableTo": "execution_workspaces", + "columnsFrom": [ + "execution_workspace_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.join_requests": { + "name": "join_requests", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "invite_id": { + "name": "invite_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "company_id": { + "name": "company_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "request_type": { + "name": "request_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'pending_approval'" + }, + "request_ip": { + "name": "request_ip", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "requesting_user_id": { + "name": "requesting_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "request_email_snapshot": { + "name": "request_email_snapshot", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "agent_name": { + "name": "agent_name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "adapter_type": { + "name": "adapter_type", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "capabilities": { + "name": "capabilities", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "agent_defaults_payload": { + "name": "agent_defaults_payload", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "claim_secret_hash": { + "name": "claim_secret_hash", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "claim_secret_expires_at": { + "name": "claim_secret_expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "claim_secret_consumed_at": { + "name": "claim_secret_consumed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_agent_id": { + "name": "created_agent_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "approved_by_user_id": { + "name": "approved_by_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "approved_at": { + "name": "approved_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "rejected_by_user_id": { + "name": "rejected_by_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "rejected_at": { + "name": "rejected_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "join_requests_invite_unique_idx": { + "name": "join_requests_invite_unique_idx", + "columns": [ + { + "expression": "invite_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "join_requests_company_status_type_created_idx": { + "name": "join_requests_company_status_type_created_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "request_type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "join_requests_invite_id_invites_id_fk": { + "name": "join_requests_invite_id_invites_id_fk", + "tableFrom": "join_requests", + "tableTo": "invites", + "columnsFrom": [ + "invite_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "join_requests_company_id_companies_id_fk": { + "name": "join_requests_company_id_companies_id_fk", + "tableFrom": "join_requests", + "tableTo": "companies", + "columnsFrom": [ + "company_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "join_requests_created_agent_id_agents_id_fk": { + "name": "join_requests_created_agent_id_agents_id_fk", + "tableFrom": "join_requests", + "tableTo": "agents", + "columnsFrom": [ + "created_agent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.labels": { + "name": "labels", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "company_id": { + "name": "company_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "color": { + "name": "color", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "labels_company_idx": { + "name": "labels_company_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "labels_company_name_idx": { + "name": "labels_company_name_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "name", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "labels_company_id_companies_id_fk": { + "name": "labels_company_id_companies_id_fk", + "tableFrom": "labels", + "tableTo": "companies", + "columnsFrom": [ + "company_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.plugin_company_settings": { + "name": "plugin_company_settings", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "company_id": { + "name": "company_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "plugin_id": { + "name": "plugin_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "enabled": { + "name": "enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "settings_json": { + "name": "settings_json", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'::jsonb" + }, + "last_error": { + "name": "last_error", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "plugin_company_settings_company_idx": { + "name": "plugin_company_settings_company_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "plugin_company_settings_plugin_idx": { + "name": "plugin_company_settings_plugin_idx", + "columns": [ + { + "expression": "plugin_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "plugin_company_settings_company_plugin_uq": { + "name": "plugin_company_settings_company_plugin_uq", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "plugin_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "plugin_company_settings_company_id_companies_id_fk": { + "name": "plugin_company_settings_company_id_companies_id_fk", + "tableFrom": "plugin_company_settings", + "tableTo": "companies", + "columnsFrom": [ + "company_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "plugin_company_settings_plugin_id_plugins_id_fk": { + "name": "plugin_company_settings_plugin_id_plugins_id_fk", + "tableFrom": "plugin_company_settings", + "tableTo": "plugins", + "columnsFrom": [ + "plugin_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.plugin_config": { + "name": "plugin_config", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "plugin_id": { + "name": "plugin_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "config_json": { + "name": "config_json", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'::jsonb" + }, + "last_error": { + "name": "last_error", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "plugin_config_plugin_id_idx": { + "name": "plugin_config_plugin_id_idx", + "columns": [ + { + "expression": "plugin_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "plugin_config_plugin_id_plugins_id_fk": { + "name": "plugin_config_plugin_id_plugins_id_fk", + "tableFrom": "plugin_config", + "tableTo": "plugins", + "columnsFrom": [ + "plugin_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.plugin_entities": { + "name": "plugin_entities", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "plugin_id": { + "name": "plugin_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "entity_type": { + "name": "entity_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "scope_kind": { + "name": "scope_kind", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "scope_id": { + "name": "scope_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "external_id": { + "name": "external_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "data": { + "name": "data", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'::jsonb" + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "plugin_entities_plugin_idx": { + "name": "plugin_entities_plugin_idx", + "columns": [ + { + "expression": "plugin_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "plugin_entities_type_idx": { + "name": "plugin_entities_type_idx", + "columns": [ + { + "expression": "entity_type", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "plugin_entities_scope_idx": { + "name": "plugin_entities_scope_idx", + "columns": [ + { + "expression": "scope_kind", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "scope_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "plugin_entities_external_idx": { + "name": "plugin_entities_external_idx", + "columns": [ + { + "expression": "plugin_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "entity_type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "external_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "plugin_entities_plugin_id_plugins_id_fk": { + "name": "plugin_entities_plugin_id_plugins_id_fk", + "tableFrom": "plugin_entities", + "tableTo": "plugins", + "columnsFrom": [ + "plugin_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.plugin_job_runs": { + "name": "plugin_job_runs", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "job_id": { + "name": "job_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "plugin_id": { + "name": "plugin_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "trigger": { + "name": "trigger", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "duration_ms": { + "name": "duration_ms", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "error": { + "name": "error", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "logs": { + "name": "logs", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + }, + "started_at": { + "name": "started_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "finished_at": { + "name": "finished_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "plugin_job_runs_job_idx": { + "name": "plugin_job_runs_job_idx", + "columns": [ + { + "expression": "job_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "plugin_job_runs_plugin_idx": { + "name": "plugin_job_runs_plugin_idx", + "columns": [ + { + "expression": "plugin_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "plugin_job_runs_status_idx": { + "name": "plugin_job_runs_status_idx", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "plugin_job_runs_job_id_plugin_jobs_id_fk": { + "name": "plugin_job_runs_job_id_plugin_jobs_id_fk", + "tableFrom": "plugin_job_runs", + "tableTo": "plugin_jobs", + "columnsFrom": [ + "job_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "plugin_job_runs_plugin_id_plugins_id_fk": { + "name": "plugin_job_runs_plugin_id_plugins_id_fk", + "tableFrom": "plugin_job_runs", + "tableTo": "plugins", + "columnsFrom": [ + "plugin_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.plugin_jobs": { + "name": "plugin_jobs", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "plugin_id": { + "name": "plugin_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "job_key": { + "name": "job_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "schedule": { + "name": "schedule", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'active'" + }, + "last_run_at": { + "name": "last_run_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "next_run_at": { + "name": "next_run_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "plugin_jobs_plugin_idx": { + "name": "plugin_jobs_plugin_idx", + "columns": [ + { + "expression": "plugin_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "plugin_jobs_next_run_idx": { + "name": "plugin_jobs_next_run_idx", + "columns": [ + { + "expression": "next_run_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "plugin_jobs_unique_idx": { + "name": "plugin_jobs_unique_idx", + "columns": [ + { + "expression": "plugin_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "job_key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "plugin_jobs_plugin_id_plugins_id_fk": { + "name": "plugin_jobs_plugin_id_plugins_id_fk", + "tableFrom": "plugin_jobs", + "tableTo": "plugins", + "columnsFrom": [ + "plugin_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.plugin_logs": { + "name": "plugin_logs", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "plugin_id": { + "name": "plugin_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "level": { + "name": "level", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'info'" + }, + "message": { + "name": "message", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "meta": { + "name": "meta", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "plugin_logs_plugin_time_idx": { + "name": "plugin_logs_plugin_time_idx", + "columns": [ + { + "expression": "plugin_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "plugin_logs_level_idx": { + "name": "plugin_logs_level_idx", + "columns": [ + { + "expression": "level", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "plugin_logs_plugin_id_plugins_id_fk": { + "name": "plugin_logs_plugin_id_plugins_id_fk", + "tableFrom": "plugin_logs", + "tableTo": "plugins", + "columnsFrom": [ + "plugin_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.plugin_state": { + "name": "plugin_state", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "plugin_id": { + "name": "plugin_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "scope_kind": { + "name": "scope_kind", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "scope_id": { + "name": "scope_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "namespace": { + "name": "namespace", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'default'" + }, + "state_key": { + "name": "state_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "value_json": { + "name": "value_json", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "plugin_state_plugin_scope_idx": { + "name": "plugin_state_plugin_scope_idx", + "columns": [ + { + "expression": "plugin_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "scope_kind", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "plugin_state_plugin_id_plugins_id_fk": { + "name": "plugin_state_plugin_id_plugins_id_fk", + "tableFrom": "plugin_state", + "tableTo": "plugins", + "columnsFrom": [ + "plugin_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "plugin_state_unique_entry_idx": { + "name": "plugin_state_unique_entry_idx", + "nullsNotDistinct": true, + "columns": [ + "plugin_id", + "scope_kind", + "scope_id", + "namespace", + "state_key" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.plugin_webhook_deliveries": { + "name": "plugin_webhook_deliveries", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "plugin_id": { + "name": "plugin_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "webhook_key": { + "name": "webhook_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "external_id": { + "name": "external_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "duration_ms": { + "name": "duration_ms", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "error": { + "name": "error", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "payload": { + "name": "payload", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "headers": { + "name": "headers", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'::jsonb" + }, + "started_at": { + "name": "started_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "finished_at": { + "name": "finished_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "plugin_webhook_deliveries_plugin_idx": { + "name": "plugin_webhook_deliveries_plugin_idx", + "columns": [ + { + "expression": "plugin_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "plugin_webhook_deliveries_status_idx": { + "name": "plugin_webhook_deliveries_status_idx", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "plugin_webhook_deliveries_key_idx": { + "name": "plugin_webhook_deliveries_key_idx", + "columns": [ + { + "expression": "webhook_key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "plugin_webhook_deliveries_plugin_id_plugins_id_fk": { + "name": "plugin_webhook_deliveries_plugin_id_plugins_id_fk", + "tableFrom": "plugin_webhook_deliveries", + "tableTo": "plugins", + "columnsFrom": [ + "plugin_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.plugins": { + "name": "plugins", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "plugin_key": { + "name": "plugin_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "package_name": { + "name": "package_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "version": { + "name": "version", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "api_version": { + "name": "api_version", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 1 + }, + "categories": { + "name": "categories", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + }, + "manifest_json": { + "name": "manifest_json", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'installed'" + }, + "install_order": { + "name": "install_order", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "package_path": { + "name": "package_path", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "last_error": { + "name": "last_error", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "installed_at": { + "name": "installed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "plugins_plugin_key_idx": { + "name": "plugins_plugin_key_idx", + "columns": [ + { + "expression": "plugin_key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "plugins_status_idx": { + "name": "plugins_status_idx", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.principal_permission_grants": { + "name": "principal_permission_grants", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "company_id": { + "name": "company_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "principal_type": { + "name": "principal_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "principal_id": { + "name": "principal_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "permission_key": { + "name": "permission_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "scope": { + "name": "scope", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "granted_by_user_id": { + "name": "granted_by_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "principal_permission_grants_unique_idx": { + "name": "principal_permission_grants_unique_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "principal_type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "principal_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "permission_key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "principal_permission_grants_company_permission_idx": { + "name": "principal_permission_grants_company_permission_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "permission_key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "principal_permission_grants_company_id_companies_id_fk": { + "name": "principal_permission_grants_company_id_companies_id_fk", + "tableFrom": "principal_permission_grants", + "tableTo": "companies", + "columnsFrom": [ + "company_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.project_goals": { + "name": "project_goals", + "schema": "", + "columns": { + "project_id": { + "name": "project_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "goal_id": { + "name": "goal_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "company_id": { + "name": "company_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "project_goals_project_idx": { + "name": "project_goals_project_idx", + "columns": [ + { + "expression": "project_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "project_goals_goal_idx": { + "name": "project_goals_goal_idx", + "columns": [ + { + "expression": "goal_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "project_goals_company_idx": { + "name": "project_goals_company_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "project_goals_project_id_projects_id_fk": { + "name": "project_goals_project_id_projects_id_fk", + "tableFrom": "project_goals", + "tableTo": "projects", + "columnsFrom": [ + "project_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "project_goals_goal_id_goals_id_fk": { + "name": "project_goals_goal_id_goals_id_fk", + "tableFrom": "project_goals", + "tableTo": "goals", + "columnsFrom": [ + "goal_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "project_goals_company_id_companies_id_fk": { + "name": "project_goals_company_id_companies_id_fk", + "tableFrom": "project_goals", + "tableTo": "companies", + "columnsFrom": [ + "company_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "project_goals_project_id_goal_id_pk": { + "name": "project_goals_project_id_goal_id_pk", + "columns": [ + "project_id", + "goal_id" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.project_workspaces": { + "name": "project_workspaces", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "company_id": { + "name": "company_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "project_id": { + "name": "project_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "source_type": { + "name": "source_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'local_path'" + }, + "cwd": { + "name": "cwd", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "repo_url": { + "name": "repo_url", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "repo_ref": { + "name": "repo_ref", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "default_ref": { + "name": "default_ref", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "visibility": { + "name": "visibility", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'default'" + }, + "setup_command": { + "name": "setup_command", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "cleanup_command": { + "name": "cleanup_command", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "remote_provider": { + "name": "remote_provider", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "remote_workspace_ref": { + "name": "remote_workspace_ref", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "shared_workspace_key": { + "name": "shared_workspace_key", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "metadata": { + "name": "metadata", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "is_primary": { + "name": "is_primary", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "project_workspaces_company_project_idx": { + "name": "project_workspaces_company_project_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "project_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "project_workspaces_project_primary_idx": { + "name": "project_workspaces_project_primary_idx", + "columns": [ + { + "expression": "project_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "is_primary", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "project_workspaces_project_source_type_idx": { + "name": "project_workspaces_project_source_type_idx", + "columns": [ + { + "expression": "project_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "source_type", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "project_workspaces_company_shared_key_idx": { + "name": "project_workspaces_company_shared_key_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "shared_workspace_key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "project_workspaces_project_remote_ref_idx": { + "name": "project_workspaces_project_remote_ref_idx", + "columns": [ + { + "expression": "project_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "remote_provider", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "remote_workspace_ref", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "project_workspaces_company_id_companies_id_fk": { + "name": "project_workspaces_company_id_companies_id_fk", + "tableFrom": "project_workspaces", + "tableTo": "companies", + "columnsFrom": [ + "company_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "project_workspaces_project_id_projects_id_fk": { + "name": "project_workspaces_project_id_projects_id_fk", + "tableFrom": "project_workspaces", + "tableTo": "projects", + "columnsFrom": [ + "project_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.projects": { + "name": "projects", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "company_id": { + "name": "company_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "goal_id": { + "name": "goal_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'backlog'" + }, + "lead_agent_id": { + "name": "lead_agent_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "target_date": { + "name": "target_date", + "type": "date", + "primaryKey": false, + "notNull": false + }, + "color": { + "name": "color", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "env": { + "name": "env", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "pause_reason": { + "name": "pause_reason", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "paused_at": { + "name": "paused_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "execution_workspace_policy": { + "name": "execution_workspace_policy", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "archived_at": { + "name": "archived_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "projects_company_idx": { + "name": "projects_company_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "projects_company_id_companies_id_fk": { + "name": "projects_company_id_companies_id_fk", + "tableFrom": "projects", + "tableTo": "companies", + "columnsFrom": [ + "company_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "projects_goal_id_goals_id_fk": { + "name": "projects_goal_id_goals_id_fk", + "tableFrom": "projects", + "tableTo": "goals", + "columnsFrom": [ + "goal_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "projects_lead_agent_id_agents_id_fk": { + "name": "projects_lead_agent_id_agents_id_fk", + "tableFrom": "projects", + "tableTo": "agents", + "columnsFrom": [ + "lead_agent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.routine_runs": { + "name": "routine_runs", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "company_id": { + "name": "company_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "routine_id": { + "name": "routine_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "trigger_id": { + "name": "trigger_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "source": { + "name": "source", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'received'" + }, + "triggered_at": { + "name": "triggered_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "idempotency_key": { + "name": "idempotency_key", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "trigger_payload": { + "name": "trigger_payload", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "linked_issue_id": { + "name": "linked_issue_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "coalesced_into_run_id": { + "name": "coalesced_into_run_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "failure_reason": { + "name": "failure_reason", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "completed_at": { + "name": "completed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "routine_runs_company_routine_idx": { + "name": "routine_runs_company_routine_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "routine_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "routine_runs_trigger_idx": { + "name": "routine_runs_trigger_idx", + "columns": [ + { + "expression": "trigger_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "routine_runs_linked_issue_idx": { + "name": "routine_runs_linked_issue_idx", + "columns": [ + { + "expression": "linked_issue_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "routine_runs_trigger_idempotency_idx": { + "name": "routine_runs_trigger_idempotency_idx", + "columns": [ + { + "expression": "trigger_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "idempotency_key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "routine_runs_company_id_companies_id_fk": { + "name": "routine_runs_company_id_companies_id_fk", + "tableFrom": "routine_runs", + "tableTo": "companies", + "columnsFrom": [ + "company_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "routine_runs_routine_id_routines_id_fk": { + "name": "routine_runs_routine_id_routines_id_fk", + "tableFrom": "routine_runs", + "tableTo": "routines", + "columnsFrom": [ + "routine_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "routine_runs_trigger_id_routine_triggers_id_fk": { + "name": "routine_runs_trigger_id_routine_triggers_id_fk", + "tableFrom": "routine_runs", + "tableTo": "routine_triggers", + "columnsFrom": [ + "trigger_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "routine_runs_linked_issue_id_issues_id_fk": { + "name": "routine_runs_linked_issue_id_issues_id_fk", + "tableFrom": "routine_runs", + "tableTo": "issues", + "columnsFrom": [ + "linked_issue_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.routine_triggers": { + "name": "routine_triggers", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "company_id": { + "name": "company_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "routine_id": { + "name": "routine_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "kind": { + "name": "kind", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "label": { + "name": "label", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "enabled": { + "name": "enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "cron_expression": { + "name": "cron_expression", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "timezone": { + "name": "timezone", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "next_run_at": { + "name": "next_run_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "last_fired_at": { + "name": "last_fired_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "public_id": { + "name": "public_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "secret_id": { + "name": "secret_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "signing_mode": { + "name": "signing_mode", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "replay_window_sec": { + "name": "replay_window_sec", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "last_rotated_at": { + "name": "last_rotated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "last_result": { + "name": "last_result", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_by_agent_id": { + "name": "created_by_agent_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "created_by_user_id": { + "name": "created_by_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "updated_by_agent_id": { + "name": "updated_by_agent_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "updated_by_user_id": { + "name": "updated_by_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "routine_triggers_company_routine_idx": { + "name": "routine_triggers_company_routine_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "routine_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "routine_triggers_company_kind_idx": { + "name": "routine_triggers_company_kind_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "kind", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "routine_triggers_next_run_idx": { + "name": "routine_triggers_next_run_idx", + "columns": [ + { + "expression": "next_run_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "routine_triggers_public_id_idx": { + "name": "routine_triggers_public_id_idx", + "columns": [ + { + "expression": "public_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "routine_triggers_public_id_uq": { + "name": "routine_triggers_public_id_uq", + "columns": [ + { + "expression": "public_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "routine_triggers_company_id_companies_id_fk": { + "name": "routine_triggers_company_id_companies_id_fk", + "tableFrom": "routine_triggers", + "tableTo": "companies", + "columnsFrom": [ + "company_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "routine_triggers_routine_id_routines_id_fk": { + "name": "routine_triggers_routine_id_routines_id_fk", + "tableFrom": "routine_triggers", + "tableTo": "routines", + "columnsFrom": [ + "routine_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "routine_triggers_secret_id_company_secrets_id_fk": { + "name": "routine_triggers_secret_id_company_secrets_id_fk", + "tableFrom": "routine_triggers", + "tableTo": "company_secrets", + "columnsFrom": [ + "secret_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "routine_triggers_created_by_agent_id_agents_id_fk": { + "name": "routine_triggers_created_by_agent_id_agents_id_fk", + "tableFrom": "routine_triggers", + "tableTo": "agents", + "columnsFrom": [ + "created_by_agent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "routine_triggers_updated_by_agent_id_agents_id_fk": { + "name": "routine_triggers_updated_by_agent_id_agents_id_fk", + "tableFrom": "routine_triggers", + "tableTo": "agents", + "columnsFrom": [ + "updated_by_agent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.routines": { + "name": "routines", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "company_id": { + "name": "company_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "project_id": { + "name": "project_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "goal_id": { + "name": "goal_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "parent_issue_id": { + "name": "parent_issue_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "assignee_agent_id": { + "name": "assignee_agent_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "priority": { + "name": "priority", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'medium'" + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'active'" + }, + "concurrency_policy": { + "name": "concurrency_policy", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'coalesce_if_active'" + }, + "catch_up_policy": { + "name": "catch_up_policy", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'skip_missed'" + }, + "variables": { + "name": "variables", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + }, + "created_by_agent_id": { + "name": "created_by_agent_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "created_by_user_id": { + "name": "created_by_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "updated_by_agent_id": { + "name": "updated_by_agent_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "updated_by_user_id": { + "name": "updated_by_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "last_triggered_at": { + "name": "last_triggered_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "last_enqueued_at": { + "name": "last_enqueued_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "routines_company_status_idx": { + "name": "routines_company_status_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "routines_company_assignee_idx": { + "name": "routines_company_assignee_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "assignee_agent_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "routines_company_project_idx": { + "name": "routines_company_project_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "project_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "routines_company_id_companies_id_fk": { + "name": "routines_company_id_companies_id_fk", + "tableFrom": "routines", + "tableTo": "companies", + "columnsFrom": [ + "company_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "routines_project_id_projects_id_fk": { + "name": "routines_project_id_projects_id_fk", + "tableFrom": "routines", + "tableTo": "projects", + "columnsFrom": [ + "project_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "routines_goal_id_goals_id_fk": { + "name": "routines_goal_id_goals_id_fk", + "tableFrom": "routines", + "tableTo": "goals", + "columnsFrom": [ + "goal_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "routines_parent_issue_id_issues_id_fk": { + "name": "routines_parent_issue_id_issues_id_fk", + "tableFrom": "routines", + "tableTo": "issues", + "columnsFrom": [ + "parent_issue_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "routines_assignee_agent_id_agents_id_fk": { + "name": "routines_assignee_agent_id_agents_id_fk", + "tableFrom": "routines", + "tableTo": "agents", + "columnsFrom": [ + "assignee_agent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "routines_created_by_agent_id_agents_id_fk": { + "name": "routines_created_by_agent_id_agents_id_fk", + "tableFrom": "routines", + "tableTo": "agents", + "columnsFrom": [ + "created_by_agent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "routines_updated_by_agent_id_agents_id_fk": { + "name": "routines_updated_by_agent_id_agents_id_fk", + "tableFrom": "routines", + "tableTo": "agents", + "columnsFrom": [ + "updated_by_agent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.user_sidebar_preferences": { + "name": "user_sidebar_preferences", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "company_order": { + "name": "company_order", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "user_sidebar_preferences_user_uq": { + "name": "user_sidebar_preferences_user_uq", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.workspace_operations": { + "name": "workspace_operations", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "company_id": { + "name": "company_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "execution_workspace_id": { + "name": "execution_workspace_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "heartbeat_run_id": { + "name": "heartbeat_run_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "phase": { + "name": "phase", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "command": { + "name": "command", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "cwd": { + "name": "cwd", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'running'" + }, + "exit_code": { + "name": "exit_code", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "log_store": { + "name": "log_store", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "log_ref": { + "name": "log_ref", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "log_bytes": { + "name": "log_bytes", + "type": "bigint", + "primaryKey": false, + "notNull": false + }, + "log_sha256": { + "name": "log_sha256", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "log_compressed": { + "name": "log_compressed", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "stdout_excerpt": { + "name": "stdout_excerpt", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "stderr_excerpt": { + "name": "stderr_excerpt", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "metadata": { + "name": "metadata", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "started_at": { + "name": "started_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "finished_at": { + "name": "finished_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "workspace_operations_company_run_started_idx": { + "name": "workspace_operations_company_run_started_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "heartbeat_run_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "started_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workspace_operations_company_workspace_started_idx": { + "name": "workspace_operations_company_workspace_started_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "execution_workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "started_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "workspace_operations_company_id_companies_id_fk": { + "name": "workspace_operations_company_id_companies_id_fk", + "tableFrom": "workspace_operations", + "tableTo": "companies", + "columnsFrom": [ + "company_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "workspace_operations_execution_workspace_id_execution_workspaces_id_fk": { + "name": "workspace_operations_execution_workspace_id_execution_workspaces_id_fk", + "tableFrom": "workspace_operations", + "tableTo": "execution_workspaces", + "columnsFrom": [ + "execution_workspace_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "workspace_operations_heartbeat_run_id_heartbeat_runs_id_fk": { + "name": "workspace_operations_heartbeat_run_id_heartbeat_runs_id_fk", + "tableFrom": "workspace_operations", + "tableTo": "heartbeat_runs", + "columnsFrom": [ + "heartbeat_run_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.workspace_runtime_services": { + "name": "workspace_runtime_services", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true + }, + "company_id": { + "name": "company_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "project_id": { + "name": "project_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "project_workspace_id": { + "name": "project_workspace_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "execution_workspace_id": { + "name": "execution_workspace_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "issue_id": { + "name": "issue_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "scope_type": { + "name": "scope_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "scope_id": { + "name": "scope_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "service_name": { + "name": "service_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "lifecycle": { + "name": "lifecycle", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "reuse_key": { + "name": "reuse_key", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "command": { + "name": "command", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "cwd": { + "name": "cwd", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "port": { + "name": "port", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "url": { + "name": "url", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "provider": { + "name": "provider", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "provider_ref": { + "name": "provider_ref", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "owner_agent_id": { + "name": "owner_agent_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "started_by_run_id": { + "name": "started_by_run_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "last_used_at": { + "name": "last_used_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "started_at": { + "name": "started_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "stopped_at": { + "name": "stopped_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "stop_policy": { + "name": "stop_policy", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "health_status": { + "name": "health_status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'unknown'" + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "workspace_runtime_services_company_workspace_status_idx": { + "name": "workspace_runtime_services_company_workspace_status_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "project_workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workspace_runtime_services_company_execution_workspace_status_idx": { + "name": "workspace_runtime_services_company_execution_workspace_status_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "execution_workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workspace_runtime_services_company_project_status_idx": { + "name": "workspace_runtime_services_company_project_status_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "project_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workspace_runtime_services_run_idx": { + "name": "workspace_runtime_services_run_idx", + "columns": [ + { + "expression": "started_by_run_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workspace_runtime_services_company_updated_idx": { + "name": "workspace_runtime_services_company_updated_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "updated_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "workspace_runtime_services_company_id_companies_id_fk": { + "name": "workspace_runtime_services_company_id_companies_id_fk", + "tableFrom": "workspace_runtime_services", + "tableTo": "companies", + "columnsFrom": [ + "company_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "workspace_runtime_services_project_id_projects_id_fk": { + "name": "workspace_runtime_services_project_id_projects_id_fk", + "tableFrom": "workspace_runtime_services", + "tableTo": "projects", + "columnsFrom": [ + "project_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "workspace_runtime_services_project_workspace_id_project_workspaces_id_fk": { + "name": "workspace_runtime_services_project_workspace_id_project_workspaces_id_fk", + "tableFrom": "workspace_runtime_services", + "tableTo": "project_workspaces", + "columnsFrom": [ + "project_workspace_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "workspace_runtime_services_execution_workspace_id_execution_workspaces_id_fk": { + "name": "workspace_runtime_services_execution_workspace_id_execution_workspaces_id_fk", + "tableFrom": "workspace_runtime_services", + "tableTo": "execution_workspaces", + "columnsFrom": [ + "execution_workspace_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "workspace_runtime_services_issue_id_issues_id_fk": { + "name": "workspace_runtime_services_issue_id_issues_id_fk", + "tableFrom": "workspace_runtime_services", + "tableTo": "issues", + "columnsFrom": [ + "issue_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "workspace_runtime_services_owner_agent_id_agents_id_fk": { + "name": "workspace_runtime_services_owner_agent_id_agents_id_fk", + "tableFrom": "workspace_runtime_services", + "tableTo": "agents", + "columnsFrom": [ + "owner_agent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "workspace_runtime_services_started_by_run_id_heartbeat_runs_id_fk": { + "name": "workspace_runtime_services_started_by_run_id_heartbeat_runs_id_fk", + "tableFrom": "workspace_runtime_services", + "tableTo": "heartbeat_runs", + "columnsFrom": [ + "started_by_run_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + } + }, + "enums": {}, + "schemas": {}, + "sequences": {}, + "roles": {}, + "policies": {}, + "views": {}, + "_meta": { + "columns": {}, + "schemas": {}, + "tables": {} + } +} \ No newline at end of file diff --git a/packages/db/src/migrations/meta/_journal.json b/packages/db/src/migrations/meta/_journal.json index 82dffcf5..bce93f11 100644 --- a/packages/db/src/migrations/meta/_journal.json +++ b/packages/db/src/migrations/meta/_journal.json @@ -393,6 +393,13 @@ "when": 1775825256196, "tag": "0055_kind_weapon_omega", "breakpoints": true + }, + { + "idx": 56, + "version": "7", + "when": 1776084034244, + "tag": "0056_spooky_ultragirl", + "breakpoints": true } ] } \ No newline at end of file diff --git a/packages/db/src/schema/company_user_sidebar_preferences.ts b/packages/db/src/schema/company_user_sidebar_preferences.ts new file mode 100644 index 00000000..7d776875 --- /dev/null +++ b/packages/db/src/schema/company_user_sidebar_preferences.ts @@ -0,0 +1,22 @@ +import { pgTable, uuid, text, timestamp, jsonb, uniqueIndex, index } from "drizzle-orm/pg-core"; +import { companies } from "./companies.js"; + +export const companyUserSidebarPreferences = pgTable( + "company_user_sidebar_preferences", + { + id: uuid("id").primaryKey().defaultRandom(), + companyId: uuid("company_id").notNull().references(() => companies.id, { onDelete: "cascade" }), + userId: text("user_id").notNull(), + projectOrder: jsonb("project_order").$type().notNull().default([]), + createdAt: timestamp("created_at", { withTimezone: true }).notNull().defaultNow(), + updatedAt: timestamp("updated_at", { withTimezone: true }).notNull().defaultNow(), + }, + (table) => ({ + companyIdx: index("company_user_sidebar_preferences_company_idx").on(table.companyId), + userIdx: index("company_user_sidebar_preferences_user_idx").on(table.userId), + companyUserUq: uniqueIndex("company_user_sidebar_preferences_company_user_uq").on( + table.companyId, + table.userId, + ), + }), +); diff --git a/packages/db/src/schema/index.ts b/packages/db/src/schema/index.ts index 1f86ca67..4814303c 100644 --- a/packages/db/src/schema/index.ts +++ b/packages/db/src/schema/index.ts @@ -3,10 +3,12 @@ export { companyLogos } from "./company_logos.js"; export { authUsers, authSessions, authAccounts, authVerifications } from "./auth.js"; export { instanceSettings } from "./instance_settings.js"; export { instanceUserRoles } from "./instance_user_roles.js"; +export { userSidebarPreferences } from "./user_sidebar_preferences.js"; export { agents } from "./agents.js"; export { boardApiKeys } from "./board_api_keys.js"; export { cliAuthChallenges } from "./cli_auth_challenges.js"; export { companyMemberships } from "./company_memberships.js"; +export { companyUserSidebarPreferences } from "./company_user_sidebar_preferences.js"; export { principalPermissionGrants } from "./principal_permission_grants.js"; export { invites } from "./invites.js"; export { joinRequests } from "./join_requests.js"; diff --git a/packages/db/src/schema/user_sidebar_preferences.ts b/packages/db/src/schema/user_sidebar_preferences.ts new file mode 100644 index 00000000..759f4718 --- /dev/null +++ b/packages/db/src/schema/user_sidebar_preferences.ts @@ -0,0 +1,15 @@ +import { pgTable, uuid, text, timestamp, jsonb, uniqueIndex } from "drizzle-orm/pg-core"; + +export const userSidebarPreferences = pgTable( + "user_sidebar_preferences", + { + id: uuid("id").primaryKey().defaultRandom(), + userId: text("user_id").notNull(), + companyOrder: jsonb("company_order").$type().notNull().default([]), + createdAt: timestamp("created_at", { withTimezone: true }).notNull().defaultNow(), + updatedAt: timestamp("updated_at", { withTimezone: true }).notNull().defaultNow(), + }, + (table) => ({ + userUq: uniqueIndex("user_sidebar_preferences_user_uq").on(table.userId), + }), +); diff --git a/packages/shared/src/api.ts b/packages/shared/src/api.ts index 0414bda1..ea3278be 100644 --- a/packages/shared/src/api.ts +++ b/packages/shared/src/api.ts @@ -13,6 +13,7 @@ export const API = { activity: `${API_PREFIX}/activity`, dashboard: `${API_PREFIX}/dashboard`, sidebarBadges: `${API_PREFIX}/sidebar-badges`, + sidebarPreferences: `${API_PREFIX}/sidebar-preferences`, invites: `${API_PREFIX}/invites`, joinRequests: `${API_PREFIX}/join-requests`, members: `${API_PREFIX}/members`, diff --git a/packages/shared/src/index.ts b/packages/shared/src/index.ts index 8ec63026..0cc99826 100644 --- a/packages/shared/src/index.ts +++ b/packages/shared/src/index.ts @@ -232,7 +232,11 @@ export type { ExecutionWorkspaceCloseReadiness, ExecutionWorkspaceCloseReadinessState, ProjectWorkspaceRuntimeConfig, + WorkspaceCommandDefinition, + WorkspaceCommandKind, + WorkspaceRuntimeControlTarget, WorkspaceRuntimeService, + WorkspaceRuntimeServiceStateMap, WorkspaceOperation, WorkspaceOperationPhase, WorkspaceOperationStatus, @@ -301,6 +305,7 @@ export type { DashboardSummary, ActivityEvent, SidebarBadges, + SidebarOrderPreference, InboxDismissal, CompanyMembership, PrincipalPermissionGrant, @@ -374,6 +379,21 @@ export type { ProviderQuotaResult, } from "./types/index.js"; +export { + sidebarOrderPreferenceSchema, + upsertSidebarOrderPreferenceSchema, + type UpsertSidebarOrderPreference, +} from "./validators/sidebar-preferences.js"; + +export { workspaceRuntimeControlTargetSchema } from "./validators/execution-workspace.js"; +export { + findWorkspaceCommandDefinition, + listWorkspaceCommandDefinitions, + listWorkspaceServiceCommandDefinitions, + matchWorkspaceRuntimeServiceToCommand, + scoreWorkspaceRuntimeServiceMatch, +} from "./workspace-commands.js"; + export { DEFAULT_FEEDBACK_DATA_SHARING_PREFERENCE, FEEDBACK_TARGET_TYPES, diff --git a/packages/shared/src/types/index.ts b/packages/shared/src/types/index.ts index 13625a68..6c38141d 100644 --- a/packages/shared/src/types/index.ts +++ b/packages/shared/src/types/index.ts @@ -71,7 +71,11 @@ export type { ExecutionWorkspaceCloseReadiness, ExecutionWorkspaceCloseReadinessState, ProjectWorkspaceRuntimeConfig, + WorkspaceCommandDefinition, + WorkspaceCommandKind, + WorkspaceRuntimeControlTarget, WorkspaceRuntimeService, + WorkspaceRuntimeServiceStateMap, WorkspaceRuntimeDesiredState, ExecutionWorkspaceStrategyType, ExecutionWorkspaceMode, @@ -165,6 +169,7 @@ export type { LiveEvent } from "./live.js"; export type { DashboardSummary } from "./dashboard.js"; export type { ActivityEvent } from "./activity.js"; export type { SidebarBadges } from "./sidebar-badges.js"; +export type { SidebarOrderPreference } from "./sidebar-preferences.js"; export type { InboxDismissal } from "./inbox-dismissal.js"; export type { CompanyMembership, diff --git a/packages/shared/src/types/sidebar-preferences.ts b/packages/shared/src/types/sidebar-preferences.ts new file mode 100644 index 00000000..457b5f0b --- /dev/null +++ b/packages/shared/src/types/sidebar-preferences.ts @@ -0,0 +1,4 @@ +export interface SidebarOrderPreference { + orderedIds: string[]; + updatedAt: Date | null; +} diff --git a/packages/shared/src/types/workspace-runtime.ts b/packages/shared/src/types/workspace-runtime.ts index 2b2c4e2d..9e287200 100644 --- a/packages/shared/src/types/workspace-runtime.ts +++ b/packages/shared/src/types/workspace-runtime.ts @@ -46,6 +46,27 @@ export type ExecutionWorkspaceCloseActionKind = | "remove_local_directory"; export type WorkspaceRuntimeDesiredState = "running" | "stopped"; +export type WorkspaceRuntimeServiceStateMap = Record; +export type WorkspaceCommandKind = "service" | "job"; + +export interface WorkspaceCommandSource { + type: "paperclip"; + key: "commands" | "services" | "jobs"; + index: number; +} + +export interface WorkspaceCommandDefinition { + id: string; + name: string; + kind: WorkspaceCommandKind; + command: string | null; + cwd: string | null; + lifecycle: "shared" | "ephemeral" | null; + serviceIndex: number | null; + disabledReason: string | null; + rawConfig: Record; + source: WorkspaceCommandSource; +} export interface ExecutionWorkspaceStrategy { type: ExecutionWorkspaceStrategyType; @@ -62,11 +83,19 @@ export interface ExecutionWorkspaceConfig { cleanupCommand: string | null; workspaceRuntime: Record | null; desiredState: WorkspaceRuntimeDesiredState | null; + serviceStates?: WorkspaceRuntimeServiceStateMap | null; } export interface ProjectWorkspaceRuntimeConfig { workspaceRuntime: Record | null; desiredState: WorkspaceRuntimeDesiredState | null; + serviceStates?: WorkspaceRuntimeServiceStateMap | null; +} + +export interface WorkspaceRuntimeControlTarget { + workspaceCommandId?: string | null; + runtimeServiceId?: string | null; + serviceIndex?: number | null; } export interface ExecutionWorkspaceCloseAction { @@ -187,6 +216,7 @@ export interface WorkspaceRuntimeService { stoppedAt: Date | null; stopPolicy: Record | null; healthStatus: "unknown" | "healthy" | "unhealthy"; + configIndex?: number | null; createdAt: Date; updatedAt: Date; } diff --git a/packages/shared/src/validators/execution-workspace.ts b/packages/shared/src/validators/execution-workspace.ts index 9914d74e..fad63823 100644 --- a/packages/shared/src/validators/execution-workspace.ts +++ b/packages/shared/src/validators/execution-workspace.ts @@ -14,6 +14,13 @@ export const executionWorkspaceConfigSchema = z.object({ cleanupCommand: z.string().optional().nullable(), workspaceRuntime: z.record(z.unknown()).optional().nullable(), desiredState: z.enum(["running", "stopped"]).optional().nullable(), + serviceStates: z.record(z.enum(["running", "stopped"])).optional().nullable(), +}).strict(); + +export const workspaceRuntimeControlTargetSchema = z.object({ + workspaceCommandId: z.string().min(1).optional().nullable(), + runtimeServiceId: z.string().uuid().optional().nullable(), + serviceIndex: z.number().int().nonnegative().optional().nullable(), }).strict(); export const executionWorkspaceCloseReadinessStateSchema = z.enum([ @@ -88,6 +95,7 @@ export const workspaceRuntimeServiceSchema = z.object({ stoppedAt: z.coerce.date().nullable(), stopPolicy: z.record(z.unknown()).nullable(), healthStatus: z.enum(["unknown", "healthy", "unhealthy"]), + configIndex: z.number().int().nonnegative().nullable().optional(), createdAt: z.coerce.date(), updatedAt: z.coerce.date(), }).strict(); diff --git a/packages/shared/src/validators/index.ts b/packages/shared/src/validators/index.ts index aca19625..4f8e659b 100644 --- a/packages/shared/src/validators/index.ts +++ b/packages/shared/src/validators/index.ts @@ -32,6 +32,11 @@ export { upsertIssueFeedbackVoteSchema, type UpsertIssueFeedbackVote, } from "./feedback.js"; +export { + sidebarOrderPreferenceSchema, + upsertSidebarOrderPreferenceSchema, + type UpsertSidebarOrderPreference, +} from "./sidebar-preferences.js"; export { companySkillSourceTypeSchema, companySkillTrustLevelSchema, diff --git a/packages/shared/src/validators/project.ts b/packages/shared/src/validators/project.ts index f0030fb7..8e8549ca 100644 --- a/packages/shared/src/validators/project.ts +++ b/packages/shared/src/validators/project.ts @@ -31,6 +31,7 @@ export const projectExecutionWorkspacePolicySchema = z export const projectWorkspaceRuntimeConfigSchema = z.object({ workspaceRuntime: z.record(z.unknown()).optional().nullable(), desiredState: z.enum(["running", "stopped"]).optional().nullable(), + serviceStates: z.record(z.enum(["running", "stopped"])).optional().nullable(), }).strict(); const projectWorkspaceSourceTypeSchema = z.enum(["local_path", "git_repo", "remote_managed", "non_git_path"]); diff --git a/packages/shared/src/validators/sidebar-preferences.ts b/packages/shared/src/validators/sidebar-preferences.ts new file mode 100644 index 00000000..a8d109d5 --- /dev/null +++ b/packages/shared/src/validators/sidebar-preferences.ts @@ -0,0 +1,14 @@ +import { z } from "zod"; + +const sidebarOrderedIdSchema = z.string().uuid(); + +export const sidebarOrderPreferenceSchema = z.object({ + orderedIds: z.array(sidebarOrderedIdSchema), + updatedAt: z.coerce.date().nullable(), +}); + +export const upsertSidebarOrderPreferenceSchema = z.object({ + orderedIds: z.array(sidebarOrderedIdSchema), +}); + +export type UpsertSidebarOrderPreference = z.infer; diff --git a/packages/shared/src/workspace-commands.test.ts b/packages/shared/src/workspace-commands.test.ts new file mode 100644 index 00000000..1fc74478 --- /dev/null +++ b/packages/shared/src/workspace-commands.test.ts @@ -0,0 +1,56 @@ +import { describe, expect, it } from "vitest"; +import { + findWorkspaceCommandDefinition, + listWorkspaceCommandDefinitions, + matchWorkspaceRuntimeServiceToCommand, +} from "./workspace-commands.js"; + +describe("workspace command helpers", () => { + it("derives service and job commands from command-first runtime config", () => { + const commands = listWorkspaceCommandDefinitions({ + commands: [ + { id: "web", name: "web", kind: "service", command: "pnpm dev" }, + { id: "db-migrate", name: "db:migrate", kind: "job", command: "pnpm db:migrate" }, + ], + }); + + expect(commands).toEqual([ + expect.objectContaining({ id: "web", kind: "service", serviceIndex: 0 }), + expect.objectContaining({ id: "db-migrate", kind: "job", serviceIndex: null }), + ]); + }); + + it("falls back to legacy services and jobs arrays", () => { + const commands = listWorkspaceCommandDefinitions({ + services: [{ name: "web", command: "pnpm dev" }], + jobs: [{ name: "lint", command: "pnpm lint" }], + }); + + expect(commands).toEqual([ + expect.objectContaining({ id: "service:web", kind: "service", serviceIndex: 0 }), + expect.objectContaining({ id: "job:lint", kind: "job", serviceIndex: null }), + ]); + }); + + it("matches a configured service command to the current runtime service", () => { + const workspaceRuntime = { + commands: [ + { id: "web", name: "web", kind: "service", command: "pnpm dev", cwd: "." }, + ], + }; + const command = findWorkspaceCommandDefinition(workspaceRuntime, "web"); + expect(command).not.toBeNull(); + + const match = matchWorkspaceRuntimeServiceToCommand(command!, [ + { + id: "runtime-web", + serviceName: "web", + command: "pnpm dev", + cwd: "/repo", + configIndex: null, + }, + ]); + + expect(match).toEqual(expect.objectContaining({ id: "runtime-web" })); + }); +}); diff --git a/packages/shared/src/workspace-commands.ts b/packages/shared/src/workspace-commands.ts new file mode 100644 index 00000000..0510d3fb --- /dev/null +++ b/packages/shared/src/workspace-commands.ts @@ -0,0 +1,204 @@ +import type { WorkspaceCommandDefinition, WorkspaceRuntimeService } from "./types/workspace-runtime.js"; + +function isRecord(value: unknown): value is Record { + return typeof value === "object" && value !== null && !Array.isArray(value); +} + +function readNonEmptyString(value: unknown): string | null { + if (typeof value !== "string") return null; + const trimmed = value.trim(); + return trimmed.length > 0 ? trimmed : null; +} + +function slugify(value: string | null | undefined) { + const normalized = (value ?? "") + .trim() + .toLowerCase() + .replace(/[^a-z0-9]+/g, "-") + .replace(/-+/g, "-") + .replace(/^-+|-+$/g, ""); + return normalized.length > 0 ? normalized : null; +} + +function deriveWorkspaceCommandId(input: { + kind: WorkspaceCommandDefinition["kind"]; + explicitId: string | null; + name: string; + index: number; +}) { + const explicitId = slugify(input.explicitId); + if (explicitId) return explicitId; + const nameSlug = slugify(input.name); + return nameSlug ? `${input.kind}:${nameSlug}` : `${input.kind}:${input.index + 1}`; +} + +function buildWorkspaceCommandDefinition(input: { + entry: Record; + kind: WorkspaceCommandDefinition["kind"]; + sourceKey: WorkspaceCommandDefinition["source"]["key"]; + sourceIndex: number; + serviceIndex: number | null; + fallbackName: string; +}): WorkspaceCommandDefinition { + return { + id: deriveWorkspaceCommandId({ + kind: input.kind, + explicitId: readNonEmptyString(input.entry.id), + name: + readNonEmptyString(input.entry.name) + ?? readNonEmptyString(input.entry.label) + ?? readNonEmptyString(input.entry.title) + ?? input.fallbackName, + index: input.sourceIndex, + }), + name: + readNonEmptyString(input.entry.name) + ?? readNonEmptyString(input.entry.label) + ?? readNonEmptyString(input.entry.title) + ?? input.fallbackName, + kind: input.kind, + command: readNonEmptyString(input.entry.command), + cwd: readNonEmptyString(input.entry.cwd), + lifecycle: + input.kind === "service" + ? input.entry.lifecycle === "ephemeral" + ? "ephemeral" + : "shared" + : null, + serviceIndex: input.serviceIndex, + disabledReason: readNonEmptyString(input.entry.disabledReason), + rawConfig: { ...input.entry }, + source: { + type: "paperclip", + key: input.sourceKey, + index: input.sourceIndex, + }, + }; +} + +function uniqueWorkspaceCommandId( + seen: Set, + commandId: string, + sourceKey: WorkspaceCommandDefinition["source"]["key"], + sourceIndex: number, +) { + if (!seen.has(commandId)) { + seen.add(commandId); + return commandId; + } + const fallbackId = `${commandId}-${sourceKey}-${sourceIndex + 1}`; + seen.add(fallbackId); + return fallbackId; +} + +function readCommandEntries( + workspaceRuntime: Record | null | undefined, + key: "commands" | "services" | "jobs", +) { + const raw = workspaceRuntime?.[key]; + return Array.isArray(raw) ? raw.filter((entry): entry is Record => isRecord(entry)) : []; +} + +export function listWorkspaceCommandDefinitions( + workspaceRuntime: Record | null | undefined, +): WorkspaceCommandDefinition[] { + if (!workspaceRuntime) return []; + + const commandEntries = readCommandEntries(workspaceRuntime, "commands"); + const seenIds = new Set(); + let nextServiceIndex = 0; + + const finalize = (command: WorkspaceCommandDefinition) => ({ + ...command, + id: uniqueWorkspaceCommandId(seenIds, command.id, command.source.key, command.source.index), + }); + + if (commandEntries.length > 0) { + return commandEntries.map((entry, index) => + finalize(buildWorkspaceCommandDefinition({ + entry, + kind: entry.kind === "job" ? "job" : "service", + sourceKey: "commands", + sourceIndex: index, + serviceIndex: entry.kind === "job" ? null : nextServiceIndex++, + fallbackName: entry.kind === "job" ? `Job ${index + 1}` : `Service ${index + 1}`, + }))); + } + + const serviceDefinitions = readCommandEntries(workspaceRuntime, "services").map((entry, index) => + finalize(buildWorkspaceCommandDefinition({ + entry, + kind: "service", + sourceKey: "services", + sourceIndex: index, + serviceIndex: nextServiceIndex++, + fallbackName: `Service ${index + 1}`, + }))); + const jobDefinitions = readCommandEntries(workspaceRuntime, "jobs").map((entry, index) => + finalize(buildWorkspaceCommandDefinition({ + entry, + kind: "job", + sourceKey: "jobs", + sourceIndex: index, + serviceIndex: null, + fallbackName: `Job ${index + 1}`, + }))); + + return [...serviceDefinitions, ...jobDefinitions]; +} + +export function listWorkspaceServiceCommandDefinitions( + workspaceRuntime: Record | null | undefined, +) { + return listWorkspaceCommandDefinitions(workspaceRuntime).filter((command) => command.kind === "service"); +} + +export function findWorkspaceCommandDefinition( + workspaceRuntime: Record | null | undefined, + workspaceCommandId: string | null | undefined, +) { + const normalizedId = readNonEmptyString(workspaceCommandId); + if (!normalizedId) return null; + return listWorkspaceCommandDefinitions(workspaceRuntime).find((command) => command.id === normalizedId) ?? null; +} + +export function scoreWorkspaceRuntimeServiceMatch( + command: Pick, + runtimeService: Pick, +) { + if (command.serviceIndex !== null && runtimeService.configIndex !== null && runtimeService.configIndex !== undefined) { + return runtimeService.configIndex === command.serviceIndex ? 100 : -1; + } + + let score = 0; + if (runtimeService.serviceName === command.name) score += 4; + if ((runtimeService.command ?? null) === (command.command ?? null)) score += 4; + if ( + command.cwd + && runtimeService.cwd + && (runtimeService.cwd === command.cwd || runtimeService.cwd.endsWith(`/${command.cwd}`)) + ) { + score += 2; + } + return score; +} + +export function matchWorkspaceRuntimeServiceToCommand< + T extends Pick, +>( + command: Pick, + runtimeServices: T[] | null | undefined, +) { + let bestMatch: T | null = null; + let bestScore = -1; + + for (const runtimeService of runtimeServices ?? []) { + const score = scoreWorkspaceRuntimeServiceMatch(command, runtimeService); + if (score > bestScore) { + bestMatch = runtimeService; + bestScore = score; + } + } + + return bestScore > 0 ? bestMatch : null; +} diff --git a/scripts/provision-worktree.sh b/scripts/provision-worktree.sh index 9fbd3ccc..ba90e9ff 100644 --- a/scripts/provision-worktree.sh +++ b/scripts/provision-worktree.sh @@ -53,6 +53,24 @@ run_paperclipai_command() { return 1 } +paperclipai_command_available() { + if command -v pnpm >/dev/null 2>&1 && pnpm paperclipai --help >/dev/null 2>&1; then + return 0 + fi + + local base_cli_tsx_path="$base_cwd/cli/node_modules/tsx/dist/cli.mjs" + local base_cli_entry_path="$base_cwd/cli/src/index.ts" + if command -v node >/dev/null 2>&1 && [[ -f "$base_cli_tsx_path" ]] && [[ -f "$base_cli_entry_path" ]]; then + return 0 + fi + + if command -v paperclipai >/dev/null 2>&1; then + return 0 + fi + + return 1 +} + run_isolated_worktree_init() { run_paperclipai_command \ worktree \ @@ -318,7 +336,9 @@ main().catch((error) => { EOF } -if ! run_isolated_worktree_init; then +if paperclipai_command_available; then + run_isolated_worktree_init +else echo "paperclipai CLI not available in this workspace; writing isolated fallback config without DB seeding." >&2 write_fallback_worktree_config fi @@ -384,14 +404,49 @@ if [[ -f "$worktree_cwd/package.json" && -f "$worktree_cwd/pnpm-lock.yaml" ]]; t done } - ( - cd "$worktree_cwd" - pnpm install --frozen-lockfile - ) || { - restore_moved_symlinks - exit 1 + run_pnpm_install() { + local stdout_path stderr_path + stdout_path="$(mktemp)" + stderr_path="$(mktemp)" + + if ( + cd "$worktree_cwd" + pnpm install "$@" + ) >"$stdout_path" 2>"$stderr_path"; then + cat "$stdout_path" + cat "$stderr_path" >&2 + rm -f "$stdout_path" "$stderr_path" + return 0 + fi + + local exit_code=$? + cat "$stdout_path" + cat "$stderr_path" >&2 + if grep -q "ERR_PNPM_OUTDATED_LOCKFILE" "$stdout_path" "$stderr_path"; then + rm -f "$stdout_path" "$stderr_path" + return 90 + fi + + rm -f "$stdout_path" "$stderr_path" + return "$exit_code" } + if run_pnpm_install --frozen-lockfile; then + : + else + install_exit_code=$? + if [[ "$install_exit_code" -eq 90 ]]; then + echo "pnpm-lock.yaml is out of date in this execution workspace; retrying install without --frozen-lockfile." >&2 + run_pnpm_install --no-frozen-lockfile || { + restore_moved_symlinks + exit 1 + } + else + restore_moved_symlinks + exit "$install_exit_code" + fi + fi + cleanup_moved_symlinks fi diff --git a/server/src/__tests__/execution-workspaces-service.test.ts b/server/src/__tests__/execution-workspaces-service.test.ts index ca6b38d5..dbeef472 100644 --- a/server/src/__tests__/execution-workspaces-service.test.ts +++ b/server/src/__tests__/execution-workspaces-service.test.ts @@ -43,6 +43,7 @@ describe("execution workspace config helpers", () => { teardownCommand: "bash ./scripts/teardown-worktree.sh", cleanupCommand: "pkill -f vite || true", desiredState: null, + serviceStates: null, workspaceRuntime: { services: [{ name: "web", command: "pnpm dev", port: 3100 }], }, @@ -73,6 +74,7 @@ describe("execution workspace config helpers", () => { teardownCommand: "bash ./scripts/teardown-worktree.sh", cleanupCommand: "pkill -f vite || true", desiredState: null, + serviceStates: null, workspaceRuntime: { services: [{ name: "web", command: "pnpm dev" }], }, diff --git a/server/src/__tests__/sidebar-preferences-routes.test.ts b/server/src/__tests__/sidebar-preferences-routes.test.ts new file mode 100644 index 00000000..b5166f0b --- /dev/null +++ b/server/src/__tests__/sidebar-preferences-routes.test.ts @@ -0,0 +1,166 @@ +import express from "express"; +import request from "supertest"; +import { beforeEach, describe, expect, it, vi } from "vitest"; +import { errorHandler } from "../middleware/index.js"; +import { sidebarPreferenceRoutes } from "../routes/sidebar-preferences.js"; + +const mockSidebarPreferenceService = vi.hoisted(() => ({ + getCompanyOrder: vi.fn(), + upsertCompanyOrder: vi.fn(), + getProjectOrder: vi.fn(), + upsertProjectOrder: vi.fn(), +})); +const mockLogActivity = vi.hoisted(() => vi.fn()); + +vi.mock("../services/index.js", () => ({ + sidebarPreferenceService: () => mockSidebarPreferenceService, + logActivity: mockLogActivity, +})); + +function createApp(actor: Record) { + const app = express(); + app.use(express.json()); + app.use((req, _res, next) => { + req.actor = actor as never; + next(); + }); + app.use("/api", sidebarPreferenceRoutes({} as never)); + app.use(errorHandler); + return app; +} + +const ORDERED_IDS = [ + "11111111-1111-4111-8111-111111111111", + "22222222-2222-4222-8222-222222222222", +]; + +describe("sidebar preference routes", () => { + beforeEach(() => { + vi.clearAllMocks(); + mockSidebarPreferenceService.getCompanyOrder.mockResolvedValue({ + orderedIds: ORDERED_IDS, + updatedAt: null, + }); + mockSidebarPreferenceService.upsertCompanyOrder.mockResolvedValue({ + orderedIds: ORDERED_IDS, + updatedAt: null, + }); + mockSidebarPreferenceService.getProjectOrder.mockResolvedValue({ + orderedIds: ORDERED_IDS, + updatedAt: null, + }); + mockSidebarPreferenceService.upsertProjectOrder.mockResolvedValue({ + orderedIds: ORDERED_IDS, + updatedAt: null, + }); + }); + + it("returns company rail order for board users", async () => { + const app = createApp({ + type: "board", + userId: "user-1", + source: "session", + isInstanceAdmin: false, + companyIds: ["company-1"], + }); + + const res = await request(app).get("/api/sidebar-preferences/me"); + + expect(res.status).toBe(200); + expect(res.body).toEqual({ + orderedIds: ORDERED_IDS, + updatedAt: null, + }); + expect(mockSidebarPreferenceService.getCompanyOrder).toHaveBeenCalledWith("user-1"); + }); + + it("updates company rail order for board users", async () => { + const app = createApp({ + type: "board", + userId: "user-1", + source: "local_implicit", + isInstanceAdmin: true, + companyIds: ["company-1"], + }); + + const res = await request(app) + .put("/api/sidebar-preferences/me") + .send({ orderedIds: ORDERED_IDS }); + + expect(res.status).toBe(200); + expect(mockSidebarPreferenceService.upsertCompanyOrder).toHaveBeenCalledWith("user-1", ORDERED_IDS); + }); + + it("returns project order for companies the board user can access", async () => { + const app = createApp({ + type: "board", + userId: "user-1", + source: "session", + isInstanceAdmin: false, + companyIds: ["company-1"], + }); + + const res = await request(app).get("/api/companies/company-1/sidebar-preferences/me"); + + expect(res.status).toBe(200); + expect(mockSidebarPreferenceService.getProjectOrder).toHaveBeenCalledWith("company-1", "user-1"); + }); + + it("logs project order updates for company-scoped writes", async () => { + const app = createApp({ + type: "board", + userId: "user-1", + source: "session", + isInstanceAdmin: false, + companyIds: ["company-1"], + runId: "run-1", + }); + + const res = await request(app) + .put("/api/companies/company-1/sidebar-preferences/me") + .send({ orderedIds: ORDERED_IDS }); + + expect(res.status).toBe(200); + expect(mockSidebarPreferenceService.upsertProjectOrder).toHaveBeenCalledWith("company-1", "user-1", ORDERED_IDS); + expect(mockLogActivity).toHaveBeenCalledWith( + {} as never, + expect.objectContaining({ + companyId: "company-1", + action: "sidebar_preferences.project_order_updated", + details: expect.objectContaining({ + userId: "user-1", + orderedIds: ORDERED_IDS, + }), + }), + ); + }); + + it("rejects company-scoped reads when the board user lacks company access", async () => { + const app = createApp({ + type: "board", + userId: "user-1", + source: "session", + isInstanceAdmin: false, + companyIds: ["company-2"], + }); + + const res = await request(app).get("/api/companies/company-1/sidebar-preferences/me"); + + expect(res.status).toBe(403); + expect(mockSidebarPreferenceService.getProjectOrder).not.toHaveBeenCalled(); + }); + + it("rejects agent callers", async () => { + const app = createApp({ + type: "agent", + agentId: "agent-1", + companyId: "company-1", + source: "agent_key", + }); + + const res = await request(app).get("/api/sidebar-preferences/me"); + + expect(res.status).toBe(403); + expect(mockSidebarPreferenceService.getCompanyOrder).not.toHaveBeenCalled(); + }); +}); diff --git a/server/src/__tests__/workspace-runtime.test.ts b/server/src/__tests__/workspace-runtime.test.ts index e1908c5f..e44f40da 100644 --- a/server/src/__tests__/workspace-runtime.test.ts +++ b/server/src/__tests__/workspace-runtime.test.ts @@ -19,16 +19,21 @@ import { } from "@paperclipai/db"; import { eq } from "drizzle-orm"; import { + buildWorkspaceRuntimeDesiredStatePatch, cleanupExecutionWorkspaceArtifacts, + ensurePersistedExecutionWorkspaceAvailable, ensureServerWorkspaceLinksCurrent, ensureRuntimeServicesForRun, + listConfiguredRuntimeServiceEntries, normalizeAdapterManagedRuntimeServices, reconcilePersistedRuntimeServicesOnStartup, realizeExecutionWorkspace, releaseRuntimeServicesForRun, resetRuntimeServicesForTests, + resolveWorkspaceRuntimeReadinessTimeoutSec, resolveShell, sanitizeRuntimeServiceBaseEnv, + startRuntimeServicesForWorkspaceControl, stopRuntimeServicesForExecutionWorkspace, type RealizedExecutionWorkspace, } from "../services/workspace-runtime.ts"; @@ -367,6 +372,42 @@ describe("realizeExecutionWorkspace", () => { expect(second.branchName).toBe(first.branchName); }); + it("rejects reusing an empty directory that only looks like a worktree because it sits inside the repo", async () => { + const repoRoot = await createTempRepo(); + const branchName = "PAP-447-add-worktree-support"; + const poisonedPath = path.join(repoRoot, ".paperclip", "worktrees", branchName); + await fs.mkdir(poisonedPath, { recursive: true }); + + await expect( + realizeExecutionWorkspace({ + base: { + baseCwd: repoRoot, + source: "project_primary", + projectId: "project-1", + workspaceId: "workspace-1", + repoUrl: null, + repoRef: "HEAD", + }, + config: { + workspaceStrategy: { + type: "git_worktree", + branchTemplate: "{{issue.identifier}}-{{slug}}", + }, + }, + issue: { + id: "issue-1", + identifier: "PAP-447", + title: "Add Worktree Support", + }, + agent: { + id: "agent-1", + name: "Codex Coder", + companyId: "company-1", + }, + }), + ).rejects.toThrow(/not a reusable git worktree \(path is not registered in `git worktree list`\)\./); + }); + it("reuses the current linked worktree instead of nesting another worktree inside it", async () => { const repoRoot = await createTempRepo(); const branchName = "PAP-1355-worktree-reuse"; @@ -408,6 +449,68 @@ describe("realizeExecutionWorkspace", () => { await expect(fs.realpath(realized.worktreePath ?? "")).resolves.toBe(expectedWorktreePath); }); + it("rejects reusing a linked worktree whose branch drifted from the expected issue branch", async () => { + const repoRoot = await createTempRepo(); + + const initial = await realizeExecutionWorkspace({ + base: { + baseCwd: repoRoot, + source: "project_primary", + projectId: "project-1", + workspaceId: "workspace-1", + repoUrl: null, + repoRef: "HEAD", + }, + config: { + workspaceStrategy: { + type: "git_worktree", + branchTemplate: "{{issue.identifier}}-{{slug}}", + }, + }, + issue: { + id: "issue-1", + identifier: "PAP-447", + title: "Add Worktree Support", + }, + agent: { + id: "agent-1", + name: "Codex Coder", + companyId: "company-1", + }, + }); + + await runGit(initial.cwd, ["checkout", "-b", "unexpected-branch"]); + + await expect( + realizeExecutionWorkspace({ + base: { + baseCwd: repoRoot, + source: "project_primary", + projectId: "project-1", + workspaceId: "workspace-1", + repoUrl: null, + repoRef: "HEAD", + }, + config: { + workspaceStrategy: { + type: "git_worktree", + branchTemplate: "{{issue.identifier}}-{{slug}}", + }, + }, + issue: { + id: "issue-1", + identifier: "PAP-447", + title: "Add Worktree Support", + }, + agent: { + id: "agent-1", + name: "Codex Coder", + companyId: "company-1", + }, + }), + ).rejects.toThrow(/not a reusable git worktree \(worktree HEAD is on "unexpected-branch" instead of "PAP-447-add-worktree-support"\)\./); + }); + it("reuses an already checked out branch from git worktree metadata even when the target path differs", async () => { const repoRoot = await createTempRepo(); const branchName = "PAP-1355-worktree-reuse"; @@ -1033,6 +1136,137 @@ describe("realizeExecutionWorkspace", () => { ); }, 30_000); + it("fails instead of writing an unseeded fallback config when worktree init errors after CLI detection succeeds", async () => { + const tempRoot = await fs.mkdtemp(path.join(os.tmpdir(), "paperclip-worktree-provision-fail-")); + const baseRoot = path.join(tempRoot, "base"); + const worktreeRoot = path.join(tempRoot, "worktree"); + const fakeBin = path.join(tempRoot, "bin"); + const fakePnpmPath = path.join(fakeBin, "pnpm"); + const scriptPath = path.join(worktreeRoot, "provision-worktree.sh"); + + try { + await fs.mkdir(baseRoot, { recursive: true }); + await fs.mkdir(worktreeRoot, { recursive: true }); + await fs.mkdir(fakeBin, { recursive: true }); + await fs.copyFile(provisionWorktreeScriptPath, scriptPath); + await fs.chmod(scriptPath, 0o755); + await fs.writeFile( + fakePnpmPath, + [ + "#!/bin/sh", + "if [ \"$1\" = \"paperclipai\" ] && [ \"$2\" = \"--help\" ]; then", + " exit 0", + "fi", + "if [ \"$1\" = \"paperclipai\" ] && [ \"$2\" = \"worktree\" ] && [ \"$3\" = \"init\" ]; then", + " echo \"simulated init failure\" >&2", + " exit 42", + "fi", + "exit 0", + "", + ].join("\n"), + "utf8", + ); + await fs.chmod(fakePnpmPath, 0o755); + + let caught: Error | null = null; + try { + await execFileAsync(scriptPath, [], { + cwd: worktreeRoot, + env: { + ...process.env, + PATH: `${fakeBin}:${process.env.PATH ?? ""}`, + PAPERCLIP_WORKSPACE_BASE_CWD: baseRoot, + PAPERCLIP_WORKSPACE_CWD: worktreeRoot, + }, + }); + } catch (error) { + caught = error as Error; + } + + expect(caught).toBeTruthy(); + expect(String(caught)).toContain("simulated init failure"); + await expect(fs.stat(path.join(worktreeRoot, ".paperclip", "config.json"))).rejects.toThrow(); + await expect(fs.stat(path.join(worktreeRoot, ".paperclip", ".env"))).rejects.toThrow(); + } finally { + await fs.rm(tempRoot, { recursive: true, force: true }); + } + }); + + it("retries worktree-local pnpm install without a frozen lockfile when the lockfile is outdated", async () => { + const tempRoot = await fs.mkdtemp(path.join(os.tmpdir(), "paperclip-worktree-outdated-lockfile-")); + const baseRoot = path.join(tempRoot, "base"); + const worktreeRoot = path.join(tempRoot, "worktree"); + const fakeBin = path.join(tempRoot, "bin"); + const fakePnpmPath = path.join(fakeBin, "pnpm"); + const scriptPath = path.join(worktreeRoot, "provision-worktree.sh"); + + try { + await fs.mkdir(path.join(baseRoot, "node_modules"), { recursive: true }); + await fs.mkdir(worktreeRoot, { recursive: true }); + await fs.mkdir(fakeBin, { recursive: true }); + await fs.copyFile(provisionWorktreeScriptPath, scriptPath); + await fs.chmod(scriptPath, 0o755); + await fs.writeFile( + path.join(worktreeRoot, "package.json"), + JSON.stringify( + { + name: "workspace-root", + private: true, + packageManager: "pnpm@9.15.4", + }, + null, + 2, + ), + "utf8", + ); + await fs.writeFile( + path.join(worktreeRoot, "pnpm-lock.yaml"), + ["lockfileVersion: '9.0'", "", "importers:", " .: {}", ""].join("\n"), + "utf8", + ); + await fs.writeFile( + fakePnpmPath, + [ + "#!/bin/sh", + "if [ \"$1\" = \"paperclipai\" ] && [ \"$2\" = \"--help\" ]; then", + " exit 1", + "fi", + "if [ \"$1\" = \"install\" ] && [ \"$2\" = \"--frozen-lockfile\" ]; then", + " echo \"ERR_PNPM_OUTDATED_LOCKFILE\" >&2", + " exit 1", + "fi", + "if [ \"$1\" = \"install\" ] && [ \"$2\" = \"--no-frozen-lockfile\" ]; then", + " mkdir -p \"$PWD/node_modules\"", + " : > \"$PWD/node_modules/.retry-success\"", + " exit 0", + "fi", + "exit 0", + "", + ].join("\n"), + "utf8", + ); + await fs.chmod(fakePnpmPath, 0o755); + + const result = await execFileAsync(scriptPath, [], { + cwd: worktreeRoot, + env: { + ...process.env, + PATH: `${fakeBin}:${process.env.PATH ?? ""}`, + PAPERCLIP_WORKSPACE_BASE_CWD: baseRoot, + PAPERCLIP_WORKSPACE_CWD: worktreeRoot, + }, + }); + + expect(result.stderr).toContain("retrying install without --frozen-lockfile"); + await expect(fs.readFile(path.join(worktreeRoot, "node_modules", ".retry-success"), "utf8")).resolves.toBe(""); + await expect(fs.readFile(path.join(worktreeRoot, ".paperclip", "config.json"), "utf8")).resolves.toContain( + "\"database\"", + ); + } finally { + await fs.rm(tempRoot, { recursive: true, force: true }); + } + }); + it( "provisions worktree-local pnpm node_modules instead of reusing base-repo links", async () => { @@ -1290,6 +1524,187 @@ describe("realizeExecutionWorkspace", () => { expect(actualHead).toBe(expectedHead); }); + it("reattaches a missing persisted git worktree before manual control starts it", async () => { + const repoRoot = await createTempRepo(); + const branchName = "PAP-451-restore-persisted-worktree"; + await fs.mkdir(path.join(repoRoot, "scripts"), { recursive: true }); + await fs.writeFile( + path.join(repoRoot, "scripts", "restore.sh"), + [ + "#!/usr/bin/env bash", + "set -euo pipefail", + "printf '%s\\n' \"$PAPERCLIP_WORKSPACE_BRANCH\" > .paperclip-restored-branch", + ].join("\n"), + "utf8", + ); + await fs.chmod(path.join(repoRoot, "scripts", "restore.sh"), 0o755); + await runGit(repoRoot, ["add", "scripts/restore.sh"]); + await runGit(repoRoot, ["commit", "-m", "Add restore script"]); + + await runGit(repoRoot, ["checkout", "-b", branchName]); + await fs.writeFile(path.join(repoRoot, "feature.txt"), "persisted\n", "utf8"); + await runGit(repoRoot, ["add", "feature.txt"]); + await runGit(repoRoot, ["commit", "-m", "Add persisted feature"]); + const expectedHead = (await execFileAsync("git", ["rev-parse", branchName], { cwd: repoRoot })).stdout.trim(); + await runGit(repoRoot, ["checkout", "main"]); + + const initial = await realizeExecutionWorkspace({ + base: { + baseCwd: repoRoot, + source: "project_primary", + projectId: "project-1", + workspaceId: "workspace-1", + repoUrl: null, + repoRef: "HEAD", + }, + config: { + workspaceStrategy: { + type: "git_worktree", + branchTemplate: "{{issue.identifier}}-{{slug}}", + provisionCommand: "bash ./scripts/restore.sh", + }, + }, + issue: { + id: "issue-1", + identifier: "PAP-451", + title: "Restore persisted worktree", + }, + agent: { + id: "agent-1", + name: "Codex Coder", + companyId: "company-1", + }, + }); + + await fs.rm(initial.cwd, { recursive: true, force: true }); + + const restored = await ensurePersistedExecutionWorkspaceAvailable({ + base: { + baseCwd: repoRoot, + source: "project_primary", + projectId: "project-1", + workspaceId: "workspace-1", + repoUrl: null, + repoRef: "HEAD", + }, + workspace: { + mode: "isolated_workspace", + strategyType: "git_worktree", + cwd: initial.cwd, + providerRef: initial.worktreePath, + projectId: "project-1", + projectWorkspaceId: "workspace-1", + repoUrl: null, + baseRef: "HEAD", + branchName, + config: { + provisionCommand: "bash ./scripts/restore.sh", + }, + }, + issue: { + id: "issue-1", + identifier: "PAP-451", + title: "Restore persisted worktree", + }, + agent: { + id: "agent-1", + name: "Codex Coder", + companyId: "company-1", + }, + }); + + expect(restored).not.toBeNull(); + expect(restored?.cwd).toBe(initial.cwd); + await expect(fs.readFile(path.join(initial.cwd, "feature.txt"), "utf8")).resolves.toBe("persisted\n"); + await expect(fs.readFile(path.join(initial.cwd, ".paperclip-restored-branch"), "utf8")).resolves.toBe(`${branchName}\n`); + const actualHead = (await execFileAsync("git", ["rev-parse", "HEAD"], { cwd: initial.cwd })).stdout.trim(); + expect(actualHead).toBe(expectedHead); + }); + + it("reprovisions an existing persisted git worktree before manual control starts it", async () => { + const repoRoot = await createTempRepo(); + await fs.mkdir(path.join(repoRoot, "scripts"), { recursive: true }); + await fs.writeFile( + path.join(repoRoot, "scripts", "restore.sh"), + [ + "#!/usr/bin/env bash", + "set -euo pipefail", + "printf 'reprovisioned\\n' > .paperclip-restored-state", + ].join("\n"), + "utf8", + ); + await fs.chmod(path.join(repoRoot, "scripts", "restore.sh"), 0o755); + await runGit(repoRoot, ["add", "scripts/restore.sh"]); + await runGit(repoRoot, ["commit", "-m", "Add reprovision script"]); + + const initial = await realizeExecutionWorkspace({ + base: { + baseCwd: repoRoot, + source: "project_primary", + projectId: "project-1", + workspaceId: "workspace-1", + repoUrl: null, + repoRef: "HEAD", + }, + config: { + workspaceStrategy: { + type: "git_worktree", + branchTemplate: "{{issue.identifier}}-{{slug}}", + provisionCommand: "bash ./scripts/restore.sh", + }, + }, + issue: { + id: "issue-1", + identifier: "PAP-452", + title: "Reprovision persisted worktree", + }, + agent: { + id: "agent-1", + name: "Codex Coder", + companyId: "company-1", + }, + }); + + await fs.rm(path.join(initial.cwd, ".paperclip-restored-state"), { force: true }); + + await ensurePersistedExecutionWorkspaceAvailable({ + base: { + baseCwd: repoRoot, + source: "project_primary", + projectId: "project-1", + workspaceId: "workspace-1", + repoUrl: null, + repoRef: "HEAD", + }, + workspace: { + mode: "isolated_workspace", + strategyType: "git_worktree", + cwd: initial.cwd, + providerRef: initial.worktreePath, + projectId: "project-1", + projectWorkspaceId: "workspace-1", + repoUrl: null, + baseRef: "HEAD", + branchName: initial.branchName, + config: { + provisionCommand: "bash ./scripts/restore.sh", + }, + }, + issue: { + id: "issue-1", + identifier: "PAP-452", + title: "Reprovision persisted worktree", + }, + agent: { + id: "agent-1", + name: "Codex Coder", + companyId: "company-1", + }, + }); + + await expect(fs.readFile(path.join(initial.cwd, ".paperclip-restored-state"), "utf8")).resolves.toBe("reprovisioned\n"); + }); + it("auto-detects the default branch when baseRef is not configured", async () => { // Create a repo with "master" as default branch (not "main") const repoRoot = await createTempRepo("master"); @@ -1977,6 +2392,234 @@ describe("ensureRuntimeServicesForRun", () => { await releaseRuntimeServicesForRun(runId); leasedRunIds.delete(runId); }); + + it("starts only the selected workspace-controlled runtime service", async () => { + const workspaceRoot = await fs.mkdtemp(path.join(os.tmpdir(), "paperclip-runtime-control-start-")); + const workspace = buildWorkspace(workspaceRoot); + + const services = await startRuntimeServicesForWorkspaceControl({ + actor: { + id: "agent-1", + name: "Codex Coder", + companyId: "company-1", + }, + issue: null, + workspace, + executionWorkspaceId: "execution-workspace-control-start", + config: { + workspaceRuntime: { + services: [ + { + name: "web", + command: + "node -e \"require('node:http').createServer((req,res)=>res.end('web')).listen(Number(process.env.PORT), '127.0.0.1')\"", + port: { type: "auto" }, + readiness: { + type: "http", + urlTemplate: "http://127.0.0.1:{{port}}", + timeoutSec: 10, + intervalMs: 100, + }, + lifecycle: "shared", + reuseScope: "execution_workspace", + }, + { + name: "worker", + command: + "node -e \"require('node:http').createServer((req,res)=>res.end('worker')).listen(Number(process.env.PORT), '127.0.0.1')\"", + port: { type: "auto" }, + readiness: { + type: "http", + urlTemplate: "http://127.0.0.1:{{port}}", + timeoutSec: 10, + intervalMs: 100, + }, + lifecycle: "shared", + reuseScope: "execution_workspace", + }, + ], + }, + }, + adapterEnv: {}, + serviceIndex: 1, + }); + + expect(services).toHaveLength(1); + expect(services[0]?.serviceName).toBe("worker"); + await expect(fetch(services[0]!.url!)).resolves.toMatchObject({ ok: true }); + + await stopRuntimeServicesForExecutionWorkspace({ + executionWorkspaceId: "execution-workspace-control-start", + workspaceCwd: workspace.cwd, + }); + }); + + it("stops only the selected execution workspace runtime service", async () => { + const workspaceRoot = await fs.mkdtemp(path.join(os.tmpdir(), "paperclip-runtime-control-stop-")); + const workspace = buildWorkspace(workspaceRoot); + + const services = await startRuntimeServicesForWorkspaceControl({ + actor: { + id: "agent-1", + name: "Codex Coder", + companyId: "company-1", + }, + issue: null, + workspace, + executionWorkspaceId: "execution-workspace-control-stop", + config: { + workspaceRuntime: { + services: [ + { + name: "web", + command: + "node -e \"require('node:http').createServer((req,res)=>res.end('web')).listen(Number(process.env.PORT), '127.0.0.1')\"", + port: { type: "auto" }, + readiness: { + type: "http", + urlTemplate: "http://127.0.0.1:{{port}}", + timeoutSec: 10, + intervalMs: 100, + }, + lifecycle: "shared", + reuseScope: "execution_workspace", + stopPolicy: { + type: "manual", + }, + }, + { + name: "worker", + command: + "node -e \"require('node:http').createServer((req,res)=>res.end('worker')).listen(Number(process.env.PORT), '127.0.0.1')\"", + port: { type: "auto" }, + readiness: { + type: "http", + urlTemplate: "http://127.0.0.1:{{port}}", + timeoutSec: 10, + intervalMs: 100, + }, + lifecycle: "shared", + reuseScope: "execution_workspace", + stopPolicy: { + type: "manual", + }, + }, + ], + }, + }, + adapterEnv: {}, + }); + + expect(services).toHaveLength(2); + const web = services.find((service) => service.serviceName === "web"); + const worker = services.find((service) => service.serviceName === "worker"); + + await stopRuntimeServicesForExecutionWorkspace({ + executionWorkspaceId: "execution-workspace-control-stop", + workspaceCwd: workspace.cwd, + runtimeServiceId: web?.id ?? null, + }); + + await expect(fetch(web!.url!)).rejects.toThrow(); + await expect(fetch(worker!.url!)).resolves.toMatchObject({ ok: true }); + + await stopRuntimeServicesForExecutionWorkspace({ + executionWorkspaceId: "execution-workspace-control-stop", + workspaceCwd: workspace.cwd, + runtimeServiceId: worker?.id ?? null, + }); + }); +}); + +describe("buildWorkspaceRuntimeDesiredStatePatch", () => { + it("derives service entries from command-first runtime config", () => { + const services = listConfiguredRuntimeServiceEntries({ + workspaceRuntime: { + commands: [ + { id: "web", name: "web", kind: "service", command: "pnpm dev" }, + { id: "db-migrate", name: "db:migrate", kind: "job", command: "pnpm db:migrate" }, + ], + }, + }); + + expect(services).toEqual([ + expect.objectContaining({ + id: "web", + kind: "service", + command: "pnpm dev", + }), + ]); + }); + + it("preserves sibling service state when updating a single configured runtime service", () => { + const patch = buildWorkspaceRuntimeDesiredStatePatch({ + config: { + workspaceRuntime: { + services: [ + { name: "web", command: "pnpm dev" }, + { name: "worker", command: "pnpm worker" }, + ], + }, + }, + currentDesiredState: "running", + currentServiceStates: null, + action: "stop", + serviceIndex: 1, + }); + + expect(patch).toEqual({ + desiredState: "running", + serviceStates: { + "0": "running", + "1": "stopped", + }, + }); + }); +}); + +describe("resolveWorkspaceRuntimeReadinessTimeoutSec", () => { + it("extends the default readiness timeout for dev-server commands", () => { + expect( + resolveWorkspaceRuntimeReadinessTimeoutSec({ + command: "pnpm dev", + readiness: { + type: "http", + urlTemplate: "http://127.0.0.1:{{port}}", + }, + }), + ).toBe(90); + expect( + resolveWorkspaceRuntimeReadinessTimeoutSec({ + command: "npm run dev -- --host 127.0.0.1", + readiness: { + type: "http", + urlTemplate: "http://127.0.0.1:{{port}}", + }, + }), + ).toBe(90); + }); + + it("keeps explicit readiness timeouts and non-dev defaults unchanged", () => { + expect( + resolveWorkspaceRuntimeReadinessTimeoutSec({ + command: "pnpm dev", + readiness: { + type: "http", + timeoutSec: 12, + urlTemplate: "http://127.0.0.1:{{port}}", + }, + }), + ).toBe(12); + expect( + resolveWorkspaceRuntimeReadinessTimeoutSec({ + command: "node server.js", + readiness: { + type: "http", + urlTemplate: "http://127.0.0.1:{{port}}", + }, + }), + ).toBe(30); + }); }); describe("resolveShell (shell fallback)", () => { @@ -1993,13 +2636,18 @@ describe("resolveShell (shell fallback)", () => { }); it("returns process.env.SHELL when set", () => { - process.env.SHELL = "/usr/bin/zsh"; - expect(resolveShell()).toBe("/usr/bin/zsh"); + process.env.SHELL = process.execPath; + expect(resolveShell()).toBe(process.execPath); }); it("trims whitespace from SHELL env var", () => { - process.env.SHELL = " /usr/bin/fish "; - expect(resolveShell()).toBe("/usr/bin/fish"); + process.env.SHELL = ` ${process.execPath} `; + expect(resolveShell()).toBe(process.execPath); + }); + + it("preserves non-absolute shell names so PATH lookup still works", () => { + process.env.SHELL = "zsh"; + expect(resolveShell()).toBe("zsh"); }); it("falls back to /bin/sh on non-Windows when SHELL is unset", () => { @@ -2031,6 +2679,12 @@ describe("resolveShell (shell fallback)", () => { Object.defineProperty(process, "platform", { value: "win32" }); expect(resolveShell()).toBe("sh"); }); + + it("falls back when SHELL points to a missing absolute path", () => { + process.env.SHELL = "/definitely/missing/zsh"; + Object.defineProperty(process, "platform", { value: "linux" }); + expect(resolveShell()).toBe("/bin/sh"); + }); }); describeEmbeddedPostgres("workspace runtime startup reconciliation", () => { diff --git a/server/src/app.ts b/server/src/app.ts index dd89cd7f..f148dca4 100644 --- a/server/src/app.ts +++ b/server/src/app.ts @@ -24,6 +24,7 @@ import { costRoutes } from "./routes/costs.js"; import { activityRoutes } from "./routes/activity.js"; import { dashboardRoutes } from "./routes/dashboard.js"; import { sidebarBadgeRoutes } from "./routes/sidebar-badges.js"; +import { sidebarPreferenceRoutes } from "./routes/sidebar-preferences.js"; import { inboxDismissalRoutes } from "./routes/inbox-dismissals.js"; import { instanceSettingsRoutes } from "./routes/instance-settings.js"; import { llmRoutes } from "./routes/llms.js"; @@ -167,6 +168,7 @@ export async function createApp( api.use(activityRoutes(db)); api.use(dashboardRoutes(db)); api.use(sidebarBadgeRoutes(db)); + api.use(sidebarPreferenceRoutes(db)); api.use(inboxDismissalRoutes(db)); api.use(instanceSettingsRoutes(db)); const hostServicesDisposers = new Map void>(); diff --git a/server/src/routes/execution-workspaces.ts b/server/src/routes/execution-workspaces.ts index 4fa20425..963be2ed 100644 --- a/server/src/routes/execution-workspaces.ts +++ b/server/src/routes/execution-workspaces.ts @@ -1,15 +1,24 @@ import { and, eq } from "drizzle-orm"; -import { Router } from "express"; +import { Router, type Request, type Response } from "express"; import type { Db } from "@paperclipai/db"; import { issues, projects, projectWorkspaces } from "@paperclipai/db"; -import { updateExecutionWorkspaceSchema } from "@paperclipai/shared"; +import { + findWorkspaceCommandDefinition, + matchWorkspaceRuntimeServiceToCommand, + updateExecutionWorkspaceSchema, + workspaceRuntimeControlTargetSchema, +} from "@paperclipai/shared"; import { validate } from "../middleware/validate.js"; import { executionWorkspaceService, logActivity, workspaceOperationService } from "../services/index.js"; import { mergeExecutionWorkspaceConfig, readExecutionWorkspaceConfig } from "../services/execution-workspaces.js"; import { parseProjectExecutionWorkspacePolicy } from "../services/execution-workspace-policy.js"; import { readProjectWorkspaceRuntimeConfig } from "../services/project-workspace-runtime-config.js"; import { + buildWorkspaceRuntimeDesiredStatePatch, cleanupExecutionWorkspaceArtifacts, + ensurePersistedExecutionWorkspaceAvailable, + listConfiguredRuntimeServiceEntries, + runWorkspaceJobForControl, startRuntimeServicesForWorkspaceControl, stopRuntimeServicesForExecutionWorkspace, } from "../services/workspace-runtime.js"; @@ -72,11 +81,11 @@ export function executionWorkspaceRoutes(db: Db) { res.json(operations); }); - router.post("/execution-workspaces/:id/runtime-services/:action", async (req, res) => { + async function handleExecutionWorkspaceRuntimeCommand(req: Request, res: Response) { const id = req.params.id as string; const action = String(req.params.action ?? "").trim().toLowerCase(); - if (action !== "start" && action !== "stop" && action !== "restart") { - res.status(404).json({ error: "Runtime service action not found" }); + if (action !== "start" && action !== "stop" && action !== "restart" && action !== "run") { + res.status(404).json({ error: "Workspace command action not found" }); return; } @@ -89,7 +98,7 @@ export function executionWorkspaceRoutes(db: Db) { const workspaceCwd = existing.cwd; if (!workspaceCwd) { - res.status(422).json({ error: "Execution workspace needs a local path before Paperclip can manage local runtime services" }); + res.status(422).json({ error: "Execution workspace needs a local path before Paperclip can run workspace commands" }); return; } @@ -115,10 +124,68 @@ export function executionWorkspaceRoutes(db: Db) { const projectWorkspaceRuntime = readProjectWorkspaceRuntimeConfig( (projectWorkspace?.metadata as Record | null) ?? null, )?.workspaceRuntime ?? null; + const projectPolicy = existing.projectId + ? await db + .select({ + executionWorkspacePolicy: projects.executionWorkspacePolicy, + }) + .from(projects) + .where( + and( + eq(projects.id, existing.projectId), + eq(projects.companyId, existing.companyId), + ), + ) + .then((rows) => parseProjectExecutionWorkspacePolicy(rows[0]?.executionWorkspacePolicy)) + : null; const effectiveRuntimeConfig = existing.config?.workspaceRuntime ?? projectWorkspaceRuntime ?? null; + const target = req.body as { workspaceCommandId?: string | null; runtimeServiceId?: string | null; serviceIndex?: number | null }; + const configuredServices = effectiveRuntimeConfig + ? listConfiguredRuntimeServiceEntries({ workspaceRuntime: effectiveRuntimeConfig }) + : []; + const workspaceCommand = effectiveRuntimeConfig + ? findWorkspaceCommandDefinition(effectiveRuntimeConfig, target.workspaceCommandId ?? null) + : null; + if (target.workspaceCommandId && !workspaceCommand) { + res.status(404).json({ error: "Workspace command not found for this execution workspace" }); + return; + } + if (target.runtimeServiceId && !(existing.runtimeServices ?? []).some((service) => service.id === target.runtimeServiceId)) { + res.status(404).json({ error: "Runtime service not found for this execution workspace" }); + return; + } + const matchedRuntimeService = + workspaceCommand?.kind === "service" && !target.runtimeServiceId + ? matchWorkspaceRuntimeServiceToCommand(workspaceCommand, existing.runtimeServices ?? []) + : null; + const selectedRuntimeServiceId = target.runtimeServiceId ?? matchedRuntimeService?.id ?? null; + const selectedServiceIndex = + workspaceCommand?.kind === "service" + ? workspaceCommand.serviceIndex + : target.serviceIndex ?? null; + if ( + selectedServiceIndex !== undefined + && selectedServiceIndex !== null + && (selectedServiceIndex < 0 || selectedServiceIndex >= configuredServices.length) + ) { + res.status(422).json({ error: "Selected runtime service is not defined in this execution workspace runtime config" }); + return; + } + if (workspaceCommand?.kind === "job" && action !== "run") { + res.status(422).json({ error: `Workspace job "${workspaceCommand.name}" can only be run` }); + return; + } + if (workspaceCommand?.kind === "service" && action === "run") { + res.status(422).json({ error: `Workspace service "${workspaceCommand.name}" should be started or restarted, not run` }); + return; + } + if (action === "run" && !workspaceCommand) { + res.status(422).json({ error: "Select a workspace job to run" }); + return; + } if ((action === "start" || action === "restart") && !effectiveRuntimeConfig) { - res.status(422).json({ error: "Execution workspace has no runtime service configuration or inherited project workspace default" }); + res.status(422).json({ error: "Execution workspace has no workspace command configuration or inherited project workspace default" }); return; } @@ -133,13 +200,101 @@ export function executionWorkspaceRoutes(db: Db) { const operation = await recorder.recordOperation({ phase: action === "stop" ? "workspace_teardown" : "workspace_provision", - command: `workspace runtime ${action}`, + command: workspaceCommand?.command ?? `workspace command ${action}`, cwd: existing.cwd, metadata: { action, executionWorkspaceId: existing.id, + workspaceCommandId: workspaceCommand?.id ?? target.workspaceCommandId ?? null, + workspaceCommandKind: workspaceCommand?.kind ?? null, + workspaceCommandName: workspaceCommand?.name ?? null, + runtimeServiceId: selectedRuntimeServiceId, + serviceIndex: selectedServiceIndex, }, run: async () => { + const ensureWorkspaceAvailable = async () => + await ensurePersistedExecutionWorkspaceAvailable({ + base: { + baseCwd: projectWorkspace?.cwd ?? workspaceCwd, + source: existing.mode === "shared_workspace" ? "project_primary" : "task_session", + projectId: existing.projectId, + workspaceId: existing.projectWorkspaceId, + repoUrl: existing.repoUrl, + repoRef: existing.baseRef, + }, + workspace: { + mode: existing.mode, + strategyType: existing.strategyType, + cwd: existing.cwd, + providerRef: existing.providerRef, + projectId: existing.projectId, + projectWorkspaceId: existing.projectWorkspaceId, + repoUrl: existing.repoUrl, + baseRef: existing.baseRef, + branchName: existing.branchName, + config: { + ...existing.config, + provisionCommand: + existing.config?.provisionCommand + ?? projectPolicy?.workspaceStrategy?.provisionCommand + ?? null, + }, + }, + issue: existing.sourceIssueId + ? { + id: existing.sourceIssueId, + identifier: null, + title: existing.name, + } + : null, + agent: { + id: actor.agentId ?? null, + name: actor.actorType === "user" ? "Board" : "Agent", + companyId: existing.companyId, + }, + recorder, + }); + + if (action === "run") { + if (!workspaceCommand || workspaceCommand.kind !== "job") { + throw new Error("Workspace job selection is required"); + } + const availableWorkspace = await ensureWorkspaceAvailable(); + if (!availableWorkspace) { + throw new Error("Execution workspace needs a local path before Paperclip can run workspace commands"); + } + return await runWorkspaceJobForControl({ + actor: { + id: actor.agentId ?? null, + name: actor.actorType === "user" ? "Board" : "Agent", + companyId: existing.companyId, + }, + issue: existing.sourceIssueId + ? { + id: existing.sourceIssueId, + identifier: null, + title: existing.name, + } + : null, + workspace: availableWorkspace, + command: workspaceCommand.rawConfig, + adapterEnv: {}, + recorder, + metadata: { + action, + executionWorkspaceId: existing.id, + workspaceCommandId: workspaceCommand.id, + }, + }).then((nestedOperation) => ({ + status: "succeeded" as const, + exitCode: 0, + metadata: { + nestedOperationId: nestedOperation?.id ?? null, + runtimeServiceCount, + }, + })); + } + const onLog = async (stream: "stdout" | "stderr", chunk: string) => { if (stream === "stdout") stdout.push(chunk); else stderr.push(chunk); @@ -150,10 +305,15 @@ export function executionWorkspaceRoutes(db: Db) { db, executionWorkspaceId: existing.id, workspaceCwd, + runtimeServiceId: selectedRuntimeServiceId, }); } if (action === "start" || action === "restart") { + const availableWorkspace = await ensureWorkspaceAvailable(); + if (!availableWorkspace) { + throw new Error("Execution workspace needs a local path before Paperclip can manage local runtime services"); + } const startedServices = await startRuntimeServicesForWorkspaceControl({ db, actor: { @@ -168,32 +328,41 @@ export function executionWorkspaceRoutes(db: Db) { title: existing.name, } : null, - workspace: { - baseCwd: workspaceCwd, - source: existing.mode === "shared_workspace" ? "project_primary" : "task_session", - projectId: existing.projectId, - workspaceId: existing.projectWorkspaceId, - repoUrl: existing.repoUrl, - repoRef: existing.baseRef, - strategy: existing.strategyType === "git_worktree" ? "git_worktree" : "project_primary", - cwd: workspaceCwd, - branchName: existing.branchName, - worktreePath: existing.strategyType === "git_worktree" ? workspaceCwd : null, - warnings: [], - created: false, - }, + workspace: availableWorkspace, executionWorkspaceId: existing.id, config: { workspaceRuntime: effectiveRuntimeConfig }, adapterEnv: {}, onLog, + serviceIndex: selectedServiceIndex, }); runtimeServiceCount = startedServices.length; } else { - runtimeServiceCount = 0; + runtimeServiceCount = selectedRuntimeServiceId ? Math.max(0, (existing.runtimeServices?.length ?? 1) - 1) : 0; } + const currentDesiredState: "running" | "stopped" = + existing.config?.desiredState + ?? ((existing.runtimeServices ?? []).some((service) => service.status === "starting" || service.status === "running") + ? "running" + : "stopped"); + const nextRuntimeState: { + desiredState: "running" | "stopped"; + serviceStates: Record | null | undefined; + } = selectedRuntimeServiceId && (selectedServiceIndex === undefined || selectedServiceIndex === null) + ? { + desiredState: currentDesiredState, + serviceStates: existing.config?.serviceStates ?? null, + } + : buildWorkspaceRuntimeDesiredStatePatch({ + config: { workspaceRuntime: effectiveRuntimeConfig }, + currentDesiredState, + currentServiceStates: existing.config?.serviceStates ?? null, + action, + serviceIndex: selectedServiceIndex, + }); const metadata = mergeExecutionWorkspaceConfig(existing.metadata as Record | null, { - desiredState: action === "stop" ? "stopped" : "running", + desiredState: nextRuntimeState.desiredState, + serviceStates: nextRuntimeState.serviceStates, }); await svc.update(existing.id, { metadata }); @@ -209,6 +378,9 @@ export function executionWorkspaceRoutes(db: Db) { : "Started execution workspace runtime services.\n", metadata: { runtimeServiceCount, + workspaceCommandId: workspaceCommand?.id ?? target.workspaceCommandId ?? null, + runtimeServiceId: selectedRuntimeServiceId, + serviceIndex: selectedServiceIndex, }, }; }, @@ -231,6 +403,11 @@ export function executionWorkspaceRoutes(db: Db) { entityId: existing.id, details: { runtimeServiceCount, + workspaceCommandId: workspaceCommand?.id ?? target.workspaceCommandId ?? null, + workspaceCommandKind: workspaceCommand?.kind ?? null, + workspaceCommandName: workspaceCommand?.name ?? null, + runtimeServiceId: selectedRuntimeServiceId, + serviceIndex: selectedServiceIndex, }, }); @@ -238,7 +415,10 @@ export function executionWorkspaceRoutes(db: Db) { workspace, operation, }); - }); + } + + router.post("/execution-workspaces/:id/runtime-services/:action", validate(workspaceRuntimeControlTargetSchema), handleExecutionWorkspaceRuntimeCommand); + router.post("/execution-workspaces/:id/runtime-commands/:action", validate(workspaceRuntimeControlTargetSchema), handleExecutionWorkspaceRuntimeCommand); router.patch("/execution-workspaces/:id", validate(updateExecutionWorkspaceSchema), async (req, res) => { const id = req.params.id as string; diff --git a/server/src/routes/index.ts b/server/src/routes/index.ts index bca52940..2b6506f5 100644 --- a/server/src/routes/index.ts +++ b/server/src/routes/index.ts @@ -12,6 +12,7 @@ export { costRoutes } from "./costs.js"; export { activityRoutes } from "./activity.js"; export { dashboardRoutes } from "./dashboard.js"; export { sidebarBadgeRoutes } from "./sidebar-badges.js"; +export { sidebarPreferenceRoutes } from "./sidebar-preferences.js"; export { inboxDismissalRoutes } from "./inbox-dismissals.js"; export { llmRoutes } from "./llms.js"; export { accessRoutes } from "./access.js"; diff --git a/server/src/routes/projects.ts b/server/src/routes/projects.ts index cd523dce..71cd792b 100644 --- a/server/src/routes/projects.ts +++ b/server/src/routes/projects.ts @@ -1,18 +1,27 @@ -import { Router, type Request } from "express"; +import { Router, type Request, type Response } from "express"; import type { Db } from "@paperclipai/db"; import { createProjectSchema, createProjectWorkspaceSchema, + findWorkspaceCommandDefinition, isUuidLike, + matchWorkspaceRuntimeServiceToCommand, updateProjectSchema, updateProjectWorkspaceSchema, + workspaceRuntimeControlTargetSchema, } from "@paperclipai/shared"; import { trackProjectCreated } from "@paperclipai/shared/telemetry"; import { validate } from "../middleware/validate.js"; import { projectService, logActivity, secretService, workspaceOperationService } from "../services/index.js"; import { conflict } from "../errors.js"; import { assertCompanyAccess, getActorInfo } from "./authz.js"; -import { startRuntimeServicesForWorkspaceControl, stopRuntimeServicesForProjectWorkspace } from "../services/workspace-runtime.js"; +import { + buildWorkspaceRuntimeDesiredStatePatch, + listConfiguredRuntimeServiceEntries, + runWorkspaceJobForControl, + startRuntimeServicesForWorkspaceControl, + stopRuntimeServicesForProjectWorkspace, +} from "../services/workspace-runtime.js"; import { getTelemetryClient } from "../telemetry.js"; export function projectRoutes(db: Db) { @@ -259,12 +268,12 @@ export function projectRoutes(db: Db) { }, ); - router.post("/projects/:id/workspaces/:workspaceId/runtime-services/:action", async (req, res) => { + async function handleProjectWorkspaceRuntimeCommand(req: Request, res: Response) { const id = req.params.id as string; const workspaceId = req.params.workspaceId as string; const action = String(req.params.action ?? "").trim().toLowerCase(); - if (action !== "start" && action !== "stop" && action !== "restart") { - res.status(404).json({ error: "Runtime service action not found" }); + if (action !== "start" && action !== "stop" && action !== "restart" && action !== "run") { + res.status(404).json({ error: "Workspace command action not found" }); return; } @@ -283,13 +292,55 @@ export function projectRoutes(db: Db) { const workspaceCwd = workspace.cwd; if (!workspaceCwd) { - res.status(422).json({ error: "Project workspace needs a local path before Paperclip can manage local runtime services" }); + res.status(422).json({ error: "Project workspace needs a local path before Paperclip can run workspace commands" }); return; } const runtimeConfig = workspace.runtimeConfig?.workspaceRuntime ?? null; + const target = req.body as { workspaceCommandId?: string | null; runtimeServiceId?: string | null; serviceIndex?: number | null }; + const configuredServices = runtimeConfig ? listConfiguredRuntimeServiceEntries({ workspaceRuntime: runtimeConfig }) : []; + const workspaceCommand = runtimeConfig + ? findWorkspaceCommandDefinition(runtimeConfig, target.workspaceCommandId ?? null) + : null; + if (target.workspaceCommandId && !workspaceCommand) { + res.status(404).json({ error: "Workspace command not found for this project workspace" }); + return; + } + if (target.runtimeServiceId && !(workspace.runtimeServices ?? []).some((service) => service.id === target.runtimeServiceId)) { + res.status(404).json({ error: "Runtime service not found for this project workspace" }); + return; + } + const matchedRuntimeService = + workspaceCommand?.kind === "service" && !target.runtimeServiceId + ? matchWorkspaceRuntimeServiceToCommand(workspaceCommand, workspace.runtimeServices ?? []) + : null; + const selectedRuntimeServiceId = target.runtimeServiceId ?? matchedRuntimeService?.id ?? null; + const selectedServiceIndex = + workspaceCommand?.kind === "service" + ? workspaceCommand.serviceIndex + : target.serviceIndex ?? null; + if ( + selectedServiceIndex !== undefined + && selectedServiceIndex !== null + && (selectedServiceIndex < 0 || selectedServiceIndex >= configuredServices.length) + ) { + res.status(422).json({ error: "Selected runtime service is not defined in this project workspace runtime config" }); + return; + } + if (workspaceCommand?.kind === "job" && action !== "run") { + res.status(422).json({ error: `Workspace job "${workspaceCommand.name}" can only be run` }); + return; + } + if (workspaceCommand?.kind === "service" && action === "run") { + res.status(422).json({ error: `Workspace service "${workspaceCommand.name}" should be started or restarted, not run` }); + return; + } + if (action === "run" && !workspaceCommand) { + res.status(422).json({ error: "Select a workspace job to run" }); + return; + } if ((action === "start" || action === "restart") && !runtimeConfig) { - res.status(422).json({ error: "Project workspace has no runtime service configuration" }); + res.status(422).json({ error: "Project workspace has no workspace command configuration" }); return; } @@ -301,14 +352,63 @@ export function projectRoutes(db: Db) { const operation = await recorder.recordOperation({ phase: action === "stop" ? "workspace_teardown" : "workspace_provision", - command: `workspace runtime ${action}`, + command: workspaceCommand?.command ?? `workspace command ${action}`, cwd: workspace.cwd, metadata: { action, projectId: project.id, projectWorkspaceId: workspace.id, + workspaceCommandId: workspaceCommand?.id ?? target.workspaceCommandId ?? null, + workspaceCommandKind: workspaceCommand?.kind ?? null, + workspaceCommandName: workspaceCommand?.name ?? null, + runtimeServiceId: selectedRuntimeServiceId, + serviceIndex: selectedServiceIndex, }, run: async () => { + if (action === "run") { + if (!workspaceCommand || workspaceCommand.kind !== "job") { + throw new Error("Workspace job selection is required"); + } + return await runWorkspaceJobForControl({ + actor: { + id: actor.agentId ?? null, + name: actor.actorType === "user" ? "Board" : "Agent", + companyId: project.companyId, + }, + issue: null, + workspace: { + baseCwd: workspaceCwd, + source: "project_primary", + projectId: project.id, + workspaceId: workspace.id, + repoUrl: workspace.repoUrl, + repoRef: workspace.repoRef, + strategy: "project_primary", + cwd: workspaceCwd, + branchName: workspace.defaultRef ?? workspace.repoRef ?? null, + worktreePath: null, + warnings: [], + created: false, + }, + command: workspaceCommand.rawConfig, + adapterEnv: {}, + recorder, + metadata: { + action, + projectId: project.id, + projectWorkspaceId: workspace.id, + workspaceCommandId: workspaceCommand.id, + }, + }).then((nestedOperation) => ({ + status: "succeeded" as const, + exitCode: 0, + metadata: { + nestedOperationId: nestedOperation?.id ?? null, + runtimeServiceCount, + }, + })); + } + const onLog = async (stream: "stdout" | "stderr", chunk: string) => { if (stream === "stdout") stdout.push(chunk); else stderr.push(chunk); @@ -318,6 +418,7 @@ export function projectRoutes(db: Db) { await stopRuntimeServicesForProjectWorkspace({ db, projectWorkspaceId: workspace.id, + runtimeServiceId: selectedRuntimeServiceId, }); } @@ -347,15 +448,37 @@ export function projectRoutes(db: Db) { config: { workspaceRuntime: runtimeConfig }, adapterEnv: {}, onLog, + serviceIndex: selectedServiceIndex, }); runtimeServiceCount = startedServices.length; } else { - runtimeServiceCount = 0; + runtimeServiceCount = selectedRuntimeServiceId ? Math.max(0, (workspace.runtimeServices?.length ?? 1) - 1) : 0; } + const currentDesiredState: "running" | "stopped" = + workspace.runtimeConfig?.desiredState + ?? ((workspace.runtimeServices ?? []).some((service) => service.status === "starting" || service.status === "running") + ? "running" + : "stopped"); + const nextRuntimeState: { + desiredState: "running" | "stopped"; + serviceStates: Record | null | undefined; + } = selectedRuntimeServiceId && (selectedServiceIndex === undefined || selectedServiceIndex === null) + ? { + desiredState: currentDesiredState, + serviceStates: workspace.runtimeConfig?.serviceStates ?? null, + } + : buildWorkspaceRuntimeDesiredStatePatch({ + config: { workspaceRuntime: runtimeConfig }, + currentDesiredState, + currentServiceStates: workspace.runtimeConfig?.serviceStates ?? null, + action, + serviceIndex: selectedServiceIndex, + }); await svc.updateWorkspace(project.id, workspace.id, { runtimeConfig: { - desiredState: action === "stop" ? "stopped" : "running", + desiredState: nextRuntimeState.desiredState, + serviceStates: nextRuntimeState.serviceStates, }, }); @@ -371,6 +494,9 @@ export function projectRoutes(db: Db) { : "Started project workspace runtime services.\n", metadata: { runtimeServiceCount, + workspaceCommandId: workspaceCommand?.id ?? target.workspaceCommandId ?? null, + runtimeServiceId: selectedRuntimeServiceId, + serviceIndex: selectedServiceIndex, }, }; }, @@ -389,6 +515,11 @@ export function projectRoutes(db: Db) { details: { projectWorkspaceId: workspace.id, runtimeServiceCount, + workspaceCommandId: workspaceCommand?.id ?? target.workspaceCommandId ?? null, + workspaceCommandKind: workspaceCommand?.kind ?? null, + workspaceCommandName: workspaceCommand?.name ?? null, + runtimeServiceId: selectedRuntimeServiceId, + serviceIndex: selectedServiceIndex, }, }); @@ -396,7 +527,10 @@ export function projectRoutes(db: Db) { workspace: updatedWorkspace, operation, }); - }); + } + + router.post("/projects/:id/workspaces/:workspaceId/runtime-services/:action", validate(workspaceRuntimeControlTargetSchema), handleProjectWorkspaceRuntimeCommand); + router.post("/projects/:id/workspaces/:workspaceId/runtime-commands/:action", validate(workspaceRuntimeControlTargetSchema), handleProjectWorkspaceRuntimeCommand); router.delete("/projects/:id/workspaces/:workspaceId", async (req, res) => { const id = req.params.id as string; diff --git a/server/src/routes/sidebar-preferences.ts b/server/src/routes/sidebar-preferences.ts new file mode 100644 index 00000000..f7247c44 --- /dev/null +++ b/server/src/routes/sidebar-preferences.ts @@ -0,0 +1,71 @@ +import { Router, type Request, type Response } from "express"; +import type { Db } from "@paperclipai/db"; +import { upsertSidebarOrderPreferenceSchema } from "@paperclipai/shared"; +import { validate } from "../middleware/validate.js"; +import { logActivity, sidebarPreferenceService } from "../services/index.js"; +import { assertBoard, assertCompanyAccess, getActorInfo } from "./authz.js"; + +function requireBoardUserId(req: Request, res: Response): string | null { + assertBoard(req); + if (!req.actor.userId) { + res.status(403).json({ error: "Board user context required" }); + return null; + } + return req.actor.userId; +} + +export function sidebarPreferenceRoutes(db: Db) { + const router = Router(); + const svc = sidebarPreferenceService(db); + + router.get("/sidebar-preferences/me", async (req, res) => { + const userId = requireBoardUserId(req, res); + if (!userId) return; + res.json(await svc.getCompanyOrder(userId)); + }); + + router.put("/sidebar-preferences/me", validate(upsertSidebarOrderPreferenceSchema), async (req, res) => { + const userId = requireBoardUserId(req, res); + if (!userId) return; + res.json(await svc.upsertCompanyOrder(userId, req.body.orderedIds)); + }); + + router.get("/companies/:companyId/sidebar-preferences/me", async (req, res) => { + const companyId = req.params.companyId as string; + assertCompanyAccess(req, companyId); + const userId = requireBoardUserId(req, res); + if (!userId) return; + res.json(await svc.getProjectOrder(companyId, userId)); + }); + + router.put( + "/companies/:companyId/sidebar-preferences/me", + validate(upsertSidebarOrderPreferenceSchema), + async (req, res) => { + const companyId = req.params.companyId as string; + assertCompanyAccess(req, companyId); + const userId = requireBoardUserId(req, res); + if (!userId) return; + + const result = await svc.upsertProjectOrder(companyId, userId, req.body.orderedIds); + const actor = getActorInfo(req); + await logActivity(db, { + companyId, + actorType: actor.actorType, + actorId: actor.actorId, + agentId: actor.agentId, + runId: actor.runId, + action: "sidebar_preferences.project_order_updated", + entityType: "company", + entityId: companyId, + details: { + userId, + orderedIds: result.orderedIds, + }, + }); + res.json(result); + }, + ); + + return router; +} diff --git a/server/src/services/execution-workspaces.ts b/server/src/services/execution-workspaces.ts index 58a43210..af1c3ee8 100644 --- a/server/src/services/execution-workspaces.ts +++ b/server/src/services/execution-workspaces.ts @@ -192,6 +192,11 @@ export function readExecutionWorkspaceConfig(metadata: Record | cleanupCommand: readNullableString(raw.cleanupCommand), workspaceRuntime: cloneRecord(raw.workspaceRuntime), desiredState: raw.desiredState === "running" || raw.desiredState === "stopped" ? raw.desiredState : null, + serviceStates: isRecord(raw.serviceStates) + ? Object.fromEntries( + Object.entries(raw.serviceStates).filter(([, state]) => state === "running" || state === "stopped"), + ) as ExecutionWorkspaceConfig["serviceStates"] + : null, }; const hasConfig = Object.values(config).some((value) => { @@ -214,6 +219,7 @@ export function mergeExecutionWorkspaceConfig( cleanupCommand: null, workspaceRuntime: null, desiredState: null, + serviceStates: null, }; if (patch === null) { @@ -232,6 +238,14 @@ export function mergeExecutionWorkspaceConfig( ? patch.desiredState : null : current.desiredState, + serviceStates: + patch.serviceStates !== undefined && isRecord(patch.serviceStates) + ? Object.fromEntries( + Object.entries(patch.serviceStates).filter(([, state]) => state === "running" || state === "stopped"), + ) as ExecutionWorkspaceConfig["serviceStates"] + : patch.serviceStates !== undefined + ? null + : current.serviceStates, }; const hasConfig = Object.values(nextConfig).some((value) => { @@ -247,6 +261,7 @@ export function mergeExecutionWorkspaceConfig( cleanupCommand: nextConfig.cleanupCommand, workspaceRuntime: nextConfig.workspaceRuntime, desiredState: nextConfig.desiredState, + serviceStates: nextConfig.serviceStates ?? null, }; } else { delete nextMetadata.config; diff --git a/server/src/services/index.ts b/server/src/services/index.ts index 909fb05e..c279dd13 100644 --- a/server/src/services/index.ts +++ b/server/src/services/index.ts @@ -19,6 +19,7 @@ export { financeService } from "./finance.js"; export { heartbeatService } from "./heartbeat.js"; export { dashboardService } from "./dashboard.js"; export { sidebarBadgeService } from "./sidebar-badges.js"; +export { sidebarPreferenceService } from "./sidebar-preferences.js"; export { inboxDismissalService } from "./inbox-dismissals.js"; export { accessService } from "./access.js"; export { boardAuthService } from "./board-auth.js"; diff --git a/server/src/services/project-workspace-runtime-config.ts b/server/src/services/project-workspace-runtime-config.ts index 8252fecd..9a36c957 100644 --- a/server/src/services/project-workspace-runtime-config.ts +++ b/server/src/services/project-workspace-runtime-config.ts @@ -12,6 +12,13 @@ function readDesiredState(value: unknown): ProjectWorkspaceRuntimeConfig["desire return value === "running" || value === "stopped" ? value : null; } +function readServiceStates(value: unknown): ProjectWorkspaceRuntimeConfig["serviceStates"] { + if (!isRecord(value)) return null; + const entries = Object.entries(value).filter(([, state]) => state === "running" || state === "stopped"); + if (entries.length === 0) return null; + return Object.fromEntries(entries) as ProjectWorkspaceRuntimeConfig["serviceStates"]; +} + export function readProjectWorkspaceRuntimeConfig( metadata: Record | null | undefined, ): ProjectWorkspaceRuntimeConfig | null { @@ -21,9 +28,10 @@ export function readProjectWorkspaceRuntimeConfig( const config: ProjectWorkspaceRuntimeConfig = { workspaceRuntime: cloneRecord(raw.workspaceRuntime), desiredState: readDesiredState(raw.desiredState), + serviceStates: readServiceStates(raw.serviceStates), }; - const hasConfig = config.workspaceRuntime !== null || config.desiredState !== null; + const hasConfig = config.workspaceRuntime !== null || config.desiredState !== null || config.serviceStates !== null; return hasConfig ? config : null; } @@ -35,6 +43,7 @@ export function mergeProjectWorkspaceRuntimeConfig( const current = readProjectWorkspaceRuntimeConfig(metadata) ?? { workspaceRuntime: null, desiredState: null, + serviceStates: null, }; if (patch === null) { @@ -47,9 +56,11 @@ export function mergeProjectWorkspaceRuntimeConfig( patch.workspaceRuntime !== undefined ? cloneRecord(patch.workspaceRuntime) : current.workspaceRuntime, desiredState: patch.desiredState !== undefined ? readDesiredState(patch.desiredState) : current.desiredState, + serviceStates: + patch.serviceStates !== undefined ? readServiceStates(patch.serviceStates) : current.serviceStates, }; - if (nextConfig.workspaceRuntime === null && nextConfig.desiredState === null) { + if (nextConfig.workspaceRuntime === null && nextConfig.desiredState === null && nextConfig.serviceStates === null) { delete nextMetadata.runtimeConfig; } else { nextMetadata.runtimeConfig = nextConfig; diff --git a/server/src/services/sidebar-preferences.ts b/server/src/services/sidebar-preferences.ts new file mode 100644 index 00000000..f029e43b --- /dev/null +++ b/server/src/services/sidebar-preferences.ts @@ -0,0 +1,97 @@ +import { and, eq } from "drizzle-orm"; +import type { Db } from "@paperclipai/db"; +import { + companyUserSidebarPreferences, + userSidebarPreferences, +} from "@paperclipai/db"; +import type { SidebarOrderPreference } from "@paperclipai/shared"; + +function normalizeOrderedIds(value: unknown): string[] { + if (!Array.isArray(value)) return []; + + const orderedIds: string[] = []; + const seen = new Set(); + for (const item of value) { + if (typeof item !== "string") continue; + const trimmed = item.trim(); + if (!trimmed || seen.has(trimmed)) continue; + seen.add(trimmed); + orderedIds.push(trimmed); + } + return orderedIds; +} + +function toPreference(orderedIds: unknown, updatedAt: Date | null): SidebarOrderPreference { + return { + orderedIds: normalizeOrderedIds(orderedIds), + updatedAt, + }; +} + +export function sidebarPreferenceService(db: Db) { + return { + async getCompanyOrder(userId: string): Promise { + const row = await db.query.userSidebarPreferences.findFirst({ + where: eq(userSidebarPreferences.userId, userId), + }); + return toPreference(row?.companyOrder ?? [], row?.updatedAt ?? null); + }, + + async upsertCompanyOrder(userId: string, orderedIds: string[]): Promise { + const now = new Date(); + const normalized = normalizeOrderedIds(orderedIds); + const [row] = await db + .insert(userSidebarPreferences) + .values({ + userId, + companyOrder: normalized, + updatedAt: now, + }) + .onConflictDoUpdate({ + target: [userSidebarPreferences.userId], + set: { + companyOrder: normalized, + updatedAt: now, + }, + }) + .returning(); + return toPreference(row?.companyOrder ?? normalized, row?.updatedAt ?? now); + }, + + async getProjectOrder(companyId: string, userId: string): Promise { + const row = await db.query.companyUserSidebarPreferences.findFirst({ + where: and( + eq(companyUserSidebarPreferences.companyId, companyId), + eq(companyUserSidebarPreferences.userId, userId), + ), + }); + return toPreference(row?.projectOrder ?? [], row?.updatedAt ?? null); + }, + + async upsertProjectOrder( + companyId: string, + userId: string, + orderedIds: string[], + ): Promise { + const now = new Date(); + const normalized = normalizeOrderedIds(orderedIds); + const [row] = await db + .insert(companyUserSidebarPreferences) + .values({ + companyId, + userId, + projectOrder: normalized, + updatedAt: now, + }) + .onConflictDoUpdate({ + target: [companyUserSidebarPreferences.companyId, companyUserSidebarPreferences.userId], + set: { + projectOrder: normalized, + updatedAt: now, + }, + }) + .returning(); + return toPreference(row?.projectOrder ?? normalized, row?.updatedAt ?? now); + }, + }; +} diff --git a/server/src/services/workspace-runtime.ts b/server/src/services/workspace-runtime.ts index 846af64f..87a093a5 100644 --- a/server/src/services/workspace-runtime.ts +++ b/server/src/services/workspace-runtime.ts @@ -8,6 +8,11 @@ import { setTimeout as delay } from "node:timers/promises"; import type { AdapterRuntimeServiceReport } from "@paperclipai/adapter-utils"; import type { Db } from "@paperclipai/db"; import { executionWorkspaces, projectWorkspaces, workspaceRuntimeServices } from "@paperclipai/db"; +import { + listWorkspaceServiceCommandDefinitions, + type WorkspaceRuntimeDesiredState, + type WorkspaceRuntimeServiceStateMap, +} from "@paperclipai/shared"; import { and, desc, eq, inArray } from "drizzle-orm"; import { asNumber, asString, parseObject, renderTemplate } from "../adapters/utils.js"; import { resolveHomeAwarePath } from "../home-paths.js"; @@ -26,7 +31,11 @@ import { readExecutionWorkspaceConfig } from "./execution-workspaces.js"; import { readProjectWorkspaceRuntimeConfig } from "./project-workspace-runtime-config.js"; export function resolveShell(): string { - return process.env.SHELL?.trim() || (process.platform === "win32" ? "sh" : "/bin/sh"); + const fallback = process.platform === "win32" ? "sh" : "/bin/sh"; + const shell = process.env.SHELL?.trim(); + if (!shell) return fallback; + if (path.isAbsolute(shell) && !existsSync(shell)) return fallback; + return shell; } export interface ExecutionWorkspaceInput { @@ -604,6 +613,56 @@ async function directoryExists(value: string) { return fs.stat(value).then((stats) => stats.isDirectory()).catch(() => false); } +async function listLinkedGitWorktreePaths(repoRoot: string): Promise> { + const output = await runGit(["worktree", "list", "--porcelain"], repoRoot); + const paths = new Set(); + for (const line of output.split("\n")) { + if (!line.startsWith("worktree ")) continue; + const worktree = line.slice("worktree ".length).trim(); + if (!worktree) continue; + paths.add(path.resolve(worktree)); + } + return paths; +} + +async function validateLinkedGitWorktree(input: { + repoRoot: string; + worktreePath: string; + expectedBranchName: string | null; +}): Promise<{ valid: true } | { valid: false; reason: string }> { + const resolvedWorktreePath = path.resolve(input.worktreePath); + const listedWorktrees = await listLinkedGitWorktreePaths(input.repoRoot); + if (!listedWorktrees.has(resolvedWorktreePath)) { + return { + valid: false, + reason: "path is not registered in `git worktree list`", + }; + } + + const worktreeTopLevel = await runGit(["rev-parse", "--show-toplevel"], resolvedWorktreePath).catch(() => null); + if (!worktreeTopLevel || path.resolve(worktreeTopLevel) !== resolvedWorktreePath) { + return { + valid: false, + reason: "git resolves this path to a different repository root", + }; + } + + if (input.expectedBranchName) { + const currentBranch = await runGit( + ["symbolic-ref", "--quiet", "--short", "HEAD"], + resolvedWorktreePath, + ).catch(() => null); + if (currentBranch !== input.expectedBranchName) { + return { + valid: false, + reason: `worktree HEAD is on "${currentBranch ?? ""}" instead of "${input.expectedBranchName}"`, + }; + } + } + + return { valid: true }; +} + function terminateChildProcess(child: ChildProcess) { if (!child.pid) return; if (process.platform !== "win32") { @@ -777,13 +836,13 @@ async function recordWorkspaceCommandOperation( ) { if (!recorder) { await runWorkspaceCommand(input); - return; + return null; } let stdout = ""; let stderr = ""; let code: number | null = null; - await recorder.recordOperation({ + const operation = await recorder.recordOperation({ phase: input.phase, command: input.command, cwd: input.cwd, @@ -818,7 +877,7 @@ async function recordWorkspaceCommandOperation( }, }); - if (code === 0) return; + if (code === 0) return operation; const details = [stderr.trim(), stdout.trim()].filter(Boolean).join("\n"); throw new Error( @@ -1004,18 +1063,32 @@ export async function realizeExecutionWorkspace(input: { }; } + async function validateReusableWorktree(reusablePath: string) { + return await validateLinkedGitWorktree({ + repoRoot, + worktreePath: reusablePath, + expectedBranchName: branchName, + }).catch(() => null); + } + const existingWorktree = await directoryExists(worktreePath); - if (existingWorktree && await isGitCheckout(worktreePath)) { - return await reuseExistingWorktree(worktreePath); + if (existingWorktree) { + const validation = await validateReusableWorktree(worktreePath); + if (validation?.valid) { + return await reuseExistingWorktree(worktreePath); + } + const reason = validation && !validation.valid ? ` (${validation.reason})` : ""; + throw new Error(`Configured worktree path "${worktreePath}" already exists and is not a reusable git worktree${reason}.`); } const registeredBranchWorktree = await findRegisteredGitWorktreeByBranch(repoRoot, branchName); - if (registeredBranchWorktree && await isGitCheckout(registeredBranchWorktree)) { - return await reuseExistingWorktree(registeredBranchWorktree); - } - - if (existingWorktree) { - throw new Error(`Configured worktree path "${worktreePath}" already exists and is not a git worktree.`); + if (registeredBranchWorktree) { + const validation = await validateReusableWorktree(registeredBranchWorktree); + if (validation?.valid) { + return await reuseExistingWorktree(registeredBranchWorktree); + } + const reason = validation && !validation.valid ? ` (${validation.reason})` : ""; + throw new Error(`Registered worktree for branch "${branchName}" at "${registeredBranchWorktree}" is not reusable${reason}.`); } try { @@ -1087,6 +1160,147 @@ export async function realizeExecutionWorkspace(input: { }; } +export async function ensurePersistedExecutionWorkspaceAvailable(input: { + base: ExecutionWorkspaceInput; + workspace: { + mode: string | null | undefined; + strategyType: string | null | undefined; + cwd: string | null | undefined; + providerRef: string | null | undefined; + projectId: string | null | undefined; + projectWorkspaceId: string | null | undefined; + repoUrl: string | null | undefined; + baseRef: string | null | undefined; + branchName: string | null | undefined; + config?: { + provisionCommand?: string | null; + } | null; + }; + issue: ExecutionWorkspaceIssueRef | null; + agent: ExecutionWorkspaceAgentRef; + recorder?: WorkspaceOperationRecorder | null; +}): Promise { + const cwd = asString(input.workspace.cwd ?? input.workspace.providerRef, "").trim(); + if (!cwd) return null; + + const strategy = input.workspace.strategyType === "git_worktree" ? "git_worktree" : "project_primary"; + const realized: RealizedExecutionWorkspace = { + baseCwd: input.base.baseCwd, + source: input.workspace.mode === "shared_workspace" ? "project_primary" : "task_session", + projectId: input.workspace.projectId ?? input.base.projectId, + workspaceId: input.workspace.projectWorkspaceId ?? input.base.workspaceId, + repoUrl: input.workspace.repoUrl ?? input.base.repoUrl, + repoRef: input.workspace.baseRef ?? input.base.repoRef, + strategy, + cwd, + branchName: input.workspace.branchName ?? null, + worktreePath: strategy === "git_worktree" ? (input.workspace.providerRef ?? cwd) : null, + warnings: [], + created: false, + }; + const provisionCommand = asString(input.workspace.config?.provisionCommand, "").trim(); + + if (strategy !== "git_worktree") { + return realized; + } + if (await directoryExists(cwd)) { + if (provisionCommand) { + const repoRoot = await runGit(["rev-parse", "--show-toplevel"], input.base.baseCwd); + await provisionExecutionWorktree({ + strategy: { + type: "git_worktree", + provisionCommand, + }, + base: input.base, + repoRoot, + worktreePath: realized.worktreePath ?? cwd, + branchName: realized.branchName ?? "", + issue: input.issue, + agent: input.agent, + created: false, + recorder: input.recorder ?? null, + }); + } + return realized; + } + + const repoRoot = await runGit(["rev-parse", "--show-toplevel"], input.base.baseCwd); + const worktreePath = realized.worktreePath ?? cwd; + const branchName = asString(input.workspace.branchName, "").trim(); + if (!branchName) { + throw new Error(`Execution workspace "${cwd}" is missing and cannot be restored because no branch name is recorded.`); + } + + await fs.mkdir(path.dirname(worktreePath), { recursive: true }); + await runGit(["worktree", "prune"], repoRoot).catch(() => {}); + + let created = false; + try { + await recordGitOperation(input.recorder, { + phase: "worktree_prepare", + args: ["worktree", "add", worktreePath, branchName], + cwd: repoRoot, + metadata: { + repoRoot, + worktreePath, + branchName, + baseRef: input.workspace.baseRef ?? input.base.repoRef ?? null, + created: false, + restored: true, + }, + successMessage: `Reattached missing git worktree at ${worktreePath}\n`, + failureLabel: `git worktree add ${worktreePath}`, + }); + } catch (error) { + if ( + !gitErrorIncludes(error, "invalid reference") + && !gitErrorIncludes(error, "not a commit") + && !gitErrorIncludes(error, "unknown revision") + ) { + throw error; + } + const baseRef = input.workspace.baseRef ?? await detectDefaultBranch(repoRoot) ?? "HEAD"; + await recordGitOperation(input.recorder, { + phase: "worktree_prepare", + args: ["worktree", "add", "-b", branchName, worktreePath, baseRef], + cwd: repoRoot, + metadata: { + repoRoot, + worktreePath, + branchName, + baseRef, + created: true, + restored: true, + }, + successMessage: `Recreated missing git worktree at ${worktreePath}\n`, + failureLabel: `git worktree add ${worktreePath}`, + }); + created = true; + } + + await provisionExecutionWorktree({ + strategy: { + type: "git_worktree", + ...(provisionCommand ? { provisionCommand } : {}), + }, + base: input.base, + repoRoot, + worktreePath, + branchName, + issue: input.issue, + agent: input.agent, + created, + recorder: input.recorder ?? null, + }); + + return { + ...realized, + cwd: worktreePath, + worktreePath, + created, + }; +} + export async function cleanupExecutionWorkspaceArtifacts(input: { workspace: { id: string; @@ -1380,6 +1594,83 @@ function resolveRuntimeServiceReuseIdentity(input: { }; } +function resolveWorkspaceCommandExecution(input: { + command: Record; + workspace: RealizedExecutionWorkspace; + agent: ExecutionWorkspaceAgentRef; + issue: ExecutionWorkspaceIssueRef | null; + adapterEnv: Record; +}) { + const name = + asString(input.command.name, "") + || asString(input.command.label, "") + || asString(input.command.title, "") + || "workspace command"; + const command = asString(input.command.command, ""); + const templateData = buildTemplateData({ + workspace: input.workspace, + agent: input.agent, + issue: input.issue, + adapterEnv: input.adapterEnv, + port: null, + }); + const cwd = resolveConfiguredPath( + renderTemplate(asString(input.command.cwd, "."), templateData), + input.workspace.cwd, + ); + const env = { + ...sanitizeRuntimeServiceBaseEnv(process.env), + ...input.adapterEnv, + ...renderRuntimeServiceEnv({ + envConfig: parseObject(input.command.env), + templateData, + }), + } as Record; + + return { + name, + command, + cwd, + env, + }; +} + +export async function runWorkspaceJobForControl(input: { + actor: ExecutionWorkspaceAgentRef; + issue: ExecutionWorkspaceIssueRef | null; + workspace: RealizedExecutionWorkspace; + command: Record; + adapterEnv?: Record; + recorder?: WorkspaceOperationRecorder | null; + metadata?: Record | null; +}) { + const resolved = resolveWorkspaceCommandExecution({ + command: input.command, + workspace: input.workspace, + agent: input.actor, + issue: input.issue, + adapterEnv: input.adapterEnv ?? {}, + }); + if (!resolved.command) { + throw new Error(`Workspace job "${resolved.name}" is missing command`); + } + + await ensureServerWorkspaceLinksCurrent(resolved.cwd); + return await recordWorkspaceCommandOperation(input.recorder, { + phase: "workspace_provision", + command: resolved.command, + cwd: resolved.cwd, + env: resolved.env, + label: `Workspace job "${resolved.name}"`, + metadata: { + workspaceCommandKind: "job", + workspaceCommandName: resolved.name, + ...(input.metadata ?? {}), + }, + successMessage: `Completed workspace job "${resolved.name}"\n`, + }); +} + function resolveServiceScopeId(input: { service: Record; workspace: RealizedExecutionWorkspace; @@ -1406,6 +1697,21 @@ function resolveServiceScopeId(input: { return { scopeType: "run" as const, scopeId: input.runId }; } +function looksLikeWorkspaceDevServerCommand(command: string) { + const normalized = command.trim().toLowerCase(); + if (!normalized) return false; + return /(?:^|\s)(?:pnpm|npm|yarn|bun)\s+(?:run\s+)?dev(?:\s|$)/.test(normalized); +} + +export function resolveWorkspaceRuntimeReadinessTimeoutSec(service: Record) { + const readiness = parseObject(service.readiness); + const explicitTimeoutSec = asNumber(readiness.timeoutSec, 0); + if (explicitTimeoutSec > 0) { + return Math.max(1, explicitTimeoutSec); + } + return looksLikeWorkspaceDevServerCommand(asString(service.command, "")) ? 90 : 30; +} + async function waitForReadiness(input: { service: Record; url: string | null; @@ -1413,7 +1719,7 @@ async function waitForReadiness(input: { const readiness = parseObject(input.service.readiness); const readinessType = asString(readiness.type, ""); if (readinessType !== "http" || !input.url) return; - const timeoutSec = Math.max(1, asNumber(readiness.timeoutSec, 30)); + const timeoutSec = resolveWorkspaceRuntimeReadinessTimeoutSec(input.service); const intervalMs = Math.max(100, asNumber(readiness.intervalMs, 500)); const deadline = Date.now() + timeoutSec * 1000; let lastError = "service did not become ready"; @@ -1735,6 +2041,11 @@ async function startLocalRuntimeService(input: { detached: process.platform !== "win32", stdio: ["ignore", "pipe", "pipe"], }); + const spawnErrorPromise = new Promise((_, reject) => { + child.once("error", (err) => { + reject(err); + }); + }); let stderrExcerpt = ""; let stdoutExcerpt = ""; child.stdout?.on("data", async (chunk) => { @@ -1749,7 +2060,10 @@ async function startLocalRuntimeService(input: { }); try { - await waitForReadiness({ service: input.service, url }); + await Promise.race([ + waitForReadiness({ service: input.service, url }), + spawnErrorPromise, + ]); } catch (err) { terminateChildProcess(child); throw new Error( @@ -1913,10 +2227,78 @@ function registerRuntimeService(db: Db | undefined, record: RuntimeServiceRecord } function readRuntimeServiceEntries(config: Record) { - const runtime = parseObject(config.workspaceRuntime); - return Array.isArray(runtime.services) - ? runtime.services.filter((entry): entry is Record => typeof entry === "object" && entry !== null) - : []; + return listWorkspaceServiceCommandDefinitions(parseObject(config.workspaceRuntime)) + .map((command) => command.rawConfig); +} + +export function listConfiguredRuntimeServiceEntries(config: Record) { + return readRuntimeServiceEntries(config); +} + +function readConfiguredServiceStates(config: Record) { + const raw = parseObject(config.serviceStates); + const states: WorkspaceRuntimeServiceStateMap = {}; + for (const [key, value] of Object.entries(raw)) { + if (value === "running" || value === "stopped") { + states[key] = value; + } + } + return states; +} + +export function buildWorkspaceRuntimeDesiredStatePatch(input: { + config: Record; + currentDesiredState: WorkspaceRuntimeDesiredState | null; + currentServiceStates: WorkspaceRuntimeServiceStateMap | null | undefined; + action: "start" | "stop" | "restart"; + serviceIndex?: number | null; +}): { + desiredState: WorkspaceRuntimeDesiredState; + serviceStates: WorkspaceRuntimeServiceStateMap | null; +} { + const configuredServices = listConfiguredRuntimeServiceEntries(input.config); + const fallbackState: WorkspaceRuntimeDesiredState = input.currentDesiredState === "running" ? "running" : "stopped"; + const nextServiceStates: WorkspaceRuntimeServiceStateMap = {}; + + for (let index = 0; index < configuredServices.length; index += 1) { + nextServiceStates[String(index)] = input.currentServiceStates?.[String(index)] ?? fallbackState; + } + + const nextState: WorkspaceRuntimeDesiredState = input.action === "stop" ? "stopped" : "running"; + if (input.serviceIndex === undefined || input.serviceIndex === null) { + for (let index = 0; index < configuredServices.length; index += 1) { + nextServiceStates[String(index)] = nextState; + } + } else if (input.serviceIndex >= 0 && input.serviceIndex < configuredServices.length) { + nextServiceStates[String(input.serviceIndex)] = nextState; + } + + const desiredState = Object.values(nextServiceStates).some((state) => state === "running") ? "running" : "stopped"; + + return { + desiredState, + serviceStates: Object.keys(nextServiceStates).length > 0 ? nextServiceStates : null, + }; +} + +function selectRuntimeServiceEntries(input: { + config: Record; + serviceIndex?: number | null; + respectDesiredStates?: boolean; + defaultDesiredState?: WorkspaceRuntimeDesiredState | null; + serviceStates?: WorkspaceRuntimeServiceStateMap | null; +}) { + const entries = listConfiguredRuntimeServiceEntries(input.config); + const states = input.serviceStates ?? readConfiguredServiceStates(input.config); + const fallbackState: WorkspaceRuntimeDesiredState = input.defaultDesiredState === "running" ? "running" : "stopped"; + + return entries.filter((_, index) => { + if (input.serviceIndex !== undefined && input.serviceIndex !== null) { + return index === input.serviceIndex; + } + if (!input.respectDesiredStates) return true; + return (states[String(index)] ?? fallbackState) === "running"; + }); } export async function ensureRuntimeServicesForRun(input: { @@ -2011,8 +2393,16 @@ export async function startRuntimeServicesForWorkspaceControl(input: { config: Record; adapterEnv: Record; onLog?: (stream: "stdout" | "stderr", chunk: string) => Promise; + serviceIndex?: number | null; + respectDesiredStates?: boolean; }): Promise { - const rawServices = readRuntimeServiceEntries(input.config); + const rawServices = selectRuntimeServiceEntries({ + config: input.config, + serviceIndex: input.serviceIndex, + respectDesiredStates: input.respectDesiredStates, + defaultDesiredState: input.config.desiredState === "running" ? "running" : "stopped", + serviceStates: readConfiguredServiceStates(input.config), + }); const refs: RuntimeServiceRef[] = []; const invocationId = input.invocationId ?? randomUUID(); @@ -2102,10 +2492,12 @@ export async function stopRuntimeServicesForExecutionWorkspace(input: { db?: Db; executionWorkspaceId: string; workspaceCwd?: string | null; + runtimeServiceId?: string | null; }) { const normalizedWorkspaceCwd = input.workspaceCwd ? path.resolve(input.workspaceCwd) : null; const matchingServiceIds = Array.from(runtimeServicesById.values()) .filter((record) => { + if (input.runtimeServiceId) return record.id === input.runtimeServiceId; if (record.executionWorkspaceId === input.executionWorkspaceId) return true; if (!normalizedWorkspaceCwd || !record.cwd) return false; const resolvedCwd = path.resolve(record.cwd); @@ -2121,19 +2513,37 @@ export async function stopRuntimeServicesForExecutionWorkspace(input: { } if (input.db) { - await markPersistedRuntimeServicesStoppedForExecutionWorkspace({ - db: input.db, - executionWorkspaceId: input.executionWorkspaceId, - }); + if (input.runtimeServiceId) { + const now = new Date(); + await input.db + .update(workspaceRuntimeServices) + .set({ + status: "stopped", + healthStatus: "unknown", + stoppedAt: now, + lastUsedAt: now, + updatedAt: now, + }) + .where(eq(workspaceRuntimeServices.id, input.runtimeServiceId)); + } else { + await markPersistedRuntimeServicesStoppedForExecutionWorkspace({ + db: input.db, + executionWorkspaceId: input.executionWorkspaceId, + }); + } } } export async function stopRuntimeServicesForProjectWorkspace(input: { db?: Db; projectWorkspaceId: string; + runtimeServiceId?: string | null; }) { const matchingServiceIds = Array.from(runtimeServicesById.values()) - .filter((record) => record.projectWorkspaceId === input.projectWorkspaceId && record.scopeType === "project_workspace") + .filter((record) => { + if (input.runtimeServiceId) return record.id === input.runtimeServiceId; + return record.projectWorkspaceId === input.projectWorkspaceId && record.scopeType === "project_workspace"; + }) .map((record) => record.id); for (const serviceId of matchingServiceIds) { @@ -2152,11 +2562,13 @@ export async function stopRuntimeServicesForProjectWorkspace(input: { updatedAt: now, }) .where( - and( - eq(workspaceRuntimeServices.projectWorkspaceId, input.projectWorkspaceId), - eq(workspaceRuntimeServices.scopeType, "project_workspace"), - inArray(workspaceRuntimeServices.status, ["starting", "running"]), - ), + input.runtimeServiceId + ? eq(workspaceRuntimeServices.id, input.runtimeServiceId) + : and( + eq(workspaceRuntimeServices.projectWorkspaceId, input.projectWorkspaceId), + eq(workspaceRuntimeServices.scopeType, "project_workspace"), + inArray(workspaceRuntimeServices.status, ["starting", "running"]), + ), ); } } @@ -2292,6 +2704,7 @@ export async function restartDesiredRuntimeServicesOnStartup(db: Db) { const projectWorkspaceRows = await db .select() .from(projectWorkspaces); + const projectWorkspaceRowsById = new Map(projectWorkspaceRows.map((row) => [row.id, row] as const)); for (const row of projectWorkspaceRows) { const runtimeConfig = readProjectWorkspaceRuntimeConfig((row.metadata as Record | null) ?? null); @@ -2316,8 +2729,13 @@ export async function restartDesiredRuntimeServicesOnStartup(db: Db) { warnings: [], created: false, }, - config: { workspaceRuntime: runtimeConfig.workspaceRuntime }, + config: { + workspaceRuntime: runtimeConfig.workspaceRuntime, + desiredState: runtimeConfig.desiredState, + serviceStates: runtimeConfig.serviceStates ?? null, + }, adapterEnv: {}, + respectDesiredStates: true, }); if (refs.length > 0) restarted += refs.filter((ref) => !ref.reused).length; } catch { @@ -2332,7 +2750,13 @@ export async function restartDesiredRuntimeServicesOnStartup(db: Db) { for (const row of executionWorkspaceRows) { const config = readExecutionWorkspaceConfig((row.metadata as Record | null) ?? null); - if (config?.desiredState !== "running" || !config.workspaceRuntime || !row.cwd) continue; + const inheritedRuntimeConfig = row.projectWorkspaceId + ? readProjectWorkspaceRuntimeConfig( + (projectWorkspaceRowsById.get(row.projectWorkspaceId)?.metadata as Record | null) ?? null, + )?.workspaceRuntime ?? null + : null; + const effectiveRuntimeConfig = config?.workspaceRuntime ?? inheritedRuntimeConfig; + if (config?.desiredState !== "running" || !effectiveRuntimeConfig || !row.cwd) continue; try { const refs = await startRuntimeServicesForWorkspaceControl({ @@ -2360,8 +2784,13 @@ export async function restartDesiredRuntimeServicesOnStartup(db: Db) { created: false, }, executionWorkspaceId: row.id, - config: { workspaceRuntime: config.workspaceRuntime }, + config: { + workspaceRuntime: effectiveRuntimeConfig, + desiredState: config.desiredState, + serviceStates: config.serviceStates ?? null, + }, adapterEnv: {}, + respectDesiredStates: true, }); if (refs.length > 0) restarted += refs.filter((ref) => !ref.reused).length; } catch { diff --git a/ui/src/App.tsx b/ui/src/App.tsx index 7aac9dfa..cfe0f487 100644 --- a/ui/src/App.tsx +++ b/ui/src/App.tsx @@ -162,6 +162,7 @@ function boardRoutes() { } /> } /> } /> + } /> } /> } /> } /> @@ -352,6 +353,7 @@ export function App() { } /> } /> } /> + } /> } /> } /> } /> diff --git a/ui/src/api/execution-workspaces.ts b/ui/src/api/execution-workspaces.ts index 3644af77..2c10a4dc 100644 --- a/ui/src/api/execution-workspaces.ts +++ b/ui/src/api/execution-workspaces.ts @@ -1,5 +1,11 @@ -import type { ExecutionWorkspace, ExecutionWorkspaceCloseReadiness, WorkspaceOperation } from "@paperclipai/shared"; +import type { + ExecutionWorkspace, + ExecutionWorkspaceCloseReadiness, + WorkspaceOperation, + WorkspaceRuntimeControlTarget, +} from "@paperclipai/shared"; import { api } from "./client"; +import { sanitizeWorkspaceRuntimeControlTarget } from "./workspace-runtime-control"; export const executionWorkspacesApi = { list: ( @@ -26,10 +32,23 @@ export const executionWorkspacesApi = { api.get(`/execution-workspaces/${id}/close-readiness`), listWorkspaceOperations: (id: string) => api.get(`/execution-workspaces/${id}/workspace-operations`), - controlRuntimeServices: (id: string, action: "start" | "stop" | "restart") => + controlRuntimeServices: ( + id: string, + action: "start" | "stop" | "restart", + target: WorkspaceRuntimeControlTarget = {}, + ) => api.post<{ workspace: ExecutionWorkspace; operation: WorkspaceOperation }>( `/execution-workspaces/${id}/runtime-services/${action}`, - {}, + sanitizeWorkspaceRuntimeControlTarget(target), + ), + controlRuntimeCommands: ( + id: string, + action: "start" | "stop" | "restart" | "run", + target: WorkspaceRuntimeControlTarget = {}, + ) => + api.post<{ workspace: ExecutionWorkspace; operation: WorkspaceOperation }>( + `/execution-workspaces/${id}/runtime-commands/${action}`, + sanitizeWorkspaceRuntimeControlTarget(target), ), update: (id: string, data: Record) => api.patch(`/execution-workspaces/${id}`, data), }; diff --git a/ui/src/api/index.ts b/ui/src/api/index.ts index 13c72f1f..5a73a7d1 100644 --- a/ui/src/api/index.ts +++ b/ui/src/api/index.ts @@ -15,5 +15,6 @@ export { dashboardApi } from "./dashboard"; export { heartbeatsApi } from "./heartbeats"; export { instanceSettingsApi } from "./instanceSettings"; export { sidebarBadgesApi } from "./sidebarBadges"; +export { sidebarPreferencesApi } from "./sidebarPreferences"; export { inboxDismissalsApi } from "./inboxDismissals"; export { companySkillsApi } from "./companySkills"; diff --git a/ui/src/api/projects.ts b/ui/src/api/projects.ts index 763718ff..e975f829 100644 --- a/ui/src/api/projects.ts +++ b/ui/src/api/projects.ts @@ -1,5 +1,11 @@ -import type { Project, ProjectWorkspace, WorkspaceOperation } from "@paperclipai/shared"; +import type { + Project, + ProjectWorkspace, + WorkspaceOperation, + WorkspaceRuntimeControlTarget, +} from "@paperclipai/shared"; import { api } from "./client"; +import { sanitizeWorkspaceRuntimeControlTarget } from "./workspace-runtime-control"; function withCompanyScope(path: string, companyId?: string) { if (!companyId) return path; @@ -32,10 +38,22 @@ export const projectsApi = { workspaceId: string, action: "start" | "stop" | "restart", companyId?: string, + target: WorkspaceRuntimeControlTarget = {}, ) => api.post<{ workspace: ProjectWorkspace; operation: WorkspaceOperation }>( projectPath(projectId, companyId, `/workspaces/${encodeURIComponent(workspaceId)}/runtime-services/${action}`), - {}, + sanitizeWorkspaceRuntimeControlTarget(target), + ), + controlWorkspaceCommands: ( + projectId: string, + workspaceId: string, + action: "start" | "stop" | "restart" | "run", + companyId?: string, + target: WorkspaceRuntimeControlTarget = {}, + ) => + api.post<{ workspace: ProjectWorkspace; operation: WorkspaceOperation }>( + projectPath(projectId, companyId, `/workspaces/${encodeURIComponent(workspaceId)}/runtime-commands/${action}`), + sanitizeWorkspaceRuntimeControlTarget(target), ), removeWorkspace: (projectId: string, workspaceId: string, companyId?: string) => api.delete(projectPath(projectId, companyId, `/workspaces/${encodeURIComponent(workspaceId)}`)), diff --git a/ui/src/api/sidebarPreferences.ts b/ui/src/api/sidebarPreferences.ts new file mode 100644 index 00000000..cd472774 --- /dev/null +++ b/ui/src/api/sidebarPreferences.ts @@ -0,0 +1,12 @@ +import type { SidebarOrderPreference, UpsertSidebarOrderPreference } from "@paperclipai/shared"; +import { api } from "./client"; + +export const sidebarPreferencesApi = { + getCompanyOrder: () => api.get("/sidebar-preferences/me"), + updateCompanyOrder: (data: UpsertSidebarOrderPreference) => + api.put("/sidebar-preferences/me", data), + getProjectOrder: (companyId: string) => + api.get(`/companies/${companyId}/sidebar-preferences/me`), + updateProjectOrder: (companyId: string, data: UpsertSidebarOrderPreference) => + api.put(`/companies/${companyId}/sidebar-preferences/me`, data), +}; diff --git a/ui/src/api/workspace-runtime-control.test.ts b/ui/src/api/workspace-runtime-control.test.ts new file mode 100644 index 00000000..ac7b2858 --- /dev/null +++ b/ui/src/api/workspace-runtime-control.test.ts @@ -0,0 +1,28 @@ +import { describe, expect, it } from "vitest"; +import { sanitizeWorkspaceRuntimeControlTarget } from "./workspace-runtime-control"; + +describe("sanitizeWorkspaceRuntimeControlTarget", () => { + it("drops unexpected keys while preserving the selected runtime target", () => { + const sanitized = sanitizeWorkspaceRuntimeControlTarget({ + workspaceCommandId: "web", + runtimeServiceId: "service-1", + serviceIndex: 2, + ...( { action: "start" } as Record ), + }); + + expect(sanitized).toEqual({ + workspaceCommandId: "web", + runtimeServiceId: "service-1", + serviceIndex: 2, + }); + expect("action" in sanitized).toBe(false); + }); + + it("normalizes an omitted target to nullable fields", () => { + expect(sanitizeWorkspaceRuntimeControlTarget()).toEqual({ + workspaceCommandId: null, + runtimeServiceId: null, + serviceIndex: null, + }); + }); +}); diff --git a/ui/src/api/workspace-runtime-control.ts b/ui/src/api/workspace-runtime-control.ts new file mode 100644 index 00000000..d99d6f1c --- /dev/null +++ b/ui/src/api/workspace-runtime-control.ts @@ -0,0 +1,11 @@ +import type { WorkspaceRuntimeControlTarget } from "@paperclipai/shared"; + +export function sanitizeWorkspaceRuntimeControlTarget( + target: WorkspaceRuntimeControlTarget = {}, +): WorkspaceRuntimeControlTarget { + return { + workspaceCommandId: target.workspaceCommandId ?? null, + runtimeServiceId: target.runtimeServiceId ?? null, + serviceIndex: target.serviceIndex ?? null, + }; +} diff --git a/ui/src/components/CompanyRail.tsx b/ui/src/components/CompanyRail.tsx index 0448c79a..496dd40a 100644 --- a/ui/src/components/CompanyRail.tsx +++ b/ui/src/components/CompanyRail.tsx @@ -1,6 +1,6 @@ -import { useCallback, useEffect, useMemo, useState } from "react"; +import { useCallback, useMemo } from "react"; import { Paperclip, Plus } from "lucide-react"; -import { useQueries } from "@tanstack/react-query"; +import { useQueries, useQuery } from "@tanstack/react-query"; import { DndContext, closestCenter, @@ -22,6 +22,8 @@ import { cn } from "../lib/utils"; import { queryKeys } from "../lib/queryKeys"; import { sidebarBadgesApi } from "../api/sidebarBadges"; import { heartbeatsApi } from "../api/heartbeats"; +import { authApi } from "../api/auth"; +import { useCompanyOrder } from "../hooks/useCompanyOrder"; import { useLocation, useNavigate } from "@/lib/router"; import { Tooltip, @@ -31,42 +33,6 @@ import { import type { Company } from "@paperclipai/shared"; import { CompanyPatternIcon } from "./CompanyPatternIcon"; -const ORDER_STORAGE_KEY = "paperclip.companyOrder"; - -function getStoredOrder(): string[] { - try { - const raw = localStorage.getItem(ORDER_STORAGE_KEY); - if (raw) return JSON.parse(raw); - } catch { /* ignore */ } - return []; -} - -function saveOrder(ids: string[]) { - localStorage.setItem(ORDER_STORAGE_KEY, JSON.stringify(ids)); -} - -/** Sort companies by stored order, appending any new ones at the end. */ -function sortByStoredOrder(companies: Company[]): Company[] { - const order = getStoredOrder(); - if (order.length === 0) return companies; - - const byId = new Map(companies.map((c) => [c.id, c])); - const sorted: Company[] = []; - - for (const id of order) { - const c = byId.get(id); - if (c) { - sorted.push(c); - byId.delete(id); - } - } - // Append any companies not in stored order - for (const c of byId.values()) { - sorted.push(c); - } - return sorted; -} - function SortableCompanyItem({ company, isSelected, @@ -103,6 +69,10 @@ function SortableCompanyItem({ { + if (isDragging) { + e.preventDefault(); + return; + } e.preventDefault(); onSelect(); }} @@ -164,6 +134,11 @@ export function CompanyRail() { () => companies.filter((company) => company.status !== "archived"), [companies], ); + const { data: session } = useQuery({ + queryKey: queryKeys.auth.session, + queryFn: () => authApi.getSession(), + }); + const currentUserId = session?.user?.id ?? session?.session?.userId ?? null; const companyIds = useMemo(() => sidebarCompanies.map((company) => company.id), [sidebarCompanies]); const liveRunsQueries = useQueries({ @@ -195,52 +170,10 @@ export function CompanyRail() { return result; }, [companyIds, sidebarBadgeQueries]); - // Maintain sorted order in local state, synced from companies + localStorage - const [orderedIds, setOrderedIds] = useState(() => - sortByStoredOrder(sidebarCompanies).map((c) => c.id) - ); - - // Re-sync orderedIds from localStorage whenever companies changes. - // Handles initial data load (companies starts as [] before query resolves) - // and subsequent refetches triggered by live updates. - useEffect(() => { - if (sidebarCompanies.length === 0) { - setOrderedIds([]); - return; - } - setOrderedIds(sortByStoredOrder(sidebarCompanies).map((c) => c.id)); - }, [sidebarCompanies]); - - // Sync order across tabs via the native storage event - useEffect(() => { - const handleStorage = (e: StorageEvent) => { - if (e.key !== ORDER_STORAGE_KEY) return; - try { - const ids: string[] = e.newValue ? JSON.parse(e.newValue) : []; - setOrderedIds(ids); - } catch { /* ignore malformed data */ } - }; - window.addEventListener("storage", handleStorage); - return () => window.removeEventListener("storage", handleStorage); - }, []); - - // Re-derive when companies change (new company added/removed) - const orderedCompanies = useMemo(() => { - const byId = new Map(sidebarCompanies.map((c) => [c.id, c])); - const result: Company[] = []; - for (const id of orderedIds) { - const c = byId.get(id); - if (c) { - result.push(c); - byId.delete(id); - } - } - // Append any new companies not yet in our order - for (const c of byId.values()) { - result.push(c); - } - return result; - }, [sidebarCompanies, orderedIds]); + const { orderedCompanies, persistOrder } = useCompanyOrder({ + companies: sidebarCompanies, + userId: currentUserId, + }); // Require 8px of movement before starting a drag to avoid interfering with clicks const sensors = useSensors( @@ -260,11 +193,9 @@ export function CompanyRail() { const newIndex = ids.indexOf(over.id as string); if (oldIndex === -1 || newIndex === -1) return; - const newIds = arrayMove(ids, oldIndex, newIndex); - setOrderedIds(newIds); - saveOrder(newIds); + persistOrder(arrayMove(ids, oldIndex, newIndex)); }, - [orderedCompanies] + [orderedCompanies, persistOrder] ); return ( diff --git a/ui/src/components/IssueFiltersPopover.test.tsx b/ui/src/components/IssueFiltersPopover.test.tsx new file mode 100644 index 00000000..d09c48c9 --- /dev/null +++ b/ui/src/components/IssueFiltersPopover.test.tsx @@ -0,0 +1,83 @@ +// @vitest-environment jsdom + +import { act } from "react"; +import type { ReactNode } from "react"; +import { createRoot } from "react-dom/client"; +import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; +import { IssueFiltersPopover } from "./IssueFiltersPopover"; +import { defaultIssueFilterState } from "../lib/issue-filters"; + +// eslint-disable-next-line @typescript-eslint/no-explicit-any +(globalThis as any).IS_REACT_ACT_ENVIRONMENT = true; + +vi.mock("@/components/ui/popover", () => ({ + Popover: ({ children }: { children: ReactNode }) =>
{children}
, + PopoverTrigger: ({ children }: { children: ReactNode }) => <>{children}, + PopoverContent: ({ children, className }: { children: ReactNode; className?: string }) => ( +
+ {children} +
+ ), +})); + +vi.mock("@/components/ui/button", () => ({ + Button: ({ children, ...props }: React.ButtonHTMLAttributes) => ( + + ), +})); + +vi.mock("@/components/ui/checkbox", () => ({ + Checkbox: ({ checked }: { checked?: boolean }) => , +})); + +vi.mock("./StatusIcon", () => ({ + StatusIcon: ({ status }: { status: string }) => {status}, +})); + +vi.mock("./PriorityIcon", () => ({ + PriorityIcon: ({ priority }: { priority: string }) => {priority}, +})); + +describe("IssueFiltersPopover", () => { + let container: HTMLDivElement; + + beforeEach(() => { + container = document.createElement("div"); + document.body.appendChild(container); + }); + + afterEach(() => { + document.body.innerHTML = ""; + }); + + it("uses a scrollable popover and a three-column desktop grid", () => { + const root = createRoot(container); + + act(() => { + root.render( + , + ); + }); + + const popoverContent = container.querySelector("[data-testid='popover-content']"); + expect(popoverContent).not.toBeNull(); + expect(popoverContent?.className).toContain("overflow-y-auto"); + expect(popoverContent?.className).toContain("max-h-[min(80vh,42rem)]"); + + const layoutGrid = Array.from(popoverContent?.querySelectorAll("div") ?? []).find((element) => + element.className.includes("md:grid-cols-3"), + ); + expect(layoutGrid?.className).toContain("grid-cols-1"); + }); +}); diff --git a/ui/src/components/IssueFiltersPopover.tsx b/ui/src/components/IssueFiltersPopover.tsx index 02f63acf..2d21d49a 100644 --- a/ui/src/components/IssueFiltersPopover.tsx +++ b/ui/src/components/IssueFiltersPopover.tsx @@ -80,7 +80,10 @@ export function IssueFiltersPopover({ ) : null} - +
Filters @@ -120,24 +123,24 @@ export function IssueFiltersPopover({
-
-
- Status -
- {issueStatusOrder.map((status) => ( - - ))} +
+
+
+ Status +
+ {issueStatusOrder.map((status) => ( + + ))} +
-
-
Priority
@@ -153,7 +156,9 @@ export function IssueFiltersPopover({ ))}
+
+
Assignee
@@ -186,6 +191,25 @@ export function IssueFiltersPopover({
+ {projects && projects.length > 0 ? ( +
+ Project +
+ {projects.map((project) => ( + + ))} +
+
+ ) : null} +
+ +
{labels && labels.length > 0 ? (
Labels @@ -204,23 +228,6 @@ export function IssueFiltersPopover({
) : null} - {projects && projects.length > 0 ? ( -
- Project -
- {projects.map((project) => ( - - ))} -
-
- ) : null} - {workspaces && workspaces.length > 0 ? (
Workspace diff --git a/ui/src/components/IssueGroupHeader.tsx b/ui/src/components/IssueGroupHeader.tsx new file mode 100644 index 00000000..4b747c9b --- /dev/null +++ b/ui/src/components/IssueGroupHeader.tsx @@ -0,0 +1,48 @@ +import type { ReactNode } from "react"; +import { ChevronRight } from "lucide-react"; +import { cn } from "../lib/utils"; + +type IssueGroupHeaderProps = { + label: string; + collapsible?: boolean; + collapsed?: boolean; + onToggle?: () => void; + trailing?: ReactNode; + className?: string; +}; + +export function IssueGroupHeader({ + label, + collapsible = false, + collapsed = false, + onToggle, + trailing, + className, +}: IssueGroupHeaderProps) { + return ( +
+ {collapsible ? ( + + ) : ( +
+ + {label} + +
+ )} + {trailing ?
{trailing}
: null} +
+ ); +} diff --git a/ui/src/components/IssuesList.test.tsx b/ui/src/components/IssuesList.test.tsx index 03f78feb..10ad1d88 100644 --- a/ui/src/components/IssuesList.test.tsx +++ b/ui/src/components/IssuesList.test.tsx @@ -351,8 +351,8 @@ describe("IssuesList", () => { }); }); - it("reuses the inbox issue column controls and persisted column visibility", async () => { - localStorage.setItem("paperclip:inbox:issue-columns", JSON.stringify(["id", "assignee"])); + it("uses context-scoped persisted column visibility", async () => { + localStorage.setItem("paperclip:test-issues:company-1:issue-columns", JSON.stringify(["id", "assignee"])); const assignedIssue = createIssue({ id: "issue-assigned", @@ -387,8 +387,41 @@ describe("IssuesList", () => { }); }); + it("preserves stored grouping across refresh when initial assignees are applied", async () => { + localStorage.setItem( + "paperclip:test-issues:company-1", + JSON.stringify({ groupBy: "status", sortField: "updated", sortDir: "desc" }), + ); + + const todoIssue = createIssue({ id: "issue-todo", title: "Alpha", status: "todo", assigneeAgentId: "agent-1" }); + const doneIssue = createIssue({ id: "issue-done", title: "Beta", status: "done", assigneeAgentId: "agent-1" }); + + const { root } = renderWithQueryClient( + undefined} + />, + container, + ); + + await waitForAssertion(() => { + expect(container.textContent).toContain("Todo"); + expect(container.textContent).toContain("Done"); + expect(container.textContent).toContain("Alpha"); + expect(container.textContent).toContain("Beta"); + }); + + act(() => { + root.unmount(); + }); + }); + it("filters the list to a single workspace when a workspace name is clicked", async () => { - localStorage.setItem("paperclip:inbox:issue-columns", JSON.stringify(["id", "workspace"])); + localStorage.setItem("paperclip:test-issues:company-1:issue-columns", JSON.stringify(["id", "workspace"])); mockInstanceSettingsApi.getExperimental.mockResolvedValue({ enableIsolatedWorkspaces: true }); mockExecutionWorkspacesApi.list.mockResolvedValue([ { diff --git a/ui/src/components/IssuesList.tsx b/ui/src/components/IssuesList.tsx index 64b37f26..2ac9b89c 100644 --- a/ui/src/components/IssuesList.tsx +++ b/ui/src/components/IssuesList.tsx @@ -26,10 +26,8 @@ import { import { DEFAULT_INBOX_ISSUE_COLUMNS, getAvailableInboxIssueColumns, - loadInboxIssueColumns, normalizeInboxIssueColumns, resolveIssueWorkspaceName, - saveInboxIssueColumns, type InboxIssueColumn, } from "../lib/inbox"; import { cn } from "../lib/utils"; @@ -43,13 +41,14 @@ import { import { StatusIcon } from "./StatusIcon"; import { EmptyState } from "./EmptyState"; import { Identity } from "./Identity"; +import { IssueGroupHeader } from "./IssueGroupHeader"; import { IssueFiltersPopover } from "./IssueFiltersPopover"; import { IssueRow } from "./IssueRow"; import { PageSkeleton } from "./PageSkeleton"; import { Button } from "@/components/ui/button"; import { Input } from "@/components/ui/input"; import { Popover, PopoverTrigger, PopoverContent } from "@/components/ui/popover"; -import { Collapsible, CollapsibleTrigger, CollapsibleContent } from "@/components/ui/collapsible"; +import { Collapsible, CollapsibleContent } from "@/components/ui/collapsible"; import { CircleDot, Plus, ArrowUpDown, Layers, Check, ChevronRight, List, ListTree, Columns3, User, Search } from "lucide-react"; import { KanbanBoard } from "./KanbanBoard"; import { buildIssueTree, countDescendants } from "../lib/issue-tree"; @@ -79,6 +78,7 @@ const defaultViewState: IssueViewState = { collapsedGroups: [], collapsedParents: [], }; + function getViewState(key: string): IssueViewState { try { const raw = localStorage.getItem(key); @@ -91,6 +91,43 @@ function saveViewState(key: string, state: IssueViewState) { localStorage.setItem(key, JSON.stringify(state)); } +function getInitialViewState(key: string, initialAssignees?: string[]): IssueViewState { + const stored = getViewState(key); + if (!initialAssignees) return stored; + return { + ...stored, + assignees: initialAssignees, + statuses: [], + }; +} + +function getIssueColumnsStorageKey(key: string): string { + return `${key}:issue-columns`; +} + +function loadIssueColumns(key: string): InboxIssueColumn[] { + try { + const raw = localStorage.getItem(getIssueColumnsStorageKey(key)); + if (raw === null) return DEFAULT_INBOX_ISSUE_COLUMNS; + const parsed = JSON.parse(raw); + if (!Array.isArray(parsed)) return DEFAULT_INBOX_ISSUE_COLUMNS; + return normalizeInboxIssueColumns(parsed); + } catch { + return DEFAULT_INBOX_ISSUE_COLUMNS; + } +} + +function saveIssueColumns(key: string, columns: InboxIssueColumn[]) { + try { + localStorage.setItem( + getIssueColumnsStorageKey(key), + JSON.stringify(normalizeInboxIssueColumns(columns)), + ); + } catch { + // Ignore localStorage failures. + } +} + function sortIssues(issues: Issue[], state: IssueViewState): Issue[] { const sorted = [...issues]; const dir = state.sortDir === "asc" ? 1 : -1; @@ -240,17 +277,13 @@ export function IssuesList({ // Scope the storage key per company so folding/view state is independent across companies. const scopedKey = selectedCompanyId ? `${viewStateKey}:${selectedCompanyId}` : viewStateKey; + const initialAssigneesKey = initialAssignees?.join("|") ?? ""; - const [viewState, setViewState] = useState(() => { - if (initialAssignees) { - return { ...defaultViewState, assignees: initialAssignees, statuses: [] }; - } - return getViewState(scopedKey); - }); + const [viewState, setViewState] = useState(() => getInitialViewState(scopedKey, initialAssignees)); const [assigneePickerIssueId, setAssigneePickerIssueId] = useState(null); const [assigneeSearch, setAssigneeSearch] = useState(""); const [issueSearch, setIssueSearch] = useState(initialSearch ?? ""); - const [visibleIssueColumns, setVisibleIssueColumns] = useState(loadInboxIssueColumns); + const [visibleIssueColumns, setVisibleIssueColumns] = useState(() => loadIssueColumns(scopedKey)); const deferredIssueSearch = useDeferredValue(issueSearch); const normalizedIssueSearch = deferredIssueSearch.trim().toLowerCase(); @@ -258,16 +291,23 @@ export function IssuesList({ setIssueSearch(initialSearch ?? ""); }, [initialSearch]); - // Reload view state from localStorage when company changes (scopedKey changes). - const prevScopedKey = useRef(scopedKey); + // Reload view state whenever the persisted context changes. + const prevViewStateContextKey = useRef(`${scopedKey}::${initialAssigneesKey}`); useEffect(() => { - if (prevScopedKey.current !== scopedKey) { - prevScopedKey.current = scopedKey; - setViewState(initialAssignees - ? { ...defaultViewState, assignees: initialAssignees, statuses: [] } - : getViewState(scopedKey)); + const nextContextKey = `${scopedKey}::${initialAssigneesKey}`; + if (prevViewStateContextKey.current !== nextContextKey) { + prevViewStateContextKey.current = nextContextKey; + setViewState(getInitialViewState(scopedKey, initialAssignees)); } - }, [scopedKey, initialAssignees]); + }, [scopedKey, initialAssignees, initialAssigneesKey]); + + const prevColumnsScopedKey = useRef(scopedKey); + useEffect(() => { + if (prevColumnsScopedKey.current !== scopedKey) { + prevColumnsScopedKey.current = scopedKey; + setVisibleIssueColumns(loadIssueColumns(scopedKey)); + } + }, [scopedKey]); const updateView = useCallback((patch: Partial) => { setViewState((prev) => { @@ -521,8 +561,8 @@ export function IssuesList({ const setIssueColumns = useCallback((next: InboxIssueColumn[]) => { const normalized = normalizeInboxIssueColumns(next); setVisibleIssueColumns(normalized); - saveInboxIssueColumns(normalized); - }, []); + saveIssueColumns(scopedKey, normalized); + }, [scopedKey]); const toggleIssueColumn = useCallback((column: InboxIssueColumn, enabled: boolean) => { if (enabled) { @@ -723,22 +763,28 @@ export function IssuesList({ }} > {group.label && ( -
- - - - {group.label} - - - -
+ { + updateView({ + collapsedGroups: viewState.collapsedGroups.includes(group.key) + ? viewState.collapsedGroups.filter((k) => k !== group.key) + : [...viewState.collapsedGroups, group.key], + }); + }} + trailing={( + + )} + /> )} {(() => { diff --git a/ui/src/components/ProjectWorkspaceSummaryCard.test.tsx b/ui/src/components/ProjectWorkspaceSummaryCard.test.tsx new file mode 100644 index 00000000..dc4388f5 --- /dev/null +++ b/ui/src/components/ProjectWorkspaceSummaryCard.test.tsx @@ -0,0 +1,192 @@ +// @vitest-environment jsdom + +import { act } from "react"; +import type { ComponentProps, ReactNode } from "react"; +import { createRoot } from "react-dom/client"; +import type { ExecutionWorkspace, Issue } from "@paperclipai/shared"; +import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; +import type { ProjectWorkspaceSummary } from "../lib/project-workspaces-tab"; +import { ProjectWorkspaceSummaryCard } from "./ProjectWorkspaceSummaryCard"; + +vi.mock("@/lib/router", () => ({ + Link: ({ children, to, ...props }: ComponentProps<"a"> & { to: string }) => {children}, +})); + +vi.mock("./IssuesQuicklook", () => ({ + IssuesQuicklook: ({ children }: { children: ReactNode }) => <>{children}, +})); + +vi.mock("./CopyText", () => ({ + CopyText: ({ children }: { children: ReactNode }) => {children}, +})); + +// eslint-disable-next-line @typescript-eslint/no-explicit-any +(globalThis as any).IS_REACT_ACT_ENVIRONMENT = true; + +function createIssue(overrides: Partial = {}): Issue { + return { + id: overrides.id ?? "issue-1", + companyId: overrides.companyId ?? "company-1", + projectId: overrides.projectId ?? "project-1", + projectWorkspaceId: overrides.projectWorkspaceId ?? null, + goalId: overrides.goalId ?? null, + parentId: overrides.parentId ?? null, + title: overrides.title ?? "Issue", + description: overrides.description ?? null, + status: overrides.status ?? "todo", + priority: overrides.priority ?? "medium", + assigneeAgentId: overrides.assigneeAgentId ?? null, + assigneeUserId: overrides.assigneeUserId ?? null, + checkoutRunId: overrides.checkoutRunId ?? null, + executionRunId: overrides.executionRunId ?? null, + executionAgentNameKey: overrides.executionAgentNameKey ?? null, + executionLockedAt: overrides.executionLockedAt ?? null, + createdByAgentId: overrides.createdByAgentId ?? null, + createdByUserId: overrides.createdByUserId ?? null, + issueNumber: overrides.issueNumber ?? 1, + identifier: overrides.identifier ?? "PAP-1", + requestDepth: overrides.requestDepth ?? 0, + billingCode: overrides.billingCode ?? null, + assigneeAdapterOverrides: overrides.assigneeAdapterOverrides ?? null, + executionWorkspaceId: overrides.executionWorkspaceId ?? null, + executionWorkspacePreference: overrides.executionWorkspacePreference ?? null, + executionWorkspaceSettings: overrides.executionWorkspaceSettings ?? null, + startedAt: overrides.startedAt ?? null, + completedAt: overrides.completedAt ?? null, + cancelledAt: overrides.cancelledAt ?? null, + hiddenAt: overrides.hiddenAt ?? null, + createdAt: overrides.createdAt ?? new Date("2026-04-12T00:00:00Z"), + updatedAt: overrides.updatedAt ?? new Date("2026-04-12T00:00:00Z"), + } as Issue; +} + +function createSummary(overrides: Partial = {}): ProjectWorkspaceSummary { + return { + key: overrides.key ?? "execution:workspace-1", + kind: overrides.kind ?? "execution_workspace", + workspaceId: overrides.workspaceId ?? "workspace-1", + workspaceName: overrides.workspaceName ?? "PAP-989-multi-user-implementation", + cwd: overrides.cwd ?? "/worktrees/PAP-989-multi-user-implementation", + branchName: overrides.branchName ?? "PAP-989-multi-user-implementation", + lastUpdatedAt: overrides.lastUpdatedAt ?? new Date("2026-04-12T00:00:00Z"), + projectWorkspaceId: overrides.projectWorkspaceId ?? "project-workspace-1", + executionWorkspaceId: overrides.executionWorkspaceId ?? "workspace-1", + executionWorkspaceStatus: overrides.executionWorkspaceStatus ?? "active", + serviceCount: overrides.serviceCount ?? 2, + runningServiceCount: overrides.runningServiceCount ?? 0, + primaryServiceUrl: overrides.primaryServiceUrl ?? "http://127.0.0.1:62474", + hasRuntimeConfig: overrides.hasRuntimeConfig ?? true, + issues: overrides.issues ?? [ + createIssue({ id: "issue-1", identifier: "PAP-1364" }), + createIssue({ id: "issue-2", identifier: "PAP-1367" }), + createIssue({ id: "issue-3", identifier: "PAP-1362" }), + createIssue({ id: "issue-4", identifier: "PAP-1363" }), + createIssue({ id: "issue-5", identifier: "PAP-1340" }), + ], + }; +} + +describe("ProjectWorkspaceSummaryCard", () => { + let container: HTMLDivElement; + + beforeEach(() => { + container = document.createElement("div"); + document.body.appendChild(container); + }); + + afterEach(() => { + document.body.innerHTML = ""; + }); + + it("renders a stacked mobile-friendly summary with metadata labels and compact issue pills", () => { + const root = createRoot(container); + act(() => { + root.render( + {}} + onCloseWorkspace={() => {}} + />, + ); + }); + + expect(container.textContent).toContain("Execution workspace"); + expect(container.textContent).toContain("Branch"); + expect(container.textContent).toContain("Path"); + expect(container.textContent).toContain("Service"); + expect(container.textContent).toContain("Linked issues"); + expect(container.textContent).toContain("Start services"); + expect(container.textContent).toContain("Close workspace"); + expect(container.textContent).toContain("+1 more"); + + const actions = container.querySelector('[data-testid="workspace-summary-actions"]'); + expect(actions?.className).toContain("flex-col"); + + act(() => { + root.unmount(); + }); + }); + + it("uses project workspace routes and omits close controls for project workspaces", () => { + const runtimeSpy = vi.fn(); + const closeSpy = vi.fn(); + const root = createRoot(container); + + act(() => { + root.render( + , + ); + }); + + const titleLink = container.querySelector("a[href='/projects/paperclip-app/workspaces/workspace-1']"); + expect(titleLink).not.toBeNull(); + expect(container.textContent).not.toContain("Close workspace"); + expect(container.textContent).not.toContain("Start services"); + + act(() => { + root.unmount(); + }); + }); + + it("shows retry close for cleanup failures", () => { + const root = createRoot(container); + + act(() => { + root.render( + {}} + onCloseWorkspace={() => {}} + />, + ); + }); + + expect(container.textContent).toContain("Retry close"); + + act(() => { + root.unmount(); + }); + }); +}); diff --git a/ui/src/components/ProjectWorkspaceSummaryCard.tsx b/ui/src/components/ProjectWorkspaceSummaryCard.tsx new file mode 100644 index 00000000..2c34951f --- /dev/null +++ b/ui/src/components/ProjectWorkspaceSummaryCard.tsx @@ -0,0 +1,230 @@ +import { Link } from "@/lib/router"; +import type { ExecutionWorkspace, Issue } from "@paperclipai/shared"; +import { Button } from "@/components/ui/button"; +import { CopyText } from "./CopyText"; +import { IssuesQuicklook } from "./IssuesQuicklook"; +import type { ProjectWorkspaceSummary } from "../lib/project-workspaces-tab"; +import { cn, projectWorkspaceUrl } from "../lib/utils"; +import { timeAgo } from "../lib/timeAgo"; +import { Copy, ExternalLink, FolderOpen, GitBranch, Loader2, Play, Square } from "lucide-react"; + +function workspaceKindLabel(kind: ProjectWorkspaceSummary["kind"]) { + return kind === "execution_workspace" ? "Execution workspace" : "Project workspace"; +} + +function truncatePath(path: string) { + const parts = path.split("/").filter(Boolean); + if (parts.length <= 3) return path; + return `…/${parts.slice(-3).join("/")}`; +} + +interface ProjectWorkspaceSummaryCardProps { + projectRef: string; + summary: ProjectWorkspaceSummary; + runtimeActionKey: string | null; + runtimeActionPending: boolean; + onRuntimeAction: (input: { + key: string; + kind: "project_workspace" | "execution_workspace"; + workspaceId: string; + action: "start" | "stop" | "restart"; + }) => void; + onCloseWorkspace: (input: { + id: string; + name: string; + status: ExecutionWorkspace["status"]; + }) => void; +} + +export function ProjectWorkspaceSummaryCard({ + projectRef, + summary, + runtimeActionKey, + runtimeActionPending, + onRuntimeAction, + onCloseWorkspace, +}: ProjectWorkspaceSummaryCardProps) { + const visibleIssues = summary.issues.slice(0, 4); + const hiddenIssueCount = Math.max(summary.issues.length - visibleIssues.length, 0); + const workspaceHref = + summary.kind === "project_workspace" + ? projectWorkspaceUrl({ id: projectRef, urlKey: projectRef }, summary.workspaceId) + : `/execution-workspaces/${summary.workspaceId}`; + const hasRunningServices = summary.runningServiceCount > 0; + const actionKey = `${summary.key}:${hasRunningServices ? "stop" : "start"}`; + + return ( +
+
+
+
+
+ + {workspaceKindLabel(summary.kind)} + + + Updated {timeAgo(summary.lastUpdatedAt)} + + {summary.serviceCount > 0 ? ( + + + {summary.runningServiceCount}/{summary.serviceCount} services + + ) : null} + {summary.executionWorkspaceStatus ? ( + + {summary.executionWorkspaceStatus.replace(/_/g, " ")} + + ) : null} +
+ + {summary.workspaceName} + +
+ +
+ {summary.hasRuntimeConfig ? ( + + ) : null} + {summary.kind === "execution_workspace" && summary.executionWorkspaceId && summary.executionWorkspaceStatus ? ( + + ) : null} +
+
+ +
+ + {summary.issues.length > 0 ? ( +
+
+ Linked issues +
+
+ {visibleIssues.map((issue) => ( + + ))} + {hiddenIssueCount > 0 ? ( + + +{hiddenIssueCount} more + + ) : null} +
+
+ ) : null} +
+
+ ); +} + +function IssuePill({ issue }: { issue: Issue }) { + return ( + + + {issue.identifier ?? issue.id.slice(0, 8)} + + + ); +} diff --git a/ui/src/components/SidebarProjects.tsx b/ui/src/components/SidebarProjects.tsx index 9688bc78..2f8b2aee 100644 --- a/ui/src/components/SidebarProjects.tsx +++ b/ui/src/components/SidebarProjects.tsx @@ -76,7 +76,11 @@ function SortableProjectItem({ { + onClick={(e) => { + if (isDragging) { + e.preventDefault(); + return; + } if (isMobile) setSidebarOpen(false); }} className={cn( diff --git a/ui/src/components/WorkspaceRuntimeControls.test.tsx b/ui/src/components/WorkspaceRuntimeControls.test.tsx new file mode 100644 index 00000000..7f8b0929 --- /dev/null +++ b/ui/src/components/WorkspaceRuntimeControls.test.tsx @@ -0,0 +1,269 @@ +// @vitest-environment jsdom + +import { act } from "react"; +import { createRoot } from "react-dom/client"; +import type { WorkspaceRuntimeService } from "@paperclipai/shared"; +import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; +import { + buildWorkspaceRuntimeControlItems, + buildWorkspaceRuntimeControlSections, + WorkspaceRuntimeControls, +} from "./WorkspaceRuntimeControls"; + +// eslint-disable-next-line @typescript-eslint/no-explicit-any +(globalThis as any).IS_REACT_ACT_ENVIRONMENT = true; + +function createRuntimeService(overrides: Partial = {}): WorkspaceRuntimeService { + return { + id: overrides.id ?? "service-1", + companyId: overrides.companyId ?? "company-1", + projectId: overrides.projectId ?? "project-1", + projectWorkspaceId: overrides.projectWorkspaceId ?? "workspace-1", + executionWorkspaceId: overrides.executionWorkspaceId ?? null, + issueId: overrides.issueId ?? null, + scopeType: overrides.scopeType ?? "project_workspace", + scopeId: overrides.scopeId ?? "workspace-1", + serviceName: overrides.serviceName ?? "web", + status: overrides.status ?? "stopped", + lifecycle: overrides.lifecycle ?? "shared", + reuseKey: overrides.reuseKey ?? null, + command: overrides.command ?? "pnpm dev", + cwd: overrides.cwd ?? "/repo", + port: overrides.port ?? null, + url: overrides.url ?? null, + provider: overrides.provider ?? "local_process", + providerRef: overrides.providerRef ?? null, + ownerAgentId: overrides.ownerAgentId ?? null, + startedByRunId: overrides.startedByRunId ?? null, + lastUsedAt: overrides.lastUsedAt ?? new Date("2026-04-12T00:00:00.000Z"), + startedAt: overrides.startedAt ?? new Date("2026-04-12T00:00:00.000Z"), + stoppedAt: overrides.stoppedAt ?? null, + stopPolicy: overrides.stopPolicy ?? null, + healthStatus: overrides.healthStatus ?? "unknown", + configIndex: overrides.configIndex ?? null, + createdAt: overrides.createdAt ?? new Date("2026-04-12T00:00:00.000Z"), + updatedAt: overrides.updatedAt ?? new Date("2026-04-12T00:00:00.000Z"), + }; +} + +describe("buildWorkspaceRuntimeControlSections", () => { + it("separates service and job commands while matching running services", () => { + const sections = buildWorkspaceRuntimeControlSections({ + runtimeConfig: { + commands: [ + { id: "web", name: "web", kind: "service", command: "pnpm dev" }, + { id: "db-migrate", name: "db:migrate", kind: "job", command: "pnpm db:migrate" }, + ], + }, + runtimeServices: [ + createRuntimeService({ id: "service-web", serviceName: "web", status: "running" }), + ], + canStartServices: true, + canRunJobs: true, + }); + + expect(sections.services).toHaveLength(1); + expect(sections.jobs).toHaveLength(1); + expect(sections.services[0]).toMatchObject({ + title: "web", + statusLabel: "running", + workspaceCommandId: "web", + runtimeServiceId: "service-web", + }); + expect(sections.jobs[0]).toMatchObject({ + title: "db:migrate", + statusLabel: "run once", + workspaceCommandId: "db-migrate", + }); + }); +}); + +describe("buildWorkspaceRuntimeControlItems", () => { + it("keeps the legacy flat export shape for stale importers", () => { + const items = buildWorkspaceRuntimeControlItems({ + runtimeConfig: { + commands: [ + { id: "web", name: "web", kind: "service", command: "pnpm dev" }, + { id: "db-migrate", name: "db:migrate", kind: "job", command: "pnpm db:migrate" }, + ], + }, + runtimeServices: [ + createRuntimeService({ id: "service-web", serviceName: "web", status: "running" }), + ], + canStartServices: true, + canRunJobs: true, + }); + + expect(items).toHaveLength(1); + expect(items[0]).toMatchObject({ + title: "web", + status: "running", + statusLabel: "running", + runtimeServiceId: "service-web", + }); + }); +}); + +describe("WorkspaceRuntimeControls", () => { + let container: HTMLDivElement; + + beforeEach(() => { + container = document.createElement("div"); + document.body.appendChild(container); + }); + + afterEach(() => { + document.body.innerHTML = ""; + }); + + it("renders service and job actions distinctly", () => { + const sections = buildWorkspaceRuntimeControlSections({ + runtimeConfig: { + commands: [ + { id: "web", name: "web", kind: "service", command: "pnpm dev" }, + { id: "db-migrate", name: "db:migrate", kind: "job", command: "pnpm db:migrate" }, + ], + }, + runtimeServices: [ + createRuntimeService({ id: "service-web", serviceName: "web", status: "running" }), + ], + canStartServices: true, + canRunJobs: true, + }); + + const root = createRoot(container); + act(() => { + root.render( + , + ); + }); + + const buttons = Array.from(container.querySelectorAll("button")).map((button) => button.textContent?.trim()); + expect(buttons).toEqual(["Stop", "Restart", "Run"]); + expect(container.textContent).toContain("Services"); + expect(container.textContent).toContain("Jobs"); + + act(() => root.unmount()); + }); + + it("shows disabled actions when local command prerequisites are missing", () => { + const sections = buildWorkspaceRuntimeControlSections({ + runtimeConfig: { + commands: [ + { id: "web", name: "web", kind: "service", command: "pnpm dev" }, + { id: "db-migrate", name: "db:migrate", kind: "job", command: "pnpm db:migrate" }, + ], + }, + runtimeServices: [], + canStartServices: false, + canRunJobs: false, + }); + + const root = createRoot(container); + act(() => { + root.render( + , + ); + }); + + const buttons = Array.from(container.querySelectorAll("button")); + expect(buttons.every((button) => button.hasAttribute("disabled"))).toBe(true); + expect(container.textContent).toContain("Add a workspace path first."); + + act(() => root.unmount()); + }); + + it("hides the disabled hint once services can already run", () => { + const sections = buildWorkspaceRuntimeControlSections({ + runtimeConfig: { + commands: [ + { id: "web", name: "web", kind: "service", command: "pnpm dev" }, + ], + }, + runtimeServices: [ + createRuntimeService({ id: "service-web", serviceName: "web", status: "running" }), + ], + canStartServices: true, + }); + + const root = createRoot(container); + act(() => { + root.render( + , + ); + }); + + expect(container.textContent).not.toContain("Add runtime settings first."); + + act(() => root.unmount()); + }); + + it("hides the health badge for stopped services", () => { + const sections = buildWorkspaceRuntimeControlSections({ + runtimeConfig: { + commands: [ + { id: "web", name: "web", kind: "service", command: "pnpm dev" }, + ], + }, + runtimeServices: [ + createRuntimeService({ id: "service-web", serviceName: "web", status: "stopped", healthStatus: "unknown" }), + ], + canStartServices: true, + }); + + const root = createRoot(container); + act(() => { + root.render( + , + ); + }); + + expect(container.textContent).not.toContain("unknown"); + + act(() => root.unmount()); + }); + + it("accepts the legacy items prop without crashing", () => { + const items = buildWorkspaceRuntimeControlItems({ + runtimeConfig: { + commands: [ + { id: "web", name: "web", kind: "service", command: "pnpm dev" }, + ], + }, + runtimeServices: [], + canStartServices: false, + }); + + const root = createRoot(container); + act(() => { + root.render( + , + ); + }); + + expect(container.textContent).toContain("Services"); + expect(container.textContent).toContain("Add runtime settings first."); + expect(Array.from(container.querySelectorAll("button")).map((button) => button.textContent?.trim())).toEqual(["Start"]); + + act(() => root.unmount()); + }); +}); diff --git a/ui/src/components/WorkspaceRuntimeControls.tsx b/ui/src/components/WorkspaceRuntimeControls.tsx new file mode 100644 index 00000000..ef75181f --- /dev/null +++ b/ui/src/components/WorkspaceRuntimeControls.tsx @@ -0,0 +1,439 @@ +import type { + WorkspaceCommandDefinition, + WorkspaceRuntimeControlTarget, + WorkspaceRuntimeService, +} from "@paperclipai/shared"; +import { + listWorkspaceCommandDefinitions, + matchWorkspaceRuntimeServiceToCommand, +} from "@paperclipai/shared"; +import { Activity, ExternalLink, Loader2, Play, RotateCcw, Square } from "lucide-react"; +import { Button } from "@/components/ui/button"; +import { cn } from "@/lib/utils"; + +export type WorkspaceRuntimeAction = "start" | "stop" | "restart" | "run"; + +export type WorkspaceRuntimeControlRequest = WorkspaceRuntimeControlTarget & { + action: WorkspaceRuntimeAction; +}; + +export type WorkspaceRuntimeControlItem = { + key: string; + title: string; + kind: "service" | "job"; + statusLabel: string; + lifecycle: "shared" | "ephemeral" | null; + healthStatus: "unknown" | "healthy" | "unhealthy" | null; + command: string | null; + cwd: string | null; + port: number | null; + url: string | null; + canStart: boolean; + canRun: boolean; + workspaceCommandId?: string | null; + runtimeServiceId?: string | null; + serviceIndex?: number | null; + disabledReason?: string | null; +}; + +export type WorkspaceRuntimeControlSections = { + services: WorkspaceRuntimeControlItem[]; + jobs: WorkspaceRuntimeControlItem[]; + otherServices: WorkspaceRuntimeControlItem[]; +}; + +type LegacyWorkspaceRuntimeControlItem = WorkspaceRuntimeControlItem & { + status?: string | null; +}; + +type WorkspaceRuntimeControlsProps = { + sections: WorkspaceRuntimeControlSections; + items?: never; + isPending?: boolean; + pendingRequest?: WorkspaceRuntimeControlRequest | null; + serviceEmptyMessage?: string; + jobEmptyMessage?: string; + emptyMessage?: never; + disabledHint?: string | null; + onAction: (request: WorkspaceRuntimeControlRequest) => void; + className?: string; +} | { + sections?: never; + items: LegacyWorkspaceRuntimeControlItem[]; + isPending?: boolean; + pendingRequest?: WorkspaceRuntimeControlRequest | null; + serviceEmptyMessage?: never; + jobEmptyMessage?: never; + emptyMessage?: string; + disabledHint?: string | null; + onAction: (request: WorkspaceRuntimeControlRequest) => void; + className?: string; +}; + +export function hasRunningRuntimeServices( + runtimeServices: Array<{ status: string }> | null | undefined, +) { + return (runtimeServices ?? []).some((service) => service.status === "starting" || service.status === "running"); +} + +function buildServiceItem( + command: WorkspaceCommandDefinition, + runtimeService: WorkspaceRuntimeService | null, + canStartServices: boolean, +): WorkspaceRuntimeControlItem { + return { + key: `command:${command.id}:${runtimeService?.id ?? "idle"}`, + title: command.name, + kind: "service", + statusLabel: runtimeService?.status ?? "stopped", + lifecycle: runtimeService?.lifecycle ?? command.lifecycle, + healthStatus: runtimeService?.healthStatus ?? "unknown", + command: runtimeService?.command ?? command.command, + cwd: runtimeService?.cwd ?? command.cwd, + port: runtimeService?.port ?? null, + url: runtimeService?.url ?? null, + canStart: canStartServices && !command.disabledReason, + canRun: false, + workspaceCommandId: command.id, + runtimeServiceId: runtimeService?.id ?? null, + serviceIndex: command.serviceIndex, + disabledReason: command.disabledReason, + }; +} + +function buildJobItem( + command: WorkspaceCommandDefinition, + canRunJobs: boolean, +): WorkspaceRuntimeControlItem { + return { + key: `command:${command.id}`, + title: command.name, + kind: "job", + statusLabel: "run once", + lifecycle: null, + healthStatus: null, + command: command.command, + cwd: command.cwd, + port: null, + url: null, + canStart: false, + canRun: canRunJobs && !command.disabledReason && Boolean(command.command), + workspaceCommandId: command.id, + runtimeServiceId: null, + serviceIndex: null, + disabledReason: command.disabledReason ?? (!command.command ? "This job is missing a command." : null), + }; +} + +export function buildWorkspaceRuntimeControlSections(input: { + runtimeConfig: Record | null | undefined; + runtimeServices: WorkspaceRuntimeService[] | null | undefined; + canStartServices: boolean; + canRunJobs?: boolean; +}): WorkspaceRuntimeControlSections { + const commands = listWorkspaceCommandDefinitions(input.runtimeConfig); + const runtimeServices = [...(input.runtimeServices ?? [])]; + const matchedRuntimeServiceIds = new Set(); + const services: WorkspaceRuntimeControlItem[] = []; + const jobs: WorkspaceRuntimeControlItem[] = []; + + for (const command of commands) { + if (command.kind === "job") { + jobs.push(buildJobItem(command, input.canRunJobs ?? input.canStartServices)); + continue; + } + + const runtimeService = matchWorkspaceRuntimeServiceToCommand(command, runtimeServices); + if (runtimeService) matchedRuntimeServiceIds.add(runtimeService.id); + services.push(buildServiceItem(command, runtimeService, input.canStartServices)); + } + + const otherServices = runtimeServices + .filter((runtimeService) => !matchedRuntimeServiceIds.has(runtimeService.id)) + .map((runtimeService) => ({ + key: `runtime:${runtimeService.id}`, + title: runtimeService.serviceName, + kind: "service" as const, + statusLabel: runtimeService.status, + lifecycle: runtimeService.lifecycle, + healthStatus: runtimeService.healthStatus, + command: runtimeService.command ?? null, + cwd: runtimeService.cwd ?? null, + port: runtimeService.port ?? null, + url: runtimeService.url ?? null, + canStart: false, + canRun: false, + workspaceCommandId: null, + runtimeServiceId: runtimeService.id, + serviceIndex: runtimeService.configIndex ?? null, + disabledReason: "This runtime service no longer matches a configured workspace command.", + })); + + return { + services, + jobs, + otherServices, + }; +} + +export function buildWorkspaceRuntimeControlItems(input: { + runtimeConfig: Record | null | undefined; + runtimeServices: WorkspaceRuntimeService[] | null | undefined; + canStartServices: boolean; + canRunJobs?: boolean; +}): LegacyWorkspaceRuntimeControlItem[] { + return buildWorkspaceRuntimeControlSections(input).services.map((item) => ({ + ...item, + status: item.statusLabel, + })); +} + +function requestMatchesPending( + pendingRequest: WorkspaceRuntimeControlRequest | null | undefined, + nextRequest: WorkspaceRuntimeControlRequest, +) { + return pendingRequest?.action === nextRequest.action + && (pendingRequest?.workspaceCommandId ?? null) === (nextRequest.workspaceCommandId ?? null) + && (pendingRequest?.runtimeServiceId ?? null) === (nextRequest.runtimeServiceId ?? null) + && (pendingRequest?.serviceIndex ?? null) === (nextRequest.serviceIndex ?? null); +} + +function buildRequest(item: WorkspaceRuntimeControlItem, action: WorkspaceRuntimeAction): WorkspaceRuntimeControlRequest { + return { + action, + workspaceCommandId: item.workspaceCommandId ?? null, + runtimeServiceId: item.runtimeServiceId ?? null, + serviceIndex: item.serviceIndex ?? null, + }; +} + +function CommandActionButtons({ + item, + isPending, + pendingRequest, + onAction, +}: { + item: WorkspaceRuntimeControlItem; + isPending: boolean; + pendingRequest: WorkspaceRuntimeControlRequest | null | undefined; + onAction: (request: WorkspaceRuntimeControlRequest) => void; +}) { + const actions: WorkspaceRuntimeAction[] = + item.kind === "job" + ? ["run"] + : item.statusLabel === "running" || item.statusLabel === "starting" + ? ["stop", ...(item.canStart ? ["restart" as const] : [])] + : ["start"]; + + return ( +
+ {actions.map((action) => { + const request = buildRequest(item, action); + const Icon = action === "stop" ? Square : action === "restart" ? RotateCcw : Play; + const label = action === "run" + ? "Run" + : action === "start" + ? "Start" + : action === "stop" + ? "Stop" + : "Restart"; + const showSpinner = isPending && requestMatchesPending(pendingRequest, request); + const disabled = + isPending + || (action === "run" && !item.canRun) + || ((action === "start" || action === "restart") && !item.canStart); + + return ( + + ); + })} +
+ ); +} + +function CommandSection({ + title, + description, + items, + emptyMessage, + disabledHint, + isPending, + pendingRequest, + onAction, +}: { + title: string; + description: string; + items: WorkspaceRuntimeControlItem[]; + emptyMessage: string; + disabledHint?: string | null; + isPending: boolean; + pendingRequest: WorkspaceRuntimeControlRequest | null | undefined; + onAction: (request: WorkspaceRuntimeControlRequest) => void; +}) { + return ( +
+
+
{title}
+

{description}

+
+ {items.length === 0 ? ( +
+ {emptyMessage} + {disabledHint ?

{disabledHint}

: null} +
+ ) : ( +
+ {items.map((item) => ( +
+
+
+
+
{item.title}
+
+ {item.kind} · {item.statusLabel} + {item.lifecycle ? ` · ${item.lifecycle}` : ""} +
+
+ +
+
+ {item.url ? ( + + {item.url} + + + ) : null} + {item.port ?
Port {item.port}
: null} + {item.command ?
{item.command}
: null} + {item.cwd ?
{item.cwd}
: null} + {item.disabledReason ?
{item.disabledReason}
: null} +
+ {item.healthStatus && item.statusLabel !== "stopped" ? ( +
+ + {item.healthStatus} + +
+ ) : null} +
+
+ ))} +
+ )} +
+ ); +} + +export function WorkspaceRuntimeControls({ + sections, + items, + isPending = false, + pendingRequest = null, + serviceEmptyMessage = "No services are configured for this workspace.", + jobEmptyMessage = "No one-shot jobs are configured for this workspace.", + emptyMessage, + disabledHint = null, + onAction, + className, +}: WorkspaceRuntimeControlsProps) { + const resolvedSections = sections ?? { + services: (items ?? []).map((item) => ({ + ...item, + statusLabel: item.statusLabel ?? item.status ?? "stopped", + })), + jobs: [], + otherServices: [], + }; + const resolvedServiceEmptyMessage = emptyMessage ?? serviceEmptyMessage; + const runningCount = resolvedSections.services.filter( + (item) => item.statusLabel === "running" || item.statusLabel === "starting", + ).length; + const visibleDisabledHint = runningCount > 0 || disabledHint === null ? null : disabledHint; + + return ( +
+
+
+
Workspace commands
+
+ 0 + ? "border-emerald-500/30 bg-emerald-500/10 text-emerald-700 dark:text-emerald-300" + : "border-border bg-background text-muted-foreground", + )} + > + + {runningCount > 0 ? `${runningCount} services running` : "No services running"} + + + {resolvedSections.jobs.length > 0 + ? `${resolvedSections.jobs.length} job${resolvedSections.jobs.length === 1 ? "" : "s"} available to run on demand.` + : "Each command can be controlled independently."} + +
+ {visibleDisabledHint ?

{visibleDisabledHint}

: null} +
+
+ + + + + + {resolvedSections.otherServices.length > 0 ? ( + + ) : null} +
+ ); +} diff --git a/ui/src/hooks/useCompanyOrder.ts b/ui/src/hooks/useCompanyOrder.ts new file mode 100644 index 00000000..ae42cc78 --- /dev/null +++ b/ui/src/hooks/useCompanyOrder.ts @@ -0,0 +1,100 @@ +import { useCallback, useEffect, useMemo, useState } from "react"; +import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; +import type { Company } from "@paperclipai/shared"; +import { sidebarPreferencesApi } from "../api/sidebarPreferences"; +import { queryKeys } from "../lib/queryKeys"; + +function areEqual(a: string[], b: string[]) { + if (a.length !== b.length) return false; + for (let i = 0; i < a.length; i += 1) { + if (a[i] !== b[i]) return false; + } + return true; +} + +function sortCompaniesByOrder(companies: Company[], orderedIds: string[]): Company[] { + if (companies.length === 0) return []; + if (orderedIds.length === 0) return companies; + + const byId = new Map(companies.map((company) => [company.id, company])); + const sorted: Company[] = []; + + for (const id of orderedIds) { + const company = byId.get(id); + if (!company) continue; + sorted.push(company); + byId.delete(id); + } + for (const company of byId.values()) { + sorted.push(company); + } + return sorted; +} + +function buildOrderIds(companies: Company[], orderedIds: string[]) { + return sortCompaniesByOrder(companies, orderedIds).map((company) => company.id); +} + +type UseCompanyOrderParams = { + companies: Company[]; + userId: string | null | undefined; +}; + +export function useCompanyOrder({ companies, userId }: UseCompanyOrderParams) { + const queryClient = useQueryClient(); + const queryKey = useMemo( + () => queryKeys.sidebarPreferences.companyOrder(userId ?? "__anon__"), + [userId], + ); + + const { data } = useQuery({ + queryKey, + queryFn: () => sidebarPreferencesApi.getCompanyOrder(), + enabled: Boolean(userId), + }); + + const [orderedIds, setOrderedIds] = useState(() => buildOrderIds(companies, [])); + + useEffect(() => { + const nextIds = buildOrderIds(companies, data?.orderedIds ?? []); + setOrderedIds((current) => (areEqual(current, nextIds) ? current : nextIds)); + }, [companies, data?.orderedIds]); + + const mutation = useMutation({ + mutationFn: (nextIds: string[]) => sidebarPreferencesApi.updateCompanyOrder({ orderedIds: nextIds }), + onSuccess: (preference) => { + queryClient.setQueryData(queryKey, preference); + }, + }); + + const orderedCompanies = useMemo( + () => sortCompaniesByOrder(companies, orderedIds), + [companies, orderedIds], + ); + + const persistOrder = useCallback( + (ids: string[]) => { + const idSet = new Set(companies.map((company) => company.id)); + const filtered = ids.filter((id) => idSet.has(id)); + for (const company of companies) { + if (!filtered.includes(company.id)) filtered.push(company.id); + } + + setOrderedIds((current) => (areEqual(current, filtered) ? current : filtered)); + if (!userId) return; + + queryClient.setQueryData(queryKey, (current: { orderedIds?: string[]; updatedAt?: Date | null } | undefined) => ({ + orderedIds: filtered, + updatedAt: current?.updatedAt ?? null, + })); + mutation.mutate(filtered); + }, + [companies, mutation, queryClient, queryKey, userId], + ); + + return { + orderedCompanies, + orderedIds, + persistOrder, + }; +} diff --git a/ui/src/hooks/useProjectOrder.ts b/ui/src/hooks/useProjectOrder.ts index 5b9edee4..fe2d51a0 100644 --- a/ui/src/hooks/useProjectOrder.ts +++ b/ui/src/hooks/useProjectOrder.ts @@ -1,12 +1,9 @@ import { useCallback, useEffect, useMemo, useState } from "react"; +import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; import type { Project } from "@paperclipai/shared"; -import { - getProjectOrderStorageKey, - PROJECT_ORDER_UPDATED_EVENT, - readProjectOrder, - sortProjectsByStoredOrder, - writeProjectOrder, -} from "../lib/project-order"; +import { sidebarPreferencesApi } from "../api/sidebarPreferences"; +import { sortProjectsByStoredOrder } from "../lib/project-order"; +import { queryKeys } from "../lib/queryKeys"; type UseProjectOrderParams = { projects: Project[]; @@ -14,11 +11,6 @@ type UseProjectOrderParams = { userId: string | null | undefined; }; -type ProjectOrderUpdatedDetail = { - storageKey: string; - orderedIds: string[]; -}; - function areEqual(a: string[], b: string[]) { if (a.length !== b.length) return false; for (let i = 0; i < a.length; i += 1) { @@ -32,48 +24,33 @@ function buildOrderIds(projects: Project[], orderedIds: string[]) { } export function useProjectOrder({ projects, companyId, userId }: UseProjectOrderParams) { - const storageKey = useMemo(() => { - if (!companyId) return null; - return getProjectOrderStorageKey(companyId, userId); - }, [companyId, userId]); + const queryClient = useQueryClient(); + const queryKey = useMemo( + () => queryKeys.sidebarPreferences.projectOrder(companyId ?? "__none__", userId ?? "__anon__"), + [companyId, userId], + ); + + const { data } = useQuery({ + queryKey, + queryFn: () => sidebarPreferencesApi.getProjectOrder(companyId!), + enabled: Boolean(companyId && userId), + }); const [orderedIds, setOrderedIds] = useState(() => { - if (!storageKey) return projects.map((project) => project.id); - return buildOrderIds(projects, readProjectOrder(storageKey)); + return buildOrderIds(projects, []); }); useEffect(() => { - const nextIds = storageKey - ? buildOrderIds(projects, readProjectOrder(storageKey)) - : projects.map((project) => project.id); + const nextIds = buildOrderIds(projects, data?.orderedIds ?? []); setOrderedIds((current) => (areEqual(current, nextIds) ? current : nextIds)); - }, [projects, storageKey]); + }, [data?.orderedIds, projects]); - useEffect(() => { - if (!storageKey) return; - - const syncFromIds = (ids: string[]) => { - const nextIds = buildOrderIds(projects, ids); - setOrderedIds((current) => (areEqual(current, nextIds) ? current : nextIds)); - }; - - const onStorage = (event: StorageEvent) => { - if (event.key !== storageKey) return; - syncFromIds(readProjectOrder(storageKey)); - }; - const onCustomEvent = (event: Event) => { - const detail = (event as CustomEvent).detail; - if (!detail || detail.storageKey !== storageKey) return; - syncFromIds(detail.orderedIds); - }; - - window.addEventListener("storage", onStorage); - window.addEventListener(PROJECT_ORDER_UPDATED_EVENT, onCustomEvent); - return () => { - window.removeEventListener("storage", onStorage); - window.removeEventListener(PROJECT_ORDER_UPDATED_EVENT, onCustomEvent); - }; - }, [projects, storageKey]); + const mutation = useMutation({ + mutationFn: (nextIds: string[]) => sidebarPreferencesApi.updateProjectOrder(companyId!, { orderedIds: nextIds }), + onSuccess: (preference) => { + queryClient.setQueryData(queryKey, preference); + }, + }); const orderedProjects = useMemo( () => sortProjectsByStoredOrder(projects, orderedIds), @@ -89,11 +66,15 @@ export function useProjectOrder({ projects, companyId, userId }: UseProjectOrder } setOrderedIds((current) => (areEqual(current, filtered) ? current : filtered)); - if (storageKey) { - writeProjectOrder(storageKey, filtered); - } + if (!companyId || !userId) return; + + queryClient.setQueryData(queryKey, (current: { orderedIds?: string[]; updatedAt?: Date | null } | undefined) => ({ + orderedIds: filtered, + updatedAt: current?.updatedAt ?? null, + })); + mutation.mutate(filtered); }, - [projects, storageKey], + [companyId, mutation, projects, queryClient, queryKey, userId], ); return { @@ -102,4 +83,3 @@ export function useProjectOrder({ projects, companyId, userId }: UseProjectOrder persistOrder, }; } - diff --git a/ui/src/lib/inbox.test.ts b/ui/src/lib/inbox.test.ts index 50a1f0b3..37f0fc0f 100644 --- a/ui/src/lib/inbox.test.ts +++ b/ui/src/lib/inbox.test.ts @@ -12,11 +12,13 @@ import type { } from "@paperclipai/shared"; import { DEFAULT_INBOX_ISSUE_COLUMNS, + buildInboxKeyboardNavEntries, buildInboxDismissedAtByKey, computeInboxBadgeData, filterInboxIssues, getArchivedInboxSearchIssues, getAvailableInboxIssueColumns, + getInboxWorkItemKey, getApprovalsForTab, getInboxWorkItems, getInboxKeyboardSelectionIndex, @@ -28,16 +30,22 @@ import { isMineInboxTab, loadInboxFilterPreferences, loadInboxIssueColumns, + loadInboxWorkItemGroupBy, + loadCollapsedInboxGroupKeys, loadLastInboxTab, matchesInboxIssueSearch, normalizeInboxIssueColumns, RECENT_ISSUES_LIMIT, resolveInboxNestingEnabled, resolveIssueWorkspaceName, + resolveIssueWorkspaceGroup, resolveInboxSelectionIndex, saveInboxFilterPreferences, + saveCollapsedInboxGroupKeys, saveInboxIssueColumns, + saveInboxWorkItemGroupBy, saveLastInboxTab, + shouldResetInboxWorkspaceGrouping, shouldShowInboxSection, type InboxWorkItem, } from "./inbox"; @@ -487,6 +495,71 @@ describe("inbox helpers", () => { ]); }); + it("skips hidden groups when building keyboard navigation entries", () => { + const visibleIssue = makeIssue("visible", true); + const hiddenIssue = makeIssue("hidden", true); + const approval = makeApprovalWithTimestamps("approval-1", "pending", "2026-03-11T03:00:00.000Z"); + + const entries = buildInboxKeyboardNavEntries( + [ + { + key: "visible-group", + displayItems: [{ kind: "issue", timestamp: 3, issue: visibleIssue }], + childrenByIssueId: new Map(), + }, + { + key: "hidden-group", + displayItems: [ + { kind: "issue", timestamp: 2, issue: hiddenIssue }, + { kind: "approval", timestamp: 1, approval }, + ], + childrenByIssueId: new Map(), + }, + ], + new Set(["hidden-group"]), + new Set(), + ); + + expect(entries).toEqual([ + { + type: "top", + itemKey: `visible-group:${getInboxWorkItemKey({ kind: "issue", timestamp: 3, issue: visibleIssue })}`, + item: { kind: "issue", timestamp: 3, issue: visibleIssue }, + }, + ]); + }); + + it("includes child issues only when their parent row is expanded", () => { + const parentIssue = makeIssue("parent", true); + const childIssue = makeIssue("child", true); + childIssue.parentId = parentIssue.id; + + const groupedSections = [ + { + key: "workspace:default", + displayItems: [{ kind: "issue", timestamp: 2, issue: parentIssue } satisfies InboxWorkItem], + childrenByIssueId: new Map([[parentIssue.id, [childIssue]]]), + }, + ]; + + expect( + buildInboxKeyboardNavEntries(groupedSections, new Set(), new Set()).map((entry) => entry.type === "top" + ? entry.itemKey + : entry.issueId), + ).toEqual([ + `workspace:default:${getInboxWorkItemKey({ kind: "issue", timestamp: 2, issue: parentIssue })}`, + childIssue.id, + ]); + + expect( + buildInboxKeyboardNavEntries(groupedSections, new Set(), new Set([parentIssue.id])).map((entry) => entry.type === "top" + ? entry.itemKey + : entry.issueId), + ).toEqual([ + `workspace:default:${getInboxWorkItemKey({ kind: "issue", timestamp: 2, issue: parentIssue })}`, + ]); + }); + it("sorts self-touched issues without external comments by updatedAt", () => { const recentSelfTouched = makeIssue("recent", false); recentSelfTouched.lastExternalCommentAt = null as unknown as Date; @@ -575,6 +648,22 @@ describe("inbox helpers", () => { )).toBe(true); }); + it("resolves the default workspace into an explicit grouping label", () => { + const issue = makeIssue("default", false); + issue.projectId = "project-1"; + issue.projectWorkspaceId = "project-workspace-1"; + + expect(resolveIssueWorkspaceGroup(issue, { + projectWorkspaceById: new Map([ + ["project-workspace-1", { name: "Primary workspace" }], + ]), + defaultProjectWorkspaceIdByProjectId: new Map([["project-1", "project-workspace-1"]]), + })).toEqual({ + key: "workspace:project:project-workspace-1", + label: "Primary workspace (default)", + }); + }); + it("returns archived search matches that are not already visible in the inbox", () => { const visibleIssue = makeIssue("visible", false); visibleIssue.title = "Alpha visible task"; @@ -939,4 +1028,82 @@ describe("inbox helpers", () => { { key: "join_request", label: "Join requests", items: [items[4]] }, ]); }); + + it("groups workspace sections by latest issue activity while preserving non-issue sections", () => { + const defaultIssue = makeIssue("default", true); + defaultIssue.projectId = "project-1"; + defaultIssue.projectWorkspaceId = "project-workspace-1"; + + const sharedDefaultIssue = makeIssue("shared-default", true); + sharedDefaultIssue.projectId = "project-1"; + sharedDefaultIssue.projectWorkspaceId = "project-workspace-1"; + sharedDefaultIssue.executionWorkspaceId = "execution-workspace-shared-default"; + + const featureIssue = makeIssue("feature", false); + featureIssue.projectId = "project-1"; + featureIssue.projectWorkspaceId = "project-workspace-2"; + + const execIssue = makeIssue("exec", false); + execIssue.projectId = "project-1"; + execIssue.projectWorkspaceId = "project-workspace-1"; + execIssue.executionWorkspaceId = "execution-workspace-1"; + + const items: InboxWorkItem[] = [ + { kind: "issue", timestamp: 5, issue: defaultIssue }, + { kind: "approval", timestamp: 2, approval: makeApproval("pending") }, + { kind: "issue", timestamp: 4, issue: sharedDefaultIssue }, + { kind: "issue", timestamp: 7, issue: featureIssue }, + { kind: "issue", timestamp: 9, issue: execIssue }, + ]; + + expect(groupInboxWorkItems(items, "workspace", { + executionWorkspaceById: new Map([ + ["execution-workspace-1", { name: "Feature Branch", mode: "isolated_workspace", projectWorkspaceId: "project-workspace-1" }], + ["execution-workspace-shared-default", { name: "Shared default workspace", mode: "shared_workspace", projectWorkspaceId: "project-workspace-1" }], + ]), + projectWorkspaceById: new Map([ + ["project-workspace-1", { name: "Primary workspace" }], + ["project-workspace-2", { name: "Secondary workspace" }], + ]), + defaultProjectWorkspaceIdByProjectId: new Map([["project-1", "project-workspace-1"]]), + })).toEqual([ + { key: "workspace:execution:execution-workspace-1", label: "Feature Branch", items: [items[4]] }, + { key: "workspace:project:project-workspace-2", label: "Secondary workspace", items: [items[3]] }, + { + key: "workspace:project:project-workspace-1", + label: "Primary workspace (default)", + items: [items[0], items[2]], + }, + { key: "kind:approval", label: "Approvals", items: [items[1]] }, + ]); + }); + + it("persists workspace grouping preferences", () => { + saveInboxWorkItemGroupBy("workspace"); + expect(loadInboxWorkItemGroupBy()).toBe("workspace"); + }); + + it("persists collapsed inbox groups per company", () => { + saveCollapsedInboxGroupKeys("company-1", new Set(["workspace:alpha", "workspace:beta"])); + saveCollapsedInboxGroupKeys("company-2", new Set(["type:approval"])); + + expect(loadCollapsedInboxGroupKeys("company-1")).toEqual(new Set(["workspace:alpha", "workspace:beta"])); + expect(loadCollapsedInboxGroupKeys("company-2")).toEqual(new Set(["type:approval"])); + }); + + it("returns empty collapsed inbox groups for missing or invalid storage", () => { + expect(loadCollapsedInboxGroupKeys("company-1")).toEqual(new Set()); + localStorage.setItem("paperclip:inbox:collapsed-groups:company-1", JSON.stringify({ nope: true })); + expect(loadCollapsedInboxGroupKeys("company-1")).toEqual(new Set()); + }); + + it("does not reset workspace grouping before experimental settings have loaded", () => { + expect(shouldResetInboxWorkspaceGrouping("workspace", false, false)).toBe(false); + }); + + it("resets workspace grouping only when settings are loaded and workspace grouping is unavailable", () => { + expect(shouldResetInboxWorkspaceGrouping("workspace", false, true)).toBe(true); + expect(shouldResetInboxWorkspaceGrouping("workspace", true, true)).toBe(false); + expect(shouldResetInboxWorkspaceGrouping("none", false, true)).toBe(false); + }); }); diff --git a/ui/src/lib/inbox.ts b/ui/src/lib/inbox.ts index 21b2973d..9060d0a2 100644 --- a/ui/src/lib/inbox.ts +++ b/ui/src/lib/inbox.ts @@ -22,6 +22,7 @@ export const INBOX_ISSUE_COLUMNS_KEY = "paperclip:inbox:issue-columns"; export const INBOX_NESTING_KEY = "paperclip:inbox:nesting"; export const INBOX_GROUP_BY_KEY = "paperclip:inbox:group-by"; export const INBOX_FILTER_PREFERENCES_KEY_PREFIX = "paperclip:inbox:filters"; +export const INBOX_COLLAPSED_GROUPS_KEY_PREFIX = "paperclip:inbox:collapsed-groups"; export type InboxTab = "mine" | "recent" | "unread" | "all"; export type InboxCategoryFilter = | "everything" @@ -31,7 +32,7 @@ export type InboxCategoryFilter = | "failed_runs" | "alerts"; export type InboxApprovalFilter = "all" | "actionable" | "resolved"; -export type InboxWorkItemGroupBy = "none" | "type"; +export type InboxWorkItemGroupBy = "none" | "type" | "workspace"; export const inboxIssueColumns = [ "status", "id", @@ -86,6 +87,40 @@ export interface InboxWorkItemGroup { items: InboxWorkItem[]; } +export interface InboxKeyboardGroupSection { + key: string; + displayItems: InboxWorkItem[]; + childrenByIssueId: ReadonlyMap; +} + +export type InboxKeyboardNavEntry = + | { + type: "top"; + itemKey: string; + item: InboxWorkItem; + } + | { + type: "child"; + issueId: string; + issue: Issue; + }; + +export interface InboxProjectWorkspaceLookup { + name: string; +} + +export interface InboxExecutionWorkspaceLookup { + name: string; + mode: "shared_workspace" | "isolated_workspace" | "operator_branch" | "adapter_managed" | "cloud_sandbox"; + projectWorkspaceId: string | null; +} + +export interface InboxWorkspaceGroupingOptions { + executionWorkspaceById?: ReadonlyMap; + projectWorkspaceById?: ReadonlyMap; + defaultProjectWorkspaceIdByProjectId?: ReadonlyMap; +} + const defaultInboxFilterPreferences: InboxFilterPreferences = { allCategoryFilter: "everything", allApprovalFilter: "all", @@ -130,6 +165,11 @@ function getInboxFilterPreferencesStorageKey(companyId: string | null | undefine return `${INBOX_FILTER_PREFERENCES_KEY_PREFIX}:${companyId}`; } +function getInboxCollapsedGroupsStorageKey(companyId: string | null | undefined): string | null { + if (!companyId) return null; + return `${INBOX_COLLAPSED_GROUPS_KEY_PREFIX}:${companyId}`; +} + export function loadInboxFilterPreferences( companyId: string | null | undefined, ): InboxFilterPreferences { @@ -184,6 +224,36 @@ export function saveInboxFilterPreferences( } } +export function loadCollapsedInboxGroupKeys( + companyId: string | null | undefined, +): Set { + const storageKey = getInboxCollapsedGroupsStorageKey(companyId); + if (!storageKey) return new Set(); + + try { + const raw = localStorage.getItem(storageKey); + if (!raw) return new Set(); + const parsed = JSON.parse(raw); + return new Set(normalizeStringArray(parsed)); + } catch { + return new Set(); + } +} + +export function saveCollapsedInboxGroupKeys( + companyId: string | null | undefined, + groupKeys: ReadonlySet, +) { + const storageKey = getInboxCollapsedGroupsStorageKey(companyId); + if (!storageKey) return; + + try { + localStorage.setItem(storageKey, JSON.stringify([...groupKeys])); + } catch { + // Ignore localStorage failures. + } +} + export function loadDismissedInboxAlerts(): Set { try { const raw = localStorage.getItem(DISMISSED_KEY); @@ -273,7 +343,7 @@ export function saveInboxIssueColumns(columns: InboxIssueColumn[]) { export function loadInboxWorkItemGroupBy(): InboxWorkItemGroupBy { try { const raw = localStorage.getItem(INBOX_GROUP_BY_KEY); - return raw === "type" ? raw : "none"; + return raw === "type" || raw === "workspace" ? raw : "none"; } catch { return "none"; } @@ -287,6 +357,14 @@ export function saveInboxWorkItemGroupBy(groupBy: InboxWorkItemGroupBy) { } } +export function shouldResetInboxWorkspaceGrouping( + groupBy: InboxWorkItemGroupBy, + isolatedWorkspacesEnabled: boolean, + experimentalSettingsLoaded: boolean, +): boolean { + return experimentalSettingsLoaded && groupBy === "workspace" && !isolatedWorkspacesEnabled; +} + export function shouldIncludeRoutineExecutionIssue( issue: Pick, showRoutineExecutions: boolean, @@ -307,15 +385,8 @@ export function matchesInboxIssueSearch( executionWorkspaceById, projectWorkspaceById, defaultProjectWorkspaceIdByProjectId, - }: { + }: InboxWorkspaceGroupingOptions & { isolatedWorkspacesEnabled?: boolean; - executionWorkspaceById?: ReadonlyMap; - projectWorkspaceById?: ReadonlyMap; - defaultProjectWorkspaceIdByProjectId?: ReadonlyMap; } = {}, ): boolean { const normalizedQuery = query.trim().toLowerCase(); @@ -346,12 +417,8 @@ export function getArchivedInboxSearchIssues({ searchableIssues: Issue[]; query: string; isolatedWorkspacesEnabled?: boolean; - executionWorkspaceById?: ReadonlyMap; - projectWorkspaceById?: ReadonlyMap; + executionWorkspaceById?: ReadonlyMap; + projectWorkspaceById?: ReadonlyMap; defaultProjectWorkspaceIdByProjectId?: ReadonlyMap; }): Issue[] { const normalizedQuery = query.trim(); @@ -400,21 +467,34 @@ export function getInboxSearchSupplementIssues({ .filter((issue) => !visibleIssueIds.has(issue.id)); } +function formatDefaultWorkspaceGroupLabel(name: string | null | undefined): string { + const normalizedName = name?.trim(); + return normalizedName ? `${normalizedName} (default)` : "Default workspace"; +} + +function resolveDefaultProjectWorkspaceInfo( + issue: Pick, + { + projectWorkspaceById, + defaultProjectWorkspaceIdByProjectId, + }: Pick, +): { id: string; label: string } | null { + if (!issue.projectId) return null; + const defaultProjectWorkspaceId = defaultProjectWorkspaceIdByProjectId?.get(issue.projectId) ?? null; + if (!defaultProjectWorkspaceId) return null; + return { + id: defaultProjectWorkspaceId, + label: formatDefaultWorkspaceGroupLabel(projectWorkspaceById?.get(defaultProjectWorkspaceId)?.name), + }; +} + export function resolveIssueWorkspaceName( issue: Pick, { executionWorkspaceById, projectWorkspaceById, defaultProjectWorkspaceIdByProjectId, - }: { - executionWorkspaceById?: ReadonlyMap; - projectWorkspaceById?: ReadonlyMap; - defaultProjectWorkspaceIdByProjectId?: ReadonlyMap; - }, + }: InboxWorkspaceGroupingOptions, ): string | null { const defaultProjectWorkspaceId = issue.projectId ? defaultProjectWorkspaceIdByProjectId?.get(issue.projectId) ?? null @@ -441,6 +521,74 @@ export function resolveIssueWorkspaceName( return null; } +export function resolveIssueWorkspaceGroup( + issue: Pick, + { + executionWorkspaceById, + projectWorkspaceById, + defaultProjectWorkspaceIdByProjectId, + }: InboxWorkspaceGroupingOptions = {}, +): { key: string; label: string } { + const defaultProjectWorkspace = resolveDefaultProjectWorkspaceInfo(issue, { + projectWorkspaceById, + defaultProjectWorkspaceIdByProjectId, + }); + + if (issue.executionWorkspaceId) { + const executionWorkspace = executionWorkspaceById?.get(issue.executionWorkspaceId) ?? null; + const linkedProjectWorkspaceId = + executionWorkspace?.projectWorkspaceId ?? issue.projectWorkspaceId ?? null; + const isDefaultSharedExecutionWorkspace = + executionWorkspace?.mode === "shared_workspace" + && linkedProjectWorkspaceId != null + && linkedProjectWorkspaceId === defaultProjectWorkspace?.id; + + if (isDefaultSharedExecutionWorkspace && defaultProjectWorkspace) { + return { + key: `workspace:project:${defaultProjectWorkspace.id}`, + label: defaultProjectWorkspace.label, + }; + } + + const workspaceName = executionWorkspace?.name?.trim(); + if (workspaceName) { + return { + key: `workspace:execution:${issue.executionWorkspaceId}`, + label: workspaceName, + }; + } + } + + if (issue.projectWorkspaceId) { + if (issue.projectWorkspaceId === defaultProjectWorkspace?.id) { + return { + key: `workspace:project:${defaultProjectWorkspace.id}`, + label: defaultProjectWorkspace.label, + }; + } + + const workspaceName = projectWorkspaceById?.get(issue.projectWorkspaceId)?.name?.trim(); + if (workspaceName) { + return { + key: `workspace:project:${issue.projectWorkspaceId}`, + label: workspaceName, + }; + } + } + + if (defaultProjectWorkspace) { + return { + key: `workspace:project:${defaultProjectWorkspace.id}`, + label: defaultProjectWorkspace.label, + }; + } + + return { + key: "workspace:none", + label: "No workspace", + }; +} + export function loadInboxNesting(): boolean { try { const raw = localStorage.getItem(INBOX_NESTING_KEY); @@ -642,11 +790,50 @@ const inboxWorkItemKindLabels: Record = { export function groupInboxWorkItems( items: InboxWorkItem[], groupBy: InboxWorkItemGroupBy, + options: InboxWorkspaceGroupingOptions = {}, ): InboxWorkItemGroup[] { if (groupBy === "none") { return [{ key: "__all", label: null, items }]; } + if (groupBy === "workspace") { + const groups = new Map(); + for (const item of items) { + const resolvedGroup = item.kind === "issue" + ? resolveIssueWorkspaceGroup(item.issue, options) + : { key: `kind:${item.kind}`, label: inboxWorkItemKindLabels[item.kind] }; + const existing = groups.get(resolvedGroup.key); + if (existing) { + existing.items.push(item); + existing.latestTimestamp = Math.max(existing.latestTimestamp, item.timestamp); + } else { + groups.set(resolvedGroup.key, { + label: resolvedGroup.label, + items: [item], + latestTimestamp: item.timestamp, + }); + } + } + + return [...groups.entries()] + .map(([key, value]) => ({ + key, + label: value.label, + items: value.items, + latestTimestamp: value.latestTimestamp, + })) + .sort((a, b) => { + const timestampDiff = b.latestTimestamp - a.latestTimestamp; + if (timestampDiff !== 0) return timestampDiff; + return a.label.localeCompare(b.label); + }) + .map(({ key, label, items: groupItems }) => ({ + key, + label, + items: groupItems, + })); + } + const groups = new Map(); for (const item of items) { const existing = groups.get(item.kind) ?? []; @@ -729,6 +916,48 @@ export function buildInboxNesting(items: InboxWorkItem[]): { return { displayItems, childrenByIssueId }; } +export function getInboxWorkItemKey(item: InboxWorkItem): string { + if (item.kind === "issue") return `issue:${item.issue.id}`; + if (item.kind === "approval") return `approval:${item.approval.id}`; + if (item.kind === "failed_run") return `run:${item.run.id}`; + return `join:${item.joinRequest.id}`; +} + +export function buildInboxKeyboardNavEntries( + groupedSections: ReadonlyArray, + collapsedGroupKeys: ReadonlySet, + collapsedInboxParents: ReadonlySet, +): InboxKeyboardNavEntry[] { + const entries: InboxKeyboardNavEntry[] = []; + + for (const group of groupedSections) { + if (collapsedGroupKeys.has(group.key)) continue; + + for (const item of group.displayItems) { + entries.push({ + type: "top", + itemKey: `${group.key}:${getInboxWorkItemKey(item)}`, + item, + }); + + if (item.kind !== "issue") continue; + + const children = group.childrenByIssueId.get(item.issue.id); + if (!children?.length || collapsedInboxParents.has(item.issue.id)) continue; + + for (const child of children) { + entries.push({ + type: "child", + issueId: child.id, + issue: child, + }); + } + } + } + + return entries; +} + export function shouldShowInboxSection({ tab, hasItems, diff --git a/ui/src/lib/keyboardShortcuts.test.ts b/ui/src/lib/keyboardShortcuts.test.ts index ea0019c7..6b8f88fd 100644 --- a/ui/src/lib/keyboardShortcuts.test.ts +++ b/ui/src/lib/keyboardShortcuts.test.ts @@ -8,6 +8,7 @@ import { isKeyboardShortcutTextInputTarget, resolveIssueDetailGoKeyAction, resolveInboxQuickArchiveKeyAction, + resolveInboxUndoArchiveKeyAction, shouldBlurPageSearchOnEnter, shouldBlurPageSearchOnEscape, } from "./keyboardShortcuts"; @@ -181,6 +182,36 @@ describe("keyboardShortcuts helpers", () => { })).toBe("ignore"); }); + it("undoes only a clean lowercase u press when an archive is available", () => { + const button = document.createElement("button"); + + expect(resolveInboxUndoArchiveKeyAction({ + hasUndoableArchive: true, + defaultPrevented: false, + key: "u", + metaKey: false, + ctrlKey: false, + altKey: false, + target: button, + hasOpenDialog: false, + })).toBe("undo_archive"); + }); + + it("keeps uppercase U available for mark-unread handling", () => { + const button = document.createElement("button"); + + expect(resolveInboxUndoArchiveKeyAction({ + hasUndoableArchive: true, + defaultPrevented: false, + key: "U", + metaKey: false, + ctrlKey: false, + altKey: false, + target: button, + hasOpenDialog: false, + })).toBe("ignore"); + }); + it("arms go-to-inbox on a clean g press", () => { const button = document.createElement("button"); diff --git a/ui/src/lib/keyboardShortcuts.ts b/ui/src/lib/keyboardShortcuts.ts index adc59e62..ce7b2aa8 100644 --- a/ui/src/lib/keyboardShortcuts.ts +++ b/ui/src/lib/keyboardShortcuts.ts @@ -12,6 +12,7 @@ const PAGE_SEARCH_SHORTCUT_SELECTOR = "[data-page-search-target='true']"; const MODIFIER_ONLY_KEYS = new Set(["Shift", "Meta", "Control", "Alt"]); export type InboxQuickArchiveKeyAction = "ignore" | "archive" | "disarm"; +export type InboxUndoArchiveKeyAction = "ignore" | "undo_archive"; export type IssueDetailGoKeyAction = "ignore" | "arm" | "navigate_inbox" | "focus_comment" | "disarm"; export function isKeyboardShortcutTextInputTarget(target: EventTarget | null): boolean { @@ -105,6 +106,33 @@ export function resolveInboxQuickArchiveKeyAction({ return "ignore"; } +export function resolveInboxUndoArchiveKeyAction({ + hasUndoableArchive, + defaultPrevented, + key, + metaKey, + ctrlKey, + altKey, + target, + hasOpenDialog, +}: { + hasUndoableArchive: boolean; + defaultPrevented: boolean; + key: string; + metaKey: boolean; + ctrlKey: boolean; + altKey: boolean; + target: EventTarget | null; + hasOpenDialog: boolean; +}): InboxUndoArchiveKeyAction { + if (!hasUndoableArchive) return "ignore"; + if (defaultPrevented) return "ignore"; + if (metaKey || ctrlKey || altKey || isModifierOnlyKey(key)) return "ignore"; + if (hasOpenDialog || isKeyboardShortcutTextInputTarget(target)) return "ignore"; + if (key === "u") return "undo_archive"; + return "ignore"; +} + export function resolveIssueDetailGoKeyAction({ armed, defaultPrevented, diff --git a/ui/src/lib/queryKeys.ts b/ui/src/lib/queryKeys.ts index f3e08124..f7c47b45 100644 --- a/ui/src/lib/queryKeys.ts +++ b/ui/src/lib/queryKeys.ts @@ -95,6 +95,11 @@ export const queryKeys = { auth: { session: ["auth", "session"] as const, }, + sidebarPreferences: { + companyOrder: (userId: string) => ["sidebar-preferences", "company-order", userId] as const, + projectOrder: (companyId: string, userId: string) => + ["sidebar-preferences", "project-order", companyId, userId] as const, + }, instance: { generalSettings: ["instance", "general-settings"] as const, schedulerHeartbeats: ["instance", "scheduler-heartbeats"] as const, diff --git a/ui/src/pages/CompanyImport.tsx b/ui/src/pages/CompanyImport.tsx index b2c6676c..6021246e 100644 --- a/ui/src/pages/CompanyImport.tsx +++ b/ui/src/pages/CompanyImport.tsx @@ -13,9 +13,9 @@ import { useToast } from "../context/ToastContext"; import { authApi } from "../api/auth"; import { companiesApi } from "../api/companies"; import { agentsApi } from "../api/agents"; +import { sidebarPreferencesApi } from "../api/sidebarPreferences"; import { queryKeys } from "../lib/queryKeys"; import { getAgentOrderStorageKey, writeAgentOrder } from "../lib/agent-order"; -import { getProjectOrderStorageKey, writeProjectOrder } from "../lib/project-order"; import { MarkdownBody } from "../components/MarkdownBody"; import { Button } from "@/components/ui/button"; import { EmptyState } from "../components/EmptyState"; @@ -346,7 +346,7 @@ function prefixedName(prefix: string | null, originalName: string): string { return `${prefix}-${originalName}`; } -function applyImportedSidebarOrder( +async function applyImportedSidebarOrder( preview: CompanyPortabilityPreviewResult | null, result: { company: { id: string }; @@ -381,7 +381,7 @@ function applyImportedSidebarOrder( writeAgentOrder(getAgentOrderStorageKey(result.company.id, userId), orderedAgentIds); } if (orderedProjectIds.length > 0) { - writeProjectOrder(getProjectOrderStorageKey(result.company.id, userId), orderedProjectIds); + await sidebarPreferencesApi.updateProjectOrder(result.company.id, { orderedIds: orderedProjectIds }); } } @@ -859,7 +859,7 @@ export function CompanyImport() { ?? refreshedSession?.user?.id ?? refreshedSession?.session?.userId ?? null; - applyImportedSidebarOrder(importPreview, result, sidebarOrderUserId); + await applyImportedSidebarOrder(importPreview, result, sidebarOrderUserId); setSelectedCompanyId(importedCompany.id); pushToast({ tone: "success", diff --git a/ui/src/pages/ExecutionWorkspaceDetail.tsx b/ui/src/pages/ExecutionWorkspaceDetail.tsx index 743fc077..2d47c9e4 100644 --- a/ui/src/pages/ExecutionWorkspaceDetail.tsx +++ b/ui/src/pages/ExecutionWorkspaceDetail.tsx @@ -15,6 +15,11 @@ import { issuesApi } from "../api/issues"; import { projectsApi } from "../api/projects"; import { IssuesList } from "../components/IssuesList"; import { PageTabBar } from "../components/PageTabBar"; +import { + buildWorkspaceRuntimeControlSections, + WorkspaceRuntimeControls, + type WorkspaceRuntimeControlRequest, +} from "../components/WorkspaceRuntimeControls"; import { useBreadcrumbs } from "../context/BreadcrumbContext"; import { useCompany } from "../context/CompanyContext"; import { queryKeys } from "../lib/queryKeys"; @@ -34,7 +39,7 @@ type WorkspaceFormState = { workspaceRuntime: string; }; -type ExecutionWorkspaceTab = "configuration" | "issues"; +type ExecutionWorkspaceTab = "configuration" | "runtime_logs" | "issues"; function resolveExecutionWorkspaceTab(pathname: string, workspaceId: string): ExecutionWorkspaceTab | null { const segments = pathname.split("/").filter(Boolean); @@ -42,10 +47,16 @@ function resolveExecutionWorkspaceTab(pathname: string, workspaceId: string): Ex if (executionWorkspacesIndex === -1 || segments[executionWorkspacesIndex + 1] !== workspaceId) return null; const tab = segments[executionWorkspacesIndex + 2]; if (tab === "issues") return "issues"; + if (tab === "runtime-logs") return "runtime_logs"; if (tab === "configuration") return "configuration"; return null; } +function executionWorkspaceTabPath(workspaceId: string, tab: ExecutionWorkspaceTab) { + const segment = tab === "runtime_logs" ? "runtime-logs" : tab; + return `/execution-workspaces/${workspaceId}/${segment}`; +} + function isSafeExternalUrl(value: string | null | undefined) { if (!value) return false; try { @@ -60,10 +71,6 @@ function readText(value: string | null | undefined) { return value ?? ""; } -function hasActiveRuntimeServices(workspace: ExecutionWorkspace | null | undefined) { - return (workspace?.runtimeServices ?? []).some((service) => service.status === "starting" || service.status === "running"); -} - function formatJson(value: Record | null | undefined) { if (!value || Object.keys(value).length === 0) return ""; return JSON.stringify(value, null, 2); @@ -83,7 +90,7 @@ function parseWorkspaceRuntimeJson(value: string) { if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) { return { ok: false as const, - error: "Workspace runtime JSON must be a JSON object.", + error: "Workspace commands JSON must be a JSON object.", }; } return { ok: true as const, value: parsed as Record }; @@ -294,7 +301,7 @@ function ExecutionWorkspaceIssuesList({ projects={projectOptions} liveIssueIds={liveIssueIds} projectId={project?.id} - viewStateKey={`paperclip:execution-workspace-view:${workspaceId}`} + viewStateKey="paperclip:execution-workspace-issues-view" onUpdateIssue={(id, data) => updateIssue.mutate({ id, data })} /> ); @@ -310,6 +317,7 @@ export function ExecutionWorkspaceDetail() { const [form, setForm] = useState(null); const [closeDialogOpen, setCloseDialogOpen] = useState(false); const [errorMessage, setErrorMessage] = useState(null); + const [runtimeActionErrorMessage, setRuntimeActionErrorMessage] = useState(null); const [runtimeActionMessage, setRuntimeActionMessage] = useState(null); const activeTab = workspaceId ? resolveExecutionWorkspaceTab(location.pathname, workspaceId) : null; @@ -377,6 +385,7 @@ export function ExecutionWorkspaceDetail() { if (!workspace) return; setForm(formStateFromWorkspace(workspace)); setErrorMessage(null); + setRuntimeActionErrorMessage(null); }, [workspace]); useEffect(() => { @@ -415,24 +424,26 @@ export function ExecutionWorkspaceDetail() { enabled: Boolean(workspaceId), }); const controlRuntimeServices = useMutation({ - mutationFn: (action: "start" | "stop" | "restart") => - executionWorkspacesApi.controlRuntimeServices(workspace!.id, action), - onSuccess: (result, action) => { + mutationFn: (request: WorkspaceRuntimeControlRequest) => + executionWorkspacesApi.controlRuntimeCommands(workspace!.id, request.action, request), + onSuccess: (result, request) => { queryClient.setQueryData(queryKeys.executionWorkspaces.detail(result.workspace.id), result.workspace); queryClient.invalidateQueries({ queryKey: queryKeys.executionWorkspaces.workspaceOperations(result.workspace.id) }); queryClient.invalidateQueries({ queryKey: queryKeys.projects.detail(result.workspace.projectId) }); - setErrorMessage(null); + setRuntimeActionErrorMessage(null); setRuntimeActionMessage( - action === "stop" - ? "Runtime services stopped." - : action === "restart" - ? "Runtime services restarted." - : "Runtime services started.", + request.action === "run" + ? "Workspace job completed." + : request.action === "stop" + ? "Workspace service stopped." + : request.action === "restart" + ? "Workspace service restarted." + : "Workspace service started.", ); }, onError: (error) => { setRuntimeActionMessage(null); - setErrorMessage(error instanceof Error ? error.message : "Failed to control runtime services."); + setRuntimeActionErrorMessage(error instanceof Error ? error.message : "Failed to control workspace commands."); }, }); @@ -446,22 +457,32 @@ export function ExecutionWorkspaceDetail() { } if (!workspace || !form || !initialState) return null; + const canRunWorkspaceCommands = Boolean(workspace.cwd); + const canStartRuntimeServices = Boolean(effectiveRuntimeConfig) && canRunWorkspaceCommands; + const runtimeControlSections = buildWorkspaceRuntimeControlSections({ + runtimeConfig: effectiveRuntimeConfig, + runtimeServices: workspace.runtimeServices ?? [], + canStartServices: canStartRuntimeServices, + canRunJobs: canRunWorkspaceCommands, + }); + const pendingRuntimeAction = controlRuntimeServices.isPending ? controlRuntimeServices.variables ?? null : null; + if (workspaceId && activeTab === null) { let cachedTab: ExecutionWorkspaceTab = "configuration"; try { const storedTab = localStorage.getItem(`paperclip:execution-workspace-tab:${workspaceId}`); - if (storedTab === "issues" || storedTab === "configuration") { + if (storedTab === "issues" || storedTab === "configuration" || storedTab === "runtime_logs") { cachedTab = storedTab; } } catch {} - return ; + return ; } const handleTabChange = (tab: ExecutionWorkspaceTab) => { try { localStorage.setItem(`paperclip:execution-workspace-tab:${workspace.id}`, tab); } catch {} - navigate(`/execution-workspaces/${workspace.id}/${tab}`); + navigate(executionWorkspaceTabPath(workspace.id, tab)); }; const saveChanges = () => { @@ -485,7 +506,7 @@ export function ExecutionWorkspaceDetail() { return ( <> -
+
+
+
+
+
Workspace commands
+

Services and jobs

+

+ Source: {runtimeConfigSource === "execution_workspace" + ? "execution workspace override" + : runtimeConfigSource === "project_workspace" + ? "project workspace default" + : "none"} +

+
+
+ controlRuntimeServices.mutate(request)} + /> + {runtimeActionErrorMessage ?

{runtimeActionErrorMessage}

: null} + {!runtimeActionErrorMessage && runtimeActionMessage ?

{runtimeActionMessage}

: null} +
+ handleTabChange(value as ExecutionWorkspaceTab)}> {activeTab === "configuration" ? ( -
-
-
-
-
-
- Configuration -
-

Workspace settings

-

- Edit the concrete path, repo, branch, provisioning, teardown, and runtime overrides attached to this execution workspace. -

-
- -
- - - -
- - setForm((current) => current ? { ...current, name: event.target.value } : current)} - placeholder="Execution workspace name" - /> - - - setForm((current) => current ? { ...current, branchName: event.target.value } : current)} - placeholder="PAP-946-workspace" - /> - -
- -
- - setForm((current) => current ? { ...current, cwd: event.target.value } : current)} - placeholder="/absolute/path/to/workspace" - /> - - - setForm((current) => current ? { ...current, providerRef: event.target.value } : current)} - placeholder="/path/to/worktree or provider ref" - /> - -
- -
- - setForm((current) => current ? { ...current, repoUrl: event.target.value } : current)} - placeholder="https://github.com/org/repo" - /> - - - setForm((current) => current ? { ...current, baseRef: event.target.value } : current)} - placeholder="origin/main" - /> - -
- -
- -