* 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>
Keeping the menu bar always visible on Windows/Linux added a dedicated
row of vertical space on every launch, which users didn't want. Restore
the native convention (File Explorer, Firefox, etc.): menu bar is
hidden by default and revealed with Alt. The previous PR's template
restructure (File → Settings/Exit, Help → About/Check for Updates,
no redundant "Orca" entry) still applies when the menu is shown.
Co-authored-by: Orca <help@stably.ai>
* 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>
Ctrl+PageUp/PageDown and Cmd+Shift+]/[ now always preventDefault and stop
propagation, even when the switch is a no-op (e.g. single terminal). xterm
otherwise translates these into escape sequences that leak into the shell
and flip the tab's bell indicator. Also skip redundant setActiveTab writes
in handleSwitchTerminalTab when the target is already active.
Co-authored-by: Orca <help@stably.ai>
electron-builder fetches the Electron binary and its platform tools
(notarytool, winCodeSign, nsis, squirrel, AppImage) on every run.
Cache those per-platform so repeat releases skip the re-download.
Saves ~30-90s per job, including the macOS long pole.
Key includes pnpm-lock.yaml so a bump to electron/electron-builder
invalidates stale cached binaries; restore-keys falls back to the
latest per-platform cache in between lockfile changes.
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.
PR #1053 fixed RC→RC by switching prerelease users to electron-updater's
native github provider with allowPrerelease=true. That silently broke
RC→stable: GitHubProvider.getLatestVersion() filters the atom feed by
channel, so when the running build's channel is "rc" any stable release
(channel=null) gets skipped — trapping the user on the RC channel.
Fix: keep the generic provider and resolve the newest tag ourselves by
parsing GitHub's /releases.atom (any channel, semver-newest strictly >
current version). For prerelease users we re-pin the feed to
/releases/download/<tag>/ before each check, which handles BOTH the
RC→newer-RC case from #1053 and the RC→stable case it broke. Stable
users keep the default /releases/latest/download/ feed untouched, and
the Shift-click RC opt-in path (enableIncludePrerelease) is unchanged.
Co-authored-by: Orca <help@stably.ai>
The action refuses to run unless GITHUB_EVENT_NAME=="pull_request",
so workflow_dispatch no-ops. Backfill was completed out-of-band via
a local script calling the REST labels endpoint; this one-off
workflow is no longer useful and is being deleted.
Co-authored-by: Orca <help@stably.ai>
A manually-dispatched workflow that fans out across all open PRs and
runs the size labeler against each. This is a one-time verification
of the labeler against the existing PR set; the workflow will be
removed in a follow-up after it runs.
Co-authored-by: Orca <help@stably.ai>
Adds a GitHub Actions workflow and config that labels every PR with
size/xs..size/xl based on files changed and lines added, so reviewers
can see at a glance how big a PR is. Lock files and generated/vendored
paths are excluded, and deletions aren't counted so refactors that
remove code aren't over-penalized.
Co-authored-by: Orca <help@stably.ai>
The CLI is compiled by tsc (not bundled) and launched via
ELECTRON_RUN_AS_NODE, which bypasses Electron's asar integration. After
#1090 introduced zod for envelope validation, the packaged CLI crashed
with MODULE_NOT_FOUND because zod was inside app.asar. Add it to
asarUnpack so require() can resolve it from app.asar.unpacked/node_modules.
Follow-up tracked in #1097 to add CI smoke-testing of the packaged CLI.
Co-authored-by: Orca <help@stably.ai>
* refactor(cli): split runtime-client.ts into runtime/ subsystem
Break the 413-line src/cli/runtime-client.ts into focused modules under
src/cli/runtime/:
- types.ts — RuntimeRpcSuccess/Failure, RuntimeClientError,
RuntimeRpcFailureError
- metadata.ts — readMetadata / tryReadMetadata /
getDefaultUserDataPath
- transport.ts — sendRequest: Unix-socket newline-framed JSON with
id and runtimeId verification and timeout handling
- status.ts — getCliStatus + buildCliStatusResponse +
isProcessRunning
- launch.ts — launchOrcaApp + macOS .app-bundle resolution +
ELECTRON_RUN_AS_NODE env handling
- client.ts — RuntimeClient class, now a thin composer
- index.ts — subsystem barrel
runtime-client.ts becomes a backward-compat re-export barrel so
src/cli/index.ts and the existing tests import the same symbols from
the same path. No behavior changes.
Motivation: the file had an eslint-disable max-lines override and
mixed five concerns (envelope types, wire transport, metadata I/O,
status aggregation, cross-platform app launch). Splitting them makes
each concern independently testable and unblocks adding schema
validation at the RPC boundary.
* feat(cli): validate runtime RPC envelope with Zod at decode boundary
Add RuntimeRpcEnvelopeSchema and apply it inside sendRequest so every
response frame is validated against the id/ok/result/error/_meta shape
before the CLI hands it to the caller. The payload (`result`) is left
as unknown — the TResult generic remains the caller's responsibility —
so only the envelope itself is the contract this schema enforces.
Motivation: the CLI and the Orca main runtime are separate processes
and can drift in version (older CLI vs newer app, or vice versa during
dev HMR). A malformed or partial frame used to risk mis-typed field
access downstream; it now surfaces as a single structured
`invalid_runtime_response` error.
Behavior:
- Well-formed success and failure frames continue to decode unchanged.
- Failure frames without `_meta` are accepted (the runtime may fail
before resolving its own runtimeId).
- Valid JSON that does not match the envelope shape now rejects with
`invalid_runtime_response`, matching the existing error code for
non-JSON frames.
Tests: adds a pure schema test file
(src/cli/runtime/envelope-schema.test.ts) covering accept/reject cases.
The existing integration tests in runtime-client.test.ts continue to
pass unchanged.