32f098a6fce74e6bee68332e24ef98a9e58fbdc2
308 Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
32f098a6fc |
feat: show question icon only for pending input (#782)
* fix: show question icon only for pending input * fix: address pending input review feedback * fix: reduce pending input sidebar complexity * fix: address clarification review comments * test: cover pending clarification hook |
||
|
|
58fb9ffb0f |
fix: dedupe cancel-turn clicks to stop "turn cancelled" cascade (#784)
* fix: dedupe cancel-turn clicks to stop "turn cancelled" cascade The cancel button had no in-flight feedback, so impatient users clicked it repeatedly while the agent was tearing down a slow turn (e.g. a Claude Monitor tool). Each click reached the backend and emitted its own "Turn cancelled by user" message, with the second-and-later calls also lazily creating phantom turns to host those messages. Fix on both sides: SubmitButton now tracks isCancelling locally and disables itself with a spinner while the request is in flight; the orchestrator deduplicates concurrent CancelAgent calls per session via a sync.Map guard so duplicates that slip through return immediately. * fix: address PR review feedback - Test: replace sleep-based polling in cancel-dedupe test with channel synchronization (cancelAgentEntered) per project convention. - UI: switch the cancel re-entrancy guard from state to a ref so isCancelling no longer thrashes the useCallback dep array. --------- Co-authored-by: Kandev Agent <agent@kandev.dev> |
||
|
|
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 |
||
|
|
c853044f1d |
feat(ui): collapse commits and PR changes by default in changes panel (#781)
* feat(ui): collapse commits and PR changes by default in changes panel * fix(e2e): re-expand commits section after remount in squash and focus tests CommitsSection unmounts mid-transition (when both hasStaged and hasCommits are briefly false), so it remounts with defaultCollapsed=true and loses the user's expanded state. Re-expand before assertions that follow such remounts. --------- Co-authored-by: Kandev Agent <agent@kandev.dev> |
||
|
|
a510b46e4e |
feat(multi-repo): support tasks spanning multiple repositories (#767)
* feat(task): add task_environment_repos schema and model for multi-repo support
Phase 0 of multi-repository task support: introduces the per-repository
child table and model alongside legacy single-repo fields (dual-write,
no behavior change). Lays the foundation for the orchestrator/lifecycle
refactor in subsequent phases.
- New task_environment_repos table with FK CASCADE on task_environments.
- New task_dir_name column on task_environments (idempotent ALTER).
- TaskEnvironmentRepo model + Repos slice + RepoFor helper on TaskEnvironment.
- CRUD methods + interface extension; env getters preload Repos.
- Idempotent backfill from legacy single-repo task_environments rows.
- Tests cover round-trip, CRUD, preload ordering, embedded create, cascade
delete, and backfill from a legacy-shaped row.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(lifecycle): add Repositories list to LaunchRequest and EnvPrepareRequest
Phase 1.1 of multi-repository task support: extends the lifecycle types
with explicit per-repo lists, while keeping the legacy single-repo
top-level fields as the synthesized fallback. No caller behavior changes
yet — preparers still see length-1 specs for single-repo launches.
- LaunchRequest.Repositories []RepoLaunchSpec + RepoSpecs() helper.
- EnvPrepareRequest.Repositories []RepoPrepareSpec + RepoSpecs() helper.
- EnvPrepareResult.Worktrees []RepoWorktreeResult for per-repo outcomes.
- runEnvironmentPreparer forwards req.Repositories into the prepare
request, inheriting the request-level RepoSetupScript when each
per-repo entry has none.
- Tests cover explicit-list precedence, synthesized fallback, and
repo-less requests.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(worktree,lifecycle): support multiple worktrees per session
Phases 1.2 and 1.3 of multi-repository task support. The pieces are
intertwined so they ship together — the preparer's per-repo loop
depends on the manager being able to hold one worktree per
(session, repository) pair instead of one per session.
Worktree manager:
- In-memory cache key changes from sessionID to (sessionID|repositoryID).
- New MultiRepoStore optional capability + SQLite implementation:
GetWorktreesBySessionID and GetWorktreeBySessionAndRepository.
- New Manager methods: GetAllBySessionID, GetBySessionAndRepo. Existing
GetBySessionID returns the first match (single-repo callers unchanged).
- Manager.Create's "already exists" check is now repo-scoped so the
second repo's create no longer returns the first repo's worktree.
- All cache write/delete sites updated to the composite key.
WorktreePreparer:
- New prepareMultiRepo path activated when EnvPrepareRequest.Repositories
has >=2 entries; single-repo and repo-less behavior is byte-identical.
- prepareOneRepo runs validate -> sync -> create -> fetch-PR -> per-repo
setup-script for each spec; rolls back already-created worktrees on
any failure.
- Workspace path becomes the task root (parent of any repo subdir);
legacy WorktreeID/WorktreeBranch/MainRepoGitDir mirror Worktrees[0]
for downstream code that hasn't been migrated yet.
- Per-repo setup script runs verbatim (no script-engine resolution
yet — the per-repo placeholder namespace lands in a follow-up).
Tests:
- Manager: GetAllBySessionID, GetBySessionAndRepo (hit + miss),
GetBySessionID single-repo compat, cache key separation.
- Preparer: multi-repo creates one worktree per repo; rollback removes
active worktrees on partial failure; per-repo setup script runs in
each worktree directory and emits its own step.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(orchestrator): resolve all task repos and persist per-repo rows on launch
Phase 1.4 of multi-repository task support: the orchestrator now reads
every repository linked to a task, forwards the full list through the
adapter into the lifecycle layer, and persists one TaskEnvironmentRepo
plus one TaskSessionWorktree row per repo when the launch is multi-repo.
Single-repo and repo-less tasks keep their previous code path verbatim
(dual-write — legacy single-repo fields stay populated from the primary).
- New resolveAllRepoInfo / resolveTaskRepoInfo on the executor; primary
is the first by Position.
- buildLaunchAgentRequest takes both the primary repoInfo and the full
list, populating req.Repositories []RepoSpec only when length > 1.
- New RepoSpec / RepoWorktreeResult on the orchestrator's executor
package mirror the lifecycle types so the adapter can translate
between them without leaking lifecycle types into the orchestrator
public API.
- Adapter (cmd/kandev/adapters.go) forwards req.Repositories into
lifecycle.LaunchRequest and surfaces per-repo prepare results back
on LaunchAgentResponse.Worktrees.
- persistTaskEnvironment writes env.Repos in the same create
transaction; persistTaskEnvironmentRepos handles the existing-env
reuse path. TaskEnvironment.TaskDirName is now persisted.
- persistWorktreeAssociation loops over resp.Worktrees in multi-repo,
inserting one row per repo with its own RepositoryID.
- ResumeSession now also populates req.Repositories from task repos
and stamps the existing TaskDirName so the preparer reuses the same
task root.
- repoStore / executorStore interfaces extended with the new repo and
env-repo CRUD methods; mocks updated accordingly.
Tests:
- LaunchPreparedSession with two task repositories populates
req.Repositories in order and keeps legacy single-repo top-level
fields filled from the primary.
- The same launch persists one TaskEnvironment + 2 TaskEnvironmentRepo
+ 2 TaskSessionWorktree rows.
- Single-repo launch still produces zero entries in req.Repositories
and exercises only the legacy fields.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(agentctl): git endpoints accept optional repo subpath for multi-repo
Phase 2 of multi-repository task support: every per-call git endpoint
gains an optional `repo` field (form param for GET, JSON for POST) that
selects a sub-directory of the workspace. Empty `repo` = workspace root,
preserving single-repo behavior verbatim.
- Manager.GitOperatorFor(subpath) returns a per-subpath GitOperator
(cached). Validates the subpath against path traversal, absolute
paths, and missing directories.
- New api server helper gitOpForRepo() emits a 400 with a per-operation
GitOperationResult on invalid subpaths.
- Repo field added to: pull, push, rebase, merge, abort, commit,
rename-branch, stage, unstage, discard, create-pr, revert-commit,
show-commit, reset, log, cumulative-diff. (git/status still reads
from the workspace tracker — that endpoint will gain multi-repo
support when the tracker itself is split per repo.)
- Tests cover empty subpath, valid subpath caching, traversal
rejection, and missing-directory rejection.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(web/comments): scope diff comments by repository for multi-repo tasks
Phase 3 of multi-repository task support: comments now carry an
optional repositoryId so two repos with the same file path render
their own comment threads instead of bleeding together.
- CommentBase.repositoryId (optional, backwards compatible).
- getCommentsForFile gains an optional repositoryId arg; legacy
comments without a repositoryId match any repo filter so persisted
data keeps showing.
- Diff store interface in lib/diff/types.ts mirrors the new arg.
- Tests cover per-repo filter, omitted-arg fallback, and legacy data.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(github): per-repo PR association on task PRs and watches
Phase 4 of multi-repository task support: github_task_prs and
github_pr_watches gain a repository_id column so multi-repo tasks can
have one PR per repo without colliding on (task_id) uniqueness.
- Idempotent ALTER TABLE adds repository_id to both tables.
- TaskPR.RepositoryID and PRWatch.RepositoryID model fields.
- New GetTaskPRByRepository and ListTaskPRsByTask lookups; existing
GetTaskPR keeps single-PR semantics for callers not yet migrated.
- ReplaceTaskPR's delete is scoped by repository_id when set so a
multi-repo task replacing one repo's PR no longer wipes the other.
- Tests cover per-repo storage and scoped replace.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(web): foundations for multi-repo task UI
Phase 5 of multi-repository task support: primitives the upcoming UI
work needs without changing any visible behavior yet.
- TaskRepository.checkout_branch surfaces the optional PR head branch
field that the backend already exposes.
- primaryTaskRepository(repos) picks by lowest Position so consumers
match the backend's "primary" semantics instead of `[0]`.
- New useTaskRepositories(taskId) / useTaskPrimaryRepository(taskId)
hooks read the live kanban task and return its repos sorted by
position. Future kanban/diff/UI consumers should use these instead
of poking task.repositories directly.
- Tests cover lowest-position selection and empty/single-repo edge cases.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(web): kanban card shows multi-repo "+N" chip and hydrates full repo list
Phase 6 (subset) of multi-repository task support. The kanban now
exposes the per-task repository list to the UI and renders a compact
"+N" badge on cards whose tasks span more than one repository.
- KanbanState.tasks[].repositories slim shape (id, repository_id,
base_branch, checkout_branch, position).
- SSR mapper hydrates the full list and uses primaryTaskRepository()
to pick the position-aware primary instead of [0].
- useKanbanData passes repositories through to the card props.
- KanbanCard renders the primary repo name with a small "+N" chip
when len(repositories) > 1; tooltip names the count.
- useTaskRepositories / useTaskPrimaryRepository hooks return the
slim KanbanTaskRepository shape so multi-repo consumers share one
read path.
Larger UI surfaces (task-create dialog, diff view, file browser,
mobile sheets) still target the primary repo only — they will be
ported as the multi-repo task creation flow lands. No visible change
for single-repo tasks.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(task): reject duplicate repositories on multi-repo task create
Phase 8 of multi-repository task support: surface duplicate-repository
input as a clean validation error instead of letting the unique
(task_id, repository_id) constraint surface mid-loop with some rows
already inserted.
- createTaskRepositories tracks seen repository IDs and returns
"repository <id> listed more than once for the task" before any
CreateTaskRepository call.
- Tests cover the rejection path and confirm two distinct repos still
succeed with both rows persisted in Position order.
Repository discovery and per-repo cloning were already covered by
Phase 1.4 (resolveAllRepoInfo loops ensureRepoCloned) and the
existing repository_discovery service is repo-list aware by design.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(web/task-create): repeatable repository rows for multi-repo task creation
Users can now actually create a multi-repo task: a "+ Add repository"
button below the primary repo + branch selectors expands the form with
additional (repository, base-branch) rows. Each extra row picks from a
filtered repository list that excludes the primary and any other already-
chosen extras, so duplicates are impossible to enter.
- ExtraRepositoryRow type + form-state hook (extraRepositories, add /
remove / update / reset). Lives in its own file so the main state hook
stays under the file-length lint cap.
- ExtraRepositoryRows component renders the rows and the "+ Add" button;
per-row branches are loaded on demand via the existing
useRepositoryBranches hook.
- buildRepositoriesPayload accepts the extras and appends them after the
primary, dropping rows with empty repository_id silently so an
unfinished row does not block submit. The backend's duplicate-repo
validation (Phase 8) catches anything that slips through.
- DialogFormBody is split: CreateModeSelectors (selectors + extra rows)
and useDialogHandlers move to dedicated files, keeping touched files
under the lint thresholds.
- Tests cover the multi-repo payload (ordering, drop-empty, no-primary
recovery, single-repo unchanged) and the existing
store-multi-repo.test.ts gets named constants to satisfy the
no-duplicate-string rule.
Verified: pnpm exec tsc --noEmit clean, vitest 608/608 green,
eslint --max-warnings 0 clean.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(agentctl): per-repo workspace tracker for multi-repo git/status
The git/status endpoint now accepts an optional ?repo=<subpath> query
param that selects a sub-directory of the workspace, completing the
per-repo plumbing started in the multi-repo phase. Single-repo callers
keep the workspace-root behavior unchanged.
- Manager.GetWorkspaceTrackerFor(subpath) returns a per-subpath
WorkspaceTracker (lazy + cached). Validation reuses a new
resolveSubpath helper shared with GitOperatorFor, so both code paths
reject path traversal / absolute paths / missing dirs the same way.
- Per-subpath trackers are NOT started (no polling goroutines); the
multi-repo path uses them only for synchronous GetCurrentGitStatus
queries which fall through to getGitStatus when no cache is present.
This keeps per-repo cost cheap.
- handleGitStatus reads c.Query("repo") and routes through the new
resolver, returning a 400 with an error payload on invalid input.
- Tests cover empty subpath (root), valid subpath caching, traversal
rejection.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(web/review): group file tree by repository for multi-repo task changes
The review file tree now renders per-repo top-level nodes when a task's
changes span multiple repositories, so the same file path in two repos
no longer collapses into a single ambiguous entry.
- ReviewFile.repository_id and ReviewFile.repository_name (optional)
identify which repo each file belongs to. Single-repo callers leave
them unset and the tree falls back to the existing flat grouping.
- buildFileTree splits into buildMultiRepoTree (when 2+ distinct
repository_names are present) and buildFlatTree (the original logic).
Repo roots have isRepoRoot=true and a repositoryId for downstream
filters; their children are full path subtrees per repo.
- collapse() skips repo roots so a repo with a single changed file
still shows its repo header.
- ReviewFileTree renders repo roots with a stronger label, a top
border, and an "N files" count badge; data-testid="repo-root-node"
for E2E coverage. File-level rendering is unchanged.
- Tests cover the flat-tree fallback (no repo info, single repo),
multi-repo grouping, repo-root pinning, and same-filename-different-
repos isolation.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(web/task-top-bar): "+N" repo chip on multi-repo task header
The task page top-bar breadcrumb now shows a small "+N" chip next to the
primary repository name when the task spans more than one repository,
matching the kanban-card treatment introduced earlier. Hover surfaces the
exact count via tooltip ("frontend (+2 more repos)").
- resolveTaskProps populates repositoryCount from task.repositories.length.
- TaskTopBar threads repositoryCount through to the new
RepositoryBreadcrumb subcomponent.
- The chip carries data-testid="topbar-extra-repo-count" for E2E
selectors.
- TopBarLeft was nearing the function-length / complexity caps; the
breadcrumb extraction restores headroom for future additions.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(web/task-create): disable "Add repository" when no other repos available
When the user only has the primary repository in their workspace,
clicking "+ Add repository" used to insert a row whose dropdown was
empty — looking like a bug ("no other repos show up"). The button now
disables itself when there are no remaining workspace repositories to
choose from, with a tooltip explaining why ("All workspace repositories
are already added"). The previous "primary not selected" guard is also
surfaced via tooltip.
- ExtraRepositoryRows computes remainingCount = repos − used (primary
+ already-added extras) and gates the Add button on it.
- New computeAddButtonHint helper returns a per-state title string for
the disabled cases.
- Tests cover: dropdown shows the unused workspace repos when opened
(the click path the user reported wasn't actually broken — the gate
was just missing); Add button disabled when only the primary exists.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(agentctl): workspace tracker falls back to first repo subdir for multi-repo task roots
The "Changes" panel was empty for multi-repo tasks because the
agent's workspace path is the task root — a plain holder directory
that holds one git worktree per repository as siblings. The workspace
tracker tried to run git commands against that bare root and silently
failed, so no git status updates ever reached the UI.
The tracker now detects this at construction: when the configured
workDir isn't itself a git repo, it falls back to the first immediate
child directory that is. The user gets the primary repo's changes in
the panel; cross-repo aggregation is a follow-up.
- preferGitRepoChildIfRootIsBare scans os.ReadDir entries (skipping
hidden / non-directory entries) and returns the first that has a
valid git index. Logs an info line when the rewrite happens so it's
visible in the agentctl log.
- Repo-less tasks (e.g. quick chat) and single-repo tasks keep their
previous behavior verbatim — the fallback only fires when the root
itself fails resolveGitIndexPath.
- Tests cover: fallback when the workdir is a multi-repo holder,
no-fallback for a real git repo, no-fallback when no git child
exists.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(multi-repo): per-repo workspace tracking — Changes panel shows all repos
The agent's workspace tracker is now split per-repository for multi-repo
task roots. Each repo gets its own tracker that polls git independently
and stamps its repository name onto every emitted event. The Changes
panel aggregates files from every repo and groups them under per-repo
headers using the file-tree machinery from Phase 6.
Backend (agentctl):
- GitStatusUpdate / FileListUpdate gain a RepositoryName field. Empty
for single-repo workspaces (legacy clients see no change).
- WorkspaceTracker carries repositoryName and stamps it onto every
emitted status/file-list. Two constructors: NewWorkspaceTracker for
the legacy single-repo path (with the bare-root fallback) and
NewWorkspaceTrackerForRepo for explicit per-repo trackers.
- Manager scans direct child directories for git repos at construction.
When 2+ repos are found, builds one tracker per repo subdir on top of
the root tracker (which stays bound to the bare task root and emits
nothing useful). Single-repo workspaces are unchanged.
- Manager.SubscribeWorkspaceStream / UnsubscribeWorkspaceStream fan out
one client subscription across the root + all per-repo trackers via
AttachWorkspaceStreamSubscriber / DetachWorkspaceStreamSubscriber on
the tracker. The WS handler switched from the single-tracker subscribe
to the manager fan-out.
- Tests cover: per-repo trackers spawn for multi-repo task roots,
single-repo doesn't, both repos emit GitStatusUpdates tagged with
RepositoryName.
Frontend:
- GitStatusEntry / GitStatusData carry repository_name (optional).
- session-runtime gitStatus.byEnvironmentRepo new map keyed by env id
then repository name; setGitStatus routes per-repo updates into it
while still updating the legacy byEnvironmentId for single-repo
consumers. clearGitStatus drops both maps.
- migrateEnvKeyedData migrates the new per-repo map alongside the
existing keys when sessionId resolves to an environmentId.
- WS git-status handler forwards repository_name from event.status.
- New useSessionGitStatusByRepo hook returns sorted [{repo, status}]
pairs for the active session.
- task-changes-panel mergeReviewFiles iterates statusByRepo when
available, stamping repository_name onto every uncommitted file so
the Changes panel groups them under per-repo headers (using the
ReviewFile.repository_name plumbing from Phase 6).
- Tests: per-repo routing, no overwrite on sibling repo update,
legacy single-repo fallback, clear-on-cleanup.
Verified: pnpm exec vitest run 623/623, eslint --max-warnings 0 clean,
tsc --noEmit clean, make test (backend) clean, make lint 0 issues.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(web/changes-panel): per-repo group headers above each repo's files
The Changes panel now renders a sticky "frontend / N files" header above
the diffs from each repository so the user can visually distinguish
where each change lives. Single-repo tasks render unchanged (no header
shown when there's only one unnamed group).
- ReviewDiffList groups its file list by repository_name (preserving the
caller's order within each group), then renders one RepoGroupHeader
per group followed by the existing FileDiffSection list. Header gating
rule: show when len(groups) > 1 OR the single group has a non-empty
repository name.
- Group key uses "repo:path" so a file with the same path in two repos
no longer collides on React's reconciliation.
- groupFilesByRepository + RepoGroupHeader live in a sibling
review-diff-list-groups.tsx file to keep review-diff-list.tsx under
the 600-line lint cap.
- Tests cover: no header for plain (no repo_name) files, one header per
repo for 2+ groups, header shown even for a single named repo, file
paths land under the right group's section.
Verified: vitest 627/627, eslint clean, tsc clean.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(lifecycle): propagate RepositoryName through PublishGitStatus
The agentctl WorkspaceTracker stamps every per-repo GitStatusUpdate
with RepositoryName, but the lifecycle PublishGitStatus translation
into GitEventPayload was dropping that field. The orchestrator then
forwarded a status with empty repository_name, the frontend store
routed everything into the empty-key bucket, and the Changes panel's
multi-repo grouping never triggered — no header, no per-repo split.
- GitStatusData gains RepositoryName (omitempty so single-repo
payloads stay byte-identical on the wire).
- PublishGitStatus copies update.RepositoryName onto the payload.
- Test asserts the field survives the publish → subscribe round trip
via the in-memory event bus on the GitEvent.<sessionID> subject.
Restart the agentctl process for any running multi-repo task to pick
up the new field; new launches get it automatically.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(web/changes-panel): per-repo group headers in the dockview Changes tab
The Changes tab in the dockview panel (the Files / Changes (N) tab the
user actually interacts with — not the standalone Review dialog) was
still rendering a flat file list, so multi-repo task changes either
collided onto the legacy single-status (showing only one repo's files)
or rendered without grouping.
- FileInfo gains repository_name (optional, single-repo unchanged).
- useSessionGit aggregates files from useSessionGitStatusByRepo when
multi-repo data is present, stamping each FileInfo with its
repository_name. Falls back to the legacy single status for
single-repo workspaces. Aggregation extracted into
aggregateFilesAcrossRepos to keep useSessionGit under the 100-line
function lint cap.
- ChangedFile (changes-panel.tsx + changes-panel-timeline.tsx) carries
repositoryName through to the renderer.
- FileListSection delegates to a new FileListBody that groups rows by
repositoryName; renders a small per-repo subheader when 2+ repos OR
any single named repo is present, otherwise falls back to the
pre-multi-repo flat list (same DOM shape).
- Tests cover: no header for plain (no repo_name) files, headers
per repo for 2+ groups, single named repo still gets a label.
Restart the backend so the per-repo trackers spawn for in-flight
multi-repo tasks; new launches are unaffected.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(multi-repo): per-repo git ops, unified branch endpoint, chip-style task create
Bundles a string of multi-repo fixes and refactors that came up while
testing the feature end-to-end:
- workspace_path stored on multi-repo TaskEnvironment was one level too
high, so agentctl booted with the wrong WorkDir on cold start; fixed
in resolveTaskEnvWorkspacePath + GetWorkspaceInfoForSession.
- Stage/Unstage/Discard/Commit/Revert/Reset/Amend now thread `repo`
through agentctl client → WS handler → frontend hook → SessionGit
fan-out, so the right git repo is targeted (was running at the task
root, which isn't a git repo for multi-repo tasks).
- agentctl GitLog is multi-repo aware: when no `repo` is specified and
per-repo trackers exist, fans out one log per repo and merges sorted
newest-first with RepositoryName stamped on each commit.
- Per-repo grouping for files (Changes panel) and commits, with
collapsible group headers; commits section defaults to collapsed.
Algorithm extracted into one generic groupByRepositoryName helper
(was copy-pasted three times).
- Task creation dialog rewritten around a unified `fs.repositories[]`
list — no more "primary vs extras" split. Chip-style row with one
pill per repo + branch, "+" to add, x to remove, GitHub URL toggle
inline. Discovered (on-machine) repos appear in any chip's dropdown.
Removes ~6 fields and 3 helpers from the dialog state.
- Branch listing endpoints collapsed to one:
GET /workspaces/:id/branches?repository_id=X | path=Y
Frontend collapses to a single useBranches hook with a tagged-union
source. Drops two HTTP routes, two service methods, two API client
funcs, and one whole hook file.
Tests added across the chain: workspace_path resolver, sort-commits-
by-date, manager poll-mode propagation, RepoSubpaths, chip grouping
+ collapse + per-repo isLatest, group-by-repo helper, dialog payload
builder + chip wiring + discovered-repo flow.
* feat(multi-repo): unified per-repo git controls for all VCS operations
Changes panel and top-bar VCS button render per-repo Stage all, Commit,
Unstage all, Push, Pull, Rebase, Merge, and Create PR uniformly across
single-repo and multi-repo workspaces. Operations route to the correct
git repo subpath; fan-outs collect per-repo results and surface partial
success in the toast layer.
Backend:
- Stamp RepositoryName on Reset/BranchSwitch/FileChange notifications
and forward through orchestrator handlers
- Surface per-repo errors from handleGitLogMultiRepo via PerRepoErrors
Frontend:
- pull/push/rebase/merge/abort/commit fan out via PerRepoOperationResult
- Per-repo group headers always render with action buttons
- Top-bar dropdown lists per-repo Pull/Rebase/Merge entries
- pendingStageFiles keyed per-repo to prevent cross-repo clears
- useRepoDisplayName distinguishes loading from multi-repo
- Commit dialog file summary respects stageAll
- New helpers: changes-panel-helpers.ts, changes-panel-repo-groups.tsx,
use-repo-display-name.ts
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(multi-repo): plumb repo through file/commit/diff endpoints; per-repo PRs and review UI
- backend: thread `repo` subpath through pull/push/rebase/merge/abort, file
content/update, cumulative-diff, and commit-show endpoints; per-repo
fan-out for log + cumulative-diff with each repo's own merge-base; emit
full `repositories` array on task.updated WS payload
- github: store/list TaskPRs as []*TaskPR (per-repo); orchestrator/web store
rekey to `Record<taskId, TaskPR[]>`
- web: aggregate PR icon (count badge + worst-of color, multi-PR tooltip);
branch hover card renders one row per repo with each repo's task base
branch; review dialog merges per-repo file maps with composite keys and
propagates repository_name through diff/expand/revert; commit-detail
panel scopes to the owning repo
- kanban card: chip row of repo names (up to 2 + "+N" overflow, tooltip
lists all)
- task-create dialog: gate non-worktree executors to multi-repo only; move
fresh-branch toggle inline next to the repo chips (icon-only, with
title/aria-label)
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(multi-repo): scope PR watches/associations and review UI per repository
Schema: add repository_id to UNIQUE constraints on github_pr_watches and
github_task_prs (with idempotent copy-and-rename migration) so one task
spanning N repos can hold one watch/PR per repo without collisions.
Plumb repository_id through CreatePRWatch/EnsurePRWatch/AssociatePRWithTask
/AssociatePRByURL, fan TriggerPRSync across all per-repo watches, and
disambiguate status sync by (task, owner, repo, pr_number) instead of
"first match". Add Repo to wsCreateFile/Delete/Rename and wsCreatePR/
RenameBranch so file and git ops target the right working tree.
Frontend review dialog: switch reviewedFiles, staleFiles, fileRefs,
selectedFile and comment counts to NUL-separated `${repo}\x00${path}`
keys so same-named files in different repos no longer share state.
useTaskPR consumes the new {prs: TaskPR[]} envelope.
Also: snapshot repoTrackers slice before background refresh in
SetWorkspacePollMode to prevent races with Stop(), and key
mergeCumulativeFiles by NUL-separated (repo, path) to match.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* chore: apply prettier formatting
Unblock CI prettier check on files modified earlier on this branch
that pre-date the current prettier config.
* fix: address PR review feedback and unblock E2E
Backend:
- ListWorkspaceTaskPRs background sweep now uses TriggerPRSyncAll so
multi-repo tasks refresh every per-repo PR (was only the most recently
updated watch). Caught by claude review on #767.
Frontend (no behaviour change):
- Replace literal NUL byte (0x00) in FILE_KEY_SEP and the inline key
builder in use-review-dialog with the explicit escape so the
source is plain ASCII. The runtime separator is unchanged; this just
removes a footgun that GitHub's diff renders as a visible space.
- Show the auto-resolved branch as a small read-only pill next to the
GitHub URL input so the URL-mode dialog surfaces the branch the task
will run on (previously only the URL was visible).
E2E:
- changes-panel-focus / pr-detection / pr-switcher-changes: expand the
commits-section before asserting on commit text -- the section starts
collapsed since the chip-style refactor.
- repository-selector-scroll: open the popover via repo-chip-trigger
instead of the legacy single repository-selector testid.
* test(e2e): align task-create dialog tests with chip refactor
Updates three more tests that broke when the dialog migrated from a
single repository/branch combobox to per-row repo/branch chips and the
URL-mode toggle relabel:
- create-task.spec.ts (pre-select + remembers-selections): switch
repository-selector / branch-selector lookups to repo-chip-trigger /
branch-chip-trigger.
- create-task-github-url.spec.ts (toggle test): the inverse-state label
is now "use a workspace repository", not the legacy
"or select a repository".
No code changes — purely test alignment with the existing UI.
* fix(web): re-apply workbench-chrome topbar + restore branch-selector behaviour
Two regressions surfaced by the multi-repo merge into main:
1. task-top-bar.tsx had been resolved in our favour at the workbench-chrome
merge (#761), losing the TopbarActionOverflow wiring that the new test
asserts on. Re-applies main's topbar structure (TopbarCluster groups,
TopbarActionOverflow, IntegrationsMenu, MoreToolsMenu, SettingsButton)
on top of our multi-repo additions: repositoryCount + RepositoryBreadcrumb
"+N" chip, gitStatus + isMultiRepo plumbing, and the GitAheadBehindBadges
suppression for multi-repo. GitAheadBehindBadges moved to its own file to
stay under the file-length cap.
2. The chip-style task-create dialog dropped the legacy branch-selector
semantics. Restored:
- Local-executor branch lock: the per-row branch chip is now disabled
when isLocalExecutor and fresh-branch is off. The user's checkout
dictates the branch for local execution; fresh-branch unlocks it.
- Branch override: when locked, the chip shows currentLocalBranch
instead of the value the user picked under a different executor
(so worktree -> local doesn't display a stale "develop" pick).
- Branch refresh: useBranches now exposes a refresh() callback for
id-based sources (calls listRepositoryBranches with refresh=true),
and Pill renders BranchRefreshButton in its popover header when
onRefresh is wired.
- URL-mode branch picker: GitHubUrlSection now renders a real
branch-chip-trigger Pill backed by fs.githubBranches instead of a
read-only span, restoring the "branch selectable in URL mode"
behaviour (URL mode is always remote-clone, so the local-executor
lock does not apply).
E2E spec updated: bulk-rename legacy branch-selector / repository-selector
testids to branch-chip-trigger / repo-chip-trigger.first(). All 13
create-task-branch-selector tests pass locally.
* ci: force workflow re-run
* fix(web): unblock E2E shard 2
Three issues showed up only after the merge with main, which brought back
the E2E shard 2 tests touching git/changes-panel and improve-kandev:
1. Wire the TaskCreateDialog slot props (extraFormSlot, aboveDescriptionSlot,
bottomSlot, descriptionPlaceholder) through to DialogPromptSection. They
were declared on the public API but dropped on the floor before this
change, so Improve Kandev's contributor banner ("@octocat" /
"@kandev-maint") never rendered. Also gate the "or paste a GitHub URL"
toggle on lockedFields.repository so the locked-repo dialog hides it
(matches main's behaviour and the existing test expectation).
2. Revert "commits section starts collapsed" (introduced in
|
||
|
|
6fa38cd62c |
fix(terminal): re-key user-shell RPCs to task_environment_id (#770)
* refactor(backend): re-key user-shell RPCs to task_environment_id user_shell.list/create/stop now take task_environment_id directly instead of session_id with a session→env translation. Sessions in the same task share one shell list, so the env is the natural scope. HTTP route /api/v1/sessions/:id/terminals → /api/v1/environments/:id/terminals. shell.status/subscribe/input (agent passthrough) stay session-keyed. * fix(backend): heal task_sessions.task_environment_id on boot Sessions created via paths that didn't write the FK left shell ops broken — every env-keyed user-shell RPC failed to resolve the env and the frontend silently swallowed the error. The boot heal links each orphan session to its task's existing env (idempotent, runs after backfillTaskEnvironments + healDuplicateTaskEnvironments). * refactor(web): drop session_id from user-shell API, hooks, and slice createUserShell/stopUserShell/fetchTerminals take environmentId. useUserShells takes envId directly (no environmentIdBySessionId translation). useTerminals takes both (env for shell ops, session for tab UX persistence). Slice actions key writes by env id directly, removing the silent no-op when the session→env mapping wasn't set yet. * refactor(web): wire env id through dockview, right-panel, and SSR call sites Dockview "+" menu, terminal scripts dropdown, dev-preview button, right-panel "+" button, Ctrl+\` toggle, watermark, and panel-close cleanup all source the env id from useEnvironmentId() / store and pass it to createUserShell/stopUserShell. Shell-mode passthrough terminal no longer blocks on session id — env id is sufficient. SSR resolves the active session's task_environment_id from the session list and calls fetchTerminals with the env id. * test(e2e): cover env-keyed terminal new-tab flows + WS payload guard New spec exercises the right-panel "+" button and the dockview "+ → Terminal" menu, asserts the new tab connects, and intercepts outgoing user_shell.create WS frames to verify the payload carries task_environment_id and never session_id. A heal-coverage assertion checks every listed session has task_environment_id populated post-boot. * fix(e2e): drop right-panel '+' test, dockview is the only '+' path on desktop The mobile TaskRightPanel SessionTabs aren't rendered at desktop viewports — the dockview '+' menu is the sole new-tab affordance, so testing the mobile path on chromium just timed out clicking a button that never mounted. Consolidate into a single dockview '+ → Terminal' test that asserts the new panel gets the backend-assigned 'Terminal 2' label (proving env-shared shell list) plus the WS-payload regression guard. * fix(e2e): assert dockview terminal-tab count, not 'Terminal 2' label Dockview hardcodes the panel title to 'Terminal' for every tab — only the mobile right-panel SessionTabs surface the backend's 'Terminal N' label. Switch the assertion to count tabs to keep the e2e green. * test(backend): cover SQL NULL branch of healSessionTaskEnvironmentIDs Adds insertSessionWithNullEnvID and TestHealSessionTaskEnvironmentIDs_LinksNullOrphans so the WHERE-clause IS NULL predicate is exercised — the existing tests only seeded empty-string FKs. Also adds LIMIT 1 to the heal subquery as a defensive measure: the unique index already guarantees ≤1 row per task at runtime, but the SQL now reads as deterministic in isolation. * refactor(web): drop dead taskGateOk on agent terminal WS gate Agent passthrough routes by sessionId, not taskId — taskId was only used for logging. Gating the WS effect on taskId forced agent terminals to wait for useSession() to hydrate, adding a refresh-race window that didn't guard any real precondition. connectionKey + canConnect already cover the meaningful preconditions. * fix(web): stamp env id on terminal panels for accurate cleanup addTerminalPanel now bakes environmentId into the panel's params at creation time. The dockview-layout-setup onDidRemovePanel handler prefers the stamped env over re-deriving from the active session, so closing a terminal that was opened under a different task no longer calls stopUserShell with the wrong env (which silently no-ops on the backend and leaks the shell process until session teardown). Falls back to the active session's env for legacy panels persisted before the param existed. * fix(web): keep task id logging out of terminal reconnect deps |
||
|
|
c0ce55ff9d |
fix: unblock terminals stuck on Connecting and heal task_environments (#769)
* fix(gateway): snapshot agentctl status and env id on session subscribe Late-subscribing clients (page reload, task switch, WS reconnect) miss the agentctl_ready and task_environment_id events fired during the original launch. Without them, the env-routed shell terminal gate stays stuck and the panel hangs forever on "Connecting terminal...". - appendSessionStateMessage now carries task_environment_id so the frontend can seed environmentIdBySessionId for the active session. - New appendAgentctlStatusMessage snapshots current agentctl readiness via a bounded Health probe and emits session.agentctl_ready / _starting to flip the client gate out of `undefined`. - GetOrEnsureExecutionForEnvironment now publishes AgentctlStarting to mirror EnsureWorkspaceExecutionForSession and keep gate transitions monotonic. * fix(web): drop shell-mode agentctl gate to unblock env-routed terminals The shell terminal gate required agentctlStatus.isReady to flip to ready before opening the WS, but that event may have fired before the client subscribed (page reload, task switch, local-executor task switch). The lazy create-on-terminal-connect path on the backend never fires either, because the WS never opens — chicken-egg. Backend env handler already waits for remote workspace readiness server-side (waitForRemoteExecutionReadyWithTimeout), so the client just needs the routing keys (env id + session id). Agent-mode terminals still gate on readiness — the agent process must be wired before passthrough I/O is meaningful. Extract computeCanConnect helper to keep PassthroughTerminal under the complexity ceiling. * test: cover terminal-hang on subscribe snapshot and env-routed gating Adds a backend unit test asserting appendSessionStateMessage carries task_environment_id (the deterministic RED for the env-map seed bug), plus E2E scenarios for the user-facing repros: cold load, kanban switch, hard reload, sidebar switch, and a local-executor task switch that used to poison the terminal for every subsequent task. Adds an expectTerminalConnected helper to SessionPage to detect the "Connecting terminal..." overlay. * fix(repo): heal task_environments on boot and enforce one env per task Live DB audit on the user's machine surfaced two corrupt-data classes that manifest as the user-facing "terminal doesn't appear / Failed to fetch terminals: 503" symptoms: - 15 worktree-mode envs with worktree_path set but workspace_path empty. GetOrEnsureExecutionForEnvironment returns ErrSessionWorkspaceNotReady forever, the env terminal handler 503s. - 3 tasks with two task_environments rows (race in lazy create — pairs 4-6 s apart, both ready, distinct workspace_path). healTaskEnvironmentWorkspacePaths backfills workspace_path = worktree_path on the affected rows, and healDuplicateTaskEnvironments collapses each duplicated task to its most recently updated env (re-pointing any sessions still on the loser, then dropping the loser row). ensureTaskEnvironmentTaskUniqueIndex adds a UNIQUE index on task_environments(task_id) so a future race fails loud. Order matters: heal must run first to drop duplicates before the unique index is created. * fix(repo): reject empty workspace_path on worktree envs The orchestrator's persistTaskEnvironment update branch previously left workspace_path untouched, so an env created with empty workspace_path would stay corrupt forever — terminals 503 with ErrSessionWorkspaceNotReady. Tighten the repository boundary: CreateTaskEnvironment refuses worktree-mode inserts without workspace_path, and UpdateTaskEnvironment refuses to clear it back to empty. A future writer regression now fails loud at the boundary instead of producing a row that breaks the env terminal handler. * fix(executor): refresh paths on env update and dedupe by task The persistTaskEnvironment update branch only mutated AgentExecutionID + Status, so envs created earlier with an empty workspace_path stayed permanently broken across launches. Refresh worktree + workspace fields on update via a shared computeWorkspacePath helper used by both branches. Concurrent launches for the same task previously raced inside this function — each saw existingEnv == nil and each called CreateTaskEnvironment, producing duplicate rows ~4-6 s apart in the live DB. Hold a per-task mutex (mirroring sessionLocks) and re-fetch the env inside the critical section so siblings reuse the row the first launch persisted. The repo's unique index on task_environments(task_id) is the durable guarantee; the lock just closes the window before the constraint trips. * test(repo): cover task_environments heal, validation, unique index Repository unit tests for the new heal steps and writer guards: - HealTaskEnvironmentWorkspacePaths_BackfillsEmpty / LeavesPopulatedAlone / Idempotent - HealDuplicateTaskEnvironments_KeepsMostRecent (asserts session re-link to winner) / NoOpWhenSingle - EnsureTaskEnvironmentTaskUniqueIndex_BlocksFutureDuplicates - CreateTaskEnvironment_RejectsEmptyWorkspaceForWorktree / AllowsNonWorktreeWithEmptyWorkspace - UpdateTaskEnvironment_RejectsClearingWorkspaceForWorktree Tests live in the sqlite package so they can exercise the unexported heal* helpers directly, mirroring how the boot path calls them. * fix: address PR review feedback - helpers.go: drop the 2 s blocking Health probe in appendAgentctlStatusMessage. sendSessionData runs in the WS read loop, so the probe was delaying every subscribe/focus ACK by up to 2 s. Use GetWorkspaceStream() != nil as the cached readiness signal — the stream manager only attaches it after Health succeeds, so its presence is a free, non-blocking proxy. Subsequent waitForAgentctlReady events still correct the snapshot if it picks the wrong one. - manager_execution.go: publish AgentctlStarting INSIDE createExecution, before spawning waitForAgentctlReady. The previous order published it after createExecution returned, which let the goroutine race past on fast Health and deliver Ready before Starting. Drop the now-redundant publishes in EnsureWorkspaceExecutionForSession and the env lazy-create path. - task_environment.go: replace the local executorTypeWorktree constant with models.ExecutorTypeWorktree (canonical) so a future rename of the type is caught by the compiler. - base.go: heal_workspace_path now respects the kandev path layout — task-dir-mode envs (.../.kandev/tasks/<name>/<repo>) get Dir(worktree_path) as workspace_path, mirroring computeWorkspacePath in the orchestrator. Plain worktree mode unchanged. - heal_test.go: tighten unique-constraint assertion to require a UNIQUE error string so the test fails loudly if a different DB error masks the constraint violation. Add task-dir-mode heal coverage. |
||
|
|
83e01477df |
refactor: extract shared shapes between jira and linear integrations (#759)
* refactor: extract shared shapes between jira and linear integrations Factors out the duplicated logic between internal/jira + internal/linear (backend) and components/jira + components/linear (frontend) into reusable shared shapes. Follow-up to PR #736. Backend: - New internal/integrations/secretadapter wraps secrets.SecretStore with the upsert API both jira.SecretStore and linear.SecretStore expect; drops both adapter copies from cmd/kandev/services.go. - New internal/integrations/healthpoll owns the 90s auth-health loop via a Prober interface; linear/poller.go is a thin wrapper, jira/poller.go keeps only its (jira-specific) issue-watch loop. - provideServices split into initGitHubService / initJiraService / initLinearService — now 75 lines (was ~125, exceeded the 80-line limit). Frontend: - New hooks/domains/integrations/use-integration-availability.ts and use-integration-enabled.ts; jira/linear hooks reduce to one-line delegates and move from components/<x>/ to hooks/domains/<x>/ per the project convention Greptile flagged on PR #736. - New components/integrations/{auth-status-banner,auth-error-message, validated-popover}.tsx; the four link/import-bar wrappers drop from ~110 lines to ~35 each, and JiraLinkButton picks up the clear-error-on-close behaviour LinearLinkButton already had. Docs: - New 'Adding a new third-party integration' section in CLAUDE.md/AGENTS.md documenting the shared shapes and intentional Jira/Linear divergence (transition vs state, JQL vs filter object, eventBus only on jira). * fix: address PR #759 review feedback - validated-popover: outline-with-label trigger now sets type="button" and honours triggerDisabled, matching ghost-icon (Greptile, Claude, CodeRabbit). - jira-link-button / linear-link-button: move updateTask() into the awaited fetch path so a failed title update surfaces as the popover's inline error instead of an unhandled rejection (CodeRabbit). - use-integration-availability: monotonic requestId race guard prevents a slow earlier probe from overwriting a newer auth result (CodeRabbit). - use-integration-enabled: drop unnecessary async on synchronous init (Claude). - jira/linear poller smoke tests: wrap in testing/synctest instead of time.After(2s) per backend testing convention (CodeRabbit nitpick). Skipped with reasoning on the threads: - secretadapter Set TOCTOU — single-process SQLite + single-writer per workspace; constraint violation on conflict surfaces as an error to the caller, not silent corruption. - healthpoll SetInterval validation — the function isn't called from production code today; a panic on misuse would be a footgun and silent ignore would just hide bugs differently. * fix: address PR #759 round-2 review feedback - jira-settings / linear-settings: drop the local STATUS_REFRESH_MS = 90_000 copies and import the shared INTEGRATION_STATUS_REFRESH_MS so changing the cadence only touches one file (Claude). - healthpoll: rename Poller.ProbeAll → probeAll. The only callers are the loop and the in-package tests, so widening the public API wasn't buying anything (Claude). * fix: invalidate ValidatedPopover in-flight submits on close If the user clicked Submit and dismissed the popover before the request settled, the next time they re-opened it would inherit `loading=true` (the disabled-while-loading submit button was stuck) and a late rejection from the previous session could repopulate `error` in the new one. Track the submit via a `useRef` token, bump it on close, and ignore any result/rejection whose token no longer matches. Also reset `loading` and `error` on close so the next open starts clean. Adds a regression test covering both the late-resolution and late-rejection paths. Addresses CodeRabbit review on PR #759. * ci: trigger workflow re-run GitHub Actions did not dispatch the pull_request workflows on the previous two pushes ( |
||
|
|
396325b2c9 | feat: allow reordering sidebar views (#764) | ||
|
|
5022469187 | fix: refine workbench action button layout (#762) | ||
|
|
68c94dfa3a |
feat: refine workbench chrome (#761)
* docs: define product and design direction * style: establish restrained workbench primitives * refactor: declutter board and task topbars * style: align dockview panel chrome * fix: align task topbar vcs control sizing * refactor: surface integrations navigation * fix: repair integrations and task creation workflow * refactor: include linear in integrations navigation * style: restore familiar workbench tones * fix: add dockview tab breathing room * fix: show configured integration shortcuts * fix: hover open integrations menu * fix: stabilize integrations hover menu * fix: align home and dockview topbars * fix: normalize home topbar controls * fix: compact home topbar height * fix: simplify home chrome and split buttons * fix: move integrations to left chrome * fix: restore home stats shortcut * fix: add dockview tab breathing room * fix: border home stats button * fix: streamline dockview topbar actions * fix: align task split button seam * fix: address review feedback * fix: restore topbar e2e affordances * fix: stabilize workflow e2e isolation * fix: add task topbar action overflow * fix: address PR review feedback |
||
|
|
e16debeaca | fix(preview): server-authoritative session ensure for kanban preview (#760) | ||
|
|
1eae869054 | feat: add improve kandev in-app contribution flow (#740) | ||
|
|
9d275d3009 |
refactor: route user terminals by environment (#758)
* refactor: route user terminals by environment * refactor: remove terminal compatibility shims * fix: address terminal PR review feedback * fix: wait for remote shell readiness * fix: require agent mode for session terminals |
||
|
|
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 |
||
|
|
7b5b3f329d |
feat: add Linear integration (#736)
* feat: add Linear integration
Mirrors the existing Jira integration so a workspace can connect to Linear
with a personal API key, browse and search issues, and start a Kandev task
from any issue. Backend uses Linear's GraphQL API with a workspace-scoped
config + secret store and a 90s auth-health poller; the frontend adds a
settings page, an issue dialog with state transitions, link/import buttons
in the task UI, and a paginated /linear browse page with a Start task flow.
* fix(linear): address PR review feedback
- Route httpListStates through Service.ListStates instead of clientFor
- Extract repeated workspace_id required string into a constant
- Log GetConfig errors during credential resolution instead of dropping them
- Clear popover error state on close in import + link buttons
- Pick the first workflow with steps in the quick task launcher (don't hard-pin to workflows[0])
- Reset useLinearEnabled state on workspace change via tag-based pattern
- Add cursor-pointer to anchor + Link elements per project convention
- Add tests: handlers_test.go, linear-api.test.ts, use-linear-enabled.test.ts, linear-issue-common.test.tsx
- Document internal/jira and internal/linear in CLAUDE.md/AGENTS.md
* fix(linear): address second-round PR review feedback
- Round Linear's fractional state position instead of truncating it via
int(math.Round(s.Position)) so adjacent fractional positions don't collapse
- Force LinearSettings to remount on workspace change with key={id} so the
previous workspace's config / form / testResult / teams cannot bleed into
the next workspace before the new fetches settle
- Broaden the legacy-value useLinearEnabled test to cover "true", "1", "yes",
and "legacy" so a regression to strict value === "true" would fail
* fix(linear): address third-round PR review feedback
- Propagate context.Canceled / context.DeadlineExceeded from ListStates in
GetIssue instead of swallowing them — a caller cancellation must not return
a partial-success response
- Drop the one-off contains() test helper in handlers_test.go and use
strings.Contains directly so the assertion is self-documenting
- Add getLinearIssue URL-shape test that locks identifier-as-path-segment
vs workspaceId-as-query-param so accidentally swapping them would fail
* test(linear): extract repeated literals in linear-api.test.ts
Pulls "ws-1", the API base URL, and the auth-method literal into module-
level constants so the assertions read as the route shape they care about
and the duplicate-strings ESLint rule (currently warn, threshold 4)
stays quiet as the file grows.
---------
Co-authored-by: Kandev Agent <agent@kandev.dev>
|
||
|
|
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. |
||
|
|
aa70b7dfa4 |
feat(web): mobile terminal key-bar — Ctrl/Shift modify OS-keyboard input + iOS keyboard fixes (#741)
* feat(web): add mobile terminal key-bar with sticky Ctrl modifier Mobile software keyboards lack Ctrl, Esc, Tab, arrows, and shell symbols, so users had no way to interrupt a running command (Ctrl+C), escape an editor, or navigate the shell. Adds a horizontally scrolling key-bar above the virtual keyboard — sticky Ctrl modifier, dedicated Ctrl+C / Ctrl+D buttons, and visualViewport-based docking so the bar tracks the OS keyboard as it opens and closes. * test(web): add E2E coverage for mobile terminal key-bar Nine Playwright tests on the mobile-chrome project cover visibility gating, key emission verified via captured WS frames, the sticky Ctrl state machine, dedicated Ctrl+C / Ctrl+D buttons, keyboard-aware docking, accessibility, and an end-to-end ^C echo to prove the shell path works. Adds a reusable shell.input frame capture helper and a small page-object for the key-bar. * feat(web): shared shell input helpers for the mobile terminal Introduces a small toolkit the next commit will wire into the mobile key-bar: - `refocusXtermTextarea` — re-focuses xterm's hidden textarea after a virtual-key tap so iOS Safari doesn't dismiss the soft keyboard. - `suppressIOSKeyboardAssists` — strips name/autocomplete/data-* hints on the textarea so iOS / 1Password / LastPass / Bitwarden skip the AutoFill suggestion bar. - `useShellModifiersStore` (Zustand) — module-level Ctrl/Shift latch state with toggle (off → latched → sticky → off) and consume actions. - `applyShellModifiers` — pure transform: ctrl + a-z → control byte; shift + tab → CSI Z (reverse tab); single-char only. - `sendShellInput` — single entry point that reads the store, applies the transform, sends over WS, and consumes latched modifiers. `useShellKeySender` and the xterm `onData` path in `shell-terminal.tsx` now route through `sendShellInput` so any future modifier state propagates uniformly. `suppressIOSKeyboardAssists` is wired into `useTerminalInit` immediately after `terminal.open()`. * feat(web): mobile key-bar pivots to OS-keyboard input + pins to keyboard Reworks the mobile terminal key-bar around two insights: 1. The OS soft keyboard already provides letters — duplicating them in-bar was confusing and didn't address the real problem (Ctrl couldn't modify what the user typed via the OS keyboard, because that input went straight to xterm and bypassed React state). 2. iOS Safari `position: fixed; bottom:` drifts during visual-viewport scroll while the keyboard is open. Anchoring via `top` tied to the visual viewport's bottom edge stays glued. Changes: - `MobileTerminalKeybar` reads Ctrl/Shift state from the shared `useShellModifiersStore` and toggles it on tap. The actual chord transform now happens in `sendShellInput`, which both the bar and xterm's `onData` path call — so a Ctrl latch set from the bar modifies the next character typed on the OS keyboard. - Drops `LETTER_KEYS` and `chordChar` from the bar; adds a Shift toggle (Shift+Tab → reverse-tab from the bar; pairs with the modifier transform for OS-keyboard taps). - Latched state now shows the same ring as sticky (with sticky using a stronger `ring-primary`), so the first tap is visibly active. - `touch-action: manipulation` on each button so taps in the `overflow-x-auto` row don't get reinterpreted as scroll. - `useVisualViewportOffset` exports `viewportBottom = offsetTop + height`. When the keyboard is open the bar uses `top: viewportBottom - KEYBAR_HEIGHT_PX` and `bottom: auto`; closed, it keeps the `bottom`-based positioning so it sits above the bottom nav. - `SessionMobileLayout` computes the terminal panel's `paddingBottom` so terminal content's bottom edge sits exactly at the bar's top edge whether the keyboard is up or not. Tests cover the new modifier flow, Shift+Tab, the absence of in-bar letter buttons, and the `top`-anchored positioning when the keyboard is reported as open. * test(web): rewrite mobile key-bar e2e around real user flows The prior e2e leaned on in-bar letter buttons that no longer exist. Rewrites the spec around the journeys a mobile user actually performs: tap ^C to cancel, latch Ctrl + type a letter on the OS keyboard, sticky Ctrl across multiple chords, Shift+Tab from the bar, history nav with the arrow button, Esc, symbol keys, and the iOS positioning fixes (top-anchored when the keyboard opens, tracks visual viewport scroll, terminal panel reserves space for the bar). Adds a `shift` getter to the page object. * style(web): apply prettier formatting * fix(web): keep modifiers armed on dropped sends + tighten input handler Addresses bot review on PR #741: - `sendShellInput` no longer consumes Ctrl/Shift when the WS client isn't available — the chord stays armed so the user can retry once reconnected instead of seeing the latch silently clear. - Adds `send-shell-input.test.ts` with the matrix the helper needs: empty data, ctrl chord + auto-release, sticky retention, shift+tab → CSI Z, shift passthrough on non-tab, and the WS-disconnected no-send/no-consume path. - Removes dead `ctrlChord` export and its tests — the only consumer was its own test, and `applyShellModifiers` covers the same transform end-to-end. - Drops the unused `send` parameter from `useShellInputHandler` (the effect now goes through `sendShellInput` directly), removing a spurious dep that would otherwise re-fire the effect on identity changes. - Documents that `KEY_SEQUENCES.home`/`end` are intentionally readline shortcuts (Ctrl+A/E), not VT Home/End sequences, so future readers don't mistake it for a bug. * refactor(web): split mobile key-bar helpers, tighten null-viewport test Addresses two non-blocking suggestions from PR #741 review: - Move `KeyDef`, `KEYS`, `ModifierButton`, and `KeybarButton` out of `mobile-terminal-keybar.tsx` into a sibling helpers file. The main component drops from 213 to 115 lines, comfortably under the 200-line component convention. - Assert `viewportBottom === 0` in the null-viewport branch of `use-visual-viewport-offset.test.ts` so all three return-shape fields are covered there. |
||
|
|
8fb2e5ba52 | fix(lifecycle): publish agentctl events on workspace restore (#731) | ||
|
|
4277ef495a | fix(web): self-recover dockview layout from corrupt persisted state (#729) | ||
|
|
8694de20ea | fix(web): prevent crowding in narrow changes panel and dockview tabs (#727) | ||
|
|
2a3df653f7 |
feat(filters): add 'Has PR' task sidebar filter (#713)
Co-authored-by: Kandev Agent <agent@kandev.dev> |
||
|
|
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.
|
||
|
|
54b98ed9eb |
feat(jira): add Jira integration for ticket browsing, import, and task linking (#705)
* feat(jira): add Jira integration for ticket browsing, import, and task linking Adds a workspace-scoped Atlassian Cloud integration: per-workspace settings (site URL, email, API token or session cookie), encrypted secret storage, and a REST client for tickets, projects, search, and transitions. A background poller probes credentials on a 90s cadence and the keepalive runs immediately on save so the UI's connected indicator updates without delay. The /jira page provides ticket browsing with filter pills, custom JQL, saved views (including JQL), and configurable task-launch presets that prefill TaskCreateDialog. JiraLinkButton on the task top bar links an existing task to a ticket by prepending its key to the title; the ticket popover surfaces details, transitions, and re-auth prompts on session expiry. Topbar shows separate icon buttons for GitHub and Jira when each is configured. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(jira): address PR review feedback - Token-based pagination (pageToken/isLast) replaces broken offset/total pagination - Race-fix in service.clientFor: re-check cache after acquiring write lock - sync.Mutex guards poller started/cancel state against concurrent Start/Stop - Channel-based + atomic.Int32 test sync replaces time.Sleep polling in poller_test - Async auth probe on SetConfig with detached context.Background goroutine - Distinguish 4xx vs 5xx in httpSetConfig via ErrInvalidConfig sentinel - Propagate 4xx APIErrors from ListTransitions in GetTicket - Distinguish "not found" from real errors in jiraSecretAdapter.Exists - Log secret Exists errors instead of swallowing - walkADF: handle softBreak (was only hardBreak despite the doc-comment) - Consolidate JIRA_KEY_RE in jira-ticket-common; remove duplicates - ticket-row imports formatRelative/statusBadgeClass from common - isFilterState type guard validates SavedView.filters shape - task-presets readStorage preserves user's "cleared all" intent (returns []) - task-presets-section gates draft sync on `loaded` to avoid wiping in-progress edits - jira-api: caller `init` no longer overrides method/body via spread order - jira-settings: disable Test button when no saved/typed secret Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(jira): record auth probe failure even when probe ctx expires - RecordAuthHealth now uses a fresh context.Background-derived ctx with a 5s timeout for the UpdateAuthHealth DB write, so a probe that consumes its full 15s deadline still persists the failure (greptile P1). - SetConfig goroutine drops its redundant outer timeout — RecordAuthHealth manages its own probe timeout, so context.Background is enough. - Add Service.SetProbeHook so tests can synchronize on probe completion; service_test waitForAuthProbe now blocks on the hook channel instead of time.Sleep polling. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(jira): address coderabbit review feedback (round 3) - url.PathEscape ticketKey in GetTicket/ListTransitions/DoTransition paths to harden against malicious keys ('../../foo', 'KEY?override=x'). - factoryHit on the test fixture is now sync/atomic.Int32; the async probe goroutine spawned by SetConfig races the test thread under -race. - GetConfig logs Reveal failures (was silently dropped, leaving the UI to show "no expiry info" indistinguishably from a freshly-saved cookie). - DeleteConfig logs secret-delete errors instead of swallowing them. - clientFor distinguishes a secret-store failure from "not configured" so the UI doesn't render the configure-CTA for transient backend errors. - Unify ErrNotConfigured wire code: HTTP and WebSocket both return "JIRA_NOT_CONFIGURED" via a shared constant. - JiraLinkButton strips a leading "PROJ-123: " before re-prefixing so re-linking a task replaces the prefix instead of stacking it. - jira-settings clears testResult on save/delete so the alert reflects only the currently-saved credentials. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * test(jira): cover Reveal error path in clientFor Reveal-error case is distinct from "no config row" — the former wraps the underlying error, the latter returns ErrNotConfigured. Without this test the changed contract has no regression coverage. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(jira): route secret adapter Set through Exists Treating every Get error as "not found" turned a transient DB error on an existing row into a constraint-violation Create that masked the real cause. Reuse Exists so the "secret not found:" prefix is the only path that falls through to Create. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * test(jira): cover jiraSecretAdapter.Set branching Routing Set through Exists changed the contract: a transient DB error in Get now propagates instead of cascading into a constraint-violation Create. Add three tests so a future refactor cannot regress this without CI catching it. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(jira): split Reveal errors and guard Save for new configs - resolveCredentials now mirrors the clientFor split: a Reveal error surfaces as `read jira secret: %w` with a warn log instead of collapsing into "no token stored — paste one to test". - disableSave gains the same missing-secret guard as disableTest so a brand-new config can no longer POST an empty secret and leave the workspace stuck with hasSecret=false. 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> |
||
|
|
b04967bf70 | fix(web): show skeleton in file tree header while workspace path loads (#704) | ||
|
|
7a2160f1f9 | feat: show repository scripts in dockview "+" menu (#703) | ||
|
|
d431688557 | feat(web): auto-open plan panel with unseen-changes indicator (#650) | ||
|
|
74d44bd26b | feat(acp): support claude-acp Monitor tool and fix incremental tool_call updates (#698) | ||
|
|
9dc1065010 | fix(web): align sidebar filter toolbar height with panel headers (#699) | ||
|
|
c092fdeb1c | fix: ui polish — unified topbar, selector consistency, quick actions editor improvements (#693) | ||
|
|
1fc3544625 | fix: subtask sessions inherit agent profile from parent task (#692) | ||
|
|
046ac5c1b6 |
feat(web): add Ctrl+F search to session, plan, and terminal panels (#686)
* feat(backend): add session-scoped message search endpoint
Adds SearchMessages across repository/service/handler layers and a new
WebSocket action "message.search". Returns lightweight hits with a
60-char snippet around the first match. Session-scoped only: results
never leak across sessions. Uses dialect-aware LIKE/ILIKE so it works
on SQLite today and Postgres later.
* feat(web): add shared panel search bar and Ctrl+F hook
Introduces the reusable PanelSearchBar overlay and usePanelSearch hook
used by the per-panel search features. The hook listens on the window
and fires only when focus is within the panel root, preventDefaults the
browser find-in-page, and wires Esc to close. PanelRoot becomes a
forwardRef so panel components can expose their root element to the
hook. Adds SHORTCUTS.FIND_IN_PANEL plus shared highlight/flash styles.
* feat(web): add Ctrl+F search to terminal panels
Mounts the xterm addon-search on both passthrough and shell terminals,
exposing next/prev plus case-sensitivity and regex toggles. Invalid
regex surfaces an inline error without firing the search. Because xterm
consumes keystrokes before React, the Ctrl/Cmd+F intercept is added to
attachCustomKeyEventHandler so the shortcut opens the bar from inside
the terminal. Results counter comes from onDidChangeResults; decorations
clear on close.
* feat(web): add Ctrl+F search to plan panel
Adds a custom TipTap extension that builds a ProseMirror DecorationSet
over the document and exposes setPlanSearchQuery / planSearchNext /
planSearchPrev / clearPlanSearch commands. The current match gets an
extra class and scrolls into view. Match count is read from the plugin
state via transaction subscription, so edits to the document update the
counter automatically. Find-only — no replace.
* feat(web): add Ctrl+F search to session chat panel
Wires the new backend message.search action into a debounced client
hook and renders a hits dropdown under the search bar, with the query
highlighted in each snippet. Clicking a hit scrolls the message into
view and flashes it. When the target message is older than what is
currently loaded, the hook loops through loadOlder pages until the
message renders, making search work across lazy-loaded history without
needing to scroll-and-wait manually.
* fix(web): lazy-load xterm addon-search to avoid SSR crash
addon-search is published as a UMD bundle that references `self` at
module load. Statically importing it breaks the Next.js static
prerender of /. Deferring the import to an effect keeps the module out
of the SSR graph without changing runtime behaviour.
* fix(web): propagate plan editor via state so plan-search hook re-runs
The plan-search hook subscribed to editor transactions inside a
useEffect that depended on a ref object. Because ref.current changes do
not trigger effect re-runs, if the Editor instance was not yet ready
when the hook first mounted, the transaction handler never attached and
the match counter stayed at 0 / 0. Tracking the editor via state (in
addition to the ref) causes the deps to change when the Editor becomes
available, so the handler attaches and the counter updates.
* feat(web): allow overriding message list renderer via ?renderer= query
Exposes the "native" / "virtuoso" choice at runtime so E2E coverage can
exercise both paths without a redeploy or code change. Defaults to the
existing "native" strategy when no override is present.
* test(web): add Playwright coverage for panel-scoped Ctrl+F search
Adds an e2e/tests/search/ suite with five specs:
- panel-search-shared: cross-panel contract (Ctrl+F opens, autofocus,
Esc closes, close button, state reset on reopen) parametrised over
session, plan, and terminal.
- terminal-search: xterm addon-search integration — key intercept
without leaking "f" to the PTY, case + regex toggles (incl. invalid
regex error), Esc does not reach the shell, backspace adjusts count.
- plan-search: TipTap plugin — highlight count, exactly one current
match, counter, Enter / Shift+Enter wrapping, close clears
decorations, no case/regex toggles in plan bar.
- session-search: WebSocket-backed hits list, "No matches" state,
click-to-flash, cross-session isolation, close, and virtuoso renderer
coverage via the new ?renderer= override.
- panel-focus-routing: bar mounts inside the focused panel only.
Three tests capture GIFs for the PR description via prCapture when
CAPTURE_PR_ASSETS is set (one per panel). Helpers live in
e2e/helpers/panel-search.ts and e2e/helpers/seed-session-messages.ts;
SessionPage gains search-bar locators.
* fix(web): prevent plan-search update loop on spurious editor transactions
TipTap fires `transaction` on every cursor move/click. The match-info
handler allocated a fresh `{current, total}` object each time, so React
always saw new identity, re-rendered, and could cascade with downstream
effects (e.g. `rehydrateCommentMarks`) into a "Maximum update depth
exceeded" loop. Use a functional setState with equality bail-out so the
handler only schedules a render when the counter actually changes.
* fix(web): retry terminal search once xterm addon-search finishes loading
The SearchAddon is dynamic-imported and was stored in a ref. The
query→findNext effect called `addonRef.current?.findNext(...)` — a
silent no-op if the addon wasn't loaded yet — and its deps didn't
include addon readiness, so a query typed before the import resolved
was dropped forever. Track the addon via state so the search effect
re-runs when it becomes available (and re-binds cleanly if the terminal
is ever re-created). Also deduplicate match-info updates via functional
setState + equality check.
* fix(web): route Ctrl+F to the last-clicked panel when focus is ambient
The chat panel's message list has no focus-capturing child, so clicking
on a message left `document.activeElement === <body>` and the existing
`isFocusWithin` gate silently fell through to the browser's native find.
Fix with two complementary mechanisms:
- `usePanelSearch`: track the most-recently-clicked panel via a
window-level pointerdown capture listener. When focus is ambient
(body/html), that panel claims Ctrl+F. Window-level capture dodges
any descendant `stopPropagation` that would skip a container-level
listener.
- Chat panel root: tabIndex=-1 + onMouseDown that focuses the root when
the click target isn't already focusable. Normal path matches how
plan/terminal already work (TipTap/xterm capture focus on click).
* feat(web): show agent profile name and logo in session search hits
The hit list showed uppercase "AGENT"/"USER" with vivid blue/green
colors that clashed with the app's muted metadata aesthetic. Replace
with the actual author identity:
- Agent hits render the profile name (falling back to "Agent") with
the profile's <AgentLogo> inline — same mechanism session tabs use.
- User hits render "You" with a subtle `text-primary/80` tint that
echoes the user bubble's `bg-primary/30`, keeping a light color
differentiator without reintroducing loud blue/green.
Added `useSessionAgentIdentity` (plus a `useSessionAgentProfile` helper)
that reads from the store via two primitive selectors — the id, then
the profile — and composes the final object inside `useMemo`. A naive
single selector returning `{label, name}` triggers the "getSnapshot
should be cached" infinite loop in `useSyncExternalStore`.
* style: apply prettier formatting to search files
* fix(web): inline resolveStrategy in useMemo to satisfy react-hooks/use-memo
* fix(backend): escape LIKE metacharacters in message search
User query of `%` previously built LIKE pattern `%%` which matched every
message in the session; `file_name` also matched `filename`, `file name`,
etc. Escape the three LIKE metacharacters (`\`, `%`, `_`) before wrapping
the pattern, and declare `ESCAPE '\'` on the clause. Add regression tests
covering wildcard literals and the literal-percent happy path.
* fix(backend): slice search snippet in rune space to keep UTF-8 valid
`buildSnippet` indexed and sliced on byte offsets, so content containing
multi-byte characters (emoji, CJK, accented letters) could be cut
mid-rune — `encoding/json` then replaced the invalid bytes with `\ufffd`
in the websocket response. Switch to rune slicing and per-rune
case-folding via `unicode.ToLower`; this also avoids the
`strings.ToLower` byte-length-change pitfall (e.g. Turkish İ, German ß).
Add a handler test covering emoji, CJK, and large multi-byte content.
* fix(web): cancel backfill loop when user clicks a newer session hit
`setActiveHit` awaits up to 40 `loadOlder()` iterations when the target
message isn't in the DOM. If the user clicked a second hit while those
awaits were in flight, the in-progress loop kept running and eventually
scrolled to the original target, overriding the user's intent. Gate the
loop on a generation counter so only the most recent call's backfill
drives the scroll.
* fix(web): gate plan-search scroll side-effect on dispatch
TipTap invokes commands with `dispatch === undefined` during `can()`
checks to test applicability (e.g. before enabling a menu item). The
next/prev commands called `scrollToMatch` outside the `if (dispatch)`
guard, so a dry-run invocation would scroll the editor to a match the
user didn't navigate to. Move the scroll inside the dispatch block.
* fix(web): address review nits on search UX
- formatTime now checks Number.isNaN(d.getTime()) before formatting;
Invalid Date doesn't throw, so the catch branch was unreachable and
hits with a malformed created_at rendered the literal "Invalid Date".
- prefers-reduced-motion now silences the .search-flash animation
alongside the other custom keyframe animations.
- planScript escapes `title` symmetrically with `content` so callers
passing arbitrary strings don't produce malformed JSON.
* fix(web): fold description into opts to keep seedTask under param limit
`seedTask` had 6 parameters, violating the repo's 5-parameter cap and
failing CI lint. Fold `description` into the existing `opts` object and
update all callsites.
* test(web): add unit tests for useSessionSearch + remove redundant ternary
Cover the non-trivial hook logic called out in review: debounced fetch
coalescing, request-ID cancellation for superseded queries, null
sessionId short-circuit, and close() resets. Also simplify the
`matches.length ? 0 : 0` ternary flagged as a dead branch — both sides
were already 0.
* fix(web): exclude PanelRoot (tabindex=-1) from the focus short-circuit
`target.closest("[tabindex]")` was matching PanelRoot itself (which has
`tabIndex={-1}` so it can receive programmatic focus), causing the guard
to early-return and the `panelRef.focus()` call to never fire — defeating
the Ctrl+F focus-routing that this handler was added for.
Narrow the selector to `[tabindex]:not([tabindex='-1'])` so it only
short-circuits on genuinely interactive elements.
* fix(web): read isReadOnlyMode from live ref in shell-terminal key handler
`attachCustomKeyEventHandler` is called once and has no dispose hook, so
the inner handler keeps its initial closure forever. Other values
(sessionId, send, onFindInPanel) are read via `stateRef` so they stay
current, but `isReadOnlyMode` was captured by closure — if the prop ever
toggled mid-session, the Cmd+Arrow→Home/End path would continue firing
against a read-only terminal. Include it in the state ref for
consistency.
* fix(web): flatten doubly-nested seedTask opts in virtuoso spec; drop unused MODIFIER
The regex-based transform in
|
||
|
|
e1d21e39a9 | feat(web): collapse subtasks in sidebar (#662) | ||
|
|
de040a8987 | fix: gateway auth injection and cumulative diff error handling (#682) | ||
|
|
7076a40805 | chore: change default ports to avoid common conflicts (#681) | ||
|
|
6ffb157d7e | fix: user workflow deadlock (#677) | ||
|
|
c11a6bbfd2 |
feat(github): add issue watcher with task creation and auto-cleanup (#672)
* feat(github): add issue watcher with task creation and auto-cleanup
Add a GitHub Issue Watcher feature that polls for new issues matching
configurable filters and automatically creates tasks for them. This
complements the existing PR Review Watcher with the same patterns.
Backend:
- Issue watch CRUD (models, store, service, controller, WS handlers)
- Poller integration with configurable intervals
- Event-driven task creation via orchestrator
- Repository resolution and worktree support for issue tasks
- Auto-cleanup of tasks for closed issues (CleanupClosedIssueTasks)
- GetIssueState client method across all implementations
- Default branch auto-detection from cloned repo HEAD
Frontend:
- Issue watch settings UI (table, dialog, placeholders)
- Issue link icon on task cards (green circle-dot)
- API client, store slice, and hook for issue watches
- SSR and WS hydration of issue metadata
* fix: resolve golangci-lint errors (goconst, nestif, dupl) and prettier formatting
* fix(github): fix issue watch update shape, trigger key, and label quoting
- httpUpdateIssueWatch now returns the full IssueWatch object instead of
{updated:true} so the frontend store correctly refreshes after edits
- httpTriggerIssueWatch returns new_issues_found to match TriggerIssueResponse
type; was returning new_issues so the toast count was always 0
- buildIssueFilter uses conditional quoting instead of fmt.Sprintf %q to
avoid Go escape sequences in GitHub search qualifiers
Co-authored-by: João Salavisa <undefined@users.noreply.github.com>
* fix: resolve eslint warnings (max-lines-per-function, no-nested-ternary)
* test(github): add issue watcher tests and fix review feedback
- Add TestInterpolateIssuePrompt (4 cases: empty/all/none/partial)
- Add TestCreateIssueTask (3 cases: skip/reserve+assign/release on fail)
- Add TestShouldDeleteIssueTask (4 cases: open/closed+sessions/closed/error)
- Fix WS response key new_issues -> new_issues_found
- Fix fmt.Printf -> fmt.Fprintf(os.Stderr) in store.go
- Add CleanupClosedIssueTasks call in WS trigger handler
* fix: address PR review feedback and CI failures
- Split test files to fix revive file-length-limit (service_test.go,
event_handlers_github_test.go)
- Add session checker guard in CleanupClosedIssueTasks
- Normalize poll intervals on UpdateIssueWatch
- Add cleanup call in TriggerAllIssueChecks loop
- Log cleanup errors in controller and WS handler
- Prefer refs/remotes/origin/HEAD in detectGitDefaultBranch
- Add aria-label to issue task icon
- Fix TypeScript type errors: add linkedIssues to GitHubSlice,
fix hook exports, remove invalid imports
* fix: split test files exceeding revive limit and clean up minor issues
- Split service_test.go (883→779 lines): move TestTriggerPRSync_* to
service_pr_sync_test.go
- Split event_handlers_github_test.go (806→677 lines): move
TestCreateReviewTask_* and helpers to event_handlers_github_review_test.go
- Remove redundant s.taskSessionChecker nil-check inside shouldDeleteIssueTask
(early guard at CleanupClosedIssueTasks already ensures it is non-nil)
- Remove unreachable dead guard in detectAndPersistDefaultBranch
(repo.DefaultBranch is always "" at that call site)
- Fix bogus #0 issue badge in task-session-sidebar.tsx when issueNumber is absent
Co-authored-by: João Salavisa <undefined@users.noreply.github.com>
* fix: address Claude review — poll clamp and prompt injection
- Handle v <= 0 in UpdateIssueWatch poll interval (fall back to default)
- Remove {{issue.body}} from default template to prevent prompt injection
- Update test expected output for new default template
* fix: remove {{issue.body}} from frontend DEFAULT_ISSUE_WATCH_PROMPT
Mirrors the backend fix — the UI pre-fills the prompt so the backend
fallback was never reached. The placeholder stays in
ISSUE_WATCH_PLACEHOLDERS for explicit opt-in.
* feat(github): add isIssueWatch sidebar filter dimension
Add isIssueWatch boolean to KanbanState, TaskSwitcherItem, and sidebar
filter registry — mirroring the isPRReview pattern. Issue-watch tasks
(created from issue_watch_id in metadata) can now be explicitly filtered
in the sidebar, just like PR review tasks.
Files changed:
- sidebar-view-types: add isIssueWatch dimension
- filter-dimension-registry: add Issue watch filter meta
- apply-view: add isIssueWatch extractor
- kanban types: add isIssueWatch field
- TaskSwitcherItem: add isIssueWatch field
- use-all-workflow-snapshots: derive isIssueWatch from metadata
- ssr/mapper: derive isIssueWatch from metadata
- task-session-sidebar: pass isIssueWatch, extract helpers to fix complexity
* fix: correct deleteIssueWatch response type to match backend
Backend httpDeleteIssueWatch returns {"deleted": true}; the frontend
type was { success: boolean } — a mismatch with no functional impact
(return value is ignored) but wrong per TypeScript contract.
Co-authored-by: João Salavisa <undefined@users.noreply.github.com>
* refactor: extract metadata helpers to shared lib/metadata-utils.ts
- Move isPRReviewFromMetadata, isIssueWatchFromMetadata, and
issueFieldsFromMetadata to a shared module to eliminate duplicate
definitions (sonarjs/no-identical-functions).
- Fix deleteIssueWatch response type: { success: boolean } -> { deleted: boolean }
to match the actual backend response.
* style: fix prettier formatting
* test(web): add tests for isIssueWatch filter dimension and metadata-utils
Co-authored-by: João Salavisa <undefined@users.noreply.github.com>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
* style: fix prettier formatting in test files
* fix(github): cascade-delete issue watch tasks on watch deletion and add isPRReview to SSR mapper
- DeleteIssueWatch now wraps in a transaction and deletes related
github_issue_watch_tasks rows before removing the watch; prevents
orphaned dedup rows accumulating after a watch is deleted
- snapshotToState in mapper.ts now also sets isPRReview from metadata,
matching the mapping already done in use-all-workflow-snapshots.ts
Co-authored-by: João Salavisa <undefined@users.noreply.github.com>
* style: extract duplicate string constant in metadata-utils test
* style: extract repeated WS error strings into constants in handlers.go
goconst min-occurrences: 3 — 'invalid payload' (6x) and 'id required' (4x)
both exceed the threshold. Extract to package-level constants.
Co-authored-by: João Salavisa <undefined@users.noreply.github.com>
* style: extract repeated issue key strings into constants in event_handlers_github.go
goconst min-occurrences: 3 — 'issue_number' (9x) and 'issue_watch_id' (5x)
both newly introduced in this PR exceed the threshold.
Co-authored-by: João Salavisa <undefined@users.noreply.github.com>
* style: fix prettier formatting in metadata-utils test
---------
Co-authored-by: Kandev Agent <agent@kandev.dev>
Co-authored-by: claude[bot] <41898282+claude[bot]@users.noreply.github.com>
Co-authored-by: João Salavisa <undefined@users.noreply.github.com>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
|
||
|
|
cb7546e323 | fix(web): prevent kanban topbar search from overlapping right buttons (#661) | ||
|
|
d81baf3fca | fix(web): stop "Preparing workspace" flashing on step move and refresh stale chats (#663) | ||
|
|
6909fe6854 | feat(web): session tabs on kanban preview panel (#648) | ||
|
|
f4b5abef1a | fix(web): show repo name instead of full path in task sidebar (#652) | ||
|
|
d209d2ce2a |
feat(web): sidebar filter UX polish — align ops, group steps by workflow (#647)
* fix(web): align operator column across sidebar filter rows Dimension select was `flex-1`, which made the op column drift horizontally between rows — especially on boolean dimensions where the value input is omitted. Fix by pinning the dimension and op selects to fixed widths so only the value column flexes, keeping the middle "is/in/not in" aligned across every row. * feat(web): group workflow steps by workflow in sidebar filter dropdowns The Workflow step filter previously flattened steps from every workflow into one unlabelled list, making it impossible to tell which workflow a step belonged to. Tag each step option with its workflow name and render the single- and multi-select dropdowns with a labelled group + separator per workflow. Other enum dimensions stay flat (backward-compatible via an optional `group` field on the option). * refactor(web): extract shared helper for sidebar filter option groups Both the single- and multi-select value dropdowns built group buckets with the same consecutive-key loop, which implicitly assumed options of the same group arrived back-to-back. Extract a `buildOptionGroups` helper backed by a Map so the adjacency assumption goes away and the rendering call sites shrink to one line each. Also include the stable `option.value` in the cmdk `CommandItem` value so same-titled steps inside one workflow don't collide on identity/search. * fix(web): truncate sidebar filter single-select value within popover The single-select value trigger and the free-text input were `flex-1` without `min-w-0`, so a long value (e.g. repository slug) pushed the row wider than the popover and bled the remove button past the right edge. The multi-select trigger already had `min-w-0`, which is why the "in" operator rendered correctly. Add `min-w-0` to both so the flex items can shrink and the built-in SelectValue `line-clamp-1` ellipsis engages. |
||
|
|
e1f0632325 | fix(web): anchor PR detail panel to session group on auto-open (#646) | ||
|
|
f6f45b5777 |
feat: add filter/group/sort and saved views to task sidebar (#644)
* feat(web): add sidebar view types and apply-view engine
Introduces the typed representation of a sidebar view (filter clauses,
sort spec, group key) and the pure engine that applies a view to a list
of tasks to produce a filtered, sorted, grouped result.
* feat(web): add built-in sidebar view presets
Provides the four shipped presets (All tasks, No PR reviews, Active,
Archived). The 'No PR reviews' preset filters out PR-watcher tasks that
would otherwise crowd the sidebar.
* feat(web): persist sidebar views in ui store
Adds sidebar view state to the ui slice with draft semantics for saved
views, localStorage persistence of active view + user-created views, and
root store wiring.
* feat(web): add filter dimension registry and clause editor
Registry of filterable task dimensions (status, workspace, workflow, task
type, etc.) plus the primitive UI pieces — clause editor, sort picker,
group picker, and a hook that supplies operator and value options for a
selected dimension.
* feat(web): add sidebar filter bar, popover, and view manager
Composes the filter primitives into the user-facing surfaces: the top
filter bar, the popover that hosts clause editing and sort/group
selection, the chips that switch between views, and the manager for
saving, renaming, and deleting user views.
* refactor(web): extract NewTaskButton component
Pulls the Create Task button and its command-palette registration into a
standalone memoized component in preparation for reuse across sidebar
surfaces.
* feat(web): integrate filter/view system into task sidebar
Wires the filter bar, view chips, and apply-view engine into the task
sidebar (desktop and mobile sheet). Updates task-switcher to consume the
grouped list from the engine and adds mock data for tests and previews.
* test(web): add e2e coverage for sidebar filter/view system
Page object for the filter popover plus Playwright specs covering built-in
presets, clause editing, sort/group, saved-view draft semantics, and
persistence across reloads.
* refactor(web): trim sidebar built-in views to a single seeded default
Drop the "No PR reviews", "Active", and "Archived" built-in view presets
and the isBuiltIn protection flag. The sidebar now seeds a single "All
tasks" view on first load; users can rename, overwrite, or delete it like
any other view. Deletion is guarded against removing the last remaining
view so the user can never lock themselves out.
* fix(web): humanize executor type labels in sidebar filter and grouping
The executor-type filter dropdown and group-by label were rendering raw
backend identifiers like "worktree" and "local_docker". Route both
through the existing getExecutorLabel() helper so the sidebar shows
"Worktree" and "Local Docker" like the rest of the app.
* feat(web): replace "Has PR" filter with "Is PR review"
The old dimension matched any task with an associated PR, including
tasks a user manually linked. The new dimension isolates tasks created
by the GitHub PR-review watcher specifically, which is what the filter
is useful for — hiding a wall of auto-generated review tasks.
The flag is derived from the review_watch_id metadata key the backend
already stamps onto watcher-created tasks, threaded through the kanban
snapshot and into the sidebar TaskSwitcherItem. A small migration drops
unknown dimensions (including the old "hasPR") from stored views so the
popover does not crash when reading legacy data.
* fix(web): tighten sidebar filter popover layout
Reduce the popover width (w-96 → w-[22rem]), trim section padding
(p-3 → p-2.5) and label margins (mb-2 → mb-1.5), and drop the
"No filters applied." placeholder. Extract a shared SectionLabel to
keep the three section headers visually aligned.
* feat(web): show workflow step color dot in sidebar filter dropdown
Surface the step color on the workflow-step filter's value dropdown so
each option renders with the same coloured circle used elsewhere (task
switcher, kanban column, command panel). Adds an optional color field
on filter value options; only workflow-step options populate it today.
* feat(web): replace cramped in/not-in filter editor with a multiselect dropdown
The previous "in/not in" value input rendered selected values as tiny
10px pills inside a fixed-height row that overflowed and truncated as
the list of options grew. Replace it with a combobox-style multiselect
(Popover + Command) that shows a compact summary ("N selected" or the
single label with its color dot) and opens a searchable, scrollable
list with check indicators on click.
* fix(web): show workflow and step names (not UUIDs) in sidebar grouping
Group-by Workflow and Workflow Step rendered raw UUID strings as group
headers because `TaskSwitcherItem` did not carry the resolved names.
Pass `workflowName` and `workflowStepTitle` through `toSidebarItem` and
prefer them in the group extractors.
* fix(web): drop redundant true/false selector on boolean sidebar filters
Boolean dimensions (PR review, Archived, Has diff) previously exposed
both an operator (is / is not) and a value selector (true / false),
which gives four user inputs for two real states. Hide the value input
for boolean clauses and always store `true`; negation is expressed via
the operator. Rename "Is PR review" to "PR review" since the dimension
label no longer needs to read as a question.
* refactor(web): drop Sort by Repository and relabel State to Status
Sorting by repository duplicates what Group by Repository already
offers, and "State" leaks a backend term into the picker. Remove the
repository sort key (comparator, type, picker option) and rename the
state option to "Status". Stored views referencing the removed key are
migrated back to `state` at load time so the popover does not crash.
* fix(web): tighten sidebar filter popover section spacing
The FILTERS, SORT, and GROUP BY section labels each sat above their
controls with both padded containers (p-2.5) and a 6px bottom margin,
leaving visible dead space above each header. Reduce container padding
to p-2 and label margin to mb-1 so sections read as a compact stack.
* chore(web): apply prettier formatting drift in sidebar-filter area
* revert(web): undo store.ts prettier reformat to stay under max-lines cap
The packed single-line UIA declaration in store.ts is load-bearing for
the 600-line ESLint cap: the previous chore commit unpacked it to 9
lines and pushed the file to 609 lines, turning the max-lines warning
into a lint failure. Restore the packed layout (the /* prettier-ignore */
comment is there for exactly this reason).
* refactor(web): label boolean filter ops as Show/Hide instead of is/is not
* fix(web): pull section labels flush to sidebar filter popover borders
* refactor(web): inline rename and save-as into sidebar view header row
* refactor(web): split ViewHeaderRow into mode, display, and action subcomponents
* feat(backend): persist sidebar views in user settings
Adds a SidebarViews field to UserSettings (stored in the existing
users.settings JSON blob) plus validation in the service layer:
views are capped at 50, ids and names must be non-empty and unique.
* feat(web): hydrate and sync sidebar views to backend
Moves the source of truth for saved sidebar views from localStorage
to the user settings endpoint. Views hydrate from SSR on first load;
create/rename/overwrite/delete/duplicate/toggle-collapsed all PATCH
the backend with optimistic updates and revert-on-failure. The
active view id and draft remain per-device in localStorage. A
sync bridge surfaces PATCH errors as toasts and runs a one-time
migration to upload any pre-existing local views.
As a side effect, localStorage writes and PATCH side effects are
moved out of immer producers to avoid proxy-serialization risk.
* chore(web): apply prettier to store.ts sidebar actions block
* fix(web): run migrateView on SSR-hydrated sidebar views
Defensively drop clauses with unknown dimensions and reset stale sort
keys when views come from the backend, matching the localStorage load
path. Also notes the default-state module and sidebar views in the
store structure docs.
* docs: note default-state module and sidebar views in store structure
* fix(web): type sidebar view storage wrappers properly
Drop the as-unknown-as-SidebarView and as-never casts at call sites by
making the local-storage getters/setters accept the domain type and do
the JsonValue cast once inside the wrapper.
* refactor(web): stop PATCHing on sidebar group collapse toggles
collapsedGroups is per-device visual state; routing it through
mutateViews made every expand/collapse click fire a user-settings
PATCH. Keep it local (in-memory + localStorage cache); the next real
view mutation picks up whatever state is current.
* fix(web): skip sidebar view migration when settings hydrated from server
After SSR hydration the in-memory views are already the backend's
truth, so migrating re-POSTs the same data every page load. Gate the
one-shot migration on userSettings.loaded — only run if the server
didn't answer and the slice fell back to localStorage.
|
||
|
|
31140c8b9f | fix(web): follow workflow step session switches in chat UI (#625) | ||
|
|
4b55a69145 | feat(web): vscode-style preview tabs for files, diffs, and commits (#622) | ||
|
|
66f0cc2a16 | fix(web): persist agent profile mode through bulk-edit save (#626) | ||
|
|
1f7e4dbb5e | fix: stream setup script output and keep prepare panel on failure (#607) | ||
|
|
80eacf9e1c | fix(web): use UUID fallback for attachments on non-secure contexts (#640) | ||
|
|
5b04c9c675 | perf: focus-gated git polling to reduce CPU on retained worktrees (#610) |