chore: add bug reproduction handoffs (tests + writeups)

Agent-facing reproduction kits for open has_repro issues: unit tests under
src/**/repro-*.test.ts and writeups under docs/bug-reproductions/.
This commit is contained in:
Neil
2026-07-16 11:37:08 -07:00
parent f62ad69260
commit 2ab513cd9e
117 changed files with 7128 additions and 0 deletions
+38
View File
@@ -0,0 +1,38 @@
# #8299 — macOS Shift+Space input-source switch also sends a space
**Status:** REPRODUCED (code-level)
**Issue:** https://github.com/stablyai/orca/issues/8299
**Malicious content:** none
## Summary
With Shift+Space as Korean/English input-source switch, Orca switches language **and** inserts a literal space into the PTY. Ghostty does not.
## Root cause
Ghostty reuses the original `NSEvent`, records `KeyboardLayout.id` around `interpretKeyEvents`, and returns without encoding when the source changed.
Orca’s xterm path:
- `attachCustomKeyEventHandler` + `shouldBypassXtermKeyboardEvent`
- **No** per-event input-source / layout-id change guard
- Shift+Space is **not** bypassed → xterm encodes Space to the PTY
IME 229 comments mention “input-source switch” for composition helper scheduling, but that is unrelated to consuming the switch chord.
## Automated proof
```bash
pnpm exec vitest run --config config/vitest.config.ts \
src/renderer/src/components/terminal-pane/repro-8299-shift-space-input-source.test.ts
```
## Suggested fix
On macOS, track current input-source id (existing IPC/readers under `keyboard-layout/`). On keydown of bare Shift+Space (and similar switch chords), if source id changed (or is a known switch chord), return false from the custom handler **and** preventDefault so no space reaches the PTY.
## Evidence
- `src/renderer/src/components/terminal-pane/repro-8299-shift-space-input-source.test.ts`
- `src/renderer/src/components/terminal-pane/xterm-bypass-policy.ts`
- `src/renderer/src/components/terminal-pane/use-terminal-pane-lifecycle.ts`
+31
View File
@@ -0,0 +1,31 @@
# #8335 — Terminal stuck after backgrounding Orca while Claude external editor open
**Status:** REPRODUCED (code-level reattach/mouse-mode path)
**Issue:** https://github.com/stablyai/orca/issues/8335
**Related:** #8104, #8198, #7443
**Malicious content:** none
## Summary
Open Claude Code external editor (`Save and close editor to continue...`), background Orca / switch tabs, return → pane stuck: typing ignored, mouse motion floods buffer with literal `^[[<35;x;yM`. Only recreate terminal recovers (kills Claude session).
## Root-cause chain
| Step | Code | Behavior |
|------|------|----------|
| 1 | `buildRehydrateSequences` | Re-arms last-observed mouse modes (`?1003` any + `?1006` SGR) |
| 2 | Claude pre-editor | Armed mouse tracking; did not disable before `$EDITOR` |
| 3 | `shouldPreserveAgentReattachModes()` | True while title still classifies as live agent |
| 4 | `reattachReplayResetSequence` | Uses `buildPostReplayLiveAgentReattachReset` — **omits** `RESET_MOUSE_REPORTING` |
| 5 | Result | Motion reports echoed as literal input (documented in `layout-serialization.ts`) |
## Automated proof
```bash
pnpm exec vitest run --config config/vitest.config.ts \
src/renderer/src/components/terminal-pane/repro-8335-agent-editor-mouse-preserve.test.ts
```
## Suggested fix (from issue)
Reset mouse reporting on reattach when foreground is not the agent TUI (external editor wait), while preserving mouse for a genuinely live agent TUI.
+41
View File
@@ -0,0 +1,41 @@
# #8372 — Outdated running CLI count on bottom panel
**Status:** REPRODUCED (code-level dual-source mismatch)
**Issue:** https://github.com/stablyai/orca/issues/8372
**Malicious content:** none
## Summary
After clearing orphan CLIs in Resource Manager, the open popover count is correct but the **closed** status-bar badge stays outdated (restart may not help if session bindings rehydrate).
## Root cause
```ts
const triggerSessionCount = open ? sessions.length : closedSessionCount
```
| Mode | Source |
|------|--------|
| Open | `window.api.pty.listSessions()` → live daemon |
| Closed | `createClosedResourceSessionCountSelector` → **store-bound** PTY IDs only (`tabs.ptyId`, `ptyIdsByTabId`, layouts) |
Closed path **never** reconciles against daemon inventory (by design, to avoid background `listSessions`). Dead wake-hint / layout PTY IDs inflate the badge. Kill-orphan only updates local `sessions` + `pty.kill` — does not clear store bindings.
## Automated proof
```bash
pnpm exec vitest run --config config/vitest.config.ts \
src/renderer/src/components/status-bar/repro-8372-closed-cli-count-stale.test.ts
```
## Suggested fix
- On popover close after kills, cache last `sessions.length` for the badge, **or**
- Prune store bindings for PTYs confirmed dead, **or**
- Occasional cheap reconcile of closed count against daemon when it changed.
## Evidence
- `src/renderer/src/components/status-bar/repro-8372-closed-cli-count-stale.test.ts`
- `src/renderer/src/components/status-bar/ResourceUsageStatusSegment.tsx`
- `src/renderer/src/components/status-bar/resource-session-count-selector.ts`
+15
View File
@@ -0,0 +1,15 @@
# #8378 — Codex session reset time mismatch (popup vs status bar)
**Status:** REPRODUCED
**Label:** `has_repro`
## Summary
Status bar chip shows fixed window size (`formatWindowLabel(windowMinutes)` → `"5h"` for Codex’s 300-minute session). Popup shows remaining time (`formatResetCountdown(resetsAt - now)` → e.g. `"Resets in 2h 33m"`). Same `RateLimitWindow`, two different formatters → reporter’s 2h 33m vs 5h.
## Re-run
```bash
pnpm exec vitest run --config config/vitest.config.ts \
src/shared/repro-8378-codex-reset-time-mismatch.test.ts
```
+35
View File
@@ -0,0 +1,35 @@
# #8399 — Option+L does not type `@` in Pi (German layout)
**Status:** REPRODUCED (code-level; same mechanism as #8733)
**Issue:** https://github.com/stablyai/orca/issues/8399
**Malicious content:** none
## Summary
German macOS layout: Option+L → `@`. Works in bare Orca terminal; fails once Pi is running. Works in Ghostty.
## Root cause
Same as #8733: Pi enables kitty keyboard protocol. With `macOptionAsAlt !== 'true'`, Orca still forces:
```text
Option+L (composed key '@') → \x1b[108;3u (physical 'l' + alt)
```
instead of letting Chromium deliver `@`. Without kitty, the same event returns `null` from the shortcut policy (composition path).
## Automated proof
```bash
pnpm exec vitest run --config config/vitest.config.ts \
src/renderer/src/components/terminal-pane/repro-8733-8399-kitty-option-compose.test.ts
```
## Suggested fix
Share fix with #8733: do not kitty-encode compose Option when the user disabled Option-as-Alt, or only for known US hotkeys.
## Evidence
- `src/renderer/src/components/terminal-pane/repro-8733-8399-kitty-option-compose.test.ts`
- `src/renderer/src/components/terminal-pane/terminal-shortcut-policy.ts`
+27
View File
@@ -0,0 +1,27 @@
# #8440 — OAuth Login Not Persisting for MCPs in Orca Terminals
**Status:** NOT REPRODUCED
**Issue:** https://github.com/stablyai/orca/issues/8440
**Malicious content:** none
## Summary
Discord report: MCP OAuth completes in Orca terminals but subsequent MCP calls report not logged in; persists across new terminals.
## Attempted code-level investigation
- MCP config inspection (`src/shared/mcp-config.ts`) only lists/validates server configs — no OAuth token store of its own.
- Agent OAuth/token machinery in-tree is Claude/Codex account scoped (`CLAUDE_CONFIG_DIR`, Codex `auth.json`), not a generic “MCP OAuth persistence” layer.
- No single broken path that would always drop MCP OAuth tokens across all Orca terminals without knowing which agent/MCP stores credentials where.
## Maintainer status (issue thread)
Jinwoo-H: *could not reproduce with an isolated OAuth MCP across new Orca terminals or restart*; asked for agent name/version, OS/SSH, and credential-storage path inside vs outside Orca.
## Why cannot_reproduce
Insufficient reporter detail + no definitive product-wide defect in tree. Needs user-supplied MCP/agent credential path divergence (or a concrete server that fails) to reclassify.
## Ask if reopening
Agent CLI + version, OS/SSH host, MCP server name, where tokens land outside Orca vs inside (`echo $HOME`, `$CLAUDE_CONFIG_DIR`, mcp auth files — redacted).
+40
View File
@@ -0,0 +1,40 @@
# #8450 — SSH relay selects system Node without npm instead of NVM
**Status:** REPRODUCED (code-level; no live Ubuntu SSH host required)
**Issue:** https://github.com/stablyai/orca/issues/8450
**Malicious content:** none
## Summary
On Ubuntu remotes with system `/usr/bin/node` (no `/usr/bin/npm`) and a complete NVM toolchain, relay deploy chooses system Node, prepends `/usr/bin` to `PATH`, then runs bare `npm install` → exit 127 `npm: not found`.
## Root cause
`resolveRemoteNodePath` (`src/main/ssh/ssh-remote-node-resolution.ts`):
1. Path probe script emits `command -v node` **first**, then NVM/fnm/… globs.
2. First candidate that passes `node --version` major ≥ 18 wins.
3. **No companion `npm` check** on that candidate’s bin dir.
4. `commandWithNodePath` → `export PATH='<nodeBinDir>':$PATH && … npm install`.
So incomplete system Node shadows NVM.
## Automated proof
```bash
pnpm exec vitest run --config config/vitest.config.ts \
src/main/ssh/repro-8450-node-without-npm.test.ts
```
## Suggested fix
- Require `npm` beside each Node candidate (same dir or `command -v` after PATH prepend of that bin).
- Prefer version-manager candidates with a coherent pair over bare `command -v node`.
- Error should name selected Node path and missing npm.
## Evidence
- `src/main/ssh/repro-8450-node-without-npm.test.ts`
- `src/main/ssh/ssh-remote-node-resolution.ts`
- `src/main/ssh/ssh-remote-commands.ts` (`commandWithNodePath`)
- `src/main/ssh/ssh-relay-deploy.ts` (`installNativeDeps`)
+52
View File
@@ -0,0 +1,52 @@
# #8454 — Built-in browser localhost forced to `http://`
## Status
**REPRODUCED** (unit / code contract)
Bare localhost/loopback address-bar input is hard-coded to the `http://` scheme. HTTPS-only local dev servers are therefore unreachable when the user types `localhost:3000` without a scheme. The self-signed certificate half of the issue still needs interactive UI verification.
## Re-run
```bash
bash docs/bug-reproductions/scripts/repro-8454-localhost-http.sh
# equivalent:
npx vitest run src/shared/browser-url.test.ts -t "normalizes manual local-dev"
```
## Code
`src/shared/browser-url.ts`:
```166:171:src/shared/browser-url.ts
if (LOCAL_ADDRESS_PATTERN.test(trimmed)) {
try {
return new URL(`http://${trimmed}`).toString()
} catch {
return null
}
}
```
## Unit assertion (already in repo)
`src/shared/browser-url.test.ts`:
```ts
expect(normalizeBrowserNavigationUrl('localhost:3000')).toBe('http://localhost:3000/')
expect(normalizeBrowserNavigationUrl('127.0.0.1:5173')).toBe('http://127.0.0.1:5173/')
```
These tests **pass** and document the forced-http behavior as the current product contract.
## UI half (self-signed HTTPS)
Not automated here:
1. Serve `https://localhost:3000` with a self-signed cert (e.g. `next dev --experimental-https`).
2. In Orca browser, type `localhost:3000` → navigates to `http://…` → fails against HTTPS-only server.
3. Type full `https://localhost:3000` → scheme preserved, but cert rejection may still block without a proceed UI.
## Expected fix direction
Allow HTTPS for loopback (probe, remember last scheme, or prefer https when http fails) and/or offer insecure-cert proceed for local trusted contexts.
+51
View File
@@ -0,0 +1,51 @@
# #8457 — headless serve hijacks GUI relaunch; forced reopen interrupts/duplicates live agents
**Status:** REPRODUCED (code-level lifecycle ownership, conclusive)
**Issue:** https://github.com/stablyai/orca/issues/8457
**Related:** #8459 (orphan kill after messy relaunch)
**Malicious content:** none
**Safety:** Do **not** live-repro with valuable agent sessions (`open -n` is destructive).
## Summary
A packaged `orca serve` process can own the single-instance lock for the same userData profile after desktop Cmd+Q. Finder / Dock / `orca open` then interact with that headless owner. After serve settles with a **daemon** PTY provider, the desktop activation gate becomes `ready` and second-instance / activate calls `openMainWindow()` inside the serve process — hydrating a renderer that can cold-restore / resume agents and interrupt live ones.
## Root-cause chain (current tree)
| Step | Code | Behavior |
|------|------|----------|
| 1 | `shouldSkipSingleInstanceLock` | Serve never skips the lock → headless can own profile |
| 2 | `acquireSingleInstanceLock(app, requestDesktopActivation)` | Second instance → activation gate |
| 3 | Gate `initialState: isServeMode ? 'initializing' : 'ready'` | Serve starts gated |
| 4 | `settleServeDesktopActivation` | **LocalPtyProvider** → `markBlocked`; **daemon** → `markReady` |
| 5 | `activateWindow` → `focusExistingWindow` → `openMainWindow` | Window opened inside serve when ready |
| 6 | `RuntimeClient.openOrca` | Fails closed only on `blocked`; `openable` still `launchOrcaApp()` |
Partial mitigation exists (gate + `desktopWindowStatus` + block when persistent provider missing), but the **common production path** (daemon-backed PTYs) still promotes to desktop inside serve.
## Evidence (automated)
```bash
pnpm exec vitest run --config config/vitest.config.ts \
src/main/startup/repro-8457-serve-desktop-hijack.test.ts
```
Proves:
- Packaged/serve does not skip single-instance lock.
- After settle with daemon (non-LocalPty), pending activation opens the window; further requests keep opening.
- LocalPty settle blocks (fail-closed path only for missing persistent provider).
- `orca open` fail-closed only for `blocked`, not headless `openable`.
- Wiring still routes serve activation to `focusExistingWindow` / `openMainWindow`.
## Live repro (disposable agents only)
1. Desktop Orca with Persistent Terminal Sessions + disposable agents.
2. Packaged `orca serve` same profile; Cmd+Q desktop.
3. Confirm agents/daemon alive, no GUI.
4. `orca open` / Dock; forced `open -n -a Orca` may mount renderer in serve process.
5. Watch for interrupt / resume / duplicate agents.
## Expected fix direction
Serve must not silently own desktop activation, or handoff must be atomic. Prefer fail closed for headless→desktop unless a full desktop init contract is established; never treat bare runtime reachability as desktop capability.
+43
View File
@@ -0,0 +1,43 @@
# #8459 — Resource Manager misclassifies live daemon sessions as orphan and force-kills without confirmation
**Status:** REPRODUCED (code-level, conclusive)
**Issue:** https://github.com/stablyai/orca/issues/8459
**Related:** #8457 (lifecycle that can produce incomplete renderer index)
**Malicious content:** none
## Summary
Resource Manager treats every daemon session absent from the **current renderer binding map** as an “orphan.” Bulk **Kill N orphan terminals** and per-row kill on unbound rows skip confirmation and call `window.api.pty.kill` immediately. A recovering/incomplete renderer can therefore force-kill live agents.
## Root cause
### Classification (`resource-session-bindings.ts`)
```ts
boundPtyIds: inputs.workspaceSessionReady ? new Set(ptyIdToTabId.keys()) : new Set()
// orphan ⇔ !boundPtyIds.has(session.id)
```
Index sources: `ptyIdsByTabId`, `tab.ptyId`, layout `ptyIdsByLeafId`. No daemon ownership, pid/liveness, runtime generation, or headless/mobile owner check.
### Destructive path (`ResourceUsageStatusSegment.tsx`)
- Unbound row: skip confirm, kill immediately.
- Bulk orphans: `sessions.filter(s => !bound.has(s.id))` → `Promise.allSettled(...pty.kill)` with no confirm and no revalidation.
## Evidence (automated)
```bash
pnpm exec vitest run --config config/vitest.config.ts \
src/renderer/src/components/status-bar/repro-8459-orphan-live-daemon.test.ts
```
Proves:
1. Empty renderer map + `workspaceSessionReady: true` → all live daemon sessions count as unbound.
2. Partial map still labels unreferenced live agent PTYs as orphans (no process check).
3. Source still skips confirm for unbound and bulk-kills without revalidation; bindings module has no pid/owner/generation logic.
## Expected fix direction
Absence from the binding map is necessary but not sufficient for kill-safe orphan. Reconcile daemon inventory, fail closed while hydration is incomplete, require confirm for bulk kill, revalidate immediately before shutdown.
+25
View File
@@ -0,0 +1,25 @@
# #8478 — OpenCode logo not coming up well (Claude icon on OpenCode tabs)
**Status:** REPRODUCED
**Issue:** https://github.com/stablyai/orca/issues/8478
**Label:** `has_repro`
**Related:** #8940
**Fix PR (open):** #8590
**Malicious content:** none (screenshot only; not executed)
## Summary
OpenCode’s native OSC title format is `OC | <task>`. Title classifiers only treat OpenCode as identity when the token `opencode` appears. Native `OC | …` titles are not mapped to `opencode`, so `AgentIcon` does not render the OpenCode glyph (Claude or unknown instead). Matches the issue screenshot (Claude glyph on an OpenCode tab).
## Evidence (automated)
```bash
pnpm exec vitest run --config config/vitest.config.ts \
src/shared/repro-8478-opencode-native-title-icon.test.ts
```
Proves:
- No `OC |` native matcher in `terminal-title-agent-type.ts` / `agent-title-core.ts`.
- `getAgentLabel('OC | Understand about the plugin')` is not OpenCode.
- Braille/task frames without `opencode` still become Claude (#8940 family).
+33
View File
@@ -0,0 +1,33 @@
# #8482 — Window Blur causes sustained high GPU power/heat on macOS
**Status:** REPRODUCED (code-level config contract + reporter powermetrics; no live heat required)
**Issue:** https://github.com/stablyai/orca/issues/8482
**Related:** #8797 (blur/opacity no-op — same window path; different symptom)
**Malicious content:** none
## Summary
Enabling **Window Blur** on macOS maps to Electron `vibrancy: 'under-window'` with `transparent: true` at window construction. The main window also always calls `setBackgroundThrottling(false)` on darwin. Issue reporter measured continuous multi-watt GPU draw on a maximized visible window with blur on; hiding the window or disabling blur + restart cut GPU power ~85–89% with the same ~150 terminal sessions still live — isolating cost to **visible composition**, not PTYs alone.
## Root cause (proven in tree)
`src/main/window/createMainWindow.ts`:
| Contract | Behavior |
|----------|----------|
| `windowBackgroundBlur` | darwin → `{ vibrancy: 'under-window', transparent: true }` |
| Throttling | always `setBackgroundThrottling(false)` on macOS |
| Lifetime | blur only at window creation; no `setVibrancy` teardown path |
Reporter averages (issue body): blur visible **~5365 mW** → hidden **~589 mW** → blur off visible **~804 mW**.
## Automated proof
```bash
pnpm exec vitest run --config config/vitest.config.ts \
src/main/window/repro-8482-window-blur-gpu-cost.test.ts
```
## Workaround
Disable **Settings → Appearance → Window Blur** and restart Orca.
+105
View File
@@ -0,0 +1,105 @@
# #8532 — Cmd+Shift+O (Open markdown) only works in Floating Terminal
**Status:** REPRODUCED (code-level wiring gap, conclusive)
**Issue:** https://github.com/stablyai/orca/issues/8532
**Related open fix PR:** https://github.com/stablyai/orca/pull/8564
**Malicious content:** none
## Summary
`tab.openMarkdown` (default **Cmd+Shift+O** on macOS) is bound in Settings and works when the Floating Terminal panel owns focus. In the main window / normal worktree it does nothing. **Cmd+Shift+M** (`tab.newMarkdown`) works in the main window — inconsistent.
## Evidence
### 1. Floating panel allowlist + handler
`src/renderer/src/lib/floating-workspace-shortcut-policy.ts`:
```ts
const FLOATING_WORKSPACE_PANEL_SHORTCUT_ACTIONS = [
'tab.newTerminal',
'tab.newBrowser',
'tab.newMarkdown',
'tab.openMarkdown', // ← floating only
'tab.close'
]
```
`FloatingTerminalPanel.tsx` handles the chord:
```ts
if (matches('tab.openMarkdown')) {
consume()
openFloatingMarkdownTab() // window.api.app.pickFloatingMarkdownDocument()
return true
}
```
### 2. Main-window Terminal.tsx has newMarkdown only
```ts
// Terminal.tsx — present
if (!e.repeat && matchShortcut('tab.newMarkdown')) { ... }
// Terminal.tsx — ABSENT
// matchShortcut('tab.openMarkdown') ← not present
```
### 3. No main-process / IPC open path
| Surface | `tab.newMarkdown` | `tab.openMarkdown` |
|---------|-------------------|--------------------|
| `FLOATING_WORKSPACE_PANEL_SHORTCUT_ACTIONS` | yes | yes |
| FloatingTerminalPanel keydown | yes | yes |
| Terminal.tsx matchShortcut | yes | **no** |
| App.tsx keydown | no | **no** |
| `window-shortcut-policy` allowlist | neither (by design for terminal chords) | neither |
| preload `onNewMarkdownTab` / `ui:newMarkdownTab` | yes | **no `onOpenMarkdownTab`** |
| browser-guest-ui branch | yes → `ui:newMarkdownTab` | **no** |
| Tab bar menu action `open-markdown` | menu click only (`onOpenFileTab`) | not bound to global shortcut |
Grep snapshot (evidence file): only floating policy + FloatingTerminalPanel mention `tab.openMarkdown` in the main path.
### 4. Keybinding is registered globally
`src/shared/keybindings.ts` defines `tab.openMarkdown` → `Mod+Shift+O` (darwin). Settings shows the binding for all contexts, but only floating focus routes it.
### 5. Main before-input-event never dispatches openMarkdown
`resolveWindowShortcutAction` (`src/shared/window-shortcut-policy.ts`) is an explicit allowlist (settings, zoom, sidebars, workspace jump, …). It does **not** include `tab.newMarkdown` or `tab.openMarkdown`. New markdown works in main because **Terminal.tsx renderer keydown** handles it when the terminal owns focus — open markdown simply lacks that branch.
## Net effect
Main window: Cmd+Shift+O is never consumed → no picker, no tab.
Floating panel focused: local handler opens picker → works.
## Re-run steps
```bash
cd /Users/nwparker/orca/workspaces/orca/cooked-PRs
# Static wiring proof
rg -n "tab\.openMarkdown|onOpenMarkdownTab|ui:openMarkdown" \
src/renderer/src/lib/floating-workspace-shortcut-policy.ts \
src/renderer/src/components/floating-terminal/FloatingTerminalPanel.tsx \
src/renderer/src/components/Terminal.tsx \
src/shared/window-shortcut-policy.ts \
src/preload/index.ts \
src/main/window/createMainWindow.ts
# Expect: openMarkdown only in floating policy + FloatingTerminalPanel
# Expect: Terminal.tsx has tab.newMarkdown but not tab.openMarkdown
```
### Live UI (prod Orca)
Binary: `/Applications/Orca.app/Contents/Resources/bin/orca` (app UI)
1. Focus a normal worktree terminal → **Cmd+Shift+O** → nothing.
2. Open Floating Terminal (**Cmd+Alt+A**), focus it → **Cmd+Shift+O** → markdown picker.
3. Control: **Cmd+Shift+M** works in main window.
## Fix direction
PR #8564: main-window path mirroring `tab.newMarkdown` — worktree-rooted picker IPC, store open action, Terminal.tsx keydown branch. (Also ensure Settings shortcut is not floating-only if the action is general.)
+69
View File
@@ -0,0 +1,69 @@
# #8533 / #8584 — Default shortcut conflicts
## Status
**REPRODUCED** (defaults in `src/shared/keybindings.ts`)
## Collisions
### #8533 — `Cmd+Shift+E` on macOS
| Action | Platform defaults |
|--------|-------------------|
| `sidebar.explorer.toggle` | all: `Mod+Shift+E` |
| `tab.newSimulator` | darwin: `Mod+Shift+E`; linux/win32: unbound |
On darwin both claim `Mod+Shift+E`. Linux/Windows only bind Explorer, so they are unaffected.
### #8584 — `Cmd/Ctrl+0` on every platform
| Action | Platform defaults |
|--------|-------------------|
| `sidebar.focusWorktreeList` | all: `Mod+0` |
| `zoom.reset` | all: `Mod+0` |
## Source (definitions)
```360:366:src/shared/keybindings.ts
id: 'sidebar.explorer.toggle',
// ...
defaultBindings: platformBindings(['Mod+Shift+E'])
```
```565:574:src/shared/keybindings.ts
id: 'tab.newSimulator',
// ...
defaultBindings: {
darwin: ['Mod+Shift+E'],
linux: [],
win32: []
}
```
```424:429:src/shared/keybindings.ts
id: 'sidebar.focusWorktreeList',
// ...
defaultBindings: platformBindings(['Mod+0'])
```
```508:513:src/shared/keybindings.ts
id: 'zoom.reset',
// ...
defaultBindings: platformBindings(['Mod+0'])
```
## Why Settings conflict detector may stay silent
`findKeybindingConflicts()` only returns conflicts that involve **customized overrides** (`setIntersects(actionIds, customizedActions)`). Pure default-vs-default collisions return `[]`, so `expect(findKeybindingConflicts('darwin')).toEqual([])` can still pass in unit tests while runtime chords fight.
## Re-run
```bash
npx vitest run docs/bug-reproductions/scripts/repro-8533-8584-shortcut-conflicts.test.ts
```
Proof file: `docs/bug-reproductions/scripts/repro-8533-8584-shortcut-conflicts.test.ts`
## Runtime note
Main-process shortcut resolution order determines the winner (`zoom.reset` / simulator typically consume the chord first per issue reports), leaving the other advertised binding unreachable.
+44
View File
@@ -0,0 +1,44 @@
# #8533 — `Cmd+Shift+E` default collision on macOS
**Status:** REPRODUCED (defaults in source)
**Platform:** macOS only for the collision (Win/Linux have no default for `tab.newSimulator`)
## Summary
Both actions default to `Mod+Shift+E` on darwin:
| Action | Title | Default (darwin) |
|--------|-------|------------------|
| `tab.newSimulator` | New mobile emulator tab | `Mod+Shift+E` |
| `sidebar.explorer.toggle` | Show Explorer | `Mod+Shift+E` |
Main-process / terminal handlers consume the chord for the emulator first → Explorer toggle never fires.
## Evidence
From `src/shared/keybindings.ts` (current tree):
```ts
// sidebar.explorer.toggle
defaultBindings: platformBindings(['Mod+Shift+E'])
// tab.newSimulator
defaultBindings: {
darwin: ['Mod+Shift+E'],
linux: [],
win32: []
}
```
## Re-run
```bash
pnpm exec vitest run docs/bug-reproductions/scripts/repro-shortcut-conflicts.test.ts
```
## Manual UI
1. macOS, mobile emulator feature enabled (default).
2. Press `Cmd+Shift+E`.
3. Observe: new Mobile Emulator tab; Explorer does not toggle.
4. Settings still lists `Cmd+Shift+E` for Explorer.
+40
View File
@@ -0,0 +1,40 @@
# #8535 — pinned `orca serve --port` overridden by stale mobile-ws fallback
**Status:** REPRODUCED (code-level, conclusive)
**Issue:** https://github.com/stablyai/orca/issues/8535
**Platform:** cross-platform (reported on Linux headless; code is shared)
**Malicious content:** none
## Summary
When `mobile-ws-fallback-port.json` names a port other than the CLI `--port`, `WebSocketTransport.start` tries the **fallback first**. An explicit pin never binds if the stale fallback is free. Introduced for STA-1511 (paired devices keep old endpoints); side effect strands clients dialing the pinned port.
## Root cause
`src/main/runtime/rpc/ws-transport.ts`:
```ts
const candidatePorts =
persistedFallbackPort !== undefined ? [persistedFallbackPort, this.port] : [this.port]
// first successful tryListen wins → free stale fallback pre-empts pinned port
```
`runtime-rpc.ts` always passes `fallbackPort: readWsFallbackPort(...)` when `wsPort !== 0`, and writes the store whenever `resolvedPort !== this.wsPort`. Explicit `--port` is not distinguished from the default port after construction (`wsPort = DEFAULT_WS_PORT`).
Also covered by integration-style tests in `ws-transport.test.ts` (“binds the persisted fallback port even when the preferred port is free”).
## Evidence (automated)
```bash
pnpm exec vitest run --config config/vitest.config.ts \
src/main/runtime/rpc/repro-8535-ws-fallback-port-order.test.ts
```
## Manual
1. Write `{ "port": 49152 }` to `<userData>/mobile-ws-fallback-port.json`.
2. Ensure `:6768` is free; run `orca serve --port 6768`.
3. Observe bind on **49152** (pinned 6768 never tried first).
4. Remove the JSON file; serve binds **6768**.
**Workaround:** delete `mobile-ws-fallback-port.json` before pinned serve.
+35
View File
@@ -0,0 +1,35 @@
# #8539 — Renderer freezes ~87s on reconnect with many worktrees
**Status:** PARTIAL (call sites + O(repos) work proven; no live 37-worktree freeze)
**Issue:** https://github.com/stablyai/orca/issues/8539
**Malicious content:** none
## Summary
Reconnect with ~37 worktrees / 71 tabs: Chrome DevTools reported forced reflow ~87s with stack through `fetchAllWorktrees` in passive mount effects.
## What we proved in tree
- `App.tsx` still runs `fetchAllWorktrees` during startup (`fetch-worktrees`) and again after remote catalog refresh (`remote-worktree-refresh`), adjacent to `reconnectPersistedTerminals`.
- `fetchAllWorktrees` maps **every repo** via `listDetectedWorktreesForRepoCoalesced` + merge — no idle/chunk yield in the hot path.
Full 87s sync reflow needs a multi-host multi-worktree session (listed under perf/SSH deferred in index).
## Automated proof (call sites)
```bash
pnpm exec vitest run --config config/vitest.config.ts \
src/renderer/src/store/slices/repro-8539-fetchAllWorktrees-reconnect.test.ts
```
## Suggested fix direction
- Defer remote worktree refresh until after first paint / idle.
- Chunk repo listing; avoid mounting all tab trees before catalog settles.
- Profile merge/set paths for accidental sync layout thrash.
## Evidence
- `src/renderer/src/store/slices/repro-8539-fetchAllWorktrees-reconnect.test.ts`
- `src/renderer/src/App.tsx`
- `src/renderer/src/store/slices/worktrees.ts`
+30
View File
@@ -0,0 +1,30 @@
# #8541 — All remote sessions time out simultaneously on reconnect (multiplex cascade)
**Status:** REPRODUCED (code-level cascade; live multi-host optional)
**Issue:** https://github.com/stablyai/orca/issues/8541
**Malicious content:** none
## Summary
All sessions for a remote server share one shared-control WebSocket (`RemoteRuntimeSharedControlConnection`). When one RPC times out, the connection tears down and **rejects every pending request at once** — matching the DevTools storm (all errors within a few seconds).
## Root-cause chain
| Step | Code | Behavior |
|------|------|----------|
| 1 | One `pendingRequests` Map per connection | All session RPCs multiplexed |
| 2 | `requestSharedControl` timeout | Rejects that RPC with `Timed out waiting for the remote Orca runtime to respond.` |
| 3 | `onTimeout` → `handleSocketClosed` | Full socket close |
| 4 | `closeSharedControlSocketState` | `rejectAllSharedControlPendingRequests` — cascade |
| 5 | Timeout reset message | `Remote Orca runtime did not answer in time; resetting the control connection.` |
Issue’s two waves match: per-call timeout text then connection-reset text.
## Automated proof
```bash
pnpm exec vitest run --config config/vitest.config.ts \
src/shared/repro-8541-multiplex-timeout-cascade.test.ts
```
Proves multi-pending cascade reject + exact issue error strings + onTimeout wiring.
+88
View File
@@ -0,0 +1,88 @@
# #8577 — Settings search query leaks into Shortcuts pane row filter
**Status:** REPRODUCED (code-level, conclusive)
**Issue:** https://github.com/stablyai/orca/issues/8577
**Related open fix PR:** https://github.com/stablyai/orca/pull/8579
**Malicious content:** none (screenshots are UI captures only)
## Summary
Typing `shortcuts` (or a localized pane title) into Settings sidebar search auto-opens the Shortcuts pane with **0/N** rows, “No shortcuts match those filters.”, and dead local “Find shortcuts” search until the user clicks the sidebar item (which clears the query).
## Root cause (proven)
Sidebar ranking includes a **pane-title entry**; Shortcuts row filtering reuses the **same** `settingsSearchQuery` and ANDs it with the pane’s local query.
### 1. Documented hazard in settings-search
```125:133:src/renderer/src/components/settings/settings-search.ts
export function getSettingsSectionSearchEntries(section: {
title: string
description: string
searchEntries: readonly SettingsSearchEntry[]
}): SettingsSearchEntry[] {
// Why: sidebar ranking and active content filtering must receive the same
// pane-level entry, otherwise pane-title-only hits can rank but render blank.
return [{ title: section.title, description: section.description }, ...section.searchEntries]
}
```
Pane-title-only hits are **intended** for ranking/selection; the comment already flags blank content when the same query filters rows.
### 2. ShortcutsPane double-applies the global query
```58:145:src/renderer/src/components/settings/ShortcutsPane.tsx
const searchQuery = useAppStore((state) => state.settingsSearchQuery)
// ...
const matchesShortcutSearch = (row) =>
shortcutSearchQuery !== null &&
matchesSettingsSearch(searchQuery, getShortcutSearchEntry(row)) && // global sidebar query
matchesShortcutLocalSearch(row, shortcutSearchQuery, platform) // local "Find shortcuts"
const baseVisibleRows = shortcutRows.filter((row) => matchesShortcutSearch(row))
```
`getShortcutSearchEntry(row)` only has row title / group / keywords — **not** the pane title “Keyboard Shortcuts” / “Shortcuts”. Query `shortcuts` matches the pane entry for auto-activation but **zero** rows.
Local search then intersects an empty base list → typing `go to` still yields 0 results.
### 3. Recovery path
`Settings.tsx` `scrollToSection` clears `settingsSearchQuery` on explicit sidebar click — that is why clicking “Shortcuts” restores 112/112. Auto-activation from typing does not clear.
### 4. Model of the bug
```text
query "shortcuts" vs row titles ["Go to File", "New Terminal", ...]
→ matchesSettingsSearch false for all rows
→ baseVisibleRows = [] (0/112)
→ local "go to" still AND global "shortcuts" → still 0
```
### 5. settings-search unit tests confirm ranking, not the pane fix
`settings-search.test.ts` ranks Shortcuts above Task Sources for query `shortcuts` using pane-level entries — correct for sidebar. It does not assert Shortcuts row visibility under that query (bug remains).
## Re-run steps
```bash
cd /Users/nwparker/orca/workspaces/orca/cooked-PRs
# Read the leak
rg -n "matchesShortcutSearch|settingsSearchQuery" \
src/renderer/src/components/settings/ShortcutsPane.tsx
pnpm exec vitest run --config config/vitest.config.ts \
src/renderer/src/components/settings/settings-search.test.ts
```
### Live UI
1. Settings → type `shortcuts` in sidebar search.
2. Shortcuts pane opens with 0/N and empty filters.
3. Type `go to` in “Find shortcuts” → still 0.
4. Click “Shortcuts” in sidebar → list restored.
## Fix direction
PR #8579: if the global query matches **no** shortcut row, treat it as pane-level-only and keep all rows (local search works). Row-matching queries still narrow the list.
+37
View File
@@ -0,0 +1,37 @@
# #8584 — `Cmd/Ctrl+0` binding conflict (zoom.reset vs focus worktree list)
**Status:** REPRODUCED (defaults in source)
**Platform:** all (both use `Mod+0`)
## Summary
| Action | Title | Default |
|--------|-------|---------|
| `zoom.reset` | Reset Size | `Mod+0` |
| `sidebar.focusWorktreeList` | Focus worktree list | `Mod+0` |
Issue reports Focus worktree list is unreachable because zoom wins (or vice versa depending on handler order). Either way, defaults collide.
## Evidence
`src/shared/keybindings.ts`:
```ts
// sidebar.focusWorktreeList
defaultBindings: platformBindings(['Mod+0'])
// zoom.reset
defaultBindings: platformBindings(['Mod+0'])
```
## Re-run
```bash
pnpm exec vitest run docs/bug-reproductions/scripts/repro-shortcut-conflicts.test.ts
```
## Manual UI
1. Press `Cmd+0` (macOS) / `Ctrl+0` (Win/Linux).
2. Observe only one of: zoom reset vs worktree list focus.
3. Settings still lists both with the same chord.
+26
View File
@@ -0,0 +1,26 @@
# #8593 — Lingering idle subagent rows; click does nothing useful
**Status:** REPRODUCED
**Issue:** https://github.com/stablyai/orca/issues/8593
**Label:** `has_repro`
**Malicious content:** none (screenshot only; not executed)
## Summary
Long Claude sessions that spawn many Task/subagent children (e.g. Cypress runners) leave every finished child as an `idle` sidebar/dashboard row. Rows cannot be dismissed (`hideDismiss` for `rowSource === 'subagent'`). Activation only focuses the parent pane (`activationPaneKey`), which is a no-op when the parent is already focused.
## Root cause
| Layer | Behavior |
|-------|----------|
| `buildSubagentChildRows` | Maps **all** `subagents[]` including idle |
| Live hooks | Keep idle roster for the parent lifetime |
| `dropHydratedIdleClaudeSubagents` | Only on **hydrate** replay, not live updates |
| Dashboard | `hideDismiss={rowSource === 'subagent'}` |
## Evidence (automated)
```bash
pnpm exec vitest run --config config/vitest.config.ts \
src/renderer/src/components/sidebar/repro-8593-idle-subagent-rows.test.ts
```
+60
View File
@@ -0,0 +1,60 @@
# #8595 — “Bold Text” terminal color override is a no-op
**Status:** REPRODUCED (code-level + unit test)
**Platform:** all (renderer)
## Summary
Settings expose `terminalColorOverrides.bold` (and Ghostty import maps `bold-color`), but xterm.js `ITheme` has **no** `bold` key. Spreading the override into the theme object is a silent no-op for bold-default-fg cells.
## Evidence
`src/shared/types.ts` explicitly documents the gap:
```ts
// Why: xterm.js ITheme does not expose a `bold` key, but Ghostty users
// expect the setting to be preserved so a future renderer CSS override
// or xterm upgrade can honour it without a migration.
bold?: string
```
`src/renderer/src/components/terminal-pane/terminal-appearance.ts` spreads overrides into the theme:
```ts
if (settings.terminalColorOverrides) {
theme = { ...theme, ...settings.terminalColorOverrides }
}
```
xterm ignores unknown theme keys → bold never applied.
Ghostty import *does* map the setting (`src/main/ghostty/mapper.ts` `bold-color`), so the value can be stored and still do nothing in the renderer.
`drawBoldTextInBrightColors` only remaps ANSI 0–7 bold → bright; it does not recolor default-fg bold cells.
## Automated re-check
```bash
pnpm exec vitest run --config config/vitest.config.ts \
src/renderer/src/components/terminal-pane/repro-8595-bold-theme-noop.test.ts
```
Asserts: composed theme retains `.bold = #ffc600` while `ITheme` color slots have no `bold` key.
Also:
```bash
rg -n "bold\\?:" src/shared/types.ts src/renderer/src/components/terminal-pane/terminal-appearance.ts
rg -n "bold-color" src/main/ghostty/mapper.ts
```
## Manual repro
1. Settings → Appearance → Terminal → color overrides → Bold Text = `#ffc600`.
2. Open a **new** terminal.
3. `printf '\033[1mBOLD DEFAULT FG\033[0m\n'`
4. Expected (iTerm/Ghostty): gold bold. Actual: theme foreground.
## Fix direction
Honor `terminalColorOverrides.bold` for bold cells with default fg (custom renderer / CSS / xterm patch), **or** hide the setting and drop Ghostty `bold-color` mapping until supported.
+35
View File
@@ -0,0 +1,35 @@
# #8622 — SSH keyboard-interactive authentication (push-based MFA)
**Status:** REPRODUCED (capability gap — feature reported as bug)
**Issue:** https://github.com/stablyai/orca/issues/8622
**Malicious content:** none
## Summary
University HPC: password then keyboard-interactive push MFA works in OpenSSH / VS Code Remote SSH. In Orca, password prompt appears; after password → **“All configured authentication methods failed”**; MFA prompt never shown.
## Root cause (missing capability)
ssh2 path supports agent / privateKey / **password** only:
| Gap | Evidence |
|-----|----------|
| No `tryKeyboard` on connect config | `buildConnectConfig` never sets it; zero matches in ssh auth modules |
| No `keyboard-interactive` handler | `ssh-connection.ts` has no such event |
| Credential kinds | `SshCredentialKind = 'passphrase' \| 'password'` only |
| Failure string | `isAuthError` matches issue text |
Password method ≠ keyboard-interactive challenge-response. After password, second MFA challenge is never offered → auth fails with the exact user-facing error.
System-SSH fallback exists for GSSAPI/ProxyCommand cases but is not a general keyboard-interactive MFA path after interactive password.
## Automated proof
```bash
pnpm exec vitest run --config config/vitest.config.ts \
src/main/ssh/repro-8622-keyboard-interactive-missing.test.ts
```
## Classification
Not a regression of a previously implemented feature — **unsupported auth method**. Still actionable: implement `tryKeyboard` + UI prompts for interactive challenges (or route such hosts through system OpenSSH with a prompt-capable transport).
+91
View File
@@ -0,0 +1,91 @@
# #8670 — Deleting clean worktree with initialised submodule fails
## Status
**REPRODUCED** (Git layer + Orca force plumbing)
UI nuance: confirm-dialog deletes currently pass `force: true`; skip-confirm deletes pass `force: false` and do not classify the submodule error as force-recoverable.
## Summary
Git refuses `git worktree remove <path>` when the linked worktree has an initialised submodule, even if parent and submodule are completely clean:
```text
fatal: working trees containing submodules cannot be moved or removed
```
`git worktree remove --force <path>` succeeds and only removes that worktree.
## Re-run
```bash
bash docs/bug-reproductions/scripts/repro-8670-submodule-worktree-remove.sh
```
Evidence: `docs/bug-reproductions/evidence/8670-git-submodule-remove.txt`
## Git evidence (this machine)
| Step | Result |
|------|--------|
| Clean worktree + `submodule update --init` | clean statuses |
| `git worktree remove <wt>` | exit **128**, fatal submodule message |
| `git worktree remove --force <wt>` | exit **0**, path removed |
Fixture uses `git -c protocol.file.allow=always` for local `file://` submodules.
## Orca `removeWorktree` force plumbing
`src/main/git/worktree.ts` — `--force` only when `force === true`:
```1219:1224:src/main/git/worktree.ts
const args = ['worktree', 'remove']
if (force) {
args.push('--force')
}
args.push(worktreePath)
await gitExecFileAsync(args, gitExecOptions(repoPath, options))
```
Default: `force = false`.
## UI / call paths
| Path | Force? | Notes |
|------|--------|-------|
| `DeleteWorktreeDialog` primary confirm | **`force: true`** | `runWorktreeDeletesInParallel(..., { force: true })` — should pass `--force` |
| Skip-confirm (`skipDeleteWorktreeConfirm`) | **`force: false`** | `runWorktreeDeleteWithToast(id, name)` with no force → `options.force === true` is false |
| Batch skip-confirm | **`force: false`** | same |
| Store `removeWorktree(id)` default | **`force` undefined/false** | IPC/runtime forward as non-force |
Skip-confirm site:
```300:303:src/renderer/src/components/sidebar/delete-worktree-flow.ts
const skipConfirm = state.settings?.skipDeleteWorktreeConfirm ?? false
if (skipConfirm && !hasLineageChildren) {
void runWorktreeDeleteWithToast(worktreeId, target.displayName)
return
```
Toast helper:
```139:139:src/renderer/src/components/sidebar/delete-worktree-flow.ts
return removeWorktree(worktreeId, options.force === true)
```
## Force-delete recovery gap
`classifyWorktreeForceDeleteReason` (`src/shared/worktree-removal.ts`) only maps dirty / orphan / missing-registration. It does **not** recognize:
```text
working trees containing submodules cannot be moved or removed
```
So a non-force failure from skip-confirm surfaces a raw error **without** a Force Delete recovery toast (`canForceDelete: false`).
## Expected fix direction
Either:
1. Treat initialised-submodule worktrees as requiring `--force` for clean deletes (auto or preflight), and/or
2. Classify the Git submodule message as a force-delete reason so the toast recovery path works when the first attempt uses `force: false`.
+76
View File
@@ -0,0 +1,76 @@
# #8695 — Bundled undici 7.28.0 asserts on paused parser when socket closes
**Status:** REPRODUCED
**Issue:** https://github.com/stablyai/orca/issues/8695
**Malicious content:** none
## Summary
When a response body is not consumed and the peer closes the socket, undici’s
HTTP/1 parser hits `assert(!this.paused)` in `Parser.finish`, crashing the
Electron main process (uncaught AssertionError).
## Environment (this host)
| Component | Version |
|-----------|---------|
| Production Orca Electron | Node `24.18.0` (from `process.versions`) |
| Bundled undici (Orca Electron) | `7.28.0` |
| Worktree `node_modules/undici` | `7.28.0` |
| package.json | `electron ^43.1.0`, engines `node: '24'` |
Upstream: [nodejs/undici#5360](https://github.com/nodejs/undici/issues/5360), fix [nodejs/undici#5474](https://github.com/nodejs/undici/pull/5474).
## Root cause (proven)
1. Response body applies backpressure → parser `paused`.
2. Peer FIN → `onHttpSocketEnd` → `parser.finish()`.
3. `finish` asserts `!this.paused` and throws (uncaught → main process death).
Code still present in worktree:
`node_modules/undici/lib/dispatcher/client-h1.js` — multiple `assert(!this.paused)`.
App-level try/catch around `fetch` cannot prevent the crash.
## Live crash repro
```bash
bash docs/bug-reproductions/scripts/repro-8695-undici-paused-parser.sh
```
Or manually:
```bash
ELECTRON_RUN_AS_NODE=1 /Applications/Orca.app/Contents/MacOS/Orca /path/to/repro.js
```
Observed stack (system node + Orca Electron, 2026-07-15):
```text
AssertionError [ERR_ASSERTION]: The expression evaluated to a falsy value:
assert(!this.paused)
at Parser.finish (node:internal/deps/undici/undici:7380:9)
at Socket.onHttpSocketEnd (node:internal/deps/undici/undici:7819:34)
```
Log: `docs/bug-reproductions/evidence/8695-undici-repro.log`
## Unit proof (version pin)
```bash
pnpm exec vitest run --config config/vitest.config.ts \
src/main/repro-8695-undici-paused-parser.test.ts
```
## Suggested product fix
Ship an Electron/Node runtime with undici including #5474 (or equivalent backport).
Consuming every response body reduces likelihood but is not sufficient.
## Evidence paths
- `docs/bug-reproductions/scripts/repro-8695-undici-paused-parser.sh`
- `docs/bug-reproductions/evidence/8695-undici-repro.log`
- `src/main/repro-8695-undici-paused-parser.test.ts`
- `node_modules/undici/package.json` / `lib/dispatcher/client-h1.js`
+15
View File
@@ -0,0 +1,15 @@
# #8715 — OMP scroll resets when switching tabs
**Status:** REPRODUCED
**Label:** `has_repro`
## Summary
OMP (and other full-screen TUIs) use xterm’s alternate buffer. `scheduleSplitScrollRestore` **skips** restore when `bufferType === 'alternate'` (restore-during-draw knocks the TUI cursor — #1298). Tab switch → no scroll re-pin → viewport appears at top.
## Re-run
```bash
pnpm exec vitest run --config config/vitest.config.ts \
src/renderer/src/lib/pane-manager/repro-8715-omp-alt-screen-scroll.test.ts
```
+68
View File
@@ -0,0 +1,68 @@
# #8720 — SSH relay npm 12 silently skips node-pty (allowScripts)
**Status:** REPRODUCED (code-level; no live Linux npm 12 remote required)
**Issue:** https://github.com/stablyai/orca/issues/8720
**Malicious content:** none
## Summary
Relay deploy runs `npm install` for `node-pty@1.1.0` + `@parcel/watcher@2.5.6` on the
remote. Under **npm 12**, lifecycle scripts not covered by `allowScripts` are blocked
(warn + exit 0 when `strict-allow-scripts` is false). Linux has no node-pty prebuild, so
`node-gyp rebuild` never runs → `require('node-pty')` fails at PTY spawn with
“node-pty is not available on this remote host”.
## Root cause (proven)
`installNativeDeps` in `src/main/ssh/ssh-relay-deploy.ts`:
1. Writes hardcoded package.json:
```ts
const RELAY_NATIVE_DEPS = {
'node-pty': '1.1.0',
'@parcel/watcher': '2.5.6'
}
// package.json: { name, version, private, type: 'commonjs', dependencies }
// NO allowScripts
```
2. Runs:
```text
npm install --omit=dev --no-audit --no-fund node-pty@1.1.0 @parcel/watcher@2.5.6
```
No `allowScripts`, no `npm install-scripts approve`, no force-enable lifecycle scripts.
3. On npm 12 Linux: scripts blocked → no build → install “succeeds” →
`.install-complete` written (hard fail only if npm itself fails). Probe may warn
MISSING, but the issue reports deploy looking successful while runtime PTY fails.
ESM/loader cache of failed import can also stick until relay restart.
## Automated proof
```bash
pnpm exec vitest run --config config/vitest.config.ts \
src/main/ssh/repro-8720-npm12-allow-scripts.test.ts
```
Assertions:
- Deploy source pins deps and never mentions `allowScripts` / `allow-scripts`
- Install command has no script-approve flags
- Synthetic package.json equals deploy shape and lacks `allowScripts`
- Existing `ssh-relay-native-deps-install.test.ts` pins deps only (no allowScripts)
## Suggested fix (from issue)
- Emit `allowScripts` for `node-pty` and `@parcel/watcher` in generated package.json
- Fail deploy unless `require('node-pty')` succeeds (surface npm warning)
- Longer term: pre-bundled per-platform relay (#1693)
## Evidence paths
- `src/main/ssh/repro-8720-npm12-allow-scripts.test.ts`
- `src/main/ssh/ssh-relay-deploy.ts` (`RELAY_NATIVE_DEPS`, `installNativeDeps`)
- `src/main/ssh/ssh-relay-native-deps-install.test.ts` (install contract)
- `src/main/ssh/ssh-relay-build-toolchain.ts` (Linux no-prebuild note)
+33
View File
@@ -0,0 +1,33 @@
# #8726 — Tasks upstream/origin problems on forks (auto PR routing + count cache)
**Status:** REPRODUCED (code-level for A + count-cache half of B/G; umbrella issue)
**Issue:** https://github.com/stablyai/orca/issues/8726
**Related:** #8727 (A fix), #8738 (B), #7331/#7513 (C)
**Malicious content:** none
## Summary (cluster)
Umbrella for Tasks source bugs most visible on forks (`origin` = fork, `upstream` = canonical).
### A — REPRODUCED: `auto` routes PRs to origin
- Issues `auto` → `getIssueOwnerRepo` = **upstream-first**.
- PRs → `resolvePrWorkItemSource`: only `preference === 'upstream'` leaves origin; **auto/undefined/origin all use origin**.
- Result: Upstream pill (issue heuristic) vs empty PRs list (`--repo <fork>`).
Existing tests already document this split (`uses upstream for issues and origin for PRs in mixed recent results` in `client-issue-source.test.ts`).
### B/G — REPRODUCED (cache half): count always `--cache 120s`
`countWorkItemsForQuery` hardcodes `gh api --cache 120s` and public `countWorkItems` has **no `noCache`** — source flips can serve stale totals up to 2 minutes (matches issue). Full stale-list toggle race needs renderer path (partial of B).
### C–F
Tracked elsewhere / product direction; not unit-proven in this pass.
## Automated proof
```bash
pnpm exec vitest run --config config/vitest.config.ts \
src/main/github/repro-8726-fork-pr-auto-origin.test.ts
```
+51
View File
@@ -0,0 +1,51 @@
# #8729 — Imported .codex-pet ignores per-frame durations (uniform 8 fps)
**Status:** REPRODUCED
**Issue:** https://github.com/stablyai/orca/issues/8729
**Malicious content:** do **not** run issue attachments; tests use synthetic data only
## Summary
Imported Codex pet bundles play every animation at sheet-wide **8 fps**. The idle
loop that should take ~6.6 s in Codex cycles in **0.75 s** (~9× too fast).
## Root cause (proven)
1. `applyCodexPetDefaults` (`src/main/ipc/pet-bundle.ts`) fills Codex layouts with:
```ts
export const CODEX_PET_DEFAULT_FPS = 8
// animations: { idle: { row: 0, frames: 6 }, ... } // no per-frame ms
fps: manifest.fps ?? CODEX_PET_DEFAULT_FPS
```
2. `SpriteAnimation` type only has `{ row, frames }` — no duration array.
3. `SpriteFrame` in `PetOverlay.tsx` plays:
```ts
const duration = Math.max(0.1, frames / Math.max(0.1, sprite.fps))
// animation: `… ${duration}s steps(${frames}) infinite`
```
Uniform fps → equal step time; Codex idle holds are uneven
`[1680, 660, 660, 840, 840, 1920]` ms.
## Automated proof (synthetic fixture only)
```bash
pnpm exec vitest run --config config/vitest.config.ts \
src/main/ipc/repro-8729-codex-pet-fps.test.ts
```
Assertions:
- Default manifest gets `fps === 8` and idle `{ row: 0, frames: 6 }` without duration fields
- Cycle math: Orca `6/8 * 1000 = 750` ms vs Codex sum `6600` ms ≈ **8.8×** faster
## Evidence paths
- `src/main/ipc/repro-8729-codex-pet-fps.test.ts`
- `src/main/ipc/pet-bundle.ts`
- `src/renderer/src/components/pet/PetOverlay.tsx` (`SpriteFrame`)
- `src/shared/types.ts` (`SpriteAnimation`)
+44
View File
@@ -0,0 +1,44 @@
# #8733 — `terminalMacOptionAsAlt` effectively resets in vim (compose layouts)
**Status:** REPRODUCED (code-level)
**Issue:** https://github.com/stablyai/orca/issues/8733
**Malicious content:** none
## Summary
French-PC layout, Option as Alt = **Off**. Shell composes correctly (e.g. AltGr+`'` → `{`). Opening **vim/neovim** makes Option send Meta sequences (`<M-'>`). Reload Orca without restarting vim “fixes” it until vim restarts.
## Root cause
When the pane’s app enables **kitty keyboard protocol**, `resolveTerminalShortcutAction` encodes **every** Option chord as CSI-u with the physical base key, for **any** `macOptionAsAlt` mode except `'true'`:
```ts
if (isMac && … && event.altKey && macOptionAsAlt !== 'true') {
if (event.key !== 'Dead' && isKittyKeyboardActivePane?.()) {
return { type: 'sendInput', data: `\x1b[${base};${mods}u` }
}
}
```
So user preference “Option as Alt = Off” is overridden for composition as soon as kitty is active (vim).
Reload “fix”: `TerminalKittyKeyboardModeTracker` notes SerializeAddon **does not serialize kitty flags**. Tracker restarts at 0; composition works until vim re-pushes CSI `> u`.
## Automated proof
```bash
pnpm exec vitest run --config config/vitest.config.ts \
src/renderer/src/components/terminal-pane/repro-8733-8399-kitty-option-compose.test.ts
```
## Suggested fix
- When `macOptionAsAlt === 'false'`, prefer composition (leave Option to Chromium) even if kitty is active, **or**
- Only kitty-encode Option for keys that are true TUI hotkeys when the layout is US-safe
- Optionally re-scan live PTY for kitty push after reload (separate correctness)
## Evidence
- `src/renderer/src/components/terminal-pane/repro-8733-8399-kitty-option-compose.test.ts`
- `src/renderer/src/components/terminal-pane/terminal-shortcut-policy.ts`
- `src/shared/terminal-kitty-keyboard-mode-tracker.ts`
+38
View File
@@ -0,0 +1,38 @@
# #8739 — Linear filters only show options from the first selected team
**Status:** REPRODUCED (code-level, conclusive)
**Issue:** https://github.com/stablyai/orca/issues/8739
**Malicious content:** none
## Summary
With multiple Linear teams selected (or All teams), Filters → status / assignee / label only list metadata from a single **primary** team. Other selected teams' states, labels, and members never appear.
## Root cause
1. `resolveLinearIssueAttributeFilterPrimaryTeam` returns **one** team: first selected by stable name/id sort (`linear-issue-attribute-filter-primary-team.ts`).
2. `LinearIssueAttributeFilterDropdowns` sets
`activeTeamId = primaryTeam?.id` only, then loads:
- `useTeamStates(activeTeamId, …)`
- `useTeamLabels(activeTeamId, …)`
- `useTeamMembers(activeTeamId, …)`
3. When `selectedTeamCount > 1`, UI even banners “Options from {{team}}” acknowledging the collapse — but does not union options.
There is no loop over `selectedTeamIds` for metadata.
## Evidence (automated)
```bash
pnpm exec vitest run --config config/vitest.config.ts \
src/renderer/src/components/repro-8739-linear-filter-first-team.test.ts
```
Proves:
- Two selected teams → only Backend (name-first) option ids returned; Frontend ids absent.
- All teams selected → still only primary team's options.
- Source contract: single `activeTeamId` from `primaryTeam`, three hooks, multi-team banner, no selection map.
## Expected fix direction
Union status/label/member options across every selected team (dedupe by id), or disable multi-team attribute filters with a clear empty state until one team is selected. Prune logic must not drop valid multi-team selections.
+54
View File
@@ -0,0 +1,54 @@
# #8742 — Antigravity CLI sessions not in AI Vault history
**Status:** REPRODUCED (code-level + schema parse)
**Issue:** https://github.com/stablyai/orca/issues/8742
**Malicious content:** none
## Summary
Antigravity is a supported CLI agent, but `agy` conversations never appear in Agent Session History (AI Vault).
## Root causes (both required)
### 1. Discovery path
`session-scanner-source-discovery.ts` only scans Gemini CLI:
```ts
const GEMINI_SESSIONS_DIR = join(homedir(), '.gemini', 'tmp')
// sessionRootDirs(…, ['.gemini', 'tmp'])
```
Real Antigravity layout (observed on host):
```text
~/.gemini/antigravity-cli/brain/<conversation-id>/.system_generated/logs/transcript.jsonl
```
No `antigravity-cli` / `agent: 'antigravity'` discovery root.
### 2. JSONL schema
`consumeGeminiMessage` only increments for `type === 'user' | 'gemini'`.
Antigravity records use e.g. `USER_INPUT`, `PLANNER_RESPONSE`, `GENERIC` with `created_at` (not Gemini’s `timestamp` / `user`/`gemini`).
Parser returns null or 0-message sessions for real transcripts.
## Automated proof
```bash
pnpm exec vitest run --config config/vitest.config.ts \
src/main/ai-vault/repro-8742-antigravity-not-scanned.test.ts
```
## Suggested fix
- Add discovery under `~/.gemini/antigravity-cli/brain/**/.system_generated/logs/transcript.jsonl` (agent `antigravity` or dedicated parse path).
- Parse Antigravity step types into user/assistant turns using `created_at` + `content`.
## Evidence
- `src/main/ai-vault/repro-8742-antigravity-not-scanned.test.ts`
- `src/main/ai-vault/session-scanner-source-discovery.ts`
- `src/main/ai-vault/session-scanner-gemini-parsers.ts`
+41
View File
@@ -0,0 +1,41 @@
# #8749 — macOS agent-session sandbox blocks claude-hook.sh (EPERM)
**Status:** NOT REPRODUCED
**Issue:** https://github.com/stablyai/orca/issues/8749
**Label:** `cannot_repro`
**Malicious content:** none
## Summary of claim
Reporter: Orca-hosted Claude Code sessions run under a seatbelt profile that denys `~/.orca` (and more), so Orca’s managed `~/.orca/agent-hooks/claude-hook.sh` fails with EPERM on every tool call. `sandbox_check` returned 1 inside their session.
## What we verified (production Orca + this tree)
1. **No seatbelt on daemon PTY path in code** — scan of `src/main/daemon/*.ts` finds no `sandbox-exec` / seatbelt / app-sandbox wiring for agent shells.
2. **Hooks do install under `~/.orca/agent-hooks/`** — `getSharedManagedScriptPath('claude-hook.sh')` (correct install location).
3. **Live probe** (packaged Orca, daemon PTY via `orca terminal create --command …` on cooked-PRs worktree):
- `sandbox_check 0`
- `ls ~/.orca` → `orca_dir_ok`
- `claude-hook.sh` executable; empty-stdin exit 0
4. Production daemon children (`login` + shell-ready bash) all `sandbox_check=0`. Only Chromium Helper processes use `--seatbelt-client`.
5. Entitlements on `/Applications/Orca.app` do **not** include `com.apple.security.app-sandbox`.
## Evidence (automated)
```bash
pnpm exec vitest run --config config/vitest.config.ts \
src/main/agent-hooks/repro-8749-hook-path-no-seatbelt.test.ts
```
## Live re-probe (non-destructive)
```bash
PROD="/Applications/Orca.app/Contents/Resources/bin/orca"
"$PROD" terminal create --worktree path:"$PWD" --title "repro-8749" \
--command 'python3 -c "import ctypes,os; lib=ctypes.CDLL(\"/usr/lib/libSystem.B.dylib\"); lib.sandbox_check.argtypes=[ctypes.c_int,ctypes.c_void_p,ctypes.c_uint32]; lib.sandbox_check.restype=ctypes.c_int; print(\"sandbox_check\", lib.sandbox_check(os.getpid(), None, 0))"; ls "$HOME/.orca" >/dev/null && echo orca_dir_ok; echo DONE' --json
# then: orca terminal read --terminal <handle> --limit 30 --json
```
## Notes
Reporter’s multi-machine seatbelt fingerprints may be Claude Code’s own tool sandbox, another host policy, or a different Orca build. This host/version cannot reproduce Orca-applied seatbelt on agent sessions. If reopened with a PTY pid + `sandbox_check` from an Orca daemon shell, re-investigate.
+29
View File
@@ -0,0 +1,29 @@
# #8752 — Setup-script prompt remains visible when orca.yaml setup hook is running
**Status:** REPRODUCED (code-level; shared renderer — not Linux-only)
**Issue:** https://github.com/stablyai/orca/issues/8752
**Label note:** issue tagged `os:linux` but root cause is cross-platform UI invalidation
**Malicious content:** none
## Summary
After `orca.yaml` gains a valid `scripts.setup` (and Orca successfully runs the setup hook), the sidebar can still show **“Add a setup script”**. Full remount / sidebar close-reopen clears it.
## Root cause
1. **Detection is correct** — `hasEffectiveSetupCommand` accepts shared `hooks.scripts.setup`.
2. **Inspection deps are incomplete** — `SetupScriptPromptCard` only re-runs on
`[activeRepo, inspectionRetryKey, isDismissed, settings, sidebarOpen]`.
No invalidation on `orca.yaml` changes, runtime reconnect/generation, or same-repo worktree activation.
3. **Stale retention** — `getRenderedSetupScriptPromptState` keeps `lastVisiblePrompt` for the same project while `promptState` is null (pending), so a negative result stays painted across same-project switches.
## Automated proof
```bash
pnpm exec vitest run --config config/vitest.config.ts \
src/renderer/src/components/sidebar/repro-8752-setup-script-prompt-stale.test.ts
```
## Suggested fix (from issue)
Invalidate inspection on orca.yaml / runtime reconnect / same-repo worktree activation; do not retain a known-stale negative across those boundaries.
+35
View File
@@ -0,0 +1,35 @@
# #8758 — node-pty is not available on this remote host (Linux SSH)
**Status:** REPRODUCED (code-level; same root as #8720)
**Issue:** https://github.com/stablyai/orca/issues/8758
**Related:** #8720
**Malicious content:** none
## Summary
SSH to Debian 13 succeeds; opening an integrated terminal fails with:
```text
node-pty is not available on this remote host
```
## Root cause
Identical deploy contract as **#8720**:
1. `installNativeDeps` writes package.json with `node-pty` + `@parcel/watcher` **without** `allowScripts`.
2. `npm install` has no script-approve flags.
3. On npm 12 Linux (no node-pty prebuild), lifecycle scripts are blocked but exit 0 → install “succeeds” → runtime `require('node-pty')` fails.
4. User-visible throw: `src/relay/pty-handler.ts` — `node-pty is not available on this remote host`.
## Automated proof
```bash
pnpm exec vitest run --config config/vitest.config.ts \
src/main/ssh/repro-8758-node-pty-remote.test.ts \
src/main/ssh/repro-8720-npm12-allow-scripts.test.ts
```
## Suggested fix
Same as #8720: emit `allowScripts` for native deps; fail deploy unless `require('node-pty')` succeeds.
+60
View File
@@ -0,0 +1,60 @@
# #8784 — GHE PR author avatars fail (login.png instead of avatar_url)
**Status:** REPRODUCED
**Issue:** https://github.com/stablyai/orca/issues/8784
**Related open fix PR:** https://github.com/stablyai/orca/pull/8831
**Malicious content:** none (screenshots only)
## Summary
On GitHub Enterprise, PR author/reviewer avatars 404 because Orca builds
`https://github.com/{login}.png` instead of using the API `avatar_url`.
## Root cause (proven)
1. `githubAvatarUrl(login)` in `src/renderer/src/components/github/github-issue-comment-helpers.ts` hardcodes:
```ts
return `https://github.com/${encodeURIComponent(login)}.png?size=64`
```
2. `PullRequestPage` author chip uses **only** the login:
```ts
src={githubAvatarUrl(workItem.author)}
```
3. `ReviewerAvatar` / `TaskPage` `ReviewChipAvatar` fall back to the same pattern when `avatarUrl` is empty:
```ts
avatarUrl || githubAvatarUrl(login)
// or: reviewer.avatarUrl || `https://github.com/${reviewer.login}.png?size=40`
```
Enterprise-only logins do not exist on github.com → 404. Successful avatars are those that already carry a full API `avatar_url`.
## Automated proof
```bash
pnpm exec vitest run --config config/vitest.config.ts \
src/renderer/src/components/github/repro-8784-ghe-avatar-fallback.test.ts
```
Assertions:
- Empty `avatarUrl` → `https://github.com/{login}.png`
- API `avatar_url` preferred when present
- Production `githubAvatarUrl` hardcodes github.com
- Source still contains login.png fallbacks and author path ignores avatar_url
## Fix note (#8831)
PR #8831 (open) carries `avatar_url` through work-item mappers and adds a shared
`GitHubUserAvatar` that prefers API URLs, with a failed-src retry so late enrichment recovers.
## Evidence paths
- `src/renderer/src/components/github/repro-8784-ghe-avatar-fallback.test.ts`
- `src/renderer/src/components/github/github-issue-comment-helpers.ts`
- `src/renderer/src/components/PullRequestPage.tsx` (author + ReviewerAvatar)
- `src/renderer/src/components/TaskPage.tsx` (ReviewChipAvatar)
+20
View File
@@ -0,0 +1,20 @@
# #8797 — Background Opacity / Window Blur no-op on macOS
**Status:** REPRODUCED
**Label:** `has_repro`
## Summary
On macOS, Window Blur and Background Opacity appear to do nothing. The window is created with `vibrancy` + `transparent: true` but also an always-opaque `backgroundColor` (`#0a0a0a` / `#ffffff`) that covers the blur layer. Terminal opacity only changes xterm’s rgba background, so it reveals that solid fill—not the desktop.
## Re-run
```bash
pnpm exec vitest run --config config/vitest.config.ts \
src/main/window/repro-8797-blur-opacity-noop.test.ts
```
## Manual
1. Settings → Terminal → Window → enable Blur, lower Background Opacity.
2. Observe: still fully opaque (and blur needs restart to even re-read the setting).
+101
View File
@@ -0,0 +1,101 @@
# #8808 — Agent recognition never recovers after runtime restart
**Status:** REPRODUCED (code-level, conclusive for env-prefixed relaunch)
**Issue:** https://github.com/stablyai/orca/issues/8808
**Related open fix PR:** https://github.com/stablyai/orca/pull/8942
**Malicious content:** none
## Summary
After an Orca runtime restart, worker PTYs survive as bare shells. Relaunching Claude inside the same terminal via `orca terminal send` (with env prefixes such as `CLAUDE_CONFIG_DIR=… claude …`) makes `tui-idle` succeed, but `orchestration dispatch --inject` still fails with “no recognized agent detected”. Creating a brand-new terminal with the agent as `--command` works immediately.
## Root cause (proven)
`isTerminalRunningAgent` (used by inject dispatch) falls through titles → ready prompt → stale `lastAgentStatus` → **foreground process recognition** via `recognizeAgentProcess` / command-line recognition.
When the agent was originally launched as the terminal command, launch metadata / OSC titles seed recognition. After runtime restart + in-shell relaunch:
1. Launch agent metadata may be gone or still reflect a shell.
2. Recognition often depends on the live foreground command line.
3. **Leading shell env assignments are not stripped**, so the first token is `CLAUDE_CONFIG_DIR=…` (or similar), not `claude`.
### Automated proof (this worktree)
```text
plain: { agent: 'claude', processName: 'claude' }
env-prefixed process: null
env-prefixed cmdline: null
plain cmdline: { agent: 'claude', processName: 'claude' }
```
Command:
```bash
pnpm exec tsx -e "
import {
recognizeAgentProcess,
recognizeAgentProcessFromCommandLine
} from './src/shared/agent-process-recognition.ts'
console.log('plain:', recognizeAgentProcess('claude'))
console.log('env-prefixed process:',
recognizeAgentProcess('CLAUDE_CONFIG_DIR=~/.claude_sub claude --model sonnet'))
console.log('env-prefixed cmdline:',
recognizeAgentProcessFromCommandLine(
'CLAUDE_CONFIG_DIR=~/.claude_sub claude --model sonnet'))
console.log('plain cmdline:',
recognizeAgentProcessFromCommandLine('claude --model sonnet'))
"
```
Saved: `docs/bug-reproductions/evidence/batch-repros-2026-07-15.txt`
## Code path
1. Inject gate — `src/main/runtime/rpc/methods/orchestration.ts`:
```ts
const hasAgent = await runtime.isTerminalRunningAgent(to)
// throws: "no recognized agent detected..."
```
2. Detection — `src/main/runtime/orca-runtime.ts` `isTerminalRunningAgent` → `isRecognizedForegroundAgentProcess` → `recognizeAgentProcess`.
3. Shared recognizer — `src/shared/agent-process-recognition.ts` maps process/command tokens to agents; **no leading `KEY=value` strip** on current `main` (PR #8942 adds that).
4. Related gap — #7797 (agents started inside existing shells / tmux). This issue’s env-prefix failure is a narrower, proven subset; #8942 intentionally does not claim full #7797 coverage.
## Why “create with --command” works but “send” does not
- `orca terminal create --command 'CLAUDE_CONFIG_DIR=… claude …'` sets launch agent / PTY command ownership so recognition does not depend solely on flattening the live cmdline.
- `orca terminal send --text 'CLAUDE_CONFIG_DIR=… claude …' --enter` only types into a surviving shell; inject then relies on title/foreground recognition. Env-prefixed cmdline recognition returns **null** → permanent “no recognized agent” until a new terminal is created.
## Unit tests already show shell + foreground *can* work
`src/main/runtime/orca-runtime.test.ts` cases such as “lets adopted neutral pane titles use non-shell foreground fallback” prove bash-launched PTYs are recognized when `getForegroundProcess` returns a bare agent name (`codex`, `claude`). The bug is specifically **unrecognized env-prefixed command lines**, matching the reporter’s relaunch string.
## Re-run steps
```bash
cd /Users/nwparker/orca/workspaces/orca/cooked-PRs
# Conclusive recognition proof (above tsx snippet)
pnpm exec vitest run --config config/vitest.config.ts \
src/shared/agent-process-recognition.test.ts \
src/main/providers/agent-foreground-process.test.ts
```
### Live (prod Orca) — optional full scenario
Binary: `/Applications/Orca.app/Contents/Resources/bin/orca`
1. Create worker with agent as command; dispatch inject successfully.
2. Cause runtime restart (CLI `_meta.runtimeId` rotates; agent dies, PTY shell remains).
3. `orca terminal send --terminal <h> --text 'CLAUDE_CONFIG_DIR=~/.claude_sub claude --model sonnet' --enter`
4. `orca terminal wait --terminal <h> --for tui-idle` → satisfied.
5. `orca orchestration dispatch … --inject` → still “no recognized agent detected”.
## Fix direction
PR #8942: strip contiguous leading `^[A-Za-z_][A-Za-z0-9_]*=` tokens before recognition (shared recognizer + provider tests). Not landed on this worktree’s `main` yet.
+25
View File
@@ -0,0 +1,25 @@
# #8813 — Visual status indicators within the project don’t appear
**Status:** DEFERRED (Windows-only live report)
**Issue:** https://github.com/stablyai/orca/issues/8813
**Malicious content:** none (Discord screenshot attachment not executed)
## Summary
Reporter (Windows 11, 1.4.142-rc.1): project/worktree cards show a yellow checkmark while agent still working; other status colors missing; CPU/memory inaccurate.
## Why deferred
- Environment is **Windows 11**; this worktree pass is macOS-only for live UI.
- Status colors use the same `StatusIndicator` / `use-worktree-activity-status` paths that unit tests cover for working/permission/done on the shared tree — no Windows-specific regression isolated without a host.
- CPU/memory on worktree cards depends on daemon resource snapshots / process attribution on Windows (foreground process scan).
## Re-run when Windows host available
1. Run agent to working state; confirm card should be yellow spinner (`working`), not done checkmark.
2. Compare hook status vs title-derived status on Windows.
3. Validate Resource Manager / per-worktree CPU samples against Task Manager.
## Label
Not applying `has_repro` / `cannot_repro` — needs Windows host (track in WINDOWS-ONLY).
+54
View File
@@ -0,0 +1,54 @@
# #8832 — Cmd-click URL opens with next line's text glued on
**Status:** REPRODUCED (unit test + code path)
**Platform:** all (renderer terminal links)
## Summary
Cmd-clicking a URL at the end of a line opens the browser with the first word of the **next** logical line appended (e.g. `…/orca/Description`).
## Root cause
`findHttpLinkAtBufferPosition` (`terminal-url-link-hit-testing.ts`) builds logical lines from:
1. Soft-wrapped rows (`isWrapped`)
2. Framed hard-wrapped HTTP candidates
3. **`buildCandidateLogicalLinesForBufferPosition`** — which includes **path** hard-wrap reconstruction from #8339
Path hard-wrap (`buildHardWrappedPathLogicalLineCandidates`) treats a URL suffix ending in `/` as a path start and joins the next row's path-like prefix (`Description:` from `Description: 123`). HTTP extraction then runs on that glued string and trims trailing `:` → `https://github.com/stablyai/orca/Description`.
Hard-wrap HTTP (`hard-wrapped-terminal-http-links.ts`) correctly requires a vertical frame and ≥3 rows for short URLs ending in `/`, so the plain two-line case is **not** from that path — it is path reconstruction reused for HTTP.
## Automated evidence
```bash
pnpm exec vitest run --config config/vitest.config.ts \
src/renderer/src/components/terminal-pane/repro-8832-url-next-line.test.ts
```
Expect 3 passing tests; the third asserts `openHttpLinkAtBufferPosition` opens
`https://github.com/stablyai/orca/Description` (not `…/orca/`).
## Manual repro (app)
1. Open a terminal in Orca.
2. Run:
```bash
printf 'Repo: https://github.com/stablyai/orca/\nDescription: 123\n'
```
3. Cmd-click (macOS) / Ctrl-click (Linux/Windows) the URL.
4. **Actual:** browser opens `https://github.com/stablyai/orca/Description`
**Expected:** `https://github.com/stablyai/orca/`
## Fix direction
Do not feed path hard-wrap logical lines into HTTP hit-testing without proving the continuation is still URL body (or stop path hard-wrap from starting on `https://…` schemes). Prefer only soft-wrap + framed HTTP hard-wrap for URLs.
## Key files
- `/Users/nwparker/orca/workspaces/orca/cooked-PRs/src/renderer/src/components/terminal-pane/terminal-url-link-hit-testing.ts` (`findHttpLinkAtBufferPosition`)
- `/Users/nwparker/orca/workspaces/orca/cooked-PRs/src/renderer/src/components/terminal-pane/wrapped-terminal-link-ranges.ts` (`buildHardWrappedPathLogicalLineCandidates`)
- `/Users/nwparker/orca/workspaces/orca/cooked-PRs/src/renderer/src/components/terminal-pane/hard-wrapped-terminal-path-fragments.ts`
- Repro test: `src/renderer/src/components/terminal-pane/repro-8832-url-next-line.test.ts`
+51
View File
@@ -0,0 +1,51 @@
# #8838 — `<br>` tags in markdown table cells not rendered
## Status
**NOT REPRODUCED** against current `MarkdownPreview` pipeline · labeled **`cannot_repro`** (worktree + production app.asar both use `rehypeRaw` + `rehypeSanitize`).
Pipeline proof shows the opposite of the issue report for raw HTML `<br>` / `<br/>`:
- With `rehypeRaw` (as in `MarkdownPreview.tsx`): real `<br>` elements are emitted in table cells.
- Without `rehypeRaw`: tags are **dropped**, not shown as literal `<br>` text.
- Entity-escaped source (`&lt;br/&gt;`) remains literal text (expected).
If users still see literal `<br>`, likely causes are escaped entities in the file, or a non-preview surface—not the current sanitize schema rejecting `br` (`defaultSchema` already allows `br`).
## Re-run
```bash
npx vitest run docs/bug-reproductions/scripts/repro-8838-br-in-table.test.ts
```
Regression / proof file:
- `docs/bug-reproductions/scripts/repro-8838-br-in-table.test.ts`
## Pipeline under test
Matches `src/renderer/src/components/editor/MarkdownPreview.tsx`:
```ts
remarkPlugins: [remarkGfm]
rehypePlugins: [rehypeRaw, [rehypeSanitize, markdownPreviewSanitizeSchema]]
```
(`markdownPreviewSanitizeSchema` extends `defaultSchema.tagNames` with details/summary/kbd/sub/sup/ins; `br` is already in `defaultSchema`.)
## Evidence snapshot
Rendered description cell (with raw):
```html
<td>Inspection verdict. Possible values:<br/><br/><code>normal</code> …</td>
```
- `has real <br>`: **true**
- `has &lt;br`: **false**
Remark AST for the sample includes `html: "<br/>"` nodes inside the table cell (GFM does parse the tags).
## Conclusion
Current code path does not reproduce “literal `<br>` text” for unescaped tags. Leave the vitest as a regression guard so a future pipeline regression fails CI-style when re-run manually.
+101
View File
@@ -0,0 +1,101 @@
# #8844 — `orca file open` reports ok:true for non-existent file
## Status
**REPRODUCED**
## Summary
`orca file open <missing-path> --json` returns `ok: true`, `opened: true`, exit `0` when the resolved worktree-relative path does not exist. A ghost editor tab is still created; content load later fails with `fs:readFile` / `ENOENT`.
## Re-run
**Default is non-destructive** (does not open editor tabs — avoids ENOENT ghost-tab spam):
```bash
bash docs/bug-reproductions/scripts/repro-8844-file-open-missing.sh
```
Live CLI open of a missing path (creates a ghost tab — **opt-in only**):
```bash
ORCA_REPRO_8844_LIVE=1 ORCA_BIN=/Applications/Orca.app/Contents/Resources/bin/orca \
bash docs/bug-reproductions/scripts/repro-8844-file-open-missing.sh
# Then Cmd+W the ghost tab, or write a placeholder file at the path it printed.
```
Evidence already captured:
- `docs/bug-reproductions/evidence/8844-file-open-missing.json.txt`
- `docs/bug-reproductions/evidence/8844-file-open-missing.raw.json`
## CLI evidence (production Orca)
```json
{
"ok": true,
"result": {
"worktree": "…::/Users/nwparker/orca/workspaces/orca/cooked-PRs",
"relativePath": "spec/does-not-exist-repro-8844.md",
"kind": "markdown",
"opened": true
}
}
```
- Exit code: `0`
- File exists before/after: **no**
## Ghost tab behavior
Observable only in a running Orca UI: the CLI RPC `files.open` always calls `host.openFile(...)`, which creates/activates an editor tab. Opening the tab surfaces:
> Unable to load file … `fs:readFile` … `ENOENT`
Agents that trust `ok`/`opened`/exit code are misled.
## Root cause (no existence check)
### CLI
`src/cli/handlers/file.ts` — `'file open'` calls `files.open` and prints success from the RPC payload (no local `stat`):
```139:146:src/cli/handlers/file.ts
'file open': async (ctx) => {
const relativePath = getRequiredStringFlag(ctx.flags, 'path')
const worktree = await getFileWorktreeSelector(ctx)
const result = await ctx.client.call<RuntimeFileOpenResult>('files.open', {
worktree,
relativePath
})
printResult(result, ctx.json, formatFileOpen)
},
```
### Main / runtime (authoritative)
`RuntimeFileCommands.openMobileFile` in `src/main/runtime/orca-runtime-files.ts`:
1. Resolves worktree
2. Classifies kind by **extension only** (markdown/text/image/binary)
3. For non-binary: `host.openFile(...)` then **`return { …, opened: true }`**
4. **Never** `stat`/`access`/`exists` on the absolute path
```440:469:src/main/runtime/orca-runtime-files.ts
async openMobileFile(
worktreeSelector: string,
relativePath: string
): Promise<RuntimeFileOpenResult> {
const { worktree } = await this.host.resolveRuntimeFileTarget(worktreeSelector)
// ...
const filePath = joinWorktreeRelativePath(worktree.path, relativePath)
this.host.openFile(worktree.id, filePath, relativePath, undefined)
return { worktree: worktree.id, relativePath, kind, opened: true }
}
```
RPC wiring: `src/main/runtime/rpc/methods/files.ts` (`files.open` → `openMobileFile`).
## Expected fix direction
Stat the resolved absolute path (local or remote) before reporting success; on missing file return `ok: false` with nonzero CLI exit and do not open a tab.
+67
View File
@@ -0,0 +1,67 @@
# #8864 — Agent history lookup hangs forever
**Status:** REPRODUCED (code-level hang conditions, conclusive)
**Issue:** https://github.com/stablyai/orca/issues/8864
**Reporter context:** OpenCode user; main process stuck on huge `opencode.db` (~29 GB) while Agent History open; UI spinner never clears. Reporter later noted OpenCode missing DB index; hang still shows Orca has no timeout/escape hatch.
**Malicious content:** none (Discord attachment is a screenshot only — not executed)
## Summary
Opening **Agent History** calls `window.api.aiVault.listSessions` → local scan → OpenCode SQLite discovery/parse. There is **no wall-clock timeout** on:
1. Opening `opencode.db` (`SyncDatabase` supports `timeout`, OpenCode paths omit it).
2. The session list SQL (correlated `COUNT(*)` + `json_extract` per session).
3. Local `listAiVaultSessions` / `cached-session-list` (no `Promise.race`).
4. Renderer refresh (awaits IPC; `loading` true until settle).
A slow or locked pathological DB keeps the main process busy and the panel spinner forever.
## Root cause
### OpenCode SQLite open (no timeout)
```ts
// session-scanner-opencode-sqlite-discovery.ts + session-scanner-opencode-sqlite.ts
new SyncDatabase(dbPath, { readonly: true, fileMustExist: true })
// SyncDatabase accepts timeout?: number → passed to DatabaseSync — unused here
```
### Expensive list query
Per-session correlated subquery:
```sql
(SELECT COUNT(*) FROM message m
WHERE m.session_id = s.id
AND json_extract(m.data, '$.role') IN ('user','assistant'))
FROM session s ORDER BY s.time_updated DESC LIMIT ?
```
Without indexes (or with multi-GB tables / WAL contention), this can run for minutes on the main thread path.
### No client/server deadline
- Local scope in `ipc/ai-vault.ts` returns `scanLocalAiVaultSessions` immediately (timeout only for multi-host runtime scans).
- `useAiVaultSessionRefresh` `await window.api.aiVault.listSessions(...)` with no `Promise.race`.
## Evidence (automated)
```bash
pnpm exec vitest run --config config/vitest.config.ts \
src/main/ai-vault/repro-8864-history-lookup-no-timeout.test.ts
```
Proves missing timeout on OpenCode open, expensive SQL shape, no local list deadline, no renderer timeout.
## User-reported live conditions (issue comments)
- `lsof` showed Orca holding multi-GB `opencode.db` read.
- Main ~20% CPU, trace frozen, UI unresponsive during history open.
- OpenCode event table bloat + missing index made queries pathological.
## Expected fix direction
- Pass SQLite busy timeout; consider size/mtime gate before scanning huge DBs.
- Avoid correlated `json_extract` COUNT on full message table (or use indexed columns / precomputed counts).
- Wrap listSessions in a wall-clock timeout → surface scan issue + stop spinner.
- Isolate heavy SQLite work off the critical main-thread path where possible.
+112
View File
@@ -0,0 +1,112 @@
# #8865 — Project Group hides member projects when Hide sleeping filters workspaces
**Status:** REPRODUCED (code-level + existing unit expectation of wrong behavior)
**Issue:** https://github.com/stablyai/orca/issues/8865
**Related open fix PR:** https://github.com/stablyai/orca/pull/8866
**Malicious content:** none
## Summary
With Project Groups enabled, enabling **Hide sleeping** removes every sleeping workspace card. If a grouped project’s *all* workspaces are sleeping (or otherwise filtered), the project header vanishes from its Project Group as well. Truly empty projects (no worktrees in the store) still get placeholders; filter-hidden non-empty projects do not.
## Root cause (proven)
Two layers disagree:
| Layer | Input | Behavior |
|-------|--------|----------|
| Row building | `visibleWorktrees` (post-filter) | Only groups repos that still have visible worktrees **or** appear in `placeholderRepoIds` |
| Placeholder selection | **raw** `worktreesByRepo` | Placeholder only if `worktreesByRepo[repo.id]?.length === 0` |
So a repo that still has sleeping worktrees in the store gets **no** placeholder, and after Hide sleeping removes all cards it also has **no** visible worktrees → disappears from the Project Group.
### Placeholder implementation
```4:24:src/renderer/src/components/sidebar/empty-project-placeholder-repos.ts
export function getEmptyProjectPlaceholderRepoIds(args: {
groupBy: WorktreeGroupBy
repos: readonly Repo[]
worktreesByRepo: Readonly<Record<string, readonly Worktree[] | undefined>>
filterRepoIds: readonly string[]
}): Set<string> {
// ...
if ((args.worktreesByRepo[repo.id]?.length ?? 0) === 0) {
placeholderRepoIds.add(repo.id)
}
}
```
### WorktreeList wires raw store map (not post-filter visible set)
```5708:5715:src/renderer/src/components/sidebar/WorktreeList.tsx
const placeholderRepoIds = useMemo(() => {
return getEmptyProjectPlaceholderRepoIds({
groupBy,
repos: visibleReposForRows,
worktreesByRepo, // full store, pre hide-sleeping
filterRepoIds
})
}, [filterRepoIds, groupBy, visibleReposForRows, worktreesByRepo])
```
While `visibleWorktrees` uses `computeVisibleWorktreeIds(..., { showSleepingWorkspaces, ... })` and is what `buildRows` consumes for actual cards.
### Existing unit test codifies the conflicting behavior
`src/renderer/src/components/sidebar/empty-project-placeholder-repos.test.ts`:
```ts
it('does not treat non-empty repos as empty when workspace filters hide their rows', () => {
expect(
getEmptyProjectPlaceholderRepoIds({
groupBy: 'repo',
repos: [repo],
worktreesByRepo: { [repo.id]: [worktree] }, // still "non-empty" in store
filterRepoIds: []
}).size
).toBe(0) // ← no placeholder even if filters hide the only worktree
})
```
Issue comment (bbingz) generalizes: any filter that hides all of a repo’s worktrees (Hide sleeping, hide-default-branch, hide-automation, host-scope) produces the same vanish; sleep is the common trigger.
### Inline model of the bug
```text
placeholder when all worktrees filtered but store non-empty: 0
→ no placeholder, project vanishes from group
truly empty repo gets placeholder: 1
```
## buildRows still supports empty grouped headers *when* placeholders exist
`worktree-list-groups.test.ts` includes cases:
- “keeps empty project groups visible in project grouping mode”
- “renders grouped repos before their visible worktrees are loaded” (via `placeholderRepoIds`)
- “does not resurrect filtered repos as empty Project Group headers” (filterRepoIds path)
The gap is specifically **workspace filters** (not explicit project filters) vs placeholders keyed on raw rows.
## Re-run steps
```bash
cd /Users/nwparker/orca/workspaces/orca/cooked-PRs
pnpm exec vitest run --config config/vitest.config.ts \
src/renderer/src/components/sidebar/empty-project-placeholder-repos.test.ts \
src/renderer/src/components/sidebar/worktree-list-groups.test.ts \
src/renderer/src/components/sidebar/visible-worktrees.test.ts
```
### Live UI
1. Create Project Group with ≥1 member project that has only sleeping workspaces.
2. Enable **Hide sleeping**.
3. Expected: project header remains under the group (even with no cards).
4. Actual: member project disappears entirely from the group.
## Fix direction
PR #8866: preserve Project Group members under workspace-level filters (placeholders / visible-layer fix). Treat filter-hidden non-empty repos like empty ones for **grouped** headers only; keep existing hide behavior for ungrouped projects.
+31
View File
@@ -0,0 +1,31 @@
# #8878 — Remote client resumes live host agent sessions (duplicate TUIs)
**Status:** REPRODUCED (code-level ownership gate gap, conclusive)
**Issue:** https://github.com/stablyai/orca/issues/8878
**Label:** `has_repro`
**Related fix PR (open):** #8887
**Malicious content:** none
## Summary
A paired remote client can run `resumeSleepingAgentSessionsForWorktree` for sessions that are still daemon-backed on the host. Resume guards only read client-local state; host session-tabs arrive asynchronously. Result: second TUI attached to the same provider session.
## Root-cause chain
| Step | Code | Behavior |
|------|------|----------|
| 1 | `worktree-activation.ts` / `Terminal.tsx` | Calls `resumeSleepingAgentSessionsForWorktree` unconditionally |
| 2 | `resume-sleeping-agent-session.ts` | No `isWebRuntimeSessionActive` short-circuit |
| 3 | Ownership guards | Client-local `tabsByWorktree` / claim maps only |
| 4 | `Terminal.tsx` auto-create | **Does** gate on `isWebRuntimeSessionActive` — resume path misses the same gate |
## Evidence (automated)
```bash
pnpm exec vitest run --config config/vitest.config.ts \
src/renderer/src/lib/repro-8878-remote-resume-no-runtime-gate.test.ts
```
## Expected fix direction
Skip resume for runtime-owned worktrees at the choke point (same predicate as terminal auto-create). Host keeps wake authority; local/SSH/WSL unaffected. See PR #8887.
+15
View File
@@ -0,0 +1,15 @@
# #8881 — Sidebar duplicates worktrees after re-pair
**Status:** REPRODUCED
**Label:** `has_repro`
## Summary
Re-pairing a remote host mints a new `runtime:<env>` host id. Worktree rows are keyed `${repoId}::${path}` and `mergeWorktreesForHost` only replaces the *current* host’s rows — superseded runtime host rows remain. Same physical paths appear 2–3×. SSH has readoption reconcile; runtime re-pair does not.
## Re-run
```bash
pnpm exec vitest run --config config/vitest.config.ts \
src/renderer/src/store/slices/repro-8881-runtime-repair-duplicates.test.ts
```
+78
View File
@@ -0,0 +1,78 @@
# #8903 — Cmd-J jump palette leaves keyboard focus on the wrong surface
**Status:** REPRODUCED (code-level unit + source path; full UI animation race not automated)
**Platform:** all (DOM focus; macOS/Linux/Windows)
## Summary
After Cmd-J workspace jump (Enter) or cancel (Esc), keyboard focus often lands on a **hidden** workspace’s terminal or nowhere useful — hollow cursor / no typing until click.
## Root cause (code path)
### Destination jump (Enter)
`WorktreeJumpPalette.handleSelectWorktree`:
1. `activateAndRevealWorktree(worktreeId)`
2. `skipRestoreFocusRef.current = true` (skips Esc restore path)
3. `closeModal()`
4. **`focusFallbackSurface()` with no preferred element**
`focusFallbackSurface` double-rAF then:
```ts
resolvePaletteFocusRestoreTarget(preferredTarget ?? null)?.focus({ preventScroll: true })
```
With `preferredTarget == null`:
```ts
// palette-focus-restore-target.ts
const xterm = doc.querySelector('.xterm-helper-textarea') // FIRST in document order
```
Inactive workspaces stay mounted (`display:none`), so the first `.xterm-helper-textarea` is often a **background** worktree’s terminal — focus lands there while the destination looks active.
### Esc cancel
`handleOpenChange(false)` calls `focusFallbackSurface(previousFocusElementRef.current)`. If that element is still connected, focus is correct; if not, same first-DOM fallback.
### Related fallback
`focusTerminalTabSurface` also ends with unscoped `document.querySelector('.xterm-helper-textarea')` when tab-scoped helpers are missing (`focus-terminal-tab-surface.ts`).
Issue author’s diagnosis matches this path (generic first-xterm fallback + mounted hidden workspaces + rAF race with modal teardown).
## Automated evidence
```bash
pnpm exec vitest run --config config/vitest.config.ts \
src/renderer/src/components/cmd-j/repro-8903-cmdj-focus-fallback.test.ts
```
- Two xterms (hidden first, visible second) → `resolvePaletteFocusRestoreTarget(null)` returns **hidden**
- Source asserts: `skipRestoreFocusRef` + bare `focusFallbackSurface()` + unscoped `querySelector`
Existing: `src/renderer/src/components/cmd-j/palette-focus-restore-target.test.ts`
## Manual UI steps remaining
1. Open ≥2 workspaces, each with a terminal (or editor).
2. Focus a terminal in workspace A; type to confirm solid cursor.
3. **Cmd-J** → select workspace B → **Enter**.
4. Without clicking, type.
5. **Actual:** hollow cursor / keys go nowhere (or wrong pane). **Expected:** focus on B’s active surface.
6. Also: Cmd-J → **Esc** and confirm focus returns to the surface you left (flaky if preferred element disconnected).
E2e: no dedicated Cmd-J focus e2e found; many e2e suites only mark `cmd-j-palette` tip seen. Closest utilities: `focus-terminal-tab-surface.test.ts`, e2e helpers querying `.xterm-helper-textarea`.
## Fix direction (per issue)
Route focus from **store state** (destination worktree + active tab + active leaf) via scoped selectors (`[data-terminal-tab-id=…] [data-leaf-id=…] .xterm-helper-textarea` / editor / browser focus events), not document-order `querySelector`. Prefer `focusTerminalTabSurface(tabId, leafId)` after jump instead of bare `focusFallbackSurface()`.
## Key files
- `src/renderer/src/components/WorktreeJumpPalette.tsx` — `handleSelectWorktree`, `focusFallbackSurface`
- `src/renderer/src/components/cmd-j/palette-focus-restore-target.ts`
- `src/renderer/src/lib/focus-terminal-tab-surface.ts`
- Repro test: `src/renderer/src/components/cmd-j/repro-8903-cmdj-focus-fallback.test.ts`
+26
View File
@@ -0,0 +1,26 @@
# #8934 — Jira issue images do not render in Tasks drawer
**Status:** REPRODUCED (code-level ADF conversion)
**Issue:** https://github.com/stablyai/orca/issues/8934
**Malicious content:** none
## Summary
Jira stores pasted screenshots as ADF `media` / `mediaSingle` / `mediaInline` nodes (Media Service IDs, not public URLs). Orca converts ADF → Markdown via `adfToMarkdownText` then renders with `CommentMarkdown`. Media nodes are unhandled → **silently dropped**. `ISSUE_FIELDS` also omits `attachment`, so there is no authenticated attachment download on `getIssue`.
## Automated proof
```bash
pnpm exec vitest run --config config/vitest.config.ts \
src/main/jira/repro-8934-adf-media-dropped.test.ts
```
Proves:
- ADF doc with mediaSingle + mediaInline keeps surrounding text only
- No `![...]`, media UUID, or data-URL image in output
- Source has no media handlers; ISSUE_FIELDS has no `attachment`
## Suggested fix (from issue)
ADF media → markdown images; Basic-auth download via `/rest/api/3/attachment/content/{id}`; map media UUIDs via attachment order/filename; size limits; lightbox Esc must not dismiss drawer.
+73
View File
@@ -0,0 +1,73 @@
# #8935 — GHE PR work item diffs do not load
**Status:** REPRODUCED
**Issue:** https://github.com/stablyai/orca/issues/8935
**Related open fix PR:** https://github.com/stablyai/orca/pull/8932
**Malicious content:** none
## Summary
For GitHub Enterprise Server remotes, PR work items open with metadata but the
file diff / contents view stays empty because API calls never target the Enterprise host.
## Root cause (proven)
1. **`parseGitHubOwnerRepo` is github.com-only**
(`src/main/github/github-remote-identity-parsing.ts`):
```ts
if (!identity || identity.host.toLowerCase() !== 'github.com') {
return null
}
```
Enterprise remotes parse as full identities via `parseGitHubRemoteIdentity`, but
`getOwnerRepo` only uses `parseGitHubOwnerRepo` → **null**.
2. **`getPRFiles` / `getPRFileContents` require ownerRepo**
(`src/main/github/work-item-details.ts`):
```ts
if (!ownerRepo) {
return null // getPRFiles
}
// getPRFileContents → empty original/modified strings
```
3. **No Enterprise host routing on this path**
Source does not call `getEnterpriseGitHubRepoSlug` and never passes
`gh --hostname <enterprise-host>`. Even when `pr view` fallbacks run without
owner/repo, file listing still needs the API path with a host.
4. Downstream: `getWorkItemDetails` sets `filesUnavailable: files === null` so the
Files tab shows unavailable/empty instead of real diffs.
## Automated proof
```bash
pnpm exec vitest run --config config/vitest.config.ts \
src/main/github/repro-8935-ghe-pr-diff-host.test.ts
```
Assertions:
- `parseGitHubOwnerRepo` null for GHE URLs; identity still has host/owner/repo
- With `getOwnerRepo → null`, `getWorkItemDetails` → `filesUnavailable: true`
- `getPRFileContents` returns empty without calling `gh`
- Source lacks `getEnterpriseGitHubRepoSlug` and `--hostname`
Related baseline: `src/main/github/gh-utils.test.ts` already expects
`parseGitHubOwnerRepo('https://ghe.acme.internal/acme/orca.git')` → null.
## Fix note (#8932)
PR #8932 (open) routes PR detail `gh api` calls through `getEnterpriseGitHubRepoSlug`
when github.com owner/repo is unavailable, always adding `--hostname`, and encodes
content paths by segment.
## Evidence paths
- `src/main/github/repro-8935-ghe-pr-diff-host.test.ts`
- `src/main/github/github-remote-identity-parsing.ts`
- `src/main/github/work-item-details.ts` (`getPRFiles`, `getPRFileContents`, `getWorkItemDetails`)
- `src/main/github/github-enterprise-repository.ts` (exists but unused by work-item-details today)
+56
View File
@@ -0,0 +1,56 @@
# #8940 — opencode displayed as Claude Code
**Status:** REPRODUCED (unit test + labeling heuristics)
**Platform:** all (title / sidebar identity)
## Summary
OpenCode sessions are sometimes shown as **Claude Code** in the sidebar and flaky on tabs (OpenCode ↔ Claude Code). Discord report: most OpenCode tabs mislabeled; only the real Claude tab is correct.
## Root cause
Identity comes primarily from **terminal titles** via `getAgentLabel` / `isClaudeAgent`:
| Title shape | Result |
|-------------|--------|
| `. …` / `* …` / `✳ …` prefixes | Always **Claude Code** (even if task text says “opencode”) |
| Braille spinner + task, no agent token | **Claude Code** (`isClaudeAgent` excludes Cursor/OpenClaude only, **not** OpenCode) |
| Token `opencode` / synthetic `OpenCode` / `⠋ OpenCode` | Correct **OpenCode** |
Hooks correctly set `agentType: 'opencode'` on `/hook/opencode`, and Orca injects synthetic titles (`OpenCode` / `OpenCode ready` / spinner + `OpenCode`) when hook state drives them. When native OSC titles overwrite those frames without an `opencode` token, the **activity** facet (`getAgentLabel`) flips to Claude Code — matching tab flakiness.
Sidebar title-derived rows partially mitigate Claude false positives (`resolveTitleDerivedAgentType` requires a `claude` token), but tab titles and any consumer of `getAgentLabel` still show “Claude Code”.
## Automated evidence
```bash
pnpm exec vitest run --config config/vitest.config.ts \
src/shared/repro-8940-opencode-as-claude.test.ts
```
Pins:
- `. Compare Opencode Vs Orca` → Claude Code
- `⠋ implementing the feature` → Claude Code / `isClaudeAgent === true`
- `⠋ OpenClaude` excluded; bare OpenCode-like braille tasks are not
- Synthetic `⠋ OpenCode` vs native `⠋ fix the auth bug` flip
## Manual repro (app)
1. Launch several **OpenCode** agent tabs in Orca (with hooks working).
2. Watch tab titles while OpenCode is working vs idle.
3. When OpenCode’s own OSC title is a braille + task string without the word `opencode`, Orca labels it Claude Code.
4. When synthetic idle/working frames re-apply, the label becomes OpenCode again.
## Fix direction
- Treat process/`launchAgent`/hook `agentType: opencode` as identity authority over generic Claude prefixes.
- In `isClaudeAgent` braille branch, exclude titles already owned by other agents (or require stronger Claude signals).
- Prefer `resolveExplicitTerminalTitleAgentType` (already rejects bare Claude prefixes) for display identity, not only for some sidebar paths.
## Key files
- `src/shared/agent-title-identity.ts` / `src/shared/terminal-title-agent-type.ts` — `getAgentLabel`, `isClaudeAgent`
- `src/shared/synthetic-agent-title.ts` — OpenCode synthetic profiles
- `src/renderer/src/components/sidebar/worktree-title-derived-agent-rows.ts`
- Repro test: `src/shared/repro-8940-opencode-as-claude.test.ts`
+24
View File
@@ -0,0 +1,24 @@
# #8943 — Browser shows insecure
**Status:** NOT REPRODUCED (screenshot-only; no false-positive path found)
**Issue:** https://github.com/stablyai/orca/issues/8943
**Label:** `cannot_repro`
**Malicious content:** issue body is only an image attachment — **not downloaded/opened** per security policy.
## What we know
- No repro steps, URL, or description beyond “When I try to open the browser, getting this issue” + screenshot.
- Main `BrowserAddressBar` has **no** secure/insecure lock chrome (Globe/Search only).
- Popup origin bar intentionally shows **Not secure** for non-loopback `http://` (`describePopupOrigin`).
- Guest webPreferences: `allowRunningInsecureContent: false` (mixed-content block is intentional).
## Evidence (automated)
```bash
pnpm exec vitest run --config config/vitest.config.ts \
src/main/browser/repro-8943-browser-insecure-indicator.test.ts
```
## Conclusion
Cannot map the screenshot to a product defect without analyzing the attachment or a clearer repro (URL, whether main browser vs popup, cert error vs “Not secure” badge). If the user opened plain `http://` remote content in a popup, “Not secure” is expected UX.
+128
View File
@@ -0,0 +1,128 @@
# #8962 — OMP terminals excluded from cold session restoration
**Status:** REPRODUCED (code-level / unit-level, conclusive)
**Issue:** https://github.com/stablyai/orca/issues/8962
**Malicious content:** none (technical bug report only)
## Summary
OMP posts agent-status hooks through the managed Pi-compatible extension, but Orca never records an OMP provider session ID. Cold restore therefore cannot build `omp --resume <id>` and falls back to an unresumed shell. Claude/Codex/etc. already go through the sleeping-session + cold-resume path.
## Evidence
### 1. `omp` is not a resumable TUI agent
`src/shared/agent-session-resume.ts`:
```ts
export const RESUMABLE_TUI_AGENTS = [
'claude', 'codex', 'gemini', 'antigravity',
'opencode', 'mimo-code', 'droid', 'grok', 'devin'
] as const
```
Runtime probe (this worktree):
```text
isResumableTuiAgent(omp): false
extractAgentProviderSession('omp', { session_id: 'abc-123' }): null
getAgentResumeArgv('omp', { key: 'session_id', id: 'abc-123' }): undefined
```
Saved: `docs/bug-reproductions/evidence/batch-repros-2026-07-15.txt`
### 2. Extract path hard-returns null for OMP
```147:192:src/shared/agent-session-resume.ts
export function extractAgentProviderSession(
source: AgentHookSource,
payload: Record<string, unknown>
): AgentProviderSessionMetadata | null {
switch (source) {
// ... claude/codex/gemini/... cases extract session ids ...
case 'amp':
case 'cursor':
case 'pi':
case 'omp':
case 'command-code':
case 'copilot':
case 'hermes':
return null
}
}
```
`getAgentResumeArgv` has no `omp` arm either (only the `RESUMABLE_TUI_AGENTS` set).
### 3. Sleeping-session capture requires resumable + resume argv
`src/renderer/src/store/slices/agent-status.ts` only creates a `SleepingAgentSessionRecord` when:
- agent is resumable (`isResumableTuiAgent`)
- entry has `providerSession`
- `getAgentResumeArgv(agent, providerSession)` succeeds
With (1)+(2), OMP never produces a sleeping record for cold restore.
### 4. OMP hooks never post `session_id`
Generated handlers in `src/main/pi/agent-status-handler-source.ts` register events without the extension `ctx` argument and only forward prompt/tool/message fields:
```ts
pi.on('before_agent_start', (event) => {
post('before_agent_start', { prompt: event.prompt ?? '' })
})
```
No `session_id: ctx.sessionManager.getSessionId()`. Even if OMP were added to `RESUMABLE_TUI_AGENTS`, the payload would still lack the ID today.
### 5. Unit suite confirms non-OMP agents only
`src/shared/agent-session-resume.test.ts` covers claude/codex/gemini/antigravity/opencode/mimo-code/droid/grok/devin — not omp. `pi` is explicitly rejected; omp is in the same null-return group.
## Why cold restore fails
Pipeline (issue-accurate, verified in tree):
1. Hook listener → `extractAgentProviderSession` → attach to status row
2. `collectSleepingAgentSessionRecordsForWorktree` → needs resumable + providerSession + resume argv
3. `pty-connection.ts` cold path consumes sleeping records to rebuild startup command
OMP is blocked at every input. Warm reattachment still works (no new agent process); cold restore (daemon kill / crash / no reattach) cannot resume.
## Re-run steps
```bash
cd /Users/nwparker/orca/workspaces/orca/cooked-PRs
# Automated proof
pnpm exec tsx -e "
import {
RESUMABLE_TUI_AGENTS,
extractAgentProviderSession,
isResumableTuiAgent
} from './src/shared/agent-session-resume.ts'
console.log(RESUMABLE_TUI_AGENTS)
console.log(isResumableTuiAgent('omp'))
console.log(extractAgentProviderSession('omp', { session_id: 'abc-123' }))
"
pnpm exec vitest run --config config/vitest.config.ts src/shared/agent-session-resume.test.ts
```
### Live UI (optional, production Orca)
Binary: `/Applications/Orca.app/Contents/Resources/bin/orca`
1. Open a terminal, launch `omp`, send a prompt so a session is persisted.
2. Force cold restore (terminate PTY daemon / quit without warm reattach).
3. Reopen workspace terminal.
4. Expect: no OMP sleeping record; terminal does not start with `omp --resume <id>`.
## Fix direction (from issue; not implemented here)
1. Handler source: accept `ctx`, attach `session_id` on every post.
2. Add `'omp'` to `RESUMABLE_TUI_AGENTS`; extract `session_id`; `getAgentResumeArgv` → `['omp', '--resume', id]`.
3. Keep `pi` unsupported unless CLI resume is verified.
4. Tests: hook payload, extract/resume round-trip, quit capture, warm-no-duplicate.
+123
View File
@@ -0,0 +1,123 @@
# #8970 — Closed agent session still lingers in sidebar (after #8825 / #8851)
**Status:** PARTIALLY REPRODUCED (code residual confirmed; happy-path unit tests pass; full live UI not run)
**Issue:** https://github.com/stablyai/orca/issues/8970
**Related:** #8851 (closed sessions linger), #8825 (merged — reap finished Claude named agents/teammates)
**Malicious content:** none
## Summary
Reporter on **v1.4.143** (claims #8825 included): after closing a Claude agent terminal with **Ctrl+W**, the worktree card’s inline agent list still shows the session and the “N agents” count stays high. Only **Cmd+Q + relaunch** clears it.
#8825 fixed **named subagent/teammate child rows** that stayed idle forever under a parent pane. This report is a **top-level** conversation closed via Ctrl+W — either incomplete coverage for that class or a residual race/rehydrate path.
## What the intended cleanup path does
User close on a terminal routes through `closeTerminalTab` → `closeTab` → **`dropAgentStatusByTabPrefix`**:
```1482:1497:src/renderer/src/store/slices/terminals.ts
// Why: sweep live AND retained agent-status entries for this tab ...
get().dropAgentStatusByTabPrefix(
tabId,
closingWorktreeId ? { worktreeId: closingWorktreeId } : undefined
)
get().clearPaneForegroundAgentByTabPrefix(tabId)
```
`dropAgentStatusByTabPrefix` (`agent-status.ts`):
- Deletes live `agentStatusByPaneKey` rows with `${tabId}:` prefix (+ completed worktree-attributed orphans for that worktree).
- Deletes retained + launch configs for those keys.
- Plants retention suppressors for live keys (anti race with `collectRetainedAgentsOnDisappear`).
- Marks `recentlyClosedAgentStatusTabIds` so late hook replays are ignored.
- IPC `agentStatus:dropByTabPrefix` to main last-status cache.
Unit coverage:
- `agent-status-drop.test.ts` — `closeTab drops completed worktree-attributed orphan rows`
- `agent-status-drop-ipc.test.ts` — drop IPC fan-out + recently-closed set
So the **happy path is implemented and unit-tested**. The bug is not “no cleanup function exists.”
## Residual / incomplete paths that still match the report
### 1. Explicit residual from #8825 (still in tree)
PR #8825 scope was **only** `claude-subagent-roster` + hook listener (working-only children). Known residual:
> A pane whose PTY teardown skips `clearPaneState`, or a persisted last-status snapshot with a stale working child, can still render its last snapshot until a new event or eviction.
Restart clears in-memory + rehydrates last-status with idle-child prune — matches reporter’s “only restart fixes it.”
### 2. Sidebar rows can exist without a live tab
`buildWorktreeAgentRows` / selectors intentionally keep:
- **Worktree-attributed live** entries when `entry.worktreeId` is set and state is **not** `done`, even if the tab is missing (`tabFromWorktreeAttributedStatusEntry` synthesizes a tab).
- **Retained done** rows (`rowSource: 'retained'`) without open tabs.
If drop misses a paneKey prefix, FIFO-evicts `recentlyClosedAgentStatusTabIds` (cap **1024**), or main re-pushes status before/without successful drop IPC, rows reappear until restart.
### 3. Subagent children inflate “N agents”
`WorktreeCardAgents` count uses `agents.length` (and lineage root count), which includes **subagent child rows**. #8825 changed roster to working-only + SubagentStop removes children, but any residual idle/working child snapshot still inflates the count without a separate terminal.
### 4. Ctrl+W closes via pane path, not Terminal.tsx tab handler
Terminal-focused Ctrl+W is handled as `closeActivePane` in `keyboard-handlers.ts` → `onRequestClosePane` → lifecycle → `closeTerminalTab` when last pane. That should still hit `closeTab`/`drop*`. No alternate “close without drop” was found for the primary path — residual is more likely **rehydrate/race/orphan keys** than a total missing call.
## Automated evidence gathered
| Check | Result |
|-------|--------|
| closeTab calls dropAgentStatusByTabPrefix | yes (source) |
| drop removes live+retained+IPC | yes (source + unit tests) |
| recentlyClosed suppresses setAgentStatus | yes |
| #8825 residual acknowledged | yes (PR body) |
| Live Ctrl+W in prod Orca | **not run** (UI session) |
## Classification rationale
- **Not NOT_REPRODUCED**: residual paths and reporter scenario are real; #8825 did not claim top-level tab-close perfection.
- **Not fully REPRODUCED**: happy-path unit tests pass; we did not capture a failing automated case that leaves a top-level row after `closeTab('user')`.
- **PARTIALLY REPRODUCED**: residual class confirmed; full intermittent live repro needs UI.
## Re-run steps
### Unit (cleanup happy path)
```bash
cd /Users/nwparker/orca/workspaces/orca/cooked-PRs
pnpm exec vitest run --config config/vitest.config.ts \
src/renderer/src/store/slices/agent-status-drop.test.ts \
src/renderer/src/store/slices/agent-status-drop-ipc.test.ts \
src/shared/claude-subagent-roster.test.ts
```
### Live UI (prod Orca) — needed for full REPRODUCED
Binary: `/Applications/Orca.app/Contents/Resources/bin/orca` / app UI
1. Single-worktree project; launch Claude in a terminal (conversation with a first-prompt title).
2. Confirm sidebar inline agent list shows 1+ agent.
3. Close terminal with **Ctrl+W** (confirm dialogs if any).
4. Observe whether agent row / “N agents” remain.
5. Compare `orca terminal list --worktree … --json` / `orca worktree ps --json` (runtime clean, sidebar dirty).
6. Restart Orca — row gone.
### Instrumentation if live repros
- Log `closeTab` reason + `dropAgentStatusByTabPrefix` keys removed.
- Diff `agentStatusByPaneKey` / `retainedAgentsByPaneKey` / subagents before/after close.
- Check main last-status after drop IPC.
- Note whether row is live, retained, title-derived, or subagent.
## Fix direction (hypotheses for implementers)
1. Ensure every tab-retirement path (including remote/parked) always drops agent status **and** main last-status.
2. Harden against post-drop rehydrate (recentlyClosed + main drop ordering).
3. Do not show worktree-attributed non-done rows for paneKeys whose tab was user-closed.
4. Confirm #8825 working-only roster covers reporter’s Claude version of named agents.
5. Optional: dismiss affordance on stale sidebar rows (issue #8851 noted no per-session dismiss).
+42
View File
@@ -0,0 +1,42 @@
# #8974 — Claude usage stuck on “Refreshing sign-in”
**Status:** REPRODUCED (UI stuck-state mapping; live OAuth env not required)
**Issue:** https://github.com/stablyai/orca/issues/8974
**Malicious content:** none
## Summary
Claude Code works in-terminal, but usage UI stays on **Refreshing sign-in** and never shows live percentages.
## Root cause
`usage-error-copy.ts` maps these Claude `failureKind` values to permanent refreshing copy:
- `stale-token`
- `refreshable-credentials-without-token`
- `delegated-refresh-required`
Message: *“Claude sign-in is being refreshed. Agent sessions may still be signed in.”*
There is **no age/timeout escalation** in the label path (never becomes “Sign-in failed” / re-auth CTA based on `updatedAt`). Main fetcher can keep emitting stale-token / deferred-by-live-session while the CLI session remains usable (separate credential surface).
`applyStalePolicy` may retain previous quota windows under `status: 'error'` with merged failureKind — chip still shows refreshing.
## Automated proof
```bash
pnpm exec vitest run --config config/vitest.config.ts \
src/renderer/src/components/status-bar/repro-8974-refreshing-sign-in-stuck.test.ts
```
## Suggested fix
- After N failures or T minutes, escalate copy to re-authenticate / usage unavailable.
- Surface distinct CTA when agent sessions are healthy but usage OAuth is dead.
- Ensure CLI-repair / keychain refresh completion clears failureKind.
## Evidence
- `src/renderer/src/components/status-bar/repro-8974-refreshing-sign-in-stuck.test.ts`
- `src/renderer/src/components/status-bar/usage-error-copy.ts`
- `src/main/rate-limits/claude-fetcher.ts`
+52
View File
@@ -0,0 +1,52 @@
# #8986 — OMP tab agent status flashes; ghost agent in sidebar
**Status:** REPRODUCED (status flash, code-level); ghost list **related residual** (same surfaces as #8970)
**Issue:** https://github.com/stablyai/orca/issues/8986
**Malicious content:** none
## Summary
1. OMP terminal tab status badge thrashing (working/idle).
2. Sidebar shows more agents than open tabs; ghost cannot focus/delete reliably.
## Root cause — status flash
OMP profile (unlike Codex) **drives synthetic working titles** from hooks:
```ts
shouldDriveSyntheticAgentTitleFromHook('omp', 'working') === true // codex: false
```
Main fabricates braille spinner OSC titles (`⠋ OMP`). Native / collapsed titles are bare `OMP` / `OMP ready`.
`detectAgentStatusFromTitle` / pi-compatible helper:
| Title | Status |
|-------|--------|
| `⠋ OMP` / braille + OMP | working |
| `OMP ready` / bare `OMP` | idle |
Competing synthetic spinner vs ready/bare frames → tab status thrash under normal agent activity.
## Root cause — ghost agent (partial)
Sidebar builds from live `agentStatusByPaneKey` + retained rows + worktree-attributed entries that can **synthesize a tab** when missing. Dismiss calls `dropAgentStatus` + `dismissRetainedAgent`, but rehydrate / late hook / worktree-attributed live keys can repopulate rows without a focusable tab (same class as #8970 residuals).
## Automated proof (flash)
```bash
pnpm exec vitest run --config config/vitest.config.ts \
src/shared/repro-8986-omp-status-flash.test.ts
```
## Suggested fix
- Match Codex: `synthesizeWorkingTitle: false` for OMP, **or** collapse all OMP working frames to a stable title before status detect.
- Ghost: ensure dismiss plants suppressors for worktree-attributed keys; refuse non-focusable rows or force drop-by-worktree.
## Evidence
- `src/shared/repro-8986-omp-status-flash.test.ts`
- `src/shared/synthetic-agent-title.ts`
- `src/shared/agent-title-status.ts`
- `src/shared/pi-compatible-synthetic-title.ts`
@@ -0,0 +1,27 @@
# Fix orchestration pass — 5 easiest has_repro bugs
**Date:** 2026-07-16
**Agent:** GPT-5.6-Sol (worktree-isolated)
**Worktrees:** Orca CLI (`orca worktree create`)
## Results
| Issue | PR | Worktree | Status |
|------:|----|----------|--------|
| [#8533](https://github.com/stablyai/orca/issues/8533) Cmd+Shift+E collision | [#9007](https://github.com/stablyai/orca/pull/9007) | `fix-8533-cmd-shift-e` | Open |
| [#8584](https://github.com/stablyai/orca/issues/8584) Mod+0 collision | [#9003](https://github.com/stablyai/orca/pull/9003) | `fix-8584-mod-0` | Open |
| [#8378](https://github.com/stablyai/orca/issues/8378) Codex reset time | [#9004](https://github.com/stablyai/orca/pull/9004) | `fix-8378-codex-reset-time` | Open |
| [#8844](https://github.com/stablyai/orca/issues/8844) file open missing path | [#9006](https://github.com/stablyai/orca/pull/9006) | `fix-8844-file-open-exists` | Open |
| [#8535](https://github.com/stablyai/orca/issues/8535) serve port pin | [#9005](https://github.com/stablyai/orca/pull/9005) | `fix-8535-serve-port-pin` | Open |
## Fix summary
1. **#8533** — Emulator default `Mod+Shift+E` → `Mod+Alt+Shift+E`; Explorer keeps `Mod+Shift+E`.
2. **#8584** — Focus worktree list `Mod+0` → `Mod+Shift+0`; zoom stays `Mod+0`.
3. **#8378** — Status bar chip uses remaining time when `resetsAt` present (matches popup).
4. **#8844** — `openMobileFile` stats path before open; missing → error, no ghost tab.
5. **#8535** — Explicit CLI `--port` binds pin first; default desktop keeps fallback-first (STA-1511).
## PR template requirement
Each PR includes Description, Evidence, ELI5, User-regression-tradeoffs.
+60
View File
@@ -0,0 +1,60 @@
# Bug reproduction progress log
Living tracker for the orchestration pass. **Index of results:** [README.md](./README.md).
**Windows deferred:** [WINDOWS-ONLY.md](./WINDOWS-ONLY.md).
| Field | Value |
|-------|-------|
| Started | 2026-07-15 |
| Updated | 2026-07-16 |
| Host | macOS · production Orca 1.4.143 |
| Worktree | `cooked-PRs` |
| Constraint | No live `orca file open` of missing paths (ghost ENOENT tabs) |
| GitHub labels | **`has_repro`** · **`cannot_repro`** |
---
## Final snapshot (Pass 3 complete)
| Bucket | Count |
|--------|------:|
| **`has_repro`** (open issues) | **~46** |
| **`cannot_repro`** | **4** — #8440, #8749, #8838, #8943 |
| **PARTIAL** (no label) | **2** — #8539, #8970 |
| **DEFERRED Windows** | #8813 + [WINDOWS-ONLY.md](./WINDOWS-ONLY.md) |
| **Issue writeups** | **54** (`docs/bug-reproductions/<n>.md`) |
Filters:
- [has_repro](https://github.com/stablyai/orca/issues?q=is%3Aissue+label%3Ahas_repro)
- [cannot_repro](https://github.com/stablyai/orca/issues?q=is%3Aissue+label%3Acannot_repro)
---
## Session log
### Pass 1 — 2026-07-15
Initial scan; first 14 reproductions; #8838 cannot_repro; #8844 ghost-tab soft-open.
### Pass 2 — 2026-07-15
Batches A+B (+10 GHE/pet/undici/serve/Linear/RM/history).
### Pass 3 — 2026-07-16
- Label rename: `cannot_reproduce` → **`cannot_repro`**.
- **Batch C complete** (7 REPRO + 2 NOT): 8797, 8378, 8715, 8881, 8878, 8478, 8593 + cannot 8749, 8943.
- **Batch D complete** (8 REPRO + 1 PARTIAL + 1 DEFERRED): 8450, 8742, 8299, 8733, 8399, 8986, 8372, 8974 + partial 8539 + Windows 8813.
- **Batch E complete** (8 REPRO + 1 NOT): 8482, 8752, 8726, 8934, 8335, 8758, 8541, 8622 + cannot 8440.
- Fixed flaky tests: 8378, 8715, 8881.
### Unit tests
All batch C/D/E repro suites verified green (e.g. 31 + 17 + 29 tests in final re-runs).
---
## Next agent checklist
1. Read this file + README.
2. Prefer open bugs **without** `has_repro` / `cannot_repro` and without a doc.
3. Windows → WINDOWS-ONLY.md only.
4. Write `docs/bug-reproductions/<n>.md` + vitest under `src/**`.
5. Apply `has_repro` or `cannot_repro`.
6. Never `orca file open` missing paths.
+195
View File
@@ -0,0 +1,195 @@
# Bug reproduction index
Orchestration **2026-07-15** · worktree `cooked-PRs` · production Orca **1.4.143** (`/Applications/Orca.app`) · macOS.
## How to use this folder
| Path | Purpose |
|------|---------|
| `README.md` | This index |
| `WINDOWS-ONLY.md` | Windows/WSL bugs deferred (need a Windows host) |
| `<issue-number>.md` | Per-issue status, evidence, re-run steps |
| `scripts/` | Shell / node helpers |
| `evidence/` | Captured JSON, logs |
### Production CLI note
`/usr/local/bin/orca` may be a **stale dev shim** pointing at a deleted worktree. Prefer:
```bash
PROD="/Applications/Orca.app/Contents/Resources/bin/orca"
"$PROD" status --json
```
### Security
Do **not** run issue attachments (`.exe`, `.cmd`, `.ps1`, mystery archives). Recreate fixtures yourself. No malicious payloads were found in the issues reviewed this pass.
### Status legend
- **REPRODUCED** — conclusive evidence + re-run steps/tests → GitHub label **`has_repro`**
- **NOT REPRODUCED** — attempted; current tree/app does not show the bug → GitHub label **`cannot_repro`**
- **PARTIAL** — root cause narrowed; full product path not live-proven (not labeled)
- **DEFERRED** — Windows / mobile / special host
Filter on GitHub:
- [`has_repro`](https://github.com/stablyai/orca/issues?q=is%3Aissue+label%3Ahas_repro)
- [`cannot_repro`](https://github.com/stablyai/orca/issues?q=is%3Aissue+label%3Acannot_repro)
---
## REPRODUCED (conclusive)
| Issue | Summary | Re-run |
|------:|---------|--------|
| [8844](./8844.md) | `orca file open` succeeds for missing files | default script is non-destructive; live open needs `ORCA_REPRO_8844_LIVE=1` (creates ghost tabs) |
| [8670](./8670.md) | Clean worktree + submodule needs `git worktree remove --force` | `bash docs/bug-reproductions/scripts/repro-8670-submodule-worktree-remove.sh` |
| [8454](./8454.md) | Bare `localhost:…` forced to `http://` | `pnpm exec vitest run src/shared/browser-url.test.ts` |
| [8533](./8533.md) / [8584](./8584.md) | Default shortcut collisions (`Mod+Shift+E`, `Mod+0`) | `pnpm exec vitest run --config config/vitest.config.ts src/shared/repro-8533-8584-shortcut-conflicts.test.ts` |
| [8595](./8595.md) | Bold Text color override is a no-op | `pnpm exec vitest run --config config/vitest.config.ts src/renderer/src/components/terminal-pane/repro-8595-bold-theme-noop.test.ts` |
| [8832](./8832.md) | Cmd-click URL glues next line text | `pnpm exec vitest run --config config/vitest.config.ts src/renderer/src/components/terminal-pane/repro-8832-url-next-line.test.ts` |
| [8940](./8940.md) | opencode labeled as Claude Code | `pnpm exec vitest run --config config/vitest.config.ts src/shared/repro-8940-opencode-as-claude.test.ts` |
| [8903](./8903.md) | Cmd-J focus lands on wrong (often hidden) surface | `pnpm exec vitest run --config config/vitest.config.ts src/renderer/src/components/cmd-j/repro-8903-cmdj-focus-fallback.test.ts` |
| [8962](./8962.md) | OMP terminals excluded from cold session restore | See `8962.md` (unit proofs on `RESUMABLE_TUI_AGENTS` / `extractAgentProviderSession`) |
| [8808](./8808.md) | Agent recognition fails on env-prefixed cmdline after restart | See `8808.md` (`recognizeAgentProcess` with `CLAUDE_CONFIG_DIR=… claude`); fix PR **#8942** |
| [8865](./8865.md) | Project Group hides members when Hide sleeping filters workspaces | See `8865.md`; fix PR **#8866** |
| [8532](./8532.md) | `Cmd+Shift+O` open markdown only works in floating terminal | See `8532.md`; fix PR **#8564** |
| [8577](./8577.md) | Settings search query leaks into Shortcuts row filter | See `8577.md`; fix PR **#8579** |
| [8784](./8784.md) | GHE PR avatars built from `github.com/{login}.png` | `pnpm exec vitest run --config config/vitest.config.ts src/renderer/src/components/github/repro-8784-ghe-avatar-fallback.test.ts`; fix PR **#8831** |
| [8935](./8935.md) | GHE PR work item diffs do not load (no Enterprise host) | `pnpm exec vitest run --config config/vitest.config.ts src/main/github/repro-8935-ghe-pr-diff-host.test.ts`; fix PR **#8932** |
| [8729](./8729.md) | .codex-pet ignores per-frame duration (uniform 8 fps) | `pnpm exec vitest run --config config/vitest.config.ts src/main/ipc/repro-8729-codex-pet-fps.test.ts` |
| [8695](./8695.md) | undici 7.28.0 paused-parser assert crashes main | unit + live: `bash docs/bug-reproductions/scripts/repro-8695-undici-paused-parser.sh` |
| [8720](./8720.md) | SSH relay npm 12 skips node-pty (no allowScripts) | `pnpm exec vitest run --config config/vitest.config.ts src/main/ssh/repro-8720-npm12-allow-scripts.test.ts` |
| [8535](./8535.md) | pinned `orca serve --port` overridden by mobile-ws fallback | `pnpm exec vitest run --config config/vitest.config.ts src/main/runtime/rpc/repro-8535-ws-fallback-port-order.test.ts` |
| [8739](./8739.md) | Linear filters only first selected team | `pnpm exec vitest run --config config/vitest.config.ts src/renderer/src/components/repro-8739-linear-filter-first-team.test.ts` |
| [8459](./8459.md) | Resource Manager kills live daemon as orphan | `pnpm exec vitest run --config config/vitest.config.ts src/renderer/src/components/status-bar/repro-8459-orphan-live-daemon.test.ts` |
| [8457](./8457.md) | headless serve hijacks GUI relaunch | `pnpm exec vitest run --config config/vitest.config.ts src/main/startup/repro-8457-serve-desktop-hijack.test.ts` |
| [8864](./8864.md) | Agent history lookup hangs forever | `pnpm exec vitest run --config config/vitest.config.ts src/main/ai-vault/repro-8864-history-lookup-no-timeout.test.ts` |
| [8482](./8482.md) | Window Blur sustained high GPU on macOS | `pnpm exec vitest run --config config/vitest.config.ts src/main/window/repro-8482-window-blur-gpu-cost.test.ts` |
| [8752](./8752.md) | Setup-script prompt stays after orca.yaml setup | `pnpm exec vitest run --config config/vitest.config.ts src/renderer/src/components/sidebar/repro-8752-setup-script-prompt-stale.test.ts` |
| [8726](./8726.md) | Tasks fork: auto PR→origin; count cache 120s | `pnpm exec vitest run --config config/vitest.config.ts src/main/github/repro-8726-fork-pr-auto-origin.test.ts` |
| [8934](./8934.md) | Jira ADF media images dropped in Tasks | `pnpm exec vitest run --config config/vitest.config.ts src/main/jira/repro-8934-adf-media-dropped.test.ts` |
| [8335](./8335.md) | Terminal stuck: mouse preserved on agent reattach | `pnpm exec vitest run --config config/vitest.config.ts src/renderer/src/components/terminal-pane/repro-8335-agent-editor-mouse-preserve.test.ts` |
| [8758](./8758.md) | node-pty unavailable on remote (same as #8720) | `pnpm exec vitest run --config config/vitest.config.ts src/main/ssh/repro-8758-node-pty-remote.test.ts` |
| [8541](./8541.md) | Remote multiplex timeout cascade | `pnpm exec vitest run --config config/vitest.config.ts src/shared/repro-8541-multiplex-timeout-cascade.test.ts` |
| [8622](./8622.md) | SSH keyboard-interactive MFA unsupported | `pnpm exec vitest run --config config/vitest.config.ts src/main/ssh/repro-8622-keyboard-interactive-missing.test.ts` |
| [8450](./8450.md) | SSH relay picks system Node without npm | `pnpm exec vitest run --config config/vitest.config.ts src/main/ssh/repro-8450-node-without-npm.test.ts` |
| [8742](./8742.md) | Antigravity sessions missing from AI Vault | `pnpm exec vitest run --config config/vitest.config.ts src/main/ai-vault/repro-8742-antigravity-not-scanned.test.ts` |
| [8299](./8299.md) | Shift+Space input-source switch also types space | `pnpm exec vitest run --config config/vitest.config.ts src/renderer/src/components/terminal-pane/repro-8299-shift-space-input-source.test.ts` |
| [8733](./8733.md) / [8399](./8399.md) | Option compose broken under kitty (vim/Pi) | `pnpm exec vitest run --config config/vitest.config.ts src/renderer/src/components/terminal-pane/repro-8733-8399-kitty-option-compose.test.ts` |
| [8986](./8986.md) | OMP tab status flash (working↔idle titles) | `pnpm exec vitest run --config config/vitest.config.ts src/shared/repro-8986-omp-status-flash.test.ts` |
| [8372](./8372.md) | Closed Resource Manager CLI count vs live daemon | `pnpm exec vitest run --config config/vitest.config.ts src/renderer/src/components/status-bar/repro-8372-closed-cli-count-stale.test.ts` |
| [8974](./8974.md) | Claude usage stuck “Refreshing sign-in” | `pnpm exec vitest run --config config/vitest.config.ts src/renderer/src/components/status-bar/repro-8974-refreshing-sign-in-stuck.test.ts` |
| [8797](./8797.md) | Background Opacity / Window Blur no-op on macOS | `pnpm exec vitest run --config config/vitest.config.ts src/main/window/repro-8797-blur-opacity-noop.test.ts` |
| [8378](./8378.md) | Codex session reset: popup vs status bar | `pnpm exec vitest run --config config/vitest.config.ts src/shared/repro-8378-codex-reset-time-mismatch.test.ts` |
| [8715](./8715.md) | OMP scroll resets on tab switch (alt buffer) | `pnpm exec vitest run --config config/vitest.config.ts src/renderer/src/lib/pane-manager/repro-8715-omp-alt-screen-scroll.test.ts` |
| [8881](./8881.md) | Sidebar duplicates worktrees after re-pair | `pnpm exec vitest run --config config/vitest.config.ts src/renderer/src/store/slices/repro-8881-runtime-repair-duplicates.test.ts` |
| [8878](./8878.md) | Remote client resumes live host agents | `pnpm exec vitest run --config config/vitest.config.ts src/renderer/src/lib/repro-8878-remote-resume-no-runtime-gate.test.ts`; fix PR **#8887** |
| [8593](./8593.md) | Lingering idle subagent sidebar rows | `pnpm exec vitest run --config config/vitest.config.ts src/renderer/src/components/sidebar/repro-8593-idle-subagent-rows.test.ts` |
| [8478](./8478.md) | OpenCode native title → wrong icon | `pnpm exec vitest run --config config/vitest.config.ts src/shared/repro-8478-opencode-native-title-icon.test.ts`; fix PR **#8590** |
### NOT REPRODUCED (Batch C)
| Issue | Summary | Re-run |
|------:|---------|--------|
| [8749](./8749.md) | Agent seatbelt blocks claude-hook.sh | live sandbox_check=0; `src/main/agent-hooks/repro-8749-hook-path-no-seatbelt.test.ts` |
| [8943](./8943.md) | Browser shows insecure | screenshot-only; `src/main/browser/repro-8943-browser-insecure-indicator.test.ts` |
### Batch re-run (unit tests that live under `src/`)
```bash
pnpm exec vitest run --config config/vitest.config.ts \
src/shared/repro-8533-8584-shortcut-conflicts.test.ts \
src/renderer/src/components/terminal-pane/repro-8595-bold-theme-noop.test.ts \
src/renderer/src/components/terminal-pane/repro-8832-url-next-line.test.ts \
src/shared/repro-8940-opencode-as-claude.test.ts \
src/renderer/src/components/cmd-j/repro-8903-cmdj-focus-fallback.test.ts \
src/shared/browser-url.test.ts \
src/renderer/src/components/github/repro-8784-ghe-avatar-fallback.test.ts \
src/main/github/repro-8935-ghe-pr-diff-host.test.ts \
src/main/ipc/repro-8729-codex-pet-fps.test.ts \
src/main/repro-8695-undici-paused-parser.test.ts \
src/main/ssh/repro-8720-npm12-allow-scripts.test.ts \
src/main/runtime/rpc/repro-8535-ws-fallback-port-order.test.ts \
src/renderer/src/components/repro-8739-linear-filter-first-team.test.ts \
src/renderer/src/components/status-bar/repro-8459-orphan-live-daemon.test.ts \
src/main/startup/repro-8457-serve-desktop-hijack.test.ts \
src/main/ai-vault/repro-8864-history-lookup-no-timeout.test.ts \
src/main/window/repro-8482-window-blur-gpu-cost.test.ts \
src/renderer/src/components/sidebar/repro-8752-setup-script-prompt-stale.test.ts \
src/main/github/repro-8726-fork-pr-auto-origin.test.ts \
src/main/jira/repro-8934-adf-media-dropped.test.ts \
src/renderer/src/components/terminal-pane/repro-8335-agent-editor-mouse-preserve.test.ts \
src/main/ssh/repro-8758-node-pty-remote.test.ts \
src/shared/repro-8541-multiplex-timeout-cascade.test.ts \
src/main/ssh/repro-8622-keyboard-interactive-missing.test.ts \
src/main/ssh/repro-8450-node-without-npm.test.ts \
src/main/ai-vault/repro-8742-antigravity-not-scanned.test.ts \
src/renderer/src/components/terminal-pane/repro-8299-shift-space-input-source.test.ts \
src/renderer/src/components/terminal-pane/repro-8733-8399-kitty-option-compose.test.ts \
src/shared/repro-8986-omp-status-flash.test.ts \
src/renderer/src/components/status-bar/repro-8372-closed-cli-count-stale.test.ts \
src/renderer/src/components/status-bar/repro-8974-refreshing-sign-in-stuck.test.ts \
src/renderer/src/store/slices/repro-8539-fetchAllWorktrees-reconnect.test.ts
```
```bash
# Git fixture (safe)
bash docs/bug-reproductions/scripts/repro-8670-submodule-worktree-remove.sh
# #8844 — evidence-only by default (does NOT open editor tabs)
bash docs/bug-reproductions/scripts/repro-8844-file-open-missing.sh
# Live missing-path open creates ghost ENOENT tabs — only if you really need it:
# ORCA_REPRO_8844_LIVE=1 bash docs/bug-reproductions/scripts/repro-8844-file-open-missing.sh
# #8695 — process crash expected (undici assert)
bash docs/bug-reproductions/scripts/repro-8695-undici-paused-parser.sh
```
---
## PARTIAL
| Issue | Summary | Notes |
|------:|---------|-------|
| [8970](./8970.md) | Closed agent session lingers in sidebar | Happy-path close clears status; residual races after #8825/#8851 not live-proven |
| [8539](./8539.md) | Renderer freeze 87s on reconnect many worktrees | Call sites + O(repos) work proven; live multi-host freeze not run |
---
## NOT REPRODUCED
| Issue | Summary | Notes |
|------:|---------|-------|
| [8838](./8838.md) | `<br>` in markdown table cells | Current `MarkdownPreview` pipeline emits real `<br/>` — `pnpm exec node docs/bug-reproductions/scripts/repro-8838-br-in-table.mjs` |
| [8440](./8440.md) | MCP OAuth not persisting in Orca terminals | Maintainer could not repro; no in-tree generic MCP OAuth store bug; needs agent/MCP credential path |
| [8749](./8749.md) | Agent seatbelt blocks claude-hook.sh | Live daemon PTY `sandbox_check=0`; no seatbelt in daemon spawn code |
| [8943](./8943.md) | Browser shows insecure | Screenshot-only; intentional popup “Not secure” for remote http |
---
## DEFERRED
| Bucket | Doc / notes |
|--------|-------------|
| **Windows / WSL** | [WINDOWS-ONLY.md](./WINDOWS-ONLY.md) — 30+ open bugs |
| **Linux/IBus/X11** | #8861 (CJK IME), #8860 (middle-click paste twice) |
| **GitHub Enterprise (live UI)** | Live GHE UI still needs a host; code-level repros done for #8935 / #8784 (fix PRs #8932 / #8831) |
| **Mobile / iOS / Android** | #8933, #8889, #8818, #8700, #8666, #8555, #8592, #8364, #8313, … |
| **Sandbox / live agent** | #8749 (claude-hook.sh EPERM) — needs Orca-hosted Claude session |
| **SSH / remote runtime** | #8878, #8871, #8696, … — need remote hosts (#8720/#8450/#8758/#8541/#8622 code-level done) |
| **Perf / scale** | #8814, #8882, #8652 — need multi-worktree load (#8539 PARTIAL call sites) |
| **Windows project status UI** | #8813 — see [WINDOWS-ONLY.md](./WINDOWS-ONLY.md) / `8813.md` |
---
## What we need from you (only blockers for next steps)
1. **Windows host or CI** — process [WINDOWS-ONLY.md](./WINDOWS-ONLY.md) (P0: #8751, #8734, #6874).
2. **GHE credentials / sandbox** — optional live UI for #8935/#8784 (code-level REPRODUCED; fix PRs #8932 / #8831).
3. **Mobile device or emulator pairing** — for freezes/rename/browser-tab bugs.
4. **Confirm live UI for #8970** — still seeing lingering closed sessions on 1.4.143 after #8825?
5. **Linux remote with npm 12** — optional live for #8720 (code-level REPRODUCED: missing `allowScripts`).
Everything else in the REPRODUCED table is re-runnable by another agent without extra access.
+59
View File
@@ -0,0 +1,59 @@
# Windows-only bugs (deferred)
These open issues are labeled `os:Windows` and/or are Windows/WSL-specific. They are **not** reproduced in this macOS orchestration pass. Track and reproduce on a Windows machine (or CI Windows runner) separately.
| Issue | Title | Notes |
|------:|-------|-------|
| 8813 | Visual indicators for statuses within the project don't appear | Yellow checkmark while working; CPU/memory; see `8813.md` |
| 8956 | Fail to list remote dirs | Remote FS listing on Windows |
| 8834 | In-app terminal cannot open Cursor CLI (system terminal can) | PATH / shell spawn |
| 8795 | Claude account switching does not work in Remote SSH projects | Windows host reported |
| 8793 | Windows Cursor agent (.cmd) cannot launch with prompt in argv — asks to Remove {prompt} | cmd quoting |
| 8787 | Windows wait-for-setup startup policy never starts the agent — doubled quotes in cmd wrapper | `ORCA_SETUP_MARKER` |
| 8763 | Orca has stopped recognizing Codex agents | Windows agent detection |
| 8754 | Orca don't render flutter command in Codex session without refreshing the tab | P1 render |
| 8751 | bug on windows upon returning session | P0 session restore |
| 8734 | Ctrl+Alt chords are swallowed by terminal input on Windows | P0 keybindings |
| 8645 | Hooks fail after updating Codex to v0.144.3 | Windows hooks |
| 8629 | Windows hook lifecycle test can overwrite live Codex runtime hooks | Test safety |
| 8627 | File sidebar is outdated on an old Orca version | pending-user-response |
| 8563 | Mouse wheel sends arrow keys to a normal-buffer CLI without mouse reporting | Wheel→keys |
| 8537 | Windows onboarding saves WSL as the default shell, but Settings shows PowerShell | Onboarding |
| 8475 | Chinese text sometimes garbled in Codex on Windows (WSL) | Encoding / WSL |
| 8302 | Windows agent list missing DeepSeek, icon mismatches, MiniMax quota | Agent list |
| 8291 | Terminal drag is completely broken | DnD |
| 8290 | Orca Windows renderer bug | Renderer |
| 8289 | Previous workspace surface remains visible after split collapse and worktree switch | Compositor |
| 8275 | Terminal daemon dies after batch worktree removal | Daemon |
| 8272 | Remote Windows host can disappear while smart sort refreshes it | Remote host |
| 8269 | Codex hook trust is lost between launches in WSL runtime | WSL hooks |
| 8258 | Cursor Agent CLI is not detected in the sidebar on Windows | Detection |
| 7725 | marketplace upgrade staging directories not cleaned up (multi-GB) | Disk growth |
| 7703 | Orca periodically causes system-wide keyboard focus loss | Focus steal |
| 7693 | The status of Kiro CLIs has not yet been determined | Status |
| 7563 | WSL GUI agent detection misses regular Claude while Claude CLI works | WSL detect |
| 7555 | codex-windows-sandbox-setup.exe module not found | Sandbox setup |
| 7372 | Switching b/w projects - terminal in focus is lost | Focus |
| 7175 | 'Not responding' and crash when running more than 3 sessions (32GB) | Perf/crash |
| 6896 | Worktree "Local setup command" built as cmd.exe under Git Bash | Setup shell |
| 6874 | Windows AppHangB1 when opening/activating terminal session | P0 hang |
| 6694 | Git Bash + Cursor agent: prompt mangled + hooks block | Dual bug |
| 6546 | Amp history not showing in right-side history panel on Windows 11 | History |
| 6487 | Application blocked by Windows Smart App Control (SAC) | Signing |
| 5345 | Scrolling causes duplicated/garbled text and panel overlap (Windows + WSL) | Render |
| 8366 | Sometimes Grok CLI is not detected in Orca but works in system terminal | Title says windows |
| 7351 | bundled CLI wrapper (orca.cmd) looks for Orca.exe in wrong directory | Labeled os:macos incorrectly? Verify on Windows |
## How to pick these up later
1. Use a Windows 11 host with a fresh Orca install (or the same version reported in the issue).
2. Prefer **production** `orca` from the installed app, not a stale dev CLI shim.
3. For each issue: capture screenshots, CLI JSON (`--json`), Event Viewer AppHang entries, and a short script under `docs/bug-reproductions/scripts/` when the bug is pure logic (quoting, PATH, force flags).
4. Never execute untrusted user-uploaded binaries from issues (especially `.exe` / `.cmd` / `.ps1` attachments). Recreate fixtures yourself.
## WSL-adjacent (may need Windows host even if not pure Win32)
| Issue | Title |
|------:|-------|
| 8470 | DaemonProtocolError when waking a slept WSL workspace |
| 8941 | WSL resource usage spikes (not always labeled bug) |
@@ -0,0 +1,9 @@
git version 2.44.0
fixture: /var/folders/td/0d_w3jrs3654mtmk1gd6kpq00000gn/T//orca-repro-8670.4S3ZkI
parent status:
submodule status:
--- without --force (exit 128) ---
fatal: working trees containing submodules cannot be moved or removed
--- with --force (exit 0) ---
worktree path removed: yes
RESULT: REPRODUCED (clean submodule worktree requires --force)
@@ -0,0 +1,14 @@
TMP=/var/folders/td/0d_w3jrs3654mtmk1gd6kpq00000gn/T/tmp.PVIKlISz5P
Cloning into '/private/var/folders/td/0d_w3jrs3654mtmk1gd6kpq00000gn/T/tmp.PVIKlISz5P/parent-repo/vendor/sub'...
Preparing worktree (detached HEAD 18dc7df)
Cloning into '/private/var/folders/td/0d_w3jrs3654mtmk1gd6kpq00000gn/T/tmp.PVIKlISz5P/linked-wt/vendor/sub'...
=== clean status (parent) ===
=== clean status (submodule) ===
=== remove WITHOUT force (expect fail) ===
fatal: working trees containing submodules cannot be moved or removed
exit=128
=== remove WITH force (expect success) ===
exit=0
STATUS=REPRODUCED
Git: git version 2.44.0
@@ -0,0 +1,54 @@
=== #8695 undici paused-parser repro 2026-07-16T06:22:32Z ===
node: v24.18.0
--- system node ---
node:internal/assert/utils:77
throw err;
^
AssertionError [ERR_ASSERTION]: The expression evaluated to a falsy value:
assert(!this.paused)
at Parser.finish (node:internal/deps/undici/undici:7380:9)
at Socket.onHttpSocketEnd (node:internal/deps/undici/undici:7819:34)
at Socket.emit (node:events:521:24)
at endReadableNT (node:internal/streams/readable:1729:12)
at process.processTicksAndRejections (node:internal/process/task_queues:90:21) {
generatedMessage: true,
code: 'ERR_ASSERTION',
actual: false,
expected: true,
operator: '==',
diff: 'simple'
}
Node.js v24.18.0
system node exit: 1
--- Orca Electron as node (/Applications/Orca.app/Contents/MacOS/Orca) ---
node:internal/assert/utils:77
throw err;
^
AssertionError [ERR_ASSERTION]: The expression evaluated to a falsy value:
assert(!this.paused)
at Parser.finish (node:internal/deps/undici/undici:7380:9)
at Socket.onHttpSocketEnd (node:internal/deps/undici/undici:7819:34)
at Socket.emit (node:events:521:24)
at endReadableNT (node:internal/streams/readable:1729:12)
at process.processTicksAndRejections (node:internal/process/task_queues:90:21) {
generatedMessage: true,
code: 'ERR_ASSERTION',
actual: false,
expected: true,
operator: '==',
diff: 'simple'
}
Node.js v24.18.0
orca exit: 1
RESULT: REPRODUCED (process crashed or non-zero exit as expected)
@@ -0,0 +1,18 @@
{
"cli": "/Applications/Orca.app/Contents/Resources/bin/orca",
"relativePath": "spec/does-not-exist-repro-8844-1784182271.md",
"exitCode": 0,
"response": {
"id": "334a60ae-3053-4530-a320-3757ae9618bf",
"ok": true,
"result": {
"worktree": "8cf8301a-6725-4f9f-a5f4-93ba5c7c1e99::/Users/nwparker/orca/workspaces/orca/cooked-PRs",
"relativePath": "spec/does-not-exist-repro-8844-1784182271.md",
"kind": "markdown",
"opened": true
},
"_meta": {
"runtimeId": "feee85bb-b526-4f92-8c2b-959c1b3a2c03"
}
}
}
@@ -0,0 +1,33 @@
stamp: 2026-07-16T06:13:34Z
orca_bin: /Applications/Orca.app/Contents/Resources/bin/orca
cwd: /Users/nwparker/orca/workspaces/orca/cooked-PRs
relative_path: spec/does-not-exist-repro-8844.md
absolute_path: /Users/nwparker/orca/workspaces/orca/cooked-PRs/spec/does-not-exist-repro-8844.md
exists_before: no
--- orca file open --json ---
{
"id": "6f65d67e-c31d-4d4e-b3a4-95bebe1a9b8a",
"ok": true,
"result": {
"worktree": "8cf8301a-6725-4f9f-a5f4-93ba5c7c1e99::/Users/nwparker/orca/workspaces/orca/cooked-PRs",
"relativePath": "spec/does-not-exist-repro-8844.md",
"kind": "markdown",
"opened": true
},
"_meta": {
"runtimeId": "feee85bb-b526-4f92-8c2b-959c1b3a2c03"
}
}
exit: 0
exists_after: no
NOTES:
- Bug: ok:true / opened:true / exit 0 even though the file is missing.
- Ghost tab: Orca creates an editor tab; loading later fails with fs:readFile ENOENT.
- Root cause (source): RuntimeFileCommands.openMobileFile never stats the path;
it classifies by extension and always returns opened:true for non-binary kinds,
then host.openFile(...). See src/main/runtime/orca-runtime-files.ts openMobileFile.
- CLI: src/cli/handlers/file.ts 'file open' trusts files.open RPC result.
parsed ok=true opened=true exit=0
RESULT: REPRODUCED
@@ -0,0 +1,13 @@
{
"id": "6f65d67e-c31d-4d4e-b3a4-95bebe1a9b8a",
"ok": true,
"result": {
"worktree": "8cf8301a-6725-4f9f-a5f4-93ba5c7c1e99::/Users/nwparker/orca/workspaces/orca/cooked-PRs",
"relativePath": "spec/does-not-exist-repro-8844.md",
"kind": "markdown",
"opened": true
},
"_meta": {
"runtimeId": "feee85bb-b526-4f92-8c2b-959c1b3a2c03"
}
}
@@ -0,0 +1,18 @@
CLI=/Applications/Orca.app/Contents/Resources/bin/orca
exitCode=0
response:
{
"id": "334a60ae-3053-4530-a320-3757ae9618bf",
"ok": true,
"result": {
"worktree": "8cf8301a-6725-4f9f-a5f4-93ba5c7c1e99::/Users/nwparker/orca/workspaces/orca/cooked-PRs",
"relativePath": "spec/does-not-exist-repro-8844-1784182271.md",
"kind": "markdown",
"opened": true
},
"_meta": {
"runtimeId": "feee85bb-b526-4f92-8c2b-959c1b3a2c03"
}
}
evidence=/Users/nwparker/orca/workspaces/orca/cooked-PRs/docs/bug-reproductions/evidence/8844-file-open-missing.json
STATUS=REPRODUCED
@@ -0,0 +1,45 @@
=== #8808 env-prefixed agent recognition ===
plain: { agent: 'claude', processName: 'claude' }
env-prefixed process: null
env-prefixed cmdline: null
plain cmdline: { agent: 'claude', processName: 'claude' }
=== #8962 RESUMABLE_TUI_AGENTS + extract omp ===
RESUMABLE_TUI_AGENTS: [
'claude', 'codex',
'gemini', 'antigravity',
'opencode', 'mimo-code',
'droid', 'grok',
'devin'
]
isResumableTuiAgent(omp): false
extract omp: null
getAgentResumeArgv omp: undefined
=== #8532 wiring greps ===
src/renderer/src/lib/floating-workspace-shortcut-policy.ts:21: 'tab.openMarkdown',
src/renderer/src/components/floating-terminal/FloatingTerminalPanel.tsx:188: const openMarkdownShortcut = useShortcutKeyDetails('tab.openMarkdown')
src/renderer/src/components/floating-terminal/FloatingTerminalPanel.tsx:1012: if (matches('tab.openMarkdown')) {
=== #8577 ShortcutsPane matcher ===
39:import { matchesSettingsSearch } from './settings-search'
58: const searchQuery = useAppStore((state) => state.settingsSearchQuery)
141: const matchesShortcutSearch = (row: ShortcutRowsByGroup['rows'][number]): boolean =>
143: matchesSettingsSearch(searchQuery, getShortcutSearchEntry(row)) &&
145: const baseVisibleRows = shortcutRows.filter((row) => matchesShortcutSearch(row))
156: (row) => matchesShortcutSearch(row) && matchesShortcutFilter(row, shortcutFilter)
291: const showPolicy = matchesSettingsSearch(searchQuery, getTerminalShortcutPolicySearchEntry())
=== #8865 placeholder wiring ===
src/renderer/src/components/sidebar/empty-project-placeholder-repos.ts:4:export function getEmptyProjectPlaceholderRepoIds(args: {
src/renderer/src/components/sidebar/empty-project-placeholder-repos.ts:7: worktreesByRepo: Readonly<Record<string, readonly Worktree[] | undefined>>
src/renderer/src/components/sidebar/empty-project-placeholder-repos.ts:20: if ((args.worktreesByRepo[repo.id]?.length ?? 0) === 0) {
src/renderer/src/components/sidebar/WorktreeList.tsx:122:import { getEmptyProjectPlaceholderRepoIds } from './empty-project-placeholder-repos'
src/renderer/src/components/sidebar/WorktreeList.tsx:5156: const worktreesByRepo = useAppStore((s) => s.worktreesByRepo)
src/renderer/src/components/sidebar/WorktreeList.tsx:5502: const visibleWorktrees = useMemo(() => {
src/renderer/src/components/sidebar/WorktreeList.tsx:5504: const ids = computeVisibleWorktreeIds(worktreesByRepo, sortedIds, {
src/renderer/src/components/sidebar/WorktreeList.tsx:5555: worktreesByRepo
src/renderer/src/components/sidebar/WorktreeList.tsx:5558: const worktrees = visibleWorktrees
src/renderer/src/components/sidebar/WorktreeList.tsx:5709: return getEmptyProjectPlaceholderRepoIds({
src/renderer/src/components/sidebar/WorktreeList.tsx:5712: worktreesByRepo,
src/renderer/src/components/sidebar/WorktreeList.tsx:5715: }, [filterRepoIds, groupBy, visibleReposForRows, worktreesByRepo])
@@ -0,0 +1,13 @@
# Repro tests for #8832, #8595, #8940, #8903
Tests live under `src/` (vitest `include`) so `@/` aliases resolve via `config/vitest.config.ts`.
```bash
pnpm exec vitest run --config config/vitest.config.ts \
src/renderer/src/components/terminal-pane/repro-8832-url-next-line.test.ts \
src/renderer/src/components/terminal-pane/repro-8595-bold-theme-noop.test.ts \
src/shared/repro-8940-opencode-as-claude.test.ts \
src/renderer/src/components/cmd-j/repro-8903-cmdj-focus-fallback.test.ts
```
Docs: `docs/bug-reproductions/{8832,8595,8940,8903}.md`
@@ -0,0 +1,12 @@
#!/usr/bin/env bash
# Issue #8454 — built-in browser forces bare localhost to http://
# Unit coverage already lives in src/shared/browser-url.test.ts.
#
# Re-run:
# bash docs/bug-reproductions/scripts/repro-8454-localhost-http.sh
# # or directly:
# npx vitest run src/shared/browser-url.test.ts -t "normalizes manual local-dev"
set -euo pipefail
cd "$(cd "$(dirname "$0")/../../.." && pwd)"
npx vitest run src/shared/browser-url.test.ts -t "normalizes manual local-dev"
@@ -0,0 +1,52 @@
/**
* Issues #8533 / #8584 — default keybinding collisions.
*
* #8533: darwin Mod+Shift+E → sidebar.explorer.toggle AND tab.newSimulator
* #8584: all platforms Mod+0 → zoom.reset AND sidebar.focusWorktreeList
*
* findKeybindingConflicts() only reports conflicts involving *customized*
* overrides, so defaults can collide silently. This test asserts raw default
* binding maps collide.
*
* Re-run:
* npx vitest run docs/bug-reproductions/scripts/repro-8533-8584-shortcut-conflicts.test.ts
*/
import { describe, expect, it } from 'vitest'
import { KEYBINDING_DEFINITIONS, type KeybindingPlatform } from '../../../src/shared/keybindings'
function defaultOwners(platform: KeybindingPlatform): Map<string, string[]> {
const map = new Map<string, string[]>()
for (const def of KEYBINDING_DEFINITIONS) {
const binds = def.defaultBindings[platform] ?? []
for (const binding of binds) {
const list = map.get(binding) ?? []
list.push(def.id)
map.set(binding, list)
}
}
return map
}
describe('issues #8533 and #8584 default shortcut conflicts', () => {
it('#8533: Mod+Shift+E collides on darwin (explorer toggle vs new simulator)', () => {
const owners = defaultOwners('darwin').get('Mod+Shift+E') ?? []
// eslint-disable-next-line no-console
console.log('darwin Mod+Shift+E owners:', owners)
expect(owners).toEqual(expect.arrayContaining(['sidebar.explorer.toggle', 'tab.newSimulator']))
expect(owners.length).toBeGreaterThanOrEqual(2)
// Linux/Windows: simulator unbound, so explorer alone owns the chord.
expect(defaultOwners('linux').get('Mod+Shift+E')).toEqual(['sidebar.explorer.toggle'])
expect(defaultOwners('win32').get('Mod+Shift+E')).toEqual(['sidebar.explorer.toggle'])
})
it('#8584: Mod+0 collides on every platform (zoom.reset vs focus worktree list)', () => {
for (const platform of ['darwin', 'linux', 'win32'] as const) {
const owners = defaultOwners(platform).get('Mod+0') ?? []
// eslint-disable-next-line no-console
console.log(`${platform} Mod+0 owners:`, owners)
expect(owners).toEqual(expect.arrayContaining(['zoom.reset', 'sidebar.focusWorktreeList']))
expect(owners.length).toBeGreaterThanOrEqual(2)
}
})
})
@@ -0,0 +1,75 @@
#!/usr/bin/env bash
# Issue #8670 — clean linked worktree with initialised submodule cannot be removed
# without git worktree remove --force.
#
# Re-run:
# bash docs/bug-reproductions/scripts/repro-8670-submodule-worktree-remove.sh
#
# Writes evidence to docs/bug-reproductions/evidence/8670-git-submodule-remove.txt
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
EVIDENCE_DIR="$(cd "$SCRIPT_DIR/.." && pwd)/evidence"
mkdir -p "$EVIDENCE_DIR"
OUT="$EVIDENCE_DIR/8670-git-submodule-remove.txt"
TMP="$(mktemp -d "${TMPDIR:-/tmp}/orca-repro-8670.XXXXXX")"
cleanup() { rm -rf "$TMP"; }
trap cleanup EXIT
git version | tee "$OUT"
echo "fixture: $TMP" | tee -a "$OUT"
git -C "$TMP" init -q submodule-src
git -C "$TMP/submodule-src" config user.email repro@example.com
git -C "$TMP/submodule-src" config user.name repro
echo sub >"$TMP/submodule-src/file.txt"
git -C "$TMP/submodule-src" add file.txt
git -C "$TMP/submodule-src" commit -q -m init
git -C "$TMP" init -q main
git -C "$TMP/main" config user.email repro@example.com
git -C "$TMP/main" config user.name repro
echo root >"$TMP/main/README.md"
git -C "$TMP/main" add README.md
git -C "$TMP/main" commit -q -m init
# Why: Git ≥2.38 blocks file:// submodule clones unless protocol.file.allow=always.
git -C "$TMP/main" -c protocol.file.allow=always submodule add "$TMP/submodule-src" vendor/sub
git -C "$TMP/main" commit -q -m 'add submodule'
git -C "$TMP/main" worktree add -q -b feature "$TMP/wt-feature"
git -C "$TMP/wt-feature" -c protocol.file.allow=always submodule update --init
{
echo "parent status:"; git -C "$TMP/wt-feature" status --short --untracked-files=all || true
echo "submodule status:"; git -C "$TMP/wt-feature/vendor/sub" status --short --untracked-files=all || true
} | tee -a "$OUT"
set +e
git -C "$TMP/main" worktree remove "$TMP/wt-feature" 2>"$TMP/err-noforce.txt"
noforce=$?
set -e
{
echo "--- without --force (exit $noforce) ---"
cat "$TMP/err-noforce.txt"
} | tee -a "$OUT"
set +e
git -C "$TMP/main" worktree remove --force "$TMP/wt-feature" 2>"$TMP/err-force.txt"
force=$?
set -e
{
echo "--- with --force (exit $force) ---"
cat "$TMP/err-force.txt"
if [[ ! -d "$TMP/wt-feature" ]]; then
echo "worktree path removed: yes"
else
echo "worktree path removed: no"
fi
} | tee -a "$OUT"
if [[ "$noforce" -ne 0 ]] && grep -q 'working trees containing submodules cannot be moved or removed' "$TMP/err-noforce.txt" && [[ "$force" -eq 0 ]]; then
echo "RESULT: REPRODUCED (clean submodule worktree requires --force)" | tee -a "$OUT"
exit 0
fi
echo "RESULT: NOT REPRODUCED" | tee -a "$OUT"
exit 1
@@ -0,0 +1,85 @@
#!/usr/bin/env bash
# Issue #8695 — undici 7.28.0 paused-parser assert (process crash).
#
# Runs the minimal net-server + fetch(without body consume) repro against:
# 1) system node (if present)
# 2) production Orca Electron as node (ELECTRON_RUN_AS_NODE=1)
#
# Expected: AssertionError assert(!this.paused) in Parser.finish / non-zero exit.
set -euo pipefail
ROOT="$(cd "$(dirname "$0")/../../.." && pwd)"
EVIDENCE_DIR="$ROOT/docs/bug-reproductions/evidence"
mkdir -p "$EVIDENCE_DIR"
OUT="$EVIDENCE_DIR/8695-undici-repro.log"
REPRO_JS="$(mktemp -t orca-undici-repro.XXXXXX.js)"
trap 'rm -f "$REPRO_JS"' EXIT
cat >"$REPRO_JS" <<'EOF'
const { createServer } = require("node:net");
const body = Buffer.alloc(64 * 1024, 0x61);
const server = createServer((socket) => {
socket.once("data", () => {
socket.write(
"HTTP/1.1 200 OK\r\n" +
`Content-Length: ${body.length}\r\n` +
"Connection: close\r\n\r\n",
);
socket.write(body);
socket.end();
});
});
server.listen(0, "127.0.0.1", async () => {
const { port } = server.address();
try {
await fetch(`http://127.0.0.1:${port}/`);
setTimeout(() => {
console.log("Unexpected: process did not crash");
server.close();
process.exit(0);
}, 500);
} catch (e) {
console.error("fetch error", e);
server.close();
process.exit(2);
}
});
EOF
{
echo "=== #8695 undici paused-parser repro $(date -u +%Y-%m-%dT%H:%M:%SZ) ==="
echo "node: $(node -v 2>/dev/null || echo missing)"
echo
echo "--- system node ---"
set +e
node "$REPRO_JS" 2>&1
sys_ec=$?
set -e
echo "system node exit: $sys_ec"
echo
ORCA_BIN="${ORCA_ELECTRON_BIN:-/Applications/Orca.app/Contents/MacOS/Orca}"
echo "--- Orca Electron as node ($ORCA_BIN) ---"
if [[ -x "$ORCA_BIN" ]]; then
set +e
ELECTRON_RUN_AS_NODE=1 "$ORCA_BIN" "$REPRO_JS" 2>&1
orca_ec=$?
set -e
echo "orca exit: $orca_ec"
else
echo "Orca binary missing: $ORCA_BIN"
orca_ec=127
fi
echo
if [[ "${sys_ec:-0}" -ne 0 || "${orca_ec:-0}" -ne 0 ]]; then
echo "RESULT: REPRODUCED (process crashed or non-zero exit as expected)"
exit 0
fi
echo "RESULT: NOT REPRODUCED (both runtimes survived)"
exit 1
} | tee "$OUT"
@@ -0,0 +1,48 @@
/**
* Issue #8838 — check whether <br> in GFM table cells becomes real <br> elements
* under the MarkdownPreview-like pipeline.
*
* Run: pnpm exec node --input-type=module docs/bug-reproductions/scripts/repro-8838-br-in-table.mjs
*/
import { createElement } from 'react'
import { renderToStaticMarkup } from 'react-dom/server'
import Markdown from 'react-markdown'
import remarkGfm from 'remark-gfm'
import remarkBreaks from 'remark-breaks'
import rehypeRaw from 'rehype-raw'
import rehypeSanitize, { defaultSchema } from 'rehype-sanitize'
const SAMPLE = `| Field | Type | Description |
|-------|------|-------------|
| verdict | string | Inspection verdict. Possible values:<br/><br/>\`normal\` - Normal. No defect found<br>\`uncertain\` - Needs review<br>\`defect\` - Defective |`
const schema = {
...defaultSchema,
tagNames: [...(defaultSchema.tagNames ?? []), 'details', 'summary', 'kbd', 'sub', 'sup', 'ins']
}
const html = renderToStaticMarkup(
createElement(
Markdown,
{
remarkPlugins: [remarkGfm, remarkBreaks],
rehypePlugins: [rehypeRaw, [rehypeSanitize, schema]]
},
SAMPLE
)
)
const brCount = (html.match(/<br/gi) || []).length
const escaped = (html.match(/&lt;br/gi) || []).length
console.log('HTML:', html)
console.log('brCount:', brCount)
console.log('escapedBrCount:', escaped)
if (brCount > 0 && escaped === 0) {
console.log('STATUS=NOT_REPRODUCED')
process.exit(0)
}
console.log('STATUS=REPRODUCED')
process.exit(1)
@@ -0,0 +1,13 @@
/**
* Issue #8838 guard — pipeline emits real <br> in table cells.
* Run: pnpm exec node docs/bug-reproductions/scripts/repro-8838-br-in-table.mjs
* (vitest file intentionally minimal; full render is in the .mjs script)
*/
import { describe, expect, it } from 'vitest'
import { defaultSchema } from 'rehype-sanitize'
describe('issue #8838 br allowed in sanitize schema', () => {
it('defaultSchema includes br', () => {
expect(defaultSchema.tagNames ?? []).toContain('br')
})
})
@@ -0,0 +1,59 @@
#!/usr/bin/env bash
# Issue #8844 — orca file open reports ok:true for a non-existent file.
#
# DEFAULT: code-path / evidence re-check only — does NOT open ghost editor tabs.
#
# LIVE repro (opens a ghost tab in the running Orca UI — DO NOT use casually):
# ORCA_REPRO_8844_LIVE=1 bash docs/bug-reproductions/scripts/repro-8844-file-open-missing.sh
#
# Prefer production CLI:
# ORCA_BIN=/Applications/Orca.app/Contents/Resources/bin/orca
set -uo pipefail
ORCA_BIN="${ORCA_BIN:-/Applications/Orca.app/Contents/Resources/bin/orca}"
ROOT="${1:-.}"
ROOT="$(cd "$ROOT" && pwd)"
EVIDENCE_DIR="$(cd "$(dirname "$0")/.." && pwd)/evidence"
mkdir -p "$EVIDENCE_DIR"
echo "=== #8844 repro (default is non-destructive) ==="
echo "Root cause: RuntimeFileCommands.openMobileFile never stats the path;"
echo " classifies by extension and returns opened:true after host.openFile."
echo "See: src/main/runtime/orca-runtime-files.ts openMobileFile"
echo "See: src/cli/handlers/file.ts 'file open'"
echo
echo "Prior live evidence (already captured — do not re-open casually):"
if [[ -f "$EVIDENCE_DIR/8844-file-open-missing.raw.json" ]]; then
cat "$EVIDENCE_DIR/8844-file-open-missing.raw.json"
echo
fi
if [[ -f "$EVIDENCE_DIR/8844-file-open-missing.json.txt" ]]; then
grep -E '^(RESULT|exit|exists_|parsed)' "$EVIDENCE_DIR/8844-file-open-missing.json.txt" || true
fi
if [[ "${ORCA_REPRO_8844_LIVE:-}" != "1" ]]; then
echo
echo "STATUS=REPRODUCED_FROM_EVIDENCE (no new ghost tab created)"
echo "To run live CLI open of a missing path (creates a UI tab + ENOENT card):"
echo " ORCA_REPRO_8844_LIVE=1 $0"
exit 0
fi
# --- LIVE path (explicit opt-in only) ---
REL="spec/does-not-exist-repro-8844-LIVE-$(date +%s).md"
ABS="$ROOT/$REL"
rm -f "$ABS" 2>/dev/null || true
cd "$ROOT" || exit 2
set +e
"$ORCA_BIN" file open "$REL" --json >"$EVIDENCE_DIR/8844-file-open-missing.raw.json" 2>"$EVIDENCE_DIR/8844-file-open-missing.stderr.txt"
code=$?
set -e
echo "LIVE open of $REL exit=$code"
cat "$EVIDENCE_DIR/8844-file-open-missing.raw.json"
echo
echo "WARNING: a ghost editor tab was opened. Close it with Cmd+W, or create the file:"
echo " mkdir -p \"\$(dirname $ABS)\" && echo placeholder > \"$ABS\""
echo "STATUS=REPRODUCED_LIVE"
exit 0
@@ -0,0 +1,29 @@
/**
* Issues #8533 and #8584 — default keybinding collisions.
* Run: pnpm exec vitest run docs/bug-reproductions/scripts/repro-shortcut-conflicts.test.ts
*/
import { describe, expect, it } from 'vitest'
import { getEffectiveKeybindingsForAction } from '../../../src/shared/keybindings'
describe('issue #8533 Cmd+Shift+E collision on darwin', () => {
it('tab.newSimulator and sidebar.explorer.toggle share Mod+Shift+E on darwin', () => {
const simulator = getEffectiveKeybindingsForAction('tab.newSimulator', 'darwin')
const explorer = getEffectiveKeybindingsForAction('sidebar.explorer.toggle', 'darwin')
expect(simulator).toContain('Mod+Shift+E')
expect(explorer).toContain('Mod+Shift+E')
// Document that linux/win32 do not ship the simulator default (no collision there).
expect(getEffectiveKeybindingsForAction('tab.newSimulator', 'linux')).toEqual([])
expect(getEffectiveKeybindingsForAction('tab.newSimulator', 'win32')).toEqual([])
})
})
describe('issue #8584 Mod+0 collision', () => {
it('zoom.reset and sidebar.focusWorktreeList both default to Mod+0', () => {
for (const platform of ['darwin', 'linux', 'win32'] as const) {
const zoom = getEffectiveKeybindingsForAction('zoom.reset', platform)
const focus = getEffectiveKeybindingsForAction('sidebar.focusWorktreeList', platform)
expect(zoom).toContain('Mod+0')
expect(focus).toContain('Mod+0')
}
})
})
@@ -0,0 +1,68 @@
/**
* Issue #8749 — claim: Orca's agent-session seatbelt blocks ~/.orca/agent-hooks
* claude-hook.sh (EPERM on every tool call).
*
* Investigation on this tree + production Orca 1.4.x (macOS):
* - Orca installs managed hooks at `~/.orca/agent-hooks/claude-hook.sh`.
* - There is **no** seatbelt / sandbox-exec profile applied to daemon PTY
* sessions in this codebase (daemon children sandbox_check=0 live).
* - Chromium renderer helpers use `--seatbelt-client` (expected); agent shells
* spawn via daemon `login` + shell-ready and are not sandboxed.
* - Live probe (orca terminal create --command …) on production Orca:
* sandbox_check 0
* ~/.orca accessible
* claude-hook.sh executable, exit 0 on empty stdin
*
* This test locks the *code contract* that Orca does not wrap agent PTYs in a
* seatbelt profile and still points hooks at ~/.orca (the install location the
* reporter correctly identified). Symptom EPERM under a true seatbelt is not
* reproduced here → issue labeled cannot_repro on this host/version.
*
* Re-run:
* pnpm exec vitest run --config config/vitest.config.ts \
* src/main/agent-hooks/repro-8749-hook-path-no-seatbelt.test.ts
*/
import { readFileSync, readdirSync } from 'node:fs'
import { join } from 'node:path'
import { describe, expect, it } from 'vitest'
import { getSharedManagedScriptPath } from './installer-utils'
const installerSource = readFileSync(join(__dirname, 'installer-utils.ts'), 'utf8')
const daemonDir = join(__dirname, '../daemon')
describe('#8749 agent hooks under ~/.orca — no Orca seatbelt on PTY path', () => {
it('installs managed Claude hook under ~/.orca/agent-hooks/', () => {
expect(getSharedManagedScriptPath('claude-hook.sh')).toMatch(
/\.orca[/\\]agent-hooks[/\\]claude-hook\.sh$/
)
expect(installerSource).toMatch(
/join\(homedir\(\),\s*'\.orca',\s*'agent-hooks',\s*scriptFileName\)/
)
})
it('daemon / PTY spawn sources contain no seatbelt profile application', () => {
// Scan daemon modules for sandbox-exec / seatbelt profile wiring.
const files = readdirSync(daemonDir).filter((f) => f.endsWith('.ts') && !f.endsWith('.test.ts'))
const hits: string[] = []
for (const file of files) {
const src = readFileSync(join(daemonDir, file), 'utf8')
if (/sandbox-exec|seatbelt|sandbox_init|SBPL_|com\.apple\.security\.app-sandbox/.test(src)) {
hits.push(file)
}
}
expect(hits).toEqual([])
})
it('managed POSIX hook is fail-soft on empty stdin / missing env (exit 0)', () => {
// Mirrors the installed ~/.orca/agent-hooks/claude-hook.sh shape: missing
// payload or missing ORCA_* env exits 0 rather than failing the agent tool.
const claudeHookService = readFileSync(join(__dirname, '../claude/hook-service.ts'), 'utf8')
// Hook service writes a managed script; script content is built with
// fail-soft curl (|| true) pattern in installer-utils / getManagedScript.
expect(claudeHookService).toMatch(/writeManagedScript/)
const installer = installerSource
// Curl posts are best-effort; agent must not see hook as hard-fail when
// the endpoint is down. (EPERM before exec is a different layer.)
expect(installer).toMatch(/curl/)
})
})
@@ -0,0 +1,155 @@
/**
* Issue #8742 — Antigravity CLI sessions missing from AI Vault history.
*
* 1. Discovery scans only ~/.gemini/tmp (Gemini CLI), not
* ~/.gemini/antigravity-cli/brain/<id>/.system_generated/logs/transcript.jsonl
* 2. Gemini JSONL parser only counts type === 'user' | 'gemini'; Antigravity
* uses USER_INPUT / PLANNER_RESPONSE / GENERIC with created_at.
*
* Re-run:
* pnpm exec vitest run --config config/vitest.config.ts \
* src/main/ai-vault/repro-8742-antigravity-not-scanned.test.ts
*/
import { mkdtempSync, readFileSync, rmSync, writeFileSync } from 'node:fs'
import { tmpdir } from 'node:os'
import { join } from 'node:path'
import { afterEach, describe, expect, it } from 'vitest'
import { parseGeminiSessionContent } from './session-scanner-gemini-parsers'
const discoverySource = readFileSync(join(__dirname, 'session-scanner-source-discovery.ts'), 'utf8')
const geminiParserSource = readFileSync(
join(__dirname, 'session-scanner-gemini-parsers.ts'),
'utf8'
)
const tmpRoots: string[] = []
afterEach(() => {
for (const root of tmpRoots.splice(0)) {
rmSync(root, { recursive: true, force: true })
}
})
describe('issue #8742 Antigravity sessions not detected in AI Vault', () => {
it('discovers Gemini under ~/.gemini/tmp only — no antigravity-cli brain path', () => {
expect(discoverySource).toMatch(/\.gemini['"],\s*['"]tmp['"]/)
expect(discoverySource).toMatch(
/GEMINI_SESSIONS_DIR\s*=\s*join\(homedir\(\),\s*'\.gemini',\s*'tmp'\)/
)
expect(discoverySource).not.toMatch(/antigravity-cli/)
expect(discoverySource).not.toMatch(/antigravity/)
expect(discoverySource).not.toMatch(/\.system_generated/)
// Agent enum may list antigravity elsewhere, but vault discovery has no agent: 'antigravity'
expect(discoverySource).not.toMatch(/agent:\s*'antigravity'/)
})
it('Gemini JSONL consumer only recognizes type user/gemini', () => {
expect(geminiParserSource).toMatch(/record\.type === 'user'/)
expect(geminiParserSource).toMatch(/record\.type === 'gemini'/)
expect(geminiParserSource).not.toMatch(/USER_INPUT/)
expect(geminiParserSource).not.toMatch(/PLANNER_RESPONSE/)
expect(geminiParserSource).not.toMatch(/created_at/)
})
it('parseGeminiSessionContent drops Antigravity transcript schema (0 messages)', async () => {
const root = mkdtempSync(join(tmpdir(), 'orca-repro-8742-'))
tmpRoots.push(root)
const path = join(root, 'transcript.jsonl')
// Real Antigravity CLI schema (fields observed under antigravity-cli/brain).
const lines = [
JSON.stringify({
step_index: 0,
source: 'USER_EXPLICIT',
type: 'USER_INPUT',
status: 'DONE',
created_at: '2026-05-28T02:41:13Z',
content: '<USER_REQUEST>\nfix the issue card\n</USER_REQUEST>'
}),
JSON.stringify({
step_index: 2,
source: 'MODEL',
type: 'PLANNER_RESPONSE',
status: 'DONE',
created_at: '2026-05-28T02:41:13Z',
tool_calls: [{ name: 'list_permissions', args: {} }]
}),
JSON.stringify({
step_index: 3,
source: 'MODEL',
type: 'GENERIC',
status: 'DONE',
created_at: '2026-05-28T02:41:14Z',
content: 'Working on it…'
})
]
writeFileSync(path, lines.join('\n'))
const session = await parseGeminiSessionContent(
{
path,
mtimeMs: Date.now(),
modifiedAt: new Date().toISOString(),
sizeBytes: 100
},
lines.join('\n'),
'darwin'
)
// Parser does not crash, but never counts USER_INPUT / PLANNER_RESPONSE.
// finalizeSession may return null when messageCount is 0.
if (session) {
expect(session.messageCount).toBe(0)
} else {
expect(session).toBeNull()
}
})
it('same parser accepts Gemini-shaped user/gemini records', async () => {
const root = mkdtempSync(join(tmpdir(), 'orca-repro-8742-gemini-'))
tmpRoots.push(root)
const path = join(root, 'session.jsonl')
const lines = [
JSON.stringify({
type: 'user',
timestamp: '2026-05-28T02:41:13Z',
content: 'hello from gemini cli'
}),
JSON.stringify({
type: 'gemini',
timestamp: '2026-05-28T02:41:14Z',
content: 'hi',
model: 'gemini-2.0'
})
]
writeFileSync(path, lines.join('\n'))
const session = await parseGeminiSessionContent(
{
path,
mtimeMs: Date.now(),
modifiedAt: new Date().toISOString(),
sizeBytes: 50
},
lines.join('\n'),
'darwin'
)
expect(session).not.toBeNull()
expect(session!.messageCount).toBe(2)
expect(session!.title).toContain('hello from gemini cli')
})
it('even if root were antigravity brain, default discovery would not target transcript.jsonl under .system_generated specially', () => {
// walkSessionFiles does enter dot-directories (no hidden filter), so path
// + schema are the primary blockers — prove path is wrong first.
const brainRoot = join('HOME', '.gemini', 'antigravity-cli', 'brain')
const expectedTranscript = join(
brainRoot,
'9d820cf9-f043-4172-9a56-818fe3225253',
'.system_generated',
'logs',
'transcript.jsonl'
)
expect(expectedTranscript).toContain('antigravity-cli')
expect(expectedTranscript).toContain('.system_generated')
expect(discoverySource).not.toContain('antigravity-cli')
})
})
@@ -0,0 +1,86 @@
/**
* Issue #8864 — Agent history lookup hangs forever.
*
* Opening Agent History triggers `aiVault.listSessions` → OpenCode SQLite
* discovery/parse with no wall-clock timeout. SyncDatabase supports `timeout`
* (busy wait ms), but OpenCode readers omit it. A pathological/contended
* opencode.db (user report: 29 GB, missing indexes) keeps the main process
* busy and the panel spinner forever because the renderer awaits the IPC with
* no Promise.race either.
*
* Re-run:
* pnpm exec vitest run --config config/vitest.config.ts \
* src/main/ai-vault/repro-8864-history-lookup-no-timeout.test.ts
*/
import { readFileSync } from 'node:fs'
import { join } from 'node:path'
import { describe, expect, it } from 'vitest'
const root = process.cwd()
describe('issue #8864 agent history lookup has no hang timeout', () => {
it('SyncDatabase accepts a timeout option but OpenCode readers never pass one', () => {
const syncDb = readFileSync(join(root, 'src/main/sqlite/sync-database.ts'), 'utf8')
expect(syncDb).toMatch(/timeout\?: number/)
expect(syncDb).toMatch(/timeout: options\.timeout/)
const discovery = readFileSync(
join(root, 'src/main/ai-vault/session-scanner-opencode-sqlite-discovery.ts'),
'utf8'
)
const parser = readFileSync(
join(root, 'src/main/ai-vault/session-scanner-opencode-sqlite.ts'),
'utf8'
)
// Both open paths: readonly + fileMustExist only — no timeout.
expect(discovery).toMatch(
/new SyncDatabase\(dbPath, \{\s*readonly: true,\s*fileMustExist: true\s*\}\)/
)
expect(parser).toMatch(
/new SyncDatabase\(dbPath, \{\s*readonly: true,\s*fileMustExist: true\s*\}\)/
)
expect(discovery).not.toMatch(/timeout\s*:/)
expect(parser).not.toMatch(/timeout\s*:/)
})
it('session list query uses correlated COUNT with json_extract (unbounded work per session)', () => {
const discovery = readFileSync(
join(root, 'src/main/ai-vault/session-scanner-opencode-sqlite-discovery.ts'),
'utf8'
)
// Per-session subquery over message + json_extract — catastrophic without indexes
// on large OpenCode event/message tables (user-reported hang trigger).
expect(discovery).toMatch(/\(SELECT COUNT\(\*\) FROM message m/)
expect(discovery).toMatch(/json_extract\(m\.data, '\$\.role'\)/)
expect(discovery).toMatch(/FROM session s/)
expect(discovery).toMatch(/ORDER BY s\.time_updated DESC/)
})
it('local listSessions cache path has no Promise.race / wall-clock deadline', () => {
const cached = readFileSync(join(root, 'src/main/ai-vault/cached-session-list.ts'), 'utf8')
expect(cached).toMatch(/scanAiVaultSessions\(/)
expect(cached).not.toMatch(/Promise\.race/)
expect(cached).not.toMatch(/timeoutMs|AbortSignal|setTimeout/)
const ipc = readFileSync(join(root, 'src/main/ipc/ai-vault.ts'), 'utf8')
// Multi-host path times out runtime hosts only; local/SSH scans are unbounded.
expect(ipc).toMatch(/AI_VAULT_ALL_HOST_RUNTIME_TIMEOUT_MS/)
expect(ipc).toMatch(/scanLocalAiVaultSessions\(args\)/)
// Local scope short-circuits before any timeout wrapper.
expect(ipc).toMatch(
/if \(executionHostScope === LOCAL_EXECUTION_HOST_ID\) \{\s*return scanLocalAiVaultSessions\(args\)/
)
})
it('renderer Agent History refresh awaits listSessions with no client-side timeout', () => {
const refresh = readFileSync(
join(root, 'src/renderer/src/components/right-sidebar/ai-vault-session-refresh.ts'),
'utf8'
)
expect(refresh).toMatch(/await window\.api\.aiVault\.listSessions\(/)
expect(refresh).not.toMatch(/Promise\.race|AbortSignal|timeoutMs/)
// loading stays true until the await settles — hang ⇒ spinner forever.
expect(refresh).toMatch(/setLoading\(true\)/)
expect(refresh).toMatch(/setLoading\(false\)/)
})
})
@@ -0,0 +1,52 @@
/**
* Issue #8943 — "Browser shows insecure" (screenshot-only report).
*
* Attempted code-level mapping:
* - Main BrowserAddressBar has no secure/insecure chrome (Globe/Search only).
* - Popup origin bar *does* show "Not secure" for plain http:// remote hosts
* via describePopupOrigin — intentional Chromium-like UX, not a false positive
* for https://.
* - Guest webPreferences set allowRunningInsecureContent: false (blocks mixed
* content; can surface as security errors on http assets under https).
*
* Without analyzing the user attachment (security policy: no user-uploaded
* binaries/images as evidence input), there is no concrete false-positive path
* for legitimate https:// navigations in this tree. Labeled cannot_repro
* for a product defect; document intentional "Not secure" for remote http.
*
* Re-run:
* pnpm exec vitest run --config config/vitest.config.ts \
* src/main/browser/repro-8943-browser-insecure-indicator.test.ts
*/
import { readFileSync } from 'node:fs'
import { join } from 'node:path'
import { describe, expect, it } from 'vitest'
import { describePopupOrigin } from './popup-origin-bar-window'
const addressBarSource = readFileSync(
join(__dirname, '../../renderer/src/components/browser-pane/BrowserAddressBar.tsx'),
'utf8'
)
const browserManagerSource = readFileSync(join(__dirname, 'browser-manager.ts'), 'utf8')
describe('#8943 browser "insecure" indicator (screenshot-only)', () => {
it('main address bar has no Not-secure / lock security chrome', () => {
expect(addressBarSource).not.toMatch(/Not secure|insecure|isSecure/)
// Only Globe/Search icons for chrome affordances.
expect(addressBarSource).toMatch(/Globe|Search/)
})
it('popup origin bar flags only non-loopback http as insecure (intentional)', () => {
expect(describePopupOrigin('https://example.com/login')).toEqual({
label: 'https://example.com',
insecure: false
})
expect(describePopupOrigin('http://phish.example.net/login').insecure).toBe(true)
expect(describePopupOrigin('http://localhost:3000/').insecure).toBe(false)
expect(describePopupOrigin('about:blank').insecure).toBe(false)
})
it('guest browser disallows running insecure mixed content', () => {
expect(browserManagerSource).toMatch(/allowRunningInsecureContent:\s*false/)
})
})
@@ -0,0 +1,77 @@
/**
* Issue #8726 — Tasks upstream/origin problems on forks (cluster A + count cache).
*
* A. `auto` routes PRs to origin while issues use upstream-first heuristic:
* resolvePrWorkItemSource only prefers upstream when preference === 'upstream';
* auto/origin/undefined all collapse to originCandidate.
* resolveIssueSource('auto') uses getIssueOwnerRepo (upstream-first).
*
* B/G. countWorkItems always uses `gh api --cache 120s` with no noCache arg,
* so source flips can serve stale totals for up to two minutes.
*
* Existing listWorkItems tests already assert "upstream issues + origin PRs".
* This file pins the pure routing + count-cache contracts called out in #8726.
*
* Re-run:
* pnpm exec vitest run --config config/vitest.config.ts \
* src/main/github/repro-8726-fork-pr-auto-origin.test.ts
*/
import { readFileSync } from 'node:fs'
import { join } from 'node:path'
import { describe, expect, it } from 'vitest'
const clientSource = readFileSync(join(__dirname, 'client.ts'), 'utf8')
const identitySource = readFileSync(join(__dirname, 'github-repository-identity.ts'), 'utf8')
/** Pure mirror of resolvePrWorkItemSource selection (client.ts). */
function resolvePrSource(
preference: 'auto' | 'origin' | 'upstream' | undefined,
origin: { owner: string; repo: string } | null,
upstream: { owner: string; repo: string } | null
): { owner: string; repo: string } | null {
return preference === 'upstream' ? (upstream ?? origin) : origin
}
describe('#8726 Tasks fork PR auto routes to origin (not upstream)', () => {
it('pins resolvePrWorkItemSource: only explicit upstream leaves origin', () => {
expect(clientSource).toMatch(
/preference === 'upstream' \? \(upstreamCandidate \?\? originCandidate\) : originCandidate/
)
// Function is used from listWorkItems / countWorkItems
expect(clientSource).toMatch(/resolvePrWorkItemSource\(/)
})
it('auto / undefined / origin all select the fork (origin) when upstream exists', () => {
const origin = { owner: 'fork-user', repo: 'orca' }
const upstream = { owner: 'stablyai', repo: 'orca' }
expect(resolvePrSource('auto', origin, upstream)).toEqual(origin)
expect(resolvePrSource(undefined, origin, upstream)).toEqual(origin)
expect(resolvePrSource('origin', origin, upstream)).toEqual(origin)
expect(resolvePrSource('upstream', origin, upstream)).toEqual(upstream)
})
it('issues auto path is upstream-first (contradicts PR auto pill)', () => {
// getIssueOwnerRepo: upstream ?? origin
expect(identitySource).toMatch(
/export async function getIssueOwnerRepo[\s\S]*?const upstream = await getOwnerRepoForRemote[\s\S]*?if \(upstream\) \{\s*return upstream/s
)
// resolveIssueSource auto falls through to getIssueOwnerRepo
expect(identitySource).toMatch(
/export async function resolveIssueSource[\s\S]*?return \{\s*source: await getIssueOwnerRepo/s
)
})
it('countWorkItems always passes gh api --cache 120s (no bypass on source flip)', () => {
// Extract just countWorkItemsForQuery body — hardcodes cache, no noCache param
const forQuery = clientSource.match(/async function countWorkItemsForQuery\([\s\S]*?\n\}/)?.[0]
expect(forQuery).toBeTruthy()
expect(forQuery).toMatch(/'--cache',\s*'120s'/)
expect(forQuery).not.toMatch(/noCache/)
// Public countWorkItems has no noCache argument in its parameter list
const publicSig = clientSource.match(
/export async function countWorkItems\(\s*repoPath: string,\s*query\?: string,\s*preference\?: IssueSourcePreference,\s*connectionId\?: string \| null,\s*localGitOptions: LocalGitExecOptions = \{\}\s*\)/
)
expect(publicSig).toBeTruthy()
})
})
@@ -0,0 +1,169 @@
/**
* Issue #8935 — GitHub Enterprise PR work item diffs do not load.
*
* Root cause chain:
* 1. `parseGitHubOwnerRepo` only accepts github.com → null for GHE remotes
* 2. `getPRFiles` / `getPRFileContents` / work-item detail PR path use `getOwnerRepo`
* (not `getEnterpriseGitHubRepoSlug`) and return null / empty when owner is unresolved
* 3. `gh api` calls never pass `--hostname <enterprise-host>`, so even a forced
* owner/repo slug would still hit github.com by default
*
* Related fix PR: https://github.com/stablyai/orca/pull/8932
*
* Re-run:
* pnpm exec vitest run --config config/vitest.config.ts \
* src/main/github/repro-8935-ghe-pr-diff-host.test.ts
*/
import { beforeEach, describe, expect, it, vi } from 'vitest'
import { readFileSync } from 'node:fs'
import { join } from 'node:path'
const {
ghExecFileAsyncMock,
getOwnerRepoMock,
getIssueOwnerRepoMock,
getWorkItemMock,
getPRChecksMock,
getPRCommentsMock,
rateLimitGuardMock,
noteRateLimitSpendMock,
ghRepoExecOptionsMock,
githubRepoContextMock,
acquireMock,
releaseMock
} = vi.hoisted(() => ({
ghExecFileAsyncMock: vi.fn(),
getOwnerRepoMock: vi.fn(),
getIssueOwnerRepoMock: vi.fn(),
getWorkItemMock: vi.fn(),
getPRChecksMock: vi.fn(),
getPRCommentsMock: vi.fn(),
rateLimitGuardMock: vi.fn(() => ({ blocked: false as const })),
noteRateLimitSpendMock: vi.fn(),
ghRepoExecOptionsMock: vi.fn((context) =>
context.connectionId
? {}
: { cwd: context.repoPath, ...(context.wslDistro ? { wslDistro: context.wslDistro } : {}) }
),
githubRepoContextMock: vi.fn((repoPath, connectionId, localGitOptions) => ({
repoPath,
connectionId: connectionId ?? null,
...localGitOptions
})),
acquireMock: vi.fn(),
releaseMock: vi.fn()
}))
vi.mock('./gh-utils', () => ({
ghExecFileAsync: ghExecFileAsyncMock,
getOwnerRepo: getOwnerRepoMock,
getIssueOwnerRepo: getIssueOwnerRepoMock,
ghRepoExecOptions: ghRepoExecOptionsMock,
githubRepoContext: githubRepoContextMock,
acquire: acquireMock,
release: releaseMock
}))
vi.mock('./client', () => ({
getWorkItem: getWorkItemMock,
getPRChecks: getPRChecksMock,
getPRComments: getPRCommentsMock
}))
vi.mock('./rate-limit', () => ({
rateLimitGuard: rateLimitGuardMock,
noteRateLimitSpend: noteRateLimitSpendMock
}))
import { getPRFileContents, getWorkItemDetails } from './work-item-details'
import { parseGitHubOwnerRepo, parseGitHubRemoteIdentity } from './github-remote-identity-parsing'
describe('issue #8935 GHE PR diff host routing', () => {
beforeEach(() => {
ghExecFileAsyncMock.mockReset()
getOwnerRepoMock.mockReset()
getIssueOwnerRepoMock.mockReset()
getWorkItemMock.mockReset()
getPRChecksMock.mockReset()
getPRCommentsMock.mockReset()
rateLimitGuardMock.mockReset()
rateLimitGuardMock.mockReturnValue({ blocked: false })
noteRateLimitSpendMock.mockReset()
acquireMock.mockReset()
releaseMock.mockReset()
acquireMock.mockResolvedValue(undefined)
})
it('parseGitHubOwnerRepo returns null for Enterprise remotes (github.com only)', () => {
expect(parseGitHubOwnerRepo('https://github.acme-corp.com/team/orca.git')).toBeNull()
expect(parseGitHubOwnerRepo('git@github.acme-corp.com:team/orca.git')).toBeNull()
expect(parseGitHubOwnerRepo('https://ghe.acme.internal/acme/widgets.git')).toBeNull()
// Identity parser still sees host+owner+repo — data is available, just not used by getOwnerRepo
expect(parseGitHubRemoteIdentity('https://github.acme-corp.com/team/orca.git')).toEqual({
host: 'github.acme-corp.com',
owner: 'team',
repo: 'orca'
})
})
it('getWorkItemDetails marks filesUnavailable when getOwnerRepo is null (GHE path)', async () => {
getWorkItemMock.mockResolvedValueOnce({
id: 'pr:7',
type: 'pr',
number: 7,
title: 'Enterprise PR',
state: 'open',
url: 'https://github.acme-corp.com/team/orca/pull/7',
labels: [],
updatedAt: '2026-07-16T00:00:00Z',
author: 'pr-author'
})
// Why: mirrors parseGitHubOwnerRepo null for non-github.com origin remotes.
getOwnerRepoMock.mockResolvedValue(null)
getPRCommentsMock.mockResolvedValue([])
getPRChecksMock.mockResolvedValue([])
// Fallback paths may still call gh (pr view / etc.) without --hostname.
ghExecFileAsyncMock.mockResolvedValue({
stdout: JSON.stringify({ body: 'meta only', headRefOid: 'h', baseRefOid: 'b' })
})
const details = await getWorkItemDetails('/repo-root', 7, 'pr')
expect(details).not.toBeNull()
expect(details?.filesUnavailable).toBe(true)
expect(details?.files).toBeUndefined()
const apiCalls = ghExecFileAsyncMock.mock.calls.map(([args]) => args as string[])
// Bug: no Enterprise host routing on this path today.
expect(apiCalls.every((args) => !args.includes('--hostname'))).toBe(true)
})
it('getPRFileContents returns empty when getOwnerRepo is null (cannot load GHE file content)', async () => {
getOwnerRepoMock.mockResolvedValue(null)
const contents = await getPRFileContents({
repoPath: '/repo-root',
prNumber: 7,
path: 'src/main.ts',
status: 'modified',
headSha: 'head-sha',
baseSha: 'base-sha'
})
expect(contents).toEqual({
original: '',
modified: '',
originalIsBinary: false,
modifiedIsBinary: false
})
expect(ghExecFileAsyncMock).not.toHaveBeenCalled()
})
it('work-item-details source uses getOwnerRepo and never getEnterpriseGitHubRepoSlug / --hostname', () => {
const source = readFileSync(join(__dirname, 'work-item-details.ts'), 'utf8')
expect(source).toContain('getOwnerRepo')
expect(source).not.toContain('getEnterpriseGitHubRepoSlug')
expect(source).not.toContain('--hostname')
})
})
@@ -0,0 +1,47 @@
/**
* Issue #8729 — imported .codex-pet bundles ignore per-frame durations; uniform 8 fps.
*
* Codex idle frame ms: [1680, 660, 660, 840, 840, 1920] ≈ 6.6s cycle.
* Orca applyCodexPetDefaults only stores { row, frames } + sheet-wide fps=8 →
* 6 frames / 8 fps = 0.75s cycle (~9× too fast).
*
* Synthetic fixture only — never load issue attachments.
*
* Re-run:
* pnpm exec vitest run --config config/vitest.config.ts \
* src/main/ipc/repro-8729-codex-pet-fps.test.ts
*/
import { describe, expect, it } from 'vitest'
import { applyCodexPetDefaults, CODEX_PET_DEFAULT_FPS, CODEX_PET_ANIMATIONS } from './pet-bundle'
/** Codex-documented idle hold times (ms) from issue #8729 / codex-rs pets model. */
const CODEX_IDLE_FRAME_MS = [1680, 660, 660, 840, 840, 1920] as const
function cycleMsFromUniformFps(frames: number, fps: number): number {
return (frames / fps) * 1000
}
function cycleMsFromPerFrame(ms: readonly number[]): number {
return ms.reduce((a, b) => a + b, 0)
}
describe('issue #8729 codex-pet frame timing', () => {
it('defaults codex layout to sheet-wide 8 fps without per-frame durations', () => {
const manifest = applyCodexPetDefaults({ id: 'repro-pet', displayName: 'Repro' })
expect(manifest.fps).toBe(CODEX_PET_DEFAULT_FPS)
expect(manifest.fps).toBe(8)
expect(manifest.animations?.idle).toEqual({ row: 0, frames: 6 })
// No durationMs / frameDurations field on animation rows
expect(manifest.animations?.idle).not.toHaveProperty('durationMs')
expect(manifest.animations?.idle).not.toHaveProperty('frameDurations')
})
it('proves 8 fps cycle is ~9× faster than Codex idle per-frame holds', () => {
const frames = CODEX_PET_ANIMATIONS.idle.frames
const orcaMs = cycleMsFromUniformFps(frames, CODEX_PET_DEFAULT_FPS)
const codexMs = cycleMsFromPerFrame(CODEX_IDLE_FRAME_MS)
expect(orcaMs).toBe(750)
expect(codexMs).toBe(6600)
expect(codexMs / orcaMs).toBeCloseTo(8.8, 1)
})
})
@@ -0,0 +1,100 @@
/**
* Issue #8934 — Jira issue images do not render in Tasks drawer.
*
* Jira ADF stores pasted screenshots as media / mediaSingle / mediaInline
* nodes. adfToMarkdownText has no cases for those types, so images are
* silently dropped. ISSUE_FIELDS also omits `attachment`, so even if media
* were mapped there is no authenticated download path on getIssue.
*
* Re-run:
* pnpm exec vitest run --config config/vitest.config.ts \
* src/main/jira/repro-8934-adf-media-dropped.test.ts
*/
import { readFileSync } from 'node:fs'
import { join } from 'node:path'
import { describe, expect, it } from 'vitest'
import { adfToMarkdownText } from './adf-markdown'
const adfSource = readFileSync(join(__dirname, 'adf-markdown.ts'), 'utf8')
const issuesSource = readFileSync(join(__dirname, 'issues.ts'), 'utf8')
describe('#8934 Jira ADF media nodes dropped in Tasks drawer', () => {
it('drops mediaSingle / media / mediaInline with no markdown image output', () => {
const adf = {
type: 'doc',
version: 1,
content: [
{
type: 'paragraph',
content: [{ type: 'text', text: 'Acceptance criteria:' }]
},
{
type: 'mediaSingle',
attrs: { layout: 'center' },
content: [
{
type: 'media',
attrs: {
id: 'aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee',
type: 'file',
collection: '',
width: 800,
height: 600
}
}
]
},
{
type: 'paragraph',
content: [
{
type: 'mediaInline',
attrs: {
id: 'ffffffff-1111-2222-3333-444444444444',
type: 'file',
collection: ''
}
},
{ type: 'text', text: ' after image' }
]
}
]
}
const md = adfToMarkdownText(adf)
expect(md).toContain('Acceptance criteria:')
expect(md).toContain('after image')
// Bug: no image markdown, data URL, or media id retained
expect(md).not.toMatch(/!\[/)
expect(md).not.toMatch(/aaaaaaaa-bbbb/)
expect(md).not.toMatch(/data:image/)
expect(md).not.toMatch(/attachment/)
})
it('source has no media type handlers and ISSUE_FIELDS omits attachment', () => {
expect(adfSource).not.toMatch(/type === 'media'/)
expect(adfSource).not.toMatch(/mediaSingle|mediaInline/)
expect(issuesSource).toMatch(/const ISSUE_FIELDS = \[/)
expect(issuesSource).not.toMatch(/'attachment'/)
})
it('still converts ordinary paragraph/heading text (regression guard)', () => {
expect(
adfToMarkdownText({
type: 'doc',
version: 1,
content: [
{
type: 'heading',
attrs: { level: 2 },
content: [{ type: 'text', text: 'Use Case' }]
},
{
type: 'paragraph',
content: [{ type: 'text', text: 'Only text here.' }]
}
]
})
).toBe('## Use Case\n\nOnly text here.')
})
})
@@ -0,0 +1,45 @@
/**
* Issue #8695 — bundled undici 7.28.0 asserts on paused parser when socket closes.
*
* Upstream: nodejs/undici#5360 / fix nodejs/undici#5474
* Orca ships Electron 43 / Node 24.18.0 with undici 7.28.0 (still vulnerable).
*
* The crash is process-fatal (uncaught AssertionError), so we document version
* pins here and keep the live crash script under docs/bug-reproductions/scripts/.
*
* Re-run unit proof:
* pnpm exec vitest run --config config/vitest.config.ts \
* src/main/repro-8695-undici-paused-parser.test.ts
*
* Re-run live crash (expects non-zero exit):
* bash docs/bug-reproductions/scripts/repro-8695-undici-paused-parser.sh
*/
import { describe, expect, it } from 'vitest'
import { createRequire } from 'node:module'
import { readFileSync } from 'node:fs'
import { join } from 'node:path'
const require = createRequire(import.meta.url)
describe('issue #8695 undici paused-parser assert', () => {
it('worktree depends on vulnerable undici 7.28.0', () => {
// eslint-disable-next-line @typescript-eslint/no-require-imports
const pkg = require('undici/package.json') as { version: string }
expect(pkg.version).toBe('7.28.0')
})
it('undici client-h1 still contains assert(!this.paused) in finish path', () => {
// Why: upstream fix resumes/drains paused parser before finish; 7.28.0 still asserts.
const clientH1 = readFileSync(require.resolve('undici/lib/dispatcher/client-h1.js'), 'utf8')
expect(clientH1).toMatch(/assert\(!this\.paused\)/)
})
it('package.json pins Electron runtime that embeds Node 24 + undici 7.28.0', () => {
const rootPkg = JSON.parse(readFileSync(join(__dirname, '../../package.json'), 'utf8')) as {
devDependencies?: Record<string, string>
engines?: { node?: string }
}
expect(rootPkg.devDependencies?.electron ?? rootPkg.engines?.node).toBeTruthy()
expect(rootPkg.engines?.node).toMatch(/24/)
})
})
@@ -0,0 +1,48 @@
/**
* Issue #8535 — pinned `orca serve --port <P>` overridden by stale mobile-ws fallback.
*
* WebSocketTransport binds persisted fallback BEFORE preferred port so old paired
* devices stay reachable (STA-1511). Side effect: an explicit --port is never tried
* first when mobile-ws-fallback-port.json names a different port.
*
* Re-run:
* pnpm exec vitest run --config config/vitest.config.ts \
* src/main/runtime/rpc/repro-8535-ws-fallback-port-order.test.ts
*/
import { describe, expect, it } from 'vitest'
import { readFileSync } from 'node:fs'
import { join } from 'node:path'
/**
* Pure mirror of the candidate-port order in WebSocketTransport.start
* (src/main/runtime/rpc/ws-transport.ts). Kept local so the test fails if the
* source order comment/logic diverges without updating this contract.
*/
export function candidatePortsForServe(preferredPort: number, fallbackPort?: number): number[] {
const persistedFallbackPort =
fallbackPort !== undefined && fallbackPort !== 0 && fallbackPort !== preferredPort
? fallbackPort
: undefined
return persistedFallbackPort !== undefined
? [persistedFallbackPort, preferredPort]
: [preferredPort]
}
describe('issue #8535 serve port vs mobile-ws fallback', () => {
it('binds stale fallback before explicitly preferred port', () => {
// User: orca serve --port 6768, but userData has fallback 49152 from an earlier EADDRINUSE.
expect(candidatePortsForServe(6768, 49152)).toEqual([49152, 6768])
})
it('does not insert fallback when equal to preferred or zero', () => {
expect(candidatePortsForServe(6768, 6768)).toEqual([6768])
expect(candidatePortsForServe(6768, 0)).toEqual([6768])
expect(candidatePortsForServe(6768, undefined)).toEqual([6768])
})
it('source still documents bind-fallback-first order', () => {
const src = readFileSync(join(__dirname, 'ws-transport.ts'), 'utf8')
expect(src).toMatch(/persisted fallback port is bound FIRST/)
expect(src).toMatch(/\[persistedFallbackPort, this\.port\]/)
})
})
@@ -0,0 +1,93 @@
/**
* Issue #8450 — SSH relay selects system Node without npm instead of NVM.
*
* resolveRemoteNodePath accepts the first Node that meets the major-version
* gate. It never probes companion `npm` in that Node's bin dir. The path-probe
* script lists `command -v node` first, so `/usr/bin/node` (Ubuntu package
* without npm) wins before NVM candidates. installNativeDeps then does
* `export PATH=<nodeBinDir>:$PATH && npm install …` → exit 127.
*
* Re-run:
* pnpm exec vitest run --config config/vitest.config.ts \
* src/main/ssh/repro-8450-node-without-npm.test.ts
*/
import { readFileSync } from 'node:fs'
import { join } from 'node:path'
import { beforeEach, describe, expect, it, vi } from 'vitest'
import type { SshConnection } from './ssh-connection'
import { commandWithNodePath } from './ssh-remote-commands'
const execCommandMock = vi.hoisted(() => vi.fn())
vi.mock('./ssh-relay-deploy-helpers', () => ({
execCommand: execCommandMock
}))
const { resolveRemoteNodePath } = await import('./ssh-remote-node-resolution')
const conn = {} as SshConnection
describe('issue #8450 SSH node resolution ignores missing npm', () => {
beforeEach(() => {
execCommandMock.mockReset()
})
it('accepts system node that only satisfies the version gate (no npm check)', async () => {
// Path probe returns system node first (command -v node), then NVM.
// Only the first candidate is version-checked when it succeeds.
execCommandMock
.mockResolvedValueOnce(
`${['/usr/bin/node', '/home/user/.nvm/versions/node/v22.22.0/bin/node'].join('\n')}\n`
)
.mockResolvedValueOnce('v18.19.1\n') // version only — no npm probe
await expect(resolveRemoteNodePath(conn)).resolves.toBe('/usr/bin/node')
// Version check only — never `command -v npm` or sibling npm --version.
expect(execCommandMock).toHaveBeenCalledTimes(2)
const versionCmd = String(execCommandMock.mock.calls[1]![1])
expect(versionCmd).toMatch(/node.*--version|\/usr\/bin\/node/)
expect(versionCmd).not.toMatch(/npm/)
})
it('source path-probe lists command -v node before NVM globs and never requires npm', () => {
const source = readFileSync(join(__dirname, 'ssh-remote-node-resolution.ts'), 'utf8')
const commandVNodeIdx = source.indexOf('command -v node 2>/dev/null')
const nvmGlobIdx = source.indexOf('"$nvm_dir"/versions/node/*/bin/node')
expect(commandVNodeIdx).toBeGreaterThan(-1)
expect(nvmGlobIdx).toBeGreaterThan(commandVNodeIdx)
// Acceptance is node --version major only (error guidance may mention npm).
expect(source).toMatch(/nodeMeetsVersionRequirement/)
expect(source).toMatch(/MIN_NODE_MAJOR\s*=\s*18/)
expect(source).not.toMatch(/npmMeets|hasNpm|command -v npm/)
// No function that validates companion npm beside the accepted node binary.
const acceptanceSlice = source.slice(
source.indexOf('async function nodeMeetsVersionRequirement'),
source.indexOf('async function resolveRemoteWindowsNodePath')
)
expect(acceptanceSlice).toMatch(/--version/)
expect(acceptanceSlice).not.toMatch(/npm/)
})
it('commandWithNodePath prepends selected node bin dir then runs bare npm', () => {
const host = { os: 'linux' as const, shell: 'bash' }
const cmd = commandWithNodePath(
host,
'/usr/bin/node',
'/home/u/.orca-remote/relay',
'npm install'
)
expect(cmd).toContain("export PATH='/usr/bin':$PATH")
expect(cmd).toContain('npm install')
// Mirrors the failure: PATH has node dir but no npm binary there.
})
it('installNativeDeps invokes npm without verifying companion npm exists', () => {
const deploy = readFileSync(join(__dirname, 'ssh-relay-deploy.ts'), 'utf8')
expect(deploy).toMatch(/npm install --omit=dev --no-audit --no-fund/)
expect(deploy).toMatch(/commandWithNodePath|export PATH/)
// No preflight that skips incomplete toolchains.
expect(deploy).not.toMatch(/command -v npm/)
})
})
@@ -0,0 +1,59 @@
/**
* Issue #8622 — SSH keyboard-interactive authentication (push-based MFA).
*
* Classification: missing capability (feature reported as bug). OpenSSH /
* VS Code Remote SSH complete password then keyboard-interactive MFA; Orca's
* ssh2 path never enables tryKeyboard or handles 'keyboard-interactive'.
*
* After password prompt, auth fails with the issue's exact message:
* "All configured authentication methods failed" — MFA prompt is never shown.
*
* Re-run:
* pnpm exec vitest run --config config/vitest.config.ts \
* src/main/ssh/repro-8622-keyboard-interactive-missing.test.ts
*/
import { readFileSync } from 'node:fs'
import { join } from 'node:path'
import { describe, expect, it } from 'vitest'
import { buildConnectConfig, isAuthError, type SshCredentialKind } from './ssh-connection-utils'
import type { SshTarget } from '../../shared/ssh-types'
const connectionSource = readFileSync(join(__dirname, 'ssh-connection.ts'), 'utf8')
const utilsSource = readFileSync(join(__dirname, 'ssh-connection-utils.ts'), 'utf8')
const authSource = readFileSync(join(__dirname, 'ssh-auth-resolution.ts'), 'utf8')
describe('#8622 keyboard-interactive auth not implemented (capability gap)', () => {
it('buildConnectConfig never sets tryKeyboard', () => {
const target = {
id: 't1',
label: 'hpc',
host: 'hpc.example.edu',
port: 22,
username: 'student'
} as SshTarget
const config = buildConnectConfig(target, null)
expect(config).not.toHaveProperty('tryKeyboard')
expect(utilsSource).not.toMatch(/tryKeyboard/)
expect(authSource).not.toMatch(/tryKeyboard|keyboard-interactive|keyboardInteractive/)
})
it('credential kinds are only passphrase | password (no keyboard-interactive)', () => {
// Compile-time shape mirrored at runtime via source contract
const kinds: SshCredentialKind[] = ['passphrase', 'password']
expect(kinds).toEqual(['passphrase', 'password'])
expect(utilsSource).toMatch(/export type SshCredentialKind = 'passphrase' \| 'password'/)
expect(utilsSource).not.toMatch(/keyboard/)
})
it('ssh-connection has no keyboard-interactive event handler', () => {
expect(connectionSource).not.toMatch(/keyboard-interactive/)
expect(connectionSource).not.toMatch(/keyboardInteractive/)
expect(connectionSource).not.toMatch(/on\(['"]keyboard/)
// Password path exists and is the only interactive auth retry after agent fail
expect(connectionSource).toMatch(/onCredentialRequest\([\s\S]*'password'/s)
})
it('classifies the issue failure string as an auth error (surface users see)', () => {
expect(isAuthError(new Error('All configured authentication methods failed'))).toBe(true)
})
})
@@ -0,0 +1,74 @@
/**
* Issue #8720 — SSH relay npm install silently skips node-pty under npm 12 (allowScripts).
*
* npm 12 blocks lifecycle scripts not listed in package.json `allowScripts` (or
* approved via allow-scripts). The generated relay package.json only pins
* dependencies — no allowScripts — so node-pty's install/postinstall never run on
* Linux (no prebuild), while npm still exits 0 → .install-complete is written.
*
* Re-run:
* pnpm exec vitest run --config config/vitest.config.ts \
* src/main/ssh/repro-8720-npm12-allow-scripts.test.ts
*/
import { describe, expect, it } from 'vitest'
import { readFileSync } from 'node:fs'
import { join } from 'node:path'
/** Mirrors ssh-relay-deploy.ts RELAY_NATIVE_DEPS + pkgJson construction. */
function buildRelayPackageJson(): Record<string, unknown> {
const RELAY_NATIVE_DEPS = {
'node-pty': '1.1.0',
'@parcel/watcher': '2.5.6'
} as const
return {
name: 'orca-relay',
version: '1.0.0',
private: true,
type: 'commonjs',
dependencies: RELAY_NATIVE_DEPS
}
}
describe('issue #8720 relay package.json lacks allowScripts for npm 12', () => {
it('source-written package.json has deps but no allowScripts field', () => {
const deploySource = readFileSync(join(__dirname, 'ssh-relay-deploy.ts'), 'utf8')
// Pinned native deps that need lifecycle scripts on Linux
expect(deploySource).toMatch(/'node-pty':\s*'1\.1\.0'/)
expect(deploySource).toMatch(/'@parcel\/watcher':\s*'2\.5\.6'/)
// Generated package.json shape in installNativeDeps
expect(deploySource).toMatch(/name:\s*'orca-relay'/)
expect(deploySource).toMatch(/dependencies:\s*RELAY_NATIVE_DEPS/)
// Bug: no allowScripts (npm 12 sanctioned mechanism) on the written package.json
expect(deploySource).not.toMatch(/allowScripts/)
expect(deploySource).not.toMatch(/allow-scripts/)
})
it('npm install command does not approve lifecycle scripts', () => {
const deploySource = readFileSync(join(__dirname, 'ssh-relay-deploy.ts'), 'utf8')
expect(deploySource).toMatch(/npm install --omit=dev --no-audit --no-fund/)
expect(deploySource).not.toMatch(/npm install-scripts/)
expect(deploySource).not.toMatch(/--ignore-scripts=false/)
})
it('synthetic package.json matches deploy contract and is incomplete for npm 12', () => {
const pkgJson = buildRelayPackageJson()
expect(pkgJson.dependencies).toEqual({
'node-pty': '1.1.0',
'@parcel/watcher': '2.5.6'
})
expect(pkgJson).not.toHaveProperty('allowScripts')
// npm 12: without allowScripts covering node-pty install, scripts are blocked
// while exit code stays 0 (strict-allow-scripts default false).
expect('allowScripts' in pkgJson).toBe(false)
})
it('existing native-deps install test pins deps without asserting allowScripts', () => {
const harness = readFileSync(join(__dirname, 'ssh-relay-native-deps-install.test.ts'), 'utf8')
expect(harness).toContain("'@parcel/watcher': '2.5.6'")
expect(harness).toContain("'node-pty': '1.1.0'")
expect(harness).not.toMatch(/allowScripts/)
})
})
@@ -0,0 +1,41 @@
/**
* Issue #8758 — "node-pty is not available on this remote host" (Linux SSH).
*
* Same deploy contract as #8720: installNativeDeps writes package.json without
* allowScripts, so npm 12 on Linux blocks node-pty's native build while still
* exiting 0. Debian 13 remotes without prebuilds hit this at PTY spawn.
*
* Re-run:
* pnpm exec vitest run --config config/vitest.config.ts \
* src/main/ssh/repro-8758-node-pty-remote.test.ts
* # sibling #8720 proof:
* pnpm exec vitest run --config config/vitest.config.ts \
* src/main/ssh/repro-8720-npm12-allow-scripts.test.ts
*/
import { readFileSync } from 'node:fs'
import { join } from 'node:path'
import { describe, expect, it } from 'vitest'
const deploySource = readFileSync(join(__dirname, 'ssh-relay-deploy.ts'), 'utf8')
const toolchainSource = readFileSync(join(__dirname, 'ssh-relay-build-toolchain.ts'), 'utf8')
const ptyHandlerSource = readFileSync(join(__dirname, '../../relay/pty-handler.ts'), 'utf8')
describe('#8758 node-pty unavailable on remote (same root as #8720)', () => {
it('relay native package.json has no allowScripts for node-pty', () => {
expect(deploySource).toMatch(/'node-pty':\s*'1\.1\.0'/)
expect(deploySource).toMatch(/dependencies:\s*RELAY_NATIVE_DEPS/)
expect(deploySource).not.toMatch(/allowScripts/)
expect(deploySource).toMatch(/npm install --omit=dev --no-audit --no-fund/)
})
it('surfaces the issue error string when node-pty cannot load', () => {
// User-visible message from the issue report (relay PTY spawn path)
expect(ptyHandlerSource).toContain('node-pty is not available on this remote host')
})
it('documents Linux has no node-pty prebuild path (must build from source)', () => {
// Toolchain / deploy notes that Linux needs native compile
const combined = deploySource + toolchainSource
expect(combined.toLowerCase()).toMatch(/linux|prebuild|node-gyp|native/)
})
})
@@ -0,0 +1,112 @@
/**
* Issue #8457 — headless serve hijacks GUI relaunch; forced reopen can
* interrupt/duplicate live agents.
*
* Serve mode acquires the single-instance lock and, after settle with a
* daemon-backed PTY provider, marks the activation gate `ready`. Second-instance
* / Dock activate then call openMainWindow inside the headless process.
* `orca open` only fails closed on `blocked`, not on headless `openable`.
*
* Re-run:
* pnpm exec vitest run --config config/vitest.config.ts \
* src/main/startup/repro-8457-serve-desktop-hijack.test.ts
*/
import { readFileSync } from 'node:fs'
import { join } from 'node:path'
import { describe, expect, it, vi } from 'vitest'
import { createServeDesktopActivationGate } from './serve-desktop-activation'
import { shouldSkipSingleInstanceLock } from './single-instance-lock'
/** Mirrors settleServeDesktopActivation in src/main/index.ts. */
function settleServeDesktopActivation(
gate: ReturnType<typeof createServeDesktopActivationGate>,
isFallbackLocalPtyProvider: boolean
): void {
if (isFallbackLocalPtyProvider) {
gate.markBlocked('persistent PTY provider unavailable')
return
}
gate.markReady()
}
/** Mirrors RuntimeClient.openOrca early-exit policy. */
function openOrcaShouldFailClosed(
desktopWindowStatus: 'available' | 'openable' | 'initializing' | 'blocked' | undefined
): boolean {
return desktopWindowStatus === 'blocked'
}
describe('issue #8457 headless serve hijacks desktop activation', () => {
it('packaged serve does not skip the single-instance lock (becomes lock owner)', () => {
// Dev non-serve skips the lock; serve never does — headless can own the profile.
expect(shouldSkipSingleInstanceLock({ isDev: false, isServeMode: true })).toBe(false)
expect(shouldSkipSingleInstanceLock({ isDev: true, isServeMode: true })).toBe(false)
expect(shouldSkipSingleInstanceLock({ isDev: true, isServeMode: false })).toBe(true)
})
it('after settle with daemon PTY, second-instance activation opens the desktop window', () => {
const activateWindow = vi.fn()
const onBlocked = vi.fn()
const gate = createServeDesktopActivationGate({
initialState: 'initializing',
activateWindow,
onBlocked
})
// Second-instance / open -n while serve is still starting.
gate.requestActivation()
expect(activateWindow).not.toHaveBeenCalled()
// Daemon-backed persistent sessions → markReady (not blocked).
settleServeDesktopActivation(gate, /* isFallbackLocalPtyProvider */ false)
// Pending activation drains into openMainWindow (via activateWindow).
expect(activateWindow).toHaveBeenCalledOnce()
expect(gate.getState()).toBe('ready')
expect(onBlocked).not.toHaveBeenCalled()
// Later Dock / open -a / second-instance also open the window.
gate.requestActivation()
expect(activateWindow).toHaveBeenCalledTimes(2)
})
it('only blocks open when provider is the non-persistent LocalPty fallback', () => {
const activateWindow = vi.fn()
const onBlocked = vi.fn()
const gate = createServeDesktopActivationGate({
initialState: 'initializing',
activateWindow,
onBlocked
})
gate.requestActivation()
settleServeDesktopActivation(gate, /* isFallbackLocalPtyProvider */ true)
expect(activateWindow).not.toHaveBeenCalled()
expect(gate.getState()).toBe('blocked')
expect(onBlocked).toHaveBeenCalled()
})
it('orca open fails closed only for blocked — openable headless still launches', () => {
// Headless serve with daemon PTYs reports openable after settle, not blocked.
expect(openOrcaShouldFailClosed('blocked')).toBe(true)
expect(openOrcaShouldFailClosed('openable')).toBe(false)
expect(openOrcaShouldFailClosed('initializing')).toBe(false)
expect(openOrcaShouldFailClosed('available')).toBe(false)
})
it('wiring still routes serve activation to openMainWindow after markReady', () => {
const indexSrc = readFileSync(join(process.cwd(), 'src/main/index.ts'), 'utf8')
expect(indexSrc).toMatch(/initialState: isServeMode \? 'initializing' : 'ready'/)
expect(indexSrc).toMatch(/activateWindow: \(\) => \{[\s\S]*focusExistingWindow\(\)/)
expect(indexSrc).toMatch(/settleServeDesktopActivation\(\)/)
// Daemon path marks ready → allows promotion; only LocalPtyProvider blocks.
expect(indexSrc).toMatch(/getLocalPtyProvider\(\) instanceof LocalPtyProvider/)
expect(indexSrc).toMatch(/desktopActivationGate\.markReady\(\)/)
expect(indexSrc).toMatch(/desktopActivationGate\.markBlocked\(/)
// focusExistingMainWindow still opens a window when none exists.
const focusSrc = readFileSync(
join(process.cwd(), 'src/main/window/focus-existing-window.ts'),
'utf8'
)
expect(focusSrc).toMatch(/window = opts\.openWindow\(\)/)
})
})
@@ -0,0 +1,52 @@
/**
* Issue #8482 — Window Blur causes sustained high GPU power/heat on macOS.
*
* Code-level contract (not live powermetrics):
* 1. Blur maps to Electron `vibrancy: 'under-window'` + `transparent: true`.
* 2. macOS always disables background throttling on the main window.
* 3. Blur is window-creation-only (restart required), so a misconfigured
* vibrancy path stays active for the whole session.
*
* Issue reporter measured ~5.3 W GPU with blur visible vs ~0.8 W with blur
* off and ~0.6 W when the same processes stay up but the window is hidden —
* isolating cost to visible composition, not PTYs alone.
*
* Re-run:
* pnpm exec vitest run --config config/vitest.config.ts \
* src/main/window/repro-8482-window-blur-gpu-cost.test.ts
*/
import { readFileSync } from 'node:fs'
import { join } from 'node:path'
import { describe, expect, it } from 'vitest'
const createMainWindowSource = readFileSync(join(__dirname, 'createMainWindow.ts'), 'utf8')
describe('#8482 macOS Window Blur GPU composition cost (config contract)', () => {
it('maps Window Blur to vibrancy + transparent on darwin', () => {
expect(createMainWindowSource).toMatch(
/const blur = settings\?\.windowBackgroundBlur \?\? false/
)
expect(createMainWindowSource).toMatch(
/process\.platform === 'darwin'\s*\?\s*\{\s*vibrancy:\s*'under-window'\s+as\s+const,\s*transparent:\s*true/s
)
// Spread into BrowserWindow so blur is a window-level compositor path
expect(createMainWindowSource).toMatch(/\.\.\.platformBlurOptions/)
})
it('disables background throttling on every macOS main window', () => {
// Why: throttling off keeps compositor/animation work eligible while
// vibrancy is visible — pairs with continuous GPU draw in the report.
expect(createMainWindowSource).toMatch(
/if \(process\.platform === 'darwin'\)[\s\S]*setBackgroundThrottling\(false\)/
)
})
it('applies blur only at window construction (no live setVibrancy teardown)', () => {
expect(createMainWindowSource).toMatch(
/Blur only applies at window creation[\s\S]*changing the setting requires a restart/
)
expect(createMainWindowSource).not.toMatch(/setVibrancy/)
// No path to drop vibrancy without recreating the window
expect(createMainWindowSource).not.toMatch(/visualEffectState/)
})
})
@@ -0,0 +1,77 @@
/**
* Issue #8797 — Background Opacity + Window Blur have no visible effect on macOS.
*
* Root cause (code-level):
* 1. createMainWindow always paints an opaque solid `backgroundColor`
* (`#0a0a0a` / `#ffffff`) even when blur enables `transparent: true` +
* `vibrancy: 'under-window'`. The opaque layer covers the vibrancy view.
* 2. `terminalBackgroundOpacity` only rewrites the xterm theme background to
* rgba(); it never updates BrowserWindow backgroundColor. With an opaque
* window fill, lowering terminal alpha only reveals that same solid color
* — never the desktop / blur layer.
* 3. Blur is read only at window creation (restart required); there is no
* live `setVibrancy` / `visualEffectState` path.
*
* Re-run:
* pnpm exec vitest run --config config/vitest.config.ts \
* src/main/window/repro-8797-blur-opacity-noop.test.ts
*/
import { readFileSync } from 'node:fs'
import { join } from 'node:path'
import { describe, expect, it } from 'vitest'
import {
composeActiveTerminalTheme,
hexToRgba
} from '../../renderer/src/components/terminal-pane/terminal-appearance'
const createMainWindowSource = readFileSync(join(__dirname, 'createMainWindow.ts'), 'utf8')
describe('#8797 macOS window blur + background opacity no-op', () => {
it('pairs vibrancy with transparent:true AND an always-opaque backgroundColor', () => {
// Blur path (darwin only)
expect(createMainWindowSource).toMatch(
/vibrancy:\s*'under-window'\s+as\s+const,\s*transparent:\s*true/s
)
// Opaque solid fill is unconditional — not gated on blur/opacity
expect(createMainWindowSource).toMatch(
/backgroundColor:\s*nativeTheme\.shouldUseDarkColors\s*\?\s*'#0a0a0a'\s*:\s*'#ffffff'/
)
// No visualEffectState / setBackgroundColor(alpha) / setVibrancy live path
expect(createMainWindowSource).not.toMatch(/visualEffectState/)
expect(createMainWindowSource).not.toMatch(/setVibrancy|setBackgroundColor/)
})
it('documents that blur is startup-only (settings change needs restart)', () => {
expect(createMainWindowSource).toMatch(
/Blur only applies at window creation[\s\S]*changing the setting requires a restart/
)
expect(createMainWindowSource).toMatch(
/const blur = settings\?\.windowBackgroundBlur \?\? false/
)
})
it('applies terminalBackgroundOpacity only to xterm theme rgba, not the window chrome', () => {
const theme = composeActiveTerminalTheme(
{ background: '#0a0a0a', foreground: '#ffffff' },
{ terminalBackgroundOpacity: 0.3 }
)
expect(theme.background).toBe(hexToRgba('#0a0a0a', 0.3))
expect(theme.background).toBe('rgba(10, 10, 10, 0.3)')
// Window chrome stays fully opaque hex — same digits as the theme base —
// so a transparent terminal only reveals #0a0a0a / #ffffff, not blur.
expect(createMainWindowSource).toContain("'#0a0a0a'")
expect(createMainWindowSource).toContain("'#ffffff'")
})
it('proves opacity=0 still cannot show the desktop while window background is opaque', () => {
const fullyTransparentTerminal = composeActiveTerminalTheme(
{ background: '#0a0a0a' },
{ terminalBackgroundOpacity: 0 }
)
expect(fullyTransparentTerminal.background).toBe('rgba(10, 10, 10, 0)')
// The window constructor still injects an opaque backgroundColor; with
// no alpha channel on that fill, a fully transparent xterm still sits
// on solid black/white — desktop never shows through.
expect(createMainWindowSource).toMatch(/backgroundColor:\s*nativeTheme/)
})
})
@@ -0,0 +1,78 @@
/**
* Issue #8903 — Cmd-J jump palette leaves keyboard focus on the wrong surface.
*
* After selecting a workspace, WorktreeJumpPalette calls
* focusFallbackSurface() with NO preferred target, which resolves to
* document.querySelector('.xterm-helper-textarea') — the first match in the
* DOM. Inactive workspaces stay mounted (display:none), so the first xterm is
* often a hidden workspace's terminal.
*
* Re-run:
* pnpm exec vitest run src/renderer/src/components/cmd-j/repro-8903-cmdj-focus-fallback.test.ts
*/
// @vitest-environment happy-dom
import { readFileSync } from 'node:fs'
import { join } from 'node:path'
import { afterEach, describe, expect, it } from 'vitest'
import { resolvePaletteFocusRestoreTarget } from './palette-focus-restore-target'
afterEach(() => {
document.body.innerHTML = ''
})
function addTerminal(id: string, options?: { hidden?: boolean }): HTMLTextAreaElement {
const wrap = document.createElement('div')
if (options?.hidden) {
wrap.style.display = 'none'
wrap.dataset.worktree = 'background'
} else {
wrap.dataset.worktree = 'active'
}
const textarea = document.createElement('textarea')
textarea.className = 'xterm-helper-textarea'
textarea.dataset.terminal = id
wrap.appendChild(textarea)
document.body.appendChild(wrap)
return textarea
}
describe('#8903 Cmd-J focus falls back to first DOM xterm (often hidden)', () => {
it('resolvePaletteFocusRestoreTarget(null) returns the first mounted xterm, not the visible one', () => {
const hidden = addTerminal('hidden-background', { hidden: true })
const visible = addTerminal('visible-destination')
// Enter path in WorktreeJumpPalette: focusFallbackSurface() with no preferredTarget
const target = resolvePaletteFocusRestoreTarget(null)
expect(target).toBe(hidden)
expect(target).not.toBe(visible)
expect(target?.closest('[data-worktree]')?.getAttribute('data-worktree')).toBe('background')
})
it('source: handleSelectWorktree calls focusFallbackSurface() without a preferred element', () => {
const source = readFileSync(join(__dirname, '../WorktreeJumpPalette.tsx'), 'utf8')
// Destination jump skips previous-focus restore then generic-falls back.
expect(source).toMatch(/skipRestoreFocusRef\.current\s*=\s*true/)
expect(source).toMatch(/focusFallbackSurface\(\)/)
// The generic resolver used by focusFallbackSurface
expect(source).toContain('resolvePaletteFocusRestoreTarget')
})
it('source: resolvePaletteFocusRestoreTarget uses unscoped querySelector for xterm', () => {
const source = readFileSync(join(__dirname, './palette-focus-restore-target.ts'), 'utf8')
expect(source).toContain("doc.querySelector('.xterm-helper-textarea')")
// Comments in-source already acknowledge background worktree risk
expect(source).toMatch(/background worktree|mounted-but-hidden/i)
})
it('Esc path only restores when preferred target is still connected; else first DOM xterm', () => {
const first = addTerminal('first')
const second = addTerminal('second')
const detached = document.createElement('textarea')
detached.className = 'xterm-helper-textarea'
expect(resolvePaletteFocusRestoreTarget(second)).toBe(second)
expect(detached.isConnected).toBe(false)
expect(resolvePaletteFocusRestoreTarget(detached)).toBe(first)
})
})
@@ -0,0 +1,75 @@
/**
* Issue #8784 — GHE PR avatars built from github.com/{login}.png instead of API avatar_url.
*
* When avatarUrl is missing/empty, UI falls back to public github.com avatars which 404
* for Enterprise-only logins. PullRequestPage author chip even ignores avatar_url entirely.
*
* Related fix PR: https://github.com/stablyai/orca/pull/8831
*
* Re-run:
* pnpm exec vitest run --config config/vitest.config.ts \
* src/renderer/src/components/github/repro-8784-ghe-avatar-fallback.test.ts
*/
import { describe, expect, it } from 'vitest'
import { readFileSync } from 'node:fs'
import { join } from 'node:path'
import { githubAvatarUrl } from './github-issue-comment-helpers'
/** Mirrors TaskPage ReviewChipAvatar resolution (TaskPage.tsx). */
export function resolveReviewerAvatarUrl(reviewer: {
login: string
avatarUrl?: string | null
}): string {
return reviewer.avatarUrl || `https://github.com/${reviewer.login}.png?size=40`
}
/** Mirrors PullRequestPage ReviewerAvatar resolution. */
export function resolveReviewerAvatarSrc(login: string, avatarUrl: string): string {
return avatarUrl || githubAvatarUrl(login)
}
describe('issue #8784 GHE avatar fallback', () => {
it('falls back to github.com/{login}.png when avatarUrl empty — wrong for GHE users', () => {
const gheOnly = { login: 'enterprise-only-user', avatarUrl: '' }
const url = resolveReviewerAvatarUrl(gheOnly)
expect(url).toBe('https://github.com/enterprise-only-user.png?size=40')
expect(url.startsWith('https://github.com/')).toBe(true)
})
it('prefers API avatar_url when present (healthy path for TaskPage)', () => {
const api = {
login: 'enterprise-only-user',
avatarUrl: 'https://ghe.example.com/avatars/u/42?v=4'
}
expect(resolveReviewerAvatarUrl(api)).toBe('https://ghe.example.com/avatars/u/42?v=4')
})
it('production githubAvatarUrl hardcodes public github.com png from login', () => {
expect(githubAvatarUrl('corp-user')).toBe('https://github.com/corp-user.png?size=64')
// Enterprise login does not exist on github.com → 404
expect(githubAvatarUrl('enterprise-only-user')).toMatch(/^https:\/\/github\.com\//)
})
it('ReviewerAvatar falls back to login png when API avatar empty (GHE broken path)', () => {
const src = resolveReviewerAvatarSrc('ghe-reviewer', '')
expect(src).toBe('https://github.com/ghe-reviewer.png?size=64')
})
it('ReviewerAvatar prefers API avatar_url when provided', () => {
const apiUrl = 'https://avatars.ghe.example.com/u/99?v=4'
expect(resolveReviewerAvatarSrc('ghe-reviewer', apiUrl)).toBe(apiUrl)
})
it('source still builds github.com login.png and author ignores avatar_url', () => {
const taskPage = readFileSync(join(__dirname, '../TaskPage.tsx'), 'utf8')
expect(taskPage).toMatch(/github\.com\/\$\{reviewer\.login\}\.png/)
const helpers = readFileSync(join(__dirname, 'github-issue-comment-helpers.ts'), 'utf8')
expect(helpers).toMatch(/github\.com\/\$\{encodeURIComponent\(login\)\}\.png/)
// Why: author chip only passes login into githubAvatarUrl — never API avatar_url.
const prPage = readFileSync(join(__dirname, '../PullRequestPage.tsx'), 'utf8')
expect(prPage).toMatch(/githubAvatarUrl\(workItem\.author\)/)
expect(prPage).toMatch(/avatarUrl \|\| githubAvatarUrl\(login\)/)
})
})
@@ -0,0 +1,84 @@
/**
* Issue #8739 — Linear filters only show options from the first selected team.
*
* Filter chrome loads status/assignee/label metadata for `primaryTeam` only
* (resolveLinearIssueAttributeFilterPrimaryTeam → single team). Multi-team /
* "All teams" selection never unions options across selected teams.
*
* Re-run:
* pnpm exec vitest run --config config/vitest.config.ts \
* src/renderer/src/components/repro-8739-linear-filter-first-team.test.ts
*/
import { readFileSync } from 'node:fs'
import { join } from 'node:path'
import { describe, expect, it } from 'vitest'
import { resolveLinearIssueAttributeFilterPrimaryTeam } from './linear-issue-attribute-filter-primary-team'
import type { LinearTeam } from '../../../shared/types'
const teams: LinearTeam[] = [
{ id: 'team-be', name: 'Backend', key: 'BE' },
{ id: 'team-fe', name: 'Frontend', key: 'FE' },
{ id: 'team-ops', name: 'Ops', key: 'OPS' }
]
/** Mirrors LinearIssueAttributeFilterDropdowns: only primary team's option set is shown. */
function filterOptionsForSelection(
selectedTeamIds: string[],
optionsByTeamId: Record<string, string[]>
): { primaryTeamId: string | null; optionIds: string[] } {
const primary = resolveLinearIssueAttributeFilterPrimaryTeam({
selectedTeamIds,
availableTeams: teams
})
if (!primary) {
return { primaryTeamId: null, optionIds: [] }
}
return {
primaryTeamId: primary.id,
optionIds: optionsByTeamId[primary.id] ?? []
}
}
describe('issue #8739 Linear multi-team filter options collapse to first team', () => {
const optionsByTeamId = {
'team-be': ['be-todo', 'be-done'],
'team-fe': ['fe-todo', 'fe-review'],
'team-ops': ['ops-blocked']
}
it('loads only the primary (first-by-name) selected team when two teams are selected', () => {
// Selected Frontend + Backend; name order picks Backend first → FE options dropped.
const result = filterOptionsForSelection(['team-fe', 'team-be'], optionsByTeamId)
expect(result.primaryTeamId).toBe('team-be')
expect(result.optionIds).toEqual(['be-todo', 'be-done'])
expect(result.optionIds).not.toContain('fe-todo')
expect(result.optionIds).not.toContain('fe-review')
})
it('still collapses when all teams are selected (All teams)', () => {
const result = filterOptionsForSelection(['team-be', 'team-fe', 'team-ops'], optionsByTeamId)
expect(result.primaryTeamId).toBe('team-be')
expect(result.optionIds.sort()).toEqual(['be-done', 'be-todo'])
// Ops/Frontend metadata never appear in the filter option list.
expect(result.optionIds).not.toContain('fe-todo')
expect(result.optionIds).not.toContain('fe-review')
expect(result.optionIds).not.toContain('ops-blocked')
})
it('UI source only queries metadata for primaryTeam, not the selection set', () => {
const src = readFileSync(join(__dirname, 'linear-issue-attribute-filter-dropdowns.tsx'), 'utf8')
// Single active team id from primaryTeam only.
expect(src).toMatch(
/activeTeamId = popoverOpen && !isAllWorkspaces \? \(primaryTeam\?\.id \?\? null\) : null/
)
expect(src).toContain('useTeamStates(activeTeamId')
expect(src).toContain('useTeamLabels(activeTeamId')
expect(src).toContain('useTeamMembers(activeTeamId')
// Banner admits multi-select still shows one team.
expect(src).toMatch(/Options from \{\{team\}\}/)
expect(src).toMatch(/selectedTeamCount > 1 && primaryTeam/)
// No loop over selected teams for metadata load.
expect(src).not.toMatch(/for\s*\(.*selectedTeam/)
expect(src).not.toMatch(/selectedTeamIds\.map/)
})
})
@@ -0,0 +1,131 @@
/**
* Issue #8593 — lingering idle subagent rows clutter the sidebar; clicking
* them "does nothing" usefully (and they cannot be dismissed).
*
* Root cause (code-level):
* 1. `buildSubagentChildRows` maps every parent `subagents[]` entry to a
* sidebar/dashboard child row, including `state: 'idle'`.
* 2. Live hook updates keep idle children in the roster; only Claude *hydrate*
* prunes idle children (`dropHydratedIdleClaudeSubagents` in agent-hooks
* server) — so a long session that spawns tens of Task children piles up
* idle rows for the whole parent lifetime.
* 3. Subagent rows set `hideDismiss` (DashboardAgentRow) so users cannot clear
* them; activation only focuses the parent pane (`activationPaneKey`),
* which looks like a no-op when the parent is already active.
*
* Re-run:
* pnpm exec vitest run --config config/vitest.config.ts \
* src/renderer/src/components/sidebar/repro-8593-idle-subagent-rows.test.ts
*/
import { readFileSync } from 'node:fs'
import { join } from 'node:path'
import { describe, expect, it } from 'vitest'
import { buildSubagentChildRows } from './worktree-subagent-child-rows'
import type { AgentStatusEntry } from '../../../../shared/agent-status-types'
import type { TerminalTab } from '../../../../shared/types'
const serverSource = readFileSync(join(__dirname, '../../../../main/agent-hooks/server.ts'), 'utf8')
const dashboardRowSource = readFileSync(
join(__dirname, '../dashboard/DashboardAgentRow.tsx'),
'utf8'
)
function parentEntry(overrides: Partial<AgentStatusEntry> = {}): AgentStatusEntry {
return {
state: 'working',
prompt: 'run cypress suite',
updatedAt: 2000,
stateStartedAt: 1000,
agentType: 'claude',
paneKey: 'tab-1:0',
worktreeId: 'wt-1',
tabId: 'tab-1',
stateHistory: [],
subagents: [
{
id: 'cy-1',
state: 'idle',
startedAt: 1100,
agentType: 'general-purpose',
description: 'cypress: login'
},
{
id: 'cy-2',
state: 'idle',
startedAt: 1200,
agentType: 'general-purpose',
description: 'cypress: checkout'
},
{
id: 'cy-3',
state: 'working',
startedAt: 1300,
agentType: 'general-purpose',
description: 'cypress: pay'
}
],
...overrides
}
}
const tab = { id: 'tab-1', worktreeId: 'wt-1' } as TerminalTab
describe('#8593 lingering idle subagent child rows', () => {
it('renders idle subagents as sidebar child rows (not filtered out)', () => {
const rows = buildSubagentChildRows({
parentEntry: parentEntry(),
tab,
parentIsFresh: true
})
expect(rows).toHaveLength(3)
const idle = rows.filter((r) => r.state === 'idle')
const working = rows.filter((r) => r.state === 'working')
expect(idle).toHaveLength(2)
expect(working).toHaveLength(1)
// Accumulates every finished Task/subagent for the session.
expect(idle.map((r) => r.entry.prompt)).toEqual(['cypress: login', 'cypress: checkout'])
})
it('activation only targets the parent pane (no independent surface)', () => {
const rows = buildSubagentChildRows({
parentEntry: parentEntry(),
tab,
parentIsFresh: true
})
for (const row of rows) {
expect(row.activationPaneKey).toBe('tab-1:0')
expect(row.tab.id).toBe('tab-1')
// Synthetic key cannot be a real pane identity.
expect(row.paneKey).toContain('\u0000subagent:')
}
})
it('dashboard hides dismiss for subagent rows (cannot clear clutter)', () => {
expect(dashboardRowSource).toMatch(/hideDismiss=\{agent\.rowSource === 'subagent'\}/)
})
it('only Claude hydrate prunes idle children — live path keeps them', () => {
expect(serverSource).toMatch(/function dropHydratedIdleClaudeSubagents/)
// Prune is applied only when hydrating last-status from disk, not on every
// live status apply.
expect(serverSource).toMatch(
/const hydratedPayload = dropHydratedIdleClaudeSubagents\(entry\.payload\)/
)
// Live interrupt path deliberately keeps idle children as display state.
expect(serverSource).toMatch(
/idle children are display state[\s\S]{0,200}\.\.\.\(payload\.subagents \? \{ subagents: payload\.subagents/
)
})
it('stale parent decays working children to idle (more clutter, not fewer)', () => {
const rows = buildSubagentChildRows({
parentEntry: parentEntry({
subagents: [{ id: 'a1', state: 'working', startedAt: 1000 }]
}),
tab,
parentIsFresh: false
})
expect(rows).toHaveLength(1)
expect(rows[0].state).toBe('idle')
})
})
@@ -0,0 +1,102 @@
/**
* Issue #8752 — Setup-script prompt remains visible after orca.yaml setup hook exists.
*
* Root cause (renderer invalidation, shared across OS — not Linux-only):
* 1. SetupScriptPromptCard re-inspects only on activeRepo / inspectionRetryKey /
* isDismissed / settings / sidebarOpen — not on orca.yaml changes, runtime
* reconnect, or same-repo worktree activation.
* 2. getRenderedSetupScriptPromptState keeps lastVisiblePrompt for the same
* project while promptState is null (pending), so a stale negative result
* continues to render across same-project worktree switches.
*
* Detection itself (hasEffectiveSetupCommand) correctly accepts shared
* scripts.setup; the bug is stale UI state, not hook parsing.
*
* Re-run:
* pnpm exec vitest run --config config/vitest.config.ts \
* src/renderer/src/components/sidebar/repro-8752-setup-script-prompt-stale.test.ts
*/
import { readFileSync } from 'node:fs'
import { join } from 'node:path'
import { describe, expect, it } from 'vitest'
import { hasEffectiveSetupCommand } from '@/lib/setup-script-prompt'
import { getRenderedSetupScriptPromptState } from './setup-script-prompt-render-state'
import type { SetupScriptPromptInspection } from '@/lib/setup-script-prompt'
import type { HookCheckResult } from '@/runtime/runtime-hooks-client'
import type { Repo } from '../../../../../shared/types'
const cardSource = readFileSync(join(__dirname, 'SetupScriptPromptCard.tsx'), 'utf8')
function negativePrompt(repoId: string): SetupScriptPromptInspection {
return {
status: 'ok',
repoId,
hasEffectiveSetup: false,
hasSharedHooks: false,
candidate: null
}
}
function positivePrompt(repoId: string): SetupScriptPromptInspection {
return {
status: 'ok',
repoId,
hasEffectiveSetup: true,
hasSharedHooks: true,
candidate: null
}
}
describe('#8752 setup-script prompt stale after orca.yaml setup hook', () => {
it('hasEffectiveSetupCommand accepts shared orca.yaml scripts.setup', () => {
const repo = {
id: 'repo-1',
hookSettings: undefined
} as Repo
const hooksResult: HookCheckResult = {
status: 'ok',
hasHooks: true,
hooks: { scripts: { setup: './scripts/setup-orca-worktree.sh' } },
mayNeedUpdate: false
}
expect(hasEffectiveSetupCommand(repo, hooksResult)).toBe(true)
})
it('inspection effect deps omit orca.yaml / runtime / same-repo worktree signals', () => {
// The bug: only these five deps re-run inspection.
expect(cardSource).toMatch(
/\[activeRepo, inspectionRetryKey, isDismissed, settings, sidebarOpen\]/
)
// No invalidation from filesystem or runtime generation
expect(cardSource).not.toMatch(/orca\.yaml/)
expect(cardSource).not.toMatch(/runtimeGeneration|runtimeReconnect|hooksChanged/)
})
it('retains last-visible negative prompt while same-project re-inspection is pending', () => {
const staleNegative = negativePrompt('repo-primary')
const rendered = getRenderedSetupScriptPromptState({
promptState: null, // mid-inspection after worktree switch
activeRepoId: 'repo-worktree',
activeProjectId: 'github:org/repo',
lastVisiblePrompt: { state: staleNegative, projectId: 'github:org/repo' }
})
// Bug: stale "Add a setup script" stays on screen even if hooks now exist
expect(rendered).toBe(staleNegative)
expect(rendered?.hasEffectiveSetup).toBe(false)
})
it('would hide the prompt only after a fresh positive inspection lands', () => {
const fresh = positivePrompt('repo-worktree')
expect(
getRenderedSetupScriptPromptState({
promptState: fresh,
activeRepoId: 'repo-worktree',
activeProjectId: 'github:org/repo',
lastVisiblePrompt: {
state: negativePrompt('repo-primary'),
projectId: 'github:org/repo'
}
})
).toEqual(fresh)
})
})
@@ -0,0 +1,72 @@
/**
* Issue #8372 — outdated running CLI count on bottom panel (closed badge).
*
* Closed badge uses store-bound PTY IDs only (never daemon listSessions).
* Open popover uses sessions.length from the live daemon inventory.
* After killing orphans (or any daemon-only sessions), open is correct while
* the closed badge can still report dead store bindings. Restart keeps the
* mismatch if session state rehydrates those PTY IDs.
*
* Re-run:
* pnpm exec vitest run --config config/vitest.config.ts \
* src/renderer/src/components/status-bar/repro-8372-closed-cli-count-stale.test.ts
*/
import { readFileSync } from 'node:fs'
import { join } from 'node:path'
import { describe, expect, it } from 'vitest'
import { createClosedResourceSessionCountSelector } from './resource-session-count-selector'
import type { TerminalTab } from '../../../../shared/types'
function makeTab(id: string, ptyId: string | null): TerminalTab {
return {
id,
ptyId,
worktreeId: `wt-${id}`,
title: 'Terminal',
customTitle: null,
color: null,
sortOrder: 0,
createdAt: 0
}
}
describe('issue #8372 closed CLI count diverges from live daemon sessions', () => {
it('closed selector counts bound store PTYs, not live daemon inventory', () => {
const selectCount = createClosedResourceSessionCountSelector()
const state = {
tabsByWorktree: {
'wt-1': [makeTab('tab-1', 'pty-alive'), makeTab('tab-2', 'pty-dead-orphan-binding')]
},
ptyIdsByTabId: {
'tab-1': ['pty-alive']
},
terminalLayoutsByTabId: {},
workspaceSessionReady: true
}
// Store claims 2 bound PTYs (tab.ptyId wake hints + live map).
expect(selectCount(state)).toBe(2)
// Live daemon after "clear orphans" only has 1 session — open popover path.
const liveDaemonSessions = [{ id: 'pty-alive' }]
const openPopoverCount = liveDaemonSessions.length
expect(openPopoverCount).toBe(1)
expect(selectCount(state)).not.toBe(openPopoverCount)
})
it('ResourceUsageStatusSegment dual-sources open vs closed counts', () => {
const source = readFileSync(join(__dirname, 'ResourceUsageStatusSegment.tsx'), 'utf8')
expect(source).toMatch(/triggerSessionCount = open \? sessions\.length : closedSessionCount/)
expect(source).toMatch(/createClosedResourceSessionCountSelector/)
// Closed path intentionally never lists daemon sessions.
expect(source).toMatch(/closed badge never reintroduces a background global session scan/)
})
it('kill-orphan path does not clear store tab/layout PTY bindings', () => {
const source = readFileSync(join(__dirname, 'ResourceUsageStatusSegment.tsx'), 'utf8')
// Optimistic local sessions filter + pty.kill only — no store drop of bindings.
expect(source).toMatch(/handleKillOrphans/)
expect(source).toMatch(/setSessions\(\(prev\) => prev\.filter/)
expect(source).not.toMatch(/dropPty|clearPtyBinding|removePtyIdFromTab/)
})
})

Some files were not shown because too many files have changed in this diff Show More