* feat: add workspace sources from files panel * fix: preserve worktree identity during recovery * feat: unify workspace source actions * fix: validate recovered workspace repositories * fix: harden workspace source filesystem boundaries * test: split workspace repository chip coverage * fix: restrict repository rescan recovery paths * fix: stabilize workspace source CI checks * test: canonicalize workspace source paths * test: align workspace source container paths * fix: populate direct workspace repository names * test: preserve git fixture rewrites across restarts * test: isolate backend git fixture config * fix: resume remote workspace executions before creation * fix: preserve ssh agent identity during resume * fix: preserve SSH workspace across backend restart * fix: simplify workspace actions trigger * fix: refine add workspace repositories UX * docs: narrow workspace source UX updates * fix: avoid phantom turn on session resume * test(web): normalize workspace source request headers * feat(web): streamline workspace source picker * docs: remove trailing spaces from workspace source plan * fix: prevent resume status from creating phantom turns * fix(web): stabilize workspace source attachment * refactor(web): extract workspace source store contract * fix: avoid phantom turns on idle agent resume * fix: address backend static analysis findings * fix: preserve agentctl readiness status value * fix: preserve resume messages without active turns * fix: resume agents from promoted workspace root * chore: merge origin/main into feature/add-new-repo-folder-d97 * feat(web): explain workspace source consequences * test(websocket): count session activity subscription
18 KiB
spec, created, status
| spec | created | status |
|---|---|---|
| docs/specs/tasks/attach-workspace-sources.md | 2026-07-22 | completed |
Implementation Plan: Attach Workspace Sources
Overview
Add a durable task-level source attachment contract above the current worktree-only branch flow,
then teach host and remote runtimes to materialize the same source set. Reuse the task-create
repository controls inside a dedicated responsive Files-panel picker, publish live source/root
updates through the existing task/session data flow, and finish with desktop, mobile, Docker, and
SSH coverage. The plan preserves task_repositories for Git behavior and introduces a separate
folder attachment relation, as required by
ADR-2026-07-22-runtime-mutable-task-workspace-sources.
The active UX delta extracts the task-create source-mode presentation into a controlled shared
selector, presents Local and Remote modes inside Add sources, and combines Add sources
with Open workspace folder in one Files-toolbar action menu without changing the durable source
union or materialization contract.
Backend
Persistence and source projection
- Add
TaskWorkspaceFoldertoapps/backend/internal/task/models/models.gowith task ownership, canonical host path, collision-safe display name, global source position, and timestamps. - Add
task_workspace_foldersto both the base schema and replayable migrations inapps/backend/internal/task/repository/sqlite/base_schema.goandbase_migrations.go; enforce task/path and task/display-name uniqueness, cascade deletion, and task/position lookup indexes. - Extend
apps/backend/internal/task/repository/interface.goand add a focused SQLite repository file for folder CRUD plus a transactional batch that creates repository/folder attachments and a compensating batch that removes only rows created by the failed operation. - Extend
Task,TaskDTO,pkg/api/v1.Task, boot/snapshot queries, andtask.updatedserialization withworkspace_folders. Build a combined ordered source projection without changing existing repository DTOs or Git consumers. - Allocate positions under the existing per-task mutation lock using the maximum across repository and folder rows. Reject runtime-name collisions across both tables after applying the same directory-name sanitizer used for multi-repository worktrees.
Attachment service
- Add
apps/backend/internal/task/service/service_workspace_sources.gowithAttachWorkspaceSources(ctx, request)and typed repository/folder input/result shapes. - Validate the whole batch before writes: task exists and is repository-backed; workspace ownership; exactly one repository locator; provider-neutral URL and identity consistency; canonical explicit local Git paths; canonical arbitrary directories; branches; duplicates; display-name collisions; and idle session/turn state.
- Resolve or create repository entities through the existing
ResolveRepositoryRefpath so local trust and remote credential rules stay authoritative. Track newly created repository entities for rollback without deleting pre-existing workspace repositories. - Persist the batch, call the runtime materializer synchronously, and compensate database plus owned
filesystem/runtime entries on failure with a detached bounded context. Publish
task.updatedonly after both persistence and materialization succeed. - Refactor
AddBranchToTaskinto a compatibility adapter over a one-item repository-source request; retain its existing auto-branch naming, duplicate, and slug-collision behavior.
HTTP, WebSocket, and MCP contracts
- Register
POST /api/v1/tasks/:id/workspace-sourcesinapps/backend/internal/task/handlers/task_handlers.go; define the discriminated source request intask_http_handlers.go, map typed service errors to400/404/409/422, and return the durable source projection, effective workspace path, and affected sessions. - Add
workspace.sources.updated/session.workspace_sources_updatedevent constants and gateway routing ininternal/events/types.go,pkg/websocket/actions.go, andinternal/gateway/websocket/task_notifications.go. Emit only after agentctl has adopted the root. - Register
add_workspace_sources_kandevininternal/mcp/server/server.goand its backend handler ininternal/mcp/handlers/handlers.go; defaulttask_idto the current task and accept the same batch union as HTTP. Keepadd_branch_to_task_kandevexternally compatible while routing it through the generalized service.
Host materialization
- Replace the narrow
BranchMaterializerhook ininternal/task/service/service.gowith aWorkspaceSourceMaterializerbatch interface, retaining a thin compatibility adapter for tests and existing call sites. - Refactor
internal/backendapp/branch_materializer.gointo host source materialization that reusesworktree.Managerfor repository branches and creates a Kandev-owned task root for mixed local sources. Link live local sources into that root with platform-native directory links (Unix symlinks, Windows directory junctions), while validating the canonical target before and after link creation. - Never copy, move, delete, or write marker files into user-owned local sources. Rollback and task cleanup remove only Kandev-owned worktrees, links, and task roots.
- Extend lifecycle workspace rebinding so an idle Local execution can restart/rebind at the mixed
source root without losing its task conversation; Worktree executions keep their existing live
sibling materialization path. Persist the promoted
task_environments.workspace_pathfor future sessions and resets. - Generalize agentctl rescan from repository-only reconciliation to workspace-source reconciliation: update the workdir, rebuild repository trackers for Git entries, refresh file-tree scope for plain folders, and fail the attachment if the running agentctl cannot adopt the new root.
Container and remote materialization
- Add authenticated agentctl endpoints and client methods for validated repository clone/checkout materialization under the current workspace root.
- Route local Docker, SSH, and Sprites attachments through their live
AgentExecutionagentctl client. Reuse existing executor credential resolution for repository clones; do not persist or log tokens or credentialed URLs. - Reject folder inputs for Docker, SSH, Sprites, and remote Docker during service capability validation, before persistence or executor filesystem access. The frontend uses the same executor capability to omit the folder source kind.
- Make subsequent launch/reset preparation derive every repository from the durable source projection. Remove the task-create frontend guard that forces multi-repository work to Worktree only after the corresponding runtime launch paths support repository projection.
- Add the same materializer capability to the remote-Docker interface and tests; the executor remains
unavailable until its existing
CreateInstanceimplementation is completed.
Frontend
Shared source picker
- Extract reusable repository selection leaves from
task-create-dialog-workspace-repo-chips.tsx,task-create-dialog-remote-repo-chip.tsx,task-create-dialog-remote-repo-chips.tsx, andfolder-picker.tsxintocomponents/workspace-source-picker/without changing the task-create dialog behavior. - Keep
task-create-dialog-source-mode.tsxscoped to task creation. Add sources does not reuse its segmented mode switch: it presents one Add repository menu whose choices append workspace, local-Git, or remote rows without changing the visibility of configured rows. - Add typed mixed rows (
repository,remote_repository,folder) and auseWorkspaceSourcePickerStatehook. Workspace repository reuses the combined saved/discovered repository selector; Local Git repository exposes the local path affordance; Remote repository reuses provider-backed and pasted-URL controls. Add folder remains adjacent when supported. Appending rows never removes configured rows, so one submission can mix source kinds on Local/Worktree. Executor capability removes the folder affordance on container/remote tasks. - Decouple reusable remote-row presentation from create-task-only
DialogFormState; keep branch, provider metadata, discovery, validation, and payload conversion owned by the consuming dialog. - Add
attachTaskWorkspaceSourcestolib/api/domains/kanban-api.ts, source types tolib/types/http.tsand the kanban slice, and unit-tested payload/error normalization. - Once backend runtime support lands, delete the non-Worktree multi-repository disable resolver and
auto-switch effect in
task-create-dialog-computed.tsandtask-create-dialog-multi-repo-guard.ts; keep the no-repository Worktree restriction.
Files panel surface and live state
- Replace the separate Add sources and Open workspace folder toolbar controls with one
labeled/icon Workspace actions trigger in
file-browser-toolbar.tsx, threaded throughfile-browser.tsx,files-panel.tsx,task-files-panel.tsx, and their hooks. Its dropdown menu contains both actions. Keep the trigger enabled whenever opening the workspace folder is valid; disable only the Add sources row for archived/non-repository tasks, active turns/tool calls, or loading source state, and render the reason as visible menu copy. - Build
components/task/add-workspace-sources/around the shared picker. Desktop uses a Dialog; phone uses a full-height Drawer with fixed header/footer,min-h-0internal scrolling,100dvhsizing, safe-area bottom padding, and a single submit action. Preserve keyboard focus, Enter behavior, dismissal, pending state, per-source errors, retryable batch contents, and rows configured from every repository or folder kind. - Coordinate Radix menu close autofocus before opening the controlled Dialog/Drawer so dismissal
returns focus to the workspace-actions trigger. On phones, use the existing responsive
DropdownMenubottom-sheet treatment rather than introducing a second action-menu drawer. - Subscribe to
task.updatedfor repository/folder state andsession.workspace_sources_updatedfor the adopted workspace root. Update the kanban/session slices, reset the file-tree cache for affected environment/session IDs, and let existing repository/worktree handlers refresh Changes and editor pickers. - Keep the new phone entry point visible and touch-usable (at least 44px active dimension) without
hiding any source kind supported by the task's executor. The closest shipped mobile exemplar is
components/task/mobile/mobile-picker-sheet.tsx; reuse its focused picker hierarchy while using a full-height surface because mixed rows, branch search, validation, and batch errors can be deep.
Mobile design contract
- Desktop outcome: one compact labeled/icon Files-toolbar trigger opens a dropdown with Add sources and Open workspace folder. Selecting Add sources opens the multi-row dialog and refreshes the workspace tree after one atomic submission.
- Mobile entry point: the same workspace-actions trigger is visible in the Files toolbar with an actual 44px touch target. The shared responsive DropdownMenu becomes a short inset bottom-sheet action menu; it does not depend on hover, context click, or desktop Dockview chrome.
- Hierarchy and primary action: the action menu chooses between workspace actions. The Add sources surface then exposes one Add repository control and, when supported, Add folder. Repository location is a temporary menu choice; configured rows and validation own the single scroll body. Cancel and Add to workspace stay fixed in the footer, with submit as the sole primary action and disabled until a row exists.
- Presentation rationale: the two workspace actions are a short temporary choice, so they fit the shared inset menu. Source attachment can contain several searchable, branch-aware rows, so it continues into a full-height Drawer rather than stacking the full form inside that menu.
- Shared versus responsive: source state, validation, payload construction, picker leaves, and API calls are shared. Add sources owns its tab-free source-kind menu. Only Dialog/Drawer composition, responsive menu treatment, and toolbar hitbox treatment differ.
- Geometry: one internal vertical scroll owner, dynamic viewport height, safe-area clearance, 44px phone triggers/menu rows, contained repository popovers/drawers, and no document horizontal overflow.
Tests
- What: folder persistence, canonical uniqueness, cross-table display-name/position allocation,
cascade deletion, and SQLite/Postgres replay safety.
File:
internal/task/repository/*workspace_source*_test.goandinternal/task/repository/sqlite/*workspace_folder*_test.go. How: real database tests, including migration replay. - What: valid mixed batches, repository resolution, idle gate, duplicate/cross-workspace/path
failures, partial materialization rollback, client cancellation, and legacy add-branch parity.
File:
internal/task/service/service_workspace_sources_test.goand existing branch tests. How: table-driven service tests with real SQLite plus fake materializer failure injection. - What: HTTP status mapping, MCP current-task defaulting and batch forwarding, event truthfulness, and compatibility tool behavior. File: task handler, MCP handler/server, gateway, and service event tests beside the source. How: HTTP/WS integration tests and MCP captured-payload tests.
- What: host root promotion, link safety, Windows/Unix path behavior, idle restart/rebind,
agentctl source rescan, rollback, and relaunch reconstruction.
File:
internal/backendapp/workspace_source_materializer_test.go, lifecycle tests, and agentctl process/API tests. How: temporary Git repositories/folders plus fake lifecycle clients; platform-specific tests where link semantics differ. - What: Docker/SSH/Sprites repository clone transport, unsupported-folder rejection, credential redaction, cancellation, cleanup, and reset reconstruction. File: lifecycle executor tests and agentctl workspace-source API tests. How: table-driven unit/integration tests with fake agentctl plus existing container harnesses.
- What: mixed picker state, payload construction, duplicate errors, disabled busy state,
task/session event merges, and file-tree cache reset.
File: focused
*.test.ts(x)files beside the new picker, API helper, toolbar, and WS handlers. How: Vitest and Testing Library. - What: task creation keeps its existing mode presentation while Add sources renders no
Local/Remote tabs, appends mixed rows from one repository menu, and provides 44px mobile choices.
File:
task-create-dialog-repo-chips.test.tsx,add-workspace-sources-dialog.test.tsx, and a focused shared-mode component test. How: Testing Library controlled-state and responsive-geometry assertions. - What: one workspace-actions trigger exposes both actions, keeps Open workspace folder enabled
when Add sources is disabled, shows the disabled reason, and hands focus to/from the source
surface.
File: a focused test beside
file-browser-toolbar.tsx. How: Testing Library menu interaction and callback assertions.
E2E Tests
- Scenario: GIVEN an idle one-repository worktree task, WHEN a desktop user adds a second local
repository and a plain folder through Workspace actions → Add sources, THEN both
top-level entries appear and only the repository appears in Changes.
File:
apps/web/e2e/tests/task/add-workspace-sources.spec.ts. What to verify: both menu actions, open-folder request routing, the tab-free repository-kind menu, mixed-row preservation, dialog flow, atomic success, Files refresh, repository-aware UI, and persistence after reload. - Scenario: GIVEN the Files tab on a Pixel 5 viewport, WHEN the user opens the full-height source
picker through the workspace-actions bottom-sheet menu and attaches two source kinds through the
responsive repository menu, THEN the same task value is delivered without horizontal overflow.
File:
apps/web/e2e/tests/task/mobile-add-workspace-sources.spec.ts. What to verify: 44px trigger/menu rows, both actions, open-folder request routing, internal scroll, fixed safe-area footer, submit outcome, dismissal/focus return, and document containment. - Scenario: GIVEN a Docker task, WHEN a remote repository is attached, THEN it is readable inside
the running container; the folder source kind is absent and a forged folder request is rejected.
File:
apps/web/e2e/tests/docker/add-workspace-sources.spec.ts. What to verify: live agentctl repository materialization, capability gating, rollback, and reset/relaunch. - Scenario: GIVEN an SSH task, WHEN a repository source is attached, THEN it appears in the
remote task directory and survives backend reconnect.
File:
apps/web/e2e/tests/ssh/add-workspace-sources.spec.ts. What to verify: remote placement, rescan, persistence, and cleanup ownership.
Implementation Waves
Wave 1:
Wave 2:
Wave 3 (parallel):
Wave 4 (parallel after dependencies):
Wave 5 (parallel):
Wave 6:
Wave 7 (parallel UX delta):
Wave 8 (parallel after Tasks 11 and 12):