fix(toolbar): move Resize button back to primary bar - #96
Conversation
Resize triggers a terminal re-render and is needed frequently enough that hiding it in the Dev panel was too many taps away. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Resize must be reachable on mobile without opening the overflow row since it is needed frequently to re-fit the terminal after layout changes. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
There was a problem hiding this comment.
Pull request overview
Moves the terminal “Resize” control out of the developer-only panel and back into the standard toolbar’s secondaryActions, making it accessible alongside other frequent actions in both desktop and mobile overflow layouts.
Changes:
- Added a
resizeentry tosecondaryActionsthat tracks the click and triggershandleManualResize(). - Removed the old “Resize”
<button>from the dev tools panel so that panel is limited to diagnostic/dev controls.
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
✅ Registry ValidationTest Coverage: 93/128 features have
|
✅ Registry ValidationTest Coverage: 93/128 features have
|
Go Benchmarks (Tier 1) |
UX Analysis
|
E2E RPC Latency |
🎬 E2E Feature Demos2 shard(s) recorded feature flows for this PR. recordings shard 1 Demo preview opens directly in browser (single-file HTML). Raw WebM recordings in ZIP. Expires after 30 days. |
Frontend Terminal Throughput |
* chore(bench): update frontend throughput baseline [skip ci]
* feat(ux): re-order toolbar by usage, shared handedness hook, all-tab mobile reach (#94)
* chore(bench): update go tier1 baseline [skip ci]
* chore(bench): update go tier2 baseline [skip ci]
* chore(bench): update e2e latency baseline [skip ci]
* chore(bench): update frontend throughput baseline [skip ci]
* perf(session): lock-free shell registry + allocation hot-path fixes
Replace the per-Instance shell registry (deadlock.RWMutex + two plain maps)
with a lock-free ShellRegistry backed by puzpuzpuz/xsync/v4 CLHT map.
All mutations use Compute callbacks with copy-on-write shell structs so
callers never need to hold a lock, eliminating the heaviest contention
surface identified in pprof mutex profiles.
Also cherry-pick three allocation hot-path fixes found during profiling:
- ProcessOutput in ratelimit detector now does state/cooldown guards before
allocating string(data), eliminating the alloc on the majority of calls
- stripANSI / stripANSICodes skip the regexp replace-all when the input
contains no ESC byte, saving a string allocation per terminal output chunk
- UpdateReviewQueueState converted to a direct UPDATE WHERE query instead
of SELECT + UpdateOne, removing a redundant round-trip per review cycle
Auth setup now creates the auth/ subdirectory before writing the token file
so first-run installs don't fail with ENOENT.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* perf(session): lock-free GetTimeSinceLastMeaningfulOutput via atomic shadow
GetTimeSinceLastMeaningfulOutput() was called on every review-queue poll
tick (every 2s × N sessions), each time acquiring a deadlock.RWMutex read
lock — 2.2B contended cycles, 5094 events in profiling.
Add an int64 atomic shadow field `lastMeaningfulOutputNs` to ReviewState
that mirrors LastMeaningfulOutput as UnixNano. UpdateTimestamps() writes
both fields (under the existing write lock). SyncAtomicTimestamps() seeds
the shadow after instance construction or restore from storage.
GetTimeSinceLastMeaningfulOutput() now reads the atomic directly (no lock)
once the shadow is initialised. When the atomic is zero (cold start or test
code that sets LastMeaningfulOutput directly), it falls back to the
lock-based read, preserving correctness without requiring test changes.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* feat(ux): re-order terminal toolbar by usage frequency + shared handedness hook
- Reorder primary toolbar: Copy→Paste→Bottom→Clear→Gallery→Files→Mouse (8 visible)
- Move dev tools (Debug, Log Stream, Record, Raw, Resize) behind ⚙ Dev toggle
- Instrument every toolbar button with track() for analytics going forward
- Extract shared useHandedness() hook; sets :root[data-left-handed] so any CSS
can flip thumb-sensitive layouts without reading localStorage per-component
- mobileOverflowRow defaults to row-reverse (right-thumb reach); flips to row for
left-handed users via :root[data-left-handed] selector
- Add handedness shortcut inside Dev panel for quick toggle without leaving terminal
- Refactor BottomNav to use shared useHandedness hook (removes inline duplication)
- Apply :root[data-left-handed] flex-direction overrides to Diff, VCS, Files, Logs
toolbars across all session pane tabs
- Add Record button aria-label (a11y fix)
- Fix pre-existing ResizeObserver missing mock in BottomNav tests (7 tests now pass)
- 75 tests passing across 5 suites (25 new analytics/dev-panel tests)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* fix(lint): remove unused shellWgWait function
The shellWgWait helper was never called anywhere in the codebase,
causing a golangci-lint unused-function failure in CI.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* fix(toolbar): paste reachable on mobile, remove unused data-left-handed attr
Move Paste from inline secondaryGroup div (hidden on mobile) into the
secondaryActions array so it appears in the mobile overflow row.
Remove dead data-left-handed attribute from the overflow row div — the
CSS uses :root[data-left-handed] set by useHandedness, not the div's
own attribute.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* chore: add toolbar-reorder planning artifacts
---------
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
* fix(toolbar): move Resize button back to primary bar (#96)
* fix(toolbar): move Resize button back to primary bar
Resize triggers a terminal re-render and is needed frequently enough
that hiding it in the Dev panel was too many taps away.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* fix(toolbar): Resize as primary inline button, always visible
Resize must be reachable on mobile without opening the overflow row
since it is needed frequently to re-fit the terminal after layout changes.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
---------
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
* fix(terminal): eliminate 60fps re-render storm on selection, add copy/select UX (#95)
* chore(bench): update go tier1 baseline [skip ci]
* chore(bench): update go tier2 baseline [skip ci]
* chore(bench): update e2e latency baseline [skip ci]
* chore(bench): update frontend throughput baseline [skip ci]
* perf(session): lock-free shell registry + allocation hot-path fixes
Replace the per-Instance shell registry (deadlock.RWMutex + two plain maps)
with a lock-free ShellRegistry backed by puzpuzpuz/xsync/v4 CLHT map.
All mutations use Compute callbacks with copy-on-write shell structs so
callers never need to hold a lock, eliminating the heaviest contention
surface identified in pprof mutex profiles.
Also cherry-pick three allocation hot-path fixes found during profiling:
- ProcessOutput in ratelimit detector now does state/cooldown guards before
allocating string(data), eliminating the alloc on the majority of calls
- stripANSI / stripANSICodes skip the regexp replace-all when the input
contains no ESC byte, saving a string allocation per terminal output chunk
- UpdateReviewQueueState converted to a direct UPDATE WHERE query instead
of SELECT + UpdateOne, removing a redundant round-trip per review cycle
Auth setup now creates the auth/ subdirectory before writing the token file
so first-run installs don't fail with ENOENT.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* perf(session): lock-free GetTimeSinceLastMeaningfulOutput via atomic shadow
GetTimeSinceLastMeaningfulOutput() was called on every review-queue poll
tick (every 2s × N sessions), each time acquiring a deadlock.RWMutex read
lock — 2.2B contended cycles, 5094 events in profiling.
Add an int64 atomic shadow field `lastMeaningfulOutputNs` to ReviewState
that mirrors LastMeaningfulOutput as UnixNano. UpdateTimestamps() writes
both fields (under the existing write lock). SyncAtomicTimestamps() seeds
the shadow after instance construction or restore from storage.
GetTimeSinceLastMeaningfulOutput() now reads the atomic directly (no lock)
once the shadow is initialised. When the atomic is zero (cold start or test
code that sets LastMeaningfulOutput directly), it falls back to the
lock-based read, preserving correctness without requiring test changes.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* fix(terminal): eliminate 60fps re-render storm on selection, add copy/select UX
The root cause was setCopyButtonPos (React useState) called inside
terminal.onSelectionChange, which fires at up to 60fps during mouse drag.
Each setState triggered a full React reconcile, clearing the selection in
progress and blocking right-click.
Fix: replace both useState calls with useRef + direct DOM mutation for the
floating Copy button and toast. Both elements are now portaled to document.body
via createPortal to comply with ADR-009 (fixed positioning inside CSS transform
ancestors silently mispositioning).
Additional improvements:
- Right-click context menu (Copy, Select All, Paste) via contextmenu listener
on terminal.element; suppressed in mouse tracking mode (vim/tmux)
- Ctrl/Cmd+C with selection copies to clipboard and clears selection; without
selection passes SIGINT to PTY as before
- Ctrl/Cmd+A calls terminal.selectAll()
- Extract isMouseTracking() to shared utility (web-app/src/lib/terminal/mouseTracking.ts)
eliminating duplication between XtermTerminal and useTerminalGestures
- Replace hardcoded zIndex 9999 with named floatingTerminalUI slot (1085) in
theme-contract.css.ts between toast (1080) and tooltip (1100)
- 37 new tests covering re-render prevention, keyboard shortcuts, and context menu
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* fix(terminal): guard navigator.clipboard before calling writeText
Both the context menu Copy path and the Ctrl+C keyboard handler called
navigator.clipboard.writeText directly without checking availability.
In non-secure contexts (HTTP) or browsers without the Clipboard API,
this threw synchronously before the execCommand fallback could run.
Added navigator.clipboard?.writeText guard to both call sites; falls
through to execCommand('copy') when the API is absent.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* fix(lint): remove unused shellWgWait accessor
shellWgWait was kept during merge conflict resolution but has no callers
on this branch. golangci-lint (unused linter) flagged it as dead code.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
---------
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
* chore(bench): update go tier1 baseline [skip ci]
* chore(bench): update go tier2 baseline [skip ci]
* chore(bench): update e2e latency baseline [skip ci]
* chore(bench): update frontend throughput baseline [skip ci]
* feat(notifications): refresh toast in-place on duplicate events, skip audio
Duplicate visible-toast events (same sessionId:notificationType within 10s)
now call addNotification silently to update stale content (e.g. fork-pressure
stats) rather than being dropped entirely. Audio and native notifications
still fire only once per dedup window.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* feat(notifications): native notification auto-close and close-before-open dedup
Implements FR-3 (auto-close via setTimeout) and FR-4 (close-before-open Map
dedup) in showBrowserNotification; adds types_pb mock to notifications.test.ts
so tests pass without generated proto files in the worktree.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* perf(vcs): cache go-git repos, per-repo mutex, bounded merge-base BFS
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* perf(tokens): reduce per-entry json.Unmarshal allocations in token parser
Define jsonlUserContent as a minimal content type for user message parsing
that omits the Input json.RawMessage field. User entries never contain
tool_use blocks (only tool_result), so the Input field was always empty on
user turns, yet its allocation accounted for ~237 KB per processUserEntry
call (PerfFix-4). Also adds a benchmark that exercises the hot path.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* perf(streaming): batch terminal output frames to reduce scheduler wakeups
Adds a batch cap of 32 to the greedy drain coalesce loop in streamViaControlMode.
Without the cap the loop drains frames until the channel is empty — unbounded when
bursts are large. The cap bounds worst-case batching latency to ~3 ms at 10 K fps
while still coalescing the typical `ls -la` burst into one WriteMessage call instead
of N, reducing goroutine scheduler wakeups and syscall count.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* perf(go:optimize): Pattern F+G — fast-path guard + capacity hints
skill_detector.go: add !ContainsRune('/') guard before commandPattern
regexp so the common no-slash case is allocation-free (TestDetectCommandsInText_
ZeroAllocsWhenNoSlash asserts 0 allocs). Pre-allocate result slice with
make([]SkillActivation, 0, len(matches)).
gogit_vcs_reader.go: add capacity hints to reachableSet (64), findMergeBase
second-pass seen (mergeBaseBFSLimit), and countCommitsTo seen (32), avoiding
incremental rehashing on small-to-medium histories.
benchmark.yml: add BenchmarkDetectCommandsInText and BenchmarkTokenParser to
Tier 1 regex; add ./session/tokens to package list.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* feat(hibernation): auto-hibernate idle sessions after 20min, skip review-queue sessions; cap repoCache
- config: reduce default IdleTimeoutMinutes 120 → 20 so memory is reclaimed
sooner on idle workstations; existing user config overrides remain respected.
- sweeper: gate both idle-timeout and resource-pressure hibernation on
NeedsReview() — sessions waiting for approval or user input are never
silently hibernated regardless of idle duration.
- gogit_vcs_reader: add TTL eviction to repoCache (repoCacheTTL=30min,
repoCacheMaxEntries=100). Previously the cache grew unboundedly with each
new repo scanned; pruneRepoCache() now evicts cold entries and is triggered
when the cache exceeds the cap. accessedAtNs (atomic int64) stamps each hit
so eviction is lock-free on the hot path.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* chore(bench): update go tier1 baseline [skip ci]
* chore(bench): update go tier2 baseline [skip ci]
* chore(bench): update frontend throughput baseline [skip ci]
* fix(verify): resolve sdd:6-verify findings
Go:
- pruneRepoCache: implement LRU trim — after TTL eviction, sort remaining
entries by accessedAtNs and delete coldest until count ≤ repoCacheMaxEntries.
Previously the cap was advertised but not enforced (keys slice was dead code).
- openRepoEntry: wrap PlainOpenWithOptions error with path context; re-check
cache after pruning to eliminate redundant PlainOpen under contention.
- findMergeBase: propagate non-ErrObjectNotFound CommitObject errors instead
of silently continuing; skip only missing objects from shallow clones.
TypeScript:
- useSessionNotifications: extract approvalId before object literal, removing
two non-null assertions on optional-chain result.
- useSessionNotifications: move dedup map prune before lastShown read so the
captured value reflects post-prune state.
- useSessionNotifications.test: type makeEvent with TestNotificationEvent
interface instead of any.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* refactor(vcs): move repo/diff caches onto GoGitVCSReader; release mutex before OS calls
Move repoCache, repoCacheSize, and diffStatCache from package-level globals to
GoGitVCSReader struct fields. GoGitVCSReader{} remains valid — sync.Map and
int64 are zero-value safe. This isolates cache state per reader instance,
preventing cross-test contamination when multiple GoGitVCSReader values exist
in the same process (e.g. concurrent test runs or benchmarks).
pruneRepoCache, openRepoEntry, and openWorktree are now methods on
*GoGitVCSReader; all callers within the file use g. prefix.
HasUncommitted: release the per-repo mutex before the working-tree stat loop
and hasUntrackedFiles directory walk. These phases call only os.Lstat and
os.ReadDir — no go-git access — so holding the packfile-reader mutex was
blocking concurrent VCS operations on the same repo unnecessarily. Index entry
fields are captured as a plain []trackedFile slice before the unlock.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* chore(bench): update go tier1 baseline [skip ci]
* chore(bench): update go tier2 baseline [skip ci]
* chore(bench): update frontend throughput baseline [skip ci]
* feat(history): Story 1 — proto fields, git enrichment, fork dispatch
Task 1.1: Add 5 new fields to ClaudeHistoryEntry (branch, session_status,
git_status_summary, last_commit_message, diff_file_count) and 2 fork fields
to CreateSessionRequest (fork_source_id, fork_at_message).
Task 1.2: Cursor pagination was already implemented — no changes needed.
Task 1.3: Enrich ListClaudeHistory responses with branch (60s TTL git cache)
and session_status (cross-referenced against live instances via
GetConversationUUID). SearchService gains SetInstanceProvider(), cachedBranch(),
and liveSessionStatus(). Instance provider wired in NewSessionService.
Task 1.4: Fork dispatch in CreateSession — when fork_source_id is set,
FindConversationFilePath locates the source JSONL and ForkClaudeConversation
copies it, then resume_id is set to the new UUID so the normal start path
launches with --resume.
Also: export FindConversationFilePath from session/history.go, update
docs/tasks/TODO.md (Session Defaults → Completed, History Page Revamp → In Progress).
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* fix(history): Story 2 — fix broken resume (path validation + session type)
Root cause (Task 2.1): when resuming a deleted worktree session, the project
directory no longer exists. Without explicit validation the backend created a
Creating-status session and launched claude with --resume in a non-existent
directory. tmux silently fell back to $HOME; claude could not find the
conversation (keyed by original project path hash) and either exited
immediately or started a fresh conversation. User saw a session stuck in
Stopped state with no error message.
Fixes (Task 2.2):
- Backend: when resume_id is set and session_type is unspecified, force
SESSION_TYPE_DIRECTORY so no new worktree is created (worktree path would
produce a different project-dir hash, breaking --resume lookup).
- Backend: distinguish the resume error message — "cannot resume: project
directory no longer exists: <path>" instead of generic "path does not exist".
- Frontend: pass sessionType: SessionType.DIRECTORY explicitly on resume to
make the intent unambiguous and prevent future inference surprises.
The rename modal before launch was already implemented. No frontend modal
changes needed.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* fix(ci): resolve pre-existing lint-css-tokens and lint-custom failures
CSS tokens: add logError/logWarn/logInfo/logDebug/logTrace/logOnDark/
logOnAmber/logLive tokens to theme-contract.css.ts and all six themes.
Replace hardcoded hex in LevelFilterChips.css.ts, LogRow.css.ts,
LogViewerToolbar.css.ts (logs/ and shared/), and MemoryPressureCallout.css.ts
with vars.color.log* references. Also replace var(--background, #111) fallback
in LogRow with vars.color.background.
lint-custom: replace raw exec.CommandContext with safeexec.CommandContext in
search_service.go (norawexec rule); remove dead os/exec import.
Note: session/headless integration_test.go build failure (ClaudeRunner
interface mismatch) was already broken in origin/main before these commits
and is not addressed here.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* chore(bench): update go tier1 baseline [skip ci]
* chore(bench): update go tier2 baseline [skip ci]
* chore(bench): update frontend throughput baseline [skip ci]
* feat(history): Stories 3–5 — rich cards, virtual scroll, fork modal
Story 3 — Rich Session Cards + Inline Preview (Tasks 3.1, 3.2):
- HistoryEntryCard: status pill (running/paused/creating/stopped) with
animated pulse dot for ACTIVE; branch shown on every card from Story 1
enrichment (not just selected); shortened project path display.
- HistoryCardPreview: expand/collapse toggle on each card; lazy-fetches
last 5 messages on first expand; caches per entryId in useRef; strips
ANSI escape sequences (SGR + OSC hyperlinks) with a regex-based stripper
(no npm dependency required).
Story 4 — Virtual Scrolling + Infinite Load (Tasks 4.1–4.3):
- VirtualHistoryList: replaces HistoryGroupView in the main list; uses
@tanstack/react-virtual's useVirtualizer on a flattened array of group
headers + card items; renders only visible rows (~8 overscan).
- Infinite scroll: IntersectionObserver sentinel row fires onLoadMore when
the bottom of the list is reached, replacing the manual "Load more" button.
- Keyboard nav: ArrowDown/j and ArrowUp/k call virtualizerRef.scrollToIndex
so the focused card is always visible.
Story 5 — Fork Modal + Split-Button + Unified Search (Tasks 5.1–5.3):
- ForkModal: <dialog>-based modal with title/path/sessionType/branch/
fork-at-message slider; calls createSession with fork_source_id and
fork_at_message from the Story 1 proto fields.
- HistoryDetailPanel: "Resume" button replaced with a split-button —
primary "▶️ Resume" + "🍴" fork chevron, both open their respective modals.
- HistoryFilterBar + useHistoryFilters: added branchFilter (debounced text
input, client-side); removed separate metadata/fulltext mode toggle from
the filter bar (full-text search still accessible via the existing tab).
- page.tsx: wired fetchPreview callback, fork state/handlers, VirtualHistoryList,
ForkModal, and all new filter props.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* fix(history): virtual scroll layout + mark epic complete
- history.css.ts: add minHeight/overflow:hidden to entryList so
VirtualHistoryList's 100% height resolves in the flex chain
- VirtualHistoryList: add maxHeight fallback matching detailPanel cap
- page.tsx: wrap VirtualHistoryList in flex:1/minHeight:0 div so it
fills remaining height after the section title
- TODO.md: History Page Revamp moved to Completed
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* fix(headless): update argsCapturingRunner.Run to match ClaudeRunner interface
ClaudeRunner.Run now requires an io.Reader stdin parameter (added when the
prompt was moved off the command line to avoid /proc/pid/cmdline exposure).
The argsCapturingRunner mock in integration_test.go was not updated, causing
a build failure under -tags integration. Pass stdin through to the inner
ProcessRunner.Run call. Also rename local `copy` variable to `argsCopy` to
avoid shadowing the builtin.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* chore(bench): update go tier1 baseline [skip ci]
* chore(bench): update go tier2 baseline [skip ci]
* chore(bench): update frontend throughput baseline [skip ci]
* fix(lint): replace inline layout style with existing entryList class
history/page.tsx:325 used style={{ flex: 1, minHeight: 0 }} which triggers
the no-restricted-syntax ESLint rule against inline layout properties.
The styles.entryList class already defines identical properties (flex:1,
minHeight:0) — use it directly.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* chore(bench): update go tier1 baseline [skip ci]
* chore(bench): update go tier2 baseline [skip ci]
* chore(bench): update frontend throughput baseline [skip ci]
* chore(fmt): gofmt search_service.go and other unformatted Go files
Fixes CI fmt-check failure. Files were unformatted in main; gofmt -w applied
with no logic changes.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* chore(bench): update go tier1 baseline [skip ci]
* chore(bench): update go tier2 baseline [skip ci]
* chore(bench): update frontend throughput baseline [skip ci]
* fix(ci): resolve Benchmarks race, Demo GIFs 403, and Release Please body parse
Benchmarks: wrap all four baseline push steps in a 3-attempt retry loop
(git pull --rebase --autostash && git push) so rapid consecutive pushes no
longer cause a permanent rejected-push failure.
Demo GIFs: add permissions: contents: write to the publish-demos job so
github-actions[bot] can commit the recorded GIF files to main.
Release Please: add commit-search-body: false to release-please-config.json
so the conventional commit parser does not attempt to parse commit message
bodies for breaking-change footers. Without this, commit bodies containing
Go source code (e.g. make([]T, 0, n)) cause an unexpected-token parse error.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* chore(bench): update go tier1 baseline [skip ci]
* chore(bench): update go tier2 baseline [skip ci]
* chore(bench): update e2e latency baseline [skip ci]
* chore(bench): update frontend throughput baseline [skip ci]
* feat(insights): time range filter, session detail, cost projections, perf (#98)
* fix(verify): resolve sdd:6-verify findings
Go:
- pruneRepoCache: implement LRU trim — after TTL eviction, sort remaining
entries by accessedAtNs and delete coldest until count ≤ repoCacheMaxEntries.
Previously the cap was advertised but not enforced (keys slice was dead code).
- openRepoEntry: wrap PlainOpenWithOptions error with path context; re-check
cache after pruning to eliminate redundant PlainOpen under contention.
- findMergeBase: propagate non-ErrObjectNotFound CommitObject errors instead
of silently continuing; skip only missing objects from shallow clones.
TypeScript:
- useSessionNotifications: extract approvalId before object literal, removing
two non-null assertions on optional-chain result.
- useSessionNotifications: move dedup map prune before lastShown read so the
captured value reflects post-prune state.
- useSessionNotifications.test: type makeEvent with TestNotificationEvent
interface instead of any.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* refactor(vcs): move repo/diff caches onto GoGitVCSReader; release mutex before OS calls
Move repoCache, repoCacheSize, and diffStatCache from package-level globals to
GoGitVCSReader struct fields. GoGitVCSReader{} remains valid — sync.Map and
int64 are zero-value safe. This isolates cache state per reader instance,
preventing cross-test contamination when multiple GoGitVCSReader values exist
in the same process (e.g. concurrent test runs or benchmarks).
pruneRepoCache, openRepoEntry, and openWorktree are now methods on
*GoGitVCSReader; all callers within the file use g. prefix.
HasUncommitted: release the per-repo mutex before the working-tree stat loop
and hasUntrackedFiles directory walk. These phases call only os.Lstat and
os.ReadDir — no go-git access — so holding the packfile-reader mutex was
blocking concurrent VCS operations on the same repo unnecessarily. Index entry
fields are captured as a plain []trackedFile slice before the unlock.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* feat(insights): time range filter, session detail drawer, cost projections, perf
R1 — Time range filter: preset buttons (Today/7d/30d/90d/All/Custom) + custom
date picker persisted via URL search params; from/to wired through to
GetInsightsSummary and WatchInsights RPCs.
R2 — Per-session detail drawer: slide-over panel (createPortal) showing
session metadata, tools breakdown, and skill activations; click any session
row to open; Escape/backdrop closes.
R3 — Cost projections: useProjectedCost computes projected monthly spend from
daily buckets (requires ≥7 days of data); useBudgetThreshold persists a
localStorage budget threshold with SSR hydration guard; warning banner + card
styling when projected spend exceeds threshold.
R4 — Skeleton loaders: InsightsDashboardSkeleton shown immediately on mount
before first RPC response; chart dynamic() imports use Skeleton as loading
placeholder; Suspense boundary added for useSearchParams.
R5 — Virtual scrolling: TableVirtuoso (react-virtuoso) for >50 session rows;
scroll position preserved across live updates.
R6 — Smooth live updates: surgical per-session state patch on WatchInsights
events; exponential backoff reconnect (1s→30s cap) replaces silent drop;
chart data transforms wrapped in useMemo to prevent recharts re-renders.
R7 — Session table search/filter: Fuse.js path search + model family dropdown
+ clear button; filter state preserved across live update cycles.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* fix(insights): address review comments — date tz, UTC month bucketing, TableVirtuoso cells, test hydration assertion
- InsightsDashboard: serialize date to URL using local timezone (not UTC toISOString)
- TimeRangeFilter: parse URL date params as local T00:00:00/T23:59:59 (not UTC midnight)
- useProjectedCost: compare bucket dates using UTC methods to match server-side UTC midnight keys
- SessionsTable: return cells (not <tr>) from TableVirtuoso itemContent; use components.TableRow for row-level props
- useBudgetThreshold test: assert initial isHydrated is false (not just typeof boolean)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* chore: add planning artifacts for insights enhancement
* fix(insights): code quality, test coverage, and UX improvements
Code quality:
- Extract fmtCost/fmtTokens/fmtPct/fmtDate/shortId to insightsFormatters.ts;
standardize fmtPct to toFixed(1) across all consumers
- Name retry delay magic numbers: INITIAL_RETRY_DELAY_MS / MAX_RETRY_DELAY_MS
- Log WatchInsights stream errors to console instead of silently swallowing
Correctness:
- Filter surgical stream update events against active from/to time range;
sessions outside the current filter window no longer appear in live patches
Tests (+4):
- useProjectedCost: February non-leap (28 days), leap year (29 days),
UTC month boundary exclusion
- useBudgetThreshold: invalid localStorage value (non-numeric) returns null
UX:
- ProjectedCostCard: show inline error when budget input is ≤ 0 or NaN
- TimeRangeFilter: show range-error message when from > to (custom range)
- InsightsDashboard: map raw gRPC error codes to user-friendly messages
- SessionDetailDrawer: add aria-describedby + srOnly description for a11y
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* fix(insights): remove last duplicate fmtCost in ProjectedCostCard, import from insightsFormatters
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
---------
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
* chore(bench): update go tier1 baseline [skip ci]
* chore(bench): update go tier2 baseline [skip ci]
* chore(bench): update e2e latency baseline [skip ci]
* chore(bench): update frontend throughput baseline [skip ci]
* revert: remove unnecessary commit-search-body flag
The release-please failure is caused by missing PR creation permission
(GitHub Actions not permitted to create PRs), not commit body parsing.
The parse errors in the logs are debug messages, not failures.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* fix(lint): move useMemo above early returns in insights charts; add displayName
React hooks must be called unconditionally. DailySpendChart, ModelBreakdownChart,
and ModelOverTimeChart all called useMemo after an early return on empty data.
Move the memo calls above the early return — they are cheap no-ops on empty
arrays and the behaviour is identical.
SessionsTable: add displayName to the React.forwardRef TableBody component
passed to Virtuoso so the react/display-name lint rule is satisfied.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* chore(bench): update go tier1 baseline [skip ci]
* chore(bench): update go tier2 baseline [skip ci]
* chore(bench): update e2e latency baseline [skip ci]
* chore(bench): update frontend throughput baseline [skip ci]
* fix(lint): eslint-disable-next-line for forwardRef inside useMemo object
The react/display-name rule cannot infer a display name for React.forwardRef
calls used as values in an object literal inside useMemo. Setting .displayName
after the fact is not visible to the lint rule at that location. Using
eslint-disable-next-line is the correct suppression for this inert pattern —
Virtuoso uses this TableBody ref purely for scroll container sizing, not for
React DevTools identification.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* chore(bench): update go tier1 baseline [skip ci]
* chore(bench): update go tier2 baseline [skip ci]
* chore(bench): update e2e latency baseline [skip ci]
* chore(bench): update frontend throughput baseline [skip ci]
* fix(ci): add push retry loop to demo GIFs workflow
Same race condition as benchmark baselines — the GIF commit push is
rejected when another commit lands on main while the recording job runs.
Retry up to 3 times with git pull --rebase --autostash before each attempt.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* fix(session): hydrate SquadSessionID from legacy conversation_id key
Fixes #38. Adds UnmarshalJSON fallback on ClaudeSessionData to hydrate SquadSessionID from the legacy conversation_id key for backward compatibility with pre-rename persisted state.
* chore(bench): update go tier1 baseline [skip ci]
* chore(bench): update go tier2 baseline [skip ci]
* chore(bench): update e2e latency baseline [skip ci]
* chore(bench): update frontend throughput baseline [skip ci]
* chore(demos): update E2E feature GIFs [skip ci]
* fix(bench): benchmark gate under 10 minutes — fix 3 root causes
Root cause 1: BenchmarkReactiveQueueManagerThroughput fails at b.N=518K
The benchmark used a per-iteration blocking drain (select + time.After). At
high b.N the event channel buffer (100) fills up, publishToClients silently
drops events, and the drain blocks forever. Fixed by:
- Restructuring to add-all-then-drain (measures producer throughput, not
round-trip latency)
- Using b.TempDir() instead of a fixed path (isolates each calibration call)
- Using a fast poll interval (10ms) via NewReviewQueuePollerWithConfig so
events arrive in the benchmark without waiting for the default 2s cycle
- Adding b.ReportMetric for delivery% to surface drop rate at high b.N
Root cause 2: BenchmarkSessionRestorePerformance spawns real tmux sessions
Takes ~176s × count iterations. Gated behind testing.Short() so it skips
with -short flag. Not a unit benchmark.
Root cause 3: CI command ran all tests AND benchmarks; count=10 too many
- Added -run='^$' (benchmarks only, no tests)
- Changed count=10 → count=5 (still statistically meaningful for ≥20%
regression detection; halves the budget)
- Added -short to skip integration benchmarks
- Changed -timeout=30m → -timeout=10m per package
Expected CI benchmark gate: <10 minutes total.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* chore(bench): update go tier1 baseline [skip ci]
* chore(bench): update go tier2 baseline [skip ci]
* chore(bench): update e2e latency baseline [skip ci]
* chore(bench): update frontend throughput baseline [skip ci]
* chore(demos): update E2E feature GIFs [skip ci]
* fix(bench): force-add gitignored baseline file; add retry + rebase on push
bench-baseline.txt matches the bench-*.txt gitignore pattern, causing
git add to fail silently and git push to find nothing to commit.
Use git add -f to force-track the intentional baseline data file.
Also add the pull --rebase retry loop (same pattern as other baseline
pushes) and match the commit message format used by tier1/tier2 baselines.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* fix(bench): make benchmark flows robust — 10 fragility fixes
Critical:
- Move benchmark-gate baseline to benchmarks/go/benchmark-gate-baseline.txt
(tracked directory, not gitignored). Previously bench-baseline.txt matched
bench-*.txt in .gitignore so it was never committed → regression check was
dead on every run.
- Fix regression regex to catch ≥100% regressions: was [2-9][0-9]\ (missed
+100%, +200%, etc.); now ([2-9][0-9]|[1-9][0-9]{2,})\.
High:
- All 5 push retry loops now fail loudly (exit 1) when all retries exhausted.
Previously the loop exited 0 (last command was 'sleep 5') so silent baseline
loss went undetected.
- Unify concurrency group: build.yml benchmark-gate now shares
baseline-push-${{ github.ref }} with benchmark.yml so 5 concurrent baseline
pushers are serialized instead of racing.
- Add timeout-minutes: 30 to benchmark-gate job (was defaulting to 6h).
- Add exponential backoff to retry loops (5s, 10s, 15s) since concurrent jobs
need more than 5s spacing.
Medium:
- Pin benchstat to v0.0.0-20260312031701-16a31bc5fbd0 in build.yml (was
@latest, inconsistent with benchmark.yml).
- Rename baseline commit message to "chore(bench): update benchmark-gate
baseline [skip ci]" — distinct from tier2 message.
- Remove dead advisory benchmark step (BenchmarkNavigation doesn't exist;
wasted 15 min per PR running nothing).
- Use github-actions[bot] identity for baseline commits (was ci@stapler-squad,
non-standard and inconsistent with benchmark.yml).
Gitignore: replace bench-*.txt with specific scratch files (bench-current.txt,
bench-diff.txt) so committed baseline files in benchmarks/go/ are not blocked.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* feat(sessions): fix InitialPrompt injection, add session goal tracking MCP + UI (#99)
* feat(sessions): fix InitialPrompt injection, add session goal tracking MCP + UI
Three improvements to session management:
1. **Fix InitialPrompt injection**: InitialPrompt was stored in DB but never
delivered to the agent. Now StartSessionDriver is called in the CreateSession
async goroutine, and the driver uses inst.InitialPrompt (sanitized) instead of
the static fallback when non-empty. Prompt sanitization strips null bytes and
collapses newlines before tmux injection.
2. **Session goal/task tracking**: New session_goal ent schema (1:1 per session)
with goal text, task hierarchy (JSON, max 3 levels/50 tasks), and status enum.
Three new MCP tools: set_session_goal, get_session_goal, update_session_task.
Thread-safe SessionGoal cache on Instance via goalMu RWMutex. Goal state
published via SessionUpdatedEvent for live UI updates.
3. **Goal visibility in UI**: GoalPanel component in session detail info tab shows
goal text + recursive task tree with status badges. Session list cards show
compact goal summary (60-char truncation + task completion fraction).
Also: steer_session MCP tool for mid-session steering; backlog tab confirmed
off by default (feature-flagged); Navigation tests for backlog visibility.
76 tests added (36 Go unit, 23 TS unit, 11 Go integration, 6 E2E planned).
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* fix(sessions): address code review findings — upsert correctness, dedup, test coverage
- C1: Always set tasks column in SessionGoal upsert (UpdateNewValues → explicit Update)
- C2: Wrap UpdateSessionTaskStatus in transaction to prevent read-modify-write race
- M1: Export IsValidTaskStatus from session package; remove duplicate in tools_goal
- M2: Reuse resolved inst in setSessionGoal (eliminate second LoadInstances call)
- M3: Log warning when DecodeTasks fails in GetSessionGoal
- M4: Bulk-load session goals in LoadInstances (N+1 → 1 query)
- M5: Remove dead goroutineCount assertion in driver idempotency test
- M6: Remove vacuous default: branch in event publication assertions
- M7: Fix UTF-8 truncation in sanitizeInitialPromptForTmux; add boundary test
- M8: Fix ambiguous getSessionGoal test assertion
- M9: Add collapse direction to GoalPanel expand toggle test
- M10: Add happy-path test for steer_session SendKeys
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* fix(sessions): address PR review comments
- truncateGoal: fix off-by-one (max+1 chars → exactly max chars)
- ValidateTaskDepth: correct misleading comment (count checked in validateTasks)
- GetSessionGoal: return shallow copy to prevent caller mutation of shared state
- proto types.proto: update tasks_json comment ("empty string" → "[]")
- update_session_task: distinguish not-found errors from internal errors
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* fix(mcp): update integration test tool count from 20 to 24
4 new tools added in this PR: steer_session, set_session_goal,
get_session_goal, update_session_task.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
---------
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
* chore(bench): update go tier1 baseline [skip ci]
* feat(approvals): dismiss notifications when user sends terminal input (#101)
* feat(approvals): dismiss notifications when user sends terminal input
When any keystroke is sent to a session, optimistically clear approval
notifications for that session and trigger a debounced re-fetch so the
UI stays in sync with actual backend state.
- ApprovalsContext: add clearForSession(sessionId) — optimistic filter
backed by an in-flight ref to handle rapid Enter presses safely
- TerminalOutput: call clearForSession on "\r" (Enter) and trigger a
300ms debounced refetch on any keystroke via useRef-based timer
- SessionCard: add suppressApprovalSubStatus prop scoped to NEEDS_APPROVAL
only so the in-tab chip/badge also clears instantly
- SessionList: pass suppressApprovalSubStatus={clearedSessions.has(id)}
- Tests: 45 new/updated tests across ApprovalsContext, TerminalOutput,
SessionCard, and mock fixtures; e2e spec scaffolded
- Feature registry: approval-enter-detection.json
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* fix(approvals): address code review findings
- Fix double-refetch flicker: skip debounce on Enter since clearForSession
already fires an eager refetch
- Guard debounced refetch behind pendingCount > 0 to avoid RPC noise
- Log refetch() rejections in clearForSession (no longer silently swallowed)
- Fix test silent-skip: wait for capturedOnData before switching to fake timers
- Remove dead ADR references from comments; replace with inline rationale
- Pass suppressApprovalSubStatus to SessionRow and suppress NEEDS_APPROVAL chip
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* fix(approvals): add feature marker and fix registry markerLine
Add // +feature: approval-enter-detection marker to ApprovalsContext.tsx
(line 2) and update the registry entry's markerLine from null to 2.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* chore(ci): add registry-diff to quick-check
Runs in ~0.6s — catches missing // +api: markers, markerLine: null,
and unregistered features locally before CI sees them.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
---------
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
* chore(bench): update e2e latency baseline [skip ci]
* chore(bench): update go tier2 baseline [skip ci]
* chore(bench): update frontend throughput baseline [skip ci]
* chore(bench): update go tier1 baseline [skip ci]
* chore(demos): update E2E feature GIFs [skip ci]
* fix(bench+lint): gofmt PR#101 files; replace rebase with reset+recommit for baselines
Lint: gofmt five files introduced in feat(approvals) PR#101 that were not
formatted before merge (tools_goal.go, tools_goal_test.go, tools_terminal.go,
instance_serialization.go, storage.go).
Benchmarks: replace git pull --rebase with git fetch+reset+recommit in all
five baseline update steps (tier1, tier2, frontend, e2e in benchmark.yml;
benchmark-gate in build.yml).
Why: baseline files are "last writer wins" — they cannot be merged. Rebasing
a baseline commit fails when another job already pushed a newer version of
the same file. The reset approach: fetch latest origin/main, hard-reset to
it (discarding the stale commit), re-copy the freshly-computed result file,
and create a clean commit. On conflict (another push landed between our fetch
and push), the retry loop re-fetches and tries again.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* chore(bench): update go tier1 baseline [skip ci]
* chore(bench): update go tier2 baseline [skip ci]
* chore(bench): update e2e latency baseline [skip ci]
* chore(bench): update frontend throughput baseline [skip ci]
* chore(demos): update E2E feature GIFs [skip ci]
* fix(nav): gate Backlog nav item behind feature flag
NAV_PAGES had Backlog hardcoded as bottomNavPrimary without any feature
flag check. Navigation.tsx correctly used useFeatureFlag("backlog") but
BottomNav and Header consumed NAV_PAGES directly, so Backlog appeared in
both the bottom bar and the hamburger menu regardless of the flag.
Add featureFlag?: string to NavPage interface. Set featureFlag: "backlog"
on the Backlog entry. BottomNav and Header now filter NAV_PAGES through
useFeatureFlags().flags before rendering, hiding any page whose featureFlag
is not enabled.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* chore(bench): update go tier1 baseline [skip ci]
* chore(bench): update e2e latency baseline [skip ci]
* chore(bench): update go tier2 baseline [skip ci]
* chore(bench): update frontend throughput baseline [skip ci]
* chore(demos): update E2E feature GIFs [skip ci]
* chore(bench): update go tier1 baseline [skip ci]
* chore(bench): update go tier2 baseline [skip ci]
* chore(bench): update e2e latency baseline [skip ci]
* chore(bench): update frontend throughput baseline [skip ci]
* chore(demos): update E2E feature GIFs [skip ci]
* feat(rules): structured rule builder, template library, analytics→rule workflow (#103)
* feat(rules): structured rule builder, template library, analytics→rule workflow
Redesigns the rules page UX to expose the full power of the backend
CommandCriteria classifier through a guided visual interface:
**Backend**
- Extend ApprovalRuleProto with 9 structured fields (programs, subcommands,
blocked_subcommands, required_flags, forbidden_flags, required_flag_prefixes,
python_modes, safe_python_imports_only, tool_category) at field numbers 20-28
- Add manual_allow/manual_deny to SubcommandStatProto, ProgramStatProto,
ToolStatProto for analytics decision-context display
- Extend ent schema with 8 JSON columns (empty-slice defaults, no migration)
- specsToRules() now builds CommandCriteria so user rules use structured matching
- ruleToSpec() exposes seed rule CommandCriteria so built-in rules display readably
**Frontend**
- RuleBuilderForm: Structured/Regex mode toggle, TagInput for programs/subcommands/
flags, Python mode checkboxes, decision segmented control, priority auto-default
by decision type (DENY→950, ESCALATE→450, ALLOW→100)
- RulePreview: live client-side preview of matching/non-matching commands
- TemplateLibrary: 13 curated templates (Python modes, git, npm, docker, MCP, etc.)
- MatchDescription: replaces raw regex chips with plain-language rule descriptions
- Analytics→rule prefill: "Add rule →" links now carry URL prefill payloads
(programs, subcommands, toolName) so the builder opens pre-populated
- Manual outcome card: shows % of escalated reviews ultimately allowed vs denied
- Edit button on user rules for inline editing
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* fix(rules): address code review findings
- BLOCKER: move in-memory spec update to after DB write succeeds in
rules_store.go Upsert(), eliminating stale in-memory state on DB failure
- CRITICAL: validate decodePrefill() fields individually instead of
blind cast; guard suggestedDecision against out-of-range integers
- MAJOR: skip priority auto-default in handleDecisionChange when user
has manually edited priority (compare against previous default first)
- MAJOR: remove Optional() from ent JSON fields that have Default([]),
making columns NOT NULL and eliminating nil-slice fragility
- Regenerate ent code after schema change
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* chore: regenerate ent and proto after rebase, fix post-merge issues
- Removed criteria_programs/criteria_subcommands from ent schema (replaced by 8-field CommandCriteria)
- Regenerated ent code and proto stubs
- Fixed SubcommandDecisionCount missing from repository.go
- Fixed criteriaPrograms/criteriaSubcommands refs in ApprovalRulesPanel, ParsedRuleCard, useApprovalRules
- Removed stale form state and old form functions from ApprovalRulesPanel
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* fix(rules): update test to use Programs field after CriteriaPrograms rename
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
---------
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
* chore(bench): update go tier1 baseline [skip ci]
* chore(bench): update go tier2 baseline [skip ci]
* chore(bench): update e2e latency baseline [skip ci]
* chore(bench): update frontend throughput baseline [skip ci]
* chore(demos): update E2E feature GIFs [skip ci]
* fix(rules): correct coverage false-positives and add three-state UX (#104)
* feat(sessions): Phase 1 programmatic control — \r fix + session_id capture for OneShot
Two improvements to OneShot (-p) session control:
1. **Fix initial prompt not submitting** (\r instead of \n): The session driver
was sending initialPrompt + "\n" (LF) to the PTY. Claude Code's interactive
readline needs \r (CR) to submit — the same signal a physical Enter key sends.
Sessions received the typed text but never executed it, causing inactivity
timeouts. Confirmed by log: "sent initial prompt" followed by 10-min idle.
The startup dialog answers ("1\n") work because those menus handle both,
but Claude's readline interface only responds to \r.
2. **Capture claude session_id from --output-format json output**: OneShot
sessions now launch with -p --output-format json. When the session exits,
the driver parses the JSON output for "session_id" and stores it as
ConversationUUID on the instance. Subsequent restarts automatically use
--resume <uuid>, sending Claude back into the same conversation with full
context instead of re-running the task from scratch.
Supporting infrastructure:
- SetClaudeConversationUUID() — thread-safe setter that fires a save callback
- SetClaudeSessionIDSavedCallback() — wired in service layer to flush to DB
- wireClaudeSessionIDCallback() — registered on all creation paths including
loadInstancesWithWiring (startup) and CreateDirectorySession (backlog)
- Prompt is now appended even when claudeSessionID is set for OneShot
sessions so continuation prompts are delivered after --resume
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* feat(sessions): Phase 2 — steer_session via --resume, --allowedTools, --permission-mode
Three programmatic control improvements over Phase 1:
1. **steer_session via subprocess**: When a OneShot session has completed
(Stopped + ConversationUUID set), steer_session now runs
'claude -p --resume <uuid> --output-format json <message>' instead of
PTY send-keys. Returns structured result text in the MCP response.
Send-keys path (interactive sessions) now uses \r correctly and
reports method: "send_keys" vs "resume_subprocess".
2. **--allowedTools**: New AllowedTools field on Instance/InstanceOptions/
proto CreateSessionRequest (field 21). When set, passed as
--allowedTools to the claude CLI at launch. Allows callers to
pre-approve specific tools (e.g. "Bash(git *),Read,Edit") without
the all-or-nothing --dangerously-skip-permissions flag.
3. **--permission-mode**: New PermissionMode field (proto field 22).
Passes --permission-mode to claude at launch. Supports values like
"acceptEdits" (auto-approve file writes only) and "auto" (full
autonomous classification).
Also:
- parseJSONField() generic helper in session_driver.go; parseClaudeSessionID()
now delegates to it instead of duplicating the scan logic
- GetClaudeConversationUUID() thread-safe getter on Instance
- RunWithResume() on Instance: spawns subprocess, parses result, updates UUID
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* fix(rules): correct coverage false-positives and add three-state UX
Three backend bugs in coveredSubcommands() caused items in the "Rule Coverage
Gaps" section to incorrectly show "✓ covered" in the drill-down panel:
1. ToolPattern filter leak: seed rules with ToolPattern=Read|Glob|Grep (no
ToolName) bypassed the Bash-tool guard because the guard only checked
spec.ToolName != "". Now also checks ToolPattern against "Bash".
2. ToolCategory false-positive: rules with ToolCategory="builtin-agent" or
"mcp-read" had their category dropped by ruleToSpec() and leaked through.
Added ToolCategory copy in ruleToSpec() and a ToolCategory guard.
3. CommandPattern="" only marked covered[""] not specific subcommands. A rule
that covers all Bash now marks all knownSubcmds as covered.
Frontend: ProgramDetailPanel coverage column now shows three states — "covered"
(rule exists + no escalated events), "partial" (rule exists but still has gaps),
"gap" (no rule). Adds data-testid for each state.
Tests: 21 new Go tests (TestCoveredSubcommands × 11, TestReclassifyGaps × 5,
TestComputeSummary × 5) and 7 new Jest tests in ProgramDetailPanel.test.tsx.
All 28 new tests pass; no pre-existing tests broken.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* test(rules): add coverage tests for case-insensitive match and bare-program sentinel
Add TC-G-12 (CriteriaPrograms EqualFold regression guard) and TC-G-13
(CommandPattern bare-program match sets covered[""] sentinel). Document
the covered[""] sentinel convention in coveredSubcommands. Fix misleading
ManualReviewRate comment in TC-G-19.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* fix(session): use proper JSON parsing in parseJSONField
Replace manual string-scanning with encoding/json unmarshaling plus
recursive tree search (searchJSONString). This correctly handles escaped
quotes in string values and nested fields like data.session_id in
stream-json output.
Fixes Copilot review comment on parseJSONField escaped-quote truncation.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* docs(plans): add rules-page-robustness planning artifacts
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
---------
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
* chore(bench): update go tier1 baseline [skip ci]
* chore(bench): update e2e latency baseline [skip ci]
* chore(bench): update frontend throughput baseline [skip ci]
* chore(bench): update go tier2 baseline [skip ci]
* chore(demos): update E2E feature GIFs [skip ci]
* fix(notifications): auto-resolve stale toasts and Chrome notifications (#109)
* feat(sessions): Phase 1 programmatic control — \r fix + session_id capture for OneShot
Two improvements to OneShot (-p) session control:
1. **Fix initial prompt not submitting** (\r instead of \n): The session driver
was sending initialPrompt + "\n" (LF) to the PTY. Claude Code's interactive
readline needs \r (CR) to submit — the same signal a physical Enter key sends.
Sessions received the typed text but never executed it, causing inactivity
timeouts. Confirmed by log: "sent initial prompt" followed by 10-min idle.
The startup dialog answers ("1\n") work because those menus handle both,
but Claude's readline interface only responds to \r.
2. **Capture claude session_id from --output-format json output**: OneShot
sessions now launch with -p --output-format json. When the session exits,
the driver parses the JSON output for "session_id" and stores it as
ConversationUUID on the instance. Subsequent restarts automatically use
--resume <uuid>, sending Claude back into the same conversation with full
context instead of re-running the task from scratch.
Supporting infrastructure:
- SetClaudeConversationUUID() — thread-safe setter that fires a save callback
- SetClaudeSessionIDSavedCallback() — wired in service layer to flush to DB
- wireClaudeSessionIDCallback() — registered on all creation paths including
loadInstancesWithWiring (startup) and CreateDirectorySession (backlog)
- Prompt is now appended even when claudeSessionID is set for OneShot
sessions so continuation prompts are delivered after --resume
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* fix(db): make approval_rule JSON fields Optional to fix SQLite migration
New JSON columns (programs, subcommands, etc.) were NOT NULL with no
SQL-level DEFAULT, causing the SQLite copy-table migration to fail for
existing rows. Adding Optional() makes the columns nullable so old rows
can be copied; new rows still get []string{} from the Go-level Default.
Also adds build backup + auto-rollback to install-service:
- make install-service saves stapler-squad.prev before building
- health check polls /health for 15s after service start
- on failure, auto-restores .prev and restarts the service
- make rollback provides a manual escape hatch
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* chore(ent): stop tracking gitignored generated files
These files are auto-generated by ent and covered by .gitignore.
Force-added in the previous commit by mistake.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* fix(notifications): auto-resolve stale toasts and Chrome notifications
Approved/timed-out approvals and cleaned-up stale sessions now clear
their toast and OS-level Chrome notification immediately instead of
lingering until the 6-minute stale timer fires.
Five root causes fixed:
- Approval timeout/cancel arms in ApprovalHandler now publish an
EventApprovalResponse event so connected clients receive a real-time
dismiss signal (RC-1).
- MarkRead is called on all three approval resolution paths (approve,
deny, timeout, cancel), dropping the unread badge count as soon as
the approval settles (RC-3).
- NotificationContext gains removeToastByApprovalId and
removeToastBySessionId; the approval_response stream event handler
calls the former before refreshHistory so the toast vanishes in the
same render cycle (RC-2).
- useReviewQueueNotifications calls removeToastBySessionId for every
session that leaves the review queue, clearing stale-process toasts
when sessions are terminated (RC-4).
- closeNativeNotification() exported from notifications.ts; called on
approval resolution and queue removal to programmatically close the
Chrome OS-level notification (RC-5).
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* refactor(notifications): address code review — fix stale closure, extract helpers, guard empty session
- Fix onSessionDeleted stale closure in useSessionService (was not ref-stabilized unlike onApprovalResponse)
- Guard removeToastBySessionId against empty-string matching sessionId-less notifications
- Extract notificationTag helpers to notifications.ts to avoid duplicated tag format strings
- Extract stampResolved helper in ApprovalHandler to deduplicate timeout/cancel arms
- Consolidate approvalNotificationStamper + notificationMetadataStore into single interface
- Fix fragile nested-select drain loop in TestHandlePermissionRequest_TimeoutPublishesApprovalResponseEvent
- Add guard for empty approvalId in useSessionService approvalResponse handler
- Update useReviewQueueNotifications test mock to include notificationTag
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
---------
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
* chore(bench): update go tier1 baseline [skip ci]
* chore(bench): update e2e latency baseline [skip ci]
* chore(bench): update frontend throughput baseline [skip ci]
* chore(bench): update go tier2 baseline [skip ci]
* feat(workflows): Quick Workflows — @slug omnibar, management UI, cron scheduling (#106)
* feat(workflows): add Quick Workflows — @slug omnibar trigger, management UI, cron scheduling
Introduces a flexible workflow definition system that lets users save named
configurations (skill/command, target directory, input template, session type,
model) and invoke them instantly from the omnibar, a dedicated management panel,
or on a cron schedule.
Key additions:
- `@slug [arg]` omnibar syntax (WorkflowDetector, priority 25, collision-free)
- `/workflows` management page: create/edit/delete workflows via WorkflowForm + WorkflowsPanel
- WorkflowScheduler (robfig/cron/v3) fires one-off sessions on schedule; hot-reloads on CRUD
- 5 new proto RPCs: CreateWorkflow, UpdateWorkflow, DeleteWorkflow, ListWorkflows, RunWorkflow
- ent ORM Workflow entity (auto-migrated on startup, slug UNIQUE index)
- Circular import avoided: WorkflowSchedulerInterface in services/, SessionServiceInterface in workflows/, deferred SetWorkflowService injection
- 64 new tests (Go unit/integration + Jest + slug validation)
- Missed-fire policy: skipped cron runs during downtime are not backfilled (v1 non-goal)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* fix(workflows): address code review findings — safety, validation, tests
BLOCKER/CRITICAL:
- Log Reload errors instead of discarding with _ (B1)
- Use errors.Is(err, session.ErrNotFound) instead of ent.IsNotFound after repo wrapping (B2)
- Add 8-second Stop() timeout to prevent hung server shutdown (C2)
- Rollback optimistic delete on RPC failure in useWorkflows (C3)
- Reject cron_enabled=true with empty cron_expression at validation layer (C4)
- Add ErrConflict sentinel; ent_workflow_repository converts ConstraintError (D3/D5)
- Validate target_directory is absolute and free of traversal components (S1/S2)
MAJOR:
- Add 5-minute timeout to cron callback context (M1)
- Add SessionTypeOneOff constant; scheduler uses it instead of magic string (M4)
- Replace window.confirm with inline confirmDeleteId state in WorkflowsPanel (M5)
- Add Limit(1000) safety cap to ListAll (A4)
Tests:
- mockScheduler struct; TestCreateWorkflow_WithCronEnabled_CallsReload (T1)
- TestRunWorkflow_HappyPath + TestRunWorkflow_NotFound (T2)
- Fix delegation test: create workflow first, assert Len==1 (T3)
- Add multi-field UpdateWorkflow test (T4)
- Add 64-char and 65-char slug boundary cases (T5)
- Add default registry @-input fallthrough test (T6)
- Add analytics.track assertion for run_workflow dispatch (T7)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* fix(workflows): address Copilot PR review — command injection, markers, registry format
- FireNow: include wf.Command in InitialPrompt (was silently dropped; sessions
launched without the core skill/command entirely)
- CreateWorkflow: add Name == "" validation for explicit CodeInvalidArgument
- scheduler.Start: remove duplicate Stop() goroutine (server.go already registers
Stop as a shutdown hook, creating a double-stop risk)
- session_service.go: add // +api: markers to all 5 workflow delegation methods
- WorkflowDetector.ts: add // +feature: marker for registry scanner
- registry JSON: fix filePath→path + add markerLine to match established schema
- WorkflowForm.tsx: tighten slug pattern ([a-z0-9]+(-[a-z0-9]+)*) + add
minLength/maxLength to reject consecutive hyphens and enforce 2–64 length
- go.mod: go mod tidy to mark robfig/cron/v3 as direct dependency
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* chore(deps): update go.sum after ent regeneration
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
---------
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
* chore(bench): update go tier1 baseline [skip ci]
* chore(bench): update e2e latency baseline [skip ci]
* chore(bench): update frontend throughput baseline [skip ci]
* chore(bench): update go tier2 baseline [skip ci]
* fix(detection): detect indented spinners and CR-overwritten esc-to-interrupt (#108)
* feat(sessions): Phase 1 programmatic control — \r fix + session_id capture for OneShot
Two improvements to OneShot (-p) session control:
1. **Fix initial prompt not submitting** (\r instead of \n): The session driver
was sending initialPrompt + "\n" (LF) to the PTY. Claude Code's interactive
readline needs \r (CR) to submit — the same signal a physical Enter key sends.
Sessions received the typed text but never executed it, causing inactivity
timeouts. Confirmed by log: "sent initial prompt" followed by 10-min idle.
The startup dialog answers ("1\n") work because those menus handle both,
but Claude's readline interface only responds to \r.
2. **Capture claude session_id from --output-format json output**: OneShot
sessions now launch with -p --output-format json. When the session exits,
the driver parses the JSON output for "session_id" and stores it as
ConversationUUID on the instance. Subsequent restarts automatically use
--resume <uuid>, sending Claude back into the same conversation with full
context instead of re-running the task from scratch.
Supporting infrastructure:
- SetClaudeConversationUUID() — thread-safe setter that fires a save callback
- SetClaudeSessionIDSavedCallback() — wired in service layer to flush to DB
- wireClaudeSessionIDCallback() — registered on all creation paths including
loadInstancesWithWiring (startup) and CreateDirectorySession (backlog)
- Prompt is now appended even when claudeSessionID is set for OneShot
sessions so continuation prompts are delivered after --resume
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* fix(db): make approval_rule JSON fields Optional to fix SQLite migration
New JSON columns (programs, subcommands, etc.) were NOT NULL with no
SQL-level DEFAULT, causing the SQLite copy-table migration to fail for
existing rows. Adding Optional() makes the columns nullable so old rows
can be copied; …
* feat(notifications): suppress alerts for auto-approved operations Eliminates notification fatigue from classifier-handled tool approvals. Previously, operations like Update/Edit on regular files fired "APPROVAL NEEDED" desktop notifications even when the classifier would immediately auto-allow them, because "Update" was missing from the seed-allow-file-tools regex. Changes: - Add "Update" to seed-allow-file-tools, seed-deny-env-write, and seed-deny-git-internals-write patterns so Claude Code's Update tool is classified correctly (was Escalating → now AutoAllow/AutoDeny) - Add NOTIFICATION_TYPE_AUTO_APPROVED = 13 proto enum value - Add ClassificationResult.Source field (populated from rule.Source) - ApprovalHandler now writes a silent pre-read audit record to NotificationHistoryStore for every auto-allow/deny via new AppendAutoApproved() method — bypasses event bus so no toast fires - Wire SetAutoApprovalLogger in server.go - NotificationPanel: auto_approved records are excluded from the main list and shown in a collapsible "Auto-handled (N)" section - 4 new Go unit tests (T-UNIT-GO-01 through T-UNIT-GO-04) - 2 new TS tests for AUTO_APPROVED mapping and filter exclusion * fix(tmux): treat registry false as advisory to fix post-Start() race DoesSessionExist() previously returned the registry result exclusively when the registry was healthy. After Start() confirms session existence via DoesSessionExistNoCache() (subprocess), the push-based registry may not yet have received the %session-created event. Any DoesSessionExist() call made immediately after Start() returns would hit the healthy registry, get false (event pending), and return false despite the session existing. Fix: when the registry returns false, fall through to cache/subprocess instead of returning false unconditionally. The registry is now advisory for the negative case — if it says YES we trust it, if it says NO we verify via subprocess. This preserves the zero-fork happy path for running sessions while eliminating the race on newly created sessions. --------- Co-authored-by: Tyler Stapler <tystapler@gmail.com>
Resize triggers a terminal re-render and is needed frequently — hiding it inside the ⚙ Dev panel was too many taps away for a routine action.
Change: Move
↔️ Resizefrom the dev panel back intosecondaryActionsso it appears in the desktop secondary group and the mobile overflow row alongside Copy/Paste/Bottom/Clear/Mouse.The dev panel now only contains true diagnostics: Debug, Log Stream, Record, Raw mode selector, and Handedness toggle.
🤖 Generated with Claude Code