Commit Graph

262 Commits

Author SHA1 Message Date
José Almeida 61723934b9 fix: isolate saved layouts from stale sessions (#1649)
* fix: isolate saved layouts from stale sessions

* fix: preserve session tabs in saved layouts

* fix: handle materialized saved layout visibility

* fix: preserve sibling sessions on env switch

* fix: load task sessions before saved layouts

* fix: preserve sibling tabs on layout fast paths

* test: cover layout session id deduplication
2026-07-10 11:18:36 +01:00
José Almeida 3e237e177a feat: add agent-authored code walkthroughs (#1647)
* feat: add code-walkthrough backend (MCP tool, service, persistence, WS events)

* feat: render agent code walkthroughs as line-anchored cards + editor window

* test: add code-walkthrough re-emit mock-agent scenario

* fix: rework code-walkthrough UX (floating card, line reveal, minimize, clickable file)

* fix: improve code walkthrough editor UX

* fix: backfill walkthrough launcher after websocket connect

* fix: improve walkthrough requests and editor anchors

* fix: clear walkthrough highlights on close

* feat: add walkthrough discard and prompt customization

* fix: send walkthrough requests through saved prompt

* fix: support postgres built-in prompt seeding

* fix: address walkthrough review feedback

* fix: address walkthrough review follow-ups

* fix: harden walkthrough lifecycle races

* fix: reuse walkthrough notification cleanup

* fix: guard stale walkthrough file opens

* fix: abort stale file opens before paint

* fix: stabilize github settings e2e readiness

* fix: use stable github settings e2e selector
2026-07-10 08:34:57 +01:00
Clément Fiere 7e2a460f59 fix(web): open each linked PR in its own tab from the "+" menu (#1636) 2026-07-09 19:23:17 +01:00
José Almeida 482ac19c68 fix(sidebar): keep created subtasks during hydration (#1626)
* fix(sidebar): keep created subtasks during hydration

* fix(sidebar): preserve placeholder subtasks during hydration

* fix(sidebar): mark websocket placeholder snapshots
2026-07-09 17:26:41 +01:00
José Almeida 9c73a2d8c3 fix: keep diff previews tied to the correct file (#1618)
* fix: keep diff previews tied to the correct file

* fix: handle legacy file editor panel restores

* test: align file editor image mock

* fix: ignore sibling repo editor sync changes

* test: use repo scoped file keys in save tests

* fix: guard file editor preview restoration

* fix: remove redundant preview path fallback

* test: cover repo scoped image previews

* test: use repo scoped keys in sync tests

* fix: guard stale file editor open fetches

* fix: distinguish repeated file editor requests

* fix: open single-repo PR diffs without repo filter

* fix: keep pending file opens alive
2026-07-09 17:25:33 +01:00
José Almeida 1ed2c59997 feat: persist and expire quick chats (#1612)
* feat: persist and expire quick chats

* fix: address quick chat review feedback

* fix: resolve quick chat review findings

* fix: close remaining quick chat review gaps

* fix: avoid repeated quick chat expiration rechecks

* fix: guard quick chat expiration deletes

* fix: expire abandoned starting quick chats

* fix: ignore missing quick chat expiration candidates
2026-07-08 12:11:28 +01:00
Carlos Florêncio 8b90ec2593 feat: improve office onboarding and workspace flows (#1606) 2026-07-08 12:07:38 +01:00
José Almeida 5105ac224c feat: cmd+click and shift+click multi-select for kanban and sidebar (#1518)
* feat: add anchored range selection to multi-select reducer

* feat: cmd+click and shift+click multi-select on kanban board

* feat: add cmd/shift multi-select to sidebar task list

* test: e2e coverage for cmd/shift multi-select

* fix: harden multi-select selection lifecycle from review feedback

* test: scope multi-select e2e helpers to active containers

* fix: add sidebar multi-select hook tests and tighten bulk-action lifecycle

* fix: prune hidden sidebar selections and order bulk moves by view

* test: cover visible-order bulk move and anchor realignment

* fix: harden sidebar bulk-move scope and keyboard selection parity

* fix: order kanban bulk moves by board position

* test: cover multi-select helpers and fix stale-id sort fallback

* fix: order kanban card context-menu moves by board position

* feat: sidebar multi-select shows only bulk-valid actions with bulk pin and delete

* fix: harden multi-select bulk ordering, selection clearing, and a11y

* fix: gate kanban card move-to-step for mixed-workflow selections

* fix: unpin fully pinned sidebar selections

* refactor: keep sidebar task components under limits

* test: restore sidebar selection console spy safely
2026-07-06 17:16:14 +01:00
Clément Fiere 26773b28d1 feat(automation): delete buttons for individual and all runs in Recent Runs (#1586) 2026-07-06 16:39:33 +01:00
Carlos Florêncio c18a13c361 feat(tasks): adopt Office-style /tasks list UX (#1593) 2026-07-03 22:56:13 +01:00
Carlos Florêncio e27a9e84aa fix: support postgres database stats page (#1589) 2026-07-03 16:58:21 +01:00
Carlos Florêncio 3f930f5029 fix: preserve task create defaults after refresh (#1575) 2026-07-02 13:59:57 +01:00
Carlos Florêncio 7893a5e33e fix: trigger workflow on agent task messages (#1555)
* fix: trigger workflow on agent task messages

* fix: harden agent task message dispatch

* fix: preserve completed task message sessions

* fix: restore review step on task message failure

* test: cover completed task message handoff

* fix: publish task message rollback events

* fix: restore task message session rollback

* fix: avoid orphaned task message rows

* docs: explain task message prompt recording

* fix: harden task message rollback cleanup

* fix: complete task message rollback cleanup

* fix: restore task message dispatch side effects

* fix: restore task state after capture failure

* fix: simplify task message rollback cleanup

* fix: preserve task message rollback snapshots

* fix: restore task message turn-start snapshots

* fix: preserve task message queue snapshots

* fix: fail closed on office task lookup

---------

Co-authored-by: root <root@kandev.ts.net>
2026-06-30 18:28:28 +01:00
Carlos Florêncio 1abb1c914e fix: keep acknowledged agent errors hidden (#1558)
* fix: keep acknowledged agent errors hidden

* fix: address agent error acknowledgement review

* fix: include fallback sessions in error acknowledgements

* test: cover agent error acknowledgement helpers

---------

Co-authored-by: root <root@kandev.ts.net>
2026-06-30 16:29:46 +01:00
Carlos Florêncio 5ed975ddbf feat: redirect and toast when focused task is auto-deleted (#1544) 2026-06-29 19:10:08 +01:00
Carlos Florêncio 8ca17e0d6e fix: preserve pinned task sessions (#1517) 2026-06-28 16:10:04 +01:00
Nicolas Lenepveu 5021066d78 feat: bind repository to Linear/Jira/Sentry issue watchers (#1491) 2026-06-25 18:33:15 +01:00
Carlos Florêncio 6126edb203 feat: add subtask completion trigger (#1473) 2026-06-23 16:07:10 +01:00
Carlos Florêncio 7ada858f17 feat: add CI PR automation controls (#1446) 2026-06-20 10:05:25 +01:00
Carlos Florêncio 2a890c7975 feat: sync task preferences across devices (#1440) 2026-06-19 12:46:46 +01:00
José Almeida 2781556ead perf: tame browser-grinding cumulative diffs during large rebases (#1430)
* perf(backend): cap cumulative diff bytes and file count during large rebases

* perf(web): coalesce cumulative-diff refetches and skip redundant status compares

* feat(web): warn when the changes panel hides files from a capped diff

* fix(backend): enforce cumulative diff total byte budget strictly

* fix(web): keep coalesced refetch timer shared across envKey subscribers

* fix(web): drop unsound git-status SHA fast-path that staled live diffs

* fix(web): cancel coalesced timer on mid-flight drain to avoid duplicate refetch

* refactor(web): move changes-panel banner and helper to a sibling file

* docs(backend): clarify cumulative diff truncated-files-count semantics

* fix(web): tighten useSelectedFileKey mode param to the all|file union
2026-06-18 17:28:21 +01:00
José Almeida 3b2e1396fa perf: fix board lag, memory growth, and rebase-driven CPU spikes (#1427)
* perf(kanban): memoize derived board data to stop re-render storms

The board rebuilt the `tasks` array (and `stepOptions`/`sharedProps`) on every
store change, producing fresh object identities that defeated downstream memos
and re-rendered every card on each WebSocket message. Wrap them in useMemo so
the board only re-renders when its inputs actually change.

* perf(session-runtime): bound runtime memory and dedupe git-status compare

Three runtime-store fixes for the crash a heavy rebase triggered:

- Cap shell + terminal output buffers at 2MB (matching the existing process
  buffer) so a chatty shell can't grow the store without limit.
- Purge per-session runtime state (messages, turns, shell/process buffers, and
  the bySessionId maps) when a session is removed; env-scoped maps are dropped
  only once no other session shares the environment.
- Run setGitStatus's deep per-file diff comparison once per update and return
  whether anything changed, instead of recomputing it 2-3x. Under a massive
  rebase (thousands of files, frequent updates) that comparison was the
  dominant CPU cost and drove the render storm.

* refactor(session-runtime): drop now-unused gitStatusWouldMutate

The git-status handler no longer pre-checks for changes (setGitStatus now
returns whether it changed), leaving gitStatusWouldMutate with no callers.

* fix(session-runtime): enforce terminal cap on first and oversized chunks

appendTerminalChunk only evicted when more than one chunk was buffered, so a
single chunk larger than the 2MB cap slipped through untrimmed; the new-terminal
branch also pushed the first chunk without going through the cap at all. Clamp
each chunk to the tail of the cap and route the new-terminal path through
appendTerminalChunk. Addresses CodeRabbit review.
2026-06-18 13:30:41 +01:00
Carlos Florêncio e930061fe4 fix(web): chat badge, dockview preview routing, and agent-error icon cleanup (#1395) 2026-06-16 20:29:35 +01:00
Carlos Florêncio d3575e2683 fix: restore office sidebar navigation (#1396) 2026-06-16 20:19:13 +01:00
Carlos Florêncio 3bac35b574 feat(web): surface ensure-session errors and persist preset layout changes (#1394) 2026-06-16 15:21:20 +01:00
José Almeida 214a8f5cce fix: scope multi-repo file editor open/save to the right repo (#1382)
* fix: scope multi-repo file editor open/save to the right repo

The editor open/save/sync/delete chain dropped the repository_name in
multi-repo task workspaces, so the backend resolved paths against the
bare task root and failed with "file not found". Thread the repo
subpath through openFile, save, sync, and delete, and store it on
FileEditorState.

* fix: pass repo from diff Edit button to multi-repo file editor

The diff/changes "Edit" button opened files without their repository
scope, so multi-repo files resolved against the bare task root and
failed with "file not found". Thread repository_name through the diff
and review toolbars into onOpenFile.

* fix: preserve repo across tab restore and stale syncs

Address review feedback: persisted file tabs dropped the repo subpath,
so a multi-repo file reopened against the bare task root after a browser
refresh. Persist and restore `repo` on StoredFileTab. Also guard the
workspace sync against a tab being swapped to a different repo mid-fetch,
and widen the remaining onOpenFile type declarations to carry repo.

* fix: thread repo into editor panel fallback fetch

Address review feedback: FileEditorPanel's useFileLoader fallback fetch
(triggered when the open-files map is transiently empty, e.g. after a
session switch) called requestFileContent without the repo subpath,
re-introducing the multi-repo "file not found" failure. Record repo on
the panel params and forward it through useFileLoader. Also assert the
full restored-tab shape in the local-storage round-trip test.

* fix: guard editor fetches against stale session/repo races

Address review feedback: file open and the panel fallback fetch could
apply a late response after the user navigated away. openFile now drops
the result if the active session changed mid-fetch (via fetchFileEditorState),
and the panel loader keys its in-flight guard by session+path+repo so a
changed target re-fetches and stale responses are discarded.
2026-06-15 17:59:05 +01:00
Carlos Florêncio fd421848fb feat: add feature toggles settings (#1372) 2026-06-15 13:33:45 +01:00
Carlos Florêncio 3a3fca7f0e feat: add resource metrics topbar (#1373) 2026-06-14 22:33:34 +01:00
Carlos Florêncio 010948767a feat: add file-backed prompt attachments (#1367) 2026-06-14 21:40:02 +01:00
Carlos Florêncio 35a12fdb2c fix: distinguish sidebar workspace types (#1364) 2026-06-14 18:27:40 +01:00
Carlos Florêncio 2dd03b5431 feat: expose ACP session debug metadata (#1359) 2026-06-13 23:38:07 +01:00
Carlos Florêncio e983cb1be1 feat: show session config in message metadata (#1354) 2026-06-13 22:32:12 +01:00
Carlos Florêncio f444fba455 perf(chat): eliminate redundant re-renders and markdown re-parsing (#1357) 2026-06-13 19:26:54 +01:00
Carlos Florêncio 8d0493d1a4 fix: persist session runtime config (#1346) 2026-06-12 08:56:14 +01:00
Carlos Florêncio d78a1a4370 fix: default changes panel to tree view (#1314) 2026-06-08 21:22:39 +01:00
José Almeida 46f20e0dfe fix(web): keep dockview group alive on task switch to stop layout collapse (#1308)
* fix(web): keep dockview group alive on task switch to stop layout collapse

On a fast-path env switch, the incoming session chat was added only after
removeEphemeralPanels closed the outgoing chat. When that chat was the last
surviving panel in its group (e.g. the default layout's chat column), removing
it first emptied — and so destroyed — the group. The captured group id then no
longer existed, and post-#1165 the old `referenceGroup: "sidebar"` fallback is
dead, so addPanel ran with an undefined position: dockview dropped the incoming
chat into the active (terminal) group and collapsed the grid root to a vertical
stack. The corrupted layout was then persisted, so it survived reloads.

Add the incoming session panel before removing the outgoing one, anchored at the
outgoing panel's current index, so the group stays alive throughout the swap.
Removing the panels ahead of the insert point shifts the new panel into the same
final slot, so computeSurvivingIndex is no longer needed.

* docs(web): note the raw-index equivalence assumption in env switch
2026-06-08 17:33:54 +01:00
Carlos Florêncio 6661255024 fix(web): restore dockview sidebar layout (#1293) 2026-06-07 21:41:40 +01:00
José Almeida 2ed65cfde3 fix(web): guard dockview measure against mid-transition sidebar width (#1288)
* fix(web): guard dockview measure against mid-transition sidebar width

The unified AppSidebar (#1165) animates its width over 300ms; the dockview
content container (a flex sibling) can be measured mid-transition at a tiny
positive width in onReady, so the default layout builds too narrow and dockview
collapses the horizontal columns into a vertical stack. measureDockviewContainer
trusted any positive parent clientWidth. Reject an implausibly-small live width
(< 50% of viewport) as a not-yet-laid-out transient and fall back to the
viewport; the resize observer then snaps to the exact size.

* fix(web): preserve live height when width guard fires; assert it
2026-06-07 12:35:29 +01:00
José Almeida 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.
2026-06-06 21:58:39 +01:00
Carlos Florêncio 5e5e115105 fix(web): prevent cross-task session leak in dockview layout switch (#1265) 2026-06-04 09:31:50 +01:00
Carlos Florêncio bf7762d939 fix(agents): split codex acp flags from agentctl auto-approve (#1253) 2026-06-02 23:10:02 +01:00
João Salavisa 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>
2026-06-02 22:10:07 +01:00
Carlos Florêncio 6b98185ac3 fix(web): restore debug UI on make start-debug (#1252) 2026-06-02 21:48:23 +01:00
João Salavisa 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>
2026-06-02 00:14:00 +01:00
João Salavisa 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>
2026-06-01 19:36:20 +01:00
João Salavisa 1fdeda904b feat: add voice mode for chat input (#1159)
* feat: add voice mode for chat input

Adds a voice input button to the chat composer with three transcription engines:
in-browser Web Speech API, in-browser Whisper via @huggingface/transformers, and
a server-side OpenAI Whisper fallback gated on KANDEV_VOICE_OPENAI_API_KEY.

User-configurable from a new /settings/voice-mode page:
- Master on/off toggle (defaults to on; opt-out, not opt-in)
- Engine preference (auto / Web Speech / Whisper Web / Whisper Server)
- Language hint (auto-detect or BCP-47 pinned)
- Activation mode (click-to-toggle or hold-to-talk)
- Auto-send after transcription
- Whisper Web model size (tiny / base / small)
- Editable Cmd+Shift+M keyboard shortcut

Voice settings live under user_settings.voice_mode as a nested JSON object —
no schema migration. Setting changes broadcast via the existing
user.settings.updated WS event so any open chat tab updates live.

The mic button is rendered on the right of the composer alongside the send
button (round primary fill; destructive fill with pulse ring while recording).
On browsers without a usable engine (e.g. iOS Chrome over HTTP) the button
stays visible as a muted greyed icon — tapping it surfaces a toast with the
reason (typically a missing secure context).

Whisper Web runs in a Web Worker with fp16 decoder weights (avoids the
MatMulNBits scale-missing crash that q4/q8 hit on WASM). Models are pulled
from the onnx-community/* mirror and cached by transformers.js in the Cache
Storage on first use.

* fix: address voice mode PR review feedback and E2E selector clash

Bot-review fixes from #1159:
- Map http.MaxBytesError from c.FormFile to 413 instead of bare 400.
- Strip BCP-47 region suffix before passing language hint to Whisper Web.
- Recover from worker crash: terminate + null worker + reset ready flag so
  next init/transcribe creates a fresh worker instead of hanging.
- Read live store state via storeApi.getState() in voice-mode settings
  savers to avoid stale-closure clobbering concurrent settings updates.
- Detach Web Speech callbacks before abort() so trailing onend/onerror
  events don't mutate hook state after cancel().
- Spread caller init before method/body in voice-api fetch so a caller-
  supplied init can't override the multipart POST.
- Replace t.Fatalf with t.Errorf+return inside test HTTP handler goroutine
  (FailNow from non-test goroutine is unsafe).
- Add Enabled/AutoSend zeroing assertions in partial-update test so the
  bool-overwrite-on-PATCH behavior is locked in explicitly.

E2E fix:
- toolbar-overflow spec used button.rounded-full which now matches both
  the send and voice buttons (strict-mode violation). Switched to the
  submit-message-button testid for an unambiguous locator.

* fix: scope voice settings rollback + whisper worker error to changed keys

Round 2 of PR review feedback on #1159:
- voice-mode settings rollback now restores only the keys patched by the
  failing request (not the whole snapshot), preserving concurrent edits
  to unrelated voiceMode fields.
- Shortcut rollback diffs previous vs next and reverts only the changed
  keys (restore prior value or delete if absent), preserving other
  concurrent shortcut edits.
- Whisper worker error handler captures the worker reference at attach
  time and only clears refs when this.worker still matches it — a stale
  error from a previously replaced worker no longer nukes the active one.
- Update stale whisper-web-models JSDoc that still pointed at Xenova
  mirrors.

* fix: voice mode progress scale, hold-mode race, race-aware rollback

Round 3 of PR review feedback on #1159:
- Normalise Whisper progress to 0–1 in ensureWhisperClient (transformers.js
  emits 0–100, the button's display + the `ready: 1` convention expect
  0–1 — fixes the 5000% mid-download display).
- Claim driverRef synchronously at the top of finishCapture so concurrent
  pointerup + pointerleave invocations early-return instead of racing and
  clobbering a freshly-started recording's ref.
- Race-aware rollback: voice-mode and shortcut catch branches now skip
  reverting a key when latest[key] !== next[key]. A failed earlier request
  no longer overwrites a later successful save to the same key.
- Update the Worker wire-protocol comment to show onnx-community/whisper-base
  instead of the stale Xenova/* example.

---------

Co-authored-by: Kandev Agent <agent@kandev.dev>
2026-06-01 14:32:39 +01:00
Carlos Florêncio 83c800879a fix(web): stop right pane width from drifting across tasks (#1201) 2026-05-31 18:48:38 +01:00
Carlos Florêncio d76836d740 fix(web): stop changes panel flicker and restore planning chat streaming (#1197) 2026-05-31 18:43:17 +01:00
Carlos Florêncio d35f36ae73 fix(web): make left sidebar width global + steady monitor-switch settling (#1140) 2026-05-30 19:16:26 +01:00
Carlos Florêncio 956e0a551d feat(web): collapse mobile task search into a topbar icon (#1157) 2026-05-30 12:54:05 +01:00