d2ab3cd5095d4ee61ec0065d7ecdeec0a00c308e
151 Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
79633ea376 | fix(web): preserve repo metadata in kanban.update and check legacy repositoryId (#1258) | ||
|
|
6b98185ac3 | fix(web): restore debug UI on make start-debug (#1252) | ||
|
|
a1143aa525 |
fix(acp): handle claude-acp async_launched subagents end-to-end (#1244)
* fix(acp): handle claude-acp async_launched subagents end-to-end Claude Agent SDK's Task tool with run_in_background=true returns session/prompt with stop_reason=end_turn while subagents stay alive in the background, reporting `_meta.claudeCode.toolResponse.status = "async_launched"` instead of a terminal top-level status. kandev previously treated stop_reason as authoritative, cancelled the subagent card on prompt return, and fired an "agent finished without producing any output" notice — even though the dispatch did produce work and the subagent's child events would arrive seconds later. Backend: - `SubagentTaskPayload` gains `IsAsync` / `OutputFile` / `CanReadOutputFile` so the async-launched envelope is preserved. - `convertToolCallResultUpdate` recognizes async_launched as terminal for the Task tool itself (dispatch is the completion). - `cancelActiveToolCalls` preserves subagent_task tool calls through the prompt-return sweep so a late terminal tool_call_update from the SDK still lands correctly (covers the in-turn-complete case). Frontend: - `subagentMetaChips` emits a "background" chip when `is_async` or `status == "async_launched"` so the card is visually distinct. - `computeEmptyTurnNotice` suppresses the notice when any subagent task message exists for the turn (the dispatch IS output). Reproduced via `acpdbg prompt` against `claude-acp` 0.31.1; captured frames in acp-debug/claude-acp-prompt-20260602-110119.jsonl drive the backend test fixtures verbatim. * fix(acp): harden async_launched override against future SDK frames Address PR review feedback (greptile #1244, claude review): - Move the async_launched override inside the mutex lock so it can inspect the stored payload kind before flipping the status. - Drop the `status == ""` precondition. The earlier in_progress synthesis (Title/RawInput/Content/Locations) was implicitly trumping the override; a future SDK frame that combines async_launched with any of those fields would have left the card stuck in_progress forever. The override is now unconditional for async_launched subagent_task frames. - Gate the override on `payload.Kind() == ToolKindSubagentTask`. A hypothetical future claude-acp tool that reuses the `async_launched` literal in its own `_meta.claudeCode.toolResponse.status` would have been incorrectly marked complete by the prior unscoped check. Pinned by two new tests: - TestConvertToolCallResultUpdate_AsyncLaunched_OverridesInProgress - TestConvertToolCallResultUpdate_AsyncLaunched_NonSubagentNotTerminated --------- Co-authored-by: Kandev Agent <agent@kandev.dev> |
||
|
|
cb7b5429d5 |
fix(tasks): harden multi-branch add_branch and surface it in UI (#1239)
Three areas were broken in the MCP add_branch_to_task flow: 1. Predictable branch names. autoBranchName counted up from existing branch-N rows. Concurrent add_branch calls raced onto the same name and the worktree directory leaked ordering. Now uses a random branch-<3 random chars> suffix via worktree.SmallSuffix. 2. Sibling materialize did not flow to the UI. The materializer published AgentctlReady with the NEW sibling worktree's path, which the frontend handler wrote onto the session row — clobbering the primary's worktree_id/branch and pointing the file browser at the sibling instead of the task root. Added TaskWorkspacePath to the payload, detected the sibling case on the frontend (existing worktree_id != payload's), kept the primary's id/branch, repointed worktree_path to the task root, bumped commits refetch, and dropped the pre-multi-repo legacy git-status entry so the Changes panel stops surfacing the frozen single-repo snapshot. File browser's resetKey now includes the worktree count so the tree reloads when the sibling lands. 3. workspace_path promotion + agentctl rescan refused to act on historical envs. A few envs had workspace_path pointing at the source repo's local_path (orchestrator computeWorkspacePath fallback to req.RepositoryPath when resp.WorktreePath is empty) instead of the primary worktree. The materializer's promotion gate compared against env.WorkspacePath's parent, so those envs were never promoted, and agentctl's resolveRescanPath only accepted parent-of-current so the rescan to the real task root was rejected. Trackers stayed on the wrong directory and the sibling's commits never appeared in the Commits panel even after a full reload. Anchored promotion on env.TaskDirName (basename cross-check) and added a recovery branch to resolveRescanPath that accepts any absolute directory which actually contains git-repo subdirs — the bearer-token middleware already gates the endpoint and scanRepositorySubdirs validates the children, so the relaxed check stays safe. Co-authored-by: Kandev Agent <agent@kandev.dev> Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com> |
||
|
|
1fdeda904b |
feat: add voice mode for chat input (#1159)
* feat: add voice mode for chat input Adds a voice input button to the chat composer with three transcription engines: in-browser Web Speech API, in-browser Whisper via @huggingface/transformers, and a server-side OpenAI Whisper fallback gated on KANDEV_VOICE_OPENAI_API_KEY. User-configurable from a new /settings/voice-mode page: - Master on/off toggle (defaults to on; opt-out, not opt-in) - Engine preference (auto / Web Speech / Whisper Web / Whisper Server) - Language hint (auto-detect or BCP-47 pinned) - Activation mode (click-to-toggle or hold-to-talk) - Auto-send after transcription - Whisper Web model size (tiny / base / small) - Editable Cmd+Shift+M keyboard shortcut Voice settings live under user_settings.voice_mode as a nested JSON object — no schema migration. Setting changes broadcast via the existing user.settings.updated WS event so any open chat tab updates live. The mic button is rendered on the right of the composer alongside the send button (round primary fill; destructive fill with pulse ring while recording). On browsers without a usable engine (e.g. iOS Chrome over HTTP) the button stays visible as a muted greyed icon — tapping it surfaces a toast with the reason (typically a missing secure context). Whisper Web runs in a Web Worker with fp16 decoder weights (avoids the MatMulNBits scale-missing crash that q4/q8 hit on WASM). Models are pulled from the onnx-community/* mirror and cached by transformers.js in the Cache Storage on first use. * fix: address voice mode PR review feedback and E2E selector clash Bot-review fixes from #1159: - Map http.MaxBytesError from c.FormFile to 413 instead of bare 400. - Strip BCP-47 region suffix before passing language hint to Whisper Web. - Recover from worker crash: terminate + null worker + reset ready flag so next init/transcribe creates a fresh worker instead of hanging. - Read live store state via storeApi.getState() in voice-mode settings savers to avoid stale-closure clobbering concurrent settings updates. - Detach Web Speech callbacks before abort() so trailing onend/onerror events don't mutate hook state after cancel(). - Spread caller init before method/body in voice-api fetch so a caller- supplied init can't override the multipart POST. - Replace t.Fatalf with t.Errorf+return inside test HTTP handler goroutine (FailNow from non-test goroutine is unsafe). - Add Enabled/AutoSend zeroing assertions in partial-update test so the bool-overwrite-on-PATCH behavior is locked in explicitly. E2E fix: - toolbar-overflow spec used button.rounded-full which now matches both the send and voice buttons (strict-mode violation). Switched to the submit-message-button testid for an unambiguous locator. * fix: scope voice settings rollback + whisper worker error to changed keys Round 2 of PR review feedback on #1159: - voice-mode settings rollback now restores only the keys patched by the failing request (not the whole snapshot), preserving concurrent edits to unrelated voiceMode fields. - Shortcut rollback diffs previous vs next and reverts only the changed keys (restore prior value or delete if absent), preserving other concurrent shortcut edits. - Whisper worker error handler captures the worker reference at attach time and only clears refs when this.worker still matches it — a stale error from a previously replaced worker no longer nukes the active one. - Update stale whisper-web-models JSDoc that still pointed at Xenova mirrors. * fix: voice mode progress scale, hold-mode race, race-aware rollback Round 3 of PR review feedback on #1159: - Normalise Whisper progress to 0–1 in ensureWhisperClient (transformers.js emits 0–100, the button's display + the `ready: 1` convention expect 0–1 — fixes the 5000% mid-download display). - Claim driverRef synchronously at the top of finishCapture so concurrent pointerup + pointerleave invocations early-return instead of racing and clobbering a freshly-started recording's ref. - Race-aware rollback: voice-mode and shortcut catch branches now skip reverting a key when latest[key] !== next[key]. A failed earlier request no longer overwrites a later successful save to the same key. - Update the Worker wire-protocol comment to show onnx-community/whisper-base instead of the stale Xenova/* example. --------- Co-authored-by: Kandev Agent <agent@kandev.dev> |
||
|
|
6734b64b54 | fix(e2e): ignore stale session state snapshots blocking idle input (#1208) | ||
|
|
d76836d740 | fix(web): stop changes panel flicker and restore planning chat streaming (#1197) | ||
|
|
dcde3da74c | feat: surface an inline notice when an agent turn produces no output (#1179) | ||
|
|
55ed674a15 | fix: dockview/editor restore races, office rebroadcast, and flaky tests (#1171) | ||
|
|
d288bd6a6d | feat: annotate debug logs with task_id for per-task filtering (#1168) | ||
|
|
b38dc21fb0 | fix: stop changes panel from showing stale or flickering content (#1128) | ||
|
|
68aefc0e08 | feat(web): system settings pages (status, database, backups, logs, updates, licenses, about) (#942) | ||
|
|
2c15cf83a9 | feat(web): add tree view for changes panel (#1026) | ||
|
|
e85c340797 | fix(web): honor window.__KANDEV_DEBUG so make start-debug logs surface (#991) | ||
|
|
862822dab1 | feat(web): add dev debug logging for git-status, dockview, and chat messages (#961) | ||
|
|
2d219f1d1b | fix(web): restore last-selected session tab on task re-entry (#951) | ||
|
|
f6bd83da33 | refactor: tighten type system across backend + frontend (#920) | ||
|
|
0b2085688b | feat(office): autonomous agent management layer (#914) | ||
|
|
0743477d48 |
feat: persist N-entry FIFO message queue (#864)
* feat(orchestrator): persist N-entry FIFO message queue
Replaces the single-slot, latest-wins, in-memory queue with a SQLite-backed
FIFO queue (default cap 10 per session, configurable via
KANDEV_QUEUE_MAX_PER_SESSION). Drain stays one-per-turn so each entry gets its
own user message and `on_turn_*` workflow cycle. Inter-task `message_task_kandev`
returns a structured `queue_full` error with sender attribution when the
target session is saturated, so the calling LLM can decide whether to retry.
Surfaces a new WS action `message.queue.remove` for single-entry deletion;
`message.queue.cancel` now means clear-all. Edit/remove are keyed on the
entry's UUID, returning `entry_not_found` when atomic-take won the drain race.
* feat(web): N-entry queue state, API, and optimistic hook
Replaces the single-slot QueueStatus with an ordered `entries[]` per session
plus capacity meta (`count`, `max`). API exposes typed `QueueFullError` and
`QueueEntryNotFoundError`; the hook surfaces optimistic edit/remove with
drain-race rollback (refetch on `entry_not_found`).
* feat(web): inline ghost-message queue UI with sender chips
Replaces the floating QueuedMessageIndicator overlay with a stack of ghost
messages rendered above the chat input. Each ghost shows a sender chip
(user / agent) and per-entry edit/remove. A small "n/max queued · clear all"
pill summarizes capacity. Inter-task entries are visually distinct and edit
is gated to the original sender.
* fix(queue): address review — auth guard, TOCTOU, error UX
- Add `messagequeue.QueuedByUser` / `QueuedByAgent` constants and centralize
the `queue_full` error code so the WS and MCP handlers stay in sync.
- WS `message.queue.{add,append,update}` default empty user_id to QueuedByUser
so the UpdateContent ownership guard always runs against a non-empty owner;
inter-task entries (`queued_by="agent"`) can no longer be overwritten by
user-initiated edits even when `user_id` is omitted from the payload.
- memoryRepository: hold the mutex through the whole append-or-insert path
(prior split released the lock between tail-check and Insert, allowing two
same-sender callers to race past coalescing). TransferSession now shifts
source positions past the destination's max so transferred entries always
drain after pre-existing destination entries, matching SQLite semantics.
- Frontend: surface edit/clear/remove failures via `sonner` toasts (drain race
→ "message already sent"); exit edit mode on failure instead of leaving the
textarea silently stuck. Hide the remove button on read-only (agent) ghosts.
- `useQueue.clearAll` no longer derives `max` from pre-clear entry count;
resets to `metaMax ?? 0` so the next status_changed event lands authoritative
capacity.
- `senderKindOf` drops the unreachable metadata fallback; route on `queued_by`
alone since `dispatchTaskMessage` always sets it.
- Test loggers now write to stderr per CLAUDE.md "stderr for logs" guideline.
* test(web): align queue e2e selectors with new ghost-message UI
The ghost-message rewrite renamed button titles ("Cancel queued message"
→ "Remove queued message", "Edit message" → "Edit queued message") and
introduced a `data-testid="queued-ghost-list"` container. The
`message-queue.spec.ts` Playwright tests still pointed at the old titles.
* fix(queue): address second review pass — auth, drain race, tests
- WS handlers (`add` / `append` / `update`) now reject `user_id="agent"`
outright, closing the gap a hostile WS client could exploit by
impersonating the inter-task sender identity to overwrite agent
entries (greptile P1).
- WS `update` / `remove` now require `session_id` so the post-mutation
`MessageQueueStatusChanged` broadcast can never be silently skipped.
- `sqliteRepository.TakeHead` checks `RowsAffected()` after DELETE;
when two concurrent take callers race past the busy-timeout the
loser sees 0 affected rows and now returns `nil, nil` instead of
the already-drained message (would otherwise dispatch the same
prompt twice).
- `Service.GetStatus` returns `Entries: []` instead of `nil` on the
error path so the wire shape stays consistent (frontend already
guards but strict consumers wouldn't tolerate the type mismatch).
- New tests: `queue-api.test.ts` covers `rethrowQueueError` mappings
(queue_full / entry_not_found / generic / Error pass-through);
`session-slice.upsert.test.ts` covers `setQueueEntries`,
`removeQueueEntry`, and `clearQueueStatus`. Backend gains an
agent-impersonation handler test.
- `rethrowQueueError` now surfaces `WSError.message` for non-queue
errors instead of stringifying the whole object.
- Cleanup: replace unused `a, c := ...` discards in service_test
with `_, _ = ...`.
* fix(queue): scope update/remove by session_id (cross-session deletion guard)
The MCP `queue_full` error payload deliberately discloses sibling-task
entry IDs (so the calling agent's LLM can reason about the wedge state).
Without a session_id filter on `DeleteByID` / `UpdateContent`, a hostile
agent that received that payload could call `wsRemoveEntry` /
`wsUpdateMessage` against another task's entries and delete or rewrite
them — only the per-entry UUID was being checked.
- `Repository.UpdateContent` and `Repository.DeleteByID` now require
`sessionID` and add `AND session_id = ?` to the SQL guard. The memory
repo mirrors the scope (lookup keyed on session before the entry scan).
- `Service.UpdateMessage` / `Service.RemoveEntry` take `sessionID`; the
WS handlers thread `req.SessionID` through (already validated as
non-empty in the previous fixup).
- TypeScript: `session_id` is now required on `updateQueuedMessage` /
`removeQueuedEntry` params so callers fail at compile time, matching
the backend contract.
- Tests cover the cross-session attack: `RemoveEntry`/`UpdateMessage`
with an attacker-supplied session_id but a real entry id from another
session must return `ErrEntryNotFound` and leave the victim entry
untouched. SQLite repo gets parallel coverage.
* fix(queue): propagate transfer error + tighten WSError narrowing
- `Service.TransferSession` now returns an error so callers can fail
closed instead of silently leaving entries orphaned on the previous
session. The two existing call sites pick the right policy:
`switchSessionForStep` (reusing an existing session) propagates the
error so the workflow switch aborts; the new-session path logs and
continues, since the new session is already committed and rolling
back here would leave a half-switched workflow.
- `asWSError` now also narrows on `message` / `details`, so WS payloads
without a `code` field are still mapped to a meaningful `Error.message`
via `rethrowQueueError` (instead of "[object Object]"). Native
`Error` instances explicitly skip the WS-error branch so they pass
through untouched, preserving the existing pass-through guarantee.
- `queue-api.test.ts`: `expect.assertions(N)` on the catch-only tests
so a missing throw fails the test instead of silently passing.
* fix(queue): check TransferSession error in service tests
The TransferSession signature change in
|
||
|
|
f97ac8bbc1 | feat: streaming agent install, PTY login terminal, docker container UX (#869) | ||
|
|
9664e13f07 | fix(web): keep "All Workflows" filter on task nav and workflow.created (#854) | ||
|
|
8c70aee27d | fix(workflows): hide improve-kandev system workflow from settings UI (#842) | ||
|
|
b1cd2d2076 | feat(github): rate-limit awareness, poller throttling, and GraphQL batching (#821) | ||
|
|
5e3248a66b |
feat(web): pin tasks and drag-to-reorder in sidebar (#829)
Co-authored-by: Kandev Agent <agent@kandev.dev> |
||
|
|
c64e835612 |
feat: allow moving tasks across workflows (#822)
* feat: allow moving tasks across workflows * fix: address PR review feedback * fix: address remaining PR review feedback * fix: remove unreachable move state event * fix: map task move conflicts correctly * fix: address PR review nitpicks * fix: use neutral task move error message |
||
|
|
c042050469 | fix: stabilize task switch flow and session recovery (#818) | ||
|
|
39b9369ae1 | fix: suppress spurious task-failed toasts on resume and reconnect (#814) | ||
|
|
b2dcd29818 |
fix(web): tab session bugs — primary star drift + tab close re-creation (#800)
* fix(web): preserve primarySessionId across kanban.update broadcasts The kanban.update WS handler rebuilt kanban.tasks without primarySessionId / primarySessionState, wiping values set by task.updated. After any board refresh the primary star vanished and selectTaskWithLayout fell into the slower async fallback path. Move task building inside setState so existing values are preserved in both single and multi snapshots. * fix(web): prevent deleted session panel from being re-created handleDelete called removeTaskSession before containerApi.removePanel, which let useAutoSessionTab re-run with effectiveSessionId still pointing at the deleted session and re-create its panel. The setupChatPanelSafetyNet had a matching gap — it re-added a panel for activeSessionId without checking the session still existed. Switch the active session (or clear it) before removeTaskSession so effectiveSessionId updates first. Add guards to useAutoSessionTab and the safety net so neither creates a panel for a session that is no longer in the store. * test(web): add session tab management regression suite Cover the three bugs fixed in this branch end-to-end: deleting a non-active session keeps it gone, deleting the active session switches focus to the remainder, deleting then immediately switching tasks does not resurrect the tab, and the primary star survives both a kanban.update broadcast (triggered via apiClient.moveTask) and a task-switch round trip. * fix(web): hand off deletion to newest remaining session, document multi-snapshot fallback - session-tab.tsx: sort remaining sessions by started_at DESC and pick the newest, instead of relying on the unspecified store ordering. - kanban.ts: comment the multi-snapshot primarySessionId fallback so the double lookup does not look like redundancy. Both per Claude review feedback on #800. * fix(web): preserve explicit-null primary, abort delete on WS failure - kanban.ts: switch the multi-snapshot fallback from `??` to an explicit `=== undefined` check. An explicitly-cleared null primary must NOT be replaced by a stale snapshot value, only an absent key. - session-tab.tsx: wsAction now returns a boolean; handleDelete bails out before mutating the store / dockview when the backend session.delete request fails. Prevents client/server desync from a transient WS failure. - kanban.test.ts: add regression covering the null-vs-undefined merge. Per CodeRabbit and Greptile review on #800. * style(web): drop redundant .slice() in session deletion sort chain filter() already returns a fresh array, so the subsequent sort() does not mutate shared store state. Per Greptile review on #800. |
||
|
|
1394b7103f |
feat: add recent task switcher (#779)
* feat: add recent task switcher * fix: address task switcher review feedback * test: cover recent task cleanup * perf: reduce recent task switcher storage reads |
||
|
|
a510b46e4e |
feat(multi-repo): support tasks spanning multiple repositories (#767)
* feat(task): add task_environment_repos schema and model for multi-repo support
Phase 0 of multi-repository task support: introduces the per-repository
child table and model alongside legacy single-repo fields (dual-write,
no behavior change). Lays the foundation for the orchestrator/lifecycle
refactor in subsequent phases.
- New task_environment_repos table with FK CASCADE on task_environments.
- New task_dir_name column on task_environments (idempotent ALTER).
- TaskEnvironmentRepo model + Repos slice + RepoFor helper on TaskEnvironment.
- CRUD methods + interface extension; env getters preload Repos.
- Idempotent backfill from legacy single-repo task_environments rows.
- Tests cover round-trip, CRUD, preload ordering, embedded create, cascade
delete, and backfill from a legacy-shaped row.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(lifecycle): add Repositories list to LaunchRequest and EnvPrepareRequest
Phase 1.1 of multi-repository task support: extends the lifecycle types
with explicit per-repo lists, while keeping the legacy single-repo
top-level fields as the synthesized fallback. No caller behavior changes
yet — preparers still see length-1 specs for single-repo launches.
- LaunchRequest.Repositories []RepoLaunchSpec + RepoSpecs() helper.
- EnvPrepareRequest.Repositories []RepoPrepareSpec + RepoSpecs() helper.
- EnvPrepareResult.Worktrees []RepoWorktreeResult for per-repo outcomes.
- runEnvironmentPreparer forwards req.Repositories into the prepare
request, inheriting the request-level RepoSetupScript when each
per-repo entry has none.
- Tests cover explicit-list precedence, synthesized fallback, and
repo-less requests.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(worktree,lifecycle): support multiple worktrees per session
Phases 1.2 and 1.3 of multi-repository task support. The pieces are
intertwined so they ship together — the preparer's per-repo loop
depends on the manager being able to hold one worktree per
(session, repository) pair instead of one per session.
Worktree manager:
- In-memory cache key changes from sessionID to (sessionID|repositoryID).
- New MultiRepoStore optional capability + SQLite implementation:
GetWorktreesBySessionID and GetWorktreeBySessionAndRepository.
- New Manager methods: GetAllBySessionID, GetBySessionAndRepo. Existing
GetBySessionID returns the first match (single-repo callers unchanged).
- Manager.Create's "already exists" check is now repo-scoped so the
second repo's create no longer returns the first repo's worktree.
- All cache write/delete sites updated to the composite key.
WorktreePreparer:
- New prepareMultiRepo path activated when EnvPrepareRequest.Repositories
has >=2 entries; single-repo and repo-less behavior is byte-identical.
- prepareOneRepo runs validate -> sync -> create -> fetch-PR -> per-repo
setup-script for each spec; rolls back already-created worktrees on
any failure.
- Workspace path becomes the task root (parent of any repo subdir);
legacy WorktreeID/WorktreeBranch/MainRepoGitDir mirror Worktrees[0]
for downstream code that hasn't been migrated yet.
- Per-repo setup script runs verbatim (no script-engine resolution
yet — the per-repo placeholder namespace lands in a follow-up).
Tests:
- Manager: GetAllBySessionID, GetBySessionAndRepo (hit + miss),
GetBySessionID single-repo compat, cache key separation.
- Preparer: multi-repo creates one worktree per repo; rollback removes
active worktrees on partial failure; per-repo setup script runs in
each worktree directory and emits its own step.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(orchestrator): resolve all task repos and persist per-repo rows on launch
Phase 1.4 of multi-repository task support: the orchestrator now reads
every repository linked to a task, forwards the full list through the
adapter into the lifecycle layer, and persists one TaskEnvironmentRepo
plus one TaskSessionWorktree row per repo when the launch is multi-repo.
Single-repo and repo-less tasks keep their previous code path verbatim
(dual-write — legacy single-repo fields stay populated from the primary).
- New resolveAllRepoInfo / resolveTaskRepoInfo on the executor; primary
is the first by Position.
- buildLaunchAgentRequest takes both the primary repoInfo and the full
list, populating req.Repositories []RepoSpec only when length > 1.
- New RepoSpec / RepoWorktreeResult on the orchestrator's executor
package mirror the lifecycle types so the adapter can translate
between them without leaking lifecycle types into the orchestrator
public API.
- Adapter (cmd/kandev/adapters.go) forwards req.Repositories into
lifecycle.LaunchRequest and surfaces per-repo prepare results back
on LaunchAgentResponse.Worktrees.
- persistTaskEnvironment writes env.Repos in the same create
transaction; persistTaskEnvironmentRepos handles the existing-env
reuse path. TaskEnvironment.TaskDirName is now persisted.
- persistWorktreeAssociation loops over resp.Worktrees in multi-repo,
inserting one row per repo with its own RepositoryID.
- ResumeSession now also populates req.Repositories from task repos
and stamps the existing TaskDirName so the preparer reuses the same
task root.
- repoStore / executorStore interfaces extended with the new repo and
env-repo CRUD methods; mocks updated accordingly.
Tests:
- LaunchPreparedSession with two task repositories populates
req.Repositories in order and keeps legacy single-repo top-level
fields filled from the primary.
- The same launch persists one TaskEnvironment + 2 TaskEnvironmentRepo
+ 2 TaskSessionWorktree rows.
- Single-repo launch still produces zero entries in req.Repositories
and exercises only the legacy fields.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(agentctl): git endpoints accept optional repo subpath for multi-repo
Phase 2 of multi-repository task support: every per-call git endpoint
gains an optional `repo` field (form param for GET, JSON for POST) that
selects a sub-directory of the workspace. Empty `repo` = workspace root,
preserving single-repo behavior verbatim.
- Manager.GitOperatorFor(subpath) returns a per-subpath GitOperator
(cached). Validates the subpath against path traversal, absolute
paths, and missing directories.
- New api server helper gitOpForRepo() emits a 400 with a per-operation
GitOperationResult on invalid subpaths.
- Repo field added to: pull, push, rebase, merge, abort, commit,
rename-branch, stage, unstage, discard, create-pr, revert-commit,
show-commit, reset, log, cumulative-diff. (git/status still reads
from the workspace tracker — that endpoint will gain multi-repo
support when the tracker itself is split per repo.)
- Tests cover empty subpath, valid subpath caching, traversal
rejection, and missing-directory rejection.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(web/comments): scope diff comments by repository for multi-repo tasks
Phase 3 of multi-repository task support: comments now carry an
optional repositoryId so two repos with the same file path render
their own comment threads instead of bleeding together.
- CommentBase.repositoryId (optional, backwards compatible).
- getCommentsForFile gains an optional repositoryId arg; legacy
comments without a repositoryId match any repo filter so persisted
data keeps showing.
- Diff store interface in lib/diff/types.ts mirrors the new arg.
- Tests cover per-repo filter, omitted-arg fallback, and legacy data.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(github): per-repo PR association on task PRs and watches
Phase 4 of multi-repository task support: github_task_prs and
github_pr_watches gain a repository_id column so multi-repo tasks can
have one PR per repo without colliding on (task_id) uniqueness.
- Idempotent ALTER TABLE adds repository_id to both tables.
- TaskPR.RepositoryID and PRWatch.RepositoryID model fields.
- New GetTaskPRByRepository and ListTaskPRsByTask lookups; existing
GetTaskPR keeps single-PR semantics for callers not yet migrated.
- ReplaceTaskPR's delete is scoped by repository_id when set so a
multi-repo task replacing one repo's PR no longer wipes the other.
- Tests cover per-repo storage and scoped replace.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(web): foundations for multi-repo task UI
Phase 5 of multi-repository task support: primitives the upcoming UI
work needs without changing any visible behavior yet.
- TaskRepository.checkout_branch surfaces the optional PR head branch
field that the backend already exposes.
- primaryTaskRepository(repos) picks by lowest Position so consumers
match the backend's "primary" semantics instead of `[0]`.
- New useTaskRepositories(taskId) / useTaskPrimaryRepository(taskId)
hooks read the live kanban task and return its repos sorted by
position. Future kanban/diff/UI consumers should use these instead
of poking task.repositories directly.
- Tests cover lowest-position selection and empty/single-repo edge cases.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(web): kanban card shows multi-repo "+N" chip and hydrates full repo list
Phase 6 (subset) of multi-repository task support. The kanban now
exposes the per-task repository list to the UI and renders a compact
"+N" badge on cards whose tasks span more than one repository.
- KanbanState.tasks[].repositories slim shape (id, repository_id,
base_branch, checkout_branch, position).
- SSR mapper hydrates the full list and uses primaryTaskRepository()
to pick the position-aware primary instead of [0].
- useKanbanData passes repositories through to the card props.
- KanbanCard renders the primary repo name with a small "+N" chip
when len(repositories) > 1; tooltip names the count.
- useTaskRepositories / useTaskPrimaryRepository hooks return the
slim KanbanTaskRepository shape so multi-repo consumers share one
read path.
Larger UI surfaces (task-create dialog, diff view, file browser,
mobile sheets) still target the primary repo only — they will be
ported as the multi-repo task creation flow lands. No visible change
for single-repo tasks.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(task): reject duplicate repositories on multi-repo task create
Phase 8 of multi-repository task support: surface duplicate-repository
input as a clean validation error instead of letting the unique
(task_id, repository_id) constraint surface mid-loop with some rows
already inserted.
- createTaskRepositories tracks seen repository IDs and returns
"repository <id> listed more than once for the task" before any
CreateTaskRepository call.
- Tests cover the rejection path and confirm two distinct repos still
succeed with both rows persisted in Position order.
Repository discovery and per-repo cloning were already covered by
Phase 1.4 (resolveAllRepoInfo loops ensureRepoCloned) and the
existing repository_discovery service is repo-list aware by design.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(web/task-create): repeatable repository rows for multi-repo task creation
Users can now actually create a multi-repo task: a "+ Add repository"
button below the primary repo + branch selectors expands the form with
additional (repository, base-branch) rows. Each extra row picks from a
filtered repository list that excludes the primary and any other already-
chosen extras, so duplicates are impossible to enter.
- ExtraRepositoryRow type + form-state hook (extraRepositories, add /
remove / update / reset). Lives in its own file so the main state hook
stays under the file-length lint cap.
- ExtraRepositoryRows component renders the rows and the "+ Add" button;
per-row branches are loaded on demand via the existing
useRepositoryBranches hook.
- buildRepositoriesPayload accepts the extras and appends them after the
primary, dropping rows with empty repository_id silently so an
unfinished row does not block submit. The backend's duplicate-repo
validation (Phase 8) catches anything that slips through.
- DialogFormBody is split: CreateModeSelectors (selectors + extra rows)
and useDialogHandlers move to dedicated files, keeping touched files
under the lint thresholds.
- Tests cover the multi-repo payload (ordering, drop-empty, no-primary
recovery, single-repo unchanged) and the existing
store-multi-repo.test.ts gets named constants to satisfy the
no-duplicate-string rule.
Verified: pnpm exec tsc --noEmit clean, vitest 608/608 green,
eslint --max-warnings 0 clean.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(agentctl): per-repo workspace tracker for multi-repo git/status
The git/status endpoint now accepts an optional ?repo=<subpath> query
param that selects a sub-directory of the workspace, completing the
per-repo plumbing started in the multi-repo phase. Single-repo callers
keep the workspace-root behavior unchanged.
- Manager.GetWorkspaceTrackerFor(subpath) returns a per-subpath
WorkspaceTracker (lazy + cached). Validation reuses a new
resolveSubpath helper shared with GitOperatorFor, so both code paths
reject path traversal / absolute paths / missing dirs the same way.
- Per-subpath trackers are NOT started (no polling goroutines); the
multi-repo path uses them only for synchronous GetCurrentGitStatus
queries which fall through to getGitStatus when no cache is present.
This keeps per-repo cost cheap.
- handleGitStatus reads c.Query("repo") and routes through the new
resolver, returning a 400 with an error payload on invalid input.
- Tests cover empty subpath (root), valid subpath caching, traversal
rejection.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(web/review): group file tree by repository for multi-repo task changes
The review file tree now renders per-repo top-level nodes when a task's
changes span multiple repositories, so the same file path in two repos
no longer collapses into a single ambiguous entry.
- ReviewFile.repository_id and ReviewFile.repository_name (optional)
identify which repo each file belongs to. Single-repo callers leave
them unset and the tree falls back to the existing flat grouping.
- buildFileTree splits into buildMultiRepoTree (when 2+ distinct
repository_names are present) and buildFlatTree (the original logic).
Repo roots have isRepoRoot=true and a repositoryId for downstream
filters; their children are full path subtrees per repo.
- collapse() skips repo roots so a repo with a single changed file
still shows its repo header.
- ReviewFileTree renders repo roots with a stronger label, a top
border, and an "N files" count badge; data-testid="repo-root-node"
for E2E coverage. File-level rendering is unchanged.
- Tests cover the flat-tree fallback (no repo info, single repo),
multi-repo grouping, repo-root pinning, and same-filename-different-
repos isolation.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(web/task-top-bar): "+N" repo chip on multi-repo task header
The task page top-bar breadcrumb now shows a small "+N" chip next to the
primary repository name when the task spans more than one repository,
matching the kanban-card treatment introduced earlier. Hover surfaces the
exact count via tooltip ("frontend (+2 more repos)").
- resolveTaskProps populates repositoryCount from task.repositories.length.
- TaskTopBar threads repositoryCount through to the new
RepositoryBreadcrumb subcomponent.
- The chip carries data-testid="topbar-extra-repo-count" for E2E
selectors.
- TopBarLeft was nearing the function-length / complexity caps; the
breadcrumb extraction restores headroom for future additions.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(web/task-create): disable "Add repository" when no other repos available
When the user only has the primary repository in their workspace,
clicking "+ Add repository" used to insert a row whose dropdown was
empty — looking like a bug ("no other repos show up"). The button now
disables itself when there are no remaining workspace repositories to
choose from, with a tooltip explaining why ("All workspace repositories
are already added"). The previous "primary not selected" guard is also
surfaced via tooltip.
- ExtraRepositoryRows computes remainingCount = repos − used (primary
+ already-added extras) and gates the Add button on it.
- New computeAddButtonHint helper returns a per-state title string for
the disabled cases.
- Tests cover: dropdown shows the unused workspace repos when opened
(the click path the user reported wasn't actually broken — the gate
was just missing); Add button disabled when only the primary exists.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(agentctl): workspace tracker falls back to first repo subdir for multi-repo task roots
The "Changes" panel was empty for multi-repo tasks because the
agent's workspace path is the task root — a plain holder directory
that holds one git worktree per repository as siblings. The workspace
tracker tried to run git commands against that bare root and silently
failed, so no git status updates ever reached the UI.
The tracker now detects this at construction: when the configured
workDir isn't itself a git repo, it falls back to the first immediate
child directory that is. The user gets the primary repo's changes in
the panel; cross-repo aggregation is a follow-up.
- preferGitRepoChildIfRootIsBare scans os.ReadDir entries (skipping
hidden / non-directory entries) and returns the first that has a
valid git index. Logs an info line when the rewrite happens so it's
visible in the agentctl log.
- Repo-less tasks (e.g. quick chat) and single-repo tasks keep their
previous behavior verbatim — the fallback only fires when the root
itself fails resolveGitIndexPath.
- Tests cover: fallback when the workdir is a multi-repo holder,
no-fallback for a real git repo, no-fallback when no git child
exists.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(multi-repo): per-repo workspace tracking — Changes panel shows all repos
The agent's workspace tracker is now split per-repository for multi-repo
task roots. Each repo gets its own tracker that polls git independently
and stamps its repository name onto every emitted event. The Changes
panel aggregates files from every repo and groups them under per-repo
headers using the file-tree machinery from Phase 6.
Backend (agentctl):
- GitStatusUpdate / FileListUpdate gain a RepositoryName field. Empty
for single-repo workspaces (legacy clients see no change).
- WorkspaceTracker carries repositoryName and stamps it onto every
emitted status/file-list. Two constructors: NewWorkspaceTracker for
the legacy single-repo path (with the bare-root fallback) and
NewWorkspaceTrackerForRepo for explicit per-repo trackers.
- Manager scans direct child directories for git repos at construction.
When 2+ repos are found, builds one tracker per repo subdir on top of
the root tracker (which stays bound to the bare task root and emits
nothing useful). Single-repo workspaces are unchanged.
- Manager.SubscribeWorkspaceStream / UnsubscribeWorkspaceStream fan out
one client subscription across the root + all per-repo trackers via
AttachWorkspaceStreamSubscriber / DetachWorkspaceStreamSubscriber on
the tracker. The WS handler switched from the single-tracker subscribe
to the manager fan-out.
- Tests cover: per-repo trackers spawn for multi-repo task roots,
single-repo doesn't, both repos emit GitStatusUpdates tagged with
RepositoryName.
Frontend:
- GitStatusEntry / GitStatusData carry repository_name (optional).
- session-runtime gitStatus.byEnvironmentRepo new map keyed by env id
then repository name; setGitStatus routes per-repo updates into it
while still updating the legacy byEnvironmentId for single-repo
consumers. clearGitStatus drops both maps.
- migrateEnvKeyedData migrates the new per-repo map alongside the
existing keys when sessionId resolves to an environmentId.
- WS git-status handler forwards repository_name from event.status.
- New useSessionGitStatusByRepo hook returns sorted [{repo, status}]
pairs for the active session.
- task-changes-panel mergeReviewFiles iterates statusByRepo when
available, stamping repository_name onto every uncommitted file so
the Changes panel groups them under per-repo headers (using the
ReviewFile.repository_name plumbing from Phase 6).
- Tests: per-repo routing, no overwrite on sibling repo update,
legacy single-repo fallback, clear-on-cleanup.
Verified: pnpm exec vitest run 623/623, eslint --max-warnings 0 clean,
tsc --noEmit clean, make test (backend) clean, make lint 0 issues.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(web/changes-panel): per-repo group headers above each repo's files
The Changes panel now renders a sticky "frontend / N files" header above
the diffs from each repository so the user can visually distinguish
where each change lives. Single-repo tasks render unchanged (no header
shown when there's only one unnamed group).
- ReviewDiffList groups its file list by repository_name (preserving the
caller's order within each group), then renders one RepoGroupHeader
per group followed by the existing FileDiffSection list. Header gating
rule: show when len(groups) > 1 OR the single group has a non-empty
repository name.
- Group key uses "repo:path" so a file with the same path in two repos
no longer collides on React's reconciliation.
- groupFilesByRepository + RepoGroupHeader live in a sibling
review-diff-list-groups.tsx file to keep review-diff-list.tsx under
the 600-line lint cap.
- Tests cover: no header for plain (no repo_name) files, one header per
repo for 2+ groups, header shown even for a single named repo, file
paths land under the right group's section.
Verified: vitest 627/627, eslint clean, tsc clean.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(lifecycle): propagate RepositoryName through PublishGitStatus
The agentctl WorkspaceTracker stamps every per-repo GitStatusUpdate
with RepositoryName, but the lifecycle PublishGitStatus translation
into GitEventPayload was dropping that field. The orchestrator then
forwarded a status with empty repository_name, the frontend store
routed everything into the empty-key bucket, and the Changes panel's
multi-repo grouping never triggered — no header, no per-repo split.
- GitStatusData gains RepositoryName (omitempty so single-repo
payloads stay byte-identical on the wire).
- PublishGitStatus copies update.RepositoryName onto the payload.
- Test asserts the field survives the publish → subscribe round trip
via the in-memory event bus on the GitEvent.<sessionID> subject.
Restart the agentctl process for any running multi-repo task to pick
up the new field; new launches get it automatically.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(web/changes-panel): per-repo group headers in the dockview Changes tab
The Changes tab in the dockview panel (the Files / Changes (N) tab the
user actually interacts with — not the standalone Review dialog) was
still rendering a flat file list, so multi-repo task changes either
collided onto the legacy single-status (showing only one repo's files)
or rendered without grouping.
- FileInfo gains repository_name (optional, single-repo unchanged).
- useSessionGit aggregates files from useSessionGitStatusByRepo when
multi-repo data is present, stamping each FileInfo with its
repository_name. Falls back to the legacy single status for
single-repo workspaces. Aggregation extracted into
aggregateFilesAcrossRepos to keep useSessionGit under the 100-line
function lint cap.
- ChangedFile (changes-panel.tsx + changes-panel-timeline.tsx) carries
repositoryName through to the renderer.
- FileListSection delegates to a new FileListBody that groups rows by
repositoryName; renders a small per-repo subheader when 2+ repos OR
any single named repo is present, otherwise falls back to the
pre-multi-repo flat list (same DOM shape).
- Tests cover: no header for plain (no repo_name) files, headers
per repo for 2+ groups, single named repo still gets a label.
Restart the backend so the per-repo trackers spawn for in-flight
multi-repo tasks; new launches are unaffected.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(multi-repo): per-repo git ops, unified branch endpoint, chip-style task create
Bundles a string of multi-repo fixes and refactors that came up while
testing the feature end-to-end:
- workspace_path stored on multi-repo TaskEnvironment was one level too
high, so agentctl booted with the wrong WorkDir on cold start; fixed
in resolveTaskEnvWorkspacePath + GetWorkspaceInfoForSession.
- Stage/Unstage/Discard/Commit/Revert/Reset/Amend now thread `repo`
through agentctl client → WS handler → frontend hook → SessionGit
fan-out, so the right git repo is targeted (was running at the task
root, which isn't a git repo for multi-repo tasks).
- agentctl GitLog is multi-repo aware: when no `repo` is specified and
per-repo trackers exist, fans out one log per repo and merges sorted
newest-first with RepositoryName stamped on each commit.
- Per-repo grouping for files (Changes panel) and commits, with
collapsible group headers; commits section defaults to collapsed.
Algorithm extracted into one generic groupByRepositoryName helper
(was copy-pasted three times).
- Task creation dialog rewritten around a unified `fs.repositories[]`
list — no more "primary vs extras" split. Chip-style row with one
pill per repo + branch, "+" to add, x to remove, GitHub URL toggle
inline. Discovered (on-machine) repos appear in any chip's dropdown.
Removes ~6 fields and 3 helpers from the dialog state.
- Branch listing endpoints collapsed to one:
GET /workspaces/:id/branches?repository_id=X | path=Y
Frontend collapses to a single useBranches hook with a tagged-union
source. Drops two HTTP routes, two service methods, two API client
funcs, and one whole hook file.
Tests added across the chain: workspace_path resolver, sort-commits-
by-date, manager poll-mode propagation, RepoSubpaths, chip grouping
+ collapse + per-repo isLatest, group-by-repo helper, dialog payload
builder + chip wiring + discovered-repo flow.
* feat(multi-repo): unified per-repo git controls for all VCS operations
Changes panel and top-bar VCS button render per-repo Stage all, Commit,
Unstage all, Push, Pull, Rebase, Merge, and Create PR uniformly across
single-repo and multi-repo workspaces. Operations route to the correct
git repo subpath; fan-outs collect per-repo results and surface partial
success in the toast layer.
Backend:
- Stamp RepositoryName on Reset/BranchSwitch/FileChange notifications
and forward through orchestrator handlers
- Surface per-repo errors from handleGitLogMultiRepo via PerRepoErrors
Frontend:
- pull/push/rebase/merge/abort/commit fan out via PerRepoOperationResult
- Per-repo group headers always render with action buttons
- Top-bar dropdown lists per-repo Pull/Rebase/Merge entries
- pendingStageFiles keyed per-repo to prevent cross-repo clears
- useRepoDisplayName distinguishes loading from multi-repo
- Commit dialog file summary respects stageAll
- New helpers: changes-panel-helpers.ts, changes-panel-repo-groups.tsx,
use-repo-display-name.ts
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(multi-repo): plumb repo through file/commit/diff endpoints; per-repo PRs and review UI
- backend: thread `repo` subpath through pull/push/rebase/merge/abort, file
content/update, cumulative-diff, and commit-show endpoints; per-repo
fan-out for log + cumulative-diff with each repo's own merge-base; emit
full `repositories` array on task.updated WS payload
- github: store/list TaskPRs as []*TaskPR (per-repo); orchestrator/web store
rekey to `Record<taskId, TaskPR[]>`
- web: aggregate PR icon (count badge + worst-of color, multi-PR tooltip);
branch hover card renders one row per repo with each repo's task base
branch; review dialog merges per-repo file maps with composite keys and
propagates repository_name through diff/expand/revert; commit-detail
panel scopes to the owning repo
- kanban card: chip row of repo names (up to 2 + "+N" overflow, tooltip
lists all)
- task-create dialog: gate non-worktree executors to multi-repo only; move
fresh-branch toggle inline next to the repo chips (icon-only, with
title/aria-label)
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(multi-repo): scope PR watches/associations and review UI per repository
Schema: add repository_id to UNIQUE constraints on github_pr_watches and
github_task_prs (with idempotent copy-and-rename migration) so one task
spanning N repos can hold one watch/PR per repo without collisions.
Plumb repository_id through CreatePRWatch/EnsurePRWatch/AssociatePRWithTask
/AssociatePRByURL, fan TriggerPRSync across all per-repo watches, and
disambiguate status sync by (task, owner, repo, pr_number) instead of
"first match". Add Repo to wsCreateFile/Delete/Rename and wsCreatePR/
RenameBranch so file and git ops target the right working tree.
Frontend review dialog: switch reviewedFiles, staleFiles, fileRefs,
selectedFile and comment counts to NUL-separated `${repo}\x00${path}`
keys so same-named files in different repos no longer share state.
useTaskPR consumes the new {prs: TaskPR[]} envelope.
Also: snapshot repoTrackers slice before background refresh in
SetWorkspacePollMode to prevent races with Stop(), and key
mergeCumulativeFiles by NUL-separated (repo, path) to match.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* chore: apply prettier formatting
Unblock CI prettier check on files modified earlier on this branch
that pre-date the current prettier config.
* fix: address PR review feedback and unblock E2E
Backend:
- ListWorkspaceTaskPRs background sweep now uses TriggerPRSyncAll so
multi-repo tasks refresh every per-repo PR (was only the most recently
updated watch). Caught by claude review on #767.
Frontend (no behaviour change):
- Replace literal NUL byte (0x00) in FILE_KEY_SEP and the inline key
builder in use-review-dialog with the explicit escape so the
source is plain ASCII. The runtime separator is unchanged; this just
removes a footgun that GitHub's diff renders as a visible space.
- Show the auto-resolved branch as a small read-only pill next to the
GitHub URL input so the URL-mode dialog surfaces the branch the task
will run on (previously only the URL was visible).
E2E:
- changes-panel-focus / pr-detection / pr-switcher-changes: expand the
commits-section before asserting on commit text -- the section starts
collapsed since the chip-style refactor.
- repository-selector-scroll: open the popover via repo-chip-trigger
instead of the legacy single repository-selector testid.
* test(e2e): align task-create dialog tests with chip refactor
Updates three more tests that broke when the dialog migrated from a
single repository/branch combobox to per-row repo/branch chips and the
URL-mode toggle relabel:
- create-task.spec.ts (pre-select + remembers-selections): switch
repository-selector / branch-selector lookups to repo-chip-trigger /
branch-chip-trigger.
- create-task-github-url.spec.ts (toggle test): the inverse-state label
is now "use a workspace repository", not the legacy
"or select a repository".
No code changes — purely test alignment with the existing UI.
* fix(web): re-apply workbench-chrome topbar + restore branch-selector behaviour
Two regressions surfaced by the multi-repo merge into main:
1. task-top-bar.tsx had been resolved in our favour at the workbench-chrome
merge (#761), losing the TopbarActionOverflow wiring that the new test
asserts on. Re-applies main's topbar structure (TopbarCluster groups,
TopbarActionOverflow, IntegrationsMenu, MoreToolsMenu, SettingsButton)
on top of our multi-repo additions: repositoryCount + RepositoryBreadcrumb
"+N" chip, gitStatus + isMultiRepo plumbing, and the GitAheadBehindBadges
suppression for multi-repo. GitAheadBehindBadges moved to its own file to
stay under the file-length cap.
2. The chip-style task-create dialog dropped the legacy branch-selector
semantics. Restored:
- Local-executor branch lock: the per-row branch chip is now disabled
when isLocalExecutor and fresh-branch is off. The user's checkout
dictates the branch for local execution; fresh-branch unlocks it.
- Branch override: when locked, the chip shows currentLocalBranch
instead of the value the user picked under a different executor
(so worktree -> local doesn't display a stale "develop" pick).
- Branch refresh: useBranches now exposes a refresh() callback for
id-based sources (calls listRepositoryBranches with refresh=true),
and Pill renders BranchRefreshButton in its popover header when
onRefresh is wired.
- URL-mode branch picker: GitHubUrlSection now renders a real
branch-chip-trigger Pill backed by fs.githubBranches instead of a
read-only span, restoring the "branch selectable in URL mode"
behaviour (URL mode is always remote-clone, so the local-executor
lock does not apply).
E2E spec updated: bulk-rename legacy branch-selector / repository-selector
testids to branch-chip-trigger / repo-chip-trigger.first(). All 13
create-task-branch-selector tests pass locally.
* ci: force workflow re-run
* fix(web): unblock E2E shard 2
Three issues showed up only after the merge with main, which brought back
the E2E shard 2 tests touching git/changes-panel and improve-kandev:
1. Wire the TaskCreateDialog slot props (extraFormSlot, aboveDescriptionSlot,
bottomSlot, descriptionPlaceholder) through to DialogPromptSection. They
were declared on the public API but dropped on the floor before this
change, so Improve Kandev's contributor banner ("@octocat" /
"@kandev-maint") never rendered. Also gate the "or paste a GitHub URL"
toggle on lockedFields.repository so the locked-repo dialog hides it
(matches main's behaviour and the existing test expectation).
2. Revert "commits section starts collapsed" (introduced in
|
||
|
|
45ee2ff48f |
feat: key terminals + dockview layout to TaskEnvironment (#755)
* feat(backend): key user shells by TaskEnvironmentID Sessions in the same task share a TaskEnvironment. Re-key the user shell runner so terminals are scoped per env, not per session — switching between sessions of the same task now sees the same shell list. - Add TaskEnvironmentID to AgentExecution / LaunchRequest / ExecutorCreateRequest / executor.LaunchAgentRequest, propagated through ToAgentExecution and the orchestrator launch path. - Add Manager.ResolveScopeKey(sessionID) — returns env when execution carries it, falls back to sessionID for legacy paths. - Rename runner param sessionID -> scopeID in interactive_shells.go; map keys are now scopeID:terminalID. - Shell handlers + WS terminal handler resolve sid -> scopeID via ResolveScopeKey before calling the runner. * feat(web): key dockview layout + portals to TaskEnvironmentID Switching between sessions of the same task no longer rebuilds the dockview layout — terminals and panels persist because layouts are env-scoped, not session-scoped. The bottom terminal also reuses a stable sessionId across same-env switches so it doesn't reconnect to a different shell. - Rename layout persistence: setSessionLayout/getSessionLayout/*SessionMaximizeState -> setEnvLayout/getEnvLayout/*EnvMaximizeState (kandev.dockview.env-layout. prefix). - Rename dockview-session-switch.ts -> dockview-env-switch.ts; performSessionSwitch -> performEnvSwitch with oldEnvId/newEnvId/activeSessionId. - dockview-store.ts: currentLayoutSessionId -> currentLayoutEnvId; switchSessionLayout -> switchEnvLayout(oldEnvId, newEnvId, activeSessionId). Same-env switches no-op. Drop _skipLayoutSwitchForSession + consumeSkipFlag — env-keyed action handles it structurally. - panel-portal-manager: PortalEntry.envId; releaseBySession -> releaseByEnv. ENV_SCOPED_COMPONENTS in dockview-desktop-layout (renamed from SESSION_SCOPED). - useEnvSwitchCleanup replaces useSessionSwitchCleanup; tracks env changes only. - All performLayoutSwitch callers (sidebar, dropdown, header, dialog) resolve sid -> envId via environmentIdBySessionId before switching. - bottom-terminal-panel: drop sessionId prop, use useEnvironmentSessionId(). - cleanupTaskStorage now takes envIds[] alongside sessionIds[] for env-scoped layout/maximize cleanup. * test: add coverage for env-keyed terminals + layout Playwright e2e (terminal-env-keyed.spec.ts) — switching between sessions of the same task keeps terminal scrollback intact and asserts both sessions share one task_environment_id. Update the comment in sessionless-task-switch.spec.ts to reference performEnvSwitch. * fix: address env layout PR feedback * test: fix flaky handshake auth check * fix: address env scope review findings |
||
|
|
fcc7ceb8b6 |
fix(approvals): drop stuck pending permission_request messages (#723)
* fix(web): exclude permission_request from turn-complete safety net The session.turn.completed handler iterated over messages and forced metadata.status="complete" on anything carrying tool_call_id. Permission prompts also carry tool_call_id (for FE pairing with their tool_call), so this loop overwrote the user's "approved" decision on every prompt in the turn — and the FE component re-rendered the Approve/Deny buttons. * fix(backend): protect permission_request rows from tool_call sweeps Three permission_request invariants the prior code violated, mirroring the FE bug fix: - CompletePendingToolCallsForTurn: SQL UPDATE filtered by tool_call_id presence, also catching permission_request rows. Add type filter so the safety net only touches actual tool messages. - GetMessageByToolCallID: returned the oldest row carrying that tool_call_id, which races to the permission_request when timing shifts. Exclude permission_request type so a tool_update can never land on the user's approve/reject row. - applyToolCallMessageUpdate: defensive guard that refuses to overwrite a permission_request, making the invariant explicit at the layer that does the writing. Adds a regression test for CompletePendingToolCallsForTurn covering approved/rejected/pending permission rows. * test(e2e): cover approval-reappear regression with mock-agent scenario Adds a multi-permission mock-agent scenario (three sequential bash tools, each requiring approval) and an E2E test that approves all three and asserts no permission action row remains visible after the agent's turn ends, and that the resolved state survives a reload. Supporting test infrastructure: - data-testid hooks on PermissionActionRow. - permissionActionRows() / permissionApproveButtons() on SessionPage. - backend fixture force-disables AGENTCTL_AUTO_APPROVE_PERMISSIONS=false so a developer's local AUTO_APPROVE=true env doesn't silently bypass the permission UI in tests that exercise the approve/reject flow. * test(backend): cover GetMessageByToolCallID permission_request exclusion Addresses claude-review feedback on the approvals fix: - Adds a regression test that seeds both a permission_request and a tool_call with the same tool_call_id (permission_request created first, so an ORDER BY created_at ASC sweep without the type filter would race to it) and asserts the lookup returns the tool_call row. - Bumps the applyToolCallMessageUpdate invariant guard from Warn to Error: reaching that branch means a caller bug, not a transient condition, and Warn would silently swallow the violation. * fix(e2e): scope auto-approve override to permission-approval test only The previous fixture force-disabled AGENTCTL_AUTO_APPROVE_PERMISSIONS globally, which broke session-isolation.spec.ts and other tests that use scenarios with permission-required tools (e.g. /e2e:read-and-edit) and rely on auto-approve to let the agent finish without UI clicks. Default the env to "true" in the fixture; let permission-approval.spec flip it to "false" via a backend.restart() override in beforeAll, then restore in afterAll. Adds an envOverrides parameter to backend.restart. |
||
|
|
3d2a04e776 |
feat(orchestrator): deferred task move with hand-off prompt + boot/turn-end event split (#743)
* feat(orchestrator): reuse existing same-profile session on workflow step switch
When a workflow step transition needs a different agent profile, look up
an existing session on the same task that already uses the target profile
and promote it back to primary instead of creating a fresh one. Reuses
COMPLETED and FAILED sessions (CANCELLED stays excluded — explicit user
stops shouldn't be auto-revived).
Backend (event_handlers_workflow.go):
- findReusableSessionForProfile: most recently updated session matching
the target profile, skipping CANCELLED.
- reuseSessionForStep: promotes the session to primary; if terminal,
revives via reviveReusedSession.
- reviveReusedSession: distinguishes "previously launched" (has
executors_running record -> flip to WAITING_FOR_INPUT, lazy-resume via
ResumeSession with stored token) from "never launched" (no record ->
flip to CREATED so autoStartStepPrompt routes through
StartCreatedSession for a full fresh launch). Clears stale
ErrorMessage/CompletedAt so the UI stops surfacing prior failures.
- createNewSessionForStep + completeAndStopSession: shared helpers.
Backend (task_operations.go):
- StartCreatedSession no longer calls processOnTurnStartViaEngine.
User-initiated messages already fire on_turn_start in
dispatchPromptAsync. Workflow-driven auto-start prompts arrived
because the workflow just transitioned us into this step via
on_turn_complete; firing on_turn_start again cascades the workflow
back out and the redirect-to-active-session logic delivers the prompt
to the wrong session's agent.
Frontend (web):
- task.updated handler follows focus to the new primary when the user
is viewing the task and was sitting on the previous primary.
- pinnedSessionId tracks user-initiated session selection
(setActiveSession sets it; setActiveSessionAuto leaves it alone).
The terminal-handoff branch in maybeAdoptSessionOnTransition skips
replacement when the terminating session matches the pin, so manually
clicking a session no longer yanks focus back to the workflow primary
each time the pinned session terminates.
Tests:
- TestSwitchSessionForStep_ReusesExistingProfileSession (never-launched).
- TestSwitchSessionForStep_ReusesPreviouslyLaunchedSession (launched).
- TestSwitchSessionForStep_ReusesFailedSession (FAILED reuse + cleanup).
- tasks.test.ts: focus-follow behavior across 4 scenarios.
- agent-session.test.ts: pinnedSessionId guard plus migration to
setActiveSessionAuto for WS-driven adoption.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(orchestrator): deferred task move with hand-off prompt + boot/turn-end event split
Adds move_task_kandev MCP tool that lets a workflow agent (e.g. QA at "Review")
move a task to another step with a prompt for the receiving agent. The move is
queued as a PendingMove and applied on the calling agent's turn-end so on_enter
processing doesn't race the still-running turn.
Also splits agent.ready into agent.boot_ready + agent.ready at the lifecycle
layer. Previously both signals shared one event and the orchestrator
disambiguated them via a resumeInProgressSessions flag — which had a race
when the boot signal arrived before persistResumeState wrote state=STARTING,
causing the boot to fire on_turn_complete against the just-revived step and
ping-pong the task forward through the workflow.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix: address PR #743 review feedback (Greptile + CodeRabbit)
Backend:
- isTransientPromptError no longer treats ErrExecutionNotFound as transient,
preventing executeQueuedMessage from infinite-requeueing on permanent
missing-execution errors. autoStartStepPrompt's explicit errors.Is check
before the transient branch keeps the recovery path working (Greptile P2 #1)
- applyPendingMove drains the orphaned hand-off prompt on ApplyTransition
failure so it can't be misdelivered to the source step on a future turn
(Greptile P2 #2)
- handleMoveTask returns errors instead of panicking when messageQueue is
nil; queueMoveTaskPrompt returns an error so the deferred path fails fast
if the queue can't accept the prompt (CodeRabbit CR-2)
- handleMoveTask rejects when no primary session exists and a hand-off prompt
is required, instead of silently dropping it (CodeRabbit CR-3)
- autoStartStepPrompt preserves attachments from queued messages (was: silently
dropped); attachment-only messages are no longer skipped; the fallback path
also forwards attachments (CodeRabbit CR-5)
- applyPendingMove reinserts the pending move on early failure paths (config
missing, task/step load errors) so a future turn-end retries naturally
(CodeRabbit CR-6)
- registerKanbanTools split into 3 helpers (list/profile/task) to satisfy
backend function-size lint limits (CodeRabbit CR-1)
Web:
- maybeAdoptSessionOnTransition's auto-adopt path also checks pinnedSessionId
to handle the reverse-event-ordering race (old COMPLETED arrives before new
STARTING) — pinned sessions are no longer yanked off the user. Regression
test added (CodeRabbit CR-7)
Tests:
- mockAgentManager records (executionID, prompt) pairs; new helper
capturedPromptsForExecution actually filters by exec ID instead of
returning all captured prompts (CodeRabbit CR-4)
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(web): respect pinned session in primary-follow-focus path
When the user clicks a session, setActiveSession sets pinnedSessionId. If a
workflow profile switch then updates the task's primarySessionId while the
user happens to still be on the previous primary, the focus-follow block in
tasks.ts was calling setActiveSessionAuto(taskId, newPrimary) and silently
overriding the explicit pin. Add pinnedSessionId !== previousPrimary as a
fifth condition. Regression test in tasks.test.ts covers exactly this
sequence.
Addresses Greptile P1 on PR #743.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix: address CodeRabbit review on PR #743 (3 new comments)
- handleMoveTask idle path now rolls back the queued hand-off prompt when
taskSvc.MoveTask fails, so a "you were moved..." message can't be delivered
on a future turn for a transition that didn't actually happen. Added
TakeQueued to the MessageQueuer interface to support this.
- lookupSession now returns (*models.TaskSession, error) so callers can
distinguish a backend lookup failure from a legitimate "no primary session"
state. handleMoveTask maps the error case to ErrorCodeInternalError instead
of collapsing it into ErrorCodeValidation.
- queueMoveTaskPrompt returns errors instead of log-and-continue (signature
change already shipped); existing tests updated to reflect the new contract.
- Refactored TestPendingMove_ReviewToInProgress_OneTransitionOnly: the
~280-line monolithic test is now split into a pendingMoveScenario builder
+ assertion helpers, each well under the 80-line / 50-statement lint limit.
All 14 review threads resolved.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix: relax move_task_kandev prompt requirement; fix CI lint
CI surfaced two issues after rebasing onto main:
- nestif lint warning on autoStartStepPrompt's queued-message merge block.
Extracted the take-and-merge logic into takeAndMergeHandoffMessage helper,
flattening the nested conditionals.
- E2E test settings/config-management 'agent can move a task to a different
step' failed because my CR-3 fix made the move_task_kandev prompt parameter
required. That broke admin/config-mode moves where there's no agent to
hand off to. Made prompt optional: when supplied AND the source session
is mid-turn, the deferred-move flow runs (queue prompt + record
PendingMove); otherwise just move the task. Refactored handleMoveTask
into deferMoveTaskWithPrompt and applyMoveTaskImmediate to keep each
function under the 80-line lint limit.
Tool descriptions in mcp/server/server.go and mcp/server/config_handlers.go
updated to reflect prompt's now-optional status. moveTaskHandler in
config_handlers.go forwards prompt only when set.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(mcp): treat 'no primary session' as a not-found, not an internal error
GetPrimarySessionByTaskID returns a plain error string ("no primary session
found for task: <id>") when the repo has no rows. My CR-B refactor was
treating ALL lookup errors as ErrorCodeInternalError, which broke the E2E
test 'agent can move a task to a different step' (config-mode move on a
freshly-created task that has no agent yet).
Match the not-found message and return (nil, nil); only real backend
failures bubble up as InternalError. Distinguishing properly with
errors.Is would require an exported sentinel that doesn't exist; matching
the noRowsErr substring is the pragmatic fix.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
---------
Co-authored-by: Kandev Agent <agent@kandev.dev>
Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
|
||
|
|
12bd5b2cf3 |
feat: plan checkpointing with rewind UI (#694)
* feat(backend): plan revision history with checkpoint/revert
Persist every plan edit as an immutable TaskPlanRevision with author
attribution (agent vs user) and expose WS actions to list, fetch, and
revert to past revisions. Emits task_plan.revision.created and
task_plan.reverted events so the UI can react in real time.
* feat(web): plan revision history UI with rewind to past versions
Add a rewind button to the plan panel that opens a revisions list
showing each checkpoint with author (agent name or user) and timestamp,
and lets the user restore any past revision as the new head. Plan
state, hooks, and WS handlers are extended to load, cache, and react
to revision-created/reverted events.
* test(web): e2e coverage for plan checkpointing and rewind
Cover the full revision flow: plan edit creates a checkpoint,
the revisions panel lists authors and timestamps, and reverting
restores the selected revision as the new head. Extends session
page object and backend fixtures with revision-related helpers.
* fix(web): stabilize plan revisions state and avoid re-render loop
Zustand selector returned a fresh [] for missing revisions, fanning out
re-renders that tripped React's infinite-loop guard when the plan panel
mounted. Swap to a frozen empty-array sentinel, auto-load revisions on
mount so WS events fired before connect aren't lost, and gate the
popover's on-open fetch so reopening doesn't re-sort rows mid-click.
* test(web): make plan checkpointing e2e resilient to agent auto-layout
Agent-authored plan writes auto-open the plan panel and can push the
chat tab inactive, so the chat idle placeholder isn't always visible
once the agent finishes. Accept either chat or plan panel as the idle
signal, make openPlanPanel and openRewind idempotent, reopen the
popover after a revert cancel (the confirm dialog closes it), and drop
the toast assertion since no Toaster is mounted in the app.
* fix(backend): atomic plan revision write + review polish
- Add `WritePlanRevision(ctx, head, rev, coalesceLatestID)` repository method
that wraps HEAD upsert + MAX+1 compute + insert/merge in a single write tx,
closing the TOCTOU race on `revision_number` and partial-failure divergence
between HEAD and history. `upsertPlan` and `RevertPlan` both route through it.
- Add `ErrRevisionIDRequired`; return it for empty `TargetRevisionID` (was
previously conflated with `ErrRevisionNotFound`). Handler maps to validation.
- Backfill fallback `author_kind` changed `"user"` → `"agent"` to match
`CreateTaskPlan` and the column default.
- Extract shared `revisionSelectCols` constant; rename `max` local (shadowed
Go builtin) and `dto` local (shadowed package name).
* fix(web): address plan-revisions review feedback
- `mcpWrite` uses `JSON.stringify(content).slice(1, -1)` to escape backslashes,
quotes, and control chars in one pass — closes the CodeQL finding.
- `clearTaskPlan` now evicts `revisionContentCache` entries for the task's
revisions before dropping the maps, preventing orphaned cache growth.
- `RevertConfirmDialog` wraps `onRevert` in try/catch/finally so rejections
surface a toast and the dialog always dismisses.
- Hoist `TaskPlanRevision` import in `task-plan-panel.tsx`.
- `RevisionRow` derives relative time from props every render with a 30s tick,
so prop changes refresh immediately.
- Strengthen author-switch e2e with `.not.toBeEmpty()` to also reject empty or
truncated renders.
* fix(web): evict revision content cache on coalesced update
Coalesced writes update an existing revision's content on the backend, but
the WS `upsertPlanRevision` payload carries only metadata. Without eviction,
a cached preview of the revision stays stale after the agent re-writes the
plan within the coalesce window.
* fix(web): plan history UI polish from local testing
- Drop the redundant "Plan" title in the panel header bar; the dockview/mobile
tab already labels the panel.
- Author display: show "You" with the user icon for user-authored revisions,
and the agent profile name with the AgentLogo (resolved from the active
session's `agent_profile_snapshot.agent_name`) for agent revisions, matching
the icon used elsewhere in the app.
- Replace ScrollArea with a plain `max-h-96 overflow-y-auto` div so the
popover stays bounded when revision counts grow past ~7 rows.
- Drop the em dash from the revert dialog description.
* feat(web): add lineDiff helper for plan revision compare
Tiny wrapper around the `diff` package that classifies each line as add /
remove / context and strips the trailing-newline phantom entry. Also exposes
`diffSummary` for the dialog header counts. Pure functions; backed by Vitest.
* feat(web): plan revision preview + compare state
Adds two task-scoped store keys for the new dialogs:
- `previewRevisionIdByTaskId` — at most one open preview per task; cleared on
`clearTaskPlan`.
- `comparePairByTaskId` — ordered 2-slot pair. `toggleComparePair` reuses a
`nextPair` helper that handles unselect (already-picked id), fill (open
slot), and FIFO eviction (full pair). `clearTaskPlan` resets it too.
* feat(web): plan revision preview + diff dialogs
- `useTaskPlan` exposes `previewRevisionId`, `setPreviewRevision`,
`comparePair`, `toggleCompareSelection`, `clearComparePair` (extracted into
`usePreviewCompareState` to keep the surrounding hook within the size cap).
- New `PlanRevisionPreviewDialog`: read-only modal for a single revision with
Restore + "Compare with current" CTAs. Restore routes through the existing
RevertConfirmDialog so the confirmation copy stays consistent.
- New `PlanRevisionDiffDialog`: line-level unified diff between two revisions
with adds/removes highlighted; orders the pair by revision_number so the
older one is always "before"; Restore targets the older side.
- Popover row split into a clickable body (opens preview), a Compare toggle
(icon button per row), and the existing Revert button. Compare chips appear
at the top of the popover with a Diff CTA when both slots are filled.
- Both dialogs are remounted via key on revision/pair change so loader state
resets without ESLint-flagged setState-in-effect resets.
* test(web): e2e coverage for plan preview and compare-diff dialogs
Extends the SessionPage POM with locators for the new preview and diff
dialogs (chips, compare toggle, restore buttons, body + summary). Adds two
scenarios in plan-checkpointing.spec.ts:
- Click a revision row body → preview shows the right content; the Restore
CTA opens the existing confirm dialog and the revert lands.
- Toggle compare on two revisions → diff opens with v1 → v2 ordering and
shows at least one add and one remove line; the Restore CTA targets the
older side.
* feat(web): render markdown in plan preview + diff dialogs
Adds a shared `PlanReadOnlyMarkdown` Tiptap renderer that reuses the same
StarterKit + tiptap-markdown + tables/tasks/code-block extensions as the live
plan editor (minus interactive bits — slash menu, drag handles, comment
marks). Wired into both dialogs:
- Preview replaces the raw `<pre>` block with the rendered Tiptap view.
- Diff dialog now stacks two rendered sides (Before / After) with red/green
tinted borders, kept above the existing line-level breakdown so the user
gets both rendered comparison and an exact textual change list.
Tiptap doesn't ship a free diff/track-changes extension (it's part of Tiptap
Pro), so the side-by-side rendering plus the line-level list together stand
in for that.
* fix(web): plan diff dialog — text-only with unified/split modes
Following local feedback:
- Drop the rendered-markdown side-by-side panes from the diff dialog. Diff
view is text-only again, like a code review.
- Add a Unified / Split mode toggle. Split mode aligns each remove with the
adjacent add on the same row (single-line edits stay paired) and stripes
empty cells where one side has nothing.
- Fix the highlight-truncation bug: long lines used to lose their tinted
background past the viewport on horizontal scroll because the row width
was capped to the visible area. Rows now use `min-w-full w-max` so the
background extends through the full scroll width.
Preview dialog keeps the rendered Tiptap markdown — only the diff reverted
to plain text.
E2E covers both modes and the cell kinds.
* feat(web): formatPreciseTime util for minute-level timestamps
Returns 'just now', 'Xs ago', 'Xm ago' for the first hour, then 'Today, HH:MM',
'Yesterday, HH:MM', or '{Mon} {DD}[, YYYY], HH:MM' for older entries — keeps
exact minute-of-day visible instead of decaying to 'yesterday'/'2d ago'.
* fix(web): render markdown via setContent in plan read-only renderer
The tiptap-markdown extension's parser only fires on `setContent` calls, not
on the initial `content` option. Passing `content: value` to `useEditor` was
treating the markdown source as raw text, so the preview dialog rendered
unparsed markdown.
Drop the `content` option and instead call
`editor.commands.setContent(content, { emitUpdate: false })` from a useEffect
once the editor is created — which is what the live plan editor effectively
relies on through tiptap-markdown's setContent override.
* fix(web): plan history UX — bigger modals, split layout, simpler compare
- Bump dialog widths: preview now `max-w-5xl w-[92vw]`, diff
`max-w-6xl w-[95vw]`, both with taller max-height. Plenty of room for
rendered markdown and side-by-side diffs.
- Replace the table-based split layout with a CSS grid (`grid-cols-2 w-max`).
The table-fixed cells were overflowing into each other when content was
wider than half the dialog; the grid lets the natural width drive a single
horizontal scrollbar on the parent and keeps the two columns properly
separated.
- Switch row timestamps to `formatPreciseTime` so users see minute-of-day
("Today, 14:32", "Yesterday, 09:14", "Apr 21, 11:08") instead of decaying
to "yesterday".
- Simplify the compare flow: drop the per-row Compare toggle, the chip strip
at the top of the popover, and the standalone "Diff" button. Compare is
now a side effect of preview — open any version, then click "Compare with
previous" or "Compare with current". E2E test updated to use the new path.
* test(web): update plan checkpointing e2e for the new compare flow
Drops the locators for the row Compare toggle, chips, and standalone Diff
button (those UI bits are gone). Adds a locator for the new
"Compare with previous" preview button. The compare scenario now opens the
diff via preview → "Compare with current" instead of the old chip flow.
* fix(web): preview renderer reuses live editor's markdown styles
`globals.css` keeps heading sizes / list spacing / code-block padding under
.markdown-body and .tiptap-plan-wrapper, and the live editor opts in by
applying both classes (plus a conditional .dark for theming). The read-only
renderer was using `prose prose-sm` instead, which doesn't actually take
effect on the .ProseMirror node — leaving everything the same size.
Switch the read-only renderer to the same class set, follow the live
editor's `useTheme` -> `dark` pattern, override `!h-auto` so the renderer
sizes to its content, and trim the editor's default 0.75rem padding inside
the dialog body.
* fix(web): split diff has fixed width with synced horizontal scroll
The previous split layout grew the whole dialog wider when content overflowed
which felt wrong. Now:
- The dialog stays at its fixed width.
- Each pane has its own `overflow-x-auto` so each side is capped at half the
dialog and gets its own horizontal scrollbar.
- A scroll listener keeps both panes' `scrollLeft` in lockstep (with a
re-entrancy guard so the bounce-back doesn't form a loop) — corresponding
rows stay aligned visually no matter which side the user scrolls from.
- The diff body's overflow-x is hidden in split mode (the panes own it) and
only overflow-y stays on the body so vertical scroll covers both panes
at once.
* feat(backend): resolve agent profile name from session for plan revisions
The MCP-side handlers (handleCreateTaskPlan / handleUpdateTaskPlan) don't
carry the agent's display name in the request, so without a lookup every
agent-authored revision was rendering as the literal "Agent" fallback in
the history UI.
PlanService now looks up the task's session and reads the agent profile
display name from `agent_profile_snapshot` (preferring "label", then
"name", then "agent_display_name") when the request is an agent write
without an explicit author_name. Active session is tried first; falls
back to the most recent session by started_at so plans written between
turns still get the right author name. If no usable session/snapshot
exists, the existing "Agent" fallback applies.
The repo dependency type is widened from `repository.PlanRepository` to a
small private `planRepo` interface that adds the two session lookups —
the SQLite repository (`*sqliterepo.Repository`) already implements both,
so no caller wiring changes.
* fix(web): plan history popover row + restore button polish
- Popover content's default `gap-4` between flex children (from
@kandev/ui's PopoverContent) was rendering as visible space above the
top-most row. Override with `gap-0` so the list sits flush against the
header.
- Switch row layout from `items-start` to `items-center` so the
Restore button stays vertically centered against the row body even
when the body grows taller (timestamp + restored-from marker on revert
rows).
- Rename the per-row "Revert" button to "Restore" and prefix it with
IconRestore so it matches the dialog wording and the restored-from
marker icon. The data-testid stays `plan-revision-revert-button` for
e2e selector stability.
* fix(backend): plan revision review feedback + lint
- `wsGetTaskPlanRevision` now returns a 400 validation error when
`revision_id` is empty, matching `wsRevertTaskPlan`'s
`ErrRevisionIDRequired` branch (was silently mapped to 404 via the
service's lookup miss).
- `agentDisplayNameFromSnapshot` tries `name` first, then `label`, then
`agent_display_name`. The orchestrator's canonical key wins over any
stale older `label` that may still be in a session snapshot.
- Add `idx_task_plan_revisions_task_number` on
`(task_id, revision_number DESC)`. `GetLatestTaskPlanRevision` runs on
every plan write (coalesce check); the existing `(task_id, created_at)`
index doesn't help since these queries order by `revision_number`.
- Drop `Deprecated:` from `NextTaskPlanRevisionNumber`'s doc comment so
staticcheck stops flagging the existing test usage. The TOCTOU caveat
stays in the prose.
* fix(web): plan revision review feedback, prettier, e2e resilience
- `loadRevisionContent` reads `revisionContentCache` lazily through the
store API inside the callback, so its identity stays stable. Selecting
the cache map at the hook level was recreating the callback whenever
any task's content was cached, retriggering downstream content-fetch
effects.
- `task.plan.reverted` WS event becomes a no-op handler.
`task.plan.revision.created` fires for the same row first and already
upserts; the duplicate handler was double-evicting the
`revisionContentCache` entry and triggering an extra Zustand update.
- e2e: agent author assertion now uses `not.toHaveText("You")` +
`not.toBeEmpty()` instead of the literal `"Agent"` (which only
matched the legacy fallback — agent rows now resolve to the profile
display name).
- e2e: `openPlanPanel` retries the toggle a few times after a page
reload — the panel toggle can race hydration and silently no-op,
which broke the persistence test.
- Apply prettier to the files flagged by the formatting check.
* fix(plan): toast on silent revert failure + revision ownership check
- task-plan-revisions: when `onRevert` (the hook's `revertTo`) swallows
the error and returns null, the dialog used to close silently with no
feedback. Add an `else` branch that fires `toast.error("Failed to
restore plan")`.
- wsGetTaskPlanRevision: accept an optional `task_id` and reject with a
400 "Revision does not belong to task" when the loaded revision's
task doesn't match. Mirrors `wsRevertTaskPlan`'s ownership check so a
WS client can't read content for an arbitrary revision UUID outside
the tasks it already holds a reference to.
- plan-api `getPlanRevision` takes optional `taskId`; `loadRevisionContent`
threads the active task id so the check is exercised on the happy path.
|
||
|
|
d431688557 | feat(web): auto-open plan panel with unseen-changes indicator (#650) | ||
|
|
de040a8987 | fix: gateway auth injection and cumulative diff error handling (#682) | ||
|
|
7076a40805 | chore: change default ports to avoid common conflicts (#681) | ||
|
|
c078438621 | refactor: unify task.updated via single publisher and shared mapper (#676) | ||
|
|
2d1a868107 | feat: configurable CLI flags per agent profile (#653) | ||
|
|
31140c8b9f | fix(web): follow workflow step session switches in chat UI (#625) | ||
|
|
1f7e4dbb5e | fix: stream setup script output and keep prepare panel on failure (#607) | ||
|
|
5b04c9c675 | perf: focus-gated git polling to reduce CPU on retained worktrees (#610) | ||
|
|
73e58729ba | fix: apply display filters in list view (#612) | ||
|
|
a222854422 | feat: associate agent profiles with workflows and steps (#597) | ||
|
|
12613130fa |
fix(web): prevent session failure toast from re-appearing after dismiss (#591)
* fix(web): prevent session failure toast from re-appearing after dismiss * fix: address PR review feedback — add tests and restore JSDoc |
||
|
|
c08d68c0fe |
refactor: re-key dockview panel state by environmentId instead of sessionId (#491)
* refactor: re-key dockview panel state by environmentId instead of sessionId - Remove terminal from SESSION_SCOPED_COMPONENTS (uses useEnvironmentSessionId internally) - Convert ChangesContent and file-editor-panel to useEnvironmentSessionId() - Re-key sessionCommits store from bySessionId to byEnvironmentId - Re-key useCumulativeDiff cache by environmentId - Update WS handler to resolve envKey before invalidating cumulative diff cache - Update comments across portal manager, session switch, and layout files * fix: revert useEnvironmentSessionId in file-editor-panel file-editor is session-scoped and ephemeral — portal is destroyed on session switch, so useEnvironmentSessionId() has no practical effect. * fix: address CodeRabbit review comments - Pass appStore to setupPortalCleanup so terminal tab close still calls stopUserShell via activeSessionId fallback (entry.sessionId is now undefined for global panels) - Add request version guard to useCumulativeDiff to discard stale responses after environment switches * fix: scope activeSessionId fallback to terminal only in portal cleanup vscode is session-scoped and always has entry.sessionId — the activeSessionId fallback should only apply to terminal (now global). * fix: migrate stranded env-keyed data on registerSessionEnvironment When registerSessionEnvironment(sessionId, environmentId) is called, any data previously stored under the fallback sessionId key is now migrated to the proper environmentId key. This prevents data from "vanishing" when selectors start reading by environmentId after the mapping is registered. Applies to: sessionCommits, gitStatus, shell outputs/statuses, userShells. * fix: extract migrateEnvKeyedData helper to fix lint max-lines * fix: address CodeRabbit review comments - Always delete orphaned sessionId key in migrateEnvKeyedData even when environmentId key already exists (prevents stale accumulation) - Reset loading state when switching to an envKey with warm cache to avoid brief spinner flash * fix: always reset loading on envKey switch in useCumulativeDiff Prevents stuck loading state on rapid A→B→A env switches where the stale in-flight response is discarded by version guard but loading is never reset. * fix: move sync effect before fetch effect in useCumulativeDiff React 18 batches effects in declaration order. With the sync effect after the fetch effect, setLoading(false) from the sync overrode setLoading(true) from the fetch on cold-cache env switches. Moving the sync effect first ensures the fetch effect's setLoading(true) wins the batch. * test: add migrateEnvKeyedData coverage for registerSessionEnvironment Tests 4 cases: normal migration, don't-clobber existing data, no-op when sessionId equals environmentId, and partial sub-key migration. * fix: close stale-diff gaps in useCumulativeDiff sync effect - Bump requestVersionRef on envKey change so in-flight fetches for the previous envKey are discarded - Clear loadingState[envKey] so the fetch effect isn't blocked on re-entry (A→B→A scenario) |
||
|
|
61e5d5cce9 | feat(web): redesign task sidebar with repo-grouped layout and diff stats (#550) | ||
|
|
f4723ebcae | feat: sort tasks by creation date in kanban and sidebar (#511) | ||
|
|
3a71ae2fe6 | fix: complete all non-terminal tool calls when turn ends (#488) | ||
|
|
d1a028fdb6 | feat: introduce subtasks, allow sessions to task reuse executor (#419) |