mirror of
https://github.com/stablyai/orca.git
synced 2026-10-18 16:03:17 +00:00
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:
@@ -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`
|
||||
@@ -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.
|
||||
@@ -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`
|
||||
@@ -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
|
||||
```
|
||||
@@ -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`
|
||||
@@ -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).
|
||||
@@ -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`)
|
||||
@@ -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.
|
||||
@@ -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.
|
||||
@@ -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.
|
||||
@@ -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).
|
||||
@@ -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.
|
||||
@@ -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.)
|
||||
@@ -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.
|
||||
@@ -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.
|
||||
@@ -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.
|
||||
@@ -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`
|
||||
@@ -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.
|
||||
@@ -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.
|
||||
@@ -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.
|
||||
@@ -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
|
||||
```
|
||||
@@ -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.
|
||||
@@ -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).
|
||||
@@ -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`.
|
||||
@@ -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`
|
||||
@@ -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
|
||||
```
|
||||
@@ -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)
|
||||
@@ -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
|
||||
```
|
||||
@@ -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`)
|
||||
@@ -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`
|
||||
@@ -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.
|
||||
@@ -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`
|
||||
@@ -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.
|
||||
@@ -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.
|
||||
@@ -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.
|
||||
@@ -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)
|
||||
@@ -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).
|
||||
@@ -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.
|
||||
@@ -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).
|
||||
@@ -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`
|
||||
@@ -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 (`<br/>`) 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 <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.
|
||||
@@ -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.
|
||||
@@ -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.
|
||||
@@ -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.
|
||||
@@ -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.
|
||||
@@ -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
|
||||
```
|
||||
@@ -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`
|
||||
@@ -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.
|
||||
@@ -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)
|
||||
@@ -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`
|
||||
@@ -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.
|
||||
@@ -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.
|
||||
@@ -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).
|
||||
@@ -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`
|
||||
@@ -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.
|
||||
@@ -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.
|
||||
@@ -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.
|
||||
@@ -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(/<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
Reference in New Issue
Block a user