b001aa1eb8d472630ce02a00a789c19fd69b0200
45 Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
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> |
||
|
|
c959209c46 | fix(web): populate prepare progress when switching tasks client-side (#1150) | ||
|
|
3bc092bf20 |
feat(terminal): first-class user terminals — stable seq, rename, park/resume (#1009)
* feat(terminal): persist user terminals with stable seq + park state
Ordinary user shells now have a DB-backed `user_terminals` row keyed by
the agentctl PTY id, carrying a per-task monotonic seq (gaps preserved on
delete), custom_name override, and open/parked state. The terminal
service owns the create/list/rename/park/resume/destroy surface and
guards reject `bottom-panel` (cmd+J) + `script-*` ids so passthrough
flows are untouched. Closing a tab now parks (PTY keeps running);
destroy kills the PTY and removes the row.
* feat(web): expose terminal seq badge, rename, and parked-terminals menu
Wires the right-panel strip and SSR page state to the new task-keyed
terminals endpoint and discriminated UserShell union, adds the `#N`
badge for ordinary terminals, plumbs context-menu / double-click rename,
and renders a "Parked" submenu so closed-but-running terminals can be
resumed. Closing a managed tab now parks instead of destroying so the
PTY (and its buffer) survives accidental closes.
* test(terminal): e2e coverage for first-class persistent entities
Covers the persistence guarantees the user explicitly called out:
monotonic seqs, refresh-survives-rename-and-park, gap-preserving destroy,
park+resume cycle, and bottom-panel/script guards. Exercises the new
user_shell.{create,list,rename,park,resume,destroy} WS actions through
the shared ApiClient.wsRequest helper.
* fix(terminal): atomic seq allocation + propagate destroy errors
- Repository.Create folds MAX(seq) read + INSERT into a single
`INSERT … SELECT` statement so concurrent creates can no longer pick
the same seq. Locked in with TestCreate_ConcurrentSeqAllocation (25
parallel creates, all unique).
- wsUserShellStop now propagates the service-Destroy error so the
frontend stops optimistically removing a row the backend failed to
delete.
- Service.Destroy / CleanupTask log PTY-stop failures at Warn (instead
of silently swallowing) while keeping the proceed-on-failure semantics
intentional — the DB row is the source of truth for "user expects
this terminal gone".
- IsUserShellAlive copies ProcessID under the read lock before calling
r.Get(), removing the post-unlock pointer-deref race.
- Get-not-found test asserts errors.Is(err, ErrNotFound) so it locks to
the repository contract.
* fix(web): close-tab waits for backend ack + className undefined guard
- handleCloseTab defers `removeTerminal` into the `.then()` of the
park/destroy promise. A failed backend call leaves the tab visible
rather than vanishing it optimistically; the next `user_shell.list`
poll reconciles state.
- session-tabs guards `tab.className` with `?? ""` so an undefined value
no longer produces "undefined group relative..." in the rendered
class string.
- user-shells.test simplification: `createSessionRuntimeSlice` only
consumes `set`, so the test passes the slice creator straight to
`immer()` instead of wrapping it in an adapter with unused args.
* fix(terminal): task-scoped mutations + legacy-shell list + UI hardening
Round-2 review feedback (cubic P1/P2, claude, coderabbit):
Backend:
- Rename/Park/Resume/Destroy now require a task_id and verify the
terminal belongs to that task via a shared requireOwnership helper.
Cross-task mutations return ErrTaskMismatch instead of silently
succeeding by raw terminal id.
- wsUserShellList no longer drops managed-shaped agentctl shells when
the terminal service is disabled or task_id is missing — legacy
passthrough callers see their existing shells again. `managedCovered`
flag scopes the orphan filter to the DB-backed path.
- service_test.go uses the shared-cache in-memory SQLite + MaxOpenConns(1)
setup, matching repository_test.go and removing the flake-one-race-away
warning.
- New cross-task guard tests: TestRename_RejectsCrossTask,
TestPark_RejectsCrossTask, TestDestroy_RejectsCrossTask, plus a
TestRename_EmptyTaskIDSkipsOwnership for internal callers.
Frontend:
- updateUserShell patch type narrowed to Partial<Omit<UserShellInfo,
"terminalId">> so the row key can't be silently mutated.
- destroyUserShell now throws on a missing WS client instead of silently
resolving — handleCloseTab's .then() no longer fires prematurely.
- rename/park/resume/destroy API client + useTerminals hook plumb the
active task id through every mutating call.
E2E:
- New "cross-task mutating ops are rejected" spec creates a terminal
under task A and verifies task B can't rename/park/destroy it by raw
id.
- Existing specs updated to send task_id with their mutating calls.
* fix(terminal): require task_id on all mutators + wire taskID into hooks
Round-3 review feedback (cubic P1, claude blocker, coderabbit):
Backend:
- requireOwnership no longer skips the ownership check when taskID is
empty. The previous carve-out was reachable from the WS layer (where
task_id is user-controlled), so an attacker who guessed a terminal id
could omit task_id and mutate any row. CleanupTask walks the repo
directly and never touched the bypass.
- TestRename_EmptyTaskIDRejected pins the new contract.
Frontend (the real bug claude caught):
- useUserShells now accepts a taskId parameter and forwards it on the
user_shell.list WS call. Without it the backend's DB-backed ordinary-
shell path was skipped and the Zustand store wiped the SSR-hydrated
panel strip on the first WS fetch.
- Response mapping switched to a discriminated union: id (new field)
falls back to terminal_id (legacy), and kind/seq/state/pty_status/
display_name/custom_name flow into the store so the parked-terminals
submenu actually populates.
- useTerminals + useMobileTerminals read the active task id from the
store and pass it through to useUserShells.
- destroyUserShell now retries with user_shell.stop when the server
returns an unknown-action error — keeps the rollout-window
compatibility intact for older backends that only know the legacy
action name.
* fix(terminal): list parked, rollout-safe kind default, helper extraction
Round-4 review feedback (claude + greptile P1):
- useUserShells now sends include_parked: true so the "Parked terminals"
submenu populates after a page reload. Previously the WS list returned
only state=open rows; parked terminals were invisible until the user
parked something in the same session. The main strip still hides
parked entries via buildTerminalsFromShells.
- useAddTerminal stops blindly defaulting kind to "ordinary" — that
misclassified legacy passthrough payloads (terminal_id without seq)
during a rollout where the frontend is newer than the backend. The
managed-tab path then called parkUserShell on a server that didn't
know the action. Now only tags as ordinary when kind is explicit or
the row carries a seq.
- handleCloseTab extracted into a standalone useCloseTab + helpers
hoisted into use-terminals-build.ts / use-terminals-types.ts. Brings
use-terminals.ts back under the 600-line file limit and the
useTerminalActions wrapper inside the 100-line function limit.
* fix(terminal): scope-keyed shell cache + null-task guard + tighter rollout default
Round-5 review feedback (cubic P1, coderabbit x4):
- useUserShells now keys its fetch cache on env+task (`${env}:${task}`),
not just env. An earlier load without a task id (e.g. before the
active task hydrated) no longer pins the store on a legacy-only shell
list and blocks the task-scoped refetch.
- useMobileTerminals delays auto-create until a real taskID is
available. Previously a null taskID could bootstrap a legacy
non-persistent shell, mark the env as auto-created, and starve the
real task of its first-class terminal. Guard key is now env:task, and
releaseAutoCreatedEnvironment clears every scope on that env.
- useAddTerminal tightens the rollout check: a row is "ordinary" only
when `seq !== undefined`. A bare `kind: "ordinary"` without seq from
an older backend stays legacy so close → destroy on the first click.
- useRemoveTerminal reads the current activeTab from a ref instead of
closing over the value captured when handleCloseTab first ran. Async
park/destroy resolves after a user tab-switch no longer clobber the
newer selection with the stale fallback shift.
Tests:
- New "waits for taskID before auto-creating — null taskID is a no-op"
case in use-mobile-terminals.test.ts confirms the null-task guard.
- All existing mobile-terminal + slice + E2E tests pass.
* fix(terminal): live activeTab lookup + selectable-tab guard
Round-6 review feedback (cubic P2 x2):
- getActiveTab now reads the active tab from the Zustand store via
storeApi.getState() instead of a useRef/useEffect pair. Closes the
post-render race where an async close handler could resolve between
the new render's commit and the effect-driven ref sync.
- computeTerminalTabValue requires activeTab to be selectable
(present in `terminals` or === "commands") before honoring it. A
stale id left over from a parked/destroyed terminal no longer pins
the panel on a nonexistent tab and blocks the fallback shift to a
real shell.
* chore(terminal): inline savedTabFromStorage to drop file under line cap
Round-7 review feedback (claude suggestion): the useMemo around the
synchronous getSessionStorage call was zero-value memoisation that
pushed use-terminals.ts three lines over the 600-line max-lines limit.
Inlined; file now at 600 lines.
* chore(terminal): drop savedTabExists local + comment to land under 600 lines
Round-8 follow-up (claude): the previous commit's two-line comment block
held the file at 603 lines (still over the 600-line cap). Inline
savedTabExists at the only call site and remove the redundant comment —
the round-7 PR thread already explains why the useMemo was removed. File
now at 599 lines.
* fix(terminal): plumb task_id through dockview + tolerate legacy destroys
Manual-test feedback: clicking X on a dockview terminal panel surfaced
"destroy terminal: terminal does not belong to the supplied task" because
the dockview path was creating + destroying terminals without sending
task_id at all. The new requireOwnership guard then correctly rejected
the empty task_id against a non-empty term.TaskID.
Frontend:
- addTerminalPanel grows task_id + title parameters; stamps both into
panel params at creation time so the destroy cleanup can pass them
back to user_shell.destroy / .stop.
- Every dockview-side createUserShell call site now passes the active
task_id, so the backend creates a DB-backed ordinary terminal (with
seq + displayName) instead of a legacy passthrough shell.
- destroyUserShell + stopUserShell already accepted an optional task_id;
dockview-layout-setup.onDidRemovePanel now reads the stamped task id
from panel params and forwards it.
Backend:
- wsUserShellStop now requires both terminalSvc and a non-empty
req.TaskID before routing through the managed service. An empty
task_id falls through to the legacy passthrough StopUserShell so older
clients (and dockview panels persisted before task_id was stamped)
keep working.
Known follow-ups (separate task): the #N badge, rename context menu,
and parked-terminals dropdown still only show on the mobile/tablet
right-panel strip. Desktop dockview shows the seq inside the panel
title text but doesn't expose the badge/rename/parked-list UI yet.
* feat(terminal): dockview tab badges + rename context menu + existing-terminals list
Manual-test feedback round 2:
- Custom dockview tab component (TerminalTab) renders the #N pill
only when the active env has more than one ordinary terminal —
mirrors the session-tab badge semantics. Right-click on any ordinary
terminal tab now exposes Rename / Park / Destroy.
- Dockview + menu picks up a new "Terminals" section listing every
ordinary terminal for the active task (open and parked). Clicking a
row focuses the existing dockview panel if one is mounted, otherwise
re-opens it; parked rows are auto-resumed via user_shell.resume
before the panel is added. The base "Terminal" action was renamed
to "New Terminal" so the existing-terminals list is distinguishable
in the menu.
- Reopen menu uses a stable EMPTY_SHELLS sentinel for its store
selector to avoid Zustand infinite re-renders when the env has no
shells.
E2E:
- terminal-new-tab.spec uses getByRole("menuitem", { name: "New
Terminal", exact: true }) to disambiguate from the new "Terminals"
submenu label.
- Full terminal suite still passes (26/26).
* fix(terminal): tab title shows "Terminal" with seq as badge, populate store on session page
Re-reading the user feedback: the title text was rendering as
"Terminal 1" (backend's displayName), and the dockview "+" menu's
Terminals section was empty because nothing on desktop was calling
useUserShells to populate the store.
- TerminalTab.pickDisplayName now returns the literal "Terminal" for
ordinary terminals (custom names still override). The seq lives only
in the sibling badge — same split as session-tab where the agent
name is the title and the seq is a pill before it. Reading "Terminal
1" out of "[1] Terminal" instead of "[1] Terminal 1".
- LeftHeaderActions (dockview "+") now calls useUserShells on mount.
Without this the desktop layout never triggered the WS list fetch,
so the Terminals submenu and the badge "count > 1" check both saw
an empty store and silently disappeared.
- TerminalReopenMenuItems also calls useUserShells defensively (cheap
thanks to scope-keyed cache) so it works when mounted in isolation.
* feat(terminal): "New X" rows live inside their section labels
User feedback: move "New Agent" under the Agents section header and
"New Terminal" under the Terminals section header so the dockview "+"
menu reads top-to-bottom as Agents → New Agent → existing sessions →
Terminals → New Terminal → existing terminals → other panels.
- SessionReopenMenuItems accepts an optional onNewSession prop and
renders the "New Agent" row as the first item under the "Agents"
label. Render gate now keeps the section visible whenever the create
handler is supplied, even with zero existing sessions.
- TerminalReopenMenuItems gets a matching onNewTerminal prop with
the same behaviour ("New Terminal" row under the "Terminals" label).
- dockview-add-panel-items drops its standalone "New Agent" and "New
Terminal" rows since both now live inside their sections.
* feat(terminal): auto-migrate default panel to DB-backed ordinary terminal
The legacy `terminal-default` dockview panel was hard-wired to a
non-managed `shell-default` PTY so it never picked up an ordinary
seq/customName/parked state — the new "Terminal N" badge UX could
only show a badge once the user had explicitly created a second
terminal. With useEnsureDefaultTerminalOrdinary the default panel is
upgraded into a real `shell-<uuid>` row on session-page mount, so
every visible tab is now a first-class ordinary terminal.
Plus a UI-level e2e spec asserting the multi-terminal title + badge
contract, and an expectation bump on the existing rename+park spec
since visiting the session page now mints 1 extra ordinary terminal.
* fix(terminal): park ordinary terminals on tab close so they survive refresh
Dockview's tab X close button routed straight to stopUserShell (an
alias for destroyUserShell), wiping the DB row on close — after a
refresh the user had no way to resume a still-running PTY they had
just tucked away.
The X is now a "hide for now" affordance: ordinary terminals get
parked (PTY survives, row stays in DB, reappears in the "+" →
Terminals menu with a `parked` pill), while scripts / bottom-panel /
legacy passthrough shells stay on the destroy path because they have
no resume story.
Permanent delete still lives on the right-click Destroy menu item
and (in the next commit) on each reopen-menu row.
* fix(terminal): preserve DB-backed terminal panel ids across layout restore
The serializer's normalizePanel rewrote every user-created terminal
panel id to `terminal-saved-N` and dropped environmentId/taskID from
params, so after a reload that panel couldn't be matched back to its
live PTY or its store record — the tab rendered with no badge and a
stale fallback title.
Treat `shell-<uuid>` panel ids the same way we already treat
`terminal-default`: preserve id and params on restore. Legacy
ephemeral `terminal-saved-*` ids still mint a fresh PTY on restore.
Also force tabComponent=terminalTab and title="Terminal" for any
restored terminal panel so layouts persisted before the badge UX
land on the new presentation.
* feat(terminal): destroy button on reopen-menu rows for permanent delete
Destroy used to live only on the tab's right-click menu — useless once
the tab was already closed. Each ordinary terminal row in the dockview
"+" → Terminals submenu now ends with an × button that confirms-then
calls destroyUserShell, killing the PTY and dropping the DB row so the
terminal does not reappear after reload.
Pairs with the previous close-to-park change: closing the tab parks
(soft, resumable), the × button on the menu row destroys (hard,
permanent).
* fix(terminal): reopen-menu focuses existing tab, drops parked pill, says Terminate
Three small fixes to the dockview "+" → Terminals submenu:
1. Clicking a row whose terminal is already mounted now FOCUSES the
existing panel instead of duplicating it. The default-migrated
panel keeps its registry id `terminal-default` even after the
migration to a `shell-<uuid>` PTY, so a direct getPanel(uuid) miss
would fall through to addTerminalPanel and create a second tab for
the same PTY. Fall back to scanning `params.terminalId` so the
lookup catches that case.
2. Drop the "parked" pill from the row — parking is now an internal
detail (close = park) that the user doesn't need to read off the
menu. Clicking the row brings the terminal back into view either
way, so distinguishing it visually is noise.
3. Rename the row × tooltip + confirm copy from "Destroy" / "Permanently
delete" to "Terminate" to match the new tab-context-menu label.
Spec: add a test that asserts no duplicate tab when reopening an
already-open terminal, and drop the parked-pill assertion from the
existing close+reload test.
* feat(terminal): empty-group watermark uses the rich + menu
When a user splits a group (split-right / split-down) the new half is
empty and dockview renders the watermark. Before this, the watermark
showed a hard-coded Agent/Plan/Terminal/Browser row and clicking
"Terminal" called createUserShell without a taskId — so it spawned a
legacy passthrough shell that never appeared in the user-shell list,
and clicking "Agent" only ever focused the singleton chat panel
(invisible if the task had multiple sessions).
The watermark now renders the same AddPanelMenuItems content the
header "+" uses, so the user gets:
- The Agents list (existing sessions, greyed when already open)
- The Terminals list (existing terminals, greyed when already open)
- The Browser/Plan/VS Code/Changes/Files/PR options
…served behind a single "Add panel" dropdown trigger centered on the
empty group.
* feat(terminal): inline rename on the tab; drop Park, rename Destroy → Terminate
The tab title is now editable in place — picking Rename from the
right-click context menu swaps the title for an input. Enter commits,
Escape / blur-with-no-change cancels, and the new name is pushed
through renameUserShell + the user-shell store so the API and badge
list stay in sync. Replaces the previous window.prompt which felt
out of place in the dockview chrome.
While here:
- Drop the "Park (hide, keep PTY)" context-menu item. Tab close
already parks ordinary terminals, so the explicit Park action was
redundant and broke the "one obvious affordance per outcome" rule.
- Rename "Destroy" → "Terminate" in the context menu. Matches the
reopen-menu × tooltip and reads more like a system action than a
destructive UI primitive.
Spec adds an inline-rename test asserting the input appears, accepts
typed text, and commits the new title to the tab.
* fix(terminal): right-click Terminate closes the tab; drop displayName DOM warning
Two unrelated fixes both landing in terminal-tab.tsx:
- Terminate used to fire the WS destroy and update the store, but left
the dockview panel mounted with a dead PTY underneath. Now it
optimistically drops the shell row from the store, closes the panel
via api.close(), then sends the destroy — same effect as the X close
but routed to the hard-destroy path (the onDidRemovePanel cleanup
sees no shell entry and skips its park branch).
- TerminalTabBody typed `displayName` as a prop but never destructured
it, so {...props} spread it into DockviewDefaultTab and on into a DOM
div, triggering "React does not recognize the `displayName` prop on
a DOM element". Drop it from the spread.
* feat(terminal): polish + menu — badge-style indices, flush-left New rows, terminate closes panel
Polish pass on the dockview "+" → Agents / Terminals submenus:
- Replace the plain `#N` text with the same pill-style badge the tabs
use (small bg, rounded, 11px). Agents and terminals both pick this
up — so the menu row reads "[N] label" matching the tab style.
- Drop the empty index-column placeholder span on the "New X" rows so
the icon + label sit flush-left under the section header instead of
awkwardly indented.
- Strip the backend "Terminal {seq}" suffix from row labels — the
badge already carries the number, so showing "Terminal 1" next to
the "1" pill duplicated it. Custom-renamed terminals still show
their name.
- The row × button now also closes the dockview panel if the terminal
is currently open, mirroring the right-click → Terminate flow.
Spec: switch the row-by-seq lookups from `hasText: "#N"` (now stale
since the `#` prefix is gone) to a `has:` filter against the new
`reopen-terminal-seq-N` badge testid, and add a regression test for
right-click → Terminate closing the tab and removing the row in one
gesture.
* fix: address terminal review feedback
* fix: resolve terminal review suggestions
* fix: catch terminal close stop failures
* fix: harden terminal terminate failures
* fix: use pinned pnpm docker arg
* fix: satisfy backend lint after main merge
* fix: stabilize e2e after layout merge
|
||
|
|
2c15cf83a9 | feat(web): add tree view for changes panel (#1026) | ||
|
|
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) | ||
|
|
9664e13f07 | fix(web): keep "All Workflows" filter on task nav and workflow.created (#854) | ||
|
|
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
|
||
|
|
6fa38cd62c |
fix(terminal): re-key user-shell RPCs to task_environment_id (#770)
* refactor(backend): re-key user-shell RPCs to task_environment_id user_shell.list/create/stop now take task_environment_id directly instead of session_id with a session→env translation. Sessions in the same task share one shell list, so the env is the natural scope. HTTP route /api/v1/sessions/:id/terminals → /api/v1/environments/:id/terminals. shell.status/subscribe/input (agent passthrough) stay session-keyed. * fix(backend): heal task_sessions.task_environment_id on boot Sessions created via paths that didn't write the FK left shell ops broken — every env-keyed user-shell RPC failed to resolve the env and the frontend silently swallowed the error. The boot heal links each orphan session to its task's existing env (idempotent, runs after backfillTaskEnvironments + healDuplicateTaskEnvironments). * refactor(web): drop session_id from user-shell API, hooks, and slice createUserShell/stopUserShell/fetchTerminals take environmentId. useUserShells takes envId directly (no environmentIdBySessionId translation). useTerminals takes both (env for shell ops, session for tab UX persistence). Slice actions key writes by env id directly, removing the silent no-op when the session→env mapping wasn't set yet. * refactor(web): wire env id through dockview, right-panel, and SSR call sites Dockview "+" menu, terminal scripts dropdown, dev-preview button, right-panel "+" button, Ctrl+\` toggle, watermark, and panel-close cleanup all source the env id from useEnvironmentId() / store and pass it to createUserShell/stopUserShell. Shell-mode passthrough terminal no longer blocks on session id — env id is sufficient. SSR resolves the active session's task_environment_id from the session list and calls fetchTerminals with the env id. * test(e2e): cover env-keyed terminal new-tab flows + WS payload guard New spec exercises the right-panel "+" button and the dockview "+ → Terminal" menu, asserts the new tab connects, and intercepts outgoing user_shell.create WS frames to verify the payload carries task_environment_id and never session_id. A heal-coverage assertion checks every listed session has task_environment_id populated post-boot. * fix(e2e): drop right-panel '+' test, dockview is the only '+' path on desktop The mobile TaskRightPanel SessionTabs aren't rendered at desktop viewports — the dockview '+' menu is the sole new-tab affordance, so testing the mobile path on chromium just timed out clicking a button that never mounted. Consolidate into a single dockview '+ → Terminal' test that asserts the new panel gets the backend-assigned 'Terminal 2' label (proving env-shared shell list) plus the WS-payload regression guard. * fix(e2e): assert dockview terminal-tab count, not 'Terminal 2' label Dockview hardcodes the panel title to 'Terminal' for every tab — only the mobile right-panel SessionTabs surface the backend's 'Terminal N' label. Switch the assertion to count tabs to keep the e2e green. * test(backend): cover SQL NULL branch of healSessionTaskEnvironmentIDs Adds insertSessionWithNullEnvID and TestHealSessionTaskEnvironmentIDs_LinksNullOrphans so the WHERE-clause IS NULL predicate is exercised — the existing tests only seeded empty-string FKs. Also adds LIMIT 1 to the heal subquery as a defensive measure: the unique index already guarantees ≤1 row per task at runtime, but the SQL now reads as deterministic in isolation. * refactor(web): drop dead taskGateOk on agent terminal WS gate Agent passthrough routes by sessionId, not taskId — taskId was only used for logging. Gating the WS effect on taskId forced agent terminals to wait for useSession() to hydrate, adding a refresh-race window that didn't guard any real precondition. connectionKey + canConnect already cover the meaningful preconditions. * fix(web): stamp env id on terminal panels for accurate cleanup addTerminalPanel now bakes environmentId into the panel's params at creation time. The dockview-layout-setup onDidRemovePanel handler prefers the stamped env over re-deriving from the active session, so closing a terminal that was opened under a different task no longer calls stopUserShell with the wrong env (which silently no-ops on the backend and leaks the shell process until session teardown). Falls back to the active session's env for legacy panels persisted before the param existed. * fix(web): keep task id logging out of terminal reconnect deps |
||
|
|
1eae869054 | feat: add improve kandev in-app contribution flow (#740) | ||
|
|
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>
|
||
|
|
c11a6bbfd2 |
feat(github): add issue watcher with task creation and auto-cleanup (#672)
* feat(github): add issue watcher with task creation and auto-cleanup
Add a GitHub Issue Watcher feature that polls for new issues matching
configurable filters and automatically creates tasks for them. This
complements the existing PR Review Watcher with the same patterns.
Backend:
- Issue watch CRUD (models, store, service, controller, WS handlers)
- Poller integration with configurable intervals
- Event-driven task creation via orchestrator
- Repository resolution and worktree support for issue tasks
- Auto-cleanup of tasks for closed issues (CleanupClosedIssueTasks)
- GetIssueState client method across all implementations
- Default branch auto-detection from cloned repo HEAD
Frontend:
- Issue watch settings UI (table, dialog, placeholders)
- Issue link icon on task cards (green circle-dot)
- API client, store slice, and hook for issue watches
- SSR and WS hydration of issue metadata
* fix: resolve golangci-lint errors (goconst, nestif, dupl) and prettier formatting
* fix(github): fix issue watch update shape, trigger key, and label quoting
- httpUpdateIssueWatch now returns the full IssueWatch object instead of
{updated:true} so the frontend store correctly refreshes after edits
- httpTriggerIssueWatch returns new_issues_found to match TriggerIssueResponse
type; was returning new_issues so the toast count was always 0
- buildIssueFilter uses conditional quoting instead of fmt.Sprintf %q to
avoid Go escape sequences in GitHub search qualifiers
Co-authored-by: João Salavisa <undefined@users.noreply.github.com>
* fix: resolve eslint warnings (max-lines-per-function, no-nested-ternary)
* test(github): add issue watcher tests and fix review feedback
- Add TestInterpolateIssuePrompt (4 cases: empty/all/none/partial)
- Add TestCreateIssueTask (3 cases: skip/reserve+assign/release on fail)
- Add TestShouldDeleteIssueTask (4 cases: open/closed+sessions/closed/error)
- Fix WS response key new_issues -> new_issues_found
- Fix fmt.Printf -> fmt.Fprintf(os.Stderr) in store.go
- Add CleanupClosedIssueTasks call in WS trigger handler
* fix: address PR review feedback and CI failures
- Split test files to fix revive file-length-limit (service_test.go,
event_handlers_github_test.go)
- Add session checker guard in CleanupClosedIssueTasks
- Normalize poll intervals on UpdateIssueWatch
- Add cleanup call in TriggerAllIssueChecks loop
- Log cleanup errors in controller and WS handler
- Prefer refs/remotes/origin/HEAD in detectGitDefaultBranch
- Add aria-label to issue task icon
- Fix TypeScript type errors: add linkedIssues to GitHubSlice,
fix hook exports, remove invalid imports
* fix: split test files exceeding revive limit and clean up minor issues
- Split service_test.go (883→779 lines): move TestTriggerPRSync_* to
service_pr_sync_test.go
- Split event_handlers_github_test.go (806→677 lines): move
TestCreateReviewTask_* and helpers to event_handlers_github_review_test.go
- Remove redundant s.taskSessionChecker nil-check inside shouldDeleteIssueTask
(early guard at CleanupClosedIssueTasks already ensures it is non-nil)
- Remove unreachable dead guard in detectAndPersistDefaultBranch
(repo.DefaultBranch is always "" at that call site)
- Fix bogus #0 issue badge in task-session-sidebar.tsx when issueNumber is absent
Co-authored-by: João Salavisa <undefined@users.noreply.github.com>
* fix: address Claude review — poll clamp and prompt injection
- Handle v <= 0 in UpdateIssueWatch poll interval (fall back to default)
- Remove {{issue.body}} from default template to prevent prompt injection
- Update test expected output for new default template
* fix: remove {{issue.body}} from frontend DEFAULT_ISSUE_WATCH_PROMPT
Mirrors the backend fix — the UI pre-fills the prompt so the backend
fallback was never reached. The placeholder stays in
ISSUE_WATCH_PLACEHOLDERS for explicit opt-in.
* feat(github): add isIssueWatch sidebar filter dimension
Add isIssueWatch boolean to KanbanState, TaskSwitcherItem, and sidebar
filter registry — mirroring the isPRReview pattern. Issue-watch tasks
(created from issue_watch_id in metadata) can now be explicitly filtered
in the sidebar, just like PR review tasks.
Files changed:
- sidebar-view-types: add isIssueWatch dimension
- filter-dimension-registry: add Issue watch filter meta
- apply-view: add isIssueWatch extractor
- kanban types: add isIssueWatch field
- TaskSwitcherItem: add isIssueWatch field
- use-all-workflow-snapshots: derive isIssueWatch from metadata
- ssr/mapper: derive isIssueWatch from metadata
- task-session-sidebar: pass isIssueWatch, extract helpers to fix complexity
* fix: correct deleteIssueWatch response type to match backend
Backend httpDeleteIssueWatch returns {"deleted": true}; the frontend
type was { success: boolean } — a mismatch with no functional impact
(return value is ignored) but wrong per TypeScript contract.
Co-authored-by: João Salavisa <undefined@users.noreply.github.com>
* refactor: extract metadata helpers to shared lib/metadata-utils.ts
- Move isPRReviewFromMetadata, isIssueWatchFromMetadata, and
issueFieldsFromMetadata to a shared module to eliminate duplicate
definitions (sonarjs/no-identical-functions).
- Fix deleteIssueWatch response type: { success: boolean } -> { deleted: boolean }
to match the actual backend response.
* style: fix prettier formatting
* test(web): add tests for isIssueWatch filter dimension and metadata-utils
Co-authored-by: João Salavisa <undefined@users.noreply.github.com>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
* style: fix prettier formatting in test files
* fix(github): cascade-delete issue watch tasks on watch deletion and add isPRReview to SSR mapper
- DeleteIssueWatch now wraps in a transaction and deletes related
github_issue_watch_tasks rows before removing the watch; prevents
orphaned dedup rows accumulating after a watch is deleted
- snapshotToState in mapper.ts now also sets isPRReview from metadata,
matching the mapping already done in use-all-workflow-snapshots.ts
Co-authored-by: João Salavisa <undefined@users.noreply.github.com>
* style: extract duplicate string constant in metadata-utils test
* style: extract repeated WS error strings into constants in handlers.go
goconst min-occurrences: 3 — 'invalid payload' (6x) and 'id required' (4x)
both exceed the threshold. Extract to package-level constants.
Co-authored-by: João Salavisa <undefined@users.noreply.github.com>
* style: extract repeated issue key strings into constants in event_handlers_github.go
goconst min-occurrences: 3 — 'issue_number' (9x) and 'issue_watch_id' (5x)
both newly introduced in this PR exceed the threshold.
Co-authored-by: João Salavisa <undefined@users.noreply.github.com>
* style: fix prettier formatting in metadata-utils test
---------
Co-authored-by: Kandev Agent <agent@kandev.dev>
Co-authored-by: claude[bot] <41898282+claude[bot]@users.noreply.github.com>
Co-authored-by: João Salavisa <undefined@users.noreply.github.com>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
|
||
|
|
f6f45b5777 |
feat: add filter/group/sort and saved views to task sidebar (#644)
* feat(web): add sidebar view types and apply-view engine
Introduces the typed representation of a sidebar view (filter clauses,
sort spec, group key) and the pure engine that applies a view to a list
of tasks to produce a filtered, sorted, grouped result.
* feat(web): add built-in sidebar view presets
Provides the four shipped presets (All tasks, No PR reviews, Active,
Archived). The 'No PR reviews' preset filters out PR-watcher tasks that
would otherwise crowd the sidebar.
* feat(web): persist sidebar views in ui store
Adds sidebar view state to the ui slice with draft semantics for saved
views, localStorage persistence of active view + user-created views, and
root store wiring.
* feat(web): add filter dimension registry and clause editor
Registry of filterable task dimensions (status, workspace, workflow, task
type, etc.) plus the primitive UI pieces — clause editor, sort picker,
group picker, and a hook that supplies operator and value options for a
selected dimension.
* feat(web): add sidebar filter bar, popover, and view manager
Composes the filter primitives into the user-facing surfaces: the top
filter bar, the popover that hosts clause editing and sort/group
selection, the chips that switch between views, and the manager for
saving, renaming, and deleting user views.
* refactor(web): extract NewTaskButton component
Pulls the Create Task button and its command-palette registration into a
standalone memoized component in preparation for reuse across sidebar
surfaces.
* feat(web): integrate filter/view system into task sidebar
Wires the filter bar, view chips, and apply-view engine into the task
sidebar (desktop and mobile sheet). Updates task-switcher to consume the
grouped list from the engine and adds mock data for tests and previews.
* test(web): add e2e coverage for sidebar filter/view system
Page object for the filter popover plus Playwright specs covering built-in
presets, clause editing, sort/group, saved-view draft semantics, and
persistence across reloads.
* refactor(web): trim sidebar built-in views to a single seeded default
Drop the "No PR reviews", "Active", and "Archived" built-in view presets
and the isBuiltIn protection flag. The sidebar now seeds a single "All
tasks" view on first load; users can rename, overwrite, or delete it like
any other view. Deletion is guarded against removing the last remaining
view so the user can never lock themselves out.
* fix(web): humanize executor type labels in sidebar filter and grouping
The executor-type filter dropdown and group-by label were rendering raw
backend identifiers like "worktree" and "local_docker". Route both
through the existing getExecutorLabel() helper so the sidebar shows
"Worktree" and "Local Docker" like the rest of the app.
* feat(web): replace "Has PR" filter with "Is PR review"
The old dimension matched any task with an associated PR, including
tasks a user manually linked. The new dimension isolates tasks created
by the GitHub PR-review watcher specifically, which is what the filter
is useful for — hiding a wall of auto-generated review tasks.
The flag is derived from the review_watch_id metadata key the backend
already stamps onto watcher-created tasks, threaded through the kanban
snapshot and into the sidebar TaskSwitcherItem. A small migration drops
unknown dimensions (including the old "hasPR") from stored views so the
popover does not crash when reading legacy data.
* fix(web): tighten sidebar filter popover layout
Reduce the popover width (w-96 → w-[22rem]), trim section padding
(p-3 → p-2.5) and label margins (mb-2 → mb-1.5), and drop the
"No filters applied." placeholder. Extract a shared SectionLabel to
keep the three section headers visually aligned.
* feat(web): show workflow step color dot in sidebar filter dropdown
Surface the step color on the workflow-step filter's value dropdown so
each option renders with the same coloured circle used elsewhere (task
switcher, kanban column, command panel). Adds an optional color field
on filter value options; only workflow-step options populate it today.
* feat(web): replace cramped in/not-in filter editor with a multiselect dropdown
The previous "in/not in" value input rendered selected values as tiny
10px pills inside a fixed-height row that overflowed and truncated as
the list of options grew. Replace it with a combobox-style multiselect
(Popover + Command) that shows a compact summary ("N selected" or the
single label with its color dot) and opens a searchable, scrollable
list with check indicators on click.
* fix(web): show workflow and step names (not UUIDs) in sidebar grouping
Group-by Workflow and Workflow Step rendered raw UUID strings as group
headers because `TaskSwitcherItem` did not carry the resolved names.
Pass `workflowName` and `workflowStepTitle` through `toSidebarItem` and
prefer them in the group extractors.
* fix(web): drop redundant true/false selector on boolean sidebar filters
Boolean dimensions (PR review, Archived, Has diff) previously exposed
both an operator (is / is not) and a value selector (true / false),
which gives four user inputs for two real states. Hide the value input
for boolean clauses and always store `true`; negation is expressed via
the operator. Rename "Is PR review" to "PR review" since the dimension
label no longer needs to read as a question.
* refactor(web): drop Sort by Repository and relabel State to Status
Sorting by repository duplicates what Group by Repository already
offers, and "State" leaks a backend term into the picker. Remove the
repository sort key (comparator, type, picker option) and rename the
state option to "Status". Stored views referencing the removed key are
migrated back to `state` at load time so the popover does not crash.
* fix(web): tighten sidebar filter popover section spacing
The FILTERS, SORT, and GROUP BY section labels each sat above their
controls with both padded containers (p-2.5) and a 6px bottom margin,
leaving visible dead space above each header. Reduce container padding
to p-2 and label margin to mb-1 so sections read as a compact stack.
* chore(web): apply prettier formatting drift in sidebar-filter area
* revert(web): undo store.ts prettier reformat to stay under max-lines cap
The packed single-line UIA declaration in store.ts is load-bearing for
the 600-line ESLint cap: the previous chore commit unpacked it to 9
lines and pushed the file to 609 lines, turning the max-lines warning
into a lint failure. Restore the packed layout (the /* prettier-ignore */
comment is there for exactly this reason).
* refactor(web): label boolean filter ops as Show/Hide instead of is/is not
* fix(web): pull section labels flush to sidebar filter popover borders
* refactor(web): inline rename and save-as into sidebar view header row
* refactor(web): split ViewHeaderRow into mode, display, and action subcomponents
* feat(backend): persist sidebar views in user settings
Adds a SidebarViews field to UserSettings (stored in the existing
users.settings JSON blob) plus validation in the service layer:
views are capped at 50, ids and names must be non-empty and unique.
* feat(web): hydrate and sync sidebar views to backend
Moves the source of truth for saved sidebar views from localStorage
to the user settings endpoint. Views hydrate from SSR on first load;
create/rename/overwrite/delete/duplicate/toggle-collapsed all PATCH
the backend with optimistic updates and revert-on-failure. The
active view id and draft remain per-device in localStorage. A
sync bridge surfaces PATCH errors as toasts and runs a one-time
migration to upload any pre-existing local views.
As a side effect, localStorage writes and PATCH side effects are
moved out of immer producers to avoid proxy-serialization risk.
* chore(web): apply prettier to store.ts sidebar actions block
* fix(web): run migrateView on SSR-hydrated sidebar views
Defensively drop clauses with unknown dimensions and reset stale sort
keys when views come from the backend, matching the localStorage load
path. Also notes the default-state module and sidebar views in the
store structure docs.
* docs: note default-state module and sidebar views in store structure
* fix(web): type sidebar view storage wrappers properly
Drop the as-unknown-as-SidebarView and as-never casts at call sites by
making the local-storage getters/setters accept the domain type and do
the JsonValue cast once inside the wrapper.
* refactor(web): stop PATCHing on sidebar group collapse toggles
collapsedGroups is per-device visual state; routing it through
mutateViews made every expand/collapse click fire a user-settings
PATCH. Keep it local (in-memory + localStorage cache); the next real
view mutation picks up whatever state is current.
* fix(web): skip sidebar view migration when settings hydrated from server
After SSR hydration the in-memory views are already the backend's
truth, so migrating re-POSTs the same data every page load. Gate the
one-shot migration on userSettings.loaded — only run if the server
didn't answer and the slice fell back to localStorage.
|
||
|
|
1f7e4dbb5e | fix: stream setup script output and keep prepare panel on failure (#607) | ||
|
|
a222854422 | feat: associate agent profiles with workflows and steps (#597) | ||
|
|
81341263fd | feat(terminal): fix bottom padding and add font family setting (#489) | ||
|
|
d1a028fdb6 | feat: introduce subtasks, allow sessions to task reuse executor (#419) | ||
|
|
63efc85dd1 | feat: make quick chats independent of workflows (#434) | ||
|
|
06e5114174 |
feat: add clickable terminal links with configurable open behavior (#401)
* feat: add clickable terminal links with configurable open behavior Add @xterm/addon-web-links to both passthrough and shell terminals, enabling Cmd+Click on URLs. Users can configure whether links open in a new browser tab (default) or the built-in browser panel via a new setting in the Settings page. * style: fix prettier formatting * fix: address CodeRabbit review comments - Extract applyTerminalLinkBehavior helper to reduce complexity - Set default terminalLinkBehavior for first-time users in empty settings path - Use storeApi.getState() for optimistic rollback to avoid stale closures - Add beforeEach settings reset in E2E tests - Use 4xx range assertion instead of hardcoded 500 * fix: return 400 for user settings validation errors Add ErrValidation sentinel to user service and wrap validation errors from apply* functions. Handler now returns 400 Bad Request instead of 500 Internal Server Error for invalid input. Update E2E test assertion to enforce client error status (< 500). * fix: remove cmd+click mention from terminal links description * fix: mirror validation error handling in WS update path * style: fix prettier formatting |
||
|
|
d84b8873ea | feat: quick chat implementation (#393) | ||
|
|
74a8d7563e |
feat: split file search from command panel, add inline task search & configurable shortcuts (#383)
* feat(web): split file search from command panel and add inline task search
Separate the command panel (Cmd+K) from file search (Cmd+Shift+K) to
prevent file results from overpowering commands. Add inline task search
directly in the command panel — tasks appear in a "Tasks" group when
typing ≥2 characters, with workflow step badges and archived state
indicators. Archived tasks are sorted last with reduced opacity.
- Add "search-files" mode to CommandPanelMode
- Add FILE_SEARCH shortcut (Cmd/Ctrl+Shift+K)
- Move file search to dedicated mode, remove from commands mode
- Add useInlineTaskSearchEffect for commands mode task search
- Remove "Search Tasks" sub-mode entry point (now inline)
- Show file search shortcut hint in command panel footer
* feat(web): add configurable keyboard shortcuts in general settings
Add localStorage-based shortcut override system for the command panel
and file search keybindings. Users can record new key combos via the
settings UI and reset to defaults.
- Add shortcut-overrides module (get/set/reset via localStorage)
- Add KeyboardShortcutsCard with shortcut recorder UI
- Add "Keyboard Shortcuts" section to general settings page
* test(web): add e2e tests for command panel and file search split
Cover the key use cases: Cmd+K opens commands without files,
Cmd+Shift+K opens file search, inline task search with workflow step
badges, escape/backspace navigation, and panel toggle behavior.
* fix(test): remove unused seedData variable in command panel e2e test
* feat(settings): persist keyboard shortcuts to user settings API
Replace localStorage-based shortcut overrides with server-persisted
storage via the user settings API. Adds keyboard_shortcuts field to
the backend model, DTO, store, service, and controller. Frontend
reads overrides from Zustand store (hydrated from SSR/WS) and saves
via PATCH /api/v1/user/settings.
* fix(ui): prioritize tasks over commands, add step colors, fix shortcut display
- Move task results above command results in cmd+K for faster task switching
- Replace magnifier icon with task icon (IconSubtask) for task results
- Apply workflow step color to task badges
- Fix formatShortcut early return bug that dropped Shift modifier
when ctrlOrCmd was set (e.g. Cmd+Shift+K showed as Cmd+K)
- Fix same bug in shortcutToCodeMirrorKeybinding
* fix(ui): use clipboard icon for task results in command panel
* style: format command panel footer
* fix(ui): use hammer icon and map step colors to hex for task badges
* feat(ui): show repo directory and agent name on task results
Display secondary details (repository directory, agent name, last
updated) on the right side of task results in the command panel.
* fix: avoid inline selector closure to prevent getServerSnapshot loop
Select the raw repositories map from the store instead of using an
inline selector that closes over workspaceId and returns new array
references, which triggers React's getServerSnapshot infinite loop.
* feat(task): expose agent name and working directory in task DTO
Add primary_agent_name (from AgentProfileSnapshot) and
primary_working_directory (from RepositorySnapshot) to TaskDTO.
Extend the primary session info query to fetch agent_profile_snapshot
and repository_snapshot columns, and extract the new fields in the
handler layer.
* feat(ui): show agent name and working directory on command panel tasks
Replace executor name with agent name from the primary session and
display the working directory (with repository path fallback) in the
task result details.
* fix(ui): add Commands heading to separate tasks from commands in search
* fix(task): use profile display name instead of agent slug
* feat(workflow): add show_in_command_panel per-step configuration
Add a configurable boolean to each workflow step that controls whether
tasks in that step appear in the command panel (Cmd+K). Default
templates mark backlog/done steps as hidden, active steps as visible.
- Add show_in_command_panel to WorkflowStep, StepDefinition, StepPortable
- Add column with idempotent migration (DEFAULT 1 for backwards compat)
- Include field in DTO serialization and export/import round-trip
- Update all default templates (Plan & Build, Architecture, Kanban, PR Review)
- Add tests for export preservation and import round-trip
* feat(ui): wire show_in_command_panel through frontend and settings
- Add field to HTTP types (WorkflowStep, WorkflowStepDTO, StepDefinition)
- Add to kanban state types and KanbanUpdatePayload
- Map in SSR snapshot mapper and WS kanban handler
- Add "Show in command panel" checkbox to workflow step settings
- Extract StepCheckboxRow component to stay within lint limits
* feat(ui): filter command panel tasks by show_in_command_panel flag
Replace position-based heuristic (resolveExcludedStepIds) with the
per-step show_in_command_panel configuration. Tasks in steps where
the flag is false are hidden from the default Cmd+K view but still
accessible via search.
- Replace resolveExcludedStepIds with resolveVisibleStepIds
- Consolidate useInlineTaskSearchEffect params into options object
* fix(ui): differentiate task sections in command panel
Rename the inline task results heading to "Active Tasks" when showing
the default view, distinguishing it from the "Tasks" command group
that contains "Create New Task".
* fix(ui): keep auto-archive checkbox inline with other step options
* fix(ui): include show_in_command_panel in step update action
The server action was missing show_in_command_panel from its Pick type
and body builder, so the checkbox change was silently dropped.
* fix(ui): pass is_start_step and show_in_command_panel from template steps
Both normalizeWorkflowTemplate mappers (workflow-api.ts and
workspaces.ts) were only picking name/position/color/prompt/events
from backend template steps, dropping is_start_step and
show_in_command_panel. This caused all template steps to appear
with show_in_command_panel checked when creating a new workflow.
* fix(ui): fix command panel empty tasks and step update losing checkbox state
Two fixes:
- Command panel: when kanban steps haven't loaded yet, don't filter
tasks by visibility (resolveVisibleStepIds returns null for empty
steps instead of an empty Set that rejects everything)
- Workflow settings: BackendWorkflowStep type and transformWorkflowStep
were missing show_in_command_panel, so refreshing steps after an
update dropped the field and the checkbox always reverted
* fix(ui): prevent back button from stealing focus in command panel
The back button was focusable and appeared before CommandInput, so
opening file search mode focused the button instead of the input.
Add tabIndex={-1} to keep focus on the search input.
* feat(web): show archive icon for archived tasks in command panel
Replace the text badge (e.g. "COMPLETED") with an IconArchive for
archived tasks in the command panel search results. Active tasks
keep the existing IconHammer icon.
* style: fix Prettier formatting and extract useStepMaps hook
Fix Prettier formatting issues in command-panel.tsx and
workflow-pipeline-editor-panels.tsx. Extract useStepMaps hook
from useInlineTaskSearchEffect to stay within the 100-line
function limit.
* fix: address PR review feedback
- Validate keyboard shortcut payload shape before persisting (service.go)
- Normalize KeyboardShortcuts to {} on read path for legacy rows (sqlite.go)
- Deduplicate getRepoDir into getFileName (command-panel-footer.tsx)
- Show toast on shortcut persistence failure instead of swallowing (keyboard-shortcuts-card.tsx)
- Reset keyboard_shortcuts in E2E test fixture for deterministic tests (test-base.ts)
|
||
|
|
d0b5237070 | feat: improve chat input ux (#350) | ||
|
|
8ee5b56904 | feat: release notes (#298) | ||
|
|
829236f1be | feat: chat improvements (#280) | ||
|
|
61ef224ec7 | style: fix format issues (#255) | ||
|
|
0933f4375f | feat: improve stepper (#229) | ||
|
|
658bd349b5 | feat: improve workflows (#228) | ||
|
|
150c3f06c6 |
feat: session mobile design (#163)
fix: backend url |
||
|
|
510c2c56c4 | feat: Improve task approval workflow and workflow templates (#141) | ||
|
|
15e33515d2 | feat: improve task creation dialog (#135) | ||
|
|
0b573bbdda | feat(workflow): implement workflow system with steps terminology (#102) | ||
|
|
fb042e4c59 | feat: session refactor (#30) | ||
|
|
41bad65020 | feat: task_session_worktrees | ||
|
|
712610e428 | feat: rename agent_sessions to task_sessions | ||
|
|
7d0905b8cb | feat: tasks refactor | ||
|
|
004fb20618 | feat: chat improved renderer and comments pagination | ||
|
|
e965524590 | feat: task comments SSR fetch | ||
|
|
9e04b75ddf | feat: data fetching refactor ssr -> ws updates | ||
|
|
aebcf5283f | feat: display settings in kanban page | ||
|
|
5381094bba |
feat(web): enhance WebSocket handling and chat panel improvements
- Add react-markdown with remark-breaks and remark-gfm for markdown rendering - Improve WebSocket client with better reconnection handling - Enhanced task chat panel with markdown support for comments - Add comment type and metadata support for tool call rendering - Improve connection status indicator - Update task page with improved agent controls - Add WebSocket router for better message handling - Update state store with enhanced type safety |
||
|
|
a9047cf823 | feat: tasks crud | ||
|
|
47a26fcc0e | feat: web app support for boards and columns | ||
|
|
af6e482c4c | feat: web app support for workspaces | ||
|
|
f7c089d8ba | feat: ws and zustand init |