Commit Graph

1060 Commits

Author SHA1 Message Date
João Salavisa 8a4ac6c570 fix(cli): include node bin dir in service unit PATH for fnm/nvm/asdf/volta/mise (#1000)
* fix(cli): prepend node bin dir to service unit PATH so npx-based agents resolve

systemd/launchd units hard-coded a system-only PATH. Under per-user node
managers (fnm, nvm, asdf, volta, mise), node lives in a versioned
subdirectory like ~/.local/share/fnm/node-versions/v24.14.0/installation/bin,
so the daemon could exec node (ExecStart resolves the absolute path) but
could not exec npx — every ACP agent built with `npx -y <bridge-pkg>`
(claude, codex, opencode, …) failed at spawn time with "peer disconnected
before response".

`captureLauncher()` already records the node binary path. Its parent dir
is exactly where the matching npm/npx live, so prepend it to the unit's
PATH (with dedup so /usr/local/bin etc. don't appear twice).

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

* fix(cli): guard pathWithNodeBinDir against non-POSIX nodePath

`path.dirname` returns `'.'` on POSIX when given a path with no `/`
separator (e.g. a Windows-style `C:\…\node.exe`). Prepending `.` to a
service unit's PATH would put CWD ahead of system dirs in the daemon
context — a latent privilege-escalation footgun if any future caller
ever passed a relative or bare-filename nodePath. Production is safe
because `captureLauncher()` uses `process.execPath` (always absolute on
Linux/macOS), so the guard is defensive, not a behavior change.

Adds regression tests for both systemd and launchd, plus a previously
missing launchd system-mode + versioned-bin-dir test.

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:35:07 +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 76143f8159 fix(acp): restore ScheduleWakeup wire string broken by Wakeup→Run rename (#998)
PR #914 globally renamed Wakeup→Run in the ACP transport, including the
wire-level tool name check (`name != "ScheduleRun"`). That string is
hardcoded by the Claude Code SDK binary as `ScheduleWakeup`, so the
bridge's tool-call frames stopped matching and every Claude wakeup
silently never fired: kandev never armed the timer, never sent the
synthetic session/prompt, and the SDK's buffered wakeup turn only
drained when the user typed a new message.

Commit 0b2c59a2's lifecycle fix (publish AgentReady on wakeup turns)
relied on the wakeup turn actually firing — which it wasn't — so the
fix appeared not to work in production.

Restore the wire string to `"ScheduleWakeup"` and rename the surrounding
Go symbols back to wakeup-naming (`wakeupScheduler`, `pendingWakeup`,
`handleWakeupEvent`, `fireWakeup`, `wakeupPromptTimeout`) so the next
contributor can't repeat the rename. Add a comment at the match site
warning that the string is externally hardcoded, plus a negative
regression test asserting `"ScheduleRun"` is NOT recognised as a wakeup.

Co-authored-by: Kandev Agent <agent@kandev.dev>
2026-05-21 21:29:30 +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 9d2e572ffd ci: strip safe-to-review label on new fork pushes (#992)
* ci: strip safe-to-review label on new fork pushes

Mirrors the strip-safe-to-test job in preview-env.yml so a once-approved
fork PR cannot run the Claude review against new (potentially malicious)
code on subsequent pushes without fresh maintainer re-approval. Closes a
gap between the two pull_request_target workflows flagged by an external
scanner.

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

* ci: close strip-label race on fork PRs

Three reviewers (CodeRabbit, cubic, Greptile) flagged that strip-safe-to-review
ran in parallel with claude-review-fork on the same synchronize event, so the
label was still present in the webhook snapshot when the review job's if was
evaluated — the strip only blocked the next push, not the current one. Same
flaw existed for preview-env.yml's deploy-fork (worse there, since it runs
the fork's cmd/preview code with SPRITES_API_TOKEN).

Fix:
- Add `labeled` to pull_request_target types so a maintainer applying the
  opt-in label triggers a review/deploy against the current head SHA.
- Guard the label clause in claude-review-fork / deploy-fork with
  `github.event.action != 'synchronize'`. Trusted-author paths (OWNER /
  MEMBER / COLLABORATOR and the *_ALLOWLIST repo variable) still authorize
  on every event; only the label path is per-commit.
- Wrap removeLabel calls in try/catch that swallows 404 on races.
- Add workflow-level concurrency to dedupe rapid pushes (cancel-in-progress
  is false on `closed` for preview-env so cleanup can finish).

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

* ci: reorder fork PR if-gate branches to be fromJSON-safe

cubic flagged that the previous reorder put the `fromJSON(vars.*_ALLOWLIST)`
branch before the label clause. A non-empty but malformed allowlist value
would throw and skip the whole job, masking a legitimate maintainer label
approval. Reorder to: author_association -> label (with synchronize guard)
-> allowlist, so the cheap/safe branches short-circuit before any
potentially throwing fromJSON call. Applied to both fork workflows.

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:36:22 +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
José Almeida d02441201f fix(homebrew): mirror upstream PR formula fixes (#972)
* fix(homebrew): mirror upstream PR fixes

Keep the in-repo reference formula in sync with what's actually
being submitted at Homebrew/homebrew-core#283241:

- Bump url + sha256 to v0.50.0 (the version the upstream PR pins)
- Strip *musl* paths from the web bundle before libexec.install
  (brew linkage --test flags musl-linked variants of sharp on
  glibc-only Linuxbrew)
- Set KANDEV_AGENT_STANDALONE_PORT to a free port in the test block
  so agentctl doesn't collide on the default 39429

* fix(homebrew): switch test poll to quiet_system + agentctl-only

Mirror fixes from upstream Homebrew/homebrew-core#283241:
- Switch the functional test to spawn libexec/bin/agentctl (pure-Go,
  no cgo+sqlite) instead of the full backend, sidestepping a Linux
  arm64 agentctl-launcher crash unique to brew test sandbox
- Use quiet_system instead of system for the curl poll. Formula#system
  raises BuildError on the first non-zero exit, which killed the
  retry loop before agentctl had time to bind the port

* fix(homebrew): swap functional test to agentctl CLI dispatch

Mirror upstream Homebrew/homebrew-core#283241 — Linux arm64 bottle CI
keeps timing out spawning agentctl as an HTTP server. The 'agentctl
kandev' subcommand exercises the same binary's boot + flag parser
+ CLI dispatcher path without binding any ports, which fits within
the sandbox constraints of brew test.

* fix(homebrew): build agentctl static (CGO_ENABLED=0)

Mirror upstream Homebrew/homebrew-core#283241 — agentctl was
segfaulting on Linuxbrew arm64 bottle CI. agentctl is pure-Go
(no cgo deps), so building it without cgo gives a static binary
that avoids whatever dynamic-linker issue is biting on that
platform. kandev still needs cgo for mattn/go-sqlite3.

* fix(homebrew): tighten musl-prune style + assertion

Mirror upstream Homebrew/homebrew-core#283241:
- Use bundle.glob(...) instead of Pathname.glob with string interp
- Drop the if p.directory? guard so bare musl .node files (if a
  future bundler version emits them) also get stripped — rm_r
  handles both shapes
- Spell out sharp / @swc/core / lightningcss in the comment
- Tighten the agentctl smoke check to an exact-string match

* fix(homebrew): guard musl prune against ENOENT race

bundle.glob returns parent dirs alongside the musl-named .node
files inside them. rm_r on the parent makes the child path vanish
before iteration reaches it, blowing up with Errno::ENOENT. Skip
paths that no longer exist.
2026-05-21 11:29:30 +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 0b2c59a2dd fix(lifecycle): publish AgentReady on wakeup-driven turns (#875)
* fix(lifecycle): publish AgentReady on wakeup-driven turns

Wakeup turns flow Adapter.Prompt → bridge → events, bypassing
SessionManager.SendPrompt. The execution status stayed Ready across
the wakeup turn, so MarkReady's duplicate-suppression guard dropped
the second AgentReady — the orchestrator never closed the turn,
never evaluated on_turn_complete, and never dispatched queued
messages. Detect this in recordActivity and flip Ready → Running on
the first non-terminal event, re-emitting AgentRunning so the rest
of the pipeline behaves like any other turn.

* fix(lifecycle): gate wakeup flip on turn-content events and handle empty turns

Three follow-ups on the wakeup AgentReady fix:

1. Restrict the Ready → Running flip in `recordActivity` to events that
   unambiguously indicate turn-in-progress activity (message_chunk,
   reasoning, tool_call/update, plan, agent_plan, permission_request).
   The original whole-event-type flip caused a regression: claude-agent-acp
   emits available_commands_update asynchronously ~50ms after session/new,
   well after dispatchInitialPrompt fires MarkBootReady for a no-prompt
   task. The Ready → Running flip on that delayed metadata event was
   putting newly-created tasks back into Running and breaking the chat-
   input UI (changes-panel-focus E2E tests).

2. Handle the empty-wakeup edge case: a wakeup turn that emits only a
   `complete` event (empty model response) is still excluded from the
   flip whitelist, so the Ready guard in MarkReady would suppress
   AgentReady. `handleCompleteEventMarkState` now publishes AgentReady
   directly when Status is already Ready on a successful complete —
   the orchestrator still closes the turn for empty wakeup responses.

3. Fold the `executionStore != nil` check into the early-return so the
   AgentRunning publish only fires when the status flip actually succeeded.

Test-file cleanup from review feedback: dead loop, redundant
`var _ = io.MultiReader`, and `teeReader.src any` → `io.Reader`.

New regression tests:
- TestWakeup_PostBootMetadataEventsDoNotFlipToRunning — locks in the
  whitelist behavior so future events can't accidentally re-arm a
  boot-ready session.
- TestWakeup_EmptyTurnStillPublishesAgentReady — covers the
  empty-wakeup-complete fallback.

* fix(lifecycle): publish AgentRunning before AgentReady on empty wakeup turns

Follow-up on the previous wakeup fixes — addresses three issues raised in
review:

1. **Empty-wakeup AgentReady was ignored by the orchestrator.** The fallback
   in `handleCompleteEventMarkState` published AgentReady when Status was
   already Ready, but `handleAgentReady` at the orchestrator early-returns
   when `session.State != Running/Starting`. After the previous turn ended
   the session is in WaitingForInput, so the AgentReady got silently
   dropped — same suppression the wakeup fix was meant to prevent. Now the
   fallback flips Ready → Running + publishes AgentRunning first (mirroring
   `recordActivity` for the non-empty case), then falls through to MarkReady.

2. **firstActivityOnce could still publish AgentRunning on a late
   metadata event.** Gate it on `Status != Ready` so a delayed
   `available_commands_update` arriving after MarkBootReady (the original
   regression scenario) can't slip through via the once-per-execution path.
   In practice the adapter always emits `agent_capabilities` during
   Initialize before MarkBootReady, so firstActivityOnce still fires there
   with Status==Running — this is defensive hardening.

3. **Tighter test assertions.** The post-boot-metadata test now primes
   firstActivityOnce in setup and asserts strict `runningCount == 0`.
   The second-turn test verifies AgentRunning precedes each AgentReady in
   bus order so the orchestrator's session-state guard isn't violated.
   Same ordering check added to the empty-wakeup test.

* test(lifecycle): rename exec to agentExec to avoid os/exec shadow

The test variable `exec` in wakeup_e2e_test.go shadowed the imported
`os/exec` package — harmless today because all `exec.Command`/`LookPath`
calls happen before the local binding, but a future call added below
would silently fail. Rename to `agentExec` to remove the shadow.

* ci: retrigger CodeQL after transient Ruby analyze flake

The default-setup CodeQL run on PR #875 produced a flaky 'CodeQL could
not process any code written in Ruby' failure on the Ruby analyze job
(github.com/kdlbs/kandev/actions/runs/26124163769). The repo has no
Ruby code and main's CodeQL only analyzes javascript-typescript +
actions; the Ruby job was added dynamically and failed on the empty
diff. Sibling run 26124163815 on the same SHA passed. Cannot rerun
default-setup workflows via the API (403 'cannot be retried'), so
push an empty commit to retrigger the analysis.

* test(lifecycle): tighten wakeup test assertions

- wakeup_simulated_test.go: assert exact readyCount==2 instead of <2 so
  any over-emission (e.g. duplicate AgentReady on the wakeup turn) is
  also caught.
- wakeup_e2e_test.go: fail with t.Fatalf when the adapter surfaces no
  WAKEUP text instead of logging a warning. Otherwise the bus-level
  assertions below pass vacuously if the wakeup never fires.

Both fixes address review comments from cubic-dev-ai / claude[bot] on
PR #875.

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

* test(lifecycle): remove unsynchronised SessionID write in forwarder goroutine

The forwarding goroutine wrote to agentExec.SessionID without holding
the executionStore mutex, racing with the main goroutine's authoritative
assignment after NewSession returns. The forwarder's write is redundant
— line 264 sets the canonical value — and benign in practice, but
`go test -race` would flag it. Drop the write to keep the race detector
clean.

Addresses review comment from claude[bot] on PR #875.

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 01:37:03 +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
José Almeida 038587acd5 fix(homebrew): clear rubocop offenses on formula (#967)
fix(homebrew): clear two rubocop offenses

brew style on homebrew-core surfaced two offenses on the formula
copy that landed in main with PR #904:

- Layout/ExtraSpacing: double space before => in uses_from_macos
- Style/FileNull: use File::NULL instead of "/dev/null"

Mirror the same fixes already applied to the upstream PR
(Homebrew/homebrew-core#283241).
2026-05-20 22:55:17 +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
github-actions[bot] a00d56412c release: 0.50.0 v0.50.0 2026-05-19 22:51:13 +00: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 4c6614f755 docs: update roadmap with current priorities and completed items (#965) 2026-05-19 22:57:44 +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
Carlos Florêncio 8467dd94f8 fix(ws): detach dispatched handler ctx from WS connection lifetime (#959) 2026-05-19 21:43:45 +01:00
irium 9fd789ba04 fix(executor): persist worktree subdir as workspace_path (#958) 2026-05-19 20:49:00 +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 b73c00f054 fix(backend): skip redundant task-state writes when session state is unchanged (#953) 2026-05-19 18:27:16 +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
José Almeida 7115ee08d5 fix(acp): stop inventing currentModelId from AvailableModels[0] (#947)
* fix(acp): stop inventing currentModelId from AvailableModels[0]

When session/new returned an empty currentModelId, the adapter fell back
to AvailableModels[0]. Auggie returns an alphabetically-sorted list whose
[0] is a pseudo-agent ("Build Analyzer"), so the UI showed the wrong
model on every fresh load and session resume.

Drop that fallback (the configOption-based fallback stays — that one is
legitimate). When neither CurrentModelId nor a model-shaped configOption
provides a value, emit empty and let the frontend resolve to the profile
or snapshot model.

Also emit a session_models convergence event after SetModel succeeds,
so the UI updates after the lifecycle manager applies the profile model
at session init and after user-driven /session/set_model calls. Cache
configOptions alongside availableModels for inclusion in that event.

* fix(acp): tighten SetModel convergence event semantics

Address PR #947 review feedback:

- Bind the convergence event to the session captured under the same
  RLock used to read the connection. `SetModel` now snapshots
  `sessionID`, `available`, and `cachedConfig` together and passes
  them into `emitSetModelEvent`, so a concurrent session switch can
  no longer route the event to the wrong target.

- Rewrite the model-shaped configOption's `CurrentValue` to the new
  `modelID` before emitting. Without this the event carried
  `CurrentModelID = <new>` alongside `ConfigOptions[model].CurrentValue
  = <old>` — a latent inconsistency for codex-style consumers that
  read the option value. The cached slice is copied first so the
  caller's snapshot is not mutated.

- Refresh `availableConfigOptions` under `Lock` from inbound
  `ConfigOptionUpdate` notifications, so a subsequent `SetModel`
  convergence event uses the latest options instead of the
  session/new snapshot.

- Log the convergence event with `session_id` and `model_id`, matching
  the logging already on `emitSessionModels`.

- Extract a `configOptionIDModel` constant for the well-known "model"
  ID (silences goconst on the new helper).

Tests: extend `TestEmitSetModelEvent_EmitsSessionModelsWithCachedState`
to assert the `CurrentValue` rewrite and the caller-slice
no-mutation guarantee; add `TestConfigOptionUpdate_RefreshesCachedConfig`
to pin the cache refresh.

* test(acp): tighten ConfigOptionUpdate cache-refresh assertions

Address two minor follow-up suggestions from Claude on PR #947:

- Add a comment to emitSetModelEvent noting that the cachedConfig
  copy is shallow (Options shares the caller's backing array) — safe
  today because only CurrentValue is mutated, but worth flagging in
  case a future caller mutates Options in place.

- Have TestConfigOptionUpdate_RefreshesCachedConfig assert that the
  returned event's CurrentModelID resolves to the fresh value from
  the refreshed configOption, so the test pins both the cache update
  and the event payload in one place.

* test(acp): cover Category-based model option rewrite path

Address Claude's follow-up suggestion on PR #947: the previous test
only exercised the ID == "model" branch of emitSetModelEvent's
CurrentValue rewrite, so an accidental drop of the
Category == "model" branch would have passed. Add a third entry
with Category: "model" (and a non-matching ID) and assert both
matched options are rewritten while the unmatched one is untouched.
2026-05-19 14:18:51 +01:00
José Almeida 0c0c64f5c4 docs: draft homebrew-core formula and submission spec (#904)
* docs: draft homebrew-core formula and submission spec

Adds a source-build Formula/kandev.rb (parallel to the binary-install
formula in kdlbs/homebrew-kandev), plus spec.md + submission-checklist.md
under docs/specs/homebrew-core/.

* fix(homebrew): add uses_from_macos sqlite, drop head stanza

Address Greptile review on PR #904: declare sqlite as a build-time dep
for the cgo go-sqlite3 link, and drop the head stanza since
homebrew-core typically asks new formulae to omit it.

* chore(homebrew): bump formula pin to v0.49.0 for local preflight

* fix(homebrew): switch livecheck to git strategy with version regex

GithubLatest hits GitHub's API and is reserved for formulae that
consume release assets or where git tag inspection can't identify
the latest version. Ours uses the auto-generated tag archive and
publishes clean vX.Y.Z tags, so the default git strategy is the
right choice per docs.brew.sh/Brew-Livecheck.

* fix(release): replace rsync with cp -RL for cross-environment portability

Linuxbrew CI ships without rsync, which broke the homebrew-core
formula build. cp -RL provides the same dereference-symlinks behavior
on both BSD and GNU coreutils, no extra dependency required.

* Revert "fix(release): replace rsync with cp -RL for cross-environment portability"

This reverts commit 5599817f4f.

* fix(homebrew): add rsync as build dep

Linuxbrew CI ships without rsync, and package-web.sh uses
rsync --copy-links --safe-links to dereference pnpm's symlinks
while tolerating broken absolute symlinks from the content-addressed
store. Adding rsync as a build dep keeps the script unchanged.

* fix(homebrew): address maintainer review feedback

Apply review from SMillerDev on homebrew-core/pull/283241:
- Use std_go_args helper for both go build calls
- Invoke release scripts directly (rely on shebang) instead of bash
- Extract bundle assembly into scripts/release/package-bundle.sh
- Replace --version/--help test block with a functional health check
  that boots the Go backend with an isolated data dir, polls
  /api/v1/system/health, and shuts down cleanly

* fix(homebrew): address PR #904 review feedback

- spec.md: fix "no tap-tap" typo
- submission-checklist.md: bump stale v0.41.0 references to v0.49.0
- test block: rescue Errno::ESRCH/ECHILD in ensure so original failure
  diagnostic isn't masked when the backend crashes early

* chore(homebrew): drop submission-checklist.md from PR

* docs(homebrew): align spec.md with current formula

- Add rsync to build deps list and note uses_from_macos sqlite
- Update livecheck snippet to match the regex form actually shipped
- Replace --help/--version test description with the health-check
  block currently in Formula/kandev.rb

* fix(homebrew): move rsync to uses_from_macos

cubic flagged on PR #904: depends_on "rsync" trips
brew audit --new-formula because rsync ships with macOS.
uses_from_macos "rsync" => :build uses system rsync on
macOS and installs Homebrew's on Linux, which matches the
intent.

* fix(release): drop redundant chmod in package-bundle.sh

package-cli.sh:41 already runs chmod +x on cli.js when it creates
the file, so re-running chmod here is pure noise.
2026-05-19 13:52:07 +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 b196519ac5 docs(skills): capture commit + pr-fixup learnings from #935 (#943) 2026-05-18 22:25:52 +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 af68c0a8f5 chore: suppress pnpm modules-purge prompt in non-interactive installs (#932) 2026-05-18 19:59:10 +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 c62739d3c1 feat(cli): kandev service install for systemd and launchd (#926) 2026-05-17 21:12:20 +01:00