b001aa1eb8d472630ce02a00a789c19fd69b0200
1186 Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
b001aa1eb8 |
feat(web): unified app sidebar (#1165)
* feat(web): introduce unified AppSidebar and retire legacy nav surfaces
Mounts a single collapsible AppSidebar at the root layout that replaces the
former WorkspaceRail, OfficeSidebar, and dockview-embedded sidebar pane.
Wires per-section expand + collapse state through a new UI slice with
localStorage persistence, ports projects/agents/settings/tasks sections, and
trims integrations / quick-chat / settings buttons + the home breadcrumb
from the dockview top bar (debug-only MoreToolsMenu kept). Strips the
sidebar column from every preset and the panel registry; persisted
sidebar panels are dropped by the existing sanitize layer at restore time.
* test(web): point workflow-filter e2e at AppSidebar's Home link
The dockview top bar no longer renders the IconHome breadcrumb after the
unified sidebar landed — the home navigation now lives in AppSidebar's
primary nav. Update the workflow-filter spec to click that link instead.
* feat(web): wire app-sidebar primary controls, nested settings, and footer migration
Gates office-only nav (Inbox, Agents, Projects, New Task) behind the office
feature flag, renames Tasks → Kanban as the bottom flex-grow section, replaces
the gradient workspace picker with a minimal trigger when office is off, and
moves Stats/Improve/What's-New/Office out of the kanban top bar into the
AppSidebar footer + a dedicated Integrations section. Expands Settings into a
collapsible deep tree mirroring the old SettingsAppSidebar so /settings pages
can drop their nested SidebarProvider.
* feat(web): make app-sidebar resizable and inline task top bar actions
Adds a persistent drag-resize handle on the AppSidebar's right edge that
clamps between APP_SIDEBAR_EXPANDED_WIDTH and 30vw; width is stored on the
appSidebar slice and persisted to localStorage. Drops the dockview top bar's
overflow popover (TopbarActionOverflow) in favor of always-inline clusters,
and replaces the debug "More tools" dropdown with a single debug toggle button.
* feat(web): polish app-sidebar headers and consolidate task views
Section headers now lead with a clean uppercase label, drop the mono
clash, lift contrast from /60 to /70, and end with the collapse chevron
at the right so the section body aligns flush under the label.
The Tasks section's filter bar (view chips + gear in a card-tinted
strip) is replaced by a compact view dropdown that lives in the section
header. Drag-to-reorder lives in the existing filter popover, which is
now reachable from the same gear button.
Settings moves below Tasks so the kanban list keeps the flex-grow seat
in the sidebar.
* feat(web): move settings group chevrons to the right
* fix(web): drop chat from home topbar; calm tasks group headers
The kanban top bar's Quick Chat button is removed now that Quick Chat
lives in the AppSidebar; workspaceId stops being threaded into the
tablet and desktop header variants.
Group headers inside the Tasks section (kanban workflow steps) lose
the opaque background strip and the section-label uppercase styling
so they read as inline subgroup rows, not duplicate section dividers.
The tasks-section wrapper drops its negative margin so the group rows
sit flush under the TASKS label instead of bleeding out past it.
* fix(web): restore agent logos on settings tree profile leaves
SettingsLeaf gains a leadingIcon slot for pre-rendered visuals; agents
group passes <AgentLogo agentName={agent.name}> so each profile row
shows its agent's logo like it did before the old SettingsAppSidebar
was retired.
* feat(web): brand the sidebar, move New Task off the kanban top bar
The Add task button (CreateTaskTopbarButton) leaves both the desktop
and tablet kanban headers; New Task is now an unconditional primary
nav item on the AppSidebar, falling back to a disabled affordance when
no workspace is active.
AppSidebar header gains a Kandev wordmark that links to /, with a
single-letter K mark in collapsed-rail mode. PageTopbar's root variant
no longer renders the parent brand label when backLabel is empty, so
the home view's top-left "Kandev" goes away in favor of the sidebar.
* feat(web): unbind sidebar toggle by default; drop Home from breadcrumbs
TOGGLE_SIDEBAR's configurable default is now UNBOUND_SHORTCUT (the new
sentinel for "no binding"); matchesShortcut already returns false on
an empty key, so the keybind hook needs no change. Keyboard shortcuts
settings show "Unbound", expose a Clear (X) button when a custom
binding is set, and the reset icon falls back to unbound for entries
whose default is unbound.
PageTopbar's breadcrumb variant no longer prepends the Home back-link
when backHref is "/" (the AppSidebar provides Home navigation).
* feat(web): move New Task into the sidebar, retire the workspace selector
- Extract AppSidebarNewTaskItem (office vs regular create dialog) with tests
- Drop the Workspace selector from the kanban display dropdown and mobile
menu sheet; the sidebar workspace picker is now the sole switcher
- Factor SectionHeader out of AppSidebarSection; animate fixed sections via
Collapsible while the grow (Tasks) section stays flex-driven
- Polish brand header, share SIDEBAR_ITEM_ACTIVE accent-bar classes, move
task group chevrons to the right, drop the task-item selected fill
- Bump dockview layout keys to v3 and filter the orphaned 'sidebar' column
so pre-AppSidebar default layouts no longer render a broken grid
- Point the cross-workspace isolation e2e at the sidebar picker
* feat(web): sidebar settings mode, subtask shortcut, accordion settings nav
- Restore subtask creation: the sidebar New Task row now shows a trailing
subtask affordance when viewing a task (regular mode), reopening the
NewSubtaskDialog the retired dockview header dropdown used to provide
- Fix settings pages not scrolling: scroll the content region (topbar stays
pinned) via min-h-0 + overflow-y-auto
- Add a footer gear that toggles a settings-only sidebar takeover; the gear
spins on toggle. Mode is transient (never persisted) and force-expands a
collapsed rail on entry
- Make the settings nav a single-open accordion: opening one top-level group
closes the others and reveals its subsections; route changes re-sync the
open group. Extract the tree into a reusable SettingsTree
- Repoint subtask e2e specs at the new sidebar affordance
* test(web): restore create-task e2e flow after New Task moved to sidebar
The New Task relocation removed the kanban-header create-task-button and made
the sidebar entry auto-navigate + open the office dialog under the e2e profile
(office on), breaking ~20 specs that drive the regular task-create dialog.
- Give the sidebar New Task the create-task-button testid so KanbanPage
resolves it again
- Drop the post-create auto-navigation so creating a task stays on the board
(matches the retired kanban button; specs find/click the new card)
- Ungate the subtask affordance from office mode (subtasks use the compact
NewSubtaskDialog in both modes, as the old dropdown did)
- Add a useRegularMode() helper that runs a spec file with KANDEV_FEATURES_OFFICE
disabled (backend.restart bracket) and apply it to the 18 create-task specs
- Point the System sidebar-navigation spec at a /settings/system page so the
single-open settings accordion has that group expanded
Validated: create-task, create-task-disabled-tooltip, create-task-branch-selector,
and subtask all green (modulo pre-existing idleInput agent-timing flakes that
CI retries absorb).
* fix(web): sidebar task navigation, settings-mode close + transitions
- Clicking a task in the global sidebar from a non-task page now navigates to
the task route (mounts the dockview) instead of only rewriting the URL via an
in-place layout switch that assumed the dockview was already mounted
- Make the settings-mode 'Settings' header a close affordance (the footer gear
is far from the tree); clicking it exits the takeover regardless of which
group is expanded, and it fades in when opened
- Fade in the Tasks (grow) section content on expand, matching the other
sections' animated reveal
- Enable next-themes disableTransitionOnChange so dark/light flips switch every
surface in the same instant rather than each animating at its own duration
- Add a regression e2e for sidebar→dockview navigation
* fix(web): clear sidebar task highlight when off the task route
The AppSidebar is global, so tasks.activeTaskId lingered after navigating Home,
leaving the last-opened task visually selected in the sidebar. Gate the
selected/active highlight by route — only highlight while on /t/ or
/office/tasks/. Expose data-active on the sidebar task item for testability.
Adds an e2e: entering a task highlights it; returning Home clears it.
* fix(web): never build the dockview default layout at zero width
On a fresh client-side navigation into a task the dockview can mount before the
grid has painted, so measureDockviewContainer returned api.width/height = 0.
Building the default layout at width 0 makes dockview collapse the horizontal
columns into a vertical stack (chat / files+changes / terminal) instead of the
intended side-by-side layout. Fall back to the viewport when neither the live
container nor the api report a usable size; the resize observer then snaps the
grid to the exact container size.
TDD: dockview-measure.test.ts covers the zero-size fallback.
* fix(web): keep the default layout horizontal when swapping chat for the session tab
Entering a task rendered a vertical stack (chat on top, files/changes in the
middle, terminal at the bottom) instead of the side-by-side default. The
chat->session-tab swap removed the 'chat' placeholder BEFORE adding the session
panel; when chat was the only panel in the center group, removing it destroyed
that group, the stored centerGroupId went stale, resolveInitialPosition
returned undefined, and the session panel was appended as a new row — flipping
the grid root to VERTICAL with three stacked groups.
Fix: add the session panel into chat's live group first (resolveInitialPosition
now prefers the chat panel's group), THEN remove the chat placeholder, so the
center group never empties and the horizontal split is preserved.
Also fixes the pre-existing pane-resize reload/clamp e2e failures, which were
the same collapse. TDD: task-default-layout.spec.ts asserts the right column
sits beside chat.
* fix(web): remove the sidebar Settings section; settings is gear-only
Settings was still a collapsible nav section in the sidebar AND auto-expanded on
/settings routes. That also caused the reported bug: toggling the footer gear
off while on a settings page revealed the normal sidebar with the Settings
section already expanded, so the settings tree appeared to 'not close'.
- Drop the Settings nav section (and its /settings route auto-expand) — the
settings tree is reached only via the footer gear takeover
- Delete the now-unused sidebar settings-section component
- e2e: gear opens the tree and closes it even after navigating into a section
* fix(web): show office sidebar sections only while inside Office
Inbox, Projects, and Agents were gated on the office feature flag, so they were
always visible when the feature was enabled. Gate them on actually being in the
Office surface (an /office route, reached via the footer Office button) via a
new useInOffice() hook — they stay hidden in the regular workspace and appear
only after entering Office.
Unit test for the hook; e2e (office fixture) asserts the sections appear on
/office and are absent on the regular home.
* fix(web): drop the colorful workspace selector, restore the minimal one
The workspace picker swapped to a gradient/initials trigger (and gradient
swatches in the dropdown) whenever office was enabled. Always use the plain
name + chevron trigger and drop the gradient swatches, matching the previous
design. Removes the now-unused workspace-gradient helper.
* fix(web): align sidebar header, scope office New Task, exit settings on nav
- Drop the doubled right border: the resize-handle hairline is transparent at
rest, leaving the aside's border-r as the single edge (highlights on hover).
- Collapse the expanded sidebar header into one h-10 row (brand · workspace
picker · collapse) so it lines up with the page/dockview top bar.
- Gate the sidebar New Task dialog on useInOffice() (actual /office route)
instead of the bare office feature flag, so Kanban mode keeps the classic
create dialog while office is enabled.
- Close the settings takeover when navigating off the /settings surface
(Home, Kandev brand, Office, etc.), tracked on a real pathname change so the
gear still opens it in place and in-tree navigation keeps it open.
- Lock in the already-working collapsed-gear-expands behaviour with an e2e.
Tests: new-task-item unit (route gating), settings-gear e2e (Home closes,
collapsed gear expands), office gating e2e (Kanban vs Office dialog).
* fix(web): realign task top bar to h-10, refine sidebar workspace picker
- Pin TaskTopBar to h-10 so its bottom border lines up exactly with the
sidebar header (and the PageTopbar) — previously it had no fixed height and
sat ~2px off, breaking the seam across the sidebar/content edge.
- Redesign the workspace picker as a compact, muted secondary control inlined
after the Kandev brand: brand and workspace share text-sm so they sit on a
common baseline, with a faint '/' separator; the workspace reads as muted and
the brand stays primary.
- Drop the workspace switcher (and its irrelevant folder glyph) from the
collapsed rail — collapsed now shows just brand + expand.
Adds an e2e that measures the task top bar bottom edge against the sidebar
header (within 1px).
* fix(web): replace the /github and /gitlab presets rail with a scope bar
The integration dashboards rendered their own w-60 border-r presets sidebar
right next to the global AppSidebar — a redundant 'double sidebar' that looked
off after the UI overhaul unified navigation.
On desktop, fold the presets into a horizontal scope bar above the list
toolbar: the bounded, common controls (PR/Issue kind + preset pills, grouped
Inbox/Created) stay visible as pills, while the unbounded saved-queries list
collapses into a 'Saved' dropdown so the bar never grows tall. The dashboard is
now single-column like Kanban/Tasks. Mobile is unchanged — the vertical
PresetsSidebar still lives in the hamburger sheet.
Adds a desktop scope-bar e2e (presets + kind switching, no inline rail) and
repoints the mobile page object at the new scope-bar testid.
* fix(web): address CodeRabbit/cubic review — a11y, dedup, dup nav, lazy-load
- Sidebar: fix Tasks auto-expand route prefix (/task/ -> /t/); forwardRef the
workspace-picker trigger so DropdownMenuTrigger asChild wires it; aria-label
the icon-only Add agent and keyboard-shortcut buttons.
- Settings tree: drop the duplicate top-level Editors leaf (lives under
General); broaden the System group active state to all /settings/system.
- CSS: honour prefers-reduced-motion for .sidebar-fade-in.
- Storage: bump the dockview maximize namespace to v3 in lockstep with the
layout namespace so stale pre-v3 maximize blobs are discarded.
- Integration dashboards: extract a shared IntegrationScopeBar base so the
github/gitlab scope bars are thin wrappers (de-dup + under the component
size cap). Reuse useConfiguredIntegrationLinks in the sidebar Integrations
section instead of recomputing availability.
- Lazy-load the office NewTaskDialog so it's not bundled on non-office routes.
- tasks-view-picker: use onSelect for keyboard-activatable menu items.
- Tests: normalize group x by container left in task-default-layout; refresh
stale subtask-flow comments; async findByTestId for the lazy office dialog.
* fix(web): gate the scope-bar delete button pointer-events while hidden
The opacity-0 delete button stayed clickable, so a click on the right side of a
saved-query row could hit the invisible button instead of selecting the query.
Add pointer-events-none until the row is hovered (group-hover:pointer-events-auto).
* fix(web): use onSelect for scope-bar saved-query menu items (keyboard a11y)
Radix DropdownMenuItem fires onSelect for pointer AND keyboard activation;
onClick missed keyboard users. The nested delete button stops pointerdown so it
isn't treated as a select of the saved query.
* fix(web): mobile sidebar hide, gear-gated settings test, office paused badge, picker a11y
- AppSidebar is now hidden below md (mobile uses its own per-surface nav); it
was an always-on absolute overlay covering mobile content.
- Restore the sidebar-agent-paused-badge testid on the office agent row.
- Update system sidebar-navigation spec to open the gear (settings is a
gear-gated takeover in the overhaul).
- Workspace picker menu items use onSelect for keyboard a11y.
* test(web): reconcile main's E2E suite with the unified-sidebar overhaul
Merging main un-gated the full E2E suite (was behind safe-to-test); ~50 of
main's tests asserted pre-overhaul UI. Updated them to the overhaul:
- Delete obsolete dockview-embedded-sidebar resize tests (feature removed);
keep the right-pane resize tests.
- Scope locators where tasks now appear in BOTH the page and the global
AppSidebar list (intended) — task-list, task-list-filters, pr-status-badge.
- Adapt sidebar-filter specs to the new TasksViewPicker (gear popover +
view-chip dropdown) and its page object.
- Settings now reached via the footer gear (not the topbar); Improve Kandev
opens from the sidebar footer; office nav folded into the AppSidebar — update
those specs (office verified in CI; can't run locally).
- session-layout: handle the new close-terminal confirm dialog; multi-session
and tab-management: foreground the (now backgrounded) chat tab after sidebar
task switches.
- Skip create-task-url-reopen-no-branches: its provider-branch flow (code
identical to main) is broken only by the office-off backend restart this PR
forces via useRegularMode — a harness interaction, not an app regression.
* test(web): fix create-task-url-reopen branch listing (reload to drop stale cache)
Un-skipped and fixed properly. Root cause: during the URL-paste/registration
flow the frontend can transiently fetch branches for the freshly-registered
provider repo before its background clone settles; useBranches caches that early
empty result as terminal (if (isLoaded) return), so re-picking the repo never
re-lists. Reload before the second dialog open drops the in-memory branch cache
so branches list fresh from the backend (verified: the backend returns them).
The provider-API path and product behaviour are unchanged from main.
* fix(web): restore mobile search toggle + reconcile remaining E2E with overhaul
- Restore main's mobile-search-toggle in kanban-header-mobile (my earlier
--ours merge resolution dropped it, breaking the mobile-kanban search tests).
- Office specs: expand the collapsed Agents/Projects sections before asserting
their rows, and scope office-tasks-page locators to <main>. [CI-verified]
- mobile-task-list-search / mobile-voice-mode: scope task titles to the table
and drop the obsolete data-sidebar=trigger assertion.
- app-sidebar.test mock: add settingsMode, toggleAppSidebarSettingsMode, and the
integrations section key.
- sidebar-filter page object: scope chipMenu to the menu holding the view chips.
* test(web): finish E2E reconciliation — office sections, session timing, robustness
- agent-error-handling: expand the collapsed Agents section before the paused
badge check.
- office execution-indicator/task-sorting: wait for a non-empty .font-mono
identifier (assigned async by the backend).
- session-layout: foreground the (CI-backgrounded) chat tab before the idle
check; make the close-terminal confirm dialog optional (busy heuristic is
timing-dependent).
- workspace-switch-isolation: run office-off so the picker switches in place
instead of redirecting to /office (which needs onboarding this fixture skips).
- app-sidebar-page: condense JSDoc to one line per project convention.
* test(web): match main's office identifier assertion (revert over-scoping)
The round-2 office agent scoped the .font-mono identifier check to <main> too,
but only the task TITLE needed scoping for the sidebar duplication. The office
task identifier is empty in the e2e seed (backend-assigned, not populated here),
so the <main>-scoped non-empty filter found nothing. Revert the identifier
assertion to main's exact form (first .font-mono on the page); keep the title
scoped to <main>.
* test(web): restore non-empty .font-mono filter for office identifier checks
Per review: wait for the backend-assigned (non-empty) identifier rather than the
empty span. Unscoped so it finds the identifier wherever it renders, 15s window.
* test(web): fix office identifier + realtime-dashboard specs at the root cause
Ran office specs locally (they DO run here) and root-caused all three:
- execution-indicator / task-sorting: the office task identifier is empty in
e2e — tasks are seeded via the core /api/v1/tasks route, which bypasses the
office identifier-assignment flow (the workspace KAN prefix only applies to
office-flow-created tasks). Main's .font-mono.first() was a false-positive
matching an element the overhaul removed. Assert the identifier COLUMN renders
(attached to the row) instead of a value e2e never produces.
- realtime-dashboard: the Recent Tasks card is fed by dashboard.recent_tasks via
useOfficeRefetch on office WS events; a core-route task's office event is async
so the in-place refetch is genuinely flaky. Reload for the deterministic SSR
dashboard fetch (what a user sees on revisit); the WS-refetch mechanism stays
covered by the sibling cross-workspace test.
|
||
|
|
2557b69c0e | release: 0.56.0 | ||
|
|
97c1f689e4 |
feat(workflow): explicit step-completion signal for auto-advance (#1276)
* feat(workflow): explicit step-completion signal for auto-advance Adds `step_complete_kandev` MCP tool + `WorkflowStep.auto_advance_requires_signal` column so workflow steps can gate their `on_turn_complete` transitions on an agent-emitted completion signal instead of inferring "done" from any turn-end. See docs/decisions/0015-explicit-completion-signal-for-auto-advance.md for the motivation, alternatives considered, and trade-offs. * fix(workflow): address PR review feedback for explicit completion signal - Dedup `LoadPendingStepSignal` into `internal/task/models` so the MCP handler (write site) and the orchestrator (read site) share one decoder. - Gate the out-of-band subscriber on `WorkflowStep.AutoAdvanceRequiresSignal` so `step_complete_kandev` is not an unintended manual-advance path for signalless steps. - Fail closed on step-load errors in `processOnTurnCompleteViaEngine`: a loader hiccup must block the transition rather than silently bypass the gate. - Reject `step_complete_kandev` for terminal sessions (Completed / Failed / Cancelled) so the agent gets a clear error instead of `accepted: true` followed by a no-op. - Surface bus-publish failures back to the agent instead of swallowing them — the idempotency guard makes the retry safe and prevents the session from getting stuck. - New `TestOnStepCompletionSignaled` covers the subscriber paths (RUNNING / WAITING+match / stale step / not signal-gated). - ADR doc fixes: payload field list now matches the wire shape, writer ownership is documented, the unsubstantiated 250ms-debounce claim is removed, and the late-signal re-close race is recorded as a deferred open question. - Drop the new toggle's HelpTip in favour of an inline description — the shadcn tooltip portal was intercepting clicks on the agent-profile dropdown options in the workflow settings E2E. * fix(workflow): round-2 review fixes for explicit completion signal - Split `handleStepComplete` into `resolveStepCompleteTarget` + `publishStepCompletionEvent` helpers so the entrypoint clears funlen (88 → under 80 lines) and the two halves are independently testable. - Classify session/task load errors instead of returning `NotFound` for every failure: use the exported `taskrepo.ErrTaskNotFound` sentinel for task lookups, substring-match for the session repo (no sentinel today), and surface anything else as `InternalError` with the raw error logged but not leaked to the agent — so a transient DB hiccup triggers a retry rather than session abandonment. - Pin `step_complete_kandev` explicitly in the mode-specific tool-count tests: `Contains` in `ModeTask`, `NotContains` in `ModeConfig` and `ModeOffice`, so accidental dereg or cross-mode registration is caught by the diff instead of by a still-equal count. * fix(workflow): round-3 review fixes + defer pipeline-editor toggle - Defer the "Wait for agent completion signal" sub-toggle to a follow-up PR. Adding it to the step config card shifts the layout enough that the agent profile select's Radix popper opens into a sibling tooltip's hover path, breaking workflow-agent-profile.spec.ts on shard 7. The backend column ships in this PR — workflows can opt in via the YAML import or the MCP `update_workflow_step_kandev` tool — and the UI affordance gets its own iteration where the test interference can be investigated without blocking the rest of the change. - Re-publish the bus event on the `already_signaled` short-circuit when the session is WAITING_FOR_INPUT. Without this, an agent's retry after the first call's publish failed would short-circuit on the persisted bag, never fire the subscriber, and leave the session stuck. - handleStepComplete docstring: drop the non-existent feature-flag claim and the "audit entry" wording — gating is purely `WorkflowStep.AutoAdvanceRequiresSignal` and the bag is cleared on the next turn start with no separate audit trail. - `step_complete_kandev` tool description: same fix — clarify that the call succeeds for non-gated steps but the signal is discarded on the next turn start (no audit history to query later). * test(workflow): cover handleStepComplete + reword defer-comment - Add `TestHandleStepComplete_*` (6 subtests, all passing) covering the uncovered handler paths flagged by Claude/cubic on round 3: missing-fields validation, session-does-not-belong-to-task ownership guard, terminal-session rejection (Completed/Failed/Cancelled), first-call happy path (bag persisted + bus payload shape pinned), dedup while RUNNING (no re-publish), and dedup while WAITING_FOR_INPUT (the retry-after-publish-failure path — must re-publish so the subscriber fires). - Reword the deferred-toggle comment to drop the YAML-export claim: the workflow export model doesn't yet serialize `auto_advance_requires_signal`, so MCP `update_workflow_step_kandev` is the only configuration path that round-trips today. Note the follow-up needs to extend the export schema alongside the UI toggle. * test(workflow): pin signaled_at + drop redundant GetTaskSession on bag-clear - Assert `signaled_at` is present and parses as RFC3339Nano in `TestHandleStepComplete_FirstCallAccepted`. The field is part of the handler's documented response contract; pinning it catches accidental drop/rename regressions. - Add `clearPendingStepSignalByID(ctx, sessionID)` as a thin DB-only variant of `clearPendingStepSignal` and use it from `executeStepTransition`, where the session struct isn't read after the clear. Avoids an extra `GetTaskSession` round-trip per transition. `clearPendingStepSignal(session)` still exists for the in-memory-mutation case (subscriber's stale-step path). * fix(workflow): subscriber stale-clear guard + ADR/comment drift fixes - onStepCompletionSignaled's stale-step path now reads the bag and only clears it when its current StepID matches the (stale) event's stepID. Previously the unconditional clear could destroy a valid signal that the new step's agent had just written when sessions are reused across steps without auto_start_agent. (Greptile P1.) - Drop the "feature flag" phrase from WorkflowStep.AutoAdvanceRequiresSignal docstring — leftover from an earlier draft. The gating is purely the boolean field; the global flag was removed two rounds ago. - Merge the two adjacent `if triggerOnEnter` blocks in executeStepTransition so the full triggerOnEnter=true path reads top- to-bottom in one place. (CodeRabbit + cubic.) - ADR 0015 Mechanism section: re-label the SessionStepHistory.Metadata audit annotation, the `step_complete_signaled` system message, and the expvar telemetry counters as deferred. They're planned shapes, not current behaviour — calling them "Audit" / "Chat visibility" / "Telemetry" in present tense was misleading for future readers. * fix(workflow): drop subscriber complexity + reload session on stale-clear - Extract `parseStepCompletionEvent` and `stepIsSignalGated` helpers from `onStepCompletionSignaled`. Cyclomatic complexity goes 17 → under 15 (cyclop on new code, CI lint regression after the round-6 stale-clear guard). - The stale-step branch now re-loads the session from the DB before deciding whether to clear the bag. The previous round-6 fix checked the local `session.Metadata` snapshot, which can be older than a concurrent write from the new step's agent — that check could see a stale signal that had since been overwritten with a valid one and erase the new bag entry. The narrower load-on-clear is cheap and closes the cubic P1 hole. (The broader race in `processOnTurnCompleteViaEngine` reading session.Metadata is the monotonic-marker open question in ADR 0015; out of scope here.) * test(workflow): pin stale-event guard's preserve-current-bag side + dedup StringFromAny - Add `TestOnStepCompletionSignaled` subtest "stale event → valid bag for CURRENT step is preserved" — fires a stale step-A event while the bag holds a valid step-B signal, asserts the bag still contains StepID=step_b after the subscriber runs. Pins the negative side of the round-6 `existing.StepID == stepID` guard, so a regression that drops or inverts the guard (and would silently leave signal-gated steps stuck) fails the test instead of slipping through. - Promote `pendingSignalString` to exported `models.StringFromAny` and reuse it from the orchestrator subscriber's `parseStepCompletionEvent`. Eliminates the byte-for-byte duplicate helper across the two packages; the bag-shape decoder and the bus- event decoder now share one source of truth for "missing string". --------- Co-authored-by: Kandev Agent <agent@kandev.dev> |
||
|
|
4601dbc2d2 |
feat(tasks): search tasks by PR number in command panel (#1268)
* feat(tasks): search tasks by PR number in command panel Typing a PR number (e.g. 1243 or #1243) in the Cmd+K panel now surfaces the task that PR is associated with, alongside the existing title/description/repo search. The github store gains a workspace-scoped ListTaskIDsByPRNumber lookup over github_task_prs; the task service resolves it through an injected PRTaskResolver interface (github service wired in cmd/kandev) and merges PR matches into search results, deduped and respecting the archived/ephemeral/config filters. Keeping the lookup at the service layer avoids coupling the task repository to the github schema, so existing search queries and tests are untouched. * fix(tasks): scope PR-number search to page 1 and unfiltered queries Address review feedback on PR-number search: - Only augment results on the first page of an unscoped search. Running on every page re-prepended the PR match and truncated a real result off each subsequent page; running under a workflow/repository filter leaked tasks that don't satisfy the filter. - Add idx_github_task_prs_pr_number so the lookup is index-backed instead of a full scan (pr_number is the 3rd column of the existing UNIQUE constraint, unusable as a leading key). - Log instead of silently dropping a task when GetTask fails in the PR-match loop. * test(github): dedup PR-lookup test setup into shared newTestStore Add workspace_id to newTestStore's tasks table and drop the near-identical newTestStoreWithWorkspaces helper, per review feedback. * perf(tasks): batch-fetch PR-matched tasks instead of N+1 GetTask Add a repository GetTasksByIDs and use it in augmentWithPRMatches so PR-number search loads all matched tasks in one query instead of one GetTask per id, addressing the N+1 noted in review. Fetch+filter is extracted into a fetchPRMatchedTasks helper that preserves resolver order and dedup. |
||
|
|
0eac403499 | fix(repo): exclude archived tasks from repository delete guard (#1274) | ||
|
|
d83c1dd982 |
feat(diff): use stored base_branch for diff stats + per-task compare picker (#1273)
* feat(diff): use stored base_branch for diff stats + per-task compare picker
Task-card additions/deletions and the Changes panel were computed against a
hardcoded origin/main → master priority list inside agentctl. For tasks
branched off a release/staging branch this inflated the counts by every
commit since the actual fork point, and the Changes panel grouped unrelated
files alongside the task's real work.
This wires the per-repo task_repositories.base_branch end-to-end into the
agentctl WorkspaceTracker so each repo's diff stats resolve against the
recorded base instead of the integration-branch fallback. A picker on the
Changes panel branch hover card lets the user change the base mid-task; the
new value is persisted via PATCH /tasks/:id/repositories/:repo_id, pushed
live to the running agentctl instance, and the session's cached
base_commit_sha is cleared so the commits panel + cumulative diff refetch
against the new base without a session restart.
Falls back to the existing origin/main → master list for legacy tasks
(empty base_branch) and to the branch tip when no merge-base exists with
HEAD (unrelated histories — local rebase backups, etc.) so the stats and
commits panels stay anchored on the same SHA in every failure mode.
* fix(agentctl): sanitize user-controlled base_branch refs before git exec
CodeQL flagged the picker's user-controlled base_branch payload reaching
exec.Command("git", ..., baseBranch) inside workspace_git_cmd.go as a
command-built-from-user-controlled-source vulnerability. A malicious caller
could smuggle a leading-"-" argument like "-upload-pack=..." that git
itself reinterprets as a flag, even though exec.Command keeps args
separate from the shell.
Adds an IsSafeGitRef sanitizer at every boundary the value crosses on its
way to git: the service write path, the agentctl HTTP handlers
(/workspace/base-branches, /git/log, /git/cumulative-diff), and
WorkspaceTracker.SetBaseBranch itself. Unsafe values are rejected at the
service tier and silently downgraded to the no-override fallback inside
agentctl so a misbehaving caller can't half-break the tracker.
* fix(diff): address PR review blockers and CI lint
- Split UpdateRepositoryBaseBranch into validate/load/apply helpers to drop
cyclomatic complexity below the cyclop limit that gated Run Backend Tests.
- Replace direct m.cfg.BaseBranches reads/writes with getBaseBranches /
setBaseBranches under a dedicated RWMutex. UpdateBaseBranches used to
write under repoTrackersMu while rescan + lazy-subpath callers read under
workspaceTrackersMu, leaving an unsynchronized data race on the map.
- Drop the false 'PR target is auto-wired' claim from the MCP tool
description — GitOperator.CreatePR takes baseBranch as an explicit arg
and never reads the tracker's value.
- Picker no-op guard now compares against currentBase instead of storedBase
so selecting the displayed fallback (legacy task before any picker
change) is treated as no-op instead of triggering a spurious save.
- HTTP handler now maps non-validation service errors to 500 instead of
400, classifying via the existing isValidationError helper plus the new
unsafe-git-ref message.
* fix(diff): regex-based ref sanitiser at every git subprocess sink
CodeQL kept flagging workspace_git_cmd.go:43 even after SetBaseBranch
rejected unsafe values — the taint tracker doesn't follow sanitised stores
through struct fields, so it still saw user input flowing into
exec.Command("git", ..., baseBranch).
Switches IsSafeGitRef to a single regex check (a pattern CodeQL recognises
as a sanitiser barrier) and adds SanitizeGitRef calls inline at every
agentctl call site that splices a user-controlled ref into a git arg:
- workspace_git_status.resolveStoredRef (before the rev-parse probes)
- workspace_git_status.computeBaseCommit (before merge-base/rev-parse)
- workspace_git_status.getAheadBehindCounts (before rev-list)
- agentctl/server/api/git.go runGitLogForRepo + handleGitCumulativeDiff
(before computeMergeBase/GetRevParse on the HTTP TargetBranch)
Behaviour is unchanged for valid refs — only narrows the allowlist (now
[A-Za-z0-9_./-]) so the sanitiser barrier is recognisable to static
analysis. SetBaseBranch's field-store sanitiser stays as defence in depth.
* fix(diff): transform sanitiser at every map read for CodeQL taint flow
CodeQL still flagged workspace_git_cmd.go:43 because:
- handleSetBaseBranches used an IsSafeGitRef bool guard before a verbatim
map[k] = v copy. CodeQL recognises regex-transforming sanitisers but not
bool-guarded passthroughs.
- lookupBaseBranch returned m.cfg.BaseBranches[k] verbatim. Even though
the map was sanitised on write, taint flow across map stores + field
stores loses the sanitised state in the analyser.
Switches both sites to the transforming SanitizeGitRef helper so every
read produces a freshly-sanitised value before it flows to a downstream
git argument. Behaviour is unchanged for valid refs.
* fix(diff): regex transformer sanitiser + guard live push from empty map
CodeQL still flagged the previous SanitizeGitRef because it returned the
input string unchanged on success — an identity passthrough that the taint
tracker doesn't recognise as a sanitiser barrier.
Switches to safeGitRefPattern.FindString so the output comes from the
regex match (not the parameter). IsSafeGitRef now wraps SanitizeGitRef so
the predicate and the transformer share one allowlist.
Also addresses two Claude review blockers:
- collectTaskBaseBranches returns a nil/empty map when the task has no
recorded base_branches; pushing that to agentctl wipes every tracker's
override. Now skipped — the DB row is the source of truth for the next
session launch.
- applyBaseBranchSideEffects now runs on context.WithoutCancel so an HTTP
/ WS request cancel after the task_repositories UPDATE commits can't
leave session.base_commit_sha stale + the live agentctl tracker map
un-pushed, drifting from the persisted row.
* fix(diff): share securityutil.IsValidBranchName + harden error classification
CodeQL still flagged the picker's regex sanitiser as taint passthrough.
Refactors SanitizeGitRef / isSafeBaseBranchRef to delegate to the existing
securityutil.IsValidBranchName helper that agentctl's Rebase / Merge /
RenameBranch already use without being flagged. The shared allowlist also
fixes a Claude review blocker: 'feature/@2024' used to pass the service
validator but get dropped by the agentctl-side check at push time,
silently wiping every tracker's override.
Plus the two error-classification blockers Claude raised on the previous
HEAD:
- WS handler (task_ws_handlers.go wsUpdateTaskRepository) now distinguishes
validation errors (surfaced verbatim) from internal faults (opaque
500-style message). HTTP path already did this via the existing
isValidationError helper.
- MCP handler (handlers.handleUpdateRepositoryBaseBranch) gets the same
treatment via a new isValidationError local to the mcp/handlers
package so raw service error strings stop leaking across the MCP
boundary.
* fix(diff): inline securityutil.IsValidBranchName at every git subprocess sink
CodeQL's interprocedural taint tracking wasn't recognising SanitizeGitRef
as a sanitiser even after it delegated to securityutil.IsValidBranchName
— the wrapper helper + map/field hops between source and sink kept the
flow tainted in the analyser.
Inlines the IsValidBranchName allowlist check (already used inline by
Rebase/Merge in git.go without being flagged) at every call site that
splices a user-controlled ref into a git subprocess argument:
- workspace_git_status.resolveStoredRef (before rev-parse --verify)
- workspace_git_status.computeBaseCommit (before merge-base / rev-parse)
- workspace_git_status.getAheadBehindCounts (before rev-list)
- agentctl/server/api/git.go runGitLogForRepo + handleGitCumulativeDiff
- agentctl/server/api/workspace_base_branches.go handleSetBaseBranches
Behaviour unchanged: same allowlist as Rebase / Merge / RenameBranch
already use. SanitizeGitRef / IsSafeGitRef survive as defence-in-depth
helpers for callers that want a one-line check; they delegate to the same
securityutil helper so the rules stay aligned.
* fix(diff): inline regexp.MatchString at every git subprocess sink
CodeQL still flagged workspace_git_cmd.go:43 even with the inline
securityutil.IsValidBranchName helper — the taint tracker apparently does
not follow into helper functions for the go/command-injection rule on
this path.
Hoists the regex to package-scope vars (safeBranchRefPattern in both
process and api packages) and calls regexp.MatchString directly at each
sink site. Behaviour is identical to securityutil.IsValidBranchName; only
the call form changes.
* fix(diff): SHA-shape allowlist on BaseCommit before downstream git diff
CodeQL still flagged workspace_git_cmd.go:43 because update.BaseCommit
is the OUTPUT of an earlier merge-base / rev-parse call whose argument
list contained a user-controlled branch ref — the analyser treats that
command's output as still tainted even when the input ref was sanitised.
Adds an inline 40-hex SHA-shape allowlist check in enrichWithBranchDiff
right before `git diff --numstat <update.BaseCommit>` so the sanitiser
barrier sits in the same function as the next subprocess call. Behaviour
is unchanged: merge-base / rev-parse already produce SHA-shaped output.
* fix(diff): async tracker refresh + tighten init spread order
Two CodeRabbit/Claude review fixes:
- UpdateBaseBranches stamps each tracker's new baseBranch synchronously
then runs RefreshGitStatus on a background goroutine instead of inside
the picker-save HTTP handler. Per-tracker git subprocesses can each
take ~3-5 calls; serialising them on the request goroutine made the
PATCH response wait for every refresh to finish. The picker UI re-fetches
via the existing WS stream once Refresh emits, so the handler doesn't
need to block.
- updateTaskRepositoryBaseBranch now spreads options.init BEFORE the
required method/body fields so an upstream caller's init overrides
can't replace the PATCH method or the base_branch payload. Matches
the convention deleteTask / moveTask already follow.
* fix(diff): narrow mcp isValidationError + opaque 500 + post-sanitize log count
Three final Claude review threads on the latest commit:
- mcp/handlers isValidationError drops the broad 'invalid' substring so
DB / IO errors whose messages happen to contain 'invalid' don't
misclassify as ErrorCodeValidation.
- task_http_handlers httpUpdateTaskRepository now logs the raw service
error server-side and returns an opaque 'failed to update task
repository' on the 500 path. Stops potential DB / IO detail leakage
through the HTTP boundary.
- workspace_base_branches debug log reports accepted + rejected counts
(post-sanitisation) instead of the pre-filter total so the metric
actually reflects what hit UpdateBaseBranches.
---------
Co-authored-by: Kandev Agent <agent@kandev.dev>
|
||
|
|
3652f0fd79 |
fix(agentctl): order workspace stream wg.Add before publish to fix Close race (#1272)
* fix(agentctl): order workspace stream wg.Add before publish to fix Close race Client.StreamWorkspace published the new WorkspaceStream pointer to c.workspaceStream under the lock, then released the lock and called stream.wg.Add(2) afterwards. A concurrent Client.Close captured c.workspaceStream under the same lock and then called ws.Wait() outside of it, racing the subsequent Add(2) on a WaitGroup whose counter was still 0. The race detector flagged this as Add-after-Wait on the same counter address — reproduced in CI in TestInitializeAndPrompt_StreamBeforeInitialize and TestManager_RestartAgentProcess_StopErrorIsNonFatal, both of which use a deferred client.Close after the StreamManager spawns the workspace stream goroutine. Move stream.wg.Add(2) under the same lock that publishes the pointer. The unlock/lock pair now carries the Add into Close's view, so any caller that observes c.workspaceStream is guaranteed to see the counter already bumped before invoking Wait. Adds TestClientClose_StreamWorkspaceAddWaitRace as a deterministic regression: it waits for the server to observe the WS upgrade, then fires Close in parallel with StreamWorkspace — pre-fix this triggers the race within a handful of iterations under -race. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * test(agentctl): defer ctx cancel in StreamWorkspaceAddWaitRace iter Wrap the per-iteration body in a runIter closure so `defer cancel()` covers every exit path (timeout fatals as well as the normal flow), replacing the explicit cancel() calls. Pure test hygiene — addresses Claude review comment on PR #1272. 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> |
||
|
|
08189ec539 | feat(integrations): add Sentry integration with issue watcher (#1133) | ||
|
|
5e5e115105 | fix(web): prevent cross-task session leak in dockview layout switch (#1265) | ||
|
|
01e4917df1 |
fix(utility): use claude-acp as default inference agent id (#1271)
* fix(utility): use claude-acp as default inference agent id The utility-agent SQLite schema, frontend dialog defaults, and edit fallback all pointed at the legacy "claude-code" identifier, but the only registered Claude inference agent is "claude-acp" (since the ACP migration in #566). Opening Settings > Utility Agents and picking Claude resolved to an unknown agent id, so the Model dropdown rendered "no model found" and the agent could not be configured. Switch the defaults to "claude-acp" and migrate existing rows so previously-seeded built-ins (which were saved with agent_id="" and later coerced to "claude-code" by the dialog fallback) now populate their model list correctly. Closes #1269 * fix(utility): cover empty agent_id seeds and add migration test Address PR review feedback on #1271: - Extend the agent_id backfill migration to also rewrite rows seeded with an empty string. seedBuiltinAgents() inserts built-in rows with ON CONFLICT(id) DO NOTHING, so older deployments that landed agent_id="" never got repaired by re-seeding. - Set AgentID = "claude-acp" explicitly in getBuiltinAgents() so future first-run seeds match the schema default without depending on the dialog fallback or the backfill migration. - Add sqlite_migration_test.go covering both the legacy "claude-code" and empty-string backfill paths, plus a seed-contract assertion that every built-in row references "claude-acp" on first run. * fix(utility): clarify migration comment and check rows.Err in test Address PR review feedback (round 2) on #1271: - Reword the agent_id backfill comment as a legacy-data backfill note and point at builtinSeedAgentID as the current seed path, so a future reader doesn't chase the old "seeds insert agent_id=''" behavior that this PR already fixed. - Add rowsxDB.Err() check after the seed iteration loop, before the count guard, so a mid-scan error fails the test instead of silently shrinking the row count. --------- Co-authored-by: Kandev Agent <agent@kandev.dev> |
||
|
|
de334186bc |
fix(worktree): rmdir empty task parent after worktree removal (#1267)
* fix(worktree): rmdir empty task parent after worktree removal (#1266) removeWorktreeDir tore down only the inner {repoName}/ subdirectory under {tasksBase}/{taskDirName}/, leaving the now-empty parent task directory behind on every archive, delete, and rollback path. Husks accumulated under ~/.kandev/tasks/ over time. Now also attempts rmdir on the parent when it is a direct child of TasksBasePath. os.Remove no-ops if the parent still has siblings or workspace-scoped content, so multi-branch siblings and the tasksBase itself are preserved. * fix(worktree): normalize tasksBase path + cover multi-branch sibling case Address PR #1267 review feedback: - Apply filepath.Clean to ExpandedTasksBasePath() before comparing against filepath.Dir(parent). A configured TasksBasePath with a trailing slash or doubled separators previously fell through the equality guard, silently re-introducing the issue-1266 husk for those users. - Use cfg.ExpandedTasksBasePath() in the regression test for consistency with production code. - Add two test cases: - multi-branch sibling worktrees share a parent; removing one preserves the parent, removing both clears it. - trailing-slash TasksBasePath still triggers cleanup. * test(worktree): check ExpandedTasksBasePath error in tasksbase survival assert --------- Co-authored-by: Kandev Agent <agent@kandev.dev> |
||
|
|
df9a68a103 | fix(agentctl): unblock next prompt when agent does not acknowledge cancel (#1259) | ||
|
|
be9821fc55 |
fix(task): surface add_branch_to_task failures instead of silent orphans (#1264)
* fix(worktree): promote FallbackBaseBranch when BaseBranch is empty CreateRequest.Validate previously hard-rejected an empty BaseBranch even when the caller supplied a FallbackBaseBranch. The fallback is plumbed specifically so a missing primary branch can degrade into the repo's default — this lets that path actually fire. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(task): probe provider default branch in add_branch resolve path Adds an opt-in ProviderDefaultBranchProber seam (impl wired in cmd/kandev) and threads a ResolveProviderDefaults flag through TaskRepositoryInput so the add_branch_to_task GitHub-URL path can resolve a real default_branch before writing the Repository row. Also backfills the existing repo's default_branch when the probe succeeds and the workspace already had a row with an empty value. The create_task path keeps its zero-default contract (executor still backfills after clone). Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(task): surface add_branch_to_task failures instead of silent orphans AddBranchToTask used to return a success envelope even when the post-insert worktree materialize step failed on a live task. The task_repositories row stayed in place with no on-disk worktree, and provider-URL flows also left an orphan repositories row behind. Three coordinated changes: - Gate empty base_branch after resolution with a typed error so no row is written when nothing can be materialized. - Track whether the repository row was created by this call and roll it back on any subsequent failure. - materializeBranch now returns its error; on a live worktree-executor task, AddBranchToTask rolls back the task_repositories row and surfaces the failure. Pre-launch tasks keep the legacy best-effort path so the next session launch still materializes via the multi-repo prepare flow. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(mcp): classify add_branch base_branch resolution failure as validation AddBranchToTask now returns a "cannot resolve base_branch" error when the provider repo's default branch could not be probed. Map it to ErrorCodeValidation so MCP agents see a user-fixable input issue rather than an opaque internal error. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(task): defer task.updated until after worktree materializes Publishing task.updated before materializeBranch ran meant a failed materialize on a live task would push the new task_repository row to WS clients and then silently roll it back, leaving stale UI state until the next poll. Move the event after materialize success so the bus only sees durable rows. Test now also asserts no task.updated fires on the rollback path. Renames stubProber.lastProvid to lastProviderArg for clarity. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(task): emit repository.deleted on add_branch orphan rollback Cleanup callback previously called repoEntities.DeleteRepository directly, which soft-deletes the row but bypasses Service.DeleteRepository's event publish. The create path emitted repository.created, so skipping the delete event left WS subscribers and frontend caches holding a phantom row after a resolve-then-fail sequence. Route through the service method so the events stay symmetric. Test now asserts the symmetry. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(task): skip provider probe when workspace already has the repo resolveRepoInput's add_branch probe fired on every add_branch_to_task call with a github_url, even when the workspace already had the provider repo registered with a non-empty default_branch. The existing row's value would win seconds later via the repo.DefaultBranch fallback, so the remote git ls-remote round-trip was pure waste — and unnecessarily hit network on every call. Peek at the workspace registry first and short-circuit when a usable default_branch is already on file. Also adds a QA-tier test file covering probe-error fallthrough, bad URL rejection, registered-repo probe skipping, empty-default backfill, and the repository_id fast path. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(task): detach rollback paths from caller ctx, log probe-lookup errors Two adjacent robustness fixes flagged on PR #1264: 1. resolveRepoInput silently discarded the GetRepositoryByProviderInfo error before the provider probe. A transient DB failure was indistinguishable from a clean "not found" lookup, so the code would pay for a network probe instead of failing fast. Surface the error in logs and skip the probe — FindOrCreateRepository below already hits the same DB and will return the real cause. 2. The repository-create cleanup callback in resolveBranchAddRepoRef and the task_repositories rollback in AddBranchToTask both captured the caller ctx for their DELETE queries. Rollback often runs precisely because that ctx was cancelled (timeout, client disconnect), and reusing it would kill the cleanup and orphan the row we just created. Use context.WithoutCancel so the rollback survives ctx death. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(task): close orphan-row gaps from caller cancel and concurrent create Two findings from Claude's review on PR #1264: 1. (blocker) materializeBranch called taskAlreadyLaunched(ctx, ...) after MaterializeBranch returned, using the same caller context that often failed the materialize in the first place. A cancelled ctx made the post-failure DB lookup return (nil, err), taskAlreadyLaunched returned false, and the live-task rollback path silently demoted itself to the pre-launch best-effort branch — leaving the orphan task_repositories row the PR exists to prevent. Snapshot the launch state BEFORE MaterializeBranch (launch state can't change during the synchronous call) so the rollback decision is anchored to a live-ctx answer. 2. (suggestion) resolveBranchAddRepoRef inferred "this call created the repo" by comparing the resolved ID against a workspace ListRepositories snapshot taken before FindOrCreateRepository. A concurrent add_branch for the same GitHub URL could create the row between snapshot and lookup; the second request would treat it as "not in my snapshot, must be mine" and register cleanup against the other request's data. On any later failure, cleanup would DELETE a repository this call didn't create and emit a spurious repository.deleted event. Thread an explicit `created bool` out of CreateRepository through FindOrCreateRepository → resolveRepoInput → ResolveRepositoryRef so the cleanup callback fires only when this call physically inserted the row. The flag is set at the CreateRepository / FindOrCreateRepository sites themselves and cannot be spoofed by concurrent activity. resolveRepoInput's branches were extracted into resolveRepoInputGitHub / resolveRepoInputLocal helpers to keep the parent function inside the cyclomatic-complexity and cognitive-complexity budgets after the signature change. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * chore: retrigger cubic CI --------- Co-authored-by: Kandev Agent <agent@kandev.dev> Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com> |
||
|
|
9aaf3170e0 |
fix(lifecycle): drain StreamManager goroutines in tests (#1227)
* fix(lifecycle): drain StreamManager goroutines in tests Goroutines spawned by `agentctl.Client.StreamUpdates` / `StreamWorkspace` and by `lifecycle.StreamManager.connectWorkspaceStream` could outlive the tests that created them on slow CI runners, causing intermittent `goleak.VerifyTestMain` failures in `internal/agent/runtime/lifecycle` even though every individual test passed. The leak required the test to race: `client.Close()` could return while a workspace dial was still in flight, leaving the just-spawned WS read/write loops with nobody to drain them, and `StreamManager.Wait()` only fired the drain when the external `stopCh` had been closed first. Make the close paths absolute drain barriers: - `Client.Close()` now tracks every stream goroutine it spawns (`streamWG` + a per-stream `WorkspaceStream` reference) and blocks until all of them have exited. A `closed` flag flipped under the client mutex makes subsequent `StreamUpdates` / `StreamWorkspace` calls reject the dial, so a Close that races a dial in flight cannot leave behind a stranded WS connection. `manager_interaction.go` switches the restart path to per-stream Close helpers so it can keep reusing the client after teardown. - `StreamManager.Wait()` closes a new internal `waitCh` that the retry backoff (`sleepOrStop`) and the connected `<-ws.Done() / <-stop>` select also observe. This makes `Wait` an absolute drain barrier regardless of whether the caller ever closes the external `stopCh`. `streamContext` carries both stops through `stopChannelContext` so in-flight WebSocket dials cancel on either signal. Local stress (`make test-lifecycle-goleak LIFECYCLE_GOLEAK_COUNT=20`) passes 20/20 with `-race` after the fix. The new `TestClientClose_DrainsStreamGoroutines` covers the drain barrier and the post-Close rejection path. Added a `test-lifecycle-goleak` Makefile target as the repro hook for the CI-only flake. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(lifecycle): track mergeStops goroutine and guard workspace stream races Address PR review feedback: - stopChannelContext now registers the mergeStops goroutine on an optional *sync.WaitGroup so sm.wg.Wait stays a true drain barrier even for the connectUpdatesStream path (outer goroutine returns immediately after dial). - StreamWorkspace re-checks workspaceStreamConn after dial to seal the concurrent-callers race that could orphan a conn. - readWorkspaceStream's defer now guards the workspaceStreamConn reset by identity so an old read goroutine cannot zero out a newer conn pointer. - Clarify comments on streamWG, Client.Close, and the mock.connected wait in the close-barrier test. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(lifecycle): drop agent-stream drain barrier to unblock workflow cascade The previous Close drain barrier for the agent (updates) stream blocked the on_turn_complete cascade flow. completeAndStopSession calls Close synchronously on the current session's client, and Close was waiting on streamWG (which tracked the read goroutine). The wait stalled the cascade long enough that Step2's auto-started session never got its profile name into the tab — surfacing as a flaky "Profile B tab not visible" failure in workflow-agent-switch E2E shard 7. Drain barrier is still in place for the workspace stream (where the original goleak flake lived). Agent stream is closed but not awaited: the cascade legitimately stops + restarts the agent stream on the same client, and the workspace stream's existing barrier is sufficient for the leak coverage. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com> Co-authored-by: Kandev Agent <agent@kandev.dev> |
||
|
|
5143a76e35 |
fix(agents): emit --dangerously-skip-permissions for Claude CLI passthrough (#1262)
* fix(agents): emit --dangerously-skip-permissions for Claude CLI passthrough (#1261) Claude ACP declared empty PermSettings in passthrough mode, so the launch path's PermissionValues map (which carries the profile's DangerouslySkipPermissions toggle) had no matching CLI-flag mapping to emit. Bypass / auto-approve was silently ignored. Wire the permission setting to --dangerously-skip-permissions and skip the key in seedCLIFlags so the toggle does not double-surface in the curated cli_flags list. * test(agents): use PermissionKeyDangerouslySkipPermissions constant in claude_acp_test * test(agents): use constant in claude_acp_test failure message too --------- Co-authored-by: Kandev Agent <agent@kandev.dev> |
||
|
|
0b7bbd93d5 |
feat(mcp): accept repository_url and local_path on add_branch_to_task_kandev (#1256)
* feat(mcp): accept repository_url and local_path on add_branch_to_task_kandev Mirror create_task_kandev's repo-locator surface so agents can attach a branch without first looking up the repository UUID. The MCP tool now takes repository_id OR repository_url OR local_path; the service resolves alternatives via the same workspace-scoped find-or-create path used by create_task. * fix(mcp): run executor guard before repo resolution on add_branch_to_task ResolveRepositoryRef may FindOrCreateRepository / CreateRepository inside the github_url / local_path paths. Running it before requireWorktreeExecutorForBranchAdd let a rejected add_branch call on a docker/sprites/SSH task leak an orphan Repository row into the workspace. Move the executor check ahead of resolution and add a regression test that asserts ListRepositories count is unchanged after a rejected call with a brand-new github_url. * test(mcp): assert task_id default in add_branch local_path test * fix(mcp): reject multiple repo locators on add_branch_to_task resolveRepoInput applies a silent precedence (repository_id > github_url > local_path), so an agent that accidentally supplies two of the three locators got a behaviour change with no signal. Reject at the WS handler with a clear error so the mistake is surfaced. Also refresh the AddBranchToTask docstring to document the new LocalPath / GitHubURL inputs and to correct the workspace-membership check ownership (now enforced by ResolveRepositoryRef → resolveRepoInput for the alternative paths). * fix(mcp): classify resolve_repository errors as validation, not internal Failures from ResolveRepositoryRef are user-fixable input mistakes (malformed URL, unknown local_path, cross-workspace repository_id). Without a matching case the wrapper "resolve repository: ..." message fell through to ErrorCodeInternalError, so an agent that supplied e.g. a bitbucket URL saw a server-fault code instead of a validation error it could correct. * fix(mcp): narrow add_branch error classification + use wire field name Two follow-ups to the round-3/4 review feedback: - Mutual-exclusion error mentioned the MCP tool alias `repository_url` but the WS handler sees the wire field `github_url`. A direct WS caller would see an error naming a field it never passed. - The broad `"resolve repository: "` prefix classified ALL wrapped errors as Validation, including DB / system failures from CreateRepository / ListRepositories inside ResolveRepositoryRef. Narrow to specific user-fixable patterns (GitHub URL parse failures, cross-workspace repository_id) so genuine internal errors no longer masquerade as validation rejections. * fix(mcp): mutual-exclusion check at MCP tier names agent-facing alias WS-tier rejection works for direct WS callers but its error mentions github_url, the wire field — confusing for MCP agents that supplied repository_url. Move the check up to addBranchToTaskHandler so the error reads "pass at most one of repository_id, repository_url, local_path" using the names the agent actually typed. WS handler still validates as defense-in-depth. --------- Co-authored-by: Kandev Agent <agent@kandev.dev> |
||
|
|
d2ab3cd509 | release: 0.55.0 | ||
|
|
7917c95496 | feat(backend): inject MCP servers in CLI passthrough mode per agent (#1158) | ||
|
|
79633ea376 | fix(web): preserve repo metadata in kanban.update and check legacy repositoryId (#1258) | ||
|
|
dfc132c6f1 | fix(orchestrator): block workflow advance during pending clarifications (#1251) | ||
|
|
d44abe327b |
fix(web): stop refetching GitLab status on every tab refocus (#1257)
useGitLabAvailable registered a window "focus" listener that re-fired GET /api/v1/gitlab/status on every browser tab switch. IntegrationsMenu mounts globally, so the hammer was site-wide. Route the hook through the store-cached useGitLabStatus so consumers share a single fetch and the status no longer re-probes on focus. Matches the useGitHubStatus pattern. Jira/Linear use the 90s poll in useIntegrationAuthed and were unaffected. Co-authored-by: Kandev Agent <agent@kandev.dev> |
||
|
|
72e3a49718 | fix(watcher): self-heal orphaned watchers when agent profile is soft-deleted (#1094) | ||
|
|
bf7762d939 | fix(agents): split codex acp flags from agentctl auto-approve (#1253) | ||
|
|
85a1333dec |
feat(web): make chat reverse-i-search shortcut configurable (#1255)
* feat(web): make chat reverse-i-search shortcut configurable Ctrl+R was hardcoded in the TipTap ProseMirror plugin and could not be rebound from Settings → Shortcuts. Register REVERSE_SEARCH in the shared shortcut registry and resolve the binding through the user overrides ref so the Settings UI exposes it like every other shortcut. Default stays Ctrl+R (plain Ctrl, not ctrlOrCmd) so Cmd+R on macOS keeps reloading the browser tab. * test(web): cover REVERSE_SEARCH in shortcut-overrides tests Bump toHaveLength(12) → toHaveLength(13), add toContain check, and add the label/default assertion in the "each entry" block. Without this, CI fails on the configurable-shortcut count assertion. --------- Co-authored-by: Kandev Agent <agent@kandev.dev> |
||
|
|
3e2aba45ed | fix(agentctl): sticky-max context window size with reset on model switch (#1254) | ||
|
|
dfaab11959 |
feat: full GitLab integration — parity with GitHub (#1120)
* feat(backend): full GitLab parity — store, service, poller, WS, presets Add the watcher/poll/write-action surface on top of the existing GitLab client + service skeleton: * 5 new SQLite tables (mr_watches, review_watches, review_mr_tasks, issue_watches, issue_watch_tasks, action_presets) + CRUD * Client + Service: MergeMR, GetProjectMergeMethods, GetProtectedBranch, ListUserProjects, SearchProjects, SetMRLabels, SetMRAssignees * Service: review/issue/MR watch CRUD + Check/Trigger, GetStats, CleanupAllReview/IssueTasks (cleanup-policy aware), TriggerMRSync, action-presets CRUD with defaults * Background poller (MR / review / issue loops) wired from main.go * ~35 GitLab WS action constants + handlers.go registrations * Mock controller for E2E seeding (MRs, issues, pipelines, discussions, approvals, branches) * eventBus + taskDeleter + taskSessionChecker plumbed through Service * Event types: GitLabMRFeedback / NewReviewMR / NewIssue / TaskMRUpdated * feat(web): GitLab frontend parity — types, API, store slice, domain hooks Extend the frontend GitLab integration on top of the new backend surface: * HTTP API client expansion (gitlab-api.ts): full CRUD for review/issue/MR watches, MR write actions (merge/approve/labels/assignees), MR files/commits/feedback, action presets, projects autocomplete, stats * Store slice expansion: review/issue/MR watches lists, action presets by workspace, stats + status with loading flags. Properties prefixed with gitlab to avoid collision with GitHub slice * Domain hooks: use-gitlab-status, use-gitlab-stats, use-gitlab-review-watches, use-gitlab-issue-watches, use-gitlab-action-presets — mirror github hook shapes * Type model: ReviewWatch, IssueWatch, MRWatch, ActionPresets, Stats, ProjectMergeMethods, MRApproval, Pipeline, MRFeedback, etc. * Backend controller HTTP routes for watches/presets/write-actions/ projects/stats so the frontend HTTP API client has endpoints to hit All passes: backend (build + vet + lint + tests), web (typecheck + lint + 2400 tests). * fix(gitlab): wire orchestrator event handlers, fix label-filter drop, add tests Address Claude review blockers on the parity PR: 1. Wire orchestrator subscriptions for GitLab review/issue watch events. Add event_handlers_gitlab.go with handleGitLabNewReviewMR / handleGitLabNewIssue, dedup reservation handshake, and task-creator interfaces (mirrors event_handlers_github.go). main.go now calls orchestratorSvc.SetGitLabService so the dedup APIs are usable 2. Fix silent label-filter drop: when a custom_query is set on an issue watch, labels were being appended to the unused filter arg. Fold labels into customQuery (or default filter) so they actually reach GitLab; extracted appendLabelsToQuery helper with explicit handling of pre-existing labels= clauses 3. Switch event payloads to pointer types (mirrors GitHub) so handlers can type-assert pointer events 4. Backend tests: store_watches_test.go (CRUD + reserve/assign for all 4 new tables + presets), service_watches_test.go (label-filter helper, cleanup policy, project normalisation), action_presets_test.go 5. Frontend slice tests: review/issue watch CRUD round-trips, action presets + stats reducer coverage All green: backend (build+vet+lint+tests), web (typecheck+lint+2403 tests). * fix(gitlab): address remaining blockers — cleanup wiring, safe defaults, nil guards Address the second round of Claude review blockers: 1. main.go: wire SetTaskDeleter + SetTaskSessionChecker on services.GitLab (mirrors GitHub). Without these, manual cleanup sweeps would always error with "task deleter not configured" 2. service_cleanup.go: default cleanup policy on transient DB error is now CleanupPolicyNever (preserve tasks) instead of CleanupPolicyAuto (silently delete). Genuine "watch was deleted" path also falls under the safe-side default; user can manually delete the orphan tasks 3. service_watches.go: add nil guards on requireStore() so the 6 MR-watch list/get/delete methods return an errStoreUnavailable error instead of panicking when the SQLite store failed to construct at boot 4. use-gitlab-status.ts + use-gitlab-stats.ts: add per-mount attemptedRef so an unreachable GitLab doesn't trigger an infinite re-fetch loop — the previous useEffect re-ran every render because the failure path left status null, satisfying the !status guard * fix(gitlab): propagate session-check errors, clamp poll interval on update Address remaining Claude blockers: * service_cleanup.go: HasUserAuthoredMessage transient errors are now preserved (return false → skip delete) instead of silently ignored; the alternative was occasionally deleting tasks a user had touched * service_watches.go: extract clampPollInterval helper applying the same bounds (0 → default, <60 → 60) as the create path; both applyReviewWatchPatch and applyIssueWatchPatch use it so user-supplied zero or tiny values via UpdateXxxWatchRequest no longer hammer GitLab * fix(gitlab): clear dupl lint, fix preset retry loop + per-workspace watch cache * service_watches.go: applyReviewWatchPatch / applyIssueWatchPatch get nolint:dupl markers — they share shape but per-domain field validation lives in the create paths, so deduplicating via generics would obscure the contract. Restores backend lint to 0 issues * use-gitlab-action-presets.ts: per-workspace attemptedRef set so a failing preset fetch doesn't retry on every render * use-gitlab-review-watches.ts + use-gitlab-issue-watches.ts: track lastFetchedRef per consumer so a workspace switch triggers a refetch. The slice-level loaded flag is shared across instances and can't double as a per-workspace cache key * fix(gitlab): add nil-store guards across review/issue watch + cleanup paths Round-3 Claude finding: ReviewWatch / IssueWatch / preset / reservation methods still called s.requireStore().X() directly. If NewStore fails at boot (table migration error), the service struct's store is nil and every list/get/update/create/reserve method panics on first request. Apply the same store-nil → errStoreUnavailable pattern from f95a929 to every remaining method: * CreateReviewWatch / CreateIssueWatch * GetReviewWatch / ListReviewWatches / ListAllReviewWatches / UpdateReviewWatch / TriggerReviewWatchAll * GetIssueWatch / ListIssueWatches / ListAllIssueWatches / UpdateIssueWatch / TriggerIssueWatchAll * ReserveReviewMRTask / AssignReviewMRTaskID / ReleaseReviewMRTask * ReserveIssueWatchTask / AssignIssueWatchTaskID / ReleaseIssueWatchTask * lookupReviewPolicy / lookupIssuePolicy (return CleanupPolicyNever) * fix(gitlab): nil-store guards on CheckXxxWatch + DeleteXxxWatch + cleanup paths Round-4 Claude finding: 4 remaining panic sites the previous nil-guard pass missed — these run from the background poller (every 5 min) and the manual delete/cleanup flows, so a boot-time NewStore failure would crash the orchestrator process on first tick. * service_watches.go: add store-nil guards to DeleteReviewWatch / DeleteIssueWatch / CheckReviewWatch / CheckIssueWatch * service_cleanup.go: guard CleanupAllReviewTasks / CleanupAllIssueTasks entry points; wrap the trailing DeleteReviewMRTask / DeleteIssueWatchTask calls in `if store := s.requireStore(); store != nil` blocks so the cleanup succeeds even when the dedup-row delete is unreachable * refactor(gitlab): split service_watches.go to stay under 800-line revive limit CI's --new-from-rev lint caught service_watches.go at 808 lines (limit 800). Split into three focused files: * service_events.go (94 LOC) — publish helpers for MR feedback / new review MR / new issue / watch lifecycle events * service_issue_watches.go (302 LOC) — Issue watch CRUD + Check + Trigger + fetch + helpers * service_reservations.go (59 LOC) — Reserve/Assign/Release dedup handles used by the orchestrator event handlers service_watches.go is now 505 lines and contains MR watch + Review watch only. No behavioral change. * fix(gitlab): address inline code-review feedback Address actionable inline comments from Claude/CodeRabbit/cubic/Greptile: * appendLabelsToQuery: use url.ParseQuery for exact key match instead of strings.Contains; previously false-matched keys like mylabels= and silently dropped the watch's labels (test added) * fetchReviewMRs: drop dead `filter = watch.CustomQuery` assignment — SearchMRs's buildMRSearchQuery returns customQuery verbatim and ignores filter when customQuery is non-empty * store.go: add workspace_id indexes on gitlab_review_watches and gitlab_issue_watches (the 5-min poller and HTTP list endpoints did full table scans) * mock_client.SearchProjects: switch from == to case-insensitive Contains so partial-query autocomplete returns the seeded project, matching the doc comment * Poller: add sync.Mutex to guard `started` field; Start/Stop are now safe to call concurrently (go test -race would have flagged the previous read/write race) * Add ErrWatchNotFound sentinel; controller_watches.go maps it to HTTP 404 via httpRespondError helper. Update/Trigger handlers previously returned 500 for missing watches, hiding the distinction from real server faults * service_events.go: log Publish errors on watch lifecycle events instead of swallowing them silently * handlers.go: reject malformed JSON payloads in list-style WS handlers rather than silently falling through to broader list/search behavior * event_handlers_gitlab.go: remove dead gitlabRepoSlug helper + blank-identifier suppression * ci: re-trigger to clear flaky E2E shard 3 * fix(gitlab): address final review round — nil guards, store merge, logs, body validation - CheckMRWatch, UpdateReviewWatch, UpdateIssueWatch: nil guards for store/req - handlers: wsNewDiscussionNote rejects empty body - action_presets: Update reads raw stored row (no default freezing); Reset validates workspace_id - service_search.GetStats: log Warn per sub-call failure before zero - event_handlers_gitlab: log Release* dedup-row errors with context - web/store.ts: post-slice merge restores every GitLab sub-state field; extract buildStateOverrides helper - web/default-state.ts: add GitLab fields to defaultState + mergeInitialState via mergeGitLabFields helper - web/gitlab-api.ts: UpdateXxxWatchRequest now Omit workspace_id 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> |
||
|
|
6b98185ac3 | fix(web): restore debug UI on make start-debug (#1252) | ||
|
|
6a684211fc |
fix(agentctl): reap disconnected ACP sessions + MCP child trees (#1249)
* fix(agentctl): reap disconnected ACP sessions + MCP child trees (#1247) Two independent leaks let opencode acp sessions accumulate indefinitely: 1. instance.Manager never reaped disconnected sessions. StopInstance only ran via the kandev DELETE route, so any session whose backend went away (UI closed mid-run, WS dropped, opencode failed to emit complete) was orphaned forever. 2. Even when DELETE fired, the ACP adapter unconditionally returned RequiresProcessKill=false. opencode acp keeps an HTTP server + MCP child tree alive after stdin EOF, so closing stdin was a no-op and the children re-parented to init. Fix both: - Plumb RuntimeConfig.RequiresProcessKill from agents.OpenCodeACP through the ExecutorCreateRequest -> CreateInstanceRequest -> InstanceConfig -> adapter.Config -> shared.Config chain. acp.Adapter now reads cfg.RequiresProcessKill instead of hardcoding false. - Belt-and-braces: process.Manager.waitForProcessExit timeout fallback now killProcessGroup first (only falls back to single-process kill if that errors), so any future ACP CLI that ignores stdin EOF still gets reaped. - Add an idle-instance reaper to instance.Manager. Per-instance activity middleware tracks inflight HTTP requests + lastActivity; a background goroutine sweeps every minute and StopInstance's any instance idle past KANDEV_ACP_IDLE_TIMEOUT (default 1h, set to 0 to disable). Long-lived /agent/stream WS connections keep the inflight counter above zero so active sessions are never reaped. - Validate stdio MCP commands at CreateInstance time via exec.LookPath; missing entries are dropped with a warn instead of spawning a permanently broken child every session (fixes the stale /snap/bin/brave MCP repro in the issue). Regression tests: process-group kill (parent+child fixture), idle reaper end-to-end, MCP validation, opencode-acp opt-in vs other ACP agents' default. * fix: address PR review feedback on #1247 fix - idle_reaper: extract reapSingleIdleInstance helper with defer cancel(), add reaperStop polling between sweep iterations so Shutdown drains in one StopInstance window instead of N * timeout (cubic, claude, greptile). - instance.Manager: use sync.Once instead of m.mu to gate reaper-stop channel close (claude). - mcpStdioValidationError: tolerate compound Command strings by resolving only the first whitespace-delimited token; reject entries with neither URL nor Command outright; document agentctl-vs-agent PATH skew in Docker/SSH executor modes (claude, greptile, cubic). - manager_killgroup_test: treat zombie state as dead via /proc/<pid>/stat on Linux + bump Eventually window to 15s — CI containers' PID 1 can delay harvesting orphans long enough to flake signal(0) probes. - instance package: add goleak TestMain for the new reaper goroutine (coderabbit). - AGENTS.md: correct SIGKILL vs SIGTERM wording; clarify the timeout fallback only fires on ctx.Done (coderabbit, cubic). --------- Co-authored-by: Kandev Agent <agent@kandev.dev> |
||
|
|
2456504d48 | fix(web): hide token usage when context window report is unreliable (#1250) | ||
|
|
13c72844d5 | docs: improve agent harness guidance for tests, PR fixup, and debugging (#1235) | ||
|
|
fac2ad5039 | fix(backend): stabilize ACP session resume and workspace git poll under contention (#1242) | ||
|
|
79d4f84bf2 | fix(backend): cache & singleflight live PR feedback fetches (#1237) | ||
|
|
570c747959 | feat(worktree): support ** and brace globs in copy_files patterns (#1248) | ||
|
|
8eb0850c51 | feat(web): resizable review dialog sidebar with persistence (#1245) | ||
|
|
f3aeab020a |
feat(web): chat history nav with ArrowUp/Down and Ctrl+R fuzzy search (#1246)
* feat(web): chat history nav with ArrowUp/Down and Ctrl+R fuzzy search ArrowUp/ArrowDown cycle the user's previous messages in the chat composer (bash-style), Ctrl+R opens a fuzzy reverse-search overlay anchored to the input. Opening the overlay drains the paginated message history in the background so search covers the whole session, not only the pages already loaded in the chat list. * fix(web): address PR review on chat history nav (#1246) - Always clear isDraining on cancel, error, and normal completion (cleanup setter + try/catch/finally in the drain loop). - Ctrl+R no longer dead-keys when the loaded history is empty: the overlay opens unconditionally and renders "No matches" until drained pages arrive. - Dedupe adjacent user prompts in the original message stream, not in the extracted output, so repeated prompts separated by agent/tool turns stay reachable from ArrowUp and Ctrl+R. - Stabilize handleReverseSearchSelect by depending on the destructured closeReverseSearch instead of the spread overlay object. - HitRow key={hit.index} so React reuses rows across re-rankings. - Reorder SSR/isOpen guards before computeStyle so window.* isn't touched during a server render. - Add use-drain-older-messages.test.ts covering normal drain, batch cap, cancellation on active->false, and rejection clearing isDraining. * fix(web): address PR feedback on chat history nav (#1246) - writeHistoryContent wraps editor mutations in try/finally so state.isApplying clears even if a chain or onChange handler throws. - Dismiss the Ctrl+R overlay on window resize and capture-phase scroll so the fixed-position portal can't drift away from the editor anchor. - Drop the unused historyRef from useChatHistory's return value. - Extract HitRow into message-history-search-row.tsx so the search file stays under the 200-line cap. * fix(web): keep Ctrl+R overlay open during keyboard navigation (#1246) - Drop capture:true on the window scroll listener so scrollIntoView inside the overlay's own result list (triggered by keyboard navigation past the visible window) no longer dismisses the overlay. - Remove the always-true isOpen prop and the unreachable guard from MessageHistorySearch; visibility is controlled solely by the parent's conditional render. --------- Co-authored-by: Kandev Agent <agent@kandev.dev> |
||
|
|
2f12c680b8 | feat(mcp): include associated PRs in task-listing tools (#1236) | ||
|
|
a1143aa525 |
fix(acp): handle claude-acp async_launched subagents end-to-end (#1244)
* fix(acp): handle claude-acp async_launched subagents end-to-end Claude Agent SDK's Task tool with run_in_background=true returns session/prompt with stop_reason=end_turn while subagents stay alive in the background, reporting `_meta.claudeCode.toolResponse.status = "async_launched"` instead of a terminal top-level status. kandev previously treated stop_reason as authoritative, cancelled the subagent card on prompt return, and fired an "agent finished without producing any output" notice — even though the dispatch did produce work and the subagent's child events would arrive seconds later. Backend: - `SubagentTaskPayload` gains `IsAsync` / `OutputFile` / `CanReadOutputFile` so the async-launched envelope is preserved. - `convertToolCallResultUpdate` recognizes async_launched as terminal for the Task tool itself (dispatch is the completion). - `cancelActiveToolCalls` preserves subagent_task tool calls through the prompt-return sweep so a late terminal tool_call_update from the SDK still lands correctly (covers the in-turn-complete case). Frontend: - `subagentMetaChips` emits a "background" chip when `is_async` or `status == "async_launched"` so the card is visually distinct. - `computeEmptyTurnNotice` suppresses the notice when any subagent task message exists for the turn (the dispatch IS output). Reproduced via `acpdbg prompt` against `claude-acp` 0.31.1; captured frames in acp-debug/claude-acp-prompt-20260602-110119.jsonl drive the backend test fixtures verbatim. * fix(acp): harden async_launched override against future SDK frames Address PR review feedback (greptile #1244, claude review): - Move the async_launched override inside the mutex lock so it can inspect the stored payload kind before flipping the status. - Drop the `status == ""` precondition. The earlier in_progress synthesis (Title/RawInput/Content/Locations) was implicitly trumping the override; a future SDK frame that combines async_launched with any of those fields would have left the card stuck in_progress forever. The override is now unconditional for async_launched subagent_task frames. - Gate the override on `payload.Kind() == ToolKindSubagentTask`. A hypothetical future claude-acp tool that reuses the `async_launched` literal in its own `_meta.claudeCode.toolResponse.status` would have been incorrectly marked complete by the prior unscoped check. Pinned by two new tests: - TestConvertToolCallResultUpdate_AsyncLaunched_OverridesInProgress - TestConvertToolCallResultUpdate_AsyncLaunched_NonSubagentNotTerminated --------- Co-authored-by: Kandev Agent <agent@kandev.dev> |
||
|
|
b0b968cb22 |
feat(web): add reverse direction to recent task switcher (#1241)
* feat(web): add reverse direction to recent task switcher Cmd/Ctrl+Shift+Space now cycles the recent task switcher backward (most-recent toward oldest), complementing the forward Cmd/Ctrl+Space. Both directions are user-configurable via Settings > Keyboard Shortcuts. - new TASK_SWITCHER_REVERSE shortcut; Shift keeps it mutually exclusive with the forward binding so both can be held together - thread a cycle direction through openSwitcher/cycleSwitcher; reverse opens on the oldest non-current task - commit on release of either binding's hold modifier - switcher footer shows both Next / Previous bindings * fix(web): commit task switcher only on the active binding's modifier Address PR review on the reverse task switcher: - track the binding that drove the switcher and commit only on ITS hold modifier release, so divergent custom forward/reverse bindings can't commit early (greptile/coderabbit) - move the CycleDirection type below the import block - correct the reverse-shortcut direction comment (oldest -> most-recent) - assert exact task titles in the reverse e2e precondition * test(web): assert task switcher stays open on Shift-release Make explicit that releasing the secondary modifier alone does not commit the reverse switcher — only the hold modifier does. |
||
|
|
f149bd1292 |
fix(office): disable create button until project + title set (#1243)
* fix(office): disable create button until project + title set Office "New Task" dialog let user submit without picking a project, which hit the backend isOfficeRequest() heuristic and failed validation with "workflow_id is required for non-ephemeral tasks". Disable the button until both title and project are set and surface the missing requirement in a tooltip instead of a backend error toast. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(office): guard handleCreate on projectId + cover with tests PR review feedback (claude, cubic): the handleCreate path only checked title and workspace, so a keyboard shortcut or future programmatic caller could still produce a request with no project and reproduce the backend 'workflow_id is required' error. Guard it there too, and add unit tests that pin the disabled-state + reason behavior of the extracted CreateTaskButton. aria-label on the trigger wrapper surfaces the reason to assistive tech even when the tooltip is closed. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * test(office): cover submitting state for CreateTaskButton PR review feedback (claude): the disabled check had three branches (missing title / missing project / submitting); only two were tested. Add the submitting case so a regression that drops it from the disabled expression is caught. 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> |
||
|
|
cb7b5429d5 |
fix(tasks): harden multi-branch add_branch and surface it in UI (#1239)
Three areas were broken in the MCP add_branch_to_task flow: 1. Predictable branch names. autoBranchName counted up from existing branch-N rows. Concurrent add_branch calls raced onto the same name and the worktree directory leaked ordering. Now uses a random branch-<3 random chars> suffix via worktree.SmallSuffix. 2. Sibling materialize did not flow to the UI. The materializer published AgentctlReady with the NEW sibling worktree's path, which the frontend handler wrote onto the session row — clobbering the primary's worktree_id/branch and pointing the file browser at the sibling instead of the task root. Added TaskWorkspacePath to the payload, detected the sibling case on the frontend (existing worktree_id != payload's), kept the primary's id/branch, repointed worktree_path to the task root, bumped commits refetch, and dropped the pre-multi-repo legacy git-status entry so the Changes panel stops surfacing the frozen single-repo snapshot. File browser's resetKey now includes the worktree count so the tree reloads when the sibling lands. 3. workspace_path promotion + agentctl rescan refused to act on historical envs. A few envs had workspace_path pointing at the source repo's local_path (orchestrator computeWorkspacePath fallback to req.RepositoryPath when resp.WorktreePath is empty) instead of the primary worktree. The materializer's promotion gate compared against env.WorkspacePath's parent, so those envs were never promoted, and agentctl's resolveRescanPath only accepted parent-of-current so the rescan to the real task root was rejected. Trackers stayed on the wrong directory and the sibling's commits never appeared in the Commits panel even after a full reload. Anchored promotion on env.TaskDirName (basename cross-check) and added a recovery branch to resolveRescanPath that accepts any absolute directory which actually contains git-repo subdirs — the bearer-token middleware already gates the endpoint and scanRepositorySubdirs validates the children, so the relaxed check stays safe. Co-authored-by: Kandev Agent <agent@kandev.dev> Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com> |
||
|
|
a814379cf6 |
fix(web): refresh commits panel snapshot on mount (#1232)
* fix(web): fetch commits snapshot on mount even when prefilled by live events The useSessionCommits hook gated its initial snapshot fetch on `commits === undefined`. When a `commit_created` WS event populated the list before mount (session already running when the panel opens, or stream replay on reconnect) and carried zero stats, the bad data stuck — the authoritative `git log --shortstat` snapshot never ran to overwrite it, leaving older commits stranded with `+0 -0` in the Commits panel. Anchor the initial-fetch gate to `sessionId` via a ref so the snapshot fires once per session regardless of how `commits` got populated. * fix(web): reset fetchedSessionRef when sessionId goes null Without resetting the ref in the !sessionId early-return path, a hook that stays mounted while sessionId cycles through null (and commits get cleared via clearSessionCommits) would not refetch when the same sessionId is reselected, because fetchedSessionRef.current would still equal it. Now the ref clears on null so reselect always re-fetches. Addresses Greptile P2 on #1232. * fix(web): reset fetchedSessionRef during disconnect teardown Move the `!sessionId` early-return ref clear before the connection gate. Without this, a session teardown that runs while WS is disconnected leaves `fetchedSessionRef.current` pinned to the old sessionId, so reconnecting to the same session never triggers the authoritative snapshot fetch. Addresses cubic P2 on #1232. --------- Co-authored-by: Kandev Agent <agent@kandev.dev> |
||
|
|
140e828ead |
fix(web): subdue voice input button to ghost style (#1233)
* fix(web): subdue voice input button to ghost style The voice button was styled as a primary-filled circle to mirror SubmitButton, but in the task-create dialog there is no adjacent Submit (the submit button lives in the footer), so the loud styling read as the dialog's primary action rather than as a secondary input control. In the chat composer it also competed visually with Submit where Submit should win. Switch to `variant="ghost"` with muted-foreground icon so voice sits in the toolbar alongside attach/enhance as one secondary action among several. Recording state keeps the pulsing destructive ring for unmistakable visual feedback but uses a tinted `bg-destructive/15` instead of a full destructive fill. Unsupported fallback also moves from `secondary` to `ghost` so the greyed-out variant matches. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * refactor(web): couple voice mic ring radius to button corner radius Use `rounded-[inherit]` on the recording pulse-ring so the corner radius follows the underlying `Button`'s actual radius instead of being hardcoded to `rounded-md`. The two happen to match today (shadcn's ghost Button defaults to `rounded-md`), but the implicit coupling would silently misalign if the Button base styles ever change. Flagged in PR review by both Claude and Greptile. 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> |
||
|
|
a8db7478ac | fix(backend): cap gh/git fork concurrency via shared subproc throttles (#1216) | ||
|
|
fd79d6170f |
feat(tasks): multi-branch tasks — N (repo, branch) pairs per task (#1226)
* feat(tasks): multi-branch tasks — N (repo, branch) pairs per task Extends multi-repo into multi-branch so a single task can hold multiple worktrees on the same repo (one per branch). Sibling worktrees layout at <task>/<repo>/ + <task>/<repo>-<slug>/, dedicated PRWatch per branch, live agentctl tracker rescan + frontend store updates so the UI splits PRs / files / commits per branch without a session restart. MCP add_branch_to_task_kandev tool is shielded to the worktree executor. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(web): unify multi-branch repo label as <repo> · <branch> across panels PR Changes and Commits panels rendered repo labels differently for multi-branch tasks — Commits showed "kandev · feature/x" while PR Changes leaked the raw "kandev-feature-x" agentctl tracker tag. Reformat tracker-tagged repo names via formatRepoLabel using the longest known-repo prefix so kandev-cli-* doesn't collapse to kandev · cli-*. * fix(tasks): address multi-branch PR review — data races, slug collisions, MCP error codes Backend - agentctl process.Manager: add repoTrackersMu RWMutex guarding repoTrackers + workspaceTracker. Reads snapshot under RLock; RescanRepositories builds trackers first, takes Lock for swap/append. Validates newWorkDir via os.Stat before publishing cfg.WorkDir. - worktree.Manager: reject invalid explicit BranchSlug up-front; cache key uses sanitized wt.BranchSlug so read/write paths are symmetric. - worktree.store: narrow isDuplicateColumnError to "duplicate column name". - task service AddBranchToTask: slug-collision guard rejects feat/a vs feat-a. - orchestrator resolvePushRepo: two-pass exact-name match before subdir prefix fallback so "backend" doesn't swallow pushes for "backend-admin". - executor buildRepoSpecs: duplicate repos always get a non-empty BranchSlug (falls back to branch-N when both branch fields sanitize to empty). - MCP handler: classifyAddBranchError maps service errors to NOT_FOUND / CONFLICT / VALIDATION_ERROR; add ErrorCodeConflict constant. - MCP server: extract registerAddBranchToTaskTool, register in task mode only (external mode contract: config tools + create_task_kandev). - workspace_rescan handler: reject malformed JSON with 400 instead of silently rescanning the current root. - branch_materializer: split MaterializeBranch into prepare + finalize helpers to satisfy the 80-line cap. Frontend - existingPrUrl returns undefined when a task has >1 PR so per-repo View PR buttons take over (no silent linking to a sibling). - formatRepoLabel checks exact known-repo match before prefix splitting so "kandev-cli" isn't mislabeled as "kandev · cli". - Repo chip key now includes base_branch so siblings differing only on base render as distinct chips. Docs - multi-branch spec uniqueness wording aligned with the canonical 4-column key (task_id, repository_id, base_branch, checkout_branch). - worktree.CreateRequest.BranchSlug doc describes the sibling layout (<repo>-<slug>) instead of the obsolete nested form. * fix(tasks): scope slug-collision check to repo only + use errors.Is for ErrTaskNotFound - service_branches: drop base_branch from the slug-collision check. Worktree path is (repo, slug); two rows on the same repo cut from different bases still collide on disk. - service_branches: prepareBranchAdd wraps taskrepo.ErrTaskNotFound via fmt.Errorf with %w so classifiers don't have to substring-match the formatted task UUID. - mcp handlers: classifyAddBranchError uses errors.Is(err, ErrTaskNotFound). * fix(agentctl): sanitize rescan work_dir before os.Stat to clear CodeQL path-injection CodeQL flagged the rescan endpoint as a path-injection sink (the HTTP-supplied work_dir flowed straight into os.Stat and downstream scanRepositorySubdirs). Add validateRescanPath which: - requires the input to be a clean absolute path with no ".." segments - rejects anything that isn't equal to or an ancestor of the current cfg.WorkDir — the only legitimate use case is the branch materializer promoting <task>/<repo>/ to <task>/ Defense in depth: agentctl binds to localhost, but any colocated process could otherwise direct it at /etc or the user's home by POSTing /api/v1/workspace/rescan. * fix(agentctl): tighten validateRescanPath, close TOCTOU on existingTrackers, add tests - Drop the strings.Contains(clean, "..") guard. filepath.Clean already resolves traversal; the substring check rejects legitimate names like /home/u/..hidden without adding defense (the ancestor check via filepath.Rel is the real boundary). - Drop the rel == "." branch in the ancestor check — early return for clean == currentClean already covers identity. - Read existingTrackers inside the write-lock section (not under RLock then re-Lock) so two concurrent rescans can't both see 0 trackers and double-bootstrap the multi-repo set. - Add table-driven TestValidateRescanPath covering rejection paths (relative, descendant, sibling, traversal-cleaned, empty input), legitimate ancestor promotion, exact match, no-anchor first-launch, and a /home/u/..hidden dotted-segment case. * fix(agentctl): break taint flow into os.Stat — derive rescan path from cfg, not HTTP body CodeQL's go/path-injection trace still flagged manager_rescan.go after the prior sanitizer because the user-supplied newWorkDir flowed (after validation) into os.Stat and then into cfg.WorkDir → tracker paths → workspace_monitor os.Stat sinks. Replace validateRescanPath with resolveRescanPath: it returns a path DERIVED from currentWorkDir (no-op = currentClean, promotion = filepath.Dir(currentClean)) and uses newPath only for comparison, never as a source for the returned string. os.Stat now receives a path whose provenance is trusted manager config. Functionally the legitimate caller (branch materializer promoting <task>/<repo>/ to <task>/) is unchanged. * test(agentctl): make TestResolveRescanPath windows-portable The Linux Backend Tests job passed but the Backend (windows) job rejected every "absolute" case because filepath.IsAbs("/task/repo") is false on Windows (no volume letter). Build inputs via filepath.Join against absRoot() (filepath.Abs of "/" / "\\") so the same table covers POSIX (`/...`) and Windows (`C:\...`) without per-OS branches. Keep the traversal-after-Clean case POSIX-only since filepath.Clean is the shared primitive and that input form needs a volume to be considered absolute on Windows anyway. * fix(agentctl): serialize RescanRepositories + re-check trackers under write lock Two concurrent RescanRepositories calls could both observe an empty tracker set (between the snapshot read and the bootstrap branch) and both enter transitionToMultiRepoMode, leaking a duplicate bare-root tracker and duplicate per-repo trackers. Same race in appendNewRepoTrackers would double-append the same new child across both callers. Fix: - Add rescanMu sync.Mutex on Manager. Acquired for the full body of RescanRepositories so the snapshot → branch → mutate sequence is serialized. repoTrackersMu still guards individual field reads from unrelated goroutines (subscribe/unsubscribe/poll-mode). - appendNewRepoTrackers re-checks membership inside the write-lock before appending and drops already-present trackers (Stop + detach outside the lock). Defense in depth. - Test: add `dotted segment is legal as promotion` case so the ..hidden-segment promotion path stays exercised. --------- Co-authored-by: Kandev Agent <agent@kandev.dev> Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com> |
||
|
|
5591cdc0bf | fix(web): add button to load older chat messages reliably (#1223) | ||
|
|
0f87f7a56b | fix(acp): enrich tool call metadata per agent with structured ACP fields (#1212) | ||
|
|
aea266ee71 |
fix(web): fix hold-to-talk on mobile and add coarse-pointer toggle fallback (#1231)
* fix(web): make voice-mode hold gesture survive touch and add mobile coverage
Mobile users with the chat composer's mic button in hold-to-talk mode lost
their recording the moment a finger crept off the 28px target, because
`buildHoldHandlers` aborted on `pointerleave`/`pointercancel` without ever
claiming the pointer.
- `buildHoldHandlers` now claims the pointer on `pointerdown` via
`setPointerCapture` and releases it on `pointerup`/`pointercancel`. The
`pointerleave` handler is dropped — capture keeps the gesture alive across
small finger movement, OS gesture handoff, and soft-keyboard reflows.
- `EnabledVoiceInputButton` resolves an `effectiveMode` via a coarse-pointer
check (`matchMedia("(pointer: coarse)")`); when the stored preference is
`hold` and the device is touch-first, the mic falls back to toggle so a
bad stored preference can't trap users in a broken UX. Persisted
`voiceMode.mode` is left untouched so docking a keyboard restores the
user's choice without a save.
- The button bumps to 40×40 on coarse-pointer devices to meet the 44pt touch-
target floor without disturbing the desktop submit/voice pair.
- The unsupported fallback path (`UnsupportedVoiceButton`) is unchanged so
LAN-HTTP mobile users still get the HTTPS explanation toast.
Adds `voice-input-button.test.tsx` covering capture/release on pointer
events, the absence of `pointerleave` regression, the coarse-pointer toggle
override, and the touch-target class swap.
Adds `mobile-voice-mode.spec.ts` (Playwright mobile-chrome / Pixel 5)
verifying the Voice Mode settings page is reachable from the mobile sidebar
trigger and that the mic button mounts with `data-effective-mode="toggle"`
on the mobile chat composer.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* test(web): drop unused testPage param from mobile-voice-mode seedTask helper
The helper only calls apiClient and does not touch the Playwright Page
object — the parameter was carried over from the neighbour spec's pattern
where the helper also navigates. Removing it also drops the now-unused
`Page` type import.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* refactor(web): drop redundant Element type cast from safePointerRelease
`Element.hasPointerCapture` and `releasePointerCapture` are already declared
in lib.dom.d.ts, so the `target as Element & { hasPointerCapture?: ... }`
casts were over-defensive. Replace with a direct `typeof` runtime check,
which still guards against the headless DOMs that strip these methods.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* test(web): assert coarse-pointer touch-target class in mobile voice spec
The previous assertion (`data-effective-mode === "toggle"`) would have
passed even if the coarse-pointer override were reverted, since the default
stored `voiceMode.mode` is already `"toggle"`. Add a check on the `h-10
w-10` class swap — that class pair is only emitted when
`useIsCoarsePointer()` returned true, so the test now genuinely exercises
the override on the e2e surface.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* test(web): extend voice-input-button unit coverage for toggle paths
- Toggle-mode + fine-pointer: pointerdown must not start recording; click
drives the toggle. Guards against an accidental future change that wires
hold pointer handlers when stored mode is "toggle".
- Recording state + click: stop() fires (not start()), exercising the
buildToggleHandler branch the original suite skipped.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* test(web): seed hold mode in E2E and fix misleading test title
- Add `voice_mode` field to `saveUserSettings` in api-client.ts so E2E
tests can PATCH the user's stored activation mode via the API.
- Seed `mode: "hold"` before the mic-button assertion so
`data-mode="hold"` + `data-effective-mode="toggle"` together prove the
coarse-pointer override fired (not that the default was already toggle).
- Rename settings-page test to drop the "from mobile sidebar" claim —
the test navigates directly to the URL; sidebar existence is still
asserted as a regression guard.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
---------
Co-authored-by: Kandev Agent <agent@kandev.dev>
Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
|
||
|
|
f23d11dc64 |
feat(web): add voice mode to task create dialog (#1230)
* feat(web): add voice mode to task create dialog prompt Wires the existing VoiceInputButton (introduced in #1159 for the chat composer) into the task-create dialog's prompt toolbar so users can dictate the initial task description. - TaskFormInputs gains an internal insertAtCursor that splices the transcript at the textarea caret with a single leading space when it follows a non-whitespace character, matching the chat composer's behaviour. A pending-cursor ref + useLayoutEffect restores the caret position after React commits the new value. - An optional onVoiceAutoSend prop is threaded from TaskCreateDialog down through DialogPromptSection -> TaskFormInputs -> VoiceInputButton. The dialog points it at formRef.current?.requestSubmit() so dictation can create the task hands-free; every existing submit gate still applies via the disabled submit button. - Voice mode is gated globally on userSettings.voiceMode.enabled, so the button self-hides for users who turned it off in settings and self-greys on browsers without a usable engine. * fix(web): invoke submit handler directly for voice auto-send `HTMLFormElement.requestSubmit()` is missing on Safari < 16, so the previous wiring silently failed with a TypeError on browsers where voice mode itself works (Safari 14.5-15 supports Web Speech / Whisper Web). Drop the form ref and call `setup.guardedHandleSubmit` with a synthetic event instead. `guardedHandleSubmit` only reads `preventDefault` off the event, and every validation gate (`validateForCreate`, session-mode description/agent checks, edit-mode title check, `submitBlockedReason`) lives inside the submit handler — so dictation with incomplete fields still no-ops, identical to the prior requestSubmit-based behaviour. This also matches the chat composer's pattern of calling the same submit function the send button uses. independent-review: voice auto-send Safari < 16 compat * test(web): expand voice splice cases for task-create dialog Adds adversarial coverage for the at-cursor splice helper that backs voice-mode dictation in the task-create dialog: - multi-line description with caret on a line boundary - transcript with internal newlines preserved as-is - existing tabs / newlines before the caret treated as whitespace - session-mode renders the voice button Splits the test file into a rendering-and-wiring `describe` and a splice-behaviour `describe` so each arrow function stays under the 100-line `max-lines-per-function` lint cap. qa: adversarial voice splice cases + session-mode parity --------- Co-authored-by: Kandev Agent <agent@kandev.dev> |
||
|
|
e2b2eadb72 | fix(web): apply repository filter correctly on task board (#1215) | ||
|
|
1eb62f2810 | docs(agentctl): clarify default_child_ordering is not enforced outside office (#1214) |