Renames the experimental pet overlay to "sidekick" with themed character
names (Claude the Mage, OpenCode the Rogue, Gremlin the Trickster).
Covers IPC channels, preload API, persisted UI state, settings flag,
on-disk userData path, components, and types.
Deletes the standalone pet-overlay design mock.
Co-authored-by: Orca <help@stably.ai>
* feat: add experimental pet overlay
Adds an opt-in 3D pet overlay pinned to the bottom-right. Gated behind
an experimental flag so three.js + GLB models stay out of the renderer
bundle for users who never enable the feature. Ships with four bundled
models plus user-uploaded custom GLBs via a pet:import IPC; a status-bar
segment provides model picker + hide toggle.
See docs/design/pet-overlay.md for the full design.
Co-authored-by: Orca <help@stably.ai>
* refactor(pet): replace 3D GLB models with 2D webp images
Co-authored-by: Orca <help@stably.ai>
* chore(pet): compress pet webp images and remove unused assets
Reduce resources/claude.webp, gremlin.webp, opencode.webp sizes; drop
the obsolete pet-overlay design doc and compress-pet-glb script now
that the GLB pipeline has been replaced by 2D webp images.
Co-authored-by: Orca <help@stably.ai>
---------
Co-authored-by: Orca <help@stably.ai>
Audited and rebased PR 1138. Squashed to remove the original untrusted commit stack; hook execution now requires explicit UI trust or CLI --run-hooks opt-in.
* feat(rate-limits): add Gemini and OpenCode Go usage tracking
- Extend provider union and RateLimitState with 'gemini' and 'opencode-go'
- Add opencodeSessionCookie to GlobalSettings (password input in GeneralPane)
- Add GeminiIcon and OpenCodeGoIcon to status bar icons
- Implement gemini-usage-fetcher: OAuth creds from ~/.gemini/oauth_creds.json,
auto-refresh expired tokens (including server-side 401 retry), loadCodeAssist
project resolution, retrieveUserQuota with pro/flash bucket mapping
- Implement opencode-go-usage-fetcher: cookie-based POST to opencode.ai/_server,
two-step workspace+subscription fetch, regex parse of text/javascript response
- Wire both fetchers into RateLimitService via Promise.allSettled for isolation
- Add formatWindowLabel() replacing hardcoded '5h'/'wk' strings in StatusBar
- Render Gemini and OpenCode Go segments in StatusBar and tooltip
- 28 new tests across fetchers, service, and label formatter
Closes#1022
* fix(persistence): additive merge for statusBarItems on load
New providers (gemini, opencode-go) were invisible to users with existing
settings because the persisted statusBarItems array (without the new entries)
overwrote the defaults. Union saved items with current defaults so new
providers appear automatically after upgrade without user intervention.
* feat(rate-limits): add Gemini multi-bucket usage
Restore provider auth wiring, preserve Gemini model buckets in detailed views, and keep compact status rendering summary-only. Avoid embedding OAuth client secrets by failing closed for auth.json refreshes.
* fix(gemini-oauth): resolve symlinks and use known-path + bundle-dir extraction
Replace the recursive directory walker with two targeted strategies:
1. Known paths: checks explicit Homebrew/Nix/npm layouts without walking.
2. Bundle dir: walks up from the binary to find package.json, then scans
the bundle dir for hash-named oauth2 chunks.
Also threads the refresh token as a plain string through
tryRefreshTokenFromBundle so both the oauth_creds.json and auth.json
paths share one refresh flow without coupling to either struct.
Windows: uses 'where gemini' and splits on newlines for multi-result output.
* fix(status-bar): fix Gemini bucket display — names, window size, model context
Three issues fixed:
1. windowMinutes was computed as time-remaining-until-reset instead of the
fixed window size. Gemini buckets are always 1-hour windows; use the
constant 60 so labels read "93% Pro 1h" instead of "93% 47m".
2. Unknown bucket names now humanize gracefully. Unknown model IDs get the
"gemini-" prefix stripped and title-cased ("gemini-3.0-ultra" → "3.0 Ultra")
instead of showing "Unknown (gemini-3.0-ultra)". Added more known model
mappings (2.0 Flash, 2.0 Flash Lite, 1.5 Pro, 1.5 Flash, Flash Lite).
3. Status bar segment now shows the most-constrained bucket name next to the
percentage so users know which model is the binding constraint.
Pure unit tests for getBucketName/deriveSessionSummary extracted to
gemini-bucket-helpers.test.ts to keep gemini-usage-fetcher.test.ts
under the 300-line lint limit.
* feat(status-bar): show all Gemini buckets individually in status bar
Instead of showing only the most-constrained bucket summary, render each
bucket by name with its remaining percentage (e.g. "Flash 93% · 3.1 Pro Preview 7%").
Falls back to the session/weekly window display for providers without buckets.
* fix(status-bar): show only Flash and 3.1 Pro Preview buckets in Gemini segment
Filter to the two most relevant buckets (Flash + 3.1 Pro Preview) to avoid
cluttering the bar. Falls back to session summary if neither bucket is present.
* fix(icons): replace GeminiIcon with official 2025 multicolor gradient
The previous icon used a simple linear gradient (blue→purple→red).
The official Google Gemini 2025 icon uses 11 blurred ellipses (feGaussianBlur)
stacked under an alpha mask to produce the characteristic multicolor glow effect.
Each instance gets unique filter/mask IDs via a module-level counter to prevent
ID collisions when the icon is rendered multiple times on the same page.
* feat(opencode): add monthly limits, workspace override, and improved cookie handling
* fix(opencode): show correct status when session cookie is missing
* fix(rate-limits): discard stale data on unavailable and show errors in tooltip
* fix(rate-limits): invalidate stale data when opencode config changes
* fix(security): resolve vulnerabilities and harden rate-limit fetchers
* fix: address critical security, performance and concurrency issues identified during code review
* fix(build): resolve claude rate-limit export and broken type definitions
* fix(build): remove unused code in claude-fetcher after rebase
* Fix claude fetcher
* fix: resolve synchronization, performance, and robustness issues in AI providers
- Convert synchronous file I/O to asynchronous to prevent main process blocking
- Implement concurrency limiting in recursive directory copying to avoid EMFILE errors
- Persist refreshed Gemini OAuth tokens to disk and improve project ID resolution
- Fix OpenCode Go fetcher to correctly handle workspace overrides and robustly resolve IDs
- Refine scraping regexes to handle nested objects and improve resilience
- Update status bar bucket names and fix Gemini model mapping typos
- Ensure proper handling of async operations in background services
* feat(gemini): deduplicate quota buckets and update model mappings
* fix(rate-limits): preserve stale data for Gemini and OpenCode Go on fetch errors
* fix: remove out-of-scope Linear changes and restore files to upstream/main
* fix(opencode-go): handle React Flight wire format and duplicate keys
The opencode.ai page uses React Server Components. Usage keys like
monthlyUsage appear twice: once as `key:$R[N]={...}` with real data
and once as `key:null` inside a billing component. Render order varies,
so on refresh the null could appear first, causing monthly to vanish or
show 100% from a sibling sub-object.
Replace flat regex extraction with extractUsageBlock, which:
- Iterates all occurrences of each key
- Skips null assignments (no { in 30-char window after colon)
- Handles the $R[N]= token between colon and opening brace
- Validates usagePercent + resetInSec as direct fields before accepting
Add regression tests using the real React Flight HTML format.
* fix(rate-limits): address PR review feedback from nwparker
Scope reverts (out-of-scope changes removed):
- Revert codex-accounts async refactor (fs-utils.ts, service.ts)
- Revert filesystem-auth.ts path-auth reordering
- Revert filesystem-mutations.ts bundled changes
- Revert repos.ts handler move; keep only -- separator fix
- Revert relay/fs-handler-git-fallback.ts RegExp try/catch
- Revert fs-handler.ts stat→lstat, useFileDeletion.ts !isRemote guard
- Revert persistence.ts statusBarItems additive merge
Feature fixes:
- Validate opencodeWorkspaceId override with ^(wrk|wk)_[A-Za-z0-9]+$ before URL interpolation
- Remove internal 5-min cache from fetchGeminiRateLimits (service polls already)
- Make saveGeminiCredentials atomic using tmp-file + rename pattern
- Add fetchGeneration counter to OpenCode to discard stale mid-flight results
- Add console.warn on safeStorage decrypt failure in persistence.ts
- Add geminiCliOAuthEnabled opt-in setting (default: false) with UI toggle and risk disclosure
- Use per-candidate AbortControllers in opencode-go fetcher
- Add fragility comments to brace-depth parsers in opencode-go fetcher
Refactor (keep under 300-line lint limit):
- Extract gemini-bucket-formatting.ts from gemini-usage-fetcher.ts
- Extract opencode-go-page-scraper.ts from opencode-go-usage-fetcher.ts
Co-authored-by: Orca <help@stably.ai>
* fix(rate-limits): restore OpenCode Go workspace ID regex + cleanup
- opencode-go-usage-fetcher: regex accidentally shipped as `\\s*` (matches
literal backslash-s) instead of `\s*` during the review-feedback commit,
breaking workspace ID extraction when no override is configured —
parseWorkspaceIds always returned []. 10 tests were failing as a result.
- gemini-usage-fetcher: drop unused `_force` parameter left over from the
removed 5-min internal cache; update service.ts call site.
- codex-accounts test fixtures: add missing `geminiCliOAuthEnabled` to
createSettings() to fix pre-existing typecheck errors introduced when
the new setting was added.
Co-authored-by: Orca <help@stably.ai>
* test(rate-limits): stabilize Gemini fetcher tests, silence max-lines
- Rewire Gemini fetcher tests to mock the CLI-credential extractor at the
module boundary instead of stubbing every fs/child_process call. The
extractor is a self-contained dependency with a simple async contract,
and was previously being reached through sync-fs mocks that stopped
matching after the extractor was refactored to node:fs/promises — three
tests were silently failing as a result. With this change, all 17
Gemini fetcher tests pass.
- Replace the "proceeds with empty projectId" test (which asserted that an
empty projectId still hits the quota API — the current fetcher
correctly short-circuits to an actionable error instead) with a test
that documents the new behavior, plus an explicit test for the
geminiCliOAuthEnabled=false unavailable path.
- Add a max-lines disable pragma to service.test.ts matching the one
already on service.ts, so the rate-limit fetch-ordering contract and
its tests remain reviewable as a single unit.
Co-authored-by: Orca <help@stably.ai>
* fix(codex-accounts): revert runtime-home-service async refactor
The review explicitly asked to drop the codex async refactor from this PR
(fires floating promises from the constructor via void, and the prepare*
helpers no longer block on sync completion — that is a behavior change
around account prep racing with launch that deserves its own design
discussion, not a drive-by).
Earlier reverts removed fs-utils.ts and service.ts but left
runtime-home-service.ts untouched, which was causing the matching
runtime-home-service.test.ts suite (11 tests) to fail in CI. This restores
the file to main so the suite is green and the PR stays scoped to the
Gemini / OpenCode Go feature.
Co-authored-by: Orca <help@stably.ai>
---------
Co-authored-by: Orca <help@stably.ai>
Co-authored-by: Neil <4138956+nwparker@users.noreply.github.com>
* Render GitHub markdown safely
Co-authored-by: Orca <help@stably.ai>
* Support PR line comment ranges in diffs
Co-authored-by: Orca <help@stably.ai>
* Add GitHub item dialog review workflow
Co-authored-by: Orca <help@stably.ai>
* Make attribution tests environment-safe
Co-authored-by: Orca <help@stably.ai>
* Wire GitHubItemDialog for row clicks and bound file cache
Row clicks in the Tasks GitHub list now open the new GitHubItemDialog
instead of the legacy GitHubItemDrawer, making the PR review workflow
reachable from the main entry point. The unused drawer and its state
are removed.
Also bounds prFileContentCache at 64 entries with LRU eviction so the
module-level map can no longer grow without limit across many PR
openings in a session.
Co-authored-by: Orca <help@stably.ai>
* Convert GitHubItemDialog to a right-side drawer
Replaces the centered Dialog modal with a Sheet drawer (side=right,
max-w 960/1100/1280 across breakpoints) so the review surface slides
in from the side instead of popping over the task list.
Co-authored-by: Orca <help@stably.ai>
* Address review feedback on GitHub work-item details
- Restore full pagination in listAssignableUsers (REST /assignees --paginate)
so repos with >100 assignees no longer silently drop users from the picker.
- Drop redundant REST /users/<login> fan-out in getMentionParticipants; the
aliased GraphQL query already returns login/name/avatarUrl.
- Parallelize mention-participant lookup with checks/participants fetch.
- Avoid mutating caller-provided objects in mergeGitHubUsers.
- Type addIssueComment / addPRReviewComment(Reply) preload wrappers as
Promise<GitHubCommentResult> instead of Promise<unknown>.
- Use typeof startLine === 'number' to harden the review-comment guard.
Co-authored-by: Orca <help@stably.ai>
---------
Co-authored-by: Orca <help@stably.ai>
Co-authored-by: Jinjing <6427696+AmethystLiang@users.noreply.github.com>
* fix(pty): honor Windows shell selection on the daemon path + shell-specific icons
The "+" menu picker and the Settings → Default Shell preference both set a
shellOverride, but the daemon-backed PTY path never forwarded it. Every
Windows terminal ended up as PowerShell (or cmd.exe via COMSPEC) no matter
what the user picked.
Fix:
- Thread shellOverride through daemon-pty-adapter → createOrAttach RPC →
terminal-host → pty-subprocess, and actually resolve the correct launch
args (chcp for CMD, $PROFILE dot-sourcing for PowerShell, /mnt/<drive>
cwd translation for WSL) on the daemon path.
- Extract the Windows shell-args decision into a shared helper
(resolveWindowsShellLaunchArgs) so LocalPtyProvider and the daemon
spawner cannot drift again.
- In ipc/pty.ts, fall back to the persisted terminalWindowsShell setting
when no per-tab override is sent, so the daemon path honors the user's
Default Shell preference the same way LocalPtyProvider already did.
UI polish on the "+" dropdown and tab strip:
- Drop the "Default" tag next to the top entry (takes too much space).
- Rename "Command Prompt" → "CMD Prompt" to fit next to the Ctrl+T hint.
- Replace the generic terminal glyph with brand-style icons per shell
(ShellIcon). Both the "+" menu and the per-tab strip use the same
icon set so a WSL tab is visually distinct from a PowerShell tab.
Co-authored-by: Orca <help@stably.ai>
* fix(daemon): add oxlint-disable max-lines to daemon-server
CI counts 312 non-blank/non-comment lines for daemon-server.ts after the
shellOverride plumbing was added, exceeding the 300-line .ts override.
The file is a single RPC route table; splitting it would leak the host
reference across modules for no readability win, so add a scoped disable
with the rationale.
Co-authored-by: Orca <help@stably.ai>
* chore(lint): disable max-lines on pre-existing 312-line tabs-hydration test
Unrelated to the shell-selection fix, but surfaced on PR CI: the file is
right at oxlint's 300-line .ts ceiling; each case in the table is a
minimal fixture + assertion, so splitting it across files would scatter
closely related regression coverage for a single reducer.
Co-authored-by: Orca <help@stably.ai>
* chore(lint): disable max-lines on shared text-search module
The shared text-search module exceeds oxlint's 300-line .ts ceiling. It
is the single source of truth for rg arg construction, rg --json parsing,
git-grep submatch parsing, and relative-path normalization shared between
the local main process and the SSH relay. Re-splitting it would
re-introduce the maxBuffer divergence the design doc explicitly calls out.
Also reverts two speculative oxlint-disable directives on daemon-server
and tabs-hydration.test — those files were not the offender; CI's error
message elides the file path but running the lint locally against the
PR-merge commit pinpointed text-search.ts.
Co-authored-by: Orca <help@stably.ai>
---------
Co-authored-by: Orca <help@stably.ai>
Extracts rg and git-grep search logic into src/shared/text-search.ts so
the local main and SSH relay paths stop reinventing arg construction,
JSON parsing, submatch regex, and accumulator/truncation semantics.
Fixes silent truncation in the relay: searchWithRg used execFile with
a 50MB maxBuffer cap that rg --json easily exceeds on large repos,
dropping matches with no error surfaced to the user. The relay now
streams via spawn, matching the local path.
See docs/design/share-text-search.md for full rationale.
Co-authored-by: Orca <help@stably.ai>
* feat(settings): add Ghostty config import
Add safe one-shot Ghostty import with preview and success summary,
and probe documented Ghostty config paths before applying changes.
Refs #958
* chore(git): ignore atl artifacts
* feat(settings): map font-weight, cursor-blink and focus-follows-mouse from Ghostty
Adds three safe direct-mapping Ghostty keys that have clear equivalents
in GlobalSettings: font-weight, cursor-style-blink, and focus-follows-mouse.
Refs #958
* feat(settings): expand Ghostty import to support colors, opacity and option-as-alt
- Add TerminalColorOverrides type grouping 21 optional xterm ITheme fields
- Add terminalBackgroundOpacity, terminalPanePaddingColor, terminalPaddingBalance
to GlobalSettings
- Extend parser to collect repeated keys as string[] (needed for palette lines)
- Map background-opacity, background, foreground, cursor-color,
selection-background/foreground, palette (0-15), window-padding-color,
window-padding-balance, macos-option-as-alt in mapper
- Merge terminalColorOverrides into xterm ITheme at theme resolution; apply
opacity as rgba() with allowTransparency enabled
- Accept hex colors with or without leading # (Ghostty omits it)
- Fix preview diff to use deep equality for object values so already-applied
color overrides no longer reappear on next import
* feat(settings): support background-blur-radius and window-padding-color extend in Ghostty import
- Map background-blur-radius > 0 to windowBackgroundBlur: true; apply
vibrancy on macOS and backgroundMaterial acrylic on Windows at window
creation (blur requires restart — no hot-reload IPC exists)
- Accept window-padding-color = extend/background as valid Ghostty values;
both map to default Orca padding behavior (undefined field) instead of
landing in unsupportedKeys
- Split mapper.test.ts into domain-scoped describes to stay under 300-line limit
* feat(settings): add Window section to Terminal settings panel
Expose terminalBackgroundOpacity, windowBackgroundBlur, terminalPaddingBalance,
terminalPanePaddingColor, and terminalColorOverrides in the settings UI so
imported Ghostty values can be viewed and changed manually.
- New TerminalWindowSection component (extracted from TerminalPane to stay
under the 400-line limit)
- Collapsible color overrides sub-section with ColorField for all 21 xterm
ITheme fields grouped as base, ANSI normal, and ANSI bright
- Reset button clears all color overrides at once
- Window blur toggle shows restart-required note (blur applies at window
creation, no hot-reload IPC exists)
- Search entries added for all new controls
* feat(settings): expand Ghostty import with scrollback, padding, divider, cursor and word-chars keys
Map 9 additional Ghostty keys to Orca settings:
- split-divider-color → terminalDividerColorDark + terminalDividerColorLight
(single value applies to both; Ghostty has no dark/light distinction)
- unfocused-split-opacity → terminalInactivePaneOpacity (direct float 0-1)
- scrollback-limit → terminalScrollbackLimit; applied to xterm scrollback option
- window-padding-x / window-padding-y → terminalPaddingX/Y; applied as CSS
vars --pane-padding-x / --pane-padding-y in terminal.css
- cursor-text → terminalColorOverrides.cursorAccent (xterm ITheme field)
- bold-color → terminalColorOverrides.bold (persisted; xterm ITheme has no
bold field yet — stored for future xterm upgrade)
- cursor-opacity → terminalCursorOpacity; blended into cursor rgba at theme
resolution time
- selection-word-chars → terminalWordSeparator; applied to xterm wordSeparator
- mouse-hide-while-typing → terminalMouseHideWhileTyping field added; renderer
application deferred (needs per-pane disposable + global mousemove listener)
* feat(settings): expose new Ghostty-imported settings in Terminal Settings UI
- Window section: scrollback limit, horizontal/vertical padding, hide mouse
while typing toggle, cursor text and bold color in Color Overrides
- Cursor section: cursor opacity NumberField
- Advanced section: word separators text input
- Search entries added for all new controls
- terminalDividerColorDark/Light and terminalInactivePaneOpacity skipped —
already present in Theme and Pane Styling sections respectively
* feat(terminal): implement mouse-hide-while-typing per pane
Register terminal.onData → cursor:none and mousemove → restore, scoped to
the pane container element. Uses the existing IDisposable per-pane pattern
(same as selectionDisposablesRef). Cleans up on pane close and effect teardown.
* refactor(settings): address ghostty import code review findings
- Centralize GhosttyImportPreview type in shared/types (remove duplicate from mapper)
- Fix parser to strip inline comments without breaking hex color values (#1a1a1a)
- Extract HEX_COLOR_RE to shared/color-validation to avoid duplication
- Remove redundant Number.isNaN checks after Number.isFinite (4 sites)
- Replace unsafe catch-all assignment with explicit font-family branch
- Migrate 280-line if-chain in mapGhosttyToOrca to FIELD_PARSERS registry
- Add human-readable setting labels in GhosttyImportModal via setting-labels map
- Add clarifying comment in index.ts re JSON.stringify undefined behavior
* fix(settings): harden ghostty import from judgment-day review
- Surface readFile errors in GhosttyImportPreview.error instead of
showing misleading 'No config found' on permission denied
- Guard handleApply against double-apply when already applied
- Strip surrounding quotes from parsed config values (font-family)
- Return null from palette handler when all entries fail validation
- Inform user when background-blur-radius radius is not preserved
- Add valuesEqual key-order stability via stableStringify
- Normalize hex colors to #-prefixed format across all color mappers
- Reject blank values before numeric parsing (Number('') === 0 trap)
- Remove selection-word-chars mapping (inverted xterm semantics)
- Guard window-padding-x/y against negative integers
- Reactive mouse-hide-while-typing on existing panes when setting toggles
- Merge terminalColorOverrides on import instead of replacing
* fix(ghostty): drop broken imports, tighten parsing, prompt restart for blur
Review found three high-impact issues in the Ghostty import: scrollback-limit
semantics are inverted/rescaled (Ghostty is bytes with 0=unlimited, xterm is
rows with 0=disabled), and window-padding-color + window-padding-balance set
CSS custom properties (--pane-padding-color, --pane-padding-balance) that
have no consuming rule anywhere in the tree — so users confirming "changes"
to those keys would see nothing happen.
Because none of the three keys have a safe mapping today, drop them from the
import and remove the dead UI controls + GlobalSettings fields + CSS var
plumbing. The mapper now lists them as unsupportedKeys alongside the
existing window-decoration / keybind / custom-shader entries.
Other fixes in the same review:
- allowTransparency now clears when background-opacity returns to 1 (prior
code only ever set it to true, leaving a stale flag with measurable render
cost).
- background-blur-radius = 0 no longer emits a misleading "radius value not
preserved" note (0 cleanly maps to blur=false with no radius to lose).
- Add a 1 MB size cap on the config read so a pathological or symlinked file
cannot OOM the main process.
- Make handleApply async and surface IPC errors inline in the modal instead
of flipping straight to "Import complete" on failure.
- Type settings.previewGhosttyImport as Promise<GhosttyImportPreview> in
preload so shape drift is caught at compile time.
- Make stableStringify recursive so future nested settings round-trip
cleanly through valuesEqual.
- Restrict window-padding-x/y and background-blur-radius to decimal ints;
prior code accepted exponent notation (1e10 sails through Number.isInteger)
and would have landed absurd values in the store.
- Window blur now shows a "Restart required" banner with a Restart now
button when the setting differs from the mount-time snapshot, mirroring
the ExperimentalPane daemon pattern. Blur only applies at BrowserWindow
creation on macOS/Windows.
Co-authored-by: Orca <help@stably.ai>
* feat(settings): move Ghostty import trigger to Terminal section header
Per review feedback: the "Import from Ghostty" row was taking its own slot
in the Terminal settings list alongside real configuration sections. Move
the trigger into the Terminal section's header (upper-right corner) as a
headerAction, next to the Terminal heading — it's a one-shot action, not a
setting.
- SettingsSection gains an optional `headerAction` slot rendered to the
right of the section title/description.
- The useGhosttyImport hook is lifted from TerminalPane into Settings.tsx
so the section header button (owned by Settings.tsx) and the modal
(still rendered inside TerminalPane) share one state instance.
- TerminalPane drops its own "Import" section + the TERMINAL_GHOSTTY_IMPORT
search entry group is no longer referenced there.
- Button carries the official Ghostty mark as a 16x16 icon so it reads
clearly as a cross-app import even before users parse the label.
useGhosttyImport now accepts `GlobalSettings | null` so the parent can call
it above the pre-load spinner guard without violating hook ordering; the
apply path no-ops until settings arrive. Related test file updated to
pass the new `ghostty` prop and to assert the trigger is *not* rendered
inside TerminalPane anymore.
Co-authored-by: Orca <help@stably.ai>
---------
Co-authored-by: Neil <4138956+nwparker@users.noreply.github.com>
Co-authored-by: Orca <help@stably.ai>
* feat(sidebar): improve PR checks and comments
Co-authored-by: Orca <help@stably.ai>
* fix(sidebar): classify bot comments via GitHub user.type instead of login heuristic
Third-party review bots like qodo-ai-reviewer, coderabbitai, and sonarcloud
don't follow the [bot] suffix or "bot"/"automation" substring convention, so
the regex-based detector misclassified them as human. Plumb REST
`user.type === 'Bot'` and GraphQL `author.__typename === 'Bot'` through to the
renderer as an authoritative isBot flag; fall back to the login heuristic only
when the data source can't report it.
Co-authored-by: Orca <help@stably.ai>
* fix(sidebar): allowlist AI review services that sign in as User accounts
qodo-ai-reviewer, coderabbitai, codium-ai and similar third-party review
services register as regular GitHub user accounts, so REST `user.type` is
"User" and their logins contain no "bot"/"automation" tokens. The previous
fix relied on the GitHub-reported type, which fails for these. Add an
explicit substring allowlist of known automation services so they still
land in the Bots tab.
Co-authored-by: Orca <help@stably.ai>
---------
Co-authored-by: Orca <help@stably.ai>
Co-authored-by: Jinjing <6427696+AmethystLiang@users.noreply.github.com>
* feat(cursor): first-class Cursor CLI agent status via ~/.cursor/hooks.json
Give cursor-agent the same hook-driven status pipeline Claude/Codex/Gemini/
OpenCode already use so working/done/permission transitions show the proper
sidebar spinner instead of falling back to title heuristics — cursor-agent
only sets its OSC title to the literal string "Cursor Agent" during a turn,
so title-based detection cannot see working→done transitions on its own.
- New CursorHookService installs a managed shell script and registers it
under ~/.cursor/hooks.json for beforeSubmitPrompt, preToolUse, postToolUse,
postToolUseFailure, beforeShellExecution, beforeMCPExecution, stop, and
afterAgentResponse (the subset that marks turn boundaries and surfaces
in-flight tool context).
- AgentHookServer gains a /hook/cursor route and a normalizeCursorEvent
mapping the camelCase cursor events to working/done/waiting, with tool
previews for preToolUse/shell/MCP and lastAssistantMessage from
afterAgentResponse. stop with status != "completed" surfaces as
interrupted (matches Claude's is_interrupt behavior).
- cursorHookService joins the startup install loop alongside Claude/Codex/
Gemini, and the IPC status handler is exposed via preload.
- 'cursor' added to WellKnownAgentType and to the renderer's
WELL_KNOWN_LABELS so the dashboard prints "Cursor" rather than the raw
'cursor' id.
Verified end-to-end against a real cursor-agent 2026.04.17-787b533 binary:
a mock hook receiver pointed at by ~/.cursor/hooks.json observes
beforeSubmitPrompt → stop for a live turn.
Co-authored-by: Orca <help@stably.ai>
* feat(cursor): wire hook events into sidebar spinner + unread pipeline
The initial hook wiring landed behind AGENT_DASHBOARD_ENABLED, which is
still false. That meant cursor-agent panes lit up no spinner and no
unread indicator — cursor's native OSC title stays literally "Cursor
Agent" across a turn, so title-based detection cannot transition.
Plumb cursor's hook stream into the existing, shipped title-tracker
pipeline (the one Claude/Codex/Pi drive working/idle/unread off) by
synthesizing OSC title sequences in the main-process hook listener:
- `working` → `\x1b]0;⠋ Cursor Agent\x07` (braille prefix → working)
- `waiting` → `\x1b]0;Cursor - action required\x07\x07`
- `done` → `\x1b]0;Cursor ready\x07\x07`
The two trailing BELs on done/waiting are load-bearing: the unread badge
keys off BEL (0x07 outside any OSC), and cursor-agent emits none on its
own. The first BEL is consumed as the OSC terminator; the second fires
the bell detector.
Also treats the bare native "Cursor Agent" title as a no-op in
`detectAgentStatusFromTitle` so cursor's own per-turn re-emissions cannot
stomp our synthesized working state back to idle. `isClaudeAgent`
excludes cursor-bearing braille titles so the Claude prompt-cache timer
doesn't fire for cursor panes.
Scope: the hook server + cursor install run unconditionally now, but
Claude/Codex/Gemini installs stay gated behind AGENT_DASHBOARD_ENABLED,
so only cursor events flow through the pipeline. No dashboard surface
is turned on.
Verified end-to-end in Electron (dev build): launched cursor-agent via
the Cursor menu item, submitted three prompts (including a tool-use
turn reading package.json). Observed tab title flip to "Cursor ready"
on done, and the tab + worktree both transitioned to unread
(unreadTerminalTabs[tabId]=true, worktree.isUnread=true). Parallel
Claude Code pane untouched.
Co-authored-by: Orca <help@stably.ai>
* fix(cursor): animate spinner frames + filter bare native title so the spinner doesn't go solid mid-turn
cursor-agent re-emits its bare "Cursor Agent" OSC title on every internal
redraw, which was stomping the single synthesized "⠋ Cursor Agent" frame
in runtimePaneTitlesByTabId within milliseconds and flipping the sidebar
dot back to solid. Two-part fix:
- Main: drive an 80ms Pi-style braille spinner from the cursor hook
channel, keyed by paneKey, torn down on pty exit via a new
registerPaneKeyTeardownListener hook in ipc/pty.
- Renderer: drop bare "Cursor Agent" titles in pty-transport so cursor's
native re-emissions cannot overwrite the synthesized working/idle
titles.
Co-authored-by: Orca <help@stably.ai>
---------
Co-authored-by: Orca <help@stably.ai>
Pi's titlebar extension emits OSC 0 titles every 80ms during agent work
(`⠋ π - cwd`, `⠙ π - cwd`, …) and a trailing idle title on `agent_end`
(`π - cwd`). Node-pty on the main side batches output every 8ms, so the
renderer frequently receives multiple title updates in a single `pty:data`
chunk.
The pty-transport's data handler used `extractLastOscTitle`, which returns
only the final OSC title in a chunk. For fast agents (e.g. Pi with a small
local model) the whole working→idle cycle often lands in one IPC payload —
the intermediate working frames were silently dropped, and the worktree
card's `detectAgentStatusFromTitle` only ever saw the idle title.
Empirically on macOS: polling `runtimePaneTitlesByTabId` 20×/0.25s during
a multi-second Pi working window showed the stored title stuck at "Pi"
throughout, while a raw `ipcRenderer.on('pty:data')` listener captured
the working frames flowing through IPC correctly. After the fix, the same
polling captures "⠋ Pi" alongside "Pi".
- Add `extractAllOscTitles` in the shared agent-detection module and feed
every OSC title in the chunk through `applyObservedTerminalTitle` in
order, so the working→idle transition reaches the store and the agent
tracker.
- Keep `extractLastOscTitle` exported for the main-process stats tracker
and orca-runtime, which only care about the most recent title.
- Pin the regression with three focused tests: per-chunk spinner frames,
dispatcher-routed end-to-end, and the exact coalesced-chunk scenario
that used to swallow the working state.