Refactor task_attempts to workspaces and sessions (Vibe Kanban) (#1569)

* All tasks are complete. Here's a summary of the refactoring that was done:

The `task_attempts` table has been refactored into `workspaces` and `sessions`:

- Created new migration `20251216142123_refactor_task_attempts_to_workspaces_sessions.sql`
- Renamed `task_attempts` table to `workspaces` (removed `executor` column)
- Created new `sessions` table with `(id, workspace_id, executor, created_at, updated_at)`
- Renamed `executor_sessions` to `coding_agent_turns` (dropped redundant `task_attempt_id`)
- Changed `execution_processes.task_attempt_id` to `session_id`
- Changed `merges.task_attempt_id` to `workspace_id`
- Changed `tasks.parent_task_attempt` to `parent_workspace_id`

- Renamed `crates/db/src/models/task_attempt.rs` → `workspace.rs`
- Created new `crates/db/src/models/session.rs`
- Renamed `executor_session.rs` → `coding_agent_turn.rs`
- Updated `execution_process.rs`:
  - Changed `task_attempt_id` field to `session_id`
  - Added workspace-level query methods (`find_latest_by_workspace_and_run_reason`, `latest_executor_profile_for_workspace`, `find_latest_external_session_id_by_workspace`, `find_running_dev_servers_by_workspace`, `has_running_non_dev_server_processes_for_workspace`)
- Updated `merge.rs`: Changed `task_attempt_id` to `workspace_id`
- Updated `task.rs`: Changed `parent_task_attempt` to `parent_workspace_id`

- Updated `container.rs`: Changed `start_execution` to take 4 parameters (workspace, session, action, reason)
- Updated `events/`: Changed event types from `TaskAttempt` to `Workspace`
- Updated `pr_monitor.rs`: Changed to use `Workspace` instead of `TaskAttempt`
- Updated `workspace_manager.rs`: Added support for `DbWorkspace` alias

- Updated `task_attempts.rs`: All handlers now create/use sessions for execution processes
- Updated `tasks.rs`: Removed `executor` from `CreateWorkspace`, fixed error type
- Updated setup helpers (codex, cursor, gh_cli) to create sessions
- Updated `error.rs`: Added `SessionError` handling

Cleanup script changes for task attempt 2f672d73-cb46-4515-b1c7-3da8f4402858

Done. All 3 `sqlx::query_scalar()` function calls have been converted to `sqlx::query_scalar!()` macros:

1. `has_running_non_dev_server_processes()` - lines 332-341
2. `has_running_non_dev_server_processes_for_workspace()` - lines 350-361
3. `find_prev_after_head_commit()` - lines 619-634

Key changes:
- Converted `?` placeholders to `$1, $2, $3` positional parameters (sqlx macro style)
- Added `as "count!: i64"` type annotation for COUNT queries
- Used `.flatten()` for the nullable `after_head_commit` column to avoid `Option<Option<String>>`

The one in `crates/remote/src/db/mod.rs` uses dynamic SQL for PostgreSQL's `ALTER ROLE` and cannot be converted to a macro.

Done. Removed the 4 unused functions from `execution_process.rs`:
- `find_running_dev_servers_by_session`
- `has_running_non_dev_server_processes` (the per-session version)
- `delete_by_session_id`
- `latest_executor_profile_for_session`

The workspace-level versions (`has_running_non_dev_server_processes_for_workspace`, `find_running_dev_servers_by_workspace`, `latest_executor_profile_for_workspace`) are still available. Everything builds successfully.

Rewbase

Revert event rename

* All done. Here's a summary of the changes:

Created a `TaskAttempt` helper struct in the backend that maintains backwards compatibility with the frontend:

1. **`crates/db/src/models/workspace.rs`**
   - Added `TaskAttempt` struct that combines `Workspace` fields + `executor` from the latest `Session`
   - Added `TaskAttempt::from_workspace()` to convert a `Workspace` by fetching its latest session's executor
   - Added `TaskAttempt::fetch_all()` for API responses

2. **`crates/db/src/models/task.rs`**
   - Changed `TaskRelationships.current_workspace` → `current_attempt: TaskAttempt`

3. **`crates/server/src/routes/task_attempts.rs`**
   - Updated `get_task_attempts` to return `Vec<TaskAttempt>`
   - Updated `get_task_attempt` to return `TaskAttempt`
   - Updated `create_task_attempt` to return `TaskAttempt`

4. **`crates/server/src/bin/generate_types.rs`**
   - Added `TaskAttempt::decl()` to the type generation list

Fixed references from `parent_task_attempt` to `parent_workspace_id` in:
- `TaskCard.tsx`
- `NoServerContent.tsx`
- `TaskPanel.tsx`
- `TaskFormDialog.tsx`
- `CreateAttemptDialog.tsx`
- `useTaskMutations.ts`
- `ProjectTasks.tsx`

- Frontend now gets `TaskAttempt` type with the `executor` field
- The `TaskRelationships.current_attempt` field provides `TaskAttempt` with executor
- Backend still uses `Workspace` + `Session` internally
- All type checks pass

Fixed. The issue was that the frontend was sending `task_attempt_id` as the query parameter, but the backend expected `workspace_id`. Now the execution processes WebSocket stream should work correctly and the "Loading History" should resolve.

* All done. Here's a summary of the changes:

1. **Added sessions endpoint** (`GET /api/sessions?workspace_id={uuid}`)
   - Created `crates/server/src/routes/sessions.rs`
   - Registered in `crates/server/src/routes/mod.rs`
   - Added `Session::decl()` to `generate_types.rs`

2. **Removed TaskAttempt from backend**
   - Removed `TaskAttempt` struct and methods from `crates/db/src/models/workspace.rs`
   - Changed `TaskRelationships.current_attempt` back to `current_workspace: Workspace` in `task.rs`
   - Updated route handlers to return `Workspace` instead of `TaskAttempt`

1. **Created `TaskAttempt` type** in `frontend/src/types/attempt.ts`
   - Defines `TaskAttempt = Workspace & { executor: string }`
   - Exports `createTaskAttempt()` helper function

2. **Updated `api.ts`**
   - Added `sessionsApi` for fetching sessions
   - Added `workspaceToTaskAttempt()` helper that fetches sessions and constructs TaskAttempt
   - Updated `attemptsApi.getAll()`, `get()`, and `create()` to internally convert Workspace → TaskAttempt

3. **Updated 20+ frontend files** to import `TaskAttempt` from `@/types/attempt` instead of `shared/types`

- Backend returns `Workspace` directly (no executor field)
- Backend has new `/api/sessions` endpoint to fetch sessions by workspace
- Frontend constructs `TaskAttempt` by combining `Workspace` + latest `Session.executor`
- All type checks pass

* The refactoring is complete. Here's a summary of what was done:

- `TaskAttempt` is now just a type alias for `Workspace` (no executor needed for most uses)
- Added `WorkspaceWithSession = Workspace & { executor: string }` for components that need executor
- Added `createWorkspaceWithSession()` helper function

- `attemptsApi.get()`, `getAll()`, `create()` now return `Workspace` directly (as `TaskAttempt`)
- Added `attemptsApi.getWithSession()` - fetches workspace + session to get executor
- Added `sessionsApi.getByWorkspace()` for fetching sessions

- Added `useTaskAttemptWithSession()` - for single attempt with executor
- Added `useTaskAttemptsWithSessions()` - for list of attempts with executor

Files that access `.executor` now use `WorkspaceWithSession`:
- `TaskPanel.tsx` - Uses `useTaskAttemptsWithSessions` and `useTaskAttemptWithSession`
- `NextActionCard.tsx` - Uses `attemptsApi.getWithSession()`
- `UserMessage.tsx` - Receives `WorkspaceWithSession` prop
- `RetryEditorInline.tsx` - Receives `WorkspaceWithSession` prop
- `DisplayConversationEntry.tsx` - Passes `WorkspaceWithSession` to children
- `VirtualizedList.tsx` - Receives `WorkspaceWithSession` prop
- `TaskAttemptPanel.tsx` - Receives `WorkspaceWithSession` prop
- `ProjectTasks.tsx` - Uses `useTaskAttemptWithSession`
- `FullAttemptLogs.tsx` - Uses `useTaskAttemptWithSession`
- `CreateAttemptDialog.tsx` - Uses `useTaskAttemptsWithSessions`

The 32 other files that only use Workspace fields continue to use `TaskAttempt` (which is just `Workspace`) without any changes needed.

Done! I've simplified the migration from 191 lines down to 106 lines by:

1. **Removed workspaces table rebuild** → simple `ALTER TABLE workspaces DROP COLUMN executor`
2. **Removed merges table rebuild** → simple `ALTER TABLE merges RENAME COLUMN task_attempt_id TO workspace_id`
3. **Removed tasks table rebuild** → simple `ALTER TABLE tasks RENAME COLUMN parent_task_attempt TO parent_workspace_id`
4. **Removed attempt_repos rebuild entirely** → FK auto-updates when `task_attempts` is renamed to `workspaces`

The only table rebuilds remaining are for:
- `execution_processes` - because the FK target changes from workspaces to sessions
- `executor_sessions` → `coding_agent_turns` - because we're dropping the `task_attempt_id` column

All done:

- **npm run lint** - passed (fixed 4 unused import errors)
- **cargo build** - passed
- **npm run build** - passed
- **tsc --noEmit** - passed

Prep db

* Done. I've renamed `TaskAttempt` to `Workspace` in all places where it was used as a type annotation. Here's the summary:

- `types/attempt.ts` - Removed `export type TaskAttempt = Workspace;`

**API Layer:**
- `lib/api.ts` - Changed return types from `TaskAttempt` to `Workspace`

**Hooks:**
- `hooks/useTaskAttempts.ts` - `TaskAttempt[]` → `Workspace[]`
- `hooks/useAttemptCreation.ts` - `TaskAttempt` → `Workspace`
- `hooks/useConversationHistory.ts` - `TaskAttempt` → `Workspace`
- `hooks/useAttempt.ts` - `TaskAttempt` → `Workspace`

**Components:**
- `contexts/ClickedElementsProvider.tsx`
- `components/DiffCard.tsx`
- `components/panels/AttemptHeaderActions.tsx`
- `components/panels/DiffsPanel.tsx`
- `components/tasks/Toolbar/GitOperations.tsx`
- `components/dialogs/tasks/GitActionsDialog.tsx`
- `components/dialogs/tasks/ViewRelatedTasksDialog.tsx`
- `components/dialogs/tasks/CreatePRDialog.tsx`
- `components/ui/actions-dropdown.tsx`

**Type Definitions:**
- `types/modal-args.d.ts`
- `types/modals.ts`

**Pages:**
- `pages/ProjectTasks.tsx`

TypeScript check passes with no errors.

Cleanup script changes for task attempt 2f672d73-cb46-4515-b1c7-3da8f4402858

* The Session type now has `executor: string | null`, which is correct. All changes are complete:

1. **Migration**: `executor TEXT` (no `NOT NULL`)
2. **Rust model**: `executor: Option<String>`
3. **sqlx queries**: Using just `executor` without type annotation (sqlx infers nullable correctly)
4. **All usages**: Wrapped string values in `Some()` where executor is set
5. **TypeScript types**: `executor: string | null`

Everything builds and lints successfully.

Done. Removed `Session::parent_workspace` and its unused `Workspace` import.

* Done. The migration has been updated with the sqlx workaround:

1. Added `COMMIT TRANSACTION;` after `PRAGMA foreign_keys = OFF;` to end sqlx's auto-transaction
2. Added `BEGIN TRANSACTION;` to start our own controlled transaction
3. Added `PRAGMA foreign_key_check;` before committing to verify FK integrity
4. Added `COMMIT;` to commit our transaction
5. Added final `BEGIN TRANSACTION;` for sqlx to close gracefully

This ensures the `PRAGMA foreign_keys = OFF` actually takes effect, preventing potential cascade deletes of `execution_process_logs` when `DROP TABLE execution_processes` runs on existing databases with data.

Fixed. The migration now has the correct order:

1. `COMMIT TRANSACTION;` - ends sqlx's auto-transaction
2. `PRAGMA foreign_keys = OFF;` - now takes effect (outside transaction)
3. `BEGIN TRANSACTION;` - starts our controlled transaction

This ensures foreign keys are actually disabled before the table operations, preventing potential cascade deletes of `execution_process_logs` when `DROP TABLE execution_processes` runs.

Done. The migration now only disables foreign keys for the section that needs it (steps 7-8, the table rebuilds with DROP TABLE). Steps 1-6 run in sqlx's normal auto-transaction with FK enforcement on.

Fix slow statement (vibe-kanban b695d0ad)

i think we are missing an index on something, seeing these logs:
2025-12-17T15:23:25.297852Z  WARN sqlx::query: slow statement: execution time exceeded alert threshold *summary*="SELECT t.id AS \\"id!: …" *db.statement*="\\n\\nSELECT\\n  t.id                            AS \\"id!: Uuid\\",\\n  t.project\_id                    AS \\"project\_id!: Uuid\\",\\n  t.title,\\n  t.description,\\n  t.status                        AS \\"status!: TaskStatus\\",\\n  t.parent\_workspace\_id           AS \\"parent\_workspace\_id: Uuid\\",\\n  t.shared\_task\_id                AS \\"shared\_task\_id: Uuid\\",\\n  t.created\_at                    AS \\"created\_at!: DateTime<Utc>\\",\\n  t.updated\_at                    AS \\"updated\_at!: DateTime<Utc>\\",\\n\\n  CASE WHEN EXISTS (\\n    SELECT 1\\n      FROM workspaces w\\n      JOIN sessions s ON s.workspace\_id = w.id\\n      JOIN execution\_processes ep ON ep.session\_id = s.id\\n     WHERE w.task\_id       = t.id\\n       AND ep.status        = 'running'\\n       AND ep.run\_reason IN ('setupscript','cleanupscript','codingagent')\\n     LIMIT 1\\n  ) THEN 1 ELSE 0 END            AS \\"has\_in\_progress\_attempt!: i64\\",\\n\\n  CASE WHEN (\\n    SELECT ep.status\\n      FROM workspaces w\\n      JOIN sessions s ON s.workspace\_id = w.id\\n      JOIN execution\_processes ep ON ep.session\_id = s.id\\n     WHERE w.task\_id       = t.id\\n     AND ep.run\_reason IN ('setupscript','cleanupscript','codingagent')\\n     ORDER BY ep.created\_at DESC\\n     LIMIT 1\\n  ) IN ('failed','killed') THEN 1 ELSE 0 END\\n                                 AS \\"last\_attempt\_failed!: i64\\",\\n\\n  ( SELECT s.executor\\n      FROM workspaces w\\n      JOIN sessions s ON s.workspace\_id = w.id\\n      WHERE w.task\_id = t.id\\n     ORDER BY s.created\_at DESC\\n      LIMIT 1\\n    )                               AS \\"executor!: String\\"\\n\\nFROM tasks t\\nWHERE t.project\_id = $1\\nORDER BY t.created\_at DESC\\n" *rows\_affected*=0 *rows\_returned*=202 *elapsed*=1.281210542s *elapsed\_secs*=1.281210542 *slow\_threshold*=1s

2025-12-17T15:23:25.350788Z  WARN sqlx::query: slow statement: execution time exceeded alert threshold *summary*="SELECT t.id AS \\"id!: …" *db.statement*="\\n\\nSELECT\\n  t.id                            AS \\"id!: Uuid\\",\\n  t.project\_id                    AS \\"project\_id!: Uuid\\",\\n  t.title,\\n  t.description,\\n  t.status                        AS \\"status!: TaskStatus\\",\\n  t.parent\_workspace\_id           AS \\"parent\_workspace\_id: Uuid\\",\\n  t.shared\_task\_id                AS \\"shared\_task\_id: Uuid\\",\\n  t.created\_at                    AS \\"created\_at!: DateTime<Utc>\\",\\n  t.updated\_at                    AS \\"updated\_at!: DateTime<Utc>\\",\\n\\n  CASE WHEN EXISTS (\\n    SELECT 1\\n      FROM workspaces w\\n      JOIN sessions s ON s.workspace\_id = w.id\\n      JOIN execution\_processes ep ON ep.session\_id = s.id\\n     WHERE w.task\_id       = t.id\\n       AND ep.status        = 'running'\\n       AND ep.run\_reason IN ('setupscript','cleanupscript','codingagent')\\n     LIMIT 1\\n  ) THEN 1 ELSE 0 END            AS \\"has\_in\_progress\_attempt!: i64\\",\\n\\n  CASE WHEN (\\n    SELECT ep.status\\n      FROM workspaces w\\n      JOIN sessions s ON s.workspace\_id = w.id\\n      JOIN execution\_processes ep ON ep.session\_id = s.id\\n     WHERE w.task\_id       = t.id\\n     AND ep.run\_reason IN ('setupscript','cleanupscript','codingagent')\\n     ORDER BY ep.created\_at DESC\\n     LIMIT 1\\n  ) IN ('failed','killed') THEN 1 ELSE 0 END\\n                                 AS \\"last\_attempt\_failed!: i64\\",\\n\\n  ( SELECT s.executor\\n      FROM workspaces w\\n      JOIN sessions s ON s.workspace\_id = w.id\\n      WHERE w.task\_id = t.id\\n     ORDER BY s.created\_at DESC\\n      LIMIT 1\\n    )                               AS \\"executor!: String\\"\\n\\nFROM tasks t\\nWHERE t.project\_id = $1\\nORDER BY t.created\_at DESC\\n" *rows\_affected*=0 *rows\_returned*=202 *elapsed*=1.333812833s *elapsed\_secs*=1.333812833 *slow\_threshold*=1s

2025-12-17T15:23:25.401326Z  WARN sqlx::query: slow statement: execution time exceeded alert threshold *summary*="INSERT INTO execution\_processes ( …" *db.statement*="\\n\\nINSERT INTO execution\_processes (\\n                    id, session\_id, run\_reason, executor\_action,\\n                    status, exit\_code, started\_at, completed\_at, created\_at, updated\_at\\n                ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)\\n" *rows\_affected*=1 *rows\_returned*=0 *elapsed*=1.383690208s *elapsed\_secs*=1.383690208 *slow\_threshold*=1s

* Address feedback (vibe-kanban 81d8dbfa)

A PR opened by your colleague (https://github.com/BloopAI/vibe-kanban/pull/1569)
 got some feedback, let's address it.

```gh-comment
{
  "id": "2627479232",
  "comment_type": "review",
  "author": "ggordonhall",
  "body": "```suggestion\r\n-- 3. Migrate data: create one session per workspace\r\nINSERT INTO sessions (id, workspace_id, executor, created_at, updated_at)\r\nSELECT gen_random_uuid(), id, executor, created_at, updated_at FROM workspaces;\r\n```\r\n",
  "created_at": "2025-12-17T15:17:50Z",
  "url": "https://github.com/BloopAI/vibe-kanban/pull/1569#discussion_r2627479232",
  "path": "crates/db/migrations/20251216142123_refactor_task_attempts_to_workspaces_sessions.sql",
  "line": 26,
  "diff_hunk": "@@ -0,0 +1,121 @@\n+-- Refactor task_attempts into workspaces and sessions\n+-- - Rename task_attempts -> workspaces (keeps workspace-related fields)\n+-- - Create sessions table (executor moves here)\n+-- - Update execution_processes.task_attempt_id -> session_id\n+-- - Rename executor_sessions -> coding_agent_turns (drop redundant task_attempt_id)\n+-- - Rename merges.task_attempt_id -> workspace_id\n+-- - Rename tasks.parent_task_attempt -> parent_workspace_id\n+\n+-- 1. Rename task_attempts to workspaces (FK refs auto-update in schema)\n+ALTER TABLE task_attempts RENAME TO workspaces;\n+\n+-- 2. Create sessions table\n+CREATE TABLE sessions (\n+    id              BLOB PRIMARY KEY,\n+    workspace_id    BLOB NOT NULL,\n+    executor        TEXT,\n+    created_at      TEXT NOT NULL DEFAULT (datetime('now', 'subsec')),\n+    updated_at      TEXT NOT NULL DEFAULT (datetime('now', 'subsec')),\n+    FOREIGN KEY (workspace_id) REFERENCES workspaces(id) ON DELETE CASCADE\n+);\n+\n+CREATE INDEX idx_sessions_workspace_id ON sessions(workspace_id);\n+\n+-- 3. Migrate data: create one session per workspace (using workspace.id as session.id for simplicity)\n+INSERT INTO sessions (id, workspace_id, executor, created_at, updated_at)\n+SELECT id, id, executor, created_at, updated_at FROM workspaces;"
}
```

```gh-comment
{
  "id": "2627515578",
  "comment_type": "review",
  "author": "ggordonhall",
  "body": "Why not rename `attempt_repos` to `workspace_repos` here now that `attempt` is a legacy concept?",
  "created_at": "2025-12-17T15:27:21Z",
  "url": "https://github.com/BloopAI/vibe-kanban/pull/1569#discussion_r2627515578",
  "path": "crates/db/migrations/20251216142123_refactor_task_attempts_to_workspaces_sessions.sql",
  "line": 118,
  "diff_hunk": "@@ -0,0 +1,121 @@\n+-- Refactor task_attempts into workspaces and sessions\n+-- - Rename task_attempts -> workspaces (keeps workspace-related fields)\n+-- - Create sessions table (executor moves here)\n+-- - Update execution_processes.task_attempt_id -> session_id\n+-- - Rename executor_sessions -> coding_agent_turns (drop redundant task_attempt_id)\n+-- - Rename merges.task_attempt_id -> workspace_id\n+-- - Rename tasks.parent_task_attempt -> parent_workspace_id\n+\n+-- 1. Rename task_attempts to workspaces (FK refs auto-update in schema)\n+ALTER TABLE task_attempts RENAME TO workspaces;\n+\n+-- 2. Create sessions table\n+CREATE TABLE sessions (\n+    id              BLOB PRIMARY KEY,\n+    workspace_id    BLOB NOT NULL,\n+    executor        TEXT,\n+    created_at      TEXT NOT NULL DEFAULT (datetime('now', 'subsec')),\n+    updated_at      TEXT NOT NULL DEFAULT (datetime('now', 'subsec')),\n+    FOREIGN KEY (workspace_id) REFERENCES workspaces(id) ON DELETE CASCADE\n+);\n+\n+CREATE INDEX idx_sessions_workspace_id ON sessions(workspace_id);\n+\n+-- 3. Migrate data: create one session per workspace (using workspace.id as session.id for simplicity)\n+INSERT INTO sessions (id, workspace_id, executor, created_at, updated_at)\n+SELECT id, id, executor, created_at, updated_at FROM workspaces;\n+\n+-- 4. Drop executor column from workspaces\n+ALTER TABLE workspaces DROP COLUMN executor;\n+\n+-- 5. Rename merges.task_attempt_id to workspace_id\n+DROP INDEX idx_merges_task_attempt_id;\n+DROP INDEX idx_merges_open_pr;\n+ALTER TABLE merges RENAME COLUMN task_attempt_id TO workspace_id;\n+CREATE INDEX idx_merges_workspace_id ON merges(workspace_id);\n+CREATE INDEX idx_merges_open_pr ON merges(workspace_id, pr_status)\n+WHERE merge_type = 'pr' AND pr_status = 'open';\n+\n+-- 6. Rename tasks.parent_task_attempt to parent_workspace_id\n+DROP INDEX IF EXISTS idx_tasks_parent_task_attempt;\n+ALTER TABLE tasks RENAME COLUMN parent_task_attempt TO parent_workspace_id;\n+CREATE INDEX idx_tasks_parent_workspace_id ON tasks(parent_workspace_id);\n+\n+-- Steps 7-8 need FK disabled to avoid cascade deletes during DROP TABLE\n+-- sqlx workaround: end auto-transaction to allow PRAGMA to take effect\n+-- https://github.com/launchbadge/sqlx/issues/2085#issuecomment-1499859906\n+COMMIT;\n+\n+PRAGMA foreign_keys = OFF;\n+\n+BEGIN TRANSACTION;\n+\n+-- 7. Update execution_processes to reference session_id instead of task_attempt_id\n+-- (needs rebuild because FK target changes from workspaces to sessions)\n+DROP INDEX IF EXISTS idx_execution_processes_task_attempt_created_at;\n+DROP INDEX IF EXISTS idx_execution_processes_task_attempt_type_created;\n+\n+CREATE TABLE execution_processes_new (\n+    id              BLOB PRIMARY KEY,\n+    session_id      BLOB NOT NULL,\n+    run_reason      TEXT NOT NULL DEFAULT 'setupscript'\n+                       CHECK (run_reason IN ('setupscript','codingagent','devserver','cleanupscript')),\n+    executor_action TEXT NOT NULL DEFAULT '{}',\n+    status          TEXT NOT NULL DEFAULT 'running'\n+                       CHECK (status IN ('running','completed','failed','killed')),\n+    exit_code       INTEGER,\n+    dropped         INTEGER NOT NULL DEFAULT 0,\n+    started_at      TEXT NOT NULL DEFAULT (datetime('now', 'subsec')),\n+    completed_at    TEXT,\n+    created_at      TEXT NOT NULL DEFAULT (datetime('now', 'subsec')),\n+    updated_at      TEXT NOT NULL DEFAULT (datetime('now', 'subsec')),\n+    FOREIGN KEY (session_id) REFERENCES sessions(id) ON DELETE CASCADE\n+);\n+\n+-- Since we used workspace.id as session.id, the task_attempt_id values map directly\n+INSERT INTO execution_processes_new (id, session_id, run_reason, executor_action, status, exit_code, dropped, started_at, completed_at, created_at, updated_at)\n+SELECT id, task_attempt_id, run_reason, executor_action, status, exit_code, dropped, started_at, completed_at, created_at, updated_at\n+FROM execution_processes;\n+\n+DROP TABLE execution_processes;\n+ALTER TABLE execution_processes_new RENAME TO execution_processes;\n+\n+-- Recreate execution_processes indexes\n+CREATE INDEX idx_execution_processes_session_id ON execution_processes(session_id);\n+CREATE INDEX idx_execution_processes_status ON execution_processes(status);\n+CREATE INDEX idx_execution_processes_run_reason ON execution_processes(run_reason);\n+\n+-- 8. Rename executor_sessions to coding_agent_turns and drop task_attempt_id\n+-- (needs rebuild to drop the redundant task_attempt_id column)\n+CREATE TABLE coding_agent_turns (\n+    id                    BLOB PRIMARY KEY,\n+    execution_process_id  BLOB NOT NULL,\n+    session_id            TEXT,\n+    prompt                TEXT,\n+    summary               TEXT,\n+    created_at            TEXT NOT NULL DEFAULT (datetime('now', 'subsec')),\n+    updated_at            TEXT NOT NULL DEFAULT (datetime('now', 'subsec')),\n+    FOREIGN KEY (execution_process_id) REFERENCES execution_processes(id) ON DELETE CASCADE\n+);\n+\n+INSERT INTO coding_agent_turns (id, execution_process_id, session_id, prompt, summary, created_at, updated_at)\n+SELECT id, execution_process_id, session_id, prompt, summary, created_at, updated_at\n+FROM executor_sessions;\n+\n+DROP TABLE executor_sessions;\n+\n+-- Recreate coding_agent_turns indexes\n+CREATE INDEX idx_coding_agent_turns_execution_process_id ON coding_agent_turns(execution_process_id);\n+CREATE INDEX idx_coding_agent_turns_session_id ON coding_agent_turns(session_id);\n+\n+-- 9. attempt_repos: no changes needed - FK auto-updated when task_attempts renamed to workspaces"
}
```

```gh-comment
{
  "id": "2627694792",
  "comment_type": "review",
  "author": "ggordonhall",
  "body": "Maybe there's a better name than `external_session_id` here? `agent_session_id`? ",
  "created_at": "2025-12-17T16:16:24Z",
  "url": "https://github.com/BloopAI/vibe-kanban/pull/1569#discussion_r2627694792",
  "path": "crates/db/src/models/execution_process.rs",
  "line": 685,
  "diff_hunk": "@@ -618,4 +680,34 @@ impl ExecutionProcess {\n             )),\n         }\n     }\n+\n+    /// Find latest coding_agent_turn session_id by workspace (across all sessions)\n+    pub async fn find_latest_external_session_id_by_workspace("
}
```

```gh-comment
{
  "id": "2627707446",
  "comment_type": "review",
  "author": "ggordonhall",
  "body": "```suggestion\r\n    pub async fn cleanup_workspace(db: &DBService, workspace: &Workspace) {\r\n```",
  "created_at": "2025-12-17T16:19:31Z",
  "url": "https://github.com/BloopAI/vibe-kanban/pull/1569#discussion_r2627707446",
  "path": "crates/local-deployment/src/container.rs",
  "line": 146,
  "diff_hunk": "@@ -142,20 +143,20 @@ impl LocalContainerService {\n         map.remove(id)\n     }\n \n-    pub async fn cleanup_attempt_workspace(db: &DBService, attempt: &TaskAttempt) {\n-        let Some(container_ref) = &attempt.container_ref else {\n+    pub async fn cleanup_workspace_container(db: &DBService, workspace: &Workspace) {"
}
```

```gh-comment
{
  "id": "2627756192",
  "comment_type": "review",
  "author": "ggordonhall",
  "body": "Update `mcp` nomenclature",
  "created_at": "2025-12-17T16:31:49Z",
  "url": "https://github.com/BloopAI/vibe-kanban/pull/1569#discussion_r2627756192",
  "path": "crates/server/src/mcp/task_server.rs",
  "line": 352,
  "diff_hunk": "@@ -350,10 +349,9 @@ impl TaskServer {\n             project_id: ctx.project.id,\n             task_id: ctx.task.id,\n             task_title: ctx.task.title,\n-            attempt_id: ctx.task_attempt.id,\n-            attempt_branch: ctx.task_attempt.branch,\n+            attempt_id: ctx.workspace.id,"
}
```

```gh-comment
{
  "id": "2628161769",
  "comment_type": "review",
  "author": "ggordonhall",
  "body": "update, and similar in other events",
  "created_at": "2025-12-17T18:27:47Z",
  "url": "https://github.com/BloopAI/vibe-kanban/pull/1569#discussion_r2628161769",
  "path": "crates/server/src/routes/task_attempts.rs",
  "line": 1335,
  "diff_hunk": "@@ -1295,7 +1332,7 @@ pub async fn start_dev_server(\n             serde_json::json!({\n                 \"task_id\": task.id.to_string(),\n                 \"project_id\": project.id.to_string(),\n-                \"attempt_id\": task_attempt.id.to_string(),\n+                \"attempt_id\": workspace.id.to_string(),"
}
```

```gh-comment
{
  "id": "2628194289",
  "comment_type": "review",
  "author": "ggordonhall",
  "body": "Ugly, but we should rename this struct to avoid confusion with the more general concept of a workspace. Ideas...\r\n\r\n- `WorktreeContainer`\r\n...\r\n...\r\n\r\nChatGPT?",
  "created_at": "2025-12-17T18:36:30Z",
  "url": "https://github.com/BloopAI/vibe-kanban/pull/1569#discussion_r2628194289",
  "path": "crates/services/src/services/workspace_manager.rs",
  "line": 3,
  "diff_hunk": "@@ -1,6 +1,6 @@\n use std::path::{Path, PathBuf};\n \n-use db::models::{repo::Repo, task_attempt::TaskAttempt};\n+use db::models::{repo::Repo, workspace::Workspace as DbWorkspace};"
}
```

```gh-comment
{
  "id": "2628198036",
  "comment_type": "review",
  "author": "ggordonhall",
  "body": "We could add a BE route for this, and similar hooks where we're aggregating this information on the fly",
  "created_at": "2025-12-17T18:37:46Z",
  "url": "https://github.com/BloopAI/vibe-kanban/pull/1569#discussion_r2628198036",
  "path": "frontend/src/hooks/useTaskAttempts.ts",
  "line": 43,
  "diff_hunk": "@@ -16,10 +20,36 @@ export function useTaskAttempts(taskId?: string, opts?: Options) {\n   const enabled = (opts?.enabled ?? true) && !!taskId;\n   const refetchInterval = opts?.refetchInterval ?? 5000;\n \n-  return useQuery<TaskAttempt[]>({\n+  return useQuery<Workspace[]>({\n     queryKey: taskAttemptKeys.byTask(taskId),\n     queryFn: () => attemptsApi.getAll(taskId!),\n     enabled,\n     refetchInterval,\n   });\n }\n+\n+/**\n+ * Hook for components that need executor field for all attempts.\n+ * Fetches all attempts and their sessions in parallel.\n+ */\n+export function useTaskAttemptsWithSessions(taskId?: string, opts?: Options) {\n+  const enabled = (opts?.enabled ?? true) && !!taskId;\n+  const refetchInterval = opts?.refetchInterval ?? 5000;\n+\n+  return useQuery<WorkspaceWithSession[]>({\n+    queryKey: taskAttemptKeys.byTaskWithSessions(taskId),\n+    queryFn: async () => {\n+      const attempts = await attemptsApi.getAll(taskId!);\n+      // Fetch sessions for all attempts in parallel"
}
```

* Address feedback (vibe-kanban 81d8dbfa)

A PR opened by your colleague (https://github.com/BloopAI/vibe-kanban/pull/1569)
 got some feedback, let's address it.

```gh-comment
{
  "id": "2627479232",
  "comment_type": "review",
  "author": "ggordonhall",
  "body": "```suggestion\r\n-- 3. Migrate data: create one session per workspace\r\nINSERT INTO sessions (id, workspace_id, executor, created_at, updated_at)\r\nSELECT gen_random_uuid(), id, executor, created_at, updated_at FROM workspaces;\r\n```\r\n",
  "created_at": "2025-12-17T15:17:50Z",
  "url": "https://github.com/BloopAI/vibe-kanban/pull/1569#discussion_r2627479232",
  "path": "crates/db/migrations/20251216142123_refactor_task_attempts_to_workspaces_sessions.sql",
  "line": 26,
  "diff_hunk": "@@ -0,0 +1,121 @@\n+-- Refactor task_attempts into workspaces and sessions\n+-- - Rename task_attempts -> workspaces (keeps workspace-related fields)\n+-- - Create sessions table (executor moves here)\n+-- - Update execution_processes.task_attempt_id -> session_id\n+-- - Rename executor_sessions -> coding_agent_turns (drop redundant task_attempt_id)\n+-- - Rename merges.task_attempt_id -> workspace_id\n+-- - Rename tasks.parent_task_attempt -> parent_workspace_id\n+\n+-- 1. Rename task_attempts to workspaces (FK refs auto-update in schema)\n+ALTER TABLE task_attempts RENAME TO workspaces;\n+\n+-- 2. Create sessions table\n+CREATE TABLE sessions (\n+    id              BLOB PRIMARY KEY,\n+    workspace_id    BLOB NOT NULL,\n+    executor        TEXT,\n+    created_at      TEXT NOT NULL DEFAULT (datetime('now', 'subsec')),\n+    updated_at      TEXT NOT NULL DEFAULT (datetime('now', 'subsec')),\n+    FOREIGN KEY (workspace_id) REFERENCES workspaces(id) ON DELETE CASCADE\n+);\n+\n+CREATE INDEX idx_sessions_workspace_id ON sessions(workspace_id);\n+\n+-- 3. Migrate data: create one session per workspace (using workspace.id as session.id for simplicity)\n+INSERT INTO sessions (id, workspace_id, executor, created_at, updated_at)\n+SELECT id, id, executor, created_at, updated_at FROM workspaces;"
}
```

```gh-comment
{
  "id": "2627515578",
  "comment_type": "review",
  "author": "ggordonhall",
  "body": "Why not rename `attempt_repos` to `workspace_repos` here now that `attempt` is a legacy concept?",
  "created_at": "2025-12-17T15:27:21Z",
  "url": "https://github.com/BloopAI/vibe-kanban/pull/1569#discussion_r2627515578",
  "path": "crates/db/migrations/20251216142123_refactor_task_attempts_to_workspaces_sessions.sql",
  "line": 118,
  "diff_hunk": "@@ -0,0 +1,121 @@\n+-- Refactor task_attempts into workspaces and sessions\n+-- - Rename task_attempts -> workspaces (keeps workspace-related fields)\n+-- - Create sessions table (executor moves here)\n+-- - Update execution_processes.task_attempt_id -> session_id\n+-- - Rename executor_sessions -> coding_agent_turns (drop redundant task_attempt_id)\n+-- - Rename merges.task_attempt_id -> workspace_id\n+-- - Rename tasks.parent_task_attempt -> parent_workspace_id\n+\n+-- 1. Rename task_attempts to workspaces (FK refs auto-update in schema)\n+ALTER TABLE task_attempts RENAME TO workspaces;\n+\n+-- 2. Create sessions table\n+CREATE TABLE sessions (\n+    id              BLOB PRIMARY KEY,\n+    workspace_id    BLOB NOT NULL,\n+    executor        TEXT,\n+    created_at      TEXT NOT NULL DEFAULT (datetime('now', 'subsec')),\n+    updated_at      TEXT NOT NULL DEFAULT (datetime('now', 'subsec')),\n+    FOREIGN KEY (workspace_id) REFERENCES workspaces(id) ON DELETE CASCADE\n+);\n+\n+CREATE INDEX idx_sessions_workspace_id ON sessions(workspace_id);\n+\n+-- 3. Migrate data: create one session per workspace (using workspace.id as session.id for simplicity)\n+INSERT INTO sessions (id, workspace_id, executor, created_at, updated_at)\n+SELECT id, id, executor, created_at, updated_at FROM workspaces;\n+\n+-- 4. Drop executor column from workspaces\n+ALTER TABLE workspaces DROP COLUMN executor;\n+\n+-- 5. Rename merges.task_attempt_id to workspace_id\n+DROP INDEX idx_merges_task_attempt_id;\n+DROP INDEX idx_merges_open_pr;\n+ALTER TABLE merges RENAME COLUMN task_attempt_id TO workspace_id;\n+CREATE INDEX idx_merges_workspace_id ON merges(workspace_id);\n+CREATE INDEX idx_merges_open_pr ON merges(workspace_id, pr_status)\n+WHERE merge_type = 'pr' AND pr_status = 'open';\n+\n+-- 6. Rename tasks.parent_task_attempt to parent_workspace_id\n+DROP INDEX IF EXISTS idx_tasks_parent_task_attempt;\n+ALTER TABLE tasks RENAME COLUMN parent_task_attempt TO parent_workspace_id;\n+CREATE INDEX idx_tasks_parent_workspace_id ON tasks(parent_workspace_id);\n+\n+-- Steps 7-8 need FK disabled to avoid cascade deletes during DROP TABLE\n+-- sqlx workaround: end auto-transaction to allow PRAGMA to take effect\n+-- https://github.com/launchbadge/sqlx/issues/2085#issuecomment-1499859906\n+COMMIT;\n+\n+PRAGMA foreign_keys = OFF;\n+\n+BEGIN TRANSACTION;\n+\n+-- 7. Update execution_processes to reference session_id instead of task_attempt_id\n+-- (needs rebuild because FK target changes from workspaces to sessions)\n+DROP INDEX IF EXISTS idx_execution_processes_task_attempt_created_at;\n+DROP INDEX IF EXISTS idx_execution_processes_task_attempt_type_created;\n+\n+CREATE TABLE execution_processes_new (\n+    id              BLOB PRIMARY KEY,\n+    session_id      BLOB NOT NULL,\n+    run_reason      TEXT NOT NULL DEFAULT 'setupscript'\n+                       CHECK (run_reason IN ('setupscript','codingagent','devserver','cleanupscript')),\n+    executor_action TEXT NOT NULL DEFAULT '{}',\n+    status          TEXT NOT NULL DEFAULT 'running'\n+                       CHECK (status IN ('running','completed','failed','killed')),\n+    exit_code       INTEGER,\n+    dropped         INTEGER NOT NULL DEFAULT 0,\n+    started_at      TEXT NOT NULL DEFAULT (datetime('now', 'subsec')),\n+    completed_at    TEXT,\n+    created_at      TEXT NOT NULL DEFAULT (datetime('now', 'subsec')),\n+    updated_at      TEXT NOT NULL DEFAULT (datetime('now', 'subsec')),\n+    FOREIGN KEY (session_id) REFERENCES sessions(id) ON DELETE CASCADE\n+);\n+\n+-- Since we used workspace.id as session.id, the task_attempt_id values map directly\n+INSERT INTO execution_processes_new (id, session_id, run_reason, executor_action, status, exit_code, dropped, started_at, completed_at, created_at, updated_at)\n+SELECT id, task_attempt_id, run_reason, executor_action, status, exit_code, dropped, started_at, completed_at, created_at, updated_at\n+FROM execution_processes;\n+\n+DROP TABLE execution_processes;\n+ALTER TABLE execution_processes_new RENAME TO execution_processes;\n+\n+-- Recreate execution_processes indexes\n+CREATE INDEX idx_execution_processes_session_id ON execution_processes(session_id);\n+CREATE INDEX idx_execution_processes_status ON execution_processes(status);\n+CREATE INDEX idx_execution_processes_run_reason ON execution_processes(run_reason);\n+\n+-- 8. Rename executor_sessions to coding_agent_turns and drop task_attempt_id\n+-- (needs rebuild to drop the redundant task_attempt_id column)\n+CREATE TABLE coding_agent_turns (\n+    id                    BLOB PRIMARY KEY,\n+    execution_process_id  BLOB NOT NULL,\n+    session_id            TEXT,\n+    prompt                TEXT,\n+    summary               TEXT,\n+    created_at            TEXT NOT NULL DEFAULT (datetime('now', 'subsec')),\n+    updated_at            TEXT NOT NULL DEFAULT (datetime('now', 'subsec')),\n+    FOREIGN KEY (execution_process_id) REFERENCES execution_processes(id) ON DELETE CASCADE\n+);\n+\n+INSERT INTO coding_agent_turns (id, execution_process_id, session_id, prompt, summary, created_at, updated_at)\n+SELECT id, execution_process_id, session_id, prompt, summary, created_at, updated_at\n+FROM executor_sessions;\n+\n+DROP TABLE executor_sessions;\n+\n+-- Recreate coding_agent_turns indexes\n+CREATE INDEX idx_coding_agent_turns_execution_process_id ON coding_agent_turns(execution_process_id);\n+CREATE INDEX idx_coding_agent_turns_session_id ON coding_agent_turns(session_id);\n+\n+-- 9. attempt_repos: no changes needed - FK auto-updated when task_attempts renamed to workspaces"
}
```

```gh-comment
{
  "id": "2627694792",
  "comment_type": "review",
  "author": "ggordonhall",
  "body": "Maybe there's a better name than `external_session_id` here? `agent_session_id`? ",
  "created_at": "2025-12-17T16:16:24Z",
  "url": "https://github.com/BloopAI/vibe-kanban/pull/1569#discussion_r2627694792",
  "path": "crates/db/src/models/execution_process.rs",
  "line": 685,
  "diff_hunk": "@@ -618,4 +680,34 @@ impl ExecutionProcess {\n             )),\n         }\n     }\n+\n+    /// Find latest coding_agent_turn session_id by workspace (across all sessions)\n+    pub async fn find_latest_external_session_id_by_workspace("
}
```

```gh-comment
{
  "id": "2627707446",
  "comment_type": "review",
  "author": "ggordonhall",
  "body": "```suggestion\r\n    pub async fn cleanup_workspace(db: &DBService, workspace: &Workspace) {\r\n```",
  "created_at": "2025-12-17T16:19:31Z",
  "url": "https://github.com/BloopAI/vibe-kanban/pull/1569#discussion_r2627707446",
  "path": "crates/local-deployment/src/container.rs",
  "line": 146,
  "diff_hunk": "@@ -142,20 +143,20 @@ impl LocalContainerService {\n         map.remove(id)\n     }\n \n-    pub async fn cleanup_attempt_workspace(db: &DBService, attempt: &TaskAttempt) {\n-        let Some(container_ref) = &attempt.container_ref else {\n+    pub async fn cleanup_workspace_container(db: &DBService, workspace: &Workspace) {"
}
```

```gh-comment
{
  "id": "2627756192",
  "comment_type": "review",
  "author": "ggordonhall",
  "body": "Update `mcp` nomenclature",
  "created_at": "2025-12-17T16:31:49Z",
  "url": "https://github.com/BloopAI/vibe-kanban/pull/1569#discussion_r2627756192",
  "path": "crates/server/src/mcp/task_server.rs",
  "line": 352,
  "diff_hunk": "@@ -350,10 +349,9 @@ impl TaskServer {\n             project_id: ctx.project.id,\n             task_id: ctx.task.id,\n             task_title: ctx.task.title,\n-            attempt_id: ctx.task_attempt.id,\n-            attempt_branch: ctx.task_attempt.branch,\n+            attempt_id: ctx.workspace.id,"
}
```

```gh-comment
{
  "id": "2628161769",
  "comment_type": "review",
  "author": "ggordonhall",
  "body": "update, and similar in other events",
  "created_at": "2025-12-17T18:27:47Z",
  "url": "https://github.com/BloopAI/vibe-kanban/pull/1569#discussion_r2628161769",
  "path": "crates/server/src/routes/task_attempts.rs",
  "line": 1335,
  "diff_hunk": "@@ -1295,7 +1332,7 @@ pub async fn start_dev_server(\n             serde_json::json!({\n                 \"task_id\": task.id.to_string(),\n                 \"project_id\": project.id.to_string(),\n-                \"attempt_id\": task_attempt.id.to_string(),\n+                \"attempt_id\": workspace.id.to_string(),"
}
```

```gh-comment
{
  "id": "2628194289",
  "comment_type": "review",
  "author": "ggordonhall",
  "body": "Ugly, but we should rename this struct to avoid confusion with the more general concept of a workspace. Ideas...\r\n\r\n- `WorktreeContainer`\r\n...\r\n...\r\n\r\nChatGPT?",
  "created_at": "2025-12-17T18:36:30Z",
  "url": "https://github.com/BloopAI/vibe-kanban/pull/1569#discussion_r2628194289",
  "path": "crates/services/src/services/workspace_manager.rs",
  "line": 3,
  "diff_hunk": "@@ -1,6 +1,6 @@\n use std::path::{Path, PathBuf};\n \n-use db::models::{repo::Repo, task_attempt::TaskAttempt};\n+use db::models::{repo::Repo, workspace::Workspace as DbWorkspace};"
}
```

```gh-comment
{
  "id": "2628198036",
  "comment_type": "review",
  "author": "ggordonhall",
  "body": "We could add a BE route for this, and similar hooks where we're aggregating this information on the fly",
  "created_at": "2025-12-17T18:37:46Z",
  "url": "https://github.com/BloopAI/vibe-kanban/pull/1569#discussion_r2628198036",
  "path": "frontend/src/hooks/useTaskAttempts.ts",
  "line": 43,
  "diff_hunk": "@@ -16,10 +20,36 @@ export function useTaskAttempts(taskId?: string, opts?: Options) {\n   const enabled = (opts?.enabled ?? true) && !!taskId;\n   const refetchInterval = opts?.refetchInterval ?? 5000;\n \n-  return useQuery<TaskAttempt[]>({\n+  return useQuery<Workspace[]>({\n     queryKey: taskAttemptKeys.byTask(taskId),\n     queryFn: () => attemptsApi.getAll(taskId!),\n     enabled,\n     refetchInterval,\n   });\n }\n+\n+/**\n+ * Hook for components that need executor field for all attempts.\n+ * Fetches all attempts and their sessions in parallel.\n+ */\n+export function useTaskAttemptsWithSessions(taskId?: string, opts?: Options) {\n+  const enabled = (opts?.enabled ?? true) && !!taskId;\n+  const refetchInterval = opts?.refetchInterval ?? 5000;\n+\n+  return useQuery<WorkspaceWithSession[]>({\n+    queryKey: taskAttemptKeys.byTaskWithSessions(taskId),\n+    queryFn: async () => {\n+      const attempts = await attemptsApi.getAll(taskId!);\n+      // Fetch sessions for all attempts in parallel"
}
```
This commit is contained in:
Alex Netsch
2025-12-18 14:45:10 +00:00
committed by GitHub
parent 8a689ae4cb
commit 4188adc2a9
155 changed files with 2831 additions and 1907 deletions

View File

@@ -21,7 +21,7 @@ import {
} from 'lucide-react';
import '@/styles/diff-style-overrides.css';
import { attemptsApi } from '@/lib/api';
import type { TaskAttempt } from 'shared/types';
import type { Workspace } from 'shared/types';
import {
useReview,
type ReviewDraft,
@@ -40,7 +40,7 @@ type Props = {
diff: Diff;
expanded: boolean;
onToggle: () => void;
selectedAttempt: TaskAttempt | null;
selectedAttempt: Workspace | null;
};
function labelAndIcon(diff: Diff) {

View File

@@ -3,12 +3,12 @@ import WYSIWYGEditor from '@/components/ui/wysiwyg';
import {
ActionType,
NormalizedEntry,
TaskAttempt,
ToolStatus,
type NormalizedEntryType,
type TaskWithAttemptStatus,
type JsonValue,
} from 'shared/types.ts';
import type { WorkspaceWithSession } from '@/types/attempt';
import type { ProcessStartPayload } from '@/types/logs';
import FileChangeRenderer from './FileChangeRenderer';
import { useExpandable } from '@/stores/useExpandableStore';
@@ -40,7 +40,7 @@ type Props = {
expansionKey: string;
diffDeletable?: boolean;
executionProcessId?: string;
taskAttempt?: TaskAttempt;
taskAttempt?: WorkspaceWithSession;
task?: TaskWithAttemptStatus;
};

View File

@@ -60,8 +60,8 @@ export function NextActionCard({
const [copied, setCopied] = useState(false);
const { data: attempt } = useQuery({
queryKey: ['attempt', attemptId],
queryFn: () => attemptsApi.get(attemptId!),
queryKey: ['attemptWithSession', attemptId],
queryFn: () => attemptsApi.getWithSession(attemptId!),
enabled: !!attemptId && failed,
});
const { capabilities } = useUserSystem();

View File

@@ -8,7 +8,7 @@ import { Button } from '@/components/ui/button';
import { Alert, AlertDescription } from '@/components/ui/alert';
import { AlertCircle, Loader2, Paperclip, Send, X } from 'lucide-react';
import { imagesApi } from '@/lib/api';
import type { TaskAttempt } from 'shared/types';
import type { WorkspaceWithSession } from '@/types/attempt';
import { useAttemptExecution } from '@/hooks/useAttemptExecution';
import { useUserSystem } from '@/components/ConfigProvider';
import { useBranchStatus } from '@/hooks/useBranchStatus';
@@ -22,7 +22,7 @@ export function RetryEditorInline({
initialContent,
onCancelled,
}: {
attempt: TaskAttempt;
attempt: WorkspaceWithSession;
executionProcessId: string;
initialContent: string;
onCancelled?: () => void;

View File

@@ -1,6 +1,7 @@
import { useState } from 'react';
import WYSIWYGEditor from '@/components/ui/wysiwyg';
import { TaskAttempt, BaseAgentCapability } from 'shared/types';
import { BaseAgentCapability } from 'shared/types';
import type { WorkspaceWithSession } from '@/types/attempt';
import { useUserSystem } from '@/components/ConfigProvider';
import { useRetryUi } from '@/contexts/RetryUiContext';
import { useAttemptExecution } from '@/hooks/useAttemptExecution';
@@ -13,7 +14,7 @@ const UserMessage = ({
}: {
content: string;
executionProcessId?: string;
taskAttempt?: TaskAttempt;
taskAttempt?: WorkspaceWithSession;
}) => {
const [isEditing, setIsEditing] = useState(false);
const { capabilities } = useUserSystem();

View File

@@ -17,9 +17,9 @@ import {
useTask,
useAttempt,
useRepoBranchSelection,
useTaskAttempts,
useProjectRepos,
} from '@/hooks';
import { useTaskAttemptsWithSessions } from '@/hooks/useTaskAttempts';
import { useProject } from '@/contexts/ProjectContext';
import { useUserSystem } from '@/components/ConfigProvider';
import { paths } from '@/lib/paths';
@@ -52,7 +52,7 @@ const CreateAttemptDialogImpl = NiceModal.create<CreateAttemptDialogProps>(
useState<ExecutorProfileId | null>(null);
const { data: attempts = [], isLoading: isLoadingAttempts } =
useTaskAttempts(taskId, {
useTaskAttemptsWithSessions(taskId, {
enabled: modal.visible,
refetchInterval: 5000,
});
@@ -61,7 +61,7 @@ const CreateAttemptDialogImpl = NiceModal.create<CreateAttemptDialogProps>(
enabled: modal.visible,
});
const parentAttemptId = task?.parent_task_attempt ?? undefined;
const parentAttemptId = task?.parent_workspace_id ?? undefined;
const { data: parentAttempt, isLoading: isLoadingParent } = useAttempt(
parentAttemptId,
{ enabled: modal.visible && !!parentAttemptId }
@@ -74,7 +74,7 @@ const CreateAttemptDialogImpl = NiceModal.create<CreateAttemptDialogProps>(
configs: repoBranchConfigs,
isLoading: isLoadingBranches,
setRepoBranch,
getAttemptRepoInputs,
getWorkspaceRepoInputs,
reset: resetBranchSelection,
} = useRepoBranchSelection({
repos: projectRepos,
@@ -147,7 +147,7 @@ const CreateAttemptDialogImpl = NiceModal.create<CreateAttemptDialogProps>(
)
return;
try {
const repos = getAttemptRepoInputs();
const repos = getWorkspaceRepoInputs();
await createAttempt({
profile: effectiveProfile,

View File

@@ -17,7 +17,7 @@ import { useCallback, useEffect, useMemo, useState } from 'react';
import { attemptsApi } from '@/lib/api.ts';
import { useTranslation } from 'react-i18next';
import { TaskAttempt, TaskWithAttemptStatus } from 'shared/types';
import { TaskWithAttemptStatus, Workspace } from 'shared/types';
import { Loader2 } from 'lucide-react';
import NiceModal, { useModal } from '@ebay/nice-modal-react';
import { useAuth, useRepoBranches } from '@/hooks';
@@ -35,7 +35,7 @@ import { useUserSystem } from '@/components/ConfigProvider';
import { defineModal } from '@/lib/modals';
interface CreatePRDialogProps {
attempt: TaskAttempt;
attempt: Workspace;
task: TaskWithAttemptStatus;
repoId: string;
targetBranch?: string;

View File

@@ -16,7 +16,7 @@ import {
GitOperationsProvider,
useGitOperationsError,
} from '@/contexts/GitOperationsContext';
import type { Merge, TaskAttempt, TaskWithAttemptStatus } from 'shared/types';
import type { Merge, TaskWithAttemptStatus, Workspace } from 'shared/types';
import NiceModal, { useModal } from '@ebay/nice-modal-react';
import { defineModal } from '@/lib/modals';
@@ -26,7 +26,7 @@ export interface GitActionsDialogProps {
}
interface GitActionsDialogContentProps {
attempt: TaskAttempt;
attempt: Workspace;
task: TaskWithAttemptStatus;
}

View File

@@ -172,7 +172,7 @@ const TaskFormDialogImpl = NiceModal.create<TaskFormDialogProps>((props) => {
title: value.title,
description: value.description,
status: value.status,
parent_task_attempt: null,
parent_workspace_id: null,
image_ids: images.length > 0 ? images.map((img) => img.id) : null,
},
},
@@ -186,7 +186,7 @@ const TaskFormDialogImpl = NiceModal.create<TaskFormDialogProps>((props) => {
title: value.title,
description: value.description,
status: null,
parent_task_attempt:
parent_workspace_id:
mode === 'subtask' ? props.parentTaskAttemptId : null,
image_ids: imageIds,
shared_task_id: null,

View File

@@ -12,12 +12,13 @@ import { PlusIcon } from 'lucide-react';
import { openTaskForm } from '@/lib/openTaskForm';
import { useTaskRelationships } from '@/hooks/useTaskRelationships';
import { DataTable, type ColumnDef } from '@/components/ui/table/data-table';
import type { Task, TaskAttempt } from 'shared/types';
import type { Task } from 'shared/types';
import type { Workspace } from 'shared/types';
export interface ViewRelatedTasksDialogProps {
attemptId: string;
projectId: string;
attempt: TaskAttempt | null;
attempt: Workspace | null;
onNavigateToTask?: (taskId: string) => void;
}

View File

@@ -16,16 +16,17 @@ import {
useConversationHistory,
} from '@/hooks/useConversationHistory';
import { Loader2 } from 'lucide-react';
import { TaskAttempt, TaskWithAttemptStatus } from 'shared/types';
import { TaskWithAttemptStatus } from 'shared/types';
import type { WorkspaceWithSession } from '@/types/attempt';
import { ApprovalFormProvider } from '@/contexts/ApprovalFormContext';
interface VirtualizedListProps {
attempt: TaskAttempt;
attempt: WorkspaceWithSession;
task?: TaskWithAttemptStatus;
}
interface MessageListContext {
attempt: TaskAttempt;
attempt: WorkspaceWithSession;
task?: TaskWithAttemptStatus;
}

View File

@@ -9,7 +9,8 @@ import {
TooltipTrigger,
} from '../ui/tooltip';
import type { LayoutMode } from '../layout/TasksLayout';
import type { TaskAttempt, TaskWithAttemptStatus } from 'shared/types';
import type { TaskWithAttemptStatus } from 'shared/types';
import type { Workspace } from 'shared/types';
import { ActionsDropdown } from '../ui/actions-dropdown';
import { usePostHog } from 'posthog-js/react';
import type { SharedTaskRecord } from '@/hooks/useProjectTasks';
@@ -19,7 +20,7 @@ interface AttemptHeaderActionsProps {
mode?: LayoutMode;
onModeChange?: (mode: LayoutMode) => void;
task: TaskWithAttemptStatus;
attempt?: TaskAttempt | null;
attempt?: Workspace | null;
sharedTask?: SharedTaskRecord;
}

View File

@@ -14,13 +14,14 @@ import {
TooltipProvider,
TooltipTrigger,
} from '@/components/ui/tooltip';
import type { TaskAttempt, Diff, DiffChangeKind } from 'shared/types';
import type { Diff, DiffChangeKind } from 'shared/types';
import type { Workspace } from 'shared/types';
import GitOperations, {
type GitOperationsInputs,
} from '@/components/tasks/Toolbar/GitOperations.tsx';
interface DiffsPanelProps {
selectedAttempt: TaskAttempt | null;
selectedAttempt: Workspace | null;
gitOps?: GitOperationsInputs;
}
@@ -151,7 +152,7 @@ interface DiffsPanelContentProps {
allCollapsed: boolean;
handleCollapseAll: () => void;
toggle: (id: string) => void;
selectedAttempt: TaskAttempt | null;
selectedAttempt: Workspace | null;
gitOps?: GitOperationsInputs;
loading: boolean;
t: (key: string, params?: Record<string, unknown>) => string;

View File

@@ -1,4 +1,5 @@
import type { TaskAttempt, TaskWithAttemptStatus } from 'shared/types';
import type { TaskWithAttemptStatus } from 'shared/types';
import type { WorkspaceWithSession } from '@/types/attempt';
import VirtualizedList from '@/components/logs/VirtualizedList';
import { TaskFollowUpSection } from '@/components/tasks/TaskFollowUpSection';
import { EntriesProvider } from '@/contexts/EntriesContext';
@@ -6,7 +7,7 @@ import { RetryUiProvider } from '@/contexts/RetryUiContext';
import type { ReactNode } from 'react';
interface TaskAttemptPanelProps {
attempt: TaskAttempt | undefined;
attempt: WorkspaceWithSession | undefined;
task: TaskWithAttemptStatus | null;
children: (sections: { logs: ReactNode; followUp: ReactNode }) => ReactNode;
}

View File

@@ -1,10 +1,11 @@
import { useTranslation } from 'react-i18next';
import { useProject } from '@/contexts/ProjectContext';
import { useTaskAttempts } from '@/hooks/useTaskAttempts';
import { useTaskAttempt } from '@/hooks/useTaskAttempt';
import { useTaskAttemptsWithSessions } from '@/hooks/useTaskAttempts';
import { useTaskAttemptWithSession } from '@/hooks/useTaskAttempt';
import { useNavigateWithSearch } from '@/hooks';
import { paths } from '@/lib/paths';
import type { TaskWithAttemptStatus, TaskAttempt } from 'shared/types';
import type { TaskWithAttemptStatus } from 'shared/types';
import type { WorkspaceWithSession } from '@/types/attempt';
import { NewCardContent } from '../ui/new-card';
import { Button } from '../ui/button';
import { PlusIcon } from 'lucide-react';
@@ -25,11 +26,10 @@ const TaskPanel = ({ task }: TaskPanelProps) => {
data: attempts = [],
isLoading: isAttemptsLoading,
isError: isAttemptsError,
} = useTaskAttempts(task?.id);
} = useTaskAttemptsWithSessions(task?.id);
const { data: parentAttempt, isLoading: isParentLoading } = useTaskAttempt(
task?.parent_task_attempt || undefined
);
const { data: parentAttempt, isLoading: isParentLoading } =
useTaskAttemptWithSession(task?.parent_workspace_id || undefined);
const formatTimeAgo = (iso: string) => {
const d = new Date(iso);
@@ -76,7 +76,7 @@ const TaskPanel = ({ task }: TaskPanelProps) => {
const titleContent = `# ${task.title || 'Task'}`;
const descriptionContent = task.description || '';
const attemptColumns: ColumnDef<TaskAttempt>[] = [
const attemptColumns: ColumnDef<WorkspaceWithSession>[] = [
{
id: 'executor',
header: '',
@@ -109,7 +109,7 @@ const TaskPanel = ({ task }: TaskPanelProps) => {
</div>
<div className="mt-6 flex-shrink-0 space-y-4">
{task.parent_task_attempt && (
{task.parent_workspace_id && (
<DataTable
data={parentAttempt ? [parentAttempt] : []}
columns={attemptColumns}

View File

@@ -45,16 +45,16 @@ export function TaskCard({
const handleParentClick = useCallback(
async (e: React.MouseEvent) => {
e.stopPropagation();
if (!task.parent_task_attempt || isNavigatingToParent) return;
if (!task.parent_workspace_id || isNavigatingToParent) return;
setIsNavigatingToParent(true);
try {
const parentAttempt = await attemptsApi.get(task.parent_task_attempt);
const parentAttempt = await attemptsApi.get(task.parent_workspace_id);
navigate(
paths.attempt(
projectId,
parentAttempt.task_id,
task.parent_task_attempt
task.parent_workspace_id
)
);
} catch (error) {
@@ -62,7 +62,7 @@ export function TaskCard({
setIsNavigatingToParent(false);
}
},
[task.parent_task_attempt, projectId, navigate, isNavigatingToParent]
[task.parent_workspace_id, projectId, navigate, isNavigatingToParent]
);
const localRef = useRef<HTMLDivElement>(null);
@@ -116,7 +116,7 @@ export function TaskCard({
{task.last_attempt_failed && (
<XCircle className="h-4 w-4 text-destructive" />
)}
{task.parent_task_attempt && (
{task.parent_workspace_id && (
<Button
variant="icon"
onClick={handleParentClick}

View File

@@ -130,7 +130,7 @@ export function NoServerContent({
title: COMPANION_INSTALL_TASK_TITLE,
description: COMPANION_INSTALL_TASK_DESCRIPTION,
status: null,
parent_task_attempt: null,
parent_workspace_id: null,
image_ids: null,
shared_task_id: null,
},

View File

@@ -19,8 +19,8 @@ import { useCallback, useMemo, useState } from 'react';
import type {
RepoBranchStatus,
Merge,
TaskAttempt,
TaskWithAttemptStatus,
Workspace,
} from 'shared/types';
import { ChangeTargetBranchDialog } from '@/components/dialogs/tasks/ChangeTargetBranchDialog';
import RepoSelector from '@/components/tasks/RepoSelector';
@@ -32,7 +32,7 @@ import { useGitOperations } from '@/hooks/useGitOperations';
import { useRepoBranches } from '@/hooks';
interface GitOperationsProps {
selectedAttempt: TaskAttempt;
selectedAttempt: Workspace;
task: TaskWithAttemptStatus;
branchStatus: RepoBranchStatus[] | null;
isAttemptRunning: boolean;

View File

@@ -9,7 +9,8 @@ import {
DropdownMenuTrigger,
} from '@/components/ui/dropdown-menu';
import { MoreHorizontal } from 'lucide-react';
import type { TaskWithAttemptStatus, TaskAttempt } from 'shared/types';
import type { TaskWithAttemptStatus } from 'shared/types';
import type { Workspace } from 'shared/types';
import { useOpenInEditor } from '@/hooks/useOpenInEditor';
import { DeleteTaskConfirmationDialog } from '@/components/dialogs/tasks/DeleteTaskConfirmationDialog';
import { ViewProcessesDialog } from '@/components/dialogs/tasks/ViewProcessesDialog';
@@ -29,7 +30,7 @@ import { useAuth } from '@/hooks';
interface ActionsDropdownProps {
task?: TaskWithAttemptStatus | null;
attempt?: TaskAttempt | null;
attempt?: Workspace | null;
sharedTask?: SharedTaskRecord;
}

View File

@@ -11,7 +11,7 @@ import type {
ComponentInfo,
SelectedComponent,
} from '@/utils/previewBridge';
import type { TaskAttempt } from 'shared/types';
import type { Workspace } from 'shared/types';
import { genId } from '@/utils/id';
export interface ClickedEntry {
@@ -47,7 +47,7 @@ export function useClickedElements() {
interface ClickedElementsProviderProps {
children: ReactNode;
attempt?: TaskAttempt | null;
attempt?: Workspace | null;
}
const MAX_ELEMENTS = 20;

View File

@@ -1,7 +1,7 @@
export { useBranchStatus } from './useBranchStatus';
export { useAttemptExecution } from './useAttemptExecution';
export { useOpenInEditor } from './useOpenInEditor';
export { useTaskAttempt } from './useTaskAttempt';
export { useTaskAttempt, useTaskAttemptWithSession } from './useTaskAttempt';
export { useTaskImages } from './useTaskImages';
export { useImageUpload } from './useImageUpload';
export { useTaskMutations } from './useTaskMutations';

View File

@@ -1,6 +1,6 @@
import { useQuery } from '@tanstack/react-query';
import { attemptsApi } from '@/lib/api';
import type { TaskAttempt } from 'shared/types';
import type { Workspace } from 'shared/types';
export const attemptKeys = {
byId: (attemptId: string | undefined) => ['attempt', attemptId] as const,
@@ -13,7 +13,7 @@ type Options = {
export function useAttempt(attemptId?: string, opts?: Options) {
const enabled = (opts?.enabled ?? true) && !!attemptId;
return useQuery<TaskAttempt>({
return useQuery<Workspace>({
queryKey: attemptKeys.byId(attemptId),
queryFn: () => attemptsApi.get(attemptId!),
enabled,

View File

@@ -1,19 +1,19 @@
import { useMutation, useQueryClient } from '@tanstack/react-query';
import { attemptsApi } from '@/lib/api';
import type {
TaskAttempt,
ExecutorProfileId,
AttemptRepoInput,
WorkspaceRepoInput,
Workspace,
} from 'shared/types';
type CreateAttemptArgs = {
profile: ExecutorProfileId;
repos: AttemptRepoInput[];
repos: WorkspaceRepoInput[];
};
type UseAttemptCreationArgs = {
taskId: string;
onSuccess?: (attempt: TaskAttempt) => void;
onSuccess?: (attempt: Workspace) => void;
};
export function useAttemptCreation({
@@ -29,10 +29,10 @@ export function useAttemptCreation({
executor_profile_id: profile,
repos,
}),
onSuccess: (newAttempt: TaskAttempt) => {
onSuccess: (newAttempt: Workspace) => {
queryClient.setQueryData(
['taskAttempts', taskId],
(old: TaskAttempt[] = []) => [newAttempt, ...old]
(old: Workspace[] = []) => [newAttempt, ...old]
);
onSuccess?.(newAttempt);
},

View File

@@ -6,8 +6,8 @@ import {
ExecutorAction,
NormalizedEntry,
PatchType,
TaskAttempt,
ToolStatus,
Workspace,
} from 'shared/types';
import { useExecutionProcessesContext } from '@/contexts/ExecutionProcessesContext';
import { useCallback, useEffect, useMemo, useRef } from 'react';
@@ -41,7 +41,7 @@ type ExecutionProcessState = {
type ExecutionProcessStateStore = Record<string, ExecutionProcessState>;
interface UseConversationHistoryParams {
attempt: TaskAttempt;
attempt: Workspace;
onEntriesUpdated: OnEntriesUpdated;
}

View File

@@ -28,7 +28,7 @@ export const useExecutionProcesses = (
let endpoint: string | undefined;
if (taskAttemptId) {
const params = new URLSearchParams({ task_attempt_id: taskAttemptId });
const params = new URLSearchParams({ workspace_id: taskAttemptId });
if (typeof showSoftDeleted === 'boolean') {
params.set('show_soft_deleted', String(showSoftDeleted));
}

View File

@@ -21,7 +21,10 @@ type UseRepoBranchSelectionReturn = {
configs: RepoBranchConfig[];
isLoading: boolean;
setRepoBranch: (repoId: string, branch: string) => void;
getAttemptRepoInputs: () => Array<{ repo_id: string; target_branch: string }>;
getWorkspaceRepoInputs: () => Array<{
repo_id: string;
target_branch: string;
}>;
reset: () => void;
};
@@ -80,7 +83,7 @@ export function useRepoBranchSelection({
setUserOverrides({});
}, []);
const getAttemptRepoInputs = useCallback(() => {
const getWorkspaceRepoInputs = useCallback(() => {
return configs
.filter((config) => config.targetBranch !== null)
.map((config) => ({
@@ -93,7 +96,7 @@ export function useRepoBranchSelection({
configs,
isLoading: isLoadingBranches,
setRepoBranch,
getAttemptRepoInputs,
getWorkspaceRepoInputs,
reset,
};
}

View File

@@ -1,5 +1,6 @@
import { useQuery } from '@tanstack/react-query';
import { attemptsApi } from '@/lib/api';
import type { WorkspaceWithSession } from '@/types/attempt';
export function useTaskAttempt(attemptId?: string) {
return useQuery({
@@ -8,3 +9,15 @@ export function useTaskAttempt(attemptId?: string) {
enabled: !!attemptId,
});
}
/**
* Hook for components that need executor field (e.g., for capability checks).
* Fetches workspace with executor from latest session.
*/
export function useTaskAttemptWithSession(attemptId?: string) {
return useQuery<WorkspaceWithSession>({
queryKey: ['taskAttemptWithSession', attemptId],
queryFn: () => attemptsApi.getWithSession(attemptId!),
enabled: !!attemptId,
});
}

View File

@@ -1,10 +1,14 @@
import { useQuery } from '@tanstack/react-query';
import { attemptsApi } from '@/lib/api';
import type { TaskAttempt } from 'shared/types';
import { attemptsApi, sessionsApi } from '@/lib/api';
import type { Workspace } from 'shared/types';
import type { WorkspaceWithSession } from '@/types/attempt';
import { createWorkspaceWithSession } from '@/types/attempt';
export const taskAttemptKeys = {
all: ['taskAttempts'] as const,
byTask: (taskId: string | undefined) => ['taskAttempts', taskId] as const,
byTaskWithSessions: (taskId: string | undefined) =>
['taskAttemptsWithSessions', taskId] as const,
};
type Options = {
@@ -16,10 +20,36 @@ export function useTaskAttempts(taskId?: string, opts?: Options) {
const enabled = (opts?.enabled ?? true) && !!taskId;
const refetchInterval = opts?.refetchInterval ?? 5000;
return useQuery<TaskAttempt[]>({
return useQuery<Workspace[]>({
queryKey: taskAttemptKeys.byTask(taskId),
queryFn: () => attemptsApi.getAll(taskId!),
enabled,
refetchInterval,
});
}
/**
* Hook for components that need executor field for all attempts.
* Fetches all attempts and their sessions in parallel.
*/
export function useTaskAttemptsWithSessions(taskId?: string, opts?: Options) {
const enabled = (opts?.enabled ?? true) && !!taskId;
const refetchInterval = opts?.refetchInterval ?? 5000;
return useQuery<WorkspaceWithSession[]>({
queryKey: taskAttemptKeys.byTaskWithSessions(taskId),
queryFn: async () => {
const attempts = await attemptsApi.getAll(taskId!);
// Fetch sessions for all attempts in parallel
const sessionsResults = await Promise.all(
attempts.map((attempt) => sessionsApi.getByWorkspace(attempt.id))
);
return attempts.map((attempt, i) => {
const executor = sessionsResults[i][0]?.executor ?? 'unknown';
return createWorkspaceWithSession(attempt, executor);
});
},
enabled,
refetchInterval,
});
}

View File

@@ -29,10 +29,10 @@ export function useTaskMutations(projectId?: string) {
onSuccess: (createdTask: Task) => {
invalidateQueries();
// Invalidate parent's relationships cache if this is a subtask
if (createdTask.parent_task_attempt) {
if (createdTask.parent_workspace_id) {
queryClient.invalidateQueries({
queryKey: taskRelationshipsKeys.byAttempt(
createdTask.parent_task_attempt
createdTask.parent_workspace_id
),
});
}
@@ -51,10 +51,10 @@ export function useTaskMutations(projectId?: string) {
onSuccess: (createdTask: TaskWithAttemptStatus) => {
invalidateQueries();
// Invalidate parent's relationships cache if this is a subtask
if (createdTask.parent_task_attempt) {
if (createdTask.parent_workspace_id) {
queryClient.invalidateQueries({
queryKey: taskRelationshipsKeys.byAttempt(
createdTask.parent_task_attempt
createdTask.parent_workspace_id
),
});
}

View File

@@ -26,7 +26,6 @@ import {
SearchResult,
ShareTaskResponse,
Task,
TaskAttempt,
TaskRelationships,
Tag,
TagSearchParams,
@@ -88,7 +87,11 @@ import {
PushTaskAttemptRequest,
RepoBranchStatus,
AbortConflictsRequest,
Session,
Workspace,
} from 'shared/types';
import type { WorkspaceWithSession } from '@/types/attempt';
import { createWorkspaceWithSession } from '@/types/attempt';
export class ApiError<E = unknown> extends Error {
public status?: number;
@@ -464,6 +467,16 @@ export const tasksApi = {
},
};
// Sessions API
export const sessionsApi = {
getByWorkspace: async (workspaceId: string): Promise<Session[]> => {
const response = await makeRequest(
`/api/sessions?workspace_id=${workspaceId}`
);
return handleApiResponse<Session[]>(response);
},
};
// Task Attempts APIs
export const attemptsApi = {
getChildren: async (attemptId: string): Promise<TaskRelationships> => {
@@ -473,22 +486,32 @@ export const attemptsApi = {
return handleApiResponse<TaskRelationships>(response);
},
getAll: async (taskId: string): Promise<TaskAttempt[]> => {
getAll: async (taskId: string): Promise<Workspace[]> => {
const response = await makeRequest(`/api/task-attempts?task_id=${taskId}`);
return handleApiResponse<TaskAttempt[]>(response);
return handleApiResponse<Workspace[]>(response);
},
get: async (attemptId: string): Promise<TaskAttempt> => {
get: async (attemptId: string): Promise<Workspace> => {
const response = await makeRequest(`/api/task-attempts/${attemptId}`);
return handleApiResponse<TaskAttempt>(response);
return handleApiResponse<Workspace>(response);
},
create: async (data: CreateTaskAttemptBody): Promise<TaskAttempt> => {
/** Get workspace with executor from latest session (for components that need executor) */
getWithSession: async (attemptId: string): Promise<WorkspaceWithSession> => {
const [workspace, sessions] = await Promise.all([
attemptsApi.get(attemptId),
sessionsApi.getByWorkspace(attemptId),
]);
const executor = sessions[0]?.executor ?? 'unknown';
return createWorkspaceWithSession(workspace, executor);
},
create: async (data: CreateTaskAttemptBody): Promise<Workspace> => {
const response = await makeRequest(`/api/task-attempts`, {
method: 'POST',
body: JSON.stringify(data),
});
return handleApiResponse<TaskAttempt>(response);
return handleApiResponse<Workspace>(response);
},
stop: async (attemptId: string): Promise<void> => {

View File

@@ -5,7 +5,7 @@ import { useParams } from 'react-router-dom';
import { AppWithStyleOverride } from '@/utils/StyleOverride';
import { WebviewContextMenu } from '@/vscode/ContextMenu';
import TaskAttemptPanel from '@/components/panels/TaskAttemptPanel';
import { useTaskAttempt } from '@/hooks/useTaskAttempt';
import { useTaskAttemptWithSession } from '@/hooks/useTaskAttempt';
import { useProjectTasks } from '@/hooks/useProjectTasks';
import { ExecutionProcessesProvider } from '@/contexts/ExecutionProcessesContext';
import { ReviewProvider } from '@/contexts/ReviewProvider';
@@ -22,7 +22,7 @@ export function FullAttemptLogsPage() {
attemptId: string;
}>();
const { data: attempt } = useTaskAttempt(attemptId);
const { data: attempt } = useTaskAttemptWithSession(attemptId);
const { tasksById } = useProjectTasks(projectId);
const task = taskId ? (tasksById[taskId] ?? null) : null;

View File

@@ -6,7 +6,7 @@ import { Card, CardContent } from '@/components/ui/card';
import { AlertTriangle, Plus, X } from 'lucide-react';
import { Loader } from '@/components/ui/loader';
import { tasksApi } from '@/lib/api';
import type { TaskAttempt, RepoBranchStatus } from 'shared/types';
import type { RepoBranchStatus, Workspace } from 'shared/types';
import { openTaskForm } from '@/lib/openTaskForm';
import { FeatureShowcaseDialog } from '@/components/dialogs/global/FeatureShowcaseDialog';
import { showcases } from '@/config/showcases';
@@ -16,7 +16,7 @@ import { usePostHog } from 'posthog-js/react';
import { useSearch } from '@/contexts/SearchContext';
import { useProject } from '@/contexts/ProjectContext';
import { useTaskAttempts } from '@/hooks/useTaskAttempts';
import { useTaskAttempt } from '@/hooks/useTaskAttempt';
import { useTaskAttemptWithSession } from '@/hooks/useTaskAttempt';
import { useMediaQuery } from '@/hooks/useMediaQuery';
import { useBranchStatus, useAttemptExecution } from '@/hooks';
import { paths } from '@/lib/paths';
@@ -103,7 +103,7 @@ function DiffsPanelContainer({
selectedTask,
branchStatus,
}: {
attempt: TaskAttempt | null;
attempt: Workspace | null;
selectedTask: TaskWithAttemptStatus | null;
branchStatus: RepoBranchStatus[] | null;
}) {
@@ -282,7 +282,7 @@ export function ProjectTasks() {
const effectiveAttemptId = attemptId === 'latest' ? undefined : attemptId;
const isTaskView = !!taskId && !effectiveAttemptId;
const { data: attempt } = useTaskAttempt(effectiveAttemptId);
const { data: attempt } = useTaskAttemptWithSession(effectiveAttemptId);
const { data: branchStatus } = useBranchStatus(attempt?.id);
@@ -761,7 +761,7 @@ export function ProjectTasks() {
title: task.title,
description: task.description,
status: newStatus,
parent_task_attempt: task.parent_task_attempt,
parent_workspace_id: task.parent_workspace_id,
image_ids: null,
});
} catch (err) {

View File

@@ -0,0 +1,22 @@
import type { Workspace } from 'shared/types';
/**
* WorkspaceWithSession includes executor from the latest Session.
* Only used by components that actually need the executor field.
*/
export type WorkspaceWithSession = Workspace & {
executor: string;
};
/**
* Create a WorkspaceWithSession from a Workspace and executor string.
*/
export function createWorkspaceWithSession(
workspace: Workspace,
executor: string
): WorkspaceWithSession {
return {
...workspace,
executor,
};
}

View File

@@ -1,11 +1,11 @@
import { TaskAttempt, TaskWithAttemptStatus } from 'shared/types';
import { TaskWithAttemptStatus, Workspace } from 'shared/types';
import type { SharedTaskRecord } from '@/hooks/useProjectTasks';
// Extend nice-modal-react to provide type safety for modal arguments
declare module '@ebay/nice-modal-react' {
interface ModalArgs {
'create-pr': {
attempt: TaskAttempt;
attempt: Workspace;
task: TaskWithAttemptStatus;
projectId: string;
};

View File

@@ -1,4 +1,4 @@
import type { TaskAttempt, TaskWithAttemptStatus } from 'shared/types';
import type { TaskWithAttemptStatus, Workspace } from 'shared/types';
import type {
ConfirmDialogProps,
DeleteTaskConfirmationDialogProps,
@@ -14,7 +14,7 @@ declare module '@ebay/nice-modal-react' {
interface ModalArgs {
// Existing modals
'create-pr': {
attempt: TaskAttempt;
attempt: Workspace;
task: TaskWithAttemptStatus;
projectId: string;
};