* 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.
* 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.
* feat(web): show repo name and enable multi-select in pipeline view
* test(web): add e2e tests for pipeline view repo name and multi-select
* fix(web): tighten pipeline-view click handling and pipeline-view e2e wait sentinel
- Toggle selection on task-button click whenever isSelected is truthy, so
a lingering selectedIds entry (after multi-select is turned off) cannot
leave a visible checkbox with a broken click target.
- switchToPipelineView() now waits for [data-testid^=\pipeline-task-\]
rather than swimlane-container, which is shared with the kanban view
and could resolve before the pipeline view renders.
Addresses Greptile review on PR #831.
* refactor(web): extract shared use-session-actions hook from SessionTab
Move WS-level session lifecycle handlers (set primary, stop, resume,
delete) into hooks/domains/session/use-session-actions so the same logic
can power both the dockview tab context menu and upcoming mobile
controls. Dockview-specific panel removal stays local in SessionTab via
the onDeleted callback.
* feat(web): humanize task and session state labels
Mobile task sheet was rendering raw enum values like "IN_PROGRESS".
Add formatTaskStateLabel + formatTaskSessionStateLabel helpers and use
them in the mobile task sheet badge. Session state badge in the new
mobile sessions picker also reuses the formatter.
* feat(web): mobile session/terminal/repo pickers + multi-terminal support
Address parity gaps on mobile by introducing per-pane header pills that
open bottom-sheet pickers, scoped to the relevant view rather than
overloading the left task sheet.
Pickers:
- Sessions pill in chat header → bottom sheet with launch / set-primary
/ stop / resume / delete (with confirm). Pill shows agent label +
idx/total badge.
- Terminals pill in terminal header → bottom sheet with add / close
(with confirm) / switch. Replaces an earlier chip-strip prototype.
- Repos pill on the top bar (multi-repo only) → bottom sheet listing
task repositories; switching jumps to a session on that repo.
Each pill renders with a chevron-down icon + count badge so the
trigger reads as a button, not a status badge.
Multi-terminal:
- New useMobileTerminals hook wraps useTerminals and auto-creates a
first shell when the env reports zero.
- MobileTerminalPane multi-mounts PassthroughTerminal instances keyed
by terminalId so switching preserves scrollback. Active xterm
registers itself in a small mobile-active-terminal sender registry;
the existing key-bar reroutes input through xterm.paste() so Ctrl /
^C / arrows reach whichever terminal is currently visible, not just
the per-session default shell.
- Skip the WebGL renderer addon on mobile (new disableWebgl prop on
PassthroughTerminal). The WebGL atlas miscomputed glyph scaling
after the desktop→mobile responsive switch and rendered text at
several times the intended font size; the canvas renderer is correct
and fast enough for shell use.
Refactors:
- Left sheet reverts to a single-purpose task list (the sectioned-hub
variant was rolled back in favour of per-pane pickers).
- session-task-switcher-sheet-hooks extracted from the sheet to keep
it under the file-size lint cap.
* fix(web): address review feedback on mobile pickers + multi-terminal
Bots flagged a real first-mount bug and a handful of smaller items;
addressed each:
- mobile-terminal-pane: track xterm in state instead of a ref so the
active-sender registration effect re-runs once xterm is ready. With
the ref, the effect ran once on mount, found xtermRef.current === null
while PassthroughTerminal was still initializing (notably under the
ResizeObserver fallback path when the container starts at 0×0), and
silently never registered — leaving the key-bar wired to the default
shell instead of the visible terminal.
- send-shell-input: wrap the activeSender call in try/catch so a stale
registry entry (xterm GC'd mid-frame) falls back to the WS path
instead of dropping the keystroke and silently consuming modifiers.
Added tests for the activeSender routing branch (success, modifier
consumption, fallback on throw).
- mobile-sessions-section: remove the dead `showCloseConfirm ? "Delete" : "Delete"`
ternary and the unused prop on SessionActionsMenu.
- use-mobile-terminals: clear the auto-create guard when addTerminal()
rejects so the user gets a retry instead of an empty terminal list.
- mobile-repo-pill: sort taskRepos by position before falling back to
the first repo, so the pill label matches the picker's ordering when
no session is active yet.
- session-task-switcher-sheet-hooks: catch loadTaskSessionsForTask
rejection in handleSelectTask (was an unhandled rejection that left
the sheet open on backend errors), and stabilize useCallback deps in
handleWorkspaceChange by spreading the SheetNavOptions fields so a
fresh literal each render doesn't defeat memoization. Extracted
selectTaskWithoutPrimarySession to keep useSheetActions under the
function-size lint cap.
* fix(web): preserve task.repositories in sheet-hooks kanban mappers
mapSnapshotToKanban (workspace switch) and handleTaskCreated (newly
created task) only carried the first repository_id, dropping the full
TaskRepository array. The mobile repo picker (useTaskRepoCount +
MobileReposSection) reads task.repositories.length, so it silently
disappeared after a workspace switch and for freshly created
multi-repo tasks. Map the array with the same shape the SSR mapper
uses (id, repository_id, base_branch, checkout_branch, position).
Also address a small nit: wrap addTerminal in an arrow at the New
terminal button so the click MouseEvent isn't passed as the (unused)
first argument.
* fix(web): de-dup mobile terminal auto-create + add state-label tests + repo-tap toast
- use-mobile-terminals: move the auto-create-first-shell guard from
per-instance refs to a module-level Set keyed by environmentId.
MobileTerminalPane mounts useMobileTerminals three times for the
same session (the pane itself, the picker pill via
useActiveTerminalPillLabel, and the picker sheet content via
MobileTerminalsList — Vaul keeps it mounted while closed). With per-
instance guards, all three effects ran on first commit and each
fired createUserShell, racing into up to three shells. The shared
Set ensures only the first effect to observe an empty list creates,
with retry-on-failure preserved (delete + next render re-checks).
- mobile-repos-section: tapping a repo with no sessions now toasts a
hint to open the session picker, instead of silently doing nothing.
- state-labels: add tests covering known mappings, null/undefined
fallbacks, and unknown-state passthrough for both formatters.
* fix(web): single useMobileTerminals via context + tests + drop dead export
Address two follow-up review items:
- Lift useMobileTerminals to a single MobileTerminalsProvider owned by
MobileTerminalPane. The picker pill and the picker sheet's list now
read state from useMobileTerminalsContext instead of calling
useMobileTerminals themselves. The previous setup had three
independent useState-backed lists that drifted out of sync after
addTerminal/removeTerminal — the pane could show a stale active
terminal and the pill count could lag until a server WS push
reconciled them. The module-level guard (added in 9488bb0c) fixed
the creation race; this fix ensures the live list stays consistent
too.
- Drop the dead `subscribeActiveTerminalSender` export from the mobile
active-terminal registry. The keybar reads via getActiveTerminalSender;
notify/subscribe was speculative scaffolding with no production
consumer. Test trimmed accordingly.
- Add use-mobile-terminals.test.ts covering the auto-create logic
(initial create, skip when shells exist, skip while loading,
multiple instances trigger only one creation, retry after failure).
Module-level guard exposes a __resetAutoCreatedEnvironmentsForTest
helper so tests start from a clean slate.
* test(web): capture binary AttachAddon frames in mobile keybar e2e helper
The mobile-terminal-keybar Playwright spec asserted on captured
shell.input WS frames. After this PR, the on-screen mobile terminal is
a PassthroughTerminal with its own AttachAddon WebSocket, and the
keybar routes input through xterm.paste() — bytes go on the binary
terminal WS, not the kandev gateway's shell.input action. The capture
helper missed those frames and every keybar-asserts-on-frame test
failed (Ctrl+L, Shift+Tab, Esc, arrows, symbols, etc.).
Extend attachShellInputCapture to also decode binary terminal frames
(skipping the resize tag 0x01 prefix used by useSendResize) so the
existing assertions on `data` continue to work regardless of which
transport carried them.
* fix(web): keybar control chars + session pill propagation + terminal close ordering
Round-3 review feedback. Several real bugs that the previous rounds'
mocks hadn't caught:
- mobile-terminal-pane: switch from xterm.paste(data) to
xterm.input(data, true). paste() wraps the payload in bracketed-paste
escapes (\\e[200~…\\e[201~) under bash/zsh/fish, so every control
byte from the keybar (^C, arrow keys, Esc, ^D, Tab) was landing in
the shell as literal pasted text instead of as a control signal.
- session-mobile-layout: TaskChatPanel and the agent PassthroughTerminal
were rendering with the URL-derived `sessionId` prop instead of the
store-driven `effectiveSessionId`. The session picker would update
the top bar and terminal pane (which already read effectiveSessionId)
but the chat content and passthrough agent stayed on the old session
until a navigation refresh. Both consumers now follow
effectiveSessionId; the now-unused `sessionId` prop is removed from
MobileChatPanelContent + MobilePanelArea.
- mobile-terminals-section: rework handleConfirmClose. Stop the remote
shell first (and only mutate the picker on success), advance
setRightPanelActiveTab to a remaining terminal when the closed one
was active so terminalTabValue doesn't dangle, and call
releaseAutoCreatedEnvironment when terminals drop to 0 so the pane
recreates a default shell on next render instead of getting stuck
on "Starting terminal…". Extracted as a useTerminalCloseHandler hook
to keep the list under the function-size lint cap.
- session-task-switcher-sheet-hooks: handleTaskCreated now merges
session-derived fields (primarySessionId, sessionCount, etc.) from
the existing kanban entry, with meta.taskSessionId as the priority
source — so an "edit" call doesn't wipe the existing session and a
"create with session" call sets primarySessionId correctly.
Extracted into buildKanbanTaskUpsert + mergeSessionFields +
mapTaskRepositories helpers.
- session-task-switcher-sheet-hooks: mapSnapshotToKanban now carries
optional step fields (allow_manual_move, prompt, is_start_step,
show_in_command_panel, agent_profile_id) so the kanban store keeps
step capabilities after a workspace switch.
- use-mobile-terminals: add releaseAutoCreatedEnvironment(envId) — a
runtime API to clear the module-level guard for one env, used by the
picker when the user closes the last terminal.
- use-mobile-terminals.test: extract SESSION_ID constant.
* fix(web): restore mobile keybar modifier transforms with manual input routing
Round-3 swapped mobile from ShellTerminal → PassthroughTerminal, which
made AttachAddon auto-send xterm.onData straight to the WebSocket.
That bypassed sendShellInput's modifier transform — so OS-keyboard
input on mobile (e.g. tap Ctrl on the key-bar, then press "l") sent
the raw "l" instead of "\\x0c" (form feed). Multiple e2e keybar tests
caught this; my prior xterm.input(data, true) attempt only fixed the
key-bar tap path, not OS-keyboard typing.
Add `manualInputRouting` + `onWsReady` props to PassthroughTerminal
(plumbed through useWebSocketConnection / startReconnectLoop /
connectWebSocket). When `manualInputRouting` is true, AttachAddon is
constructed with `bidirectional: false` so it only handles PTY → screen,
and the consumer is responsible for forwarding xterm.onData itself.
Mobile TerminalSlot now:
- passes manualInputRouting=true and an onWsReady callback that
registers an active-terminal sender writing raw bytes to that
terminal's dedicated WS (replacing the prior xterm.input registry).
- attaches its own xterm.onData listener that calls sendShellInput,
which applies the key-bar's sticky Ctrl/Shift transforms before
forwarding via the registered sender.
Net effect: both key-bar taps AND OS-keyboard typing flow through
sendShellInput on mobile, so modifiers apply uniformly. Desktop
unaffected (default bidirectional AttachAddon path unchanged).
Also extracted teardownWebSocket into ws-reconnect to keep the
use-passthrough-terminal file under the line-count cap after the
new options.
* fix(web): tighten mobile sender registration + Set Primary + launch return
- mobile-sessions-section: drop the borrowed isSessionStoppable guard
on the "Set as Primary" item. The backend session.set_primary action
has no such restriction, so a COMPLETED or FAILED session was being
silently grayed out. Disable only when isPrimary or state isn't
loaded yet.
- mobile-terminal-pane: register the active key-bar sender only while
the dedicated WebSocket is OPEN. Previously the sender went into the
registry as soon as the tab became active, so during connect /
reconnect it would silently no-op instead of letting sendShellInput
fall through to its WS path. Track ws in state, drop it on close,
and gate registration on readyState === OPEN.
- session-task-switcher-sheet-hooks: early-return after a successful
launchSession so we don't redundantly call setActiveTask after
setActiveSession. Mirrors the existing "sessions exist" branch.
* refactor: move integrations to top-level settings with install-wide configs
Jira and Linear configurations are now install-wide singletons rather than
per-workspace duplicates, and the GitHub/Jira/Linear settings pages live
under /settings/integrations instead of being nested inside each workspace.
Issue and PR watchers stay per-workspace via a workspace switcher inside
each integration page. Backend ships one-shot migrations that promote the
most-recently-updated row and copy its secret onto the new global key.
* fix: address review feedback on integrations refactor
Fix migration crash on installs whose jira_configs predate the auth-health
columns by selecting only guaranteed-present legacy columns and falling back
to defaults for the rest. Cover the empty-legacy-table and pre-health-columns
upgrade paths with new store tests.
On the frontend, refactor useIntegrationEnabled to take string parameters
instead of an inline options object so getSnapshot stays referentially stable
across renders, and stop dispatching a misleading no-op sync event when
localStorage.setItem fails in private mode.
* fix: hydrate workspace list on integration settings pages
The integration pages wrapped their content in a fresh StateProvider, which
created a nested store scope with an empty initial state and shadowed the
settings layout's hydrated workspace list. WorkspaceScopedSection then saw
zero workspaces and rendered the "Create a workspace to configure this
integration" empty state even when the user had workspaces.
Drop the redundant StateProvider from the jira and linear pages so they share
the layout's store. The github page switches to StateHydrator so it can still
seed the GitHub OAuth status without resetting the rest of the store.
* refactor(integrations): flatten watcher tables, pick workspace in create dialog
Hiding watchers behind a workspace switcher made the list invisible at a
glance — a user with multiple workspaces had to flip between each one to
see what was configured. Replace the switcher with a flat table that lists
every watch across every workspace, with a Workspace column for context, and
move workspace selection into the create dialog so the user picks once when
the watcher is born.
Backend: jira list-issue-watches and github list-review-watches /
list-issue-watches accept the workspace_id query param as optional; when
omitted, the corresponding store ListAll* method returns rows across every
workspace. Per-row mutations still require workspace_id as the IDOR guard.
Frontend: useReviewWatches / useIssueWatches / useJiraIssueWatches gain a
three-state contract — string for one workspace, undefined for all
workspaces, null to skip fetching. Watch tables grow an optional
showWorkspace column. Watch dialogs accept an optional workspaceId prop;
when absent, the dialog renders a workspace dropdown for the create flow
(disabled when editing existing rows so refs aren't orphaned). Github
review-watch-dialog crossed the 600-line lint cap so the prompt editor
moved to review-watch-prompt-field.tsx.
* fix(integrations): make watcher step dropdown reactive across workspace switches
Switching to a workspace whose workflows had to be fetched left the workflow
step dropdown stuck — the user could pick a workflow but the step Select
either showed the previous workflow's steps or appeared inert. The cause was
the per-dialog useWorkflowSteps hook deferring its empty-state reset via
Promise.resolve().then(() => setSteps([])) to dodge a setState-in-effect
lint rule, which let stale steps survive into the next render and gave Radix
nothing to anchor the popover to during the in-flight fetch.
Hoist the hook into a shared useWorkflowSteps that resets via React's
official setState-during-render pattern, exposes a loading flag, and returns
a stepPlaceholder helper so the Select can render "Loading steps…" or "No
steps in this workflow" instead of an empty popover. The Step Select is now
also disabled while loading or when there are no steps, making the state of
the dropdown unambiguous.
* fix: address PR review feedback on integrations refactor
Backend:
- linear migration: add column-presence guard mirroring the Jira fix so
installs that upgraded from the original Linear schema (without org_slug
/ last_checked_at / last_ok / last_error) don't crash startup with
"no such column"; add TestStore_MigrateLegacyPerWorkspaceTable_PreHealthColumns
- jira/handlers.go: drop the now-unused errMsgWorkspaceIDRequired constant
that was failing golangci-lint
- jira/mock_controller_test.go (new): cover setAuthHealth happy-path and
invalid-JSON-rejection so the singleton-shaped payload is locked in
Frontend:
- useIntegrationAvailable: gate the auth-health probe on loaded && enabled
so disabled integrations don't poll /config every 90s
- use-workflow-steps: initialize loading to !!workflowId so edit-mode opens
show "Loading steps…" instead of "No steps in this workflow" before the
first fetch lands
- use-workflow-steps.test.ts (new): cover the empty/non-empty workflowId,
workflow switch, fetch error, and stepPlaceholder branches
- use-linear-enabled.test: tighten the legacy-key migration assertion to
pin the migrated value rather than just verifying key cleanup
---------
Co-authored-by: Kandev Agent <agent@kandev.dev>
* feat: add Linear integration
Mirrors the existing Jira integration so a workspace can connect to Linear
with a personal API key, browse and search issues, and start a Kandev task
from any issue. Backend uses Linear's GraphQL API with a workspace-scoped
config + secret store and a 90s auth-health poller; the frontend adds a
settings page, an issue dialog with state transitions, link/import buttons
in the task UI, and a paginated /linear browse page with a Start task flow.
* fix(linear): address PR review feedback
- Route httpListStates through Service.ListStates instead of clientFor
- Extract repeated workspace_id required string into a constant
- Log GetConfig errors during credential resolution instead of dropping them
- Clear popover error state on close in import + link buttons
- Pick the first workflow with steps in the quick task launcher (don't hard-pin to workflows[0])
- Reset useLinearEnabled state on workspace change via tag-based pattern
- Add cursor-pointer to anchor + Link elements per project convention
- Add tests: handlers_test.go, linear-api.test.ts, use-linear-enabled.test.ts, linear-issue-common.test.tsx
- Document internal/jira and internal/linear in CLAUDE.md/AGENTS.md
* fix(linear): address second-round PR review feedback
- Round Linear's fractional state position instead of truncating it via
int(math.Round(s.Position)) so adjacent fractional positions don't collapse
- Force LinearSettings to remount on workspace change with key={id} so the
previous workspace's config / form / testResult / teams cannot bleed into
the next workspace before the new fetches settle
- Broaden the legacy-value useLinearEnabled test to cover "true", "1", "yes",
and "legacy" so a regression to strict value === "true" would fail
* fix(linear): address third-round PR review feedback
- Propagate context.Canceled / context.DeadlineExceeded from ListStates in
GetIssue instead of swallowing them — a caller cancellation must not return
a partial-success response
- Drop the one-off contains() test helper in handlers_test.go and use
strings.Contains directly so the assertion is self-documenting
- Add getLinearIssue URL-shape test that locks identifier-as-path-segment
vs workspaceId-as-query-param so accidentally swapping them would fail
* test(linear): extract repeated literals in linear-api.test.ts
Pulls "ws-1", the API base URL, and the auth-method literal into module-
level constants so the assertions read as the route shape they care about
and the duplicate-strings ESLint rule (currently warn, threshold 4)
stays quiet as the file grows.
---------
Co-authored-by: Kandev Agent <agent@kandev.dev>
* feat(web): hide Jira buttons when disabled or auth failing
Hide the Jira import button on the task creation dialog and the "Link Jira" button on the task top bar when the workspace toggle is off, no Jira config exists, or the most recent backend auth probe failed. Centralizes the gating logic in a shared useJiraAvailable hook.
* fix(web): address PR review feedback on Jira availability gate
- Gate the auth probe on `loaded` from useJiraEnabled so it never fires when the toggle starts disabled
- Reset cached auth on workspace change so the previous workspace's `true` can't briefly leak into the new one
- JiraTopbarButton now consumes the shared useJiraAvailable hook instead of replicating the two-step gate inline
* fix(web): apply prettier formatting to use-jira-availability.ts
* fix(web): tag Jira auth state with workspaceId to avoid poll-tick flicker
Storing `{workspaceId, authed}` together and filtering at read time invalidates stale auth synchronously when the workspace switches, removing the need for a setAuthed(false) reset that was previously firing on every 90s poll tick. Adds a fake-timer test that asserts no false render slips through across a poll tick while auth stays healthy.
---------
Co-authored-by: Kandev Agent <agent@kandev.dev>
* feat(jira): add Jira integration for ticket browsing, import, and task linking
Adds a workspace-scoped Atlassian Cloud integration: per-workspace settings
(site URL, email, API token or session cookie), encrypted secret storage,
and a REST client for tickets, projects, search, and transitions. A
background poller probes credentials on a 90s cadence and the keepalive
runs immediately on save so the UI's connected indicator updates without
delay.
The /jira page provides ticket browsing with filter pills, custom JQL,
saved views (including JQL), and configurable task-launch presets that
prefill TaskCreateDialog. JiraLinkButton on the task top bar links an
existing task to a ticket by prepending its key to the title; the ticket
popover surfaces details, transitions, and re-auth prompts on session
expiry. Topbar shows separate icon buttons for GitHub and Jira when each
is configured.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(jira): address PR review feedback
- Token-based pagination (pageToken/isLast) replaces broken offset/total pagination
- Race-fix in service.clientFor: re-check cache after acquiring write lock
- sync.Mutex guards poller started/cancel state against concurrent Start/Stop
- Channel-based + atomic.Int32 test sync replaces time.Sleep polling in poller_test
- Async auth probe on SetConfig with detached context.Background goroutine
- Distinguish 4xx vs 5xx in httpSetConfig via ErrInvalidConfig sentinel
- Propagate 4xx APIErrors from ListTransitions in GetTicket
- Distinguish "not found" from real errors in jiraSecretAdapter.Exists
- Log secret Exists errors instead of swallowing
- walkADF: handle softBreak (was only hardBreak despite the doc-comment)
- Consolidate JIRA_KEY_RE in jira-ticket-common; remove duplicates
- ticket-row imports formatRelative/statusBadgeClass from common
- isFilterState type guard validates SavedView.filters shape
- task-presets readStorage preserves user's "cleared all" intent (returns [])
- task-presets-section gates draft sync on `loaded` to avoid wiping in-progress edits
- jira-api: caller `init` no longer overrides method/body via spread order
- jira-settings: disable Test button when no saved/typed secret
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(jira): record auth probe failure even when probe ctx expires
- RecordAuthHealth now uses a fresh context.Background-derived ctx with a
5s timeout for the UpdateAuthHealth DB write, so a probe that consumes
its full 15s deadline still persists the failure (greptile P1).
- SetConfig goroutine drops its redundant outer timeout — RecordAuthHealth
manages its own probe timeout, so context.Background is enough.
- Add Service.SetProbeHook so tests can synchronize on probe completion;
service_test waitForAuthProbe now blocks on the hook channel instead of
time.Sleep polling.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(jira): address coderabbit review feedback (round 3)
- url.PathEscape ticketKey in GetTicket/ListTransitions/DoTransition paths
to harden against malicious keys ('../../foo', 'KEY?override=x').
- factoryHit on the test fixture is now sync/atomic.Int32; the async probe
goroutine spawned by SetConfig races the test thread under -race.
- GetConfig logs Reveal failures (was silently dropped, leaving the UI to
show "no expiry info" indistinguishably from a freshly-saved cookie).
- DeleteConfig logs secret-delete errors instead of swallowing them.
- clientFor distinguishes a secret-store failure from "not configured" so
the UI doesn't render the configure-CTA for transient backend errors.
- Unify ErrNotConfigured wire code: HTTP and WebSocket both return
"JIRA_NOT_CONFIGURED" via a shared constant.
- JiraLinkButton strips a leading "PROJ-123: " before re-prefixing so
re-linking a task replaces the prefix instead of stacking it.
- jira-settings clears testResult on save/delete so the alert reflects
only the currently-saved credentials.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* test(jira): cover Reveal error path in clientFor
Reveal-error case is distinct from "no config row" — the former wraps
the underlying error, the latter returns ErrNotConfigured. Without
this test the changed contract has no regression coverage.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(jira): route secret adapter Set through Exists
Treating every Get error as "not found" turned a transient DB error on
an existing row into a constraint-violation Create that masked the real
cause. Reuse Exists so the "secret not found:" prefix is the only path
that falls through to Create.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* test(jira): cover jiraSecretAdapter.Set branching
Routing Set through Exists changed the contract: a transient DB error
in Get now propagates instead of cascading into a constraint-violation
Create. Add three tests so a future refactor cannot regress this without
CI catching it.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(jira): split Reveal errors and guard Save for new configs
- resolveCredentials now mirrors the clientFor split: a Reveal error
surfaces as `read jira secret: %w` with a warn log instead of
collapsing into "no token stored — paste one to test".
- disableSave gains the same missing-secret guard as disableTest so a
brand-new config can no longer POST an empty secret and leave the
workspace stuck with hasSecret=false.
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>
* feat(github): add /github page for PRs and issues
Dedicated page under /github lists pull requests and issues across the
authenticated user's repos with preset filters, custom GitHub search
queries, per-repo narrowing, saved queries, and one-click task launching
for any PR or issue.
Performance:
- Cache user PR/issue search responses for 30s with singleflight to
coalesce concurrent misses, so preset switching and pagination avoid
upstream round trips.
- Replace the per-row PR status fetch (N+1) with a batch endpoint
POST /api/v1/github/prs/statuses that fans out concurrently on the
backend with a semaphore and caches per-PR.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* style: apply Prettier formatting
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* fix: address review feedback on PR cache + status batching
- Harden searchCacheKey against separator collisions from
user-controllable customQuery (length-prefix encoding).
- Drop repositories.loaded hydration when inner workspace fetch fails
so the client refetches instead of trusting a partial snapshot.
- Guard useGitHubSearch with a monotonic request counter to drop
stale responses when filter+page changes overlap.
- Fix usePRStatuses teardown: depend only on composed key so a new
array identity per render doesn't cancel the in-flight batch.
- Correct godocs (duplicate, outdated state:open claim).
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* fix: address additional review feedback
- Restore state:open for issue watcher: buildIssueSearchQuery no
longer injects it as a default, so existing watches were starting
to surface closed issues. Inject state:open in buildIssueFilter so
only the watcher path is affected; the /github page presets already
set their own state qualifiers.
- countCheckResults now excludes in-progress checks from the denominator
so a `1/3` badge reflects pass rate on completed checks, not a mix of
running and failed. The pending state is still carried via ChecksState.
- usePRStatuses retries after a transient fetch error (only mark the
key as completed once the fetch resolves). This also fixes missing
badges under React Strict Mode's intentional double-mount, since the
first effect's cleanup fires before the response lands and the guard
stays unset for the second mount to retry.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* test(github): add tests for my-github hooks
Covers use-committed-query, buildParams from use-github-search,
use-saved-presets readStorage validation, use-known-repos recordForKey
accumulator semantics, and use-pr-statuses batch fetch lifecycle.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* fix: address review feedback on search cache and repo accumulator
- Clamp perPage before composing the search cache key so identical
pages (e.g. perPage=150 and =100) share a cache entry instead of
creating duplicates keyed on the raw value.
- Add resetKnownReposStore and call it on /github unmount so the
module-level repo accumulator doesn't carry across navigations.
- Rename use-pr-statuses test to match its assertion (it verifies
stale statuses are cleared when the key changes and fetch fails).
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* style: apply Prettier formatting
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* fix(web): distinguish blocked from dirty in PR mergeable badge
GitHub's blocked state (required reviews/checks/branch protection)
is not a merge conflict, so don't label it "Conflicts". Surface it
as "Blocked" with the pending tone; keep "Conflicts" for dirty.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix: address remaining /github code review comments
- Route PR status + batch through handleSearchError so ErrNoClient
surfaces as 503 instead of 500 (matches the search endpoints).
- Sort workflow steps by position in the quick-task-launcher default
pick, matching the convention in issue-watch-dialog.
- Make PresetItem keyboard-accessible: role=button, tabIndex, and
Enter/Space handling on the wrapping div (it contains a nested
delete button, so it can't itself be a button).
- Extract useRepoOptions out of useGitHubPageState to bring the hook
back under the 100-line complexity limit.
- Honor ctx.Done() when acquiring the PR-status batch semaphore so a
cancelled HTTP request releases queued goroutines instead of
running each upstream fetch to completion.
- Clarify parsePaginationQuery doc: the upper bound (100) is enforced
by clampSearchPage in the client layer, not here.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(web): address /github review follow-ups
- Merge duplicate @/lib/utils imports in pr-list.
- Use rel="noopener noreferrer" on external PR/issue links (codebase
convention) and add cursor-pointer per the UI guidelines.
- Rewrite onOpenSaveDialog as an explicit if-block so the callback
returns void rather than boolean | void.
- Use PaginationEllipsis in its own PaginationItem so the ellipsis
and the adjacent page link are separate list items instead of
visually fused into a single cell.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* refactor(web): derive QuickTaskLauncher dialog state from props
Replaces the useEffect+setState anti-pattern with useMemo-derived state
driven directly by the payload prop. Drops the eslint suppression that
was needed for react-hooks/set-state-in-effect.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
---------
Co-authored-by: Kandev Agent <agent@kandev.dev>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
* feat(ui): add responsive layouts to swimlane kanban content
Mobile gets swipeable columns with tab navigation and touch-friendly
drag sensors. Tablet gets 2-column horizontal scroll with snap.
Desktop keeps the existing CSS grid layout.
* feat(ui): add mobile task sheet and search bar components
Bottom sheet (vaul Drawer) for task preview with Edit/Delete/Open Session
actions. Compact search bar component for filtering tasks on mobile.
* feat(ui): wire mobile components into kanban board
Integrate MobileFab, MobileSearchBar, and MobileTaskSheet into the
kanban board. Skip desktop preview panel on mobile — card taps open
a bottom sheet instead of navigating directly.
* refactor(ui): optimize swimlane container for mobile
Hide swimlane headers for single workflow on mobile, remove horizontal
padding on small screens. Extract filterTasks and WorkflowItem to keep
the main component under the line limit.
* test: add mobile kanban E2E tests
8 Playwright tests covering mobile layout, FAB, search, column tabs,
bottom sheet, preview panel suppression, and swimlane header hiding.
Adds mobile-chrome project (Pixel 5) to playwright config.
* fix(ui): fix mobile E2E test failures in CI
Exclude mobile tests from chromium project (wrong viewport), add
startIndex to Embla carousel to fix initial scroll race condition,
fix strict locator for menu heading, use first step for swimlane test.
* fix(ui): fix create task dialog button layout on mobile
Make StartTaskSplitButton full-width on mobile, hide dropdown chevron
on small screens, and show a visible "Create only" button instead.
* fix(ui): match header button sizes and add icon to create-only action
Align hamburger menu button with changelog/health buttons by removing
explicit h-10 w-10 override. Add IconPlus to "Create without starting
agent" in both the desktop dropdown and mobile button.
* fix(ui): address code review feedback for mobile kanban
- Base hideHeaders on workflowFilter/total count, not filtered result
- Clamp mobile column index via useMemo when steps change
- Fix wrong assertion in preview panel test (was checking toBeVisible)
- Separate altDisabled from splitDisabled (no agent needed for create-only)
- Add visible plan mode button on mobile (was hidden behind dropdown)
- Wait for mobile-kanban-layout in page object goto()
- Extract computeBaseDisabled to reduce complexity
* fix(test): fix flaky mobile E2E tests in headless Chrome
- Use instant jump (scrollTo with jump=true) for tab-triggered carousel
navigation instead of animated scroll that doesn't complete in headless
- Add data-testid to swimlane header to avoid ambiguous Badge locator
matching column tab buttons
- Add timeout to viewport assertion for resilience
* fix(ui): address review feedback and fix carousel reinitialization
- Stabilize Embla options to prevent carousel reinit on tab change
(startIndex was causing re-initialization, resetting scroll position)
- Guard programmatic scrolls from triggering select callback revert
- Remove PointerSensor from mobile DnD sensors (conflicts with touch)
- Fix plan mode button using wrong disabled prop on mobile
- Use literal name matching in columnTab locator (avoid regex injection)
- Use toHaveCount(0) for stricter preview panel absence assertion
* fix(ui): use pointerDown gating for carousel swipe sync
Embla's scrollTo doesn't work in headless Chrome, causing the select
event to fire with index 0 and reset the active tab. Fix by only
syncing Embla→tabs on user-initiated pointer interactions (swipes),
not on programmatic scrollTo calls.
Update E2E test to verify tab active state instead of carousel
viewport positions, which are unreliable in headless environments.
* fix(test): add data-testid and data-active to column tabs
Use explicit data attributes for more reliable test targeting
instead of getByRole and CSS class assertions.
* refactor(ui): use touch sensors for tablet and deduplicate getTasksForStep
* fix(ui): improve spacing for stats and settings buttons in mobile menu