Commit Graph

132 Commits

Author SHA1 Message Date
Carlos Florêncio f97ac8bbc1 feat: streaming agent install, PTY login terminal, docker container UX (#869) 2026-05-11 17:52:05 +01:00
Carlos Florêncio 9664e13f07 fix(web): keep "All Workflows" filter on task nav and workflow.created (#854) 2026-05-08 21:23:48 +01:00
Carlos Florêncio 8c70aee27d fix(workflows): hide improve-kandev system workflow from settings UI (#842) 2026-05-07 18:42:57 +01:00
Carlos Florêncio b1cd2d2076 feat(github): rate-limit awareness, poller throttling, and GraphQL batching (#821) 2026-05-06 16:51:06 +01:00
João Salavisa 5e3248a66b feat(web): pin tasks and drag-to-reorder in sidebar (#829)
Co-authored-by: Kandev Agent <agent@kandev.dev>
2026-05-05 22:06:24 +01:00
José Almeida c64e835612 feat: allow moving tasks across workflows (#822)
* feat: allow moving tasks across workflows

* fix: address PR review feedback

* fix: address remaining PR review feedback

* fix: remove unreachable move state event

* fix: map task move conflicts correctly

* fix: address PR review nitpicks

* fix: use neutral task move error message
2026-05-05 14:13:43 +01:00
Carlos Florêncio c042050469 fix: stabilize task switch flow and session recovery (#818) 2026-05-04 18:53:33 +01:00
Carlos Florêncio 39b9369ae1 fix: suppress spurious task-failed toasts on resume and reconnect (#814) 2026-05-04 17:58:59 +01:00
José Almeida b2dcd29818 fix(web): tab session bugs — primary star drift + tab close re-creation (#800)
* fix(web): preserve primarySessionId across kanban.update broadcasts

The kanban.update WS handler rebuilt kanban.tasks without
primarySessionId / primarySessionState, wiping values set by
task.updated. After any board refresh the primary star vanished and
selectTaskWithLayout fell into the slower async fallback path. Move
task building inside setState so existing values are preserved in both
single and multi snapshots.

* fix(web): prevent deleted session panel from being re-created

handleDelete called removeTaskSession before containerApi.removePanel,
which let useAutoSessionTab re-run with effectiveSessionId still
pointing at the deleted session and re-create its panel. The
setupChatPanelSafetyNet had a matching gap — it re-added a panel for
activeSessionId without checking the session still existed.

Switch the active session (or clear it) before removeTaskSession so
effectiveSessionId updates first. Add guards to useAutoSessionTab and
the safety net so neither creates a panel for a session that is no
longer in the store.

* test(web): add session tab management regression suite

Cover the three bugs fixed in this branch end-to-end: deleting a
non-active session keeps it gone, deleting the active session switches
focus to the remainder, deleting then immediately switching tasks does
not resurrect the tab, and the primary star survives both a
kanban.update broadcast (triggered via apiClient.moveTask) and a
task-switch round trip.

* fix(web): hand off deletion to newest remaining session, document multi-snapshot fallback

- session-tab.tsx: sort remaining sessions by started_at DESC and
  pick the newest, instead of relying on the unspecified store ordering.
- kanban.ts: comment the multi-snapshot primarySessionId fallback so
  the double lookup does not look like redundancy.

Both per Claude review feedback on #800.

* fix(web): preserve explicit-null primary, abort delete on WS failure

- kanban.ts: switch the multi-snapshot fallback from `??` to an
  explicit `=== undefined` check. An explicitly-cleared null primary
  must NOT be replaced by a stale snapshot value, only an absent key.
- session-tab.tsx: wsAction now returns a boolean; handleDelete bails
  out before mutating the store / dockview when the backend
  session.delete request fails. Prevents client/server desync from a
  transient WS failure.
- kanban.test.ts: add regression covering the null-vs-undefined merge.

Per CodeRabbit and Greptile review on #800.

* style(web): drop redundant .slice() in session deletion sort chain

filter() already returns a fresh array, so the subsequent sort() does
not mutate shared store state. Per Greptile review on #800.
2026-05-03 17:32:29 +01:00
José Almeida 1394b7103f feat: add recent task switcher (#779)
* feat: add recent task switcher

* fix: address task switcher review feedback

* test: cover recent task cleanup

* perf: reduce recent task switcher storage reads
2026-05-02 22:25:40 +01:00
João Salavisa 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 9e8079de). The
   collapsed-by-default tradeoff is genuine, but it broke 14+ E2E tests
   that assert on commit text in the changes panel — the win wasn't worth
   the test churn. Updated the unit test to match.

3. Update the git-changes-panel E2E tests to use the stable
   `[data-testid^="commit-row-"]` selector instead of `commitsList li`.
   The chip-style refactor introduced a per-repo group <li> wrapper, so
   counting all <li> descendants now over-counts (1 wrapper + N commits).
   Same fix in the "distinguishes pushed and unpushed" test which queried
   `li` with text and matched both the wrapper and the commit row.

Locally verified: full shard 2 (45 tests) passes; full unit-test suite
(943 tests) passes; lint + typecheck clean.

* fix: address claude review feedback

Three findings from claude[bot] review on PR #767:

- types.ts: add 13 unit tests for reviewFileKey / splitReviewFileKey
  covering bare paths, composite keys, slashes/spaces in paths, repo
  names containing slashes, and full round-trip. Asks were repeated
  across five review rounds; the round-trip coverage is what catches
  the FILE_KEY_SEP NUL-vs-space class of bug at the type level.

- mergeCumulativeFiles (api/git.go): shallow-copy each per-file payload
  before stamping repository_name + path so the source map isn't
  mutated. The previous code wrote directly into the per-repo result
  map, which would have permanently rewritten the original entry if
  any future caller iterated the same source twice.

- buildRepoSpecs (executor_execute.go / executor_resume.go): drop the
  unused execCfg parameter (and the speculative "reserved for future
  per-repo config" suppression). Per repo overrides do not exist
  today and the speculative scaffolding masked an actual dead arg.

* docs(readme): mention multi-repository task support

* fix(web/topbar): drop standalone ahead/behind badges (duplicate of split button)

The Create PR / Push button already shows the inline ↑N ↓N counts, so
the separate "3 ahead" / "2 behind" badges next to the PR icon were
just visual noise. Removes the GitAheadBehindBadges component (now
unused) and the gitStatus / isMultiRepo prop chain that fed it.

* fix(github): heal legacy repository_id='' rows on github_task_prs

A task that pre-dated the multi-repo schema kept its github_task_prs row
with repository_id=''. After the schema migration, a subsequent sync
would resolve the task's primary repository_id and CreateTaskPR with the
new value -- but GetTaskPRByRepository(task, repo) returned nil because
the legacy row had an empty repository_id, so the new code inserted a
duplicate row instead of updating the legacy one. Net effect: two rows
for the same PR, surfacing as a "+2" PR badge on a single-repo task.

Adds backfillTaskPRsRepositoryID, run on schema init right after
migratePRTablesForMultiRepo:

  1. Dedup pass: when a (task, owner, repo, pr_number) tuple has both a
     legacy (repository_id='') and a per-repo row, drop the legacy one.
  2. Backfill pass: any remaining empty-repo rows get stamped with the
     task's primary repository_id from task_repositories.

The backfill is gated on task_repositories existing -- github-store unit
tests init only this package's schema, and the dedup pass alone is the
load-bearing fix for the symptom.

Tests cover the dedup path, the cross-package-skip path, and idempotency.

* test(github): cover UPDATE backfill path for orphan legacy rows

Adds TestBackfillTaskPRsRepositoryID_BackfillsOrphanRow per claude
review feedback. The two prior tests cover the DELETE dedup path and
the early-exit when task_repositories is absent, but the UPDATE path
that stamps the primary repository_id onto an orphan legacy row had
no coverage -- a wrong column name or off-by-one WHERE would have
slipped through silently.

* feat(web/changes-panel): group PR Changes by repository

Multi-repo tasks now have one TaskPR per repo, but the PR Changes
section was still rendering files flat using only the primary PR.
Adds useActiveTaskPRsWithFiles, which fans out one WS request per
TaskPR and tracks each fetch by (owner/repo/pr_number/last_synced_at)
so a server-side sync invalidates the cache.

Each PR's files are stamped with the resolved repository_name (looked
up from the workspace repos slice), and PRFilesGroupedList groups by
that name -- mirroring how Commits and Changes already render. Single-
repo tasks (one group, empty name) still render flat.

Mechanics:

- PRChangedFile gains an optional repository_name.
- mapPRFilesToChangedFiles takes a repository_name and stamps it.
- New PRFilesGroupedList + PRFilesRepoGroup live in
  changes-panel-pr-files.tsx so changes-panel-timeline.tsx stays
  under the 600-line cap; PRFilesSection delegates to them.
- Hook uses refs (not state) for the in-flight / fetched dedup so
  setState is only called when results arrive.
- Stable EMPTY_PRS sentinel avoids the new-array-each-render trap
  that would re-trigger every effect downstream.
- Workspace repo lookup memoised once via useMemo so the merged file
  list isn't rebuilt on every render.

* fix(web/changes-panel): show per-repo PR header when task is multi-repo

Previous logic only stamped repository_name on PR files when prs.length > 1
(2+ PRs). For a multi-repo task with only ONE PR yet (one repo doesn't
have a branch / hasn't been pushed), no per-repo header rendered, even
though STAGED and COMMITS already grouped under their repo names. Now
gates the stamp on the task itself being multi-repo, matching how the
other sections decide.

* fix(executor): serialise concurrent LaunchPreparedSession per session

Two paths in task_operations.go call LaunchPreparedSession on the same
session_id:

  1. PrepareTaskSession spawns a background goroutine with launchWorkspace=
     true the moment a session is created (so file browsing works before
     the user clicks Start).
  2. StartTaskWithSession runs a foreground launch when the agent is
     actually started (workflow auto-start, user click).

Without serialisation both reach agent_manager.LaunchAgent in parallel.
The fast-path check at the top of LaunchPreparedSession (session.Agent
ExecutionID != "") only kicks in once the first call has *persisted*
its execution id; the second call grabs a stale snapshot, runs the full
env-prep + builds runtime, then loses the executionStore.Add race and
fails with "already has an agent running (race resolved during register)".
Visible in the UI as "Environment setup failed".

Multi-repo amplifies the window because env prep runs sequentially per
repo (~2x the time for two repos), so what was a rare flake on
single-repo became consistent on multi-repo tasks.

Fix: acquire the per-session lock (the same sync.Map used by the resume
path) at the entry of LaunchPreparedSession and re-fetch the session
under the lock so the fast-path check sees the AgentExecutionID the
first holder just persisted. Second caller now takes the existing-
workspace path and starts only the agent subprocess, matching the
intended single-launch semantics.

Adds TestLaunchPreparedSession_SerialisesConcurrentLaunches that fires
two concurrent LaunchPreparedSession calls with the first one blocked
inside LaunchAgent. Without the lock, both reach LaunchAgent and the
test fails with call_count == 2; with the lock only the first does.

* fix(orchestrator): per-repo push detection so each repo's PR gets associated

Multi-repo tasks would only get one PR detected per task: the agent
pushes branches in BOTH repos, but the pushTracker keyed by sessionID
alone meant agentctl's per-repo git status events overwrote each
other's ahead counts. Whichever repo's events interleaved unfavourably
lost its >0 -> 0 transition and silently dropped its push detection.
Net effect: only the first/last repo's PR got an associated TaskPR row;
the other PR existed on GitHub but Kandev never knew about it.

Fix:

- pushTracker is now keyed by "<sessionID>|<repository_name>" so each
  repo's transitions are tracked independently. Empty repository_name
  preserves the legacy single-repo behaviour.
- trackPushAndAssociatePR forwards data.Status.RepositoryName to
  detectPushAndAssociatePR.
- detectPushAndAssociatePR resolves owner/name/repository_id for the
  specific repo via new resolvePushRepo helper (looks up the task's
  task_repository whose repository.Name matches the event), then uses
  GetPRWatchBySessionAndRepo to scope the existing-watch check.
- New associatePRFromPushScoped takes the per-repo repository_id
  directly instead of always resolving the primary one. CheckSessionPR
  (manual "check now") wired through the same scoped helper.

Existing tests pass with the explicit empty repository_name argument.
The user's existing task with the missing kandev PR row needs a manual
push retrigger or DB heal — the migration only deduped legacy empty
rows; this fix prevents the gap from recurring on future tasks.

* feat(web): one PR button per repo in topbar + per-PR detail panels

PRTopbarButton previously rendered just the primary TaskPR via
useActiveTaskPR(), so multi-repo tasks with N PRs only surfaced one.
Switch to useTaskPR(activeTaskId).prs and render one button per PR --
each opens its own dockview panel keyed by owner/repo/pr_number.

Plumbing:

- dockview-store / dockview-panel-actions: addPRPanel(prKey?: string)
  builds id "pr-detail|<key>" when a key is supplied; the legacy
  single-repo "pr-detail" id stays for the auto-add path.
- dockview-desktop-layout: forwards `params` to PRDetailPanelComponent
  so the panel can receive its prKey.
- pr-detail-panel: new prTaskKey helper + the panel looks up the
  matching TaskPR from the per-task list when params.prKey is set,
  falling back to useActiveTaskPR() for legacy panels and tooltips.
- pr-topbar-button: rewritten to render a button per PR, each labelled
  with #pr_number and tooltipped with owner/repo + title; click opens
  the per-PR panel.

* feat(web): collapse multi-PR topbar to a dropdown + show every PR in the + menu

Two follow-ups on top of per-PR panels:

- The dockview "+" panel-add menu still listed only state.pr (the primary
  TaskPR), so a 2-repo task only offered "PR #2" -- never the kandev
  one. Switch to state.prs[] and render one menu item per PR; multi-repo
  rows are labelled "PR #N -- repo" so the user can tell them apart.
  Each item opens its own panel via addPRPanel(prTaskKey(pr)).

- The topbar grew one outline button per PR after the previous commit,
  which doesn't scale (3+ PRs would crowd out other actions). Single PR
  keeps the existing single-button rendering; 2+ collapses to one
  outline button labelled "N PRs" with the aggregated status colour
  (red if any failing, etc.) and a dropdown listing each PR with repo,
  number, title, and per-PR status icon. Click an item -> open that
  PR's panel.

useLeftHeaderState now reads useTaskPR(taskId).prs and forwards the
full list through AddPanelMenuState as `prs`.

* fix(web/changes-panel): PR files arrive after race-cancelled fetch

useActiveTaskPRsWithFiles dropped responses from the previous effect
instance via a `cancelled` flag, but since the dedup tracking refs
(inFlightRef / fetchedRef) carry across renders, the next effect's
early-continue still saw the key in inFlightRef and skipped a fresh
request. The ref then got populated by the cancelled response and
fetchedRef permanently locked the key out of any future fetch.

Net effect: every render after the first prs reference change left
the merged prFiles array empty -- the PR Changes section never showed
up on multi-repo tasks even though both PRs existed and the WS request
would have returned files.

Drop the cancellation flag entirely. Per-key dedup is enough to prevent
duplicate requests, and the .then handlers use functional setState so
they're safe to land after the effect re-runs.

* test: cover review-feedback gaps (mapPRFilesToChangedFiles, push multi-repo, lock test channel-sync)

Three follow-ups from claude review on PR #767:

1. apps/web/components/task/changes-panel-helpers.test.ts: 5 new tests
   for mapPRFilesToChangedFiles. Covers status mapping (added / removed /
   renamed / modified / fallback), forwarding of plus/minus/oldPath, and
   the new repository_name stamping behaviour (multi-repo path stamps
   every row; single-repo path defaults to "" so PRFilesGroupedList
   skips per-repo sub-headers).

2. event_handlers_github_test.go: TestDetectPushAndAssociatePR gains
   two multi-repo cases. resolvePushRepo must scope CreatePRWatch +
   AssociatePRWithTask to the repository_id matching the event's
   repository_name (not silently fall back to primary); unknown
   repository_name must drop the event rather than mis-associate.
   mockGitHubService captures lastCreateWatchRepositoryID /
   lastAssociateRepositoryID so the per-repo scoping is asserted
   directly.

3. executor_test.go: TestLaunchPreparedSession_SerialisesConcurrentLaunches
   replaces the time.Sleep(50ms) coordination with a buffered `entered`
   channel + bounded select wait per CLAUDE.md. The test now waits for
   the first launch to enter LaunchAgent, then asserts no second entry
   arrives within a 100ms window — without the lock the second
   goroutine reaches LaunchAgent in microseconds, so the timeout serves
   as the "didn't race in" oracle, not a synchronisation primitive.

* fix(orchestrator): detect push when first poll lands post-sync

trackPushAndAssociatePR only fired on a >0→0 ahead transition, which
required two distinct status events to be observed. In multi-repo tasks,
any repo whose first agentctl status poll happened to land after the
push had already completed (ahead=0 with remote branch set on the very
first observation) silently dropped the push event — the watch was
never created, the PR never associated. Reproduced on task
4fdff41b-095a-4158-a311-4a1a23abe064 where 2/3 repos pushed but only
one PR appeared.

Now also fire on first observation when the status already reflects a
post-push state (ahead=0 + RemoteBranch set). detectPushAndAssociatePR
is idempotent against an existing watch, so a spurious fire on a
worktree that started from an already-synced remote just costs one
GitHub FindPRByBranch call.

The deferred pushTracker.Delete in detectPushAndAssociatePR is removed:
keeping ahead=0 stored is what suppresses repeated firing on subsequent
status events. Cleanup now happens at session-delete time via a new
pushTrackerForget helper that walks all (session|repo) entries.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* fix(web/topbar): restore pr-topbar-button testid for single-PR case

The multi-PR refactor renamed PRSingleButton's testid from
pr-topbar-button to pr-topbar-button-${pr_number}, breaking E2E tests
that relied on the stable selector (task-topbar-overflow.spec.ts and
the prTopbarButton page-object helper used by vcs-button-context).

Restore the canonical testid and expose pr_number via data-pr-number
for any test that needs to discriminate. PRMultiButton already uses
the same testid; with at most one PR-area control rendered at a time,
sharing the testid is unambiguous.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* fix(orchestrator): create one PR watch per repo on multi-repo tasks

The orchestrator only ever ensured a watch for a task's PRIMARY
repository. Both call sites that create watches were primary-only:

  - ensureSessionPRWatch (called when a session starts/resumes) used
    resolvePrimaryTaskRepositoryID and made one EnsurePRWatch call.

  - buildTaskBranchList (used by the poller's reconciler) keyed dedup
    by sessionID alone, so as soon as the primary repo got a watch
    the whole session was marked "watched" and the poller skipped
    every other repo.

Result: in a multi-repo task, the secondary repos never got a watch
row, so the periodic GitHub poller never searched for their PRs and
never associated them. Push detection (which fires on >0→0 ahead
transitions vs origin/main) doesn't help here either because Ahead is
measured against the integration branch, not the upstream tracking
branch — pushing doesn't change Ahead.

Reproduced on task a7a59bd8: PRs existed on github for both kandev
and aistats, but only the primary (kandev) was associated.

Refactor:

  - resolveSessionWatchTargets walks task_repositories × session
    worktrees and returns one (repo_id, owner, repo, branch) target
    per repo. Branch priority preserves the legacy precedence
    (checkout_branch > primary fallback > worktree branch) so PR-
    review-style synthetic worktree names don't clobber the real one.

  - ensureSessionPRWatch loops over targets and ensures one watch per
    repo.

  - buildTaskBranchList emits one TaskBranchInfo per (session, repo)
    and dedups against (session_id, repository_id) pairs from
    ListActivePRWatches — replacing the old session-only dedup that
    suppressed non-primary repos.

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>
2026-05-02 20:26:42 +01:00
José Almeida 45ee2ff48f feat: key terminals + dockview layout to TaskEnvironment (#755)
* feat(backend): key user shells by TaskEnvironmentID

Sessions in the same task share a TaskEnvironment. Re-key the user shell
runner so terminals are scoped per env, not per session — switching between
sessions of the same task now sees the same shell list.

- Add TaskEnvironmentID to AgentExecution / LaunchRequest / ExecutorCreateRequest
  / executor.LaunchAgentRequest, propagated through ToAgentExecution and the
  orchestrator launch path.
- Add Manager.ResolveScopeKey(sessionID) — returns env when execution carries
  it, falls back to sessionID for legacy paths.
- Rename runner param sessionID -> scopeID in interactive_shells.go; map keys
  are now scopeID:terminalID.
- Shell handlers + WS terminal handler resolve sid -> scopeID via
  ResolveScopeKey before calling the runner.

* feat(web): key dockview layout + portals to TaskEnvironmentID

Switching between sessions of the same task no longer rebuilds the dockview
layout — terminals and panels persist because layouts are env-scoped, not
session-scoped. The bottom terminal also reuses a stable sessionId across
same-env switches so it doesn't reconnect to a different shell.

- Rename layout persistence: setSessionLayout/getSessionLayout/*SessionMaximizeState
  -> setEnvLayout/getEnvLayout/*EnvMaximizeState (kandev.dockview.env-layout. prefix).
- Rename dockview-session-switch.ts -> dockview-env-switch.ts; performSessionSwitch
  -> performEnvSwitch with oldEnvId/newEnvId/activeSessionId.
- dockview-store.ts: currentLayoutSessionId -> currentLayoutEnvId; switchSessionLayout
  -> switchEnvLayout(oldEnvId, newEnvId, activeSessionId). Same-env switches no-op.
  Drop _skipLayoutSwitchForSession + consumeSkipFlag — env-keyed action handles it
  structurally.
- panel-portal-manager: PortalEntry.envId; releaseBySession -> releaseByEnv.
  ENV_SCOPED_COMPONENTS in dockview-desktop-layout (renamed from SESSION_SCOPED).
- useEnvSwitchCleanup replaces useSessionSwitchCleanup; tracks env changes only.
- All performLayoutSwitch callers (sidebar, dropdown, header, dialog) resolve
  sid -> envId via environmentIdBySessionId before switching.
- bottom-terminal-panel: drop sessionId prop, use useEnvironmentSessionId().
- cleanupTaskStorage now takes envIds[] alongside sessionIds[] for env-scoped
  layout/maximize cleanup.

* test: add coverage for env-keyed terminals + layout

Playwright e2e (terminal-env-keyed.spec.ts) — switching between sessions of
the same task keeps terminal scrollback intact and asserts both sessions
share one task_environment_id. Update the comment in
sessionless-task-switch.spec.ts to reference performEnvSwitch.

* fix: address env layout PR feedback

* test: fix flaky handshake auth check

* fix: address env scope review findings
2026-05-01 11:00:33 +01:00
José Almeida fcc7ceb8b6 fix(approvals): drop stuck pending permission_request messages (#723)
* fix(web): exclude permission_request from turn-complete safety net

The session.turn.completed handler iterated over messages and forced
metadata.status="complete" on anything carrying tool_call_id. Permission
prompts also carry tool_call_id (for FE pairing with their tool_call),
so this loop overwrote the user's "approved" decision on every prompt
in the turn — and the FE component re-rendered the Approve/Deny buttons.

* fix(backend): protect permission_request rows from tool_call sweeps

Three permission_request invariants the prior code violated, mirroring
the FE bug fix:

- CompletePendingToolCallsForTurn: SQL UPDATE filtered by tool_call_id
  presence, also catching permission_request rows. Add type filter so
  the safety net only touches actual tool messages.
- GetMessageByToolCallID: returned the oldest row carrying that
  tool_call_id, which races to the permission_request when timing
  shifts. Exclude permission_request type so a tool_update can never
  land on the user's approve/reject row.
- applyToolCallMessageUpdate: defensive guard that refuses to overwrite
  a permission_request, making the invariant explicit at the layer that
  does the writing.

Adds a regression test for CompletePendingToolCallsForTurn covering
approved/rejected/pending permission rows.

* test(e2e): cover approval-reappear regression with mock-agent scenario

Adds a multi-permission mock-agent scenario (three sequential bash
tools, each requiring approval) and an E2E test that approves all
three and asserts no permission action row remains visible after
the agent's turn ends, and that the resolved state survives a reload.

Supporting test infrastructure:
- data-testid hooks on PermissionActionRow.
- permissionActionRows() / permissionApproveButtons() on SessionPage.
- backend fixture force-disables AGENTCTL_AUTO_APPROVE_PERMISSIONS=false
  so a developer's local AUTO_APPROVE=true env doesn't silently bypass
  the permission UI in tests that exercise the approve/reject flow.

* test(backend): cover GetMessageByToolCallID permission_request exclusion

Addresses claude-review feedback on the approvals fix:
- Adds a regression test that seeds both a permission_request and a
  tool_call with the same tool_call_id (permission_request created
  first, so an ORDER BY created_at ASC sweep without the type filter
  would race to it) and asserts the lookup returns the tool_call row.
- Bumps the applyToolCallMessageUpdate invariant guard from Warn to
  Error: reaching that branch means a caller bug, not a transient
  condition, and Warn would silently swallow the violation.

* fix(e2e): scope auto-approve override to permission-approval test only

The previous fixture force-disabled AGENTCTL_AUTO_APPROVE_PERMISSIONS
globally, which broke session-isolation.spec.ts and other tests that
use scenarios with permission-required tools (e.g. /e2e:read-and-edit)
and rely on auto-approve to let the agent finish without UI clicks.

Default the env to "true" in the fixture; let permission-approval.spec
flip it to "false" via a backend.restart() override in beforeAll, then
restore in afterAll. Adds an envOverrides parameter to backend.restart.
2026-05-01 00:25:51 +01:00
João Salavisa 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>
2026-04-30 18:17:36 +01:00
José Almeida 12bd5b2cf3 feat: plan checkpointing with rewind UI (#694)
* feat(backend): plan revision history with checkpoint/revert

Persist every plan edit as an immutable TaskPlanRevision with author
attribution (agent vs user) and expose WS actions to list, fetch, and
revert to past revisions. Emits task_plan.revision.created and
task_plan.reverted events so the UI can react in real time.

* feat(web): plan revision history UI with rewind to past versions

Add a rewind button to the plan panel that opens a revisions list
showing each checkpoint with author (agent name or user) and timestamp,
and lets the user restore any past revision as the new head. Plan
state, hooks, and WS handlers are extended to load, cache, and react
to revision-created/reverted events.

* test(web): e2e coverage for plan checkpointing and rewind

Cover the full revision flow: plan edit creates a checkpoint,
the revisions panel lists authors and timestamps, and reverting
restores the selected revision as the new head. Extends session
page object and backend fixtures with revision-related helpers.

* fix(web): stabilize plan revisions state and avoid re-render loop

Zustand selector returned a fresh [] for missing revisions, fanning out
re-renders that tripped React's infinite-loop guard when the plan panel
mounted. Swap to a frozen empty-array sentinel, auto-load revisions on
mount so WS events fired before connect aren't lost, and gate the
popover's on-open fetch so reopening doesn't re-sort rows mid-click.

* test(web): make plan checkpointing e2e resilient to agent auto-layout

Agent-authored plan writes auto-open the plan panel and can push the
chat tab inactive, so the chat idle placeholder isn't always visible
once the agent finishes. Accept either chat or plan panel as the idle
signal, make openPlanPanel and openRewind idempotent, reopen the
popover after a revert cancel (the confirm dialog closes it), and drop
the toast assertion since no Toaster is mounted in the app.

* fix(backend): atomic plan revision write + review polish

- Add `WritePlanRevision(ctx, head, rev, coalesceLatestID)` repository method
  that wraps HEAD upsert + MAX+1 compute + insert/merge in a single write tx,
  closing the TOCTOU race on `revision_number` and partial-failure divergence
  between HEAD and history. `upsertPlan` and `RevertPlan` both route through it.
- Add `ErrRevisionIDRequired`; return it for empty `TargetRevisionID` (was
  previously conflated with `ErrRevisionNotFound`). Handler maps to validation.
- Backfill fallback `author_kind` changed `"user"` → `"agent"` to match
  `CreateTaskPlan` and the column default.
- Extract shared `revisionSelectCols` constant; rename `max` local (shadowed
  Go builtin) and `dto` local (shadowed package name).

* fix(web): address plan-revisions review feedback

- `mcpWrite` uses `JSON.stringify(content).slice(1, -1)` to escape backslashes,
  quotes, and control chars in one pass — closes the CodeQL finding.
- `clearTaskPlan` now evicts `revisionContentCache` entries for the task's
  revisions before dropping the maps, preventing orphaned cache growth.
- `RevertConfirmDialog` wraps `onRevert` in try/catch/finally so rejections
  surface a toast and the dialog always dismisses.
- Hoist `TaskPlanRevision` import in `task-plan-panel.tsx`.
- `RevisionRow` derives relative time from props every render with a 30s tick,
  so prop changes refresh immediately.
- Strengthen author-switch e2e with `.not.toBeEmpty()` to also reject empty or
  truncated renders.

* fix(web): evict revision content cache on coalesced update

Coalesced writes update an existing revision's content on the backend, but
the WS `upsertPlanRevision` payload carries only metadata. Without eviction,
a cached preview of the revision stays stale after the agent re-writes the
plan within the coalesce window.

* fix(web): plan history UI polish from local testing

- Drop the redundant "Plan" title in the panel header bar; the dockview/mobile
  tab already labels the panel.
- Author display: show "You" with the user icon for user-authored revisions,
  and the agent profile name with the AgentLogo (resolved from the active
  session's `agent_profile_snapshot.agent_name`) for agent revisions, matching
  the icon used elsewhere in the app.
- Replace ScrollArea with a plain `max-h-96 overflow-y-auto` div so the
  popover stays bounded when revision counts grow past ~7 rows.
- Drop the em dash from the revert dialog description.

* feat(web): add lineDiff helper for plan revision compare

Tiny wrapper around the `diff` package that classifies each line as add /
remove / context and strips the trailing-newline phantom entry. Also exposes
`diffSummary` for the dialog header counts. Pure functions; backed by Vitest.

* feat(web): plan revision preview + compare state

Adds two task-scoped store keys for the new dialogs:

- `previewRevisionIdByTaskId` — at most one open preview per task; cleared on
  `clearTaskPlan`.
- `comparePairByTaskId` — ordered 2-slot pair. `toggleComparePair` reuses a
  `nextPair` helper that handles unselect (already-picked id), fill (open
  slot), and FIFO eviction (full pair). `clearTaskPlan` resets it too.

* feat(web): plan revision preview + diff dialogs

- `useTaskPlan` exposes `previewRevisionId`, `setPreviewRevision`,
  `comparePair`, `toggleCompareSelection`, `clearComparePair` (extracted into
  `usePreviewCompareState` to keep the surrounding hook within the size cap).
- New `PlanRevisionPreviewDialog`: read-only modal for a single revision with
  Restore + "Compare with current" CTAs. Restore routes through the existing
  RevertConfirmDialog so the confirmation copy stays consistent.
- New `PlanRevisionDiffDialog`: line-level unified diff between two revisions
  with adds/removes highlighted; orders the pair by revision_number so the
  older one is always "before"; Restore targets the older side.
- Popover row split into a clickable body (opens preview), a Compare toggle
  (icon button per row), and the existing Revert button. Compare chips appear
  at the top of the popover with a Diff CTA when both slots are filled.
- Both dialogs are remounted via key on revision/pair change so loader state
  resets without ESLint-flagged setState-in-effect resets.

* test(web): e2e coverage for plan preview and compare-diff dialogs

Extends the SessionPage POM with locators for the new preview and diff
dialogs (chips, compare toggle, restore buttons, body + summary). Adds two
scenarios in plan-checkpointing.spec.ts:

- Click a revision row body → preview shows the right content; the Restore
  CTA opens the existing confirm dialog and the revert lands.
- Toggle compare on two revisions → diff opens with v1 → v2 ordering and
  shows at least one add and one remove line; the Restore CTA targets the
  older side.

* feat(web): render markdown in plan preview + diff dialogs

Adds a shared `PlanReadOnlyMarkdown` Tiptap renderer that reuses the same
StarterKit + tiptap-markdown + tables/tasks/code-block extensions as the live
plan editor (minus interactive bits — slash menu, drag handles, comment
marks). Wired into both dialogs:

- Preview replaces the raw `<pre>` block with the rendered Tiptap view.
- Diff dialog now stacks two rendered sides (Before / After) with red/green
  tinted borders, kept above the existing line-level breakdown so the user
  gets both rendered comparison and an exact textual change list.

Tiptap doesn't ship a free diff/track-changes extension (it's part of Tiptap
Pro), so the side-by-side rendering plus the line-level list together stand
in for that.

* fix(web): plan diff dialog — text-only with unified/split modes

Following local feedback:

- Drop the rendered-markdown side-by-side panes from the diff dialog. Diff
  view is text-only again, like a code review.
- Add a Unified / Split mode toggle. Split mode aligns each remove with the
  adjacent add on the same row (single-line edits stay paired) and stripes
  empty cells where one side has nothing.
- Fix the highlight-truncation bug: long lines used to lose their tinted
  background past the viewport on horizontal scroll because the row width
  was capped to the visible area. Rows now use `min-w-full w-max` so the
  background extends through the full scroll width.

Preview dialog keeps the rendered Tiptap markdown — only the diff reverted
to plain text.

E2E covers both modes and the cell kinds.

* feat(web): formatPreciseTime util for minute-level timestamps

Returns 'just now', 'Xs ago', 'Xm ago' for the first hour, then 'Today, HH:MM',
'Yesterday, HH:MM', or '{Mon} {DD}[, YYYY], HH:MM' for older entries — keeps
exact minute-of-day visible instead of decaying to 'yesterday'/'2d ago'.

* fix(web): render markdown via setContent in plan read-only renderer

The tiptap-markdown extension's parser only fires on `setContent` calls, not
on the initial `content` option. Passing `content: value` to `useEditor` was
treating the markdown source as raw text, so the preview dialog rendered
unparsed markdown.

Drop the `content` option and instead call
`editor.commands.setContent(content, { emitUpdate: false })` from a useEffect
once the editor is created — which is what the live plan editor effectively
relies on through tiptap-markdown's setContent override.

* fix(web): plan history UX — bigger modals, split layout, simpler compare

- Bump dialog widths: preview now `max-w-5xl w-[92vw]`, diff
  `max-w-6xl w-[95vw]`, both with taller max-height. Plenty of room for
  rendered markdown and side-by-side diffs.
- Replace the table-based split layout with a CSS grid (`grid-cols-2 w-max`).
  The table-fixed cells were overflowing into each other when content was
  wider than half the dialog; the grid lets the natural width drive a single
  horizontal scrollbar on the parent and keeps the two columns properly
  separated.
- Switch row timestamps to `formatPreciseTime` so users see minute-of-day
  ("Today, 14:32", "Yesterday, 09:14", "Apr 21, 11:08") instead of decaying
  to "yesterday".
- Simplify the compare flow: drop the per-row Compare toggle, the chip strip
  at the top of the popover, and the standalone "Diff" button. Compare is
  now a side effect of preview — open any version, then click "Compare with
  previous" or "Compare with current". E2E test updated to use the new path.

* test(web): update plan checkpointing e2e for the new compare flow

Drops the locators for the row Compare toggle, chips, and standalone Diff
button (those UI bits are gone). Adds a locator for the new
"Compare with previous" preview button. The compare scenario now opens the
diff via preview → "Compare with current" instead of the old chip flow.

* fix(web): preview renderer reuses live editor's markdown styles

`globals.css` keeps heading sizes / list spacing / code-block padding under
.markdown-body and .tiptap-plan-wrapper, and the live editor opts in by
applying both classes (plus a conditional .dark for theming). The read-only
renderer was using `prose prose-sm` instead, which doesn't actually take
effect on the .ProseMirror node — leaving everything the same size.

Switch the read-only renderer to the same class set, follow the live
editor's `useTheme` -> `dark` pattern, override `!h-auto` so the renderer
sizes to its content, and trim the editor's default 0.75rem padding inside
the dialog body.

* fix(web): split diff has fixed width with synced horizontal scroll

The previous split layout grew the whole dialog wider when content overflowed
which felt wrong. Now:

- The dialog stays at its fixed width.
- Each pane has its own `overflow-x-auto` so each side is capped at half the
  dialog and gets its own horizontal scrollbar.
- A scroll listener keeps both panes' `scrollLeft` in lockstep (with a
  re-entrancy guard so the bounce-back doesn't form a loop) — corresponding
  rows stay aligned visually no matter which side the user scrolls from.
- The diff body's overflow-x is hidden in split mode (the panes own it) and
  only overflow-y stays on the body so vertical scroll covers both panes
  at once.

* feat(backend): resolve agent profile name from session for plan revisions

The MCP-side handlers (handleCreateTaskPlan / handleUpdateTaskPlan) don't
carry the agent's display name in the request, so without a lookup every
agent-authored revision was rendering as the literal "Agent" fallback in
the history UI.

PlanService now looks up the task's session and reads the agent profile
display name from `agent_profile_snapshot` (preferring "label", then
"name", then "agent_display_name") when the request is an agent write
without an explicit author_name. Active session is tried first; falls
back to the most recent session by started_at so plans written between
turns still get the right author name. If no usable session/snapshot
exists, the existing "Agent" fallback applies.

The repo dependency type is widened from `repository.PlanRepository` to a
small private `planRepo` interface that adds the two session lookups —
the SQLite repository (`*sqliterepo.Repository`) already implements both,
so no caller wiring changes.

* fix(web): plan history popover row + restore button polish

- Popover content's default `gap-4` between flex children (from
  @kandev/ui's PopoverContent) was rendering as visible space above the
  top-most row. Override with `gap-0` so the list sits flush against the
  header.
- Switch row layout from `items-start` to `items-center` so the
  Restore button stays vertically centered against the row body even
  when the body grows taller (timestamp + restored-from marker on revert
  rows).
- Rename the per-row "Revert" button to "Restore" and prefix it with
  IconRestore so it matches the dialog wording and the restored-from
  marker icon. The data-testid stays `plan-revision-revert-button` for
  e2e selector stability.

* fix(backend): plan revision review feedback + lint

- `wsGetTaskPlanRevision` now returns a 400 validation error when
  `revision_id` is empty, matching `wsRevertTaskPlan`'s
  `ErrRevisionIDRequired` branch (was silently mapped to 404 via the
  service's lookup miss).
- `agentDisplayNameFromSnapshot` tries `name` first, then `label`, then
  `agent_display_name`. The orchestrator's canonical key wins over any
  stale older `label` that may still be in a session snapshot.
- Add `idx_task_plan_revisions_task_number` on
  `(task_id, revision_number DESC)`. `GetLatestTaskPlanRevision` runs on
  every plan write (coalesce check); the existing `(task_id, created_at)`
  index doesn't help since these queries order by `revision_number`.
- Drop `Deprecated:` from `NextTaskPlanRevisionNumber`'s doc comment so
  staticcheck stops flagging the existing test usage. The TOCTOU caveat
  stays in the prose.

* fix(web): plan revision review feedback, prettier, e2e resilience

- `loadRevisionContent` reads `revisionContentCache` lazily through the
  store API inside the callback, so its identity stays stable. Selecting
  the cache map at the hook level was recreating the callback whenever
  any task's content was cached, retriggering downstream content-fetch
  effects.
- `task.plan.reverted` WS event becomes a no-op handler.
  `task.plan.revision.created` fires for the same row first and already
  upserts; the duplicate handler was double-evicting the
  `revisionContentCache` entry and triggering an extra Zustand update.
- e2e: agent author assertion now uses `not.toHaveText("You")` +
  `not.toBeEmpty()` instead of the literal `"Agent"` (which only
  matched the legacy fallback — agent rows now resolve to the profile
  display name).
- e2e: `openPlanPanel` retries the toggle a few times after a page
  reload — the panel toggle can race hydration and silently no-op,
  which broke the persistence test.
- Apply prettier to the files flagged by the formatting check.

* fix(plan): toast on silent revert failure + revision ownership check

- task-plan-revisions: when `onRevert` (the hook's `revertTo`) swallows
  the error and returns null, the dialog used to close silently with no
  feedback. Add an `else` branch that fires `toast.error("Failed to
  restore plan")`.
- wsGetTaskPlanRevision: accept an optional `task_id` and reject with a
  400 "Revision does not belong to task" when the loaded revision's
  task doesn't match. Mirrors `wsRevertTaskPlan`'s ownership check so a
  WS client can't read content for an arbitrary revision UUID outside
  the tasks it already holds a reference to.
- plan-api `getPlanRevision` takes optional `taskId`; `loadRevisionContent`
  threads the active task id so the check is exercised on the happy path.
2026-04-27 10:22:58 +01:00
Carlos Florêncio d431688557 feat(web): auto-open plan panel with unseen-changes indicator (#650) 2026-04-25 18:21:33 +01:00
Carlos Florêncio de040a8987 fix: gateway auth injection and cumulative diff error handling (#682) 2026-04-23 18:31:00 +01:00
Carlos Florêncio 7076a40805 chore: change default ports to avoid common conflicts (#681) 2026-04-23 18:09:53 +01:00
José Almeida c078438621 refactor: unify task.updated via single publisher and shared mapper (#676) 2026-04-23 15:16:31 +01:00
Carlos Florêncio 2d1a868107 feat: configurable CLI flags per agent profile (#653) 2026-04-20 23:42:14 +01:00
Carlos Florêncio 31140c8b9f fix(web): follow workflow step session switches in chat UI (#625) 2026-04-20 09:26:53 +01:00
Carlos Florêncio 1f7e4dbb5e fix: stream setup script output and keep prepare panel on failure (#607) 2026-04-19 11:08:18 +01:00
Carlos Florêncio 5b04c9c675 perf: focus-gated git polling to reduce CPU on retained worktrees (#610) 2026-04-18 11:33:04 +01:00
Francisco Magalhães 73e58729ba fix: apply display filters in list view (#612) 2026-04-17 23:12:45 +01:00
Carlos Florêncio a222854422 feat: associate agent profiles with workflows and steps (#597) 2026-04-16 10:14:45 +01:00
José Almeida 12613130fa fix(web): prevent session failure toast from re-appearing after dismiss (#591)
* fix(web): prevent session failure toast from re-appearing after dismiss

* fix: address PR review feedback — add tests and restore JSDoc
2026-04-15 14:24:12 +01:00
José Almeida c08d68c0fe refactor: re-key dockview panel state by environmentId instead of sessionId (#491)
* refactor: re-key dockview panel state by environmentId instead of sessionId

- Remove terminal from SESSION_SCOPED_COMPONENTS (uses useEnvironmentSessionId internally)
- Convert ChangesContent and file-editor-panel to useEnvironmentSessionId()
- Re-key sessionCommits store from bySessionId to byEnvironmentId
- Re-key useCumulativeDiff cache by environmentId
- Update WS handler to resolve envKey before invalidating cumulative diff cache
- Update comments across portal manager, session switch, and layout files

* fix: revert useEnvironmentSessionId in file-editor-panel

file-editor is session-scoped and ephemeral — portal is destroyed on
session switch, so useEnvironmentSessionId() has no practical effect.

* fix: address CodeRabbit review comments

- Pass appStore to setupPortalCleanup so terminal tab close still calls
  stopUserShell via activeSessionId fallback (entry.sessionId is now
  undefined for global panels)
- Add request version guard to useCumulativeDiff to discard stale
  responses after environment switches

* fix: scope activeSessionId fallback to terminal only in portal cleanup

vscode is session-scoped and always has entry.sessionId — the
activeSessionId fallback should only apply to terminal (now global).

* fix: migrate stranded env-keyed data on registerSessionEnvironment

When registerSessionEnvironment(sessionId, environmentId) is called,
any data previously stored under the fallback sessionId key is now
migrated to the proper environmentId key. This prevents data from
"vanishing" when selectors start reading by environmentId after the
mapping is registered.

Applies to: sessionCommits, gitStatus, shell outputs/statuses,
userShells.

* fix: extract migrateEnvKeyedData helper to fix lint max-lines

* fix: address CodeRabbit review comments

- Always delete orphaned sessionId key in migrateEnvKeyedData even when
  environmentId key already exists (prevents stale accumulation)
- Reset loading state when switching to an envKey with warm cache to
  avoid brief spinner flash

* fix: always reset loading on envKey switch in useCumulativeDiff

Prevents stuck loading state on rapid A→B→A env switches where the
stale in-flight response is discarded by version guard but loading
is never reset.

* fix: move sync effect before fetch effect in useCumulativeDiff

React 18 batches effects in declaration order. With the sync effect
after the fetch effect, setLoading(false) from the sync overrode
setLoading(true) from the fetch on cold-cache env switches. Moving
the sync effect first ensures the fetch effect's setLoading(true)
wins the batch.

* test: add migrateEnvKeyedData coverage for registerSessionEnvironment

Tests 4 cases: normal migration, don't-clobber existing data,
no-op when sessionId equals environmentId, and partial sub-key
migration.

* fix: close stale-diff gaps in useCumulativeDiff sync effect

- Bump requestVersionRef on envKey change so in-flight fetches for
  the previous envKey are discarded
- Clear loadingState[envKey] so the fetch effect isn't blocked on
  re-entry (A→B→A scenario)
2026-04-14 11:44:38 +01:00
Carlos Florêncio 61e5d5cce9 feat(web): redesign task sidebar with repo-grouped layout and diff stats (#550) 2026-04-07 09:12:17 +01:00
Carlos Florêncio f4723ebcae feat: sort tasks by creation date in kanban and sidebar (#511) 2026-03-31 23:31:18 +01:00
Carlos Florêncio 3a71ae2fe6 fix: complete all non-terminal tool calls when turn ends (#488) 2026-03-29 17:36:50 +01:00
José Almeida d1a028fdb6 feat: introduce subtasks, allow sessions to task reuse executor (#419) 2026-03-29 15:33:40 +01:00
Carlos Florêncio 7da6550093 fix(orchestrator): fail session with guidance when PR branch is missing (#466) 2026-03-26 19:35:17 +00:00
Carlos Florêncio 083b2b5bb0 fix(acp): improve claude acp tool messages and model selector flow (#442) 2026-03-14 21:58:31 +00:00
José Almeida f1da9644f4 feat: agent-native config mode (#396)
* feat: add MCP mode pipeline for config vs task tool registration

Thread McpMode field through the entire instance creation chain:
CreateInstanceRequest → InstanceOverrides → InstanceConfig → MCP Server.
This allows config-mode MCP sessions to register a different set of
tools than the default task-mode sessions.

* feat: add config-mode MCP tools and agentctl handlers

Register config-mode tools (workflow steps, agents, MCP config, tasks)
when MCP server mode is "config". Add 16 new WS action constants and
agentctl-side handlers that forward tool calls to the backend via the
existing BackendClient channel.

* feat: add backend MCP handlers for config-mode operations

Add backend WebSocket handlers for workflow step CRUD, agent CRUD,
MCP config get/update, and task move/delete/archive/state operations.
Wire config dependencies via SetConfigDeps pattern to avoid breaking
the existing NewHandlers constructor.

* feat: add config-mode system prompt and unit tests

Add ConfigContext system prompt listing all config-mode MCP tools.
Add unit tests for mode-based tool registration, agentctl config
handler forwarding, backend handler validation, and system prompt
content.

* test: add exhaustive E2E tests for config-mode MCP handlers

38 end-to-end tests covering the full handler → service → DB pipeline
for all 16 config-mode operations: workflow step CRUD, agent CRUD,
agent profiles, MCP config, and task management (move, delete, archive,
update state). Includes handler registration verification and two
full pipeline integration tests.

* fix: remove E2E test that depends on agent CLIs installed in CI

TestE2E_Agent_CreateMultipleAndList tried to create claude-code and
codex agents, which aren't available in CI. Agents are unique by type
ID so creating duplicates also fails. Removed the test since
CreateAndList already validates the create+list flow.

* test: remove Go E2E tests for config-mode handlers

Replacing with Playwright E2E tests.

* test: add Playwright E2E tests for config-mode features

13 tests covering workflow steps, task operations, agent/profile
settings pages, MCP config, and workflow settings round-trips.

* feat: add dynamic MCP mode reconfiguration for agentctl instances

Add SetMode() to the MCP server that clears and re-registers tools for
the new mode. Expose it via PUT /api/v1/mcp/mode on per-instance servers,
a SetMcpMode client method, and a lifecycle Manager.SetMcpMode wrapper.

This enables switching from task to config MCP tools on workspaces that
were prepared before plan_mode was set on the session.

* feat: wire McpMode through executor launch and resume pipelines

Add McpMode to LaunchOptions so callers can set it at launch time.
StartTask and StartTaskWithSession now pass McpMode="config" when
plan mode is active. startAgentOnExistingWorkspace reconfigures the
MCP server via SetMcpMode before starting the agent subprocess.

The AgentManagerClient interface gains SetMcpMode; the lifecycle
adapter, executor, and all mock implementations are updated.

* fix: pass plan_mode from workflow step when auto-starting moved tasks

handleTaskMovedNoSession was hardcoding planMode=false when calling
StartTask. Now it loads the target step and checks for enable_plan_mode
in on_enter actions, passing the correct value so config-mode MCP tools
are registered for the agent.

* test: fix config-mode E2E tests and add SetMcpMode mock stubs

- Add SetMcpMode stubs to all AgentManager mock implementations
- Fix strict mode violations with { exact: true } on tool name assertions
- Expand collapsed tool call groups before asserting individual names
- Remove temporary diagnostic code

* fix: address CodeRabbit review comments

- Add servers parameter to update_mcp_config tool schema
- Add mutex protection to SetMode for thread safety
- Create session before building prompt so config context has session ID
- Fix inconsistent ask_user_question tool name reference (use _kandev suffix)

* refactor: use canonical ws.ActionMCP constants and handle marshal errors

- Import ActionMCP* constants from pkg/websocket instead of duplicating
  them locally in config_handlers.go and handlers.go
- Handle json.MarshalIndent error in forwardToBackend instead of ignoring it

* fix: extract McpModeConfig constant and fix Prettier formatting

- Add McpModeConfig constant to avoid repeated "config" string (goconst)
- Fix Prettier formatting in config-management E2E spec

* fix: validate task state before update and strengthen sysprompt tests

- Add TaskState validation in handleUpdateTaskState to return proper
  validation error instead of internal error for invalid states
- Strengthen format-contract assertions (exact %s counts, no %! markers)
- Extract repeated test data into constants per project guidelines

* refactor: decouple config MCP mode from plan_mode, use session metadata

Replace plan_mode-based config MCP activation with config_mode in
session metadata. Add isConfigModeSession() helper used across
executor_execute, executor_interaction, and executor_resume.
Propagate task.Metadata to session.Metadata in PrepareSession so
config_mode flows from task creation through to the agent launch.

* refactor: remove task tools from config MCP mode and update sysprompt

Remove task management tools (list_tasks, move_task, delete_task,
archive_task) from ModeConfig registration — they remain available in
ModeTask. Update ConfigContext system prompt to remove task tool docs
and add example requests section. Update MCP server tests for new tool
count (18→14) and remove deleted task handler tests.

* feat: add config-chat endpoint and workspace config agent profile

Add default_config_agent_profile_id column to workspaces with SQLite
migration. Add POST /workspaces/:id/config-chat endpoint that creates
an ephemeral task with config_mode metadata and starts a session with
config MCP tools. Extract resolveConfigChatDefaults helper to stay
within lint complexity limits.

* feat: add config chat UI with workspace settings integration

Add config chat modal component and hook for starting config sessions.
Add configChat state slice with open/close actions. Add workspace API
client for config-chat endpoint. Add configuration agent profile
selector to workspace settings form. Add config chat button to
settings layout sidebar.

* test: rewrite config-mode E2E tests to use metadata and session_id

Rewrite config-management E2E tests to use createTaskWithAgent with
config_mode metadata instead of a custom fetch helper. Fix navigation
to use task.session_id instead of task.id. Add metadata parameter
support to ApiClient.createTaskWithAgent.

* fix: use floating FAB for config chat and pass default agent profile

Move config chat button from header to a floating action button in the
bottom-right corner with a sparkles icon. Pass the workspace's default
agent profile ID when opening the config chat to avoid "no agent
profile configured" error. Add default_config_agent_profile_id to
workspace slice type.

* feat: async agent launch for config-chat and extract helpers

The config-chat endpoint now launches the agent asynchronously when a
prompt is provided, matching the start_agent flow used by task creation.
Extract deleteTaskOnError and launchConfigChatAgent helpers. Remove
LaunchWorkspace from prepare step to avoid blocking.

* feat: first-interaction agent profile selection for config chat

Open the config chat modal immediately on FAB click. If no default
config agent profile is set, the empty state now shows a profile
selector before starting. The selected profile is saved as the
workspace default_config_agent_profile_id for future sessions.

* test: fix config E2E tests — remove duplicate method, use real tools

Remove duplicate startConfigChat method in ApiClient that overrode the
correct 3-param signature. Replace invalid create_agent test (requires
registered agent type) with update_agent test. Remove fragile tool calls
group UI assertion from multi-tool test.

* fix: resolve 'no active session' error in config chat

Add openConfigChatModal action that only sets isOpen and workspaceId
without touching sessionId. Previously, open() called openConfigChat
with empty sessionId which was truthy, causing QuickChatContent to
render with an invalid session.

* feat: add minimal toolbar mode for config chat input

Thread minimalToolbar prop through ChatInputArea → ChatInputContainer →
ChatInputBody → ChatInputToolbar → QuickChatContent. When enabled,
renders only the submit/cancel button, hiding plan toggle, MCP
indicator, context popover, model selector, and other controls.

* feat: replace config chat dialog with floating popover panel

Replace Dialog with Popover from @kandev/ui so the config chat floats
up from the FAB button. Reduce FAB size to h-12/w-12 with larger h-7/w-7
icon. Rename tooltip to "Configuration Chat". Remove standalone
ConfigChatFAB component from settings layout.

* feat: filter config chat tasks from quick chat listing

Add exclude_config query parameter to ListTasksByWorkspace that filters
out tasks with metadata.config_mode=true. Apply it in the quick chat
sessions API call to prevent config chat tasks from appearing as tabs.

* refactor: move config agent profile setting to agents page

Remove the Configuration Agent Profile selector from workspace settings
and add a dedicated ConfigChatAgentSection to the agents settings page
with auto-save on change.

* feat: redesign config chat empty state with inline text input

Replace clickable template buttons with subtle suggestion hints that
fill the textarea on click and disappear once the user starts typing.
Add a persistent Textarea + send button at the bottom so users can
type directly without clicking templates. This prevents the "no active
session" error since QuickChatContent only renders after startSession
succeeds.

* fix: config chat UX polish — non-clickable suggestions, profile persistence

- Make suggestion prompts purely visual (non-interactive text hints)
- Update workspace store after saving config agent profile so the
  profile selector doesn't reappear on subsequent opens
- Use useAppStoreApi for store access in callbacks

* fix: add inline error display and prevent accidental popover dismiss

- Show API errors inline below the input instead of via toast (which
  can be hidden behind the popover)
- Prevent popover from closing on outside click via onInteractOutside,
  so the user doesn't lose their typed input or active session
- Surface error state from useConfigChat hook to the empty state UI

* fix: use config-specific placeholder in config chat input

Add placeholderOverride prop through ChatInputArea and QuickChatContent
so the config chat shows "Ask anything about your configuration..."
instead of the default "Continue working on the task..." placeholder.

* feat: add default_config_agent_profile_id to workspace DTO and API

Wire the field through WorkspaceDTO, FromWorkspace(), HTTP/WS request
structs, service layer, and public API types so the config chat default
profile persists across page reloads and SSR hydration.

* feat: add config chat store actions and prompt delivery via WS

Add configChat slice actions (open, close, rename, tab management),
pendingPrompt state in useConfigChat hook, and initialPrompt support
in QuickChatContent so prompts are delivered via WS after subscription
is established rather than via HTTP (avoiding race condition).

* fix: resolve dual store issue and message fetch race condition

Replace StateProvider with StateHydrator in settings layout to prevent
a shadow Zustand store that intercepted WS events. Fix message fetch
race condition by merging WS-delivered messages with fetch results
instead of replacing them.

* feat: make config chat global with command panel integration

Add ConfigChatProvider at root layout so the popover is available on all
pages. FAB button only visible on settings pages; hidden anchor on other
pages for popover positioning. Add "Configuration Chat" command to the
command panel (Cmd+K). Add close button and header bar to the popover.

* test: add E2E tests for config chat popover and command panel

Add tests for opening config chat via FAB on settings page (with full
agent response flow) and opening via command panel on kanban page.
Fix api-client endpoint and add workspace update fields for E2E setup.

* fix: compress props destructuring to satisfy max-lines-per-function lint

Rebase merged both sides' additions into ChatInputEditorArea and
ChatInputToolbar, pushing them over the 100-line function limit.
Use object parameter with compact destructuring to reduce line count.

* fix: register config task tools and add exhaustive E2E coverage

Config-mode MCP task tools (list_tasks, move_task, delete_task,
archive_task) were defined but never registered — registerConfigTaskTools()
was never called in registerTools(). Fixed the registration and expanded
E2E tests from 5 to 15, covering all testable config-mode MCP tools
(workflow, agent, MCP config, and task management) with API-side
verification of side effects.

* feat: replace create/delete agent with create/delete agent profile in config mode

Agent creation/deletion via MCP isn't practical since agents map to
registered types. Replace with create_agent_profile and
delete_agent_profile tools that manage the per-agent configuration
presets users actually need to work with. Wires full chain: agentctl
MCP tools → WS actions → backend handlers → controller layer.

* chore: fix Prettier formatting

* fix: compress props destructuring to satisfy max-lines-per-function lint

* test: update MCP tests for agent profile tools and correct config tool count

* fix: address CodeRabbit review — normalize MCP mode, set default, consolidate tests

* fix: address CodeRabbit review — error leaks, search filter bug, stale prompt, sysprompt tools

* feat: expose all workflow step fields in config-mode MCP tools

* feat: add workflow CRUD to config-mode MCP tools

* fix: split workflow CRUD E2E test into separate tests

* fix: prevent cascade deletion of config session in delete workflow E2E test

The config-chat endpoint picks the newest workflow (ORDER BY created_at
DESC) for the ephemeral task. When the test created a deletable workflow
right before starting the config session, the session's own task landed
under that workflow. Deleting the workflow then cascaded and wiped the
running session's task, causing the MCP tool call to hang.

Add a "Config Anchor" bumper workflow so the config session uses it
instead of the deletable one.

* feat: add executor management to config-mode MCP tools

Add 8 new MCP tools for executor and executor profile CRUD in config
mode: list/create/update/delete for both executors and profiles.
Wired through the full stack (WS actions, backend handlers, agentctl
tools, sysprompt) with unit tests and E2E coverage.

* fix: auto-focus config chat input and rename popover title

* fix: remove executor CRUD MCP tools, keep executor profile management

Executors are pre-defined system entities (local, worktree, docker,
sprites). Only executor profiles should be managed via config chat.
Removes create_executor, update_executor, delete_executor MCP tools
while keeping list_executors and all executor profile tools.

* fix: add realtime UI sync for workflow steps and agent profiles via MCP

Workflow step and agent profile changes made through the config chat
MCP tools were not broadcasting events to the frontend. Executor
profile events worked because they went through the task service which
has event bus integration.

- Add WorkflowStep and AgentProfile event types to the event bus
- Add WS notification action constants for workflow step events
- Publish events from MCP handlers after step/profile CRUD operations
- Register gateway subscriptions for the new event types
- Add frontend WS handlers for workflow.step.created/updated/deleted

* fix: reuse existing config chat session when opened from command panel

The command panel always called openConfigChatModal() which reset the
session state, causing the profile picker to show again. Now it checks
for an existing active session first, matching the FAB behavior.

* fix: suppress FAB tooltip while config chat popover is open

When closing the popover via the X button, the mouse ends up over the
FAB area and the tooltip re-appears. Force the tooltip closed while
the popover is open so it cannot re-trigger on close.

* fix: thread default_config_agent_profile_id through all workspace data paths

The field was defined on the workspace type but never populated in:
- SSR mapper (page.tsx) — missing from initial hydration
- WS workspace handlers — wiped on workspace.created/updated events
- Backend publishWorkspaceEvent — not included in event data

This caused the config chat to always show the profile picker when
opened from the command panel, since the saved default was lost.

* fix: address PR review comments — workspace sync, type fixes, missing tool doc

- Sync configChat.workspaceId when promoting next tab in closeConfigChatSession
- Add update_task_state_kandev to ConfigContext system prompt
- Fix workflow step WS event types (workflow.step.* instead of step.*)
- Fix const reassignment in E2E tests (const → let)

* refactor: clean up dead code and register missing MCP tools

- Register update_task_state, delete_workflow_step, and
  reorder_workflow_steps as MCP tools in agentctl config mode
- Remove dead handleCreateAgent/handleDeleteAgent WS handlers
  and their ActionMCP constants (only profiles are managed)
- Rename openConfigChatModal to startNewConfigChat across frontend
- Update sysprompt, tests, and tool counts

* fix: remove extra next.config.ts machine

* fix: address CI failures — prettier formatting and funlen split

* fix: address coderabbit review comments across config-mode stack

- Include prompt field in workflow step broadcast payloads
- Align sysprompt task-state values with actual backend enum
- Return error instead of continuing when MCP mode switch fails
- Validate mode values in agentctl set-mcp-mode endpoint
- Use errors.Is() for sentinel error comparison
- Fix nullish coalescing to allow clearing config agent profile
- Expand StepPayload type with missing config-step fields
- Derive config agent Select value from store instead of local state

* fix: reuse persisted config chat session and reduce toolbar complexity

- Look up configChat.sessions for workspace before creating new session
- Extract MinimalToolbar and move defaults to module-level object to
  reduce ChatInputToolbar cyclomatic complexity from 16 to 7

* fix: E2E executor test strict mode violation and invalid cleanup

- Use exact match for list_executors text to avoid matching 2 elements
- Remove cleanup that tried to delete pre-existing system executor
- Reuse persisted config chat session for workspace in global commands
- Reduce ChatInputToolbar cyclomatic complexity with module-level defaults
2026-03-13 00:44:06 +00:00
Carlos Florêncio bb0c63e73b fix: resolve model selector mismatch for ACP agents (#410) 2026-03-10 22:13:28 +00:00
Carlos Florêncio bb1caaba11 feat: add ACP agent variants for Claude, Codex, Copilot, and Amp (#387) 2026-03-10 17:30:04 +00:00
José Almeida 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
2026-03-10 15:37:51 +00:00
João Salavisa 299d948af8 feat: auto-update base commit on branch switch (#399) 2026-03-09 23:19:03 +00:00
Carlos Florêncio 3820feb819 feat: suggest agent install commands and fix TUI agent startup (#398) 2026-03-09 21:54:30 +00:00
João Salavisa d84b8873ea feat: quick chat implementation (#393) 2026-03-08 22:37:37 +00:00
Carlos Florêncio b48471e063 feat: single-port architecture and browser warning fixes (#390) 2026-03-08 13:10:39 +00:00
Carlos Florêncio 0da44478d1 feat: add port proxy, symlink file save fix, and remote executor improvements (#358) 2026-03-07 14:53:21 +00:00
José Almeida 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)
2026-03-07 03:59:01 +00:00
Carlos Florêncio 21cb909ef4 feat: improve git checkout with error classification and warning propagation (#386) 2026-03-06 23:54:31 +00:00
João Salavisa 28a27afd1a refactor: move git status and commits to real-time agentctl queries (#366) 2026-03-05 08:35:02 +00:00
José Almeida 123b2f6eed feat: start task from GitHub URL (#365)
* feat(backend): accept github_url in task creation and clone on launch

Add github_url field to task creation DTO and propagate through HTTP/WS
handlers and service layer. When a task is created with a GitHub URL
instead of a local repository, the service resolves (or creates) the
repository record via FindOrCreateRepository and triggers a clone/fetch
via repoclone.Cloner during executor pre-flight.

Wire RepoCloner into the orchestrator executor so remote repositories
are fetched before the agent process starts. On resume, RepoUpdater
performs a fetch to ensure the worktree is up-to-date.

* feat(backend): add GitHub branches endpoint for remote repos

Add ListRepoBranches to the GitHub client interface (GHClient,
PATClient, NoopClient, MockClient) and expose it via
GET /api/v1/github/repos/:owner/:repo/branches.

The mock controller gains POST /api/v1/github/mock/branches for
seeding branch data in E2E tests.

* feat(web): add GitHub URL input to task create dialog

Add a toggle in the task create dialog that switches between the
existing repository selector and a free-text GitHub URL input. When a
valid GitHub URL is entered (owner/repo parsed), branches are fetched
from the new backend endpoint and auto-selected.

The dialog state, effects, validation, and submit logic are extended to
support the github_url flow alongside the existing repository flow.

* test(e2e): add tests for task creation from GitHub URL

Add E2E tests verifying:
- Full flow: toggle to GitHub URL mode, enter URL, create task, verify
  agent runs and completes in session view
- Toggle behavior: switching between GitHub URL and repository selector
  modes updates UI correctly

* fix(web): dynamic URL input sizing and extract RepoSourceInput

Replace fixed-width GitHub URL input with size-based auto-sizing
(matching InlineTaskName pattern). Extract RepoSourceInput component
from DialogHeaderContent to stay within cyclomatic complexity limits.

Add testId prop to Combobox and wire it to ExecutorProfileSelector
for E2E test targeting.

* test(e2e): add worktree executor test and fix GitHub URL repo seeding

Update GitHub URL E2E tests to pre-seed a repository with GitHub
provider info pointing to the local git repo, avoiding actual clone
attempts during task creation.

Add test for GitHub URL task creation with worktree executor type,
verifying the full flow including executor profile selection.

Extend ApiClient with listExecutors and provider fields on
createRepository for test seeding.

* fix(backend): use protocol-aware clone URL for provider-backed repos

The ensureRepoCloned function was hardcoding HTTPS clone URLs via
repositoryCloneURL(), but the user's git setup may require SSH.

Add BuildCloneURL to the RepoCloner interface so the executor uses
the Cloner's configured protocol (SSH/HTTPS) instead of always
generating HTTPS URLs. Falls back to HTTPS if BuildCloneURL fails.

This fixes workspace loading issues (terminal, file tree) for cloned
repos when the user has SSH-only authentication configured.

* test(e2e): enhance GitHub URL test with terminal and changes panel verification

* feat(web): add inline validation for GitHub URL input

- Show inline error for invalid URL format (not matching github.com/owner/repo)
- Show inline error when repository is not found or not accessible
- Mock client returns error for unknown repos (matches real GitHub API)
- E2E tests for invalid URL, nonexistent repo, and error-clearing on valid input

* fix(web): fix GitHub URL branch auto-select and E2E test stability

- Pick branch by name (main/master) for GitHub URL mode instead of
  using selectPreferredBranch which returns origin-prefixed names
- Add test timeout for worktree executor test
- Use shorter filename in terminal test to avoid xterm line-wrap issues

* fix(web): fix GitHub URL dialog polish and wrong-repo bug

UI fixes:
- Align toggle button text with dropdown text content (pl-3)
- Match GitHub URL input height to dropdown trigger (min-h-9)
- Position error text absolutely to prevent layout shift
- Remove redundant toast on repo-not-found (inline error suffices)

Bug fix:
- Prevent repo auto-select from overriding GitHub URL mode by
  guarding useRepositoryAutoSelectEffect with useGitHubUrl
- Reorder getRepositoriesPayload to check useGitHubUrl first,
  preventing stale repositoryId from winning over the GitHub URL

Add E2E test verifying correct repo usage across consecutive tasks.

* fix(web): fix dialog height mismatch and error text overlap

- Use exact h-9 on GitHub URL input to match dropdown trigger height
- Move error text from absolute-positioned overlay to inline next to
  the toggle button, preventing overlap while keeping layout stable

* fix(web): make GitHub URL input auto-size to content

Reduce minimum size from 30 to 20 and shorten placeholder so the
"/ task-name" sits closer to the URL text, matching the dropdown's
compact w-auto behavior.

* fix(web): polish GitHub URL input sizing, floating error, and placeholder color

- Match input height (h-7) and padding (px-2) to Combobox trigger button
- Size input width to exactly fit placeholder text
- Show validation error as floating popover below input instead of inline
- Keep placeholder text grey even when input has error state
- Extract useGitHubUrlState hook to fix max-lines-per-function lint

* fix(backend): return proper HTTP status codes from GitHub branches endpoint

Add GitHubAPIError type to distinguish upstream GitHub API status codes.
The branches endpoint now returns 404 (not 500) for non-existent repos,
which the logging middleware correctly logs as DEBUG instead of ERROR.

* feat(web): show error toast when a task session fails to start

Add session failure notification plumbing: WS handler detects FAILED
state with error_message, pushes to Zustand store, and a global bridge
component shows an error toast via the ToastProvider. Also adds
data-testid attributes to toast elements and types error_message on
the session state changed payload.

* style: fix prettier formatting in task-create-dialog and related files

* fix(web): extract ToastList component to fix max-lines-per-function lint

* fix: address code review feedback for GitHub URL task creation

- Preserve 403 status instead of collapsing to 401 in branches endpoint
- Use clone-generic error instead of remote-docker-specific in ensureRepoCloned
- Trim whitespace in GitHub URL validation (backend + frontend)
- Add GitHubURL field to wsUpdateTask path
- Fix DefaultBranch mapping to prefer repoInput.DefaultBranch over BaseBranch
- Clear githubBranchesLoading on early-return paths to prevent stuck state
- Gate GitHub branch loading by mode to avoid false loading states
- Encode owner/repo in API path for special characters
- Add aria-label, aria-invalid, aria-errormessage for accessibility

* fix(e2e): improve diff-update test stability with fresh locator and longer timeout

* fix(backend): add --paginate flag to gh api branches call for repos with >100 branches

* fix: address second round of code review feedback

- Add pagination support to PATClient.ListRepoBranches for repos with >100 branches
- Validate owner/repo in mock controller addBranches endpoint
- Use aria-describedby instead of aria-errormessage for broader AT support
- Add aria-live region to toast container for screen reader announcements
- Assert absence of FIRST_MODIFICATION in diff-update E2E test

* fix(backend): downgrade HTTP request logger from error to warn for 4xx/5xx responses

* fix: address third round of code review feedback

- Validate repositoryID/localPath in UpdateRepositoryLocalPath
- Copy slices in mock client to prevent external mutation
- Extract GitHub header constants and shared setter in PATClient
- Redact clone URL from logs to prevent credential leaks
- Guard against persisting empty localPath after clone
- Show fallback toast message when FAILED state has no error_message
2026-03-05 00:47:58 +00:00
José Almeida c79d138c45 feat(web): add diff expansion with expand-all in review panel (#340)
* feat(backend): add support for fetching file content at git ref

Add GetFileContentAtRef method to WorkspaceTracker for retrieving file content
at specific git refs (branches, commits, HEAD). This enables diff expansion by
fetching old file versions without checking out.

Changes:
- Extract maxFileSize constant to package level (10MB)
- Add GetFileContentAtRef to workspace_files.go using git show
- Add workspace.file.get_at_ref WebSocket action and handler
- Add agentctl client method for the new endpoint
- Register handler in workspace file handlers

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* feat(web): add diff expansion support for code review

Add lazy-loaded diff expansion to show more context in code reviews. When expansion
is enabled, users can expand hunks to see more surrounding code by clicking expand
buttons. Full file content is fetched on-demand via WebSocket.

Changes:
- Add useExpandableDiff hook for lazy-loading old/new file content
- Add requestFileContentAtRef to workspace-files.ts
- Integrate expansion into diff viewer components
- Add enableExpansion and baseRef props to DiffViewer
- Auto-load expansion content when enabled
- Pass expansion state through diff viewer hierarchy

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix(web): gracefully handle expansion props for Monaco diff viewer

Monaco diff viewer has built-in expansion that doesn't require fetching
old file content. Strip pierre-diffs-only expansion props (enableExpansion,
baseRef) when Monaco is selected to avoid passing unsupported props.

This ensures the feature works correctly regardless of which diff viewer
the user selects in settings.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* feat(backend): add diff expansion e2e scenario and fix unstaged diff base ref

- Add scenarioDiffExpansionSetup mock-agent scenario that creates a
  200-line file, commits it, then modifies lines 50 and 150 to produce
  a two-hunk uncommitted diff for testing diff expansion in the UI
- Add workspace_file_handlers for workspace.file.get_at_ref WS action
- Fix enrichWithDiffData to always diff against HEAD for unstaged/staged
  content so locally committed files show correct diffs
- Add unit tests for GetFileContentAtRef (6 sub-tests)

* feat(web): add diff expansion UI and e2e test

- Wire useExpandableDiff hook into FileDiffViewer and ReviewDiffList
  with enableExpansion and baseRef props
- Gracefully handle expansion props for Monaco diff viewer fallback
- Add e2e Playwright test for diff expansion between hunks

* fix(web): fix diff expansion by correcting hunk separators and line splitting

Three bugs prevented @pierre/diffs expand buttons from rendering:
- hunkSeparators was "simple" (no expand UI); changed to "line-info" when expansion enabled
- expandUnchanged:true showed all lines expanded (no separators); removed the option
- .split("\n") stripped trailing newlines; now uses SPLIT_WITH_NEWLINES regex to match library

* feat(web): add editor provider settings UI

Add an "Editor Providers" section to the Editors settings page with
Select dropdowns for each configurable context (Code Editor, Diff Viewer,
Chat Code Blocks, Chat Diff Blocks). Reads from and writes to the
existing localStorage-backed editor resolver store. Changes apply
immediately without requiring a save action.

* test(web): add e2e tests for editor provider settings

Verify that the editor provider dropdowns render with correct defaults,
update on selection, and persist changes across page reloads via
localStorage.

* feat(web): add expand/collapse unchanged regions toggle to diff toolbar

Refactor diff header toolbar into composable ToolbarButtons component and
add a toggle button to expand or collapse unchanged regions in both
Pierre Diffs and Monaco diff viewers. Pass enableExpansion through to
Monaco to control hideUnchangedRegions.

* fix(web): handle deleted files in diff expansion

Treat "file not found" / "no such file" errors as empty content instead
of throwing, so diff expansion works correctly for deleted files.

* chore(web): add diff expansion debug e2e helpers

* test(web): split diff expansion tests by provider (Pierre Diffs + Monaco)

Restructure diff-expansion e2e tests into two describe blocks, one per
diff viewer provider. Each block sets the provider via localStorage
before navigation and verifies the correct viewer renders:

- Pierre Diffs: checks for diffs-container element and shadow DOM
  expand buttons between hunks
- Monaco: checks for .monaco-diff-viewer wrapper and the built-in
  hideUnchangedRegions widget with "Show Unchanged Region" link

Both suites verify that clicking expand reveals collapsed middle lines.

* fix(web): fix Pierre Diffs expand button selectors in e2e tests

Use shadow DOM data attributes ([data-expand-button], [data-expand-up])
instead of XPath parent traversal which doesn't work inside shadow DOM.
Wait for expand buttons to load asynchronously before clicking.

* fix(e2e): make diff-expansion-setup scenario idempotent

The mock-agent scenario now removes and re-commits the test file before
each run, so subsequent tests in the same repo don't fail with "git
commit failed" when the file already exists. Also increase test timeouts
to match other heavy e2e tests.

* refactor(web): remove editor provider settings UI, enable expand-all in review

Remove the editor provider switching UI and its tests — Monaco's diff
expansion doesn't work and exposing provider switching adds confusion.
Show Pierre Diffs header toolbar in the review panel (remove hideHeader)
so the expand-all button is accessible. Add e2e test for expand-all.

* style(web): fix prettier formatting and lint warnings

Run prettier on files with formatting drift. Extract ToolbarBtn helper
in diff-header-toolbar to stay within max-lines-per-function limit.
Consolidate useMonacoDiffViewerState to reduce line count.

* chore(web): remove debug scaffolding, revert Monaco changes, drop stray console.log

Revert Monaco diff-viewer-toolbar and monaco-diff-viewer to main
(expansion not functional with Monaco). Remove debug-expansion
screenshot scripts and spec. Strip console.log from commit-detail-panel.
Strip enableExpansion from Monaco path in diff-viewer-resolver.

* fix(web): use commit-relative base ref for diff expansion

Use `${commitSha}^` instead of hardcoded `HEAD~1` so expansion fetches
the correct old content when viewing non-HEAD commits.

* fix(web,backend): address CodeRabbit review feedback

- Add aria-label to icon-only toolbar buttons for screen reader access
- Guard auto-load expansion effect against error state to prevent retry
  loops when WebSocket fetch fails
- Invalidate expansion cache on file/ref change with request versioning
  to prevent stale content
- Preflight blob size with git cat-file -s before git show to avoid
  memory spikes on large files

---------

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-04 00:05:39 +00:00
José Almeida c191ef2577 feat: improve git operations (branch rename, amend commit, file rename, reset) (#337) 2026-03-03 12:41:51 +00:00
Carlos Florêncio fb46873cd2 fix: include agent_profile_id in session WS events to resolve stale MCP status (#354) 2026-03-03 00:17:23 +00:00
Carlos Florêncio d0b5237070 feat: improve chat input ux (#350) 2026-03-02 21:32:21 +00:00