2430d351e643fe9556a5964ccf004e310958340b
1074 Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
2430d351e6 | feat(release): credit external contributors in release notes (#975) | ||
|
|
e85c340797 | fix(web): honor window.__KANDEV_DEBUG so make start-debug logs surface (#991) | ||
|
|
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> |
||
|
|
c9b6fe4b8a | fix(worktree): support fork PR worktree creation via pull refspec (#990) | ||
|
|
07a9784d1c |
fix(share): route share links through gist.githack so big tasks render (#1008)
* fix(share): route share links through gist.githack so big tasks render gistpreview.github.io fetches gists via the public GitHub API, which returns `content: ""` for files past GitHub's per-response content budget (~1 MB). For non-trivial sessions, share.html and snapshot.json land past that budget and gistpreview's `document.write(content)` renders a blank page. gist.githack.com proxies the raw file content directly, so multi-MB share.html renders correctly — at the cost of a one-time Cloudflare anti-phishing interstitial on first visit, accepted as the price of fidelity for big tasks. displayURL now normalises bare gist URLs and unpinned githack URLs to the canonical `gist.githack.com/<owner>/<id>/raw/share.html` form; legacy gistpreview URLs pass through unchanged (the owner is not recoverable from a gistpreview URL, so users can revoke + re-share to get the githack form going forward). * fix(share): address PR #1008 review feedback - displayURL now re-pins legacy gistpreview URLs stored without /share.html so they hit the styled view instead of silently falling back to the alphabetically-first README.md (flagged by Greptile). - Spec: unify two stale gistpreview references (API endpoint description and the publish scenario) to the canonical githack form, with a note that legacy gistpreview rows pass through. Flagged by CodeRabbit and Claude — the spec is the public contract; one stale mention undermines the migration. --------- Co-authored-by: Kandev Agent <agent@kandev.dev> |
||
|
|
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. |
||
|
|
35b18aa853 |
fix(release): build kandev binary with -tags fts5 (#1007)
* fix(release): build kandev binary with -tags fts5 The tasks_fts virtual table + sync triggers shipped in v0.50.0 (#914) require the fts5 SQLite build tag, which apps/backend/Makefile already sets. The release workflow did not, so published binaries lack the module and fail to open any DB that an FTS5-enabled build previously touched ("no such module: fts5" during schema init). Add -tags fts5 to the kandev build and propagate the standard version ldflags from the Makefile so kandev --version reports the released tag. Fixes #1006. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(release): propagate version ldflags to cross-compiled agentctl The linux/amd64 agentctl helper is built in a separate workflow step whose shell does not inherit LDFLAGS from the previous build step. Export LDFLAGS via \$GITHUB_ENV so the cross-compiled binary embeds the same Version/Commit/BuildTime metadata as the matrix-built agentctl, matching apps/backend/Makefile's build-agentctl-linux target. 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> |
||
|
|
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> |
||
|
|
3c7b6c3d93 |
fix(loginpty): drain PTY before close on natural child exit (#1004)
* fix(loginpty): drain PTY before close on natural child exit so last bytes aren't lost The supervise loop closed the PTY master immediately after the child exited, racing with readLoop's blocked Read. Go's poller cancels the in-flight Read with "file already closed" and the kernel's buffered bytes are dropped — observed as TestStart_RunsCommandAndExits flaking on CI with `expected 'hello' in output, got ""`. Give readLoop a 500ms grace window to drain naturally (Unix master sees EIO once the slave closes); only force-close if drainage doesn't complete (Windows ConPTY, which never delivers EOF on child exit). Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(loginpty): always close PTY after drain window so the master fd doesn't leak The previous revision only called sess.stop() in the timeout branch of the drain select, so on the Unix happy path (readDone fires first) the PTY master fd stayed open until GC ran the *os.File finalizer. Move stop() outside the select — by the time it runs readLoop has already returned, so there's no in-flight Read to race with — and switch the fallback to time.NewTimer with Stop() to release the timer eagerly when readDone wins. Addresses review feedback from CodeRabbit, Claude, Greptile, and cubic on #1004. 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> |
||
|
|
f39e7b06ca |
fix(agent): honor session passthrough snapshot when starting agent process (#1003)
* fix(agent): honor session passthrough snapshot when starting agent process Tasks created in agent (ACP) mode got stuck in "starting" after toggling the profile's cli_passthrough flag and restarting the server. StartAgentProcess re-resolved the live profile each launch, so the new flag rerouted existing ACP sessions into the passthrough path (and vice versa), where the workspace plumbing didn't match. Snapshot session.IsPassthrough onto LaunchAgentRequest / lifecycle.LaunchRequest / AgentExecution and let StartAgentProcess use that as the source of truth. Sessionless launches (legacy controller.LaunchAgent) still fall back to live profile resolution. Fixes #981. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(agent): make passthrough snapshot the sole routing signal in StartAgentProcess Bot review feedback follow-up (#1003): - Route on the snapshot alone (new routePassthrough helper). A transient ResolveProfile error no longer silently routes a passthrough session to ACP — the fresh path surfaces an explicit error, the resume path surfaces the existing resolvePassthroughAgent error. - Add TestStartAgentProcess_PassthroughResume_SurvivesProfileResolveError and TestStartAgentProcess_FreshPassthrough_SurfacesProfileResolveError to lock the no-silent-ACP-fallback invariant. - Add TestLaunchPreparedSession_PropagatesIsPassthrough mirroring the resume-path propagation test so both build paths are independently covered. - Strip issue-number references from source/test comments per project convention. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(agent): avoid double ResolveProfile in sessionless passthrough path routePassthrough now returns (bool, *AgentProfileInfo). When the sessionless fallback path already resolved the profile, it threads it back so StartAgentProcess can reuse it for command building instead of calling ResolveProfile a second time. Snapshot-driven routing still returns nil so the snapshot path resolves fresh — the snapshot deliberately outlives any particular live profile state. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(agent): extract startPassthroughExecution to satisfy nestif CI's golangci-lint flagged the nested-if complexity of the passthrough dispatch block in StartAgentProcess after the previous round of fixes pushed it past nestif's threshold of 5. Move the resume-vs-fresh branching plus the lazy profile resolution into a startPassthroughExecution helper so StartAgentProcess becomes a single short routing decision. No behavior change. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * docs: drop caller reference from startPassthroughExecution comment Per project convention, source comments shouldn't name the calling function — the reason it's its own method is captured by its contract (profileInfo nullability + routePassthrough handoff), not by the caller's name. 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> |
||
|
|
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> |
||
|
|
722cd31489 |
fix(cli): include ~/.local/bin in service unit PATH for user-mode installs (#994)
User-mode systemd units and launchd agents now prepend the loading user's ~/.local/bin to PATH, so user-installed agent CLIs (claude, codex, etc., installed via npm user prefix, pipx, fnm/nvm) are discoverable by the kandev daemon. Without this they appeared as `not_installed` even though they were on the user's interactive PATH. System-mode units keep the original system-only PATH. |
||
|
|
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> |
||
|
|
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> |
||
|
|
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> |
||
|
|
d14cda778e | fix(web): close clarification overlay even when WS event is lost (#997) | ||
|
|
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> |
||
|
|
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> |
||
|
|
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> |
||
|
|
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> |
||
|
|
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. |
||
|
|
b39f256e84 | fix(orchestrator): propagate RepositoryID so worktree setup script runs (#969) | ||
|
|
2ca48fc7df | feat(repositories): guard delete behind active-session check + UI fixes (#968) | ||
|
|
1f264adbc4 | fix(github): drain piled-up PR/issue review tasks via cleanup policy + manual sweep (#929) | ||
|
|
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> |
||
|
|
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> |
||
|
|
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> |
||
|
|
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> |
||
|
|
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>
|
||
|
|
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> |
||
|
|
474346bc9a |
fix(web): restore padding on office task-creation title input (#964)
Co-authored-by: Kandev Agent <agent@kandev.dev> |
||
|
|
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). |
||
|
|
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.
|
||
|
|
f2479aabce | feat(web): collapsible queue chip with animated panel (#957) | ||
|
|
a00d56412c | release: 0.50.0 v0.50.0 | ||
|
|
862822dab1 | feat(web): add dev debug logging for git-status, dockview, and chat messages (#961) | ||
|
|
4c6614f755 | docs: update roadmap with current priorities and completed items (#965) | ||
|
|
a74409af3c | fix(web): cap clarification overlay height and add mock agent ask commands (#956) | ||
|
|
e32c6a252f | fix(web): pre-fill PR head branch when launching task from GitHub PR list (#960) | ||
|
|
2d219f1d1b | fix(web): restore last-selected session tab on task re-entry (#951) | ||
|
|
8467dd94f8 | fix(ws): detach dispatched handler ctx from WS connection lifetime (#959) | ||
|
|
9fd789ba04 | fix(executor): persist worktree subdir as workspace_path (#958) | ||
|
|
a5341d5b66 | fix(web): mobile-layout fixes for onboarding wizard + dialog (#955) | ||
|
|
b73c00f054 | fix(backend): skip redundant task-state writes when session state is unchanged (#953) | ||
|
|
2bfdd518b6 | fix(web): improve inline code chip visibility in markdown (#948) | ||
|
|
3c3ae81f00 | fix(web): add delete confirmation dialog in task sidebar and mobile sheet (#949) | ||
|
|
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.
|
||
|
|
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.
|
||
|
|
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
|
||
|
|
20a660fe3e | fix(web): strip phantom session panels on env-layout restore (#944) |