Commit Graph

647 Commits

Author SHA1 Message Date
Carlos Florêncio a2b4fc0c6d fix(web): keep sidebar 3-dot menu from overlapping the subtask toggle (#1012) 2026-05-22 15:36:46 +01:00
Carlos Florêncio e85c340797 fix(web): honor window.__KANDEV_DEBUG so make start-debug logs surface (#991) 2026-05-22 13:46:11 +01:00
João Salavisa e0a4a38796 fix(vcs): default the commit dialog's stage-all checkbox to unchecked (#1010)
fix(vcs): default the commit dialog's "stage all" checkbox to unchecked

Opening a commit dialog now commits only already-staged files by
default. Users can opt in to "Stage all changes before committing"
per commit.

Co-authored-by: Kandev Agent <agent@kandev.dev>
2026-05-22 12:29:38 +01:00
Carlos Florêncio c9b6fe4b8a fix(worktree): support fork PR worktree creation via pull refspec (#990) 2026-05-22 12:15:39 +01:00
Nicolas Lenepveu e740417885 feat(linear): enrich issue watch filters with priority, labels, creator, estimate (#974)
* feat(linear): enrich issue watch filters with priority, labels, creator, estimate

Adds priority, labels (multi-select), creator (team member select), and
estimate range filters to Linear issue watchers — closing the gap with
Linear's GraphQL filter surface so users can scope auto-run watchers
nearly as tightly as they would in Linear itself.

- Backend: extend SearchFilter, map new fields to IssueFilter, add
  ListLabels / ListUsers on the Client interface, expose them over HTTP
  (GET /api/v1/linear/labels, /users) and via SearchIssues query params.
- Frontend: extend LinearSearchFilter, fetch labels + users per team,
  render new filter controls in the watcher dialog, and split helpers
  into linear-issue-watch-form.ts + linear-issue-watch-fields.tsx so
  the dialog stays under file/function size limits.

* fix(linear): address reviewer feedback on enriched filter PR

- Reject invalid priority / malformed estimate query params with 400 instead
  of silently dropping them; reject estimateMin > estimateMax. Mirror the
  bounds check in the issue-watch validator so JSON-encoded watches go
  through the same gate.
- A11y: render label/state toggle Badges inside <button aria-pressed> so
  keyboard users can navigate and screen readers can announce state.
- Recover from per-team fetch failures so a transient network blip doesn't
  permanently mark a team as "fetched-empty" — clear the in-flight marker
  when any of states/labels/users fail, allowing remount to retry.
- Tighten priority typing: LinearSearchFilter.priority is 0|1|2|3|4, and
  LinearPriority is the source of truth on the form-state side. Promote
  FormState from a type alias to an interface per repo convention.

* fix(linear): align update error message and clear fetchedTeams on cancel

- UpdateIssueWatch's filter-empty error still listed only the original
  four fields; bring it in line with the create path so the message
  matches what filterIsEmpty actually accepts.
- linear-issue-watch-fields: clear the fetchedTeams marker on effect
  cancellation as well as failure, so a remount after a fast teamKey
  switch can retry instead of being stuck with whatever partial cache
  the prior effect produced.

* fix(linear): address PR #974 reviewer findings

- linear-issue-watch-fields: delete the fetchedTeams marker on effect
  cleanup so rapid team switches (A → B → A) refetch on return instead
  of being stranded mid-load. Capture the Set in a local so the cleanup
  closure references the exact instance the effect body added to.
- graphql_client: raise the labels/users page cap from 100 to Linear's
  max of 250 via a shared linearMaxPageSize constant and a $first
  variable. Documented that a workspace exceeding that would need
  cursor-pagination here AND lazy-load in the UI.
- service_issue_watch.validateFilterBounds + handlers parseSearchNumeric
  Filters: reject negative estimate values — Linear story points are
  non-negative, so a -1 would silently match nothing.

* fix(linear): collapse duplicate hint text on States/Labels rows

When no team was selected the row showed two stacked muted lines —
the field description and the multi-select's "Pick a team…" prose —
saying overlapping things. Swap the row description based on team
selection and let the multi-select render nothing when disabled, so
exactly one line is shown in either state.

Also tightens the new fields' descriptions to match the brevity of
the existing ones (Labels, Estimate min/max).

* fix(linear): reject NaN/Inf estimates and preserve team cache after success

CodeRabbit + cubic flagged two real issues in the previous round:

- strconv.ParseFloat accepts "NaN" / "+Inf" / "-Inf" without erroring,
  and "f < 0" passes through for NaN. Without an explicit math.IsNaN /
  math.IsInf guard those values would propagate into the GraphQL
  request and the eventual json.Marshal would 500 instead of 400.
  Add the guard for both estimate_min and estimate_max, with a
  regression test covering NaN, ±Inf, negative, and inverted range.

- useTeamsAndStates was deleting the per-team cache marker on every
  cleanup, so a normal team switch evicted a healthy cache and the
  next visit re-fetched all three datasets. Track a "loaded" flag and
  only evict on cleanup when the fetch didn't complete successfully.

Also extracts the duplicated "team_key" / "team_key required" wire
strings into package-level constants per the repo's
"3+ occurrences → constant" convention.

* test(linear): add handler tests for /labels and /users endpoints

Mirrors the existing TestHTTPListStates_* pair — required team_key
returns 400, and a happy path that asserts the team_key reaches the
client and the response is 200. Closes the coverage gap CodeRabbit
flagged on the two new endpoints introduced in this PR.

* feat(linear): make priority filter multi-select

Status and label filters are already multi-select; priority being a
single Select broke the symmetry and forced single-value thinking.
Linear's own filter UI is multi-select per priority, and the GraphQL
filter takes priority: { in: [...] } natively, so the wire change is
clean.

Backend
- SearchFilter.Priority *int → Priorities []int
- buildIssueFilter emits priority: { in: priorities } when non-empty
- validateFilterBounds checks each element ∈ [0,4]
- normalizeFilter de-dupes (preserves caller order)
- HTTP query param priority → priorities, parsed as CSV
- Tests updated, including priority=0 still expressible as
  Priorities=[0] (matches "No priority" issues only)

Frontend
- LinearSearchFilter.priority → priorities: LinearPriority[]
- FormState.priority → priorities
- PRIORITY_OPTIONS dropped the (any) sentinel; "No priority" sits at
  the end of the chip row
- PriorityMultiSelect added next to State/Label multi-selects
- Priority moved to its own full-width row matching States/Labels;
  Creator promoted to a stand-alone CreatorRow

Refactor
- searchLinearIssues query-param building extracted to a flat
  table-driven helper to keep cyclomatic complexity ≤ 15

* refactor(linear): regroup watcher dialog fields and add section separators

UX pass on the watcher dialog:

- Pulled Team out into its own full-width row and paired Assignee +
  Creator in a 2-col row so the people filters sit together — the old
  Team+Assignee pairing forced a context switch between scope and
  people filters mid-form.
- Added hairlines between the four conceptual blocks (Destination /
  Filter / Automation / Prompt / Settings). Each block answers a
  different question, so a consistent rhythm helps users scan the form
  by section instead of reading one long stack.

No behaviour change; pure layout.

* refactor(linear): use shared Separator for watcher dialog section breaks

Initially extracted a local SectionDivider, but @kandev/ui/separator
(Radix-based, shadcn) already exists and is used in 14 places across
the codebase — including the sibling Jira settings page. Use that
instead so the styling tracks the rest of the app.

* refactor(linear): split parseSearchNumericFilters to satisfy cyclop

Extract priority and estimate-bound parsing into helpers so the entry
point drops below the project's max cyclomatic complexity (15).

* fix(linear): reject non-finite/negative estimates and align fmt

- validateFilterBounds rejects NaN/+-Inf in addition to negatives, so a
  watch config can't be persisted with values that would later crash
  json.Marshal. Adds focused unit tests.
- parseEstimate on the frontend treats negatives as unset so the UI
  cannot submit estimateMin/Max < 0.
- gofmt-only alignment tweak in handlers_test.go test table.
2026-05-22 10:42:42 +01:00
João Salavisa f98395f000 fix(agentctl): start per-repo workspace trackers in passthrough mode (#1002)
* fix(agentctl): start per-repo workspace trackers in passthrough mode

CLI passthrough mode starts the agent CLI via the PTY-based
InteractiveRunner instead of agentctl's startAgent / startOneShot, so the
per-repo workspace trackers created for multi-repo task roots were never
started. The bare root tracker exits immediately because it has no git
index, leaving polling completely idle — file edits made by the agent
never produced workspace-stream notifications and the Files panel stayed
stale. Move the tracker startup to instance.Manager.CreateInstance via a
new StartAllWorkspaceTrackers helper so polling fires for every repo
regardless of whether the agent runs via ACP or passthrough.

Fixes #982

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

* fix(lifecycle): serialize workspace poll-mode pushes to prevent slow-stuck mode

When a session subscribes and then immediately focuses (the normal page-load
sequence), the workspace poll aggregator used to spawn two independent
goroutines — one pushing slow, one pushing fast — whose HTTP arrival order
at agentctl was undefined. With the slow request landing after the fast
request the tracker ended up in slow mode (~30s polling) while the
aggregator's lastPushed claimed fast, and the suppress-duplicate guard then
blocked any future focus event from reconciling the divergence. Multi-repo
passthrough sessions hit this every page load: the Files panel saw
~30s-stale state even with the per-repo trackers running.

Replace the fire-and-forget goroutine per push with a single in-flight
pusher per workspace plus a last-write-wins pending slot — concurrent
callers all converge on the same pusher, which drains the queue in order
so the final queued mode is what reaches the wire last.

Regression test in manager_subscription_test.go simulates the race with an
httptest server that adds artificial latency on slow pushes; the test
fails on the old parallel-goroutine implementation and passes on the
serialized one.

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

* fix(agentctl): address PR review feedback for passthrough tracker fix

- Goroutine leak: extract stopWorkspaceTrackers() and call it
  unconditionally at the top of Manager.Stop(). Previously the early
  return when status==Stopped skipped tracker teardown in passthrough
  mode (where Start() never runs), leaking 2 goroutines per per-repo
  tracker per stopped instance. stopShellAndProcesses delegates to the
  same helper.
- Trim multi-line doc comments to one-liners per CLAUDE.md (the
  StartAllWorkspaceTrackers docstring and the CreateInstance call-site
  comment).
- Replace the fixed time.Sleep in the race regression test with a poll
  on len(calls), exiting as soon as the pusher has drained, plus a
  brief settle window after to keep the test sensitive to the late
  racing slow push the fix guards against.

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

* fix(web): refresh expanded subtrees on polling-detected file changes

The agentctl workspace tracker emits a generic `refresh` event with empty
path when its poll cycle detects file changes (it knows something changed
but not which file). The Files panel previously only refreshed the root
folder on these events, so files added under an expanded subdir never
became visible — the merge preserved the stale subtree, and the user had
to manually collapse + re-expand to see new files.

This is the user-visible symptom of #982 (Files panel does not refresh
after agent makes changes in CLI passthrough mode): the backend fixes in
this PR get the events flowing, but the frontend needs to act on them
correctly. On a `refresh` event we now refresh every currently-expanded
folder in addition to the root; for multi-repo task roots we scope to
the affected repo's branch using `repository_name`.

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

* fix: address iteration-2 review feedback

- Trim doc + inline comment blocks per CLAUDE.md (manager.go,
  file-browser-hooks.ts, workspace-files.ts).
- Replace the hand-rolled fmtSscanf/numErr port parser with strconv.Atoi
  in the race regression test.
- Assert on slowCalls in the race test to lock in the queue-collapse
  guarantee (at most one slow push lands).

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

* fix: address iteration-3 review feedback

- Remove redundant stopWorkspaceTrackers call from stopShellAndProcesses
  (it's already invoked at the top of Manager.Stop), and drop the now-
  misleading "stopping workspace tracker" debug log from the regular
  shutdown path.
- Trim three multi-line comment blocks in manager_subscription.go to
  one-liners per CLAUDE.md (workspacePushTarget, pushAsync, pushLoop).

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

* fix: trim multi-line comment block on pendingPush field

* fix: trim test docstring on race regression test

* fix(web): preserve sibling-repo subtrees on scoped refresh

The setTree reducer ran patchNode(updated) at the root, which re-matched
folderUpdates.has("") and overwrote the already-merged root children with
the raw depth-1 server response — wiping the loaded children of every
top-level repo that wasn't part of the refresh scope.

Recurse over updated.children instead of updated itself: the root has
already been merged in the block above, and each top-level repo either
gets its targeted update (if folderUpdates.has(path)) or is left alone
(preserving its loaded subtree).

Includes a regression test that fails on the old code: a refresh scoped
to "thm" used to discard "kandev"'s loaded README.md child.

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

* fix(web): trim three more comment blocks to one-liners

* fix: trim five more comment blocks to one-liners

---------

Co-authored-by: Kandev Agent <agent@kandev.dev>
Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-22 01:41:56 +01:00
João Salavisa 0f1081d423 feat(share): add public share links via GitHub Gists (#995)
* feat(share): add public share links via GitHub Gists

Adds a Share button to the chat panel header that publishes the
current session as a secret GitHub Gist on the user's account. The
share link routes through gistpreview.github.io to render share.html
as a styled kandev-themed page; snapshot.json + README.md ride along
for machine-readable + fallback views.

Snapshots run a redaction pass (absolute paths, common credential
shapes, .env contents, shell env vars) and strip <kandev-system>
prompt blocks via the shared sysprompt helper. Message filtering
whitelists user prose + tool_* types so internal noise never leaks
into shares.

Spec at docs/specs/public-share-links/spec.md.

* fix(share): coerce pendingClarification to bool for ClarificationSection

A late refactor extracted ClarificationSection with `pendingClarification: boolean`
on its props, but the call site passes the actual Message|null value. CI's tsc
build catches the mismatch; runtime was unaffected because the section only uses
the value in a truthiness check. Coerce with Boolean() at the call site to keep
the prop's intent narrow.

* fix(share): address PR #995 review feedback

Bugs (flagged by Greptile, Cubic, Claude):
 - abs-path redaction split on `root + "/"` so sibling dirs like
   /home/user/proj2 don't get corrupted when workspaceRoot is
   /home/user/proj; regression test added.
 - GHClient.DeleteGist now wraps "HTTP 404" stderr as *GitHubAPIError so
   share.IsAlreadyGone matches consistently with the PAT client; this
   restores the "gist already revoked upstream" soft-success path for
   CLI-authenticated users.
 - SnapshotSizeBytes is now measured from the same MarshalIndent payload
   the gist backend uploads, so the size shown in the UI matches the
   actual gist bytes (was ~20-40% underreported).

Polish (flagged by CodeRabbit, Cubic):
 - Tool name + tool output now HTML-escaped; tool call args and output
   render via <pre><code> instead of triple-backtick fences so payloads
   containing ``` can't break out of the code block.
 - share-list.tsx: clear `busy` via finally and break error layout out
   of the justify-between row.
 - use-shares.ts: convert UseSharesResult to interface (repo TS rules);
   reset error+isLoading when ids are missing.
 - use-shares.test.tsx: wait on the actual async outcome (shares
   populated, listShares called) instead of isLoading flipping false.
 - provider.go: replace `repo.Close` cleanup with an explicit noop
   closure so the comment ("Cleanup is a no-op") and the impl match.
 - spec.md: aligned the shareability rule across What/Failure/Scenarios/
   Out-of-scope — any non-prehistory state can be shared; added language
   tags to fenced code blocks for markdownlint.

Skipped intentionally (reply on each thread):
 - HTML-escape every metadata badge: the values are controlled enum
   strings from kandev's own schema, not user input. Defense-in-depth
   noise. Tool name (which can come from agent runtime) IS escaped.
 - useShares request-token guard against stale responses: the dialog
   owns the hook lifecycle and opens against a single (taskId,
   sessionId) — the race the comment describes can't occur here.

* fix(share): redact paths for every worktree, not just the first

A single TaskSession can carry multiple worktrees when the task spans
multiple repos. The redactor previously only saw the first worktree
path, so absolute paths from secondary worktrees leaked into public
shares instead of being rewritten to repo-relative form.

NewRedactor is now variadic and applies the abs-path rule against every
root it receives, sorted longest-first so a root that is a prefix of
another (e.g. /workspace vs /workspace/repo1) gets matched by the more
specific one first. workspaceRootFor in builder.go becomes
workspaceRootsFor and emits every WorktreePath plus the WorkspacePath
fallback.

Flagged by Cubic on PR #995.

* fix(share): address PR #995 review round 3

Bugs (flagged by Cubic + Claude on gh_client.go:714):
 - DeleteGist now calls the existing isNotFoundErr helper instead of an
   inline strings.Contains(err, "HTTP 404"). The helper also covers
   "404 Not Found" and "status: 404", which some gh versions emit, so
   the share service's "gist already revoked upstream" soft-success
   path now fires consistently across CLI auth users.

Bugs (flagged by Claude on builder.go:220):
 - toolCallBlock used to read metadata["tool_name"] and metadata["args"]
   but production never writes those keys; the typed payload lives under
   metadata["normalized"] (a *streams.NormalizedPayload) and the tool
   kind is on Message.Type. Switched to deriving ToolName from
   strings.TrimPrefix(m.Type, "tool_") (gives "read", "execute", "edit",
   …) and reading args from metadata["normalized"], with backward-compat
   fallback to the older "tool_name"/"args" keys for the debug-fixture
   replay path. New TestBuildSnapshot_ToolCall_ProductionMetadataShape
   exercises the real session shape.

Bugs (flagged by CodeRabbit on redaction.go:110):
 - Bare workspace-root occurrences (pwd output, end-of-sentence, quoted
   paths) used to leak because the matcher only handled root + "/".
   Switched to a precompiled regex per root —
   regexp.QuoteMeta(root) + `(?:/|\b)` — so both `root/...` and standalone
   `root` get stripped, while sibling dirs like /workspace2 still don't
   match. Test TestRedactor_AbsPath_StandaloneRootStripped covers the
   bare-root variants.

Doc (flagged by Claude on spec.md):
 - Two malformed fence closers (`go and `text on closing fences) corrected
   to plain ``` so the file is markdownlint-clean.

* fix(share): narrow abs-path boundary to avoid sibling-prefix corruption

Round 4 review (Claude + CodeRabbit) caught a regression in the regex
introduced by 436cb66: `(?:/|\b)` used `\b` (word boundary) to catch
bare-root cases, but `\b` fires on any word↔non-word transition. So a
sibling path like `/workspace.bak`, `/workspace-old`, or
`/workspace-other` would false-match the `/workspace` root (since
e→. and e→- are word boundaries), stripping it and leaving a
corrupted `.bak` / `-old/file.ts` in the share.

Replaced the matcher with `(/|\s|$)`: path separator, whitespace, or
end-of-string only — never punctuation or quote chars. The boundary
is captured and re-emitted via ReplaceAllStringFunc (or dropped, in
the `/` case) so prose loses only the root, not the surrounding space.

Added TestRedactor_AbsPath_SiblingsWithPunctuationNotCorrupted covering
`.bak`, `-old`, `_alt` siblings; the digit-sibling test still passes.
The quoted-bare-root case (`"/workspace"`) no longer matches and was
dropped from the standalone-root test — contrived enough that a small
leak there is preferable to corrupting realistic sibling paths.

* refactor(share): move share button into chat status bar

Replaces the dedicated PanelHeaderBarSplit above the message list with
a slot in ChatStatusBar (the existing row holding TodoIndicator and
PRStatusChip just above the chat input). The button anchors right via
ml-auto; when the "proceed to next step" affordance is present it
takes the right anchor instead and the share button stays inline.

Also tightens the iconOnly variant in ShareButton to a ghost style
that matches the muted-foreground chips in the status bar, and exports
shareableSessionStateClient from share-button.tsx so the gating logic
lives next to the entry point it controls.

Net -23 lines in task-chat-panel.tsx: the extra header bar and the
local gate helper are gone.

* docs(share): broaden tooltip + add share/ to CLAUDE.md tree

The share tooltip read "Share this completed task with a public link",
but shareableSessionStateClient allows RUNNING / IDLE / WAITING_FOR_INPUT
/ FAILED / CANCELLED too. A user hovering mid-run saw misleading copy;
reworded to "Share this task conversation as a public link" which
matches the actual gate.

Also added the missing internal/task/share/ entry to the task/ subtree
in CLAUDE.md so the new package shows up in the repo map — flagged
across multiple rounds.

* docs: add internal/task/share/ to repo map

Carryover from the share PR — AGENTS.md (which CLAUDE.md symlinks to)
was missing the share/ subpackage under task/. Adding it now so the
repo map matches what's checked in.

---------

Co-authored-by: Kandev Agent <agent@kandev.dev>
2026-05-21 23:35:41 +01:00
João Salavisa b8a09bc5ea fix(github): pick allowed merge method so squash-only repos stop 405ing (#999)
* fix(github): pick allowed merge method so squash-only repos stop 405ing

The merge button sent an empty merge_method to GitHub, which defaults to
"merge" regardless of repo settings — squash-only / rebase-only repos
returned 405 "Merge commits are not allowed on this repository". The
backend now resolves an empty method to the first allowed one via a new
GetRepoMergeMethods endpoint (5-min cache), and the frontend renders a
GitHub-style split button so users can pick among the allowed methods.

* fix(github): address PR #999 review feedback

- pr-merge-button: extract repo-methods fetch to useRepoMergeMethods
  hook (Claude bot); hide button while methods are loading so the
  primary label doesn't flicker (Claude bot); drop e.preventDefault on
  dropdown items so Radix closes the menu on selection (Greptile P1).
- gh_client / pat_client: treat missing allow_* fields as false instead
  of true so a permission-gated response can't pick a disallowed merge
  method (cubic P2).
- service.MergePR: only overwrite mergeMethod when pickDefaultMergeMethod
  returns non-empty, so a degenerate all-false config falls through to
  GitHub's default with the real error (Claude bot).

* fix(github): don't hide merge button on lookup failure

Round-2 PR feedback (Claude, Greptile, CodeRabbit, cubic): the
`if (!methods) return null` guard added in the previous round to
avoid label flicker turned a transient repo-methods lookup failure
into a permanent UI lockout — the button would stay hidden until
unmount/remount.

- pr-merge-button: revert the null-guard; treat null as "use backend
  resolver", render the plain primary button without a dropdown.
- use-repo-merge-methods: add a 5-min frontend TTL to match the
  backend cache so admin merge-strategy flips are visible without a
  page reload. Catch block now triggers a rerender, and co-waiting
  consumers attach both fulfilled/rejected handlers to avoid silent
  rejection swallowing.

* test(github): cover useRepoMergeMethods + silence rejection warning

Round-3 PR feedback (Claude):

- Swallow rejections in the originating fetch chain instead of
  rethrowing, so the first consumer's promise never escapes the hook
  as an unhandled rejection. Coalesced consumers already re-render to
  null, which is the same state a fresh "not loaded" call would show.
- Add use-repo-merge-methods.test.ts covering: null-on-missing-params,
  loading then resolution, cache hit on remount, concurrent-fetch
  coalescing, retry-on-failure (no cache poisoning, no unhandled
  rejection), and TTL-expiry refetch via fake timers.

* test(github): cover httpGetRepoMergeMethods error branches

Round-4 PR feedback (Claude): the new endpoint had only happy-path
coverage. Add tests matching httpMergePR's existing shape:

- nil client → 503 (ErrNoClient)
- GitHubAPIError{404} → 404
- generic upstream error → 500

---------

Co-authored-by: Kandev Agent <agent@kandev.dev>
2026-05-21 23:22:31 +01:00
João Salavisa f8654880f3 fix(kanban): stop stuck spinner by writing task.state=REVIEW on engine on_turn_complete (#1001)
* fix(web): stop kanban spinner when primary session is terminal

The kanban card spinner was gated only on `task.state` being IN_PROGRESS
or SCHEDULING. Workflows leave a task in the IN_PROGRESS column after
the agent's session reaches a terminal state (COMPLETED/FAILED/CANCELLED)
so the user can review and move it manually, which made the loading
icon spin indefinitely.

The frontend already receives `primary_session_state` over SSR and WS;
gate the spinner on that not being terminal. Fixes #985.

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

* fix(orchestrator): write task.state=REVIEW after engine on_turn_complete transition

The non-engine path (setSessionWaitingForInput → writeTaskReviewState) has
always written tasks.state = REVIEW when the agent's session paused for
input. The workflow-engine on_turn_complete path forgot the task-state
write: it persisted the new workflow step + flipped the session to
WAITING_FOR_INPUT but left tasks.state stale at IN_PROGRESS. That left
the kanban card stuck in IN_PROGRESS — with a spinning loader — even
after the workflow had moved it into the "Review" column.

Mirror the non-engine path: call writeTaskReviewState inside the
RUNNING/STARTING branch of applyEngineTransition. Add a regression test
that asserts tasks.state = REVIEW after an engine-driven on_turn_complete
transition.

Also tighten the frontend gate added in the previous commit to suppress
the spinner for any non-active primary-session state (WAITING_FOR_INPUT,
IDLE in addition to COMPLETED/FAILED/CANCELLED). The backend fix is the
real correction; the broader frontend gate stays as defense-in-depth
against future state-sync drift.

Refs #985.

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

* test(orchestrator): drop redundant time.Sleep in engine on_turn_complete test

writeTaskReviewState and ApplyTransition both run synchronously inside
applyEngineTransition (before processOnEnter's goroutine), so the
task-state and step assertions need no async wait. assertSessionState
already has its own 500ms polling loop for the WAITING_FOR_INPUT flip.

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

---------

Co-authored-by: Kandev Agent <agent@kandev.dev>
Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-21 22:58:14 +01:00
José Almeida d14cda778e fix(web): close clarification overlay even when WS event is lost (#997) 2026-05-21 22:14:37 +01:00
João Salavisa c31dcd973a fix(web): gate merge button on required_reviews so it matches GitHub (#993)
* fix(web): gate merge button on required_reviews so it matches GitHub

GitHub's mergeable_state can lag branch-protection state (dismissed
approvals, fresh-commit invalidations, partial polls), letting the
green merge button appear when GitHub itself still says blocked.
isPRReadyToMerge now refuses to surface the button when the recorded
approval count is below required_reviews, mirroring the "K / M required"
pair the CI popover already renders. isPRAwaitingReview follows the
same gate so the icon shows sky-blue "awaiting review" instead of
plain green when approvals are short.

* test(web): cover bug-repro path in getPRStatusColor + trim guard comments

Address Claude review on #993: trim the two new multi-line guard comments
in isPRReadyToMerge / isPRAwaitingReview to one short line each (per
CLAUDE.md), and add a getPRStatusColor regression test that confirms the
icon downgrades from emerald to sky-400 in the stale-clean + unmet
required_reviews scenario the PR fixes.

* chore(web): collapse required_reviews guard comment to one line

Final pass on CLAUDE.md's one-short-line-max comment rule, per Claude
review thread on #993.

---------

Co-authored-by: Kandev Agent <agent@kandev.dev>
2026-05-21 15:29:18 +01:00
João Salavisa 33a378a8dc fix(web): expand folder optimistically on first click in Files panel (#984)
* fix(web): expand folder optimistically on first click in Files panel

The first click on a folder in the Files panel previously deferred the
expanded-state update until after loadNodeChildren resolved, so the
chevron and children only appeared once the WebSocket round-trip
finished. Users perceived the click as a no-op and had to click again.

Flip the expanded set synchronously, then load children in the
background. Extracts the toggle logic into a testable helper and locks
in the regression with unit tests.

Closes #983

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

* docs(web): collapse toggleFolderExpand docstring to a single line

Per the CLAUDE.md "no multi-line comment blocks" rule flagged by review.

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

* fix(web): dedupe in-flight loadNodeChildren and collapse comment to one line

- Add `activeLoadsRef` + `isLoading(path)` to `useLoadingTimers`, and guard
  `loadNodeChildren` against duplicate fetches for the same path. Rapid
  double-clicks on a freshly collapsed folder no longer issue two
  identical WebSocket round-trips (Greptile P2).
- Collapse the `toggleFolderExpand` comment to a single line per
  CLAUDE.md (Claude review).
- Add `file-browser-load-children.test.ts` covering the dedupe contract,
  the post-finish re-fetch path, and the children-already-loaded
  short-circuit.

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

---------

Co-authored-by: Kandev Agent <agent@kandev.dev>
Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-21 14:01:31 +01:00
Jérémy Coatelen b39f256e84 fix(orchestrator): propagate RepositoryID so worktree setup script runs (#969) 2026-05-21 10:09:20 +01:00
Carlos Florêncio 2ca48fc7df feat(repositories): guard delete behind active-session check + UI fixes (#968) 2026-05-21 09:24:34 +01:00
José Almeida 1f264adbc4 fix(github): drain piled-up PR/issue review tasks via cleanup policy + manual sweep (#929) 2026-05-21 08:46:50 +01:00
João Salavisa 3ff3a53c75 feat(web): structured renderers for Kandev MCP tool calls (#987)
* feat(web): structured renderers for Kandev MCP tool calls

Replaces the raw-JSON dump in the chat for every Kandev MCP tool with a
per-tool structured view. Adds 20 renderers — list (workspaces, workflows,
workflow-steps, tasks, related, agents, executor-profiles, task-documents),
task actions (create/update/move/message), plan + document (get/create/update/
delete plan, get/write document, conversation), and ask_user_question.

Dispatcher in message-renderer.tsx scans tool name on metadata.title,
comment.content and normalized.generic.name (the ACP normalizer's "name"
field is the *category*, often "other", not the tool name).

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

* fix(web): address review feedback on kandev tool renderers

- ask_user_question: accept both keyed-object and positional-array
  responses shapes (replaces pickObject which silently discarded arrays).
- get_task_conversation: derive the "more not shown" footer from the
  backend's absolute total so paginated responses don't hide pages, and
  guard the leading SummaryDot when both taskId and sessionId are absent.
- IdChip: drop unused label prop and the stale label="..." callsites.

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

* fix(web): consistency follow-ups on kandev renderers

- Guard the leading SummaryDot in GetTaskPlanRenderer and
  UpdateTaskPlanRenderer the same way GetTaskConversationRenderer does.
- ListTaskDocumentsRenderer reads `total` from the result like the other
  list renderers, so a paginated document list doesn't understate.
- Add a regression test for the positional-array `responses` branch in
  matchAnswerForQuestion.

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

* fix(web): guard MoveTaskRenderer SummaryDot

Same orphan-dot fix as the other renderers — only render the dot when
both flanking IdChips are present.

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

* fix(web): final review nits on kandev renderers

- ask_user_question: use shortId() for the pending_id hint instead of
  hand-rolled slice (handles short ids correctly).
- list-renderers: add a stable index fallback to every map() key when
  id/name/title are all absent, so React reconciliation stays stable on
  malformed payloads.
- MoveTaskRenderer: extend hasExpandableContent to cover workflow/step
  rows so they remain accessible without task/prompt data.

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

---------

Co-authored-by: Kandev Agent <agent@kandev.dev>
Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-21 04:14:42 +01:00
João Salavisa 3d860290da feat(github): add merge button when PR is ready to merge (#979)
* feat(github): add merge button when PR is ready to merge

Surfaces a one-click merge action in the PR detail panel header and as a
compact green pill in the PR topbar hover popover, gated by the existing
isPRReadyToMerge predicate (open, CI success, mergeable_state=clean, and
either approved or no review process).

Backend adds MergePR to the Client interface, implements it across
PAT/gh/Noop/Mock clients, exposes PUT /api/v1/github/prs/:owner/:repo/:number/merge,
and maps GitHub 405/409 to HTTP 409 so the UI can show a clear error.

* fix(github): address PR review feedback on merge endpoint

- Map gh CLI 404/403/405/409 stderr to *GitHubAPIError via a new
  ghMergeStatusCode helper, so merge conflicts surface as HTTP 409 for
  gh CLI users (previously only PAT users got the right status).
- Service.MergePR returns ErrNoClient when unconfigured; httpMergePR
  translates that to HTTP 503 with github_not_configured, matching
  httpListRepoBranches.
- httpMergePR distinguishes empty body (io.EOF, fall through to default
  merge method) from malformed JSON (HTTP 400 invalid payload).
- PRMergeButton tracks a local merged flag so the button stays hidden
  across the async refresh window after a successful merge.

* test(github): cover merge helpers and extract PRMergeButton

- TestGhMergeStatusCode locks in the gh CLI stderr → status-code parser
  for nil, unrelated errors, the HTTP / status: / bare-phrase forms of
  404/403/405/409, and a 500 non-match guard.
- Broaden ghMergeStatusCode to also match "405 Method Not Allowed" /
  "409 Conflict" phrase forms so the merge endpoint's 409 mapping does
  not depend on the gh CLI keeping its current "HTTP <code>" prefix.
- TestHttpMergePR_MalformedJSON covers the new non-EOF bind-error branch
  (truncated body → 400). TestHttpMergePR_NoClient_Returns503 covers the
  ErrNoClient → 503 path on Service.MergePR.
- Document the post/put error-type divergence on PATClient.post so a
  future caller does not silently lose per-status mapping.
- Move PRMergeButton out of pr-shared.tsx into its own file, and reset
  the local "merged" flag when taskPR.id changes so an instance that
  re-renders for a different PR does not stay hidden.

---------

Co-authored-by: Kandev Agent <agent@kandev.dev>
2026-05-21 03:05:28 +01:00
João Salavisa 35242b2982 feat(web): drag-reorder subtasks within a parent task (#962)
* feat(web): drag-reorder subtasks within a parent task

Adds per-parent subtask ordering in the sidebar. Drag a subtask under its
parent to reorder; the chosen order is independent of the global sort and
persists per browser via localStorage.

- New `sidebarTaskPrefs.subtaskOrderByParentId` slice field and matching
  `setSubtaskOrder(parentId, ids)` action, persisted to localStorage under
  `kandev.sidebar.subtaskOrderByParentId`.
- `applyView` applies the per-parent override after `separateSubtasks`, so
  it never touches root order or the active sort spec.
- TaskSwitcher exposes `onReorderSubtasks(parentId, ids)`; each parent's
  subtask list lives in its own nested DndContext / SortableContext using
  the existing dnd-kit setup.
- Task-deletion cleanup strips the deleted id as both parent key and
  subtask entry from `subtaskOrderByParentId` in store + localStorage.

* refactor(web): address PR review feedback on subtask reordering

- Extract `pruneSubtaskOrder(map, taskId)` helper in `local-storage.ts`;
  use it in both `cleanupTaskStorage` and `removeTaskFromSidebarPrefs`
  to keep the two cleanup paths in lockstep.
- Move `SortableSubtaskRow`, `SortableSubtaskList`, and `useSubtaskDnd`
  into a new sibling file `task-switcher-subtask-dnd.tsx` so the
  switcher stays under the 600-line soft limit.
- `SortableSubtaskList` now short-circuits to a plain row map when
  `onReorderSubtasks` is undefined — read-only callers no longer get a
  misleading grab cursor or unused DnD scaffolding.

* fix(web): add stable keys to non-DnD subtask render path

When `onReorderSubtasks` is undefined, the early-return branch in
`SortableSubtaskList` mapped subtasks without keys, falling back to
React's index-based reconciliation. Wrap each child in a keyed
`Fragment` so reorders or size changes reconcile correctly.

* refactor(web): single-source DRAG_ACTIVATION_DISTANCE

Export the constant from `task-switcher-subtask-dnd.tsx` and import it
in `task-switcher.tsx` instead of declaring the same value twice.

---------

Co-authored-by: Kandev Agent <agent@kandev.dev>
2026-05-21 01:31:42 +01:00
João Salavisa 569a0d77bd fix(office): unbreak new-task dialog "Create Task" button (#976)
* fix(office): unbreak new-task dialog "Create Task" button

Office task creation silently failed: the button click sent a malformed
payload, the backend dropped it, and no toast surfaced because Sonner's
Toaster was never mounted. Three causes, all fixed here.

- web: send priority as a string id ("medium") instead of a number; the
  backend Priority field is a string and ShouldBindJSON was rejecting
  the body with "invalid payload".
- web/backend: add project_id as a top-level field on createTask. The
  dialog was burying it inside metadata where the HTTP handler never
  read it, so isOfficeRequest() returned false and validation tripped
  on "workflow_id is required for non-ephemeral tasks".
- web: mount the Sonner Toaster in the root layout. All sonner-based
  toasts across the app (not just this dialog) were silently dropped
  because no Toaster was rendered anywhere.

* fix(office): address PR #976 review feedback

- new-task-dialog: drop unreachable `|| undefined` guard on priority
  (EMPTY_DRAFT.priority is always "medium" and the picker only sets
  non-empty strings, so the fallback can never fire).
- layout: move sonner Toaster to top-right so it stops visually
  overlapping with the custom ToastProvider's bottom-right stack.
- task_http_handlers_test: add regression test asserting that a
  create-task body with project_id (and no workflow_id) reaches the
  office code path and the office workflow auto-resolves. Guards the
  exact wiring that caused the original silent failure.

---------

Co-authored-by: Kandev Agent <agent@kandev.dev>
2026-05-21 01:14:08 +01:00
João Salavisa a2d086efb8 feat(web): add @-mention for tasks in the chat composer (#963)
* feat(web): @-mention tasks in the chat composer

Lets users type @<taskname> in the chat input to reference other tasks.
The picked task is inserted as a chip and its metadata (id, workflow_id,
step, state) is appended to the prompt as a <kandev-system> block so the
agent can act on it via the kandev MCP tools.

Extends the existing TipTap mention extension with a new "task" kind;
task data comes from the kanban store (current workflow + visited
kanbanMulti snapshots), so no backend changes are needed.

* fix(web): harden task @-mentions from review feedback (#963)

- Sanitize interpolated task strings (title, id, workflow_id, step,
  state) before they land in the <kandev-system> block so a hostile
  task title (e.g. from Jira/Linear sync) can't terminate the block
  early via embedded newlines or angle brackets.
- Expand the task-mentions context block on the onSend code path too
  (used by preview-session-tabs), which was silently dropping the
  agent-facing task context when chips were present in the editor.
- Deduplicate task mentions by taskId in getTaskMentions so mentioning
  the same task twice no longer produces redundant lines.
- Skip stale state.kanban.tasks entries whose workflowStepId isn't in
  the current workflow's steps to prevent tagging them with the wrong
  workflow_id.

* fix(web): skip task mentions missing workflowId or workflowStepId (#963)

CodeRabbit flagged that getTaskMentions was filling missing
workflowId / workflowStepId with empty strings, propagating malformed
metadata into the prompt block. Skip such nodes during extraction.

---------

Co-authored-by: Kandev Agent <agent@kandev.dev>
2026-05-21 00:57:12 +01:00
João Salavisa 474346bc9a fix(web): restore padding on office task-creation title input (#964)
Co-authored-by: Kandev Agent <agent@kandev.dev>
2026-05-21 00:17:28 +01:00
Luan Martins a852be3c33 feat(web): show task linkage on GitHub PR list (#952)
* feat(web): show task linkage on GitHub PR list

Add a compact indicator next to each PR showing whether a task already
exists. When tasks are linked, show a clickable button with the task
title and a count badge for multiple tasks. When no task exists, show
'No task created yet'.

- Add usePRKeyToTasks hook that maps PR keys to task associations
- Add PRRowTaskIndicator component for the per-PR task UI
- Wire the data through github-page-client into PRList
- Add tests for the indicator and hook utility

* feat(web): improve PR task indicator with task titles and multi-task dropdown

- Show actual task title instead of PR title in single-task case
- Replace count badge with dropdown menu for multiple linked tasks
- Switch icon from GitPullRequest to Checklist
- Load all workflow snapshots on GitHub page to hydrate task titles
- Fix test to use unique task_id per fixture

* feat(web): neutralize PR task indicator color and add tooltip

The checklist icon previously mirrored PR aggregate status, which duplicated
information already shown on the PR row and misleadingly suggested it
reflected task state. Drop the color and add a tooltip surfacing the actual
task title and workflow step.

Adds e2e coverage for the GitHub PR list task indicator: empty badge,
single-task button, and tooltip content.

* fix(web): navigate to task page when clicking PR task indicator

replaceTaskUrl only mutates history.state, which sets active task in the
store but does not trigger a Next.js route change. From /github, this left
the URL pointing to /t/<id> while the page still rendered the GitHub list.
Use router.push so the click actually navigates to the task.

* test(web): cover PR task indicator click navigation

Extends the GitHub PR list task indicator spec with a click assertion
verifying the URL changes to /t/<taskId> and the PR list unmounts.
Regression guard for the replaceTaskUrl → router.push fix.

* fix(github): link task to PR at creation time, not via branch poller

Tasks started from the GitHub PR list "+ Task" button were left without a
github_task_prs row when the worktree branch did not match the PR's head
branch on GitHub. This happened for PR-review-style tasks, which check out
synthetic branches (e.g. feature/review-...-h92) that don't exist as PR
heads. The poller's FindPRByBranch could never resolve them, so only the
first task whose branch happened to match got linked — every other task
created from the same PR was missing from the indicator.

The PR is already known at "+ Task" time, so persist the linkage
immediately rather than waiting on branch-based discovery:

- AssociateExistingPRByURL: new service method that creates the
  github_task_prs row from a PR URL without creating a PR watch. The
  watch is still created later at agent start via ensureSessionPRWatch.
- POST /api/v1/github/task-prs/associate: HTTP endpoint exposing it.
- QuickTaskLauncher.handleSuccess: after task creation, fires a
  best-effort associate call with the task's repository_id and PR URL.
  Failures fall back to the legacy poller path silently.

Backend test covers two distinct tasks linking to the same PR.
E2e adds a PR with two tasks and asserts the multi-task indicator
("Tasks 2") renders alongside the single-task and empty cases.

* refactor(github): make task-PR create endpoint RESTful

Rename POST /api/v1/github/task-prs/associate to POST /api/v1/github/task-prs.
The action-named "/associate" was RPC-styled; the resource collection is the
right URL for creation alongside the existing GET. Frontend client renamed
associateTaskPR → createTaskPR to match.

* fix(github): address PR review findings

- Map invalid PR URL to HTTP 400 (was 500). New ErrInvalidPRURL sentinel
  lets the controller distinguish parse failures from genuine server
  errors. Other failure paths still return 500.
- Prefer dialog-time repositoryId in pickRepositoryIdForPR. Multi-repo
  tasks with shared branch names were ambiguous under pure branch
  matching; the matched repo id captured by the launcher dialog wins
  when present.
- Cover usePRKeyToTasks (not only the prKey util) with hook tests:
  empty input, multi-task grouping, distinct keys, and the non-array
  defensive branch.
- Lock createTaskPR request semantics: method/body now win over a
  caller's options.init so the POST JSON shape can't be overridden.

* fix(web): use TaskPR row id for dropdown key

Multi-repo tasks can produce more than one TaskPR row for the same PR
(one row per repository_id), so keying the multi-task dropdown by
task_id risked a duplicate-key warning and unstable reconciliation.
Switch to the row id, which is unique per row.
2026-05-20 22:38:44 +01:00
José Almeida f2479aabce feat(web): collapsible queue chip with animated panel (#957) 2026-05-20 01:22:25 +01:00
Carlos Florêncio 862822dab1 feat(web): add dev debug logging for git-status, dockview, and chat messages (#961) 2026-05-19 23:41:02 +01:00
Carlos Florêncio a74409af3c fix(web): cap clarification overlay height and add mock agent ask commands (#956) 2026-05-19 22:27:24 +01:00
Carlos Florêncio e32c6a252f fix(web): pre-fill PR head branch when launching task from GitHub PR list (#960) 2026-05-19 22:04:28 +01:00
Carlos Florêncio 2d219f1d1b fix(web): restore last-selected session tab on task re-entry (#951) 2026-05-19 21:44:15 +01:00
José Almeida a5341d5b66 fix(web): mobile-layout fixes for onboarding wizard + dialog (#955) 2026-05-19 19:14:21 +01:00
Carlos Florêncio 2bfdd518b6 fix(web): improve inline code chip visibility in markdown (#948) 2026-05-19 17:56:35 +01:00
Carlos Florêncio 3c3ae81f00 fix(web): add delete confirmation dialog in task sidebar and mobile sheet (#949) 2026-05-19 17:55:25 +01:00
José Almeida 54ddb0aa1e feat(web): highlight active-tab file row in Changes panel (#954)
* feat(web): track active file path for diff panels

Extend resolveFilePath so activeFilePath updates when a file-diff panel
(preview:file-diff or diff:file:<path>) becomes the active dockview
panel. Also re-subscribe onDidParametersChange for the preview file-diff
slot so swapping the preview to a different file refreshes the path.

* feat(web): highlight active-tab file row in Changes panel

When a file's diff tab is the active dockview panel, mark the matching
row in unstaged, staged, and PR Changes lists with data-active="true"
and reuse the existing bg-accent/60 highlight (same as multi-select).
PRFileRow also gains data-changes-file so it shares the existing row
selector with FileRow.

* test(web): cover active-tab highlight unit + E2E

Unit test in FileRow guards the data-active attribute and bg-accent/60
class when isActive (with and without isSelected). New E2E spec drives
the full flow: open file-a diff, switch to file-b, close the preview,
and verify highlight follows when a file moves from unstaged to staged.

* fix(web): use distinct e2e task titles for active-tab highlight spec

clickTab("Changes") fails strict-mode when the task title also contains
the word "Changes" — Playwright matches both the session tab and the
dockview Changes tab. Renames to "Active Highlight (Staged) Test".

* test(web): close pinned diff:file panels in closeFileDiffPreview

The helper previously closed only the preview slot, leaving pinned
diff:file:<path> panels (created when a preview is promoted) active.
Sweeps every diff:file: panel so the row settles to data-active=false
regardless of how the diff was opened.

* test(web): unit-cover resolveFilePath panel-id branches

Exercises every branch of the dockview-store resolveFilePath closure:
file:, diff:file:, preview:file-editor, and preview:file-diff (the
last two via params.path), plus the null-out paths. Catches regressions
in the panel-id mapping without paying for an E2E round-trip.

* fix(web): strip "changes" from active-tab spec profile names

The agent profile name becomes the dockview session tab title, so a
profile named "changes-active-highlight" makes clickTab("Changes")
match two .dv-default-tab elements (session tab + Changes tab) and
trip strict-mode. Rename to "active-highlight" / "active-highlight-
staged" to keep the locator unambiguous.

* docs(web): note multi-repo path collision in PRFilesRepoGroup

Mirrors the FileListBody comment so future authors see the same caveat
in both file-list components: activeFilePath carries the path but not
the repo, so identical paths across repos light up both rows.

* fix(web): snapshot panels before closing in closeFileDiffPreview

dockview's api.panels is a live reference; calling panel.api.close()
removes the panel in place, so iterating the live array skipped every
other diff:file:* panel when two or more were open. Filter into a
local array before closing.
2026-05-19 15:53:48 +01:00
Carlos Florêncio 20a660fe3e fix(web): strip phantom session panels on env-layout restore (#944) 2026-05-19 09:19:05 +01:00
Luan Martins 663fa6f69c feat(web): mobile file viewer with desktop parity (#945) 2026-05-19 09:18:22 +01:00
Carlos Florêncio 59ea5b7106 fix: respect user-chosen agent on workflow step transitions (#941) 2026-05-18 23:18:57 +01:00
José Almeida b4ce4d6eef refactor(web): improve compact desktop layouts (#937)
* refactor(web): improve compact desktop layouts

* fix(web): address compact desktop review feedback

* fix(web): address responsive review followups

* fix(web): preserve compact sidebar restore sizing

* fix(web): harden compact preset sidebar test

* refactor(web): extract dockview layout hooks
2026-05-18 23:08:14 +01:00
José Almeida af9d3547a6 feat(web): clearer custom-answer state and Cmd+Enter submit on clarifications (#934)
* feat(web): add clearAnswer + submit re-entry guard to useClarificationGroup

clearAnswer removes a stored answer so emptying a clarification draft can
revert the stepper to "unanswered". A small inflight ref in submitCollected
/ skipAll prevents duplicate POSTs when multiple submit paths race (e.g.
Cmd+Enter inside the custom input commits the draft and also asks the
parent to finalize the batch).

* feat(web): show selected state and Cmd+Enter submit on clarification custom input

The "Or type a custom answer…" input previously had no selected styling and
needed an Enter keypress before the carousel stepper marked the question as
answered, which made it easy to lose track of which path was the active
answer. Now:

- Typing into the custom input live-records the draft, so the stepper and
  group progress update immediately and the input picks up the same blue
  ring + check icon used by selected options. When custom is active, any
  previously picked option visually deselects (they are mutually exclusive
  at commit time).
- Picking an option after typing wipes that question's draft so the visible
  input and committed answer stay in sync.
- Cmd/Ctrl+Enter submits the bundle when every question is answered — from
  inside the custom input (commits the draft then finalises) and from
  anywhere else in the overlay. Inside the input is handled locally so
  submitCollected does not fire twice.

* test(web): cover live-record + Cmd+Enter clarification UX

Adds four cases over the existing clarification-multi mock scenario:
typing auto-selects custom and deselects the option, emptying the draft
reverts the stepper, Cmd+Enter from the last-step custom input submits
the bundle, and Cmd+Enter from outside the input submits when all answered.
Adds a session-page locator for the new custom-input container so the
data-active attribute can be asserted.

* fix(web): address bot review feedback on clarification UX

- Cmd/Ctrl+Enter inside the custom-text input no longer calls the
  per-question commit path. The draft is already live-recorded via
  onChange, so calling onSubmit here only added a stale advance-to-next
  step before the bundle was submitted. Now Cmd+Enter just finalises.
- handleSubmit depends on group.submitCollected (which the hook memoises)
  rather than the whole group object, so it no longer re-creates on every
  keystroke through the live-record path.
- Add a unit test that fires submitCollected twice concurrently and
  asserts the inflight ref keeps the wire count at 1 — direct coverage
  for the guard introduced in the previous commit instead of relying on
  the 409 round-trip test.
2026-05-18 22:32:16 +01:00
Carlos Florêncio 782fdb56ed feat(web): polish chat message rendering (#935) 2026-05-18 22:04:31 +01:00
Carlos Florêncio 117c7b220e fix(web): restore diff viewer bg override after pierre 1.1.22 rename (#939) 2026-05-18 21:50:59 +01:00
Carlos Florêncio adcde7c89e fix(backend): persist kandev-system wrap on first task prompt (#940) 2026-05-18 20:08:15 +01:00
Carlos Florêncio b3e8dcebf8 feat(web): show attachment thumbnails on queued messages (#936) 2026-05-18 20:02:30 +01:00
Carlos Florêncio f1cb1fdb21 chore(deps): bump @pierre/diffs to 1.1.22 with regression coverage (#918) 2026-05-17 22:32:02 +01:00
Carlos Florêncio 26a641ad13 fix(web): defer Enter to slash/mention suggestion when menu is open (#928) 2026-05-17 21:56:22 +01:00
Carlos Florêncio 32579db525 refactor(web): unify file-tree components on a useTree headless hook (#919) 2026-05-17 21:26:13 +01:00
Carlos Florêncio e82859990e chore(web): add pnpm typecheck script (#925) 2026-05-17 20:16:41 +01:00
Carlos Florêncio f6bd83da33 refactor: tighten type system across backend + frontend (#920) 2026-05-17 20:16:15 +01:00
Carlos Florêncio fd433626ae fix(cli): unblock make dev on WSL2 mirrored networking (#924) 2026-05-17 12:29:44 +01:00
Carlos Florêncio 0b2085688b feat(office): autonomous agent management layer (#914) 2026-05-16 20:07:57 +01:00
Luan Martins 7437194e2e fix: show PR diff when clicking file row that has local changes (#908) 2026-05-16 14:04:09 +01:00
Carlos Florêncio a5108de926 fix(windows): make conpty Close idempotent and kill orphan agentctl (#900) 2026-05-15 16:04:35 +01:00
José Almeida e144e8c530 fix: improve custom script menu readability (#905)
* fix: improve custom script menu readability

* chore: address verification drift

* chore: address custom script menu review
2026-05-14 23:57:05 +01:00