* feat: add GitHub token injection for remote executors and Docker session resume
- Add global GitHub access token configuration (backend + frontend)
- Inject GitHub token into Docker and Sprites executors via env vars and Git credential helper
- Transition Docker executor from host mounting to repository cloning
- Implement Docker session resume (reconnect to existing containers after backend restart)
- Add shell validation using exec.LookPath in agentctl
- Add scriptengine providers for agent install and agentctl placeholders
* fix: format github-status.tsx with prettier
* fix: resolve lint issues (goconst, errcheck, file-length)
* fix: address code review findings
- Fix resolveGHCLIToken to check both GITHUB_TOKEN and GH_TOKEN guards
- Fix credential helper to use \\ fallback
- Change /bin/bash to /bin/sh in credential helper for Alpine compat
- Strip embedded token from .git/config after clone in prepare scripts
- Extract buildLaunchAgentRequest helpers to stay under 80-line limit
- Add tests for GitHubAuthProvider, injectTokenIntoURL, reconnect validation
- Add GH_TOKEN-only test case for resolveGHCLIToken
* refactor(executor): extract GitHub credential resolution to separate file
Move injectGitHubToken, injectGitHubTokenFromCLI, resolveRemoteCredentials,
resolveAuthSecrets, methodIDToEnvVar, resolveGHCLIToken, and detectGHToken
out of executor_execute.go (was 803 logical lines, over the 800-line limit
after merging main) into a new executor_credentials.go file. Also adds the
nil-guard on secretStore in resolveAuthSecrets that was flagged by review.
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(backend): add session-scoped message search endpoint
Adds SearchMessages across repository/service/handler layers and a new
WebSocket action "message.search". Returns lightweight hits with a
60-char snippet around the first match. Session-scoped only: results
never leak across sessions. Uses dialect-aware LIKE/ILIKE so it works
on SQLite today and Postgres later.
* feat(web): add shared panel search bar and Ctrl+F hook
Introduces the reusable PanelSearchBar overlay and usePanelSearch hook
used by the per-panel search features. The hook listens on the window
and fires only when focus is within the panel root, preventDefaults the
browser find-in-page, and wires Esc to close. PanelRoot becomes a
forwardRef so panel components can expose their root element to the
hook. Adds SHORTCUTS.FIND_IN_PANEL plus shared highlight/flash styles.
* feat(web): add Ctrl+F search to terminal panels
Mounts the xterm addon-search on both passthrough and shell terminals,
exposing next/prev plus case-sensitivity and regex toggles. Invalid
regex surfaces an inline error without firing the search. Because xterm
consumes keystrokes before React, the Ctrl/Cmd+F intercept is added to
attachCustomKeyEventHandler so the shortcut opens the bar from inside
the terminal. Results counter comes from onDidChangeResults; decorations
clear on close.
* feat(web): add Ctrl+F search to plan panel
Adds a custom TipTap extension that builds a ProseMirror DecorationSet
over the document and exposes setPlanSearchQuery / planSearchNext /
planSearchPrev / clearPlanSearch commands. The current match gets an
extra class and scrolls into view. Match count is read from the plugin
state via transaction subscription, so edits to the document update the
counter automatically. Find-only — no replace.
* feat(web): add Ctrl+F search to session chat panel
Wires the new backend message.search action into a debounced client
hook and renders a hits dropdown under the search bar, with the query
highlighted in each snippet. Clicking a hit scrolls the message into
view and flashes it. When the target message is older than what is
currently loaded, the hook loops through loadOlder pages until the
message renders, making search work across lazy-loaded history without
needing to scroll-and-wait manually.
* fix(web): lazy-load xterm addon-search to avoid SSR crash
addon-search is published as a UMD bundle that references `self` at
module load. Statically importing it breaks the Next.js static
prerender of /. Deferring the import to an effect keeps the module out
of the SSR graph without changing runtime behaviour.
* fix(web): propagate plan editor via state so plan-search hook re-runs
The plan-search hook subscribed to editor transactions inside a
useEffect that depended on a ref object. Because ref.current changes do
not trigger effect re-runs, if the Editor instance was not yet ready
when the hook first mounted, the transaction handler never attached and
the match counter stayed at 0 / 0. Tracking the editor via state (in
addition to the ref) causes the deps to change when the Editor becomes
available, so the handler attaches and the counter updates.
* feat(web): allow overriding message list renderer via ?renderer= query
Exposes the "native" / "virtuoso" choice at runtime so E2E coverage can
exercise both paths without a redeploy or code change. Defaults to the
existing "native" strategy when no override is present.
* test(web): add Playwright coverage for panel-scoped Ctrl+F search
Adds an e2e/tests/search/ suite with five specs:
- panel-search-shared: cross-panel contract (Ctrl+F opens, autofocus,
Esc closes, close button, state reset on reopen) parametrised over
session, plan, and terminal.
- terminal-search: xterm addon-search integration — key intercept
without leaking "f" to the PTY, case + regex toggles (incl. invalid
regex error), Esc does not reach the shell, backspace adjusts count.
- plan-search: TipTap plugin — highlight count, exactly one current
match, counter, Enter / Shift+Enter wrapping, close clears
decorations, no case/regex toggles in plan bar.
- session-search: WebSocket-backed hits list, "No matches" state,
click-to-flash, cross-session isolation, close, and virtuoso renderer
coverage via the new ?renderer= override.
- panel-focus-routing: bar mounts inside the focused panel only.
Three tests capture GIFs for the PR description via prCapture when
CAPTURE_PR_ASSETS is set (one per panel). Helpers live in
e2e/helpers/panel-search.ts and e2e/helpers/seed-session-messages.ts;
SessionPage gains search-bar locators.
* fix(web): prevent plan-search update loop on spurious editor transactions
TipTap fires `transaction` on every cursor move/click. The match-info
handler allocated a fresh `{current, total}` object each time, so React
always saw new identity, re-rendered, and could cascade with downstream
effects (e.g. `rehydrateCommentMarks`) into a "Maximum update depth
exceeded" loop. Use a functional setState with equality bail-out so the
handler only schedules a render when the counter actually changes.
* fix(web): retry terminal search once xterm addon-search finishes loading
The SearchAddon is dynamic-imported and was stored in a ref. The
query→findNext effect called `addonRef.current?.findNext(...)` — a
silent no-op if the addon wasn't loaded yet — and its deps didn't
include addon readiness, so a query typed before the import resolved
was dropped forever. Track the addon via state so the search effect
re-runs when it becomes available (and re-binds cleanly if the terminal
is ever re-created). Also deduplicate match-info updates via functional
setState + equality check.
* fix(web): route Ctrl+F to the last-clicked panel when focus is ambient
The chat panel's message list has no focus-capturing child, so clicking
on a message left `document.activeElement === <body>` and the existing
`isFocusWithin` gate silently fell through to the browser's native find.
Fix with two complementary mechanisms:
- `usePanelSearch`: track the most-recently-clicked panel via a
window-level pointerdown capture listener. When focus is ambient
(body/html), that panel claims Ctrl+F. Window-level capture dodges
any descendant `stopPropagation` that would skip a container-level
listener.
- Chat panel root: tabIndex=-1 + onMouseDown that focuses the root when
the click target isn't already focusable. Normal path matches how
plan/terminal already work (TipTap/xterm capture focus on click).
* feat(web): show agent profile name and logo in session search hits
The hit list showed uppercase "AGENT"/"USER" with vivid blue/green
colors that clashed with the app's muted metadata aesthetic. Replace
with the actual author identity:
- Agent hits render the profile name (falling back to "Agent") with
the profile's <AgentLogo> inline — same mechanism session tabs use.
- User hits render "You" with a subtle `text-primary/80` tint that
echoes the user bubble's `bg-primary/30`, keeping a light color
differentiator without reintroducing loud blue/green.
Added `useSessionAgentIdentity` (plus a `useSessionAgentProfile` helper)
that reads from the store via two primitive selectors — the id, then
the profile — and composes the final object inside `useMemo`. A naive
single selector returning `{label, name}` triggers the "getSnapshot
should be cached" infinite loop in `useSyncExternalStore`.
* style: apply prettier formatting to search files
* fix(web): inline resolveStrategy in useMemo to satisfy react-hooks/use-memo
* fix(backend): escape LIKE metacharacters in message search
User query of `%` previously built LIKE pattern `%%` which matched every
message in the session; `file_name` also matched `filename`, `file name`,
etc. Escape the three LIKE metacharacters (`\`, `%`, `_`) before wrapping
the pattern, and declare `ESCAPE '\'` on the clause. Add regression tests
covering wildcard literals and the literal-percent happy path.
* fix(backend): slice search snippet in rune space to keep UTF-8 valid
`buildSnippet` indexed and sliced on byte offsets, so content containing
multi-byte characters (emoji, CJK, accented letters) could be cut
mid-rune — `encoding/json` then replaced the invalid bytes with `\ufffd`
in the websocket response. Switch to rune slicing and per-rune
case-folding via `unicode.ToLower`; this also avoids the
`strings.ToLower` byte-length-change pitfall (e.g. Turkish İ, German ß).
Add a handler test covering emoji, CJK, and large multi-byte content.
* fix(web): cancel backfill loop when user clicks a newer session hit
`setActiveHit` awaits up to 40 `loadOlder()` iterations when the target
message isn't in the DOM. If the user clicked a second hit while those
awaits were in flight, the in-progress loop kept running and eventually
scrolled to the original target, overriding the user's intent. Gate the
loop on a generation counter so only the most recent call's backfill
drives the scroll.
* fix(web): gate plan-search scroll side-effect on dispatch
TipTap invokes commands with `dispatch === undefined` during `can()`
checks to test applicability (e.g. before enabling a menu item). The
next/prev commands called `scrollToMatch` outside the `if (dispatch)`
guard, so a dry-run invocation would scroll the editor to a match the
user didn't navigate to. Move the scroll inside the dispatch block.
* fix(web): address review nits on search UX
- formatTime now checks Number.isNaN(d.getTime()) before formatting;
Invalid Date doesn't throw, so the catch branch was unreachable and
hits with a malformed created_at rendered the literal "Invalid Date".
- prefers-reduced-motion now silences the .search-flash animation
alongside the other custom keyframe animations.
- planScript escapes `title` symmetrically with `content` so callers
passing arbitrary strings don't produce malformed JSON.
* fix(web): fold description into opts to keep seedTask under param limit
`seedTask` had 6 parameters, violating the repo's 5-parameter cap and
failing CI lint. Fold `description` into the existing `opts` object and
update all callsites.
* test(web): add unit tests for useSessionSearch + remove redundant ternary
Cover the non-trivial hook logic called out in review: debounced fetch
coalescing, request-ID cancellation for superseded queries, null
sessionId short-circuit, and close() resets. Also simplify the
`matches.length ? 0 : 0` ternary flagged as a dead branch — both sides
were already 0.
* fix(web): exclude PanelRoot (tabindex=-1) from the focus short-circuit
`target.closest("[tabindex]")` was matching PanelRoot itself (which has
`tabIndex={-1}` so it can receive programmatic focus), causing the guard
to early-return and the `panelRef.focus()` call to never fire — defeating
the Ctrl+F focus-routing that this handler was added for.
Narrow the selector to `[tabindex]:not([tabindex='-1'])` so it only
short-circuits on genuinely interactive elements.
* fix(web): read isReadOnlyMode from live ref in shell-terminal key handler
`attachCustomKeyEventHandler` is called once and has no dispose hook, so
the inner handler keeps its initial closure forever. Other values
(sessionId, send, onFindInPanel) are read via `stateRef` so they stay
current, but `isReadOnlyMode` was captured by closure — if the prop ever
toggled mid-session, the Cmd+Arrow→Home/End path would continue firing
against a read-only terminal. Include it in the state ref for
consistency.
* fix(web): flatten doubly-nested seedTask opts in virtuoso spec; drop unused MODIFIER
The regex-based transform in 7152b936 over-nested one call in
session-search.spec.ts — `opts.description` received an object rather
than a string, so the virtuoso renderer test never received a proper
task description. Flatten it.
Also remove MODIFIER imports/declarations in terminal-search.spec.ts and
panel-search-shared.spec.ts that were left dangling behind `void` suppressors.
* fix(web): abort session-search backfill loop on close/unmount
Move reset logic into `close()` so the lifecycle is explicit (and avoids
the set-state-in-effect lint path the previous reset effect triggered).
Bump the `activeHitGenRef` generation counter on both `close()` and
unmount so an in-flight `setActiveHit` backfill can no longer issue
`loadOlder()` calls for a hidden UI.
Also extract `useDebouncedSearch`, `useSetActiveHit`, and a
`focusMessageElement` helper to keep `useSessionSearch` under the
100-line function cap.
* perf(web): skip PanelSearchBar debounce for session search
Session search already debounces the WS call inside `useDebouncedSearch`
(180ms), but the shared bar adds its own 150ms debounce on top — giving
a 330ms keystroke→request latency instead of the intended ~180ms.
Pass `debounceMs={0}` so the bar forwards queries immediately and the
hook-level debounce is the single source of timing truth.
* feat(github): configurable quick-action presets and PR branch checkout
- Add workspace-scoped action presets (icon, label, hint, prompt template) for PRs and Issues, editable from GitHub settings with reset-to-defaults.
- When starting a task from a PR, check out the PR head branch directly in the worktree instead of branching off main.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(github): extract action-preset service to keep service.go under line limit
Splits the preset CRUD methods out of the oversized service.go and applies prettier formatting to three frontend files flagged by CI.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(github): address review feedback on action presets
- Prevent infinite refetch loop when fetching presets fails by tracking attempted workspaces in a ref instead of re-entering the effect on every loading-state change.
- Drop unused savePR/saveIssue helpers from the hook; only save() is consumed.
- Reorder the init spread in updateGitHubActionPresets/resetGitHubActionPresets so callers can't accidentally overwrite method/body.
- Tighten the GitHubActionPreset.icon type with string & {} so known icon keys keep autocomplete.
- Sharpen the eye-icon test to assert the exact IconEye mapping.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(github): address second round of review feedback
- Seed preset editor drafts from current store state via lazy initializers so a warm mount shows the user's saved customisations instead of silently reverting to defaults when Save is clicked.
- Guard Reset button with its own in-flight state to prevent double-firing.
- Stop freezing resolved defaults into the stored row on a partial update; persist only what the store has for the untouched kind so future default improvements still reach those workspaces.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* refactor(github): tighten quick-actions editor UI
Replace the stacked PR/Issue editors and multi-line preset cards with a tabbed layout and single-line rows. Each row shows icon · label · hint inline; an "Edit prompt" toggle reveals the template textarea only when needed. Drops the redundant section headings, up/down reorder buttons, and verbose template helper label (hint moved into the textarea placeholder).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(github): widen icon select trigger so the icon is visible
The icon select trigger had a fixed 40px width which the SelectTrigger's built-in chevron, padding, and icon gap all competed for, clipping the icon out of view. Drop the width override so the trigger sizes to its content.
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(github): add issue watcher with task creation and auto-cleanup
Add a GitHub Issue Watcher feature that polls for new issues matching
configurable filters and automatically creates tasks for them. This
complements the existing PR Review Watcher with the same patterns.
Backend:
- Issue watch CRUD (models, store, service, controller, WS handlers)
- Poller integration with configurable intervals
- Event-driven task creation via orchestrator
- Repository resolution and worktree support for issue tasks
- Auto-cleanup of tasks for closed issues (CleanupClosedIssueTasks)
- GetIssueState client method across all implementations
- Default branch auto-detection from cloned repo HEAD
Frontend:
- Issue watch settings UI (table, dialog, placeholders)
- Issue link icon on task cards (green circle-dot)
- API client, store slice, and hook for issue watches
- SSR and WS hydration of issue metadata
* fix: resolve golangci-lint errors (goconst, nestif, dupl) and prettier formatting
* fix(github): fix issue watch update shape, trigger key, and label quoting
- httpUpdateIssueWatch now returns the full IssueWatch object instead of
{updated:true} so the frontend store correctly refreshes after edits
- httpTriggerIssueWatch returns new_issues_found to match TriggerIssueResponse
type; was returning new_issues so the toast count was always 0
- buildIssueFilter uses conditional quoting instead of fmt.Sprintf %q to
avoid Go escape sequences in GitHub search qualifiers
Co-authored-by: João Salavisa <undefined@users.noreply.github.com>
* fix: resolve eslint warnings (max-lines-per-function, no-nested-ternary)
* test(github): add issue watcher tests and fix review feedback
- Add TestInterpolateIssuePrompt (4 cases: empty/all/none/partial)
- Add TestCreateIssueTask (3 cases: skip/reserve+assign/release on fail)
- Add TestShouldDeleteIssueTask (4 cases: open/closed+sessions/closed/error)
- Fix WS response key new_issues -> new_issues_found
- Fix fmt.Printf -> fmt.Fprintf(os.Stderr) in store.go
- Add CleanupClosedIssueTasks call in WS trigger handler
* fix: address PR review feedback and CI failures
- Split test files to fix revive file-length-limit (service_test.go,
event_handlers_github_test.go)
- Add session checker guard in CleanupClosedIssueTasks
- Normalize poll intervals on UpdateIssueWatch
- Add cleanup call in TriggerAllIssueChecks loop
- Log cleanup errors in controller and WS handler
- Prefer refs/remotes/origin/HEAD in detectGitDefaultBranch
- Add aria-label to issue task icon
- Fix TypeScript type errors: add linkedIssues to GitHubSlice,
fix hook exports, remove invalid imports
* fix: split test files exceeding revive limit and clean up minor issues
- Split service_test.go (883→779 lines): move TestTriggerPRSync_* to
service_pr_sync_test.go
- Split event_handlers_github_test.go (806→677 lines): move
TestCreateReviewTask_* and helpers to event_handlers_github_review_test.go
- Remove redundant s.taskSessionChecker nil-check inside shouldDeleteIssueTask
(early guard at CleanupClosedIssueTasks already ensures it is non-nil)
- Remove unreachable dead guard in detectAndPersistDefaultBranch
(repo.DefaultBranch is always "" at that call site)
- Fix bogus #0 issue badge in task-session-sidebar.tsx when issueNumber is absent
Co-authored-by: João Salavisa <undefined@users.noreply.github.com>
* fix: address Claude review — poll clamp and prompt injection
- Handle v <= 0 in UpdateIssueWatch poll interval (fall back to default)
- Remove {{issue.body}} from default template to prevent prompt injection
- Update test expected output for new default template
* fix: remove {{issue.body}} from frontend DEFAULT_ISSUE_WATCH_PROMPT
Mirrors the backend fix — the UI pre-fills the prompt so the backend
fallback was never reached. The placeholder stays in
ISSUE_WATCH_PLACEHOLDERS for explicit opt-in.
* feat(github): add isIssueWatch sidebar filter dimension
Add isIssueWatch boolean to KanbanState, TaskSwitcherItem, and sidebar
filter registry — mirroring the isPRReview pattern. Issue-watch tasks
(created from issue_watch_id in metadata) can now be explicitly filtered
in the sidebar, just like PR review tasks.
Files changed:
- sidebar-view-types: add isIssueWatch dimension
- filter-dimension-registry: add Issue watch filter meta
- apply-view: add isIssueWatch extractor
- kanban types: add isIssueWatch field
- TaskSwitcherItem: add isIssueWatch field
- use-all-workflow-snapshots: derive isIssueWatch from metadata
- ssr/mapper: derive isIssueWatch from metadata
- task-session-sidebar: pass isIssueWatch, extract helpers to fix complexity
* fix: correct deleteIssueWatch response type to match backend
Backend httpDeleteIssueWatch returns {"deleted": true}; the frontend
type was { success: boolean } — a mismatch with no functional impact
(return value is ignored) but wrong per TypeScript contract.
Co-authored-by: João Salavisa <undefined@users.noreply.github.com>
* refactor: extract metadata helpers to shared lib/metadata-utils.ts
- Move isPRReviewFromMetadata, isIssueWatchFromMetadata, and
issueFieldsFromMetadata to a shared module to eliminate duplicate
definitions (sonarjs/no-identical-functions).
- Fix deleteIssueWatch response type: { success: boolean } -> { deleted: boolean }
to match the actual backend response.
* style: fix prettier formatting
* test(web): add tests for isIssueWatch filter dimension and metadata-utils
Co-authored-by: João Salavisa <undefined@users.noreply.github.com>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
* style: fix prettier formatting in test files
* fix(github): cascade-delete issue watch tasks on watch deletion and add isPRReview to SSR mapper
- DeleteIssueWatch now wraps in a transaction and deletes related
github_issue_watch_tasks rows before removing the watch; prevents
orphaned dedup rows accumulating after a watch is deleted
- snapshotToState in mapper.ts now also sets isPRReview from metadata,
matching the mapping already done in use-all-workflow-snapshots.ts
Co-authored-by: João Salavisa <undefined@users.noreply.github.com>
* style: extract duplicate string constant in metadata-utils test
* style: extract repeated WS error strings into constants in handlers.go
goconst min-occurrences: 3 — 'invalid payload' (6x) and 'id required' (4x)
both exceed the threshold. Extract to package-level constants.
Co-authored-by: João Salavisa <undefined@users.noreply.github.com>
* style: extract repeated issue key strings into constants in event_handlers_github.go
goconst min-occurrences: 3 — 'issue_number' (9x) and 'issue_watch_id' (5x)
both newly introduced in this PR exceed the threshold.
Co-authored-by: João Salavisa <undefined@users.noreply.github.com>
* style: fix prettier formatting in metadata-utils test
---------
Co-authored-by: Kandev Agent <agent@kandev.dev>
Co-authored-by: claude[bot] <41898282+claude[bot]@users.noreply.github.com>
Co-authored-by: João Salavisa <undefined@users.noreply.github.com>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
* feat(web): add sidebar view types and apply-view engine
Introduces the typed representation of a sidebar view (filter clauses,
sort spec, group key) and the pure engine that applies a view to a list
of tasks to produce a filtered, sorted, grouped result.
* feat(web): add built-in sidebar view presets
Provides the four shipped presets (All tasks, No PR reviews, Active,
Archived). The 'No PR reviews' preset filters out PR-watcher tasks that
would otherwise crowd the sidebar.
* feat(web): persist sidebar views in ui store
Adds sidebar view state to the ui slice with draft semantics for saved
views, localStorage persistence of active view + user-created views, and
root store wiring.
* feat(web): add filter dimension registry and clause editor
Registry of filterable task dimensions (status, workspace, workflow, task
type, etc.) plus the primitive UI pieces — clause editor, sort picker,
group picker, and a hook that supplies operator and value options for a
selected dimension.
* feat(web): add sidebar filter bar, popover, and view manager
Composes the filter primitives into the user-facing surfaces: the top
filter bar, the popover that hosts clause editing and sort/group
selection, the chips that switch between views, and the manager for
saving, renaming, and deleting user views.
* refactor(web): extract NewTaskButton component
Pulls the Create Task button and its command-palette registration into a
standalone memoized component in preparation for reuse across sidebar
surfaces.
* feat(web): integrate filter/view system into task sidebar
Wires the filter bar, view chips, and apply-view engine into the task
sidebar (desktop and mobile sheet). Updates task-switcher to consume the
grouped list from the engine and adds mock data for tests and previews.
* test(web): add e2e coverage for sidebar filter/view system
Page object for the filter popover plus Playwright specs covering built-in
presets, clause editing, sort/group, saved-view draft semantics, and
persistence across reloads.
* refactor(web): trim sidebar built-in views to a single seeded default
Drop the "No PR reviews", "Active", and "Archived" built-in view presets
and the isBuiltIn protection flag. The sidebar now seeds a single "All
tasks" view on first load; users can rename, overwrite, or delete it like
any other view. Deletion is guarded against removing the last remaining
view so the user can never lock themselves out.
* fix(web): humanize executor type labels in sidebar filter and grouping
The executor-type filter dropdown and group-by label were rendering raw
backend identifiers like "worktree" and "local_docker". Route both
through the existing getExecutorLabel() helper so the sidebar shows
"Worktree" and "Local Docker" like the rest of the app.
* feat(web): replace "Has PR" filter with "Is PR review"
The old dimension matched any task with an associated PR, including
tasks a user manually linked. The new dimension isolates tasks created
by the GitHub PR-review watcher specifically, which is what the filter
is useful for — hiding a wall of auto-generated review tasks.
The flag is derived from the review_watch_id metadata key the backend
already stamps onto watcher-created tasks, threaded through the kanban
snapshot and into the sidebar TaskSwitcherItem. A small migration drops
unknown dimensions (including the old "hasPR") from stored views so the
popover does not crash when reading legacy data.
* fix(web): tighten sidebar filter popover layout
Reduce the popover width (w-96 → w-[22rem]), trim section padding
(p-3 → p-2.5) and label margins (mb-2 → mb-1.5), and drop the
"No filters applied." placeholder. Extract a shared SectionLabel to
keep the three section headers visually aligned.
* feat(web): show workflow step color dot in sidebar filter dropdown
Surface the step color on the workflow-step filter's value dropdown so
each option renders with the same coloured circle used elsewhere (task
switcher, kanban column, command panel). Adds an optional color field
on filter value options; only workflow-step options populate it today.
* feat(web): replace cramped in/not-in filter editor with a multiselect dropdown
The previous "in/not in" value input rendered selected values as tiny
10px pills inside a fixed-height row that overflowed and truncated as
the list of options grew. Replace it with a combobox-style multiselect
(Popover + Command) that shows a compact summary ("N selected" or the
single label with its color dot) and opens a searchable, scrollable
list with check indicators on click.
* fix(web): show workflow and step names (not UUIDs) in sidebar grouping
Group-by Workflow and Workflow Step rendered raw UUID strings as group
headers because `TaskSwitcherItem` did not carry the resolved names.
Pass `workflowName` and `workflowStepTitle` through `toSidebarItem` and
prefer them in the group extractors.
* fix(web): drop redundant true/false selector on boolean sidebar filters
Boolean dimensions (PR review, Archived, Has diff) previously exposed
both an operator (is / is not) and a value selector (true / false),
which gives four user inputs for two real states. Hide the value input
for boolean clauses and always store `true`; negation is expressed via
the operator. Rename "Is PR review" to "PR review" since the dimension
label no longer needs to read as a question.
* refactor(web): drop Sort by Repository and relabel State to Status
Sorting by repository duplicates what Group by Repository already
offers, and "State" leaks a backend term into the picker. Remove the
repository sort key (comparator, type, picker option) and rename the
state option to "Status". Stored views referencing the removed key are
migrated back to `state` at load time so the popover does not crash.
* fix(web): tighten sidebar filter popover section spacing
The FILTERS, SORT, and GROUP BY section labels each sat above their
controls with both padded containers (p-2.5) and a 6px bottom margin,
leaving visible dead space above each header. Reduce container padding
to p-2 and label margin to mb-1 so sections read as a compact stack.
* chore(web): apply prettier formatting drift in sidebar-filter area
* revert(web): undo store.ts prettier reformat to stay under max-lines cap
The packed single-line UIA declaration in store.ts is load-bearing for
the 600-line ESLint cap: the previous chore commit unpacked it to 9
lines and pushed the file to 609 lines, turning the max-lines warning
into a lint failure. Restore the packed layout (the /* prettier-ignore */
comment is there for exactly this reason).
* refactor(web): label boolean filter ops as Show/Hide instead of is/is not
* fix(web): pull section labels flush to sidebar filter popover borders
* refactor(web): inline rename and save-as into sidebar view header row
* refactor(web): split ViewHeaderRow into mode, display, and action subcomponents
* feat(backend): persist sidebar views in user settings
Adds a SidebarViews field to UserSettings (stored in the existing
users.settings JSON blob) plus validation in the service layer:
views are capped at 50, ids and names must be non-empty and unique.
* feat(web): hydrate and sync sidebar views to backend
Moves the source of truth for saved sidebar views from localStorage
to the user settings endpoint. Views hydrate from SSR on first load;
create/rename/overwrite/delete/duplicate/toggle-collapsed all PATCH
the backend with optimistic updates and revert-on-failure. The
active view id and draft remain per-device in localStorage. A
sync bridge surfaces PATCH errors as toasts and runs a one-time
migration to upload any pre-existing local views.
As a side effect, localStorage writes and PATCH side effects are
moved out of immer producers to avoid proxy-serialization risk.
* chore(web): apply prettier to store.ts sidebar actions block
* fix(web): run migrateView on SSR-hydrated sidebar views
Defensively drop clauses with unknown dimensions and reset stale sort
keys when views come from the backend, matching the localStorage load
path. Also notes the default-state module and sidebar views in the
store structure docs.
* docs: note default-state module and sidebar views in store structure
* fix(web): type sidebar view storage wrappers properly
Drop the as-unknown-as-SidebarView and as-never casts at call sites by
making the local-storage getters/setters accept the domain type and do
the JsonValue cast once inside the wrapper.
* refactor(web): stop PATCHing on sidebar group collapse toggles
collapsedGroups is per-device visual state; routing it through
mutateViews made every expand/collapse click fire a user-settings
PATCH. Keep it local (in-memory + localStorage cache); the next real
view mutation picks up whatever state is current.
* fix(web): skip sidebar view migration when settings hydrated from server
After SSR hydration the in-memory views are already the backend's
truth, so migrating re-POSTs the same data every page load. Gate the
one-shot migration on userSettings.loaded — only run if the server
didn't answer and the slice fell back to localStorage.
* fix: prevent agentctl OOM from unbounded diff generation in workspace tracker
Add per-file size cap (10MB), binary detection (null-byte in 8KB),
diff output cap (256KB), and total budget (2MB) to workspace tracker
diff enrichment. Strip diff content from event bus path. Surface skip
reasons in frontend. Add pprof endpoints to agentctl.
* fix: address PR review feedback
Close stdout pipe on cmd.Start failure to prevent fd leak.
Limit untracked file read to maxDiffOutputSize (256KB) instead of
maxDiffFileSize (10MB) to reduce peak allocation from ~20MB to ~512KB.
* fix: add filepath.Clean to os.Stat call to satisfy CodeQL path-injection check
* fix: address second round of PR review feedback
Extract all DiffSkipReason strings as named constants. Add diff_skip_reason
to UncommittedFile type removing unsafe cast. Add unit tests for
isBinaryContent, totalDiffBytes, capDiffOutput truncation, and all
enrichUntrackedFileDiffs skip-reason paths.
* fix: use bytes.IndexByte for null-byte check clarity
* fix: show truncation banner when diff has partial content
* fix: revert diff stripping from event bus — frontend needs diffs from this path
E2E tests confirmed the frontend receives git status diffs through the
event bus (agentctl → backend WS → event bus → gateway → frontend).
Stripping diffs broke the changes panel. The per-file size caps in
workspace_git_diff.go already bound the data.
* fix: extract renderDiffContent to keep FileDiffSection under 100-line lint limit
* fix: correct onCommentRun type in renderDiffContent helper
* fix: format long destructuring line for prettier
* fix: move diffSkipReasonLabel to types.ts to keep review-diff-list under 600 lines
Co-authored-by: José Almeida <undefined@users.noreply.github.com>
* fix: coerce optional forceLoad to boolean for renderDiffContent
* fix: shorten className and comment to stay within prettier printWidth
* fix: apply prettier formatting to review-diff-list
* fix: correct off-by-one in untracked file line count
---------
Co-authored-by: claude[bot] <41898282+claude[bot]@users.noreply.github.com>
Co-authored-by: José Almeida <undefined@users.noreply.github.com>
* fix: improve plan comment formatting to match code review style
- Update formatPlanCommentsAsMarkdown to use code blocks and blockquotes
- Remove duplicate header in chat-input-area.tsx
- Update buildDocumentContext to use consistent formatting
- Fixes duplicate '### Plan Comments' headers and extra separators
* fix: add tests and fix multiline blockquote handling
- Add format.test.ts for formatPlanCommentsAsMarkdown and related functions
- Fix multiline comment text to properly prefix each line with >
- Addresses review feedback about missing tests and broken blockquotes
* fix: export and reuse toBlockquote helper to avoid duplication
* fix: update E2E test to match new plan comment format
The test was expecting 'Comment 1:' which was the old bullet-style format.
Now looks for 'Plan Comments' header instead.
* fix(web): fix dock view task-switching regressions
Three bugs caused broken UI when switching tasks in the dock view:
1. Environment key migration was dead code — data stored under fallback
sessionId keys became invisible when the environmentIdBySessionId
mapping was later set, because migrateEnvKeyedData was never called
in production (only in tests via registerSessionEnvironment).
2. Orphaned portals leaked on every fast-path session switch — the
isRestoringLayout guard in setupPortalCleanup blocked portal release
for panels removed during the fast path, accumulating invisible React
trees with live WS subscriptions.
3. Chat panel gap during fast-path transition — old session tab was
removed synchronously but new tab was deferred to a useEffect,
leaving a brief layout gap.
* fix(web): remove superfluous args in test helper
Address CodeQL finding — createSessionSlice only takes `set`, not
`get` and `api`.
* fix(web): add addPanel test assertions and doc comment
Address claude-review blocker: assert that fast-path creates the
session panel with correct id/params, and skips creation when it
already exists. Add doc comment noting syncEnvironmentMapping
requires the combined store draft.
* test(web): add PanelPortalManager.reconcile tests
Export PanelPortalManager class for testability and add tests covering
reconcile removal, no-op, and listener notification paths.
* refactor: re-key dockview panel state by environmentId instead of sessionId
- Remove terminal from SESSION_SCOPED_COMPONENTS (uses useEnvironmentSessionId internally)
- Convert ChangesContent and file-editor-panel to useEnvironmentSessionId()
- Re-key sessionCommits store from bySessionId to byEnvironmentId
- Re-key useCumulativeDiff cache by environmentId
- Update WS handler to resolve envKey before invalidating cumulative diff cache
- Update comments across portal manager, session switch, and layout files
* fix: revert useEnvironmentSessionId in file-editor-panel
file-editor is session-scoped and ephemeral — portal is destroyed on
session switch, so useEnvironmentSessionId() has no practical effect.
* fix: address CodeRabbit review comments
- Pass appStore to setupPortalCleanup so terminal tab close still calls
stopUserShell via activeSessionId fallback (entry.sessionId is now
undefined for global panels)
- Add request version guard to useCumulativeDiff to discard stale
responses after environment switches
* fix: scope activeSessionId fallback to terminal only in portal cleanup
vscode is session-scoped and always has entry.sessionId — the
activeSessionId fallback should only apply to terminal (now global).
* fix: migrate stranded env-keyed data on registerSessionEnvironment
When registerSessionEnvironment(sessionId, environmentId) is called,
any data previously stored under the fallback sessionId key is now
migrated to the proper environmentId key. This prevents data from
"vanishing" when selectors start reading by environmentId after the
mapping is registered.
Applies to: sessionCommits, gitStatus, shell outputs/statuses,
userShells.
* fix: extract migrateEnvKeyedData helper to fix lint max-lines
* fix: address CodeRabbit review comments
- Always delete orphaned sessionId key in migrateEnvKeyedData even when
environmentId key already exists (prevents stale accumulation)
- Reset loading state when switching to an envKey with warm cache to
avoid brief spinner flash
* fix: always reset loading on envKey switch in useCumulativeDiff
Prevents stuck loading state on rapid A→B→A env switches where the
stale in-flight response is discarded by version guard but loading
is never reset.
* fix: move sync effect before fetch effect in useCumulativeDiff
React 18 batches effects in declaration order. With the sync effect
after the fetch effect, setLoading(false) from the sync overrode
setLoading(true) from the fetch on cold-cache env switches. Moving
the sync effect first ensures the fetch effect's setLoading(true)
wins the batch.
* test: add migrateEnvKeyedData coverage for registerSessionEnvironment
Tests 4 cases: normal migration, don't-clobber existing data,
no-op when sessionId equals environmentId, and partial sub-key
migration.
* fix: close stale-diff gaps in useCumulativeDiff sync effect
- Bump requestVersionRef on envKey change so in-flight fetches for
the previous envKey are discarded
- Clear loadingState[envKey] so the fetch effect isn't blocked on
re-entry (A→B→A scenario)
* fix(web): recalculate dockview layout after fast-path session switch
The fast path in tryFastSessionSwitch removed ephemeral panels but
never called api.layout() to redistribute column widths, leaving a
gap on the right side of the screen. Both slow-path branches already
make this call.
* test(web): add regression test for fast-path layout call
Verifies that performSessionSwitch calls api.layout() on both the
fast path and slow path, preventing the squished-layout regression.
* test(web): cover saved-layout fast path branch
* fix(web): replace no-explicit-any cast in test
* fix(web): fix test isolation with mockReturnValueOnce
Use mockReturnValueOnce and clearAllMocks for proper test isolation
between fast-path and slow-path test cases.
* test(web): fix mock shape to match LayoutGroupIds type