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:
@@ -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) {
|
||||
|
||||
@@ -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;
|
||||
};
|
||||
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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,
|
||||
},
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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);
|
||||
},
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -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
|
||||
),
|
||||
});
|
||||
}
|
||||
|
||||
@@ -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> => {
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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) {
|
||||
|
||||
22
frontend/src/types/attempt.ts
Normal file
22
frontend/src/types/attempt.ts
Normal 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,
|
||||
};
|
||||
}
|
||||
4
frontend/src/types/modal-args.d.ts
vendored
4
frontend/src/types/modal-args.d.ts
vendored
@@ -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;
|
||||
};
|
||||
|
||||
@@ -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;
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user