Repository navigation
Conversation
📝 WalkthroughWalkthroughThe changes add WSL OpenCode materialization and guest environment handling, including relay instance-key resolution and shell startup integration. OpenCode hook processing now handles root sessions, streaming message parts, blockers, retries, recovery, and disposal. Agent and worktree status models add blocked and interrupted states, provider ownership, unread activity classification, semantic status colors, tooltip control, and accessibility metadata. Renderer components, localization, unit tests, and end-to-end coverage are updated for the new status behavior. 🚥 Pre-merge checks | ✅ 3 | ❌ 2❌ Failed checks (2 warnings)
✅ Passed checks (3 passed)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Actionable comments posted: 5
🧹 Nitpick comments (1)
tests/e2e/opencode-agent-status-chrome.spec.ts (1)
446-446: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winBuild the seeded file path with
path.join.This literal assumes
/, contrary to the repository’s cross-platform path requirement.Proposed fix
- toolInput: 'src/renderer/src/components/tab-bar/TerminalTabLeadingIcon.tsx' + toolInput: join( + 'src', + 'renderer', + 'src', + 'components', + 'tab-bar', + 'TerminalTabLeadingIcon.tsx' + )As per coding guidelines: “Use
path.joinor Electron/Node path utilities for file paths; never assume/or\.”Source: Coding guidelines
ℹ️ Review info
⚙️ Run configuration
Configuration used: Repository UI
Review profile: CHILL
Plan: Pro
Run ID: 43a74d8c-923c-4330-bf31-4822d14982cb
📒 Files selected for processing (86)
docs/STYLEGUIDE.mdsrc/main/agent-hooks/wsl-hook-relay-instance-key.tssrc/main/agent-hooks/wsl-hook-relay-launch.tssrc/main/agent-hooks/wsl-hook-relay-manager.test.tssrc/main/agent-hooks/wsl-hook-relay-manager.tssrc/main/daemon/pty-subprocess.test.tssrc/main/daemon/shell-ready.test.tssrc/main/daemon/shell-ready.tssrc/main/ipc/pty.test.tssrc/main/ipc/pty.tssrc/main/opencode/hook-plugin-authoritative-recovery.test.tssrc/main/opencode/hook-plugin-message-part-throttle.test.tssrc/main/opencode/hook-plugin-status-transitions.test.tssrc/main/opencode/hook-service.test.tssrc/main/opencode/hook-service.tssrc/main/providers/local-pty-shell-ready.tssrc/main/pty/wsl-opencode-host-materializer.test.tssrc/main/pty/wsl-opencode-shell-materializer-block.tssrc/main/pty/wsl-opencode-shell-materializer.test.tssrc/main/pty/wsl-opencode-shell-materializer.tssrc/main/pty/wsl-orca-env.test.tssrc/main/pty/wsl-orca-env.tssrc/relay/wsl-agent-hook-relay.test.tssrc/renderer/src/assets/main.csssrc/renderer/src/components/AgentStateDot.test.tssrc/renderer/src/components/AgentStateDot.tsxsrc/renderer/src/components/activity/ActivityPrototypePage.tsxsrc/renderer/src/components/dashboard/DashboardAgentRow.test.tsxsrc/renderer/src/components/dashboard/DashboardAgentRow.tsxsrc/renderer/src/components/dashboard/DashboardAgentRowToolStep.tsxsrc/renderer/src/components/feature-tips/CmdJPaletteFeatureTipVisual.test.tsxsrc/renderer/src/components/feature-tips/CmdJPaletteFeatureTipVisual.tsxsrc/renderer/src/components/feature-wall/FeatureTourWorkspaceCard.tsxsrc/renderer/src/components/feature-wall/TasksAnimatedVisual.tsxsrc/renderer/src/components/feature-wall/WorkspacesAnimatedVisual.tsxsrc/renderer/src/components/feature-wall/agents-orchestration/StatusesPage.tsxsrc/renderer/src/components/feature-wall/agents-orchestration/orchestration-cards.tsxsrc/renderer/src/components/feature-wall/feature-tour-preview-glyphs.tsxsrc/renderer/src/components/floating-terminal/FloatingTerminalToggleButton.tsxsrc/renderer/src/components/sidebar/StatusIndicator.test.tssrc/renderer/src/components/sidebar/StatusIndicator.tsxsrc/renderer/src/components/sidebar/WorktreeActivityStatusIndicator.test.tsxsrc/renderer/src/components/sidebar/WorktreeCard.compact-hover.test.tsxsrc/renderer/src/components/sidebar/WorktreeCard.pr-display.test.tsxsrc/renderer/src/components/sidebar/WorktreeCard.tsxsrc/renderer/src/components/sidebar/WorktreeCardAgents.test.tsxsrc/renderer/src/components/sidebar/WorktreeCardMeta.test.tsxsrc/renderer/src/components/sidebar/WorktreeCardMeta.tsxsrc/renderer/src/components/sidebar/WorktreeCardStatusSlot.test.tsxsrc/renderer/src/components/sidebar/WorktreeCardStatusSlot.tsxsrc/renderer/src/components/sidebar/use-worktree-activity-status.test.tsxsrc/renderer/src/components/sidebar/use-worktree-activity-status.tssrc/renderer/src/components/sidebar/worktree-agent-activity-summary.test.tssrc/renderer/src/components/sidebar/worktree-agent-activity-summary.tssrc/renderer/src/components/sidebar/worktree-card-compact-agent-row.tsxsrc/renderer/src/components/sidebar/worktree-card-meta-types.tssrc/renderer/src/components/sidebar/worktree-section-activity.test.tssrc/renderer/src/components/sidebar/worktree-section-activity.tssrc/renderer/src/components/status-bar/StatusBar.tsxsrc/renderer/src/components/tab-bar/SortableTab.rename-shortcut.test.tsxsrc/renderer/src/components/tab-bar/SortableTab.tsxsrc/renderer/src/components/tab-bar/TerminalTabLeadingIcon.test.tsxsrc/renderer/src/components/tab-bar/TerminalTabLeadingIcon.tsxsrc/renderer/src/components/tab-bar/tab-title-tooltip.test.tsxsrc/renderer/src/components/tab-bar/terminal-tab-activity-status.test.tssrc/renderer/src/components/tab-bar/terminal-tab-activity-status.tssrc/renderer/src/components/tab-bar/use-terminal-tab-status-presentation.tssrc/renderer/src/components/ui/tooltip.tsxsrc/renderer/src/i18n/locales/en.jsonsrc/renderer/src/i18n/locales/es.jsonsrc/renderer/src/i18n/locales/ja.jsonsrc/renderer/src/i18n/locales/ko.jsonsrc/renderer/src/i18n/locales/zh.jsonsrc/renderer/src/lib/agent-icon-glyphs.tsxsrc/renderer/src/lib/agent-status.test.tssrc/renderer/src/lib/agent-status.tssrc/renderer/src/lib/worktree-activity-state.test.tssrc/renderer/src/lib/worktree-activity-state.tssrc/renderer/src/lib/worktree-status.test.tssrc/renderer/src/lib/worktree-status.tssrc/shared/agent-hook-listener.test.tssrc/shared/agent-hook-listener.tssrc/shared/wsl-login-shell-command.test.tssrc/shared/wsl-login-shell-command.tssrc/shared/wsl-opencode-materializer-contract.tstests/e2e/opencode-agent-status-chrome.spec.ts
| 'async function getSessionByID(client, sessionID) {', | ||
| ' const shapes = [{ path: { id: sessionID } }, { sessionID }];', | ||
| ' for (const input of shapes) {', | ||
| ' try {', | ||
| ' const response = await client.session.get(input);', | ||
| ' if (response?.data?.id) return response.data;', | ||
| ' } catch {', | ||
| ' // Try the other SDK generation before falling back to session.list.', | ||
| ' }', | ||
| ' }', | ||
| ' return null;', | ||
| '}', | ||
| '', | ||
| 'async function resolveRootSessionID(client, sessionID) {', | ||
| ' if (!sessionID) return null;', | ||
| ' if (rootSessionById.has(sessionID)) return rootSessionById.get(sessionID);', | ||
| ' if (!client?.session) return null;', | ||
| ' // Prefer point lookups: session.list is paginated in current OpenCode,', | ||
| " // so a child's older root may not appear in the default first page.", | ||
| ' if (client.session.get) {', | ||
| ' try {', | ||
| ' const ancestry = [];', | ||
| ' const seen = new Set();', | ||
| ' let currentID = sessionID;', | ||
| ' while (currentID && seen.size < 128) {', | ||
| ' if (seen.has(currentID)) throw new Error("cyclic session lineage");', | ||
| ' seen.add(currentID);', | ||
| ' ancestry.push(currentID);', | ||
| ' if (rootSessionById.has(currentID)) {', | ||
| ' const cachedRoot = rootSessionById.get(currentID);', | ||
| ' for (const id of ancestry) rememberRootSession(id, cachedRoot);', | ||
| ' return cachedRoot;', | ||
| ' }', | ||
| ' const session = await getSessionByID(client, currentID);', | ||
| ' if (!session?.id) throw new Error("session lookup failed");', | ||
| ' if (!session.parentID) {', | ||
| ' for (const id of ancestry) rememberRootSession(id, session.id);', | ||
| ' return session.id;', | ||
| ' }', | ||
| ' currentID = session.parentID;', | ||
| ' }', | ||
| ' } catch {', | ||
| ' // Older SDKs and transient lookup failures fall back to list below.', | ||
| ' }', | ||
| ' }', | ||
| ' if (!client.session.list) return null;', | ||
| ' try {', | ||
| ' const sessions = await client.session.list();', | ||
| ' const list = Array.isArray(sessions?.data) ? sessions.data : [];', | ||
| ' const session = list.find((entry) => entry?.id === sessionID);', | ||
| ' const isChild = !!session?.parentID;', | ||
| ' if (childSessionById.size >= 128) {', | ||
| ' const first = childSessionById.keys().next().value;', | ||
| ' if (first !== undefined) childSessionById.delete(first);', | ||
| ' const response = await client.session.list();', |
There was a problem hiding this comment.
🩺 Stability & Availability | 🟠 Major | ⚡ Quick win
🧩 Analysis chain
🏁 Script executed:
#!/bin/sh
set -eu
rg -n -C4 'client\.session\.(get|list)\(' src/main/opencode/hook-service.ts
fd -i '^(package\.json|pnpm-lock\.yaml|yarn\.lock|package-lock\.json)$' \
-x rg -n '`@opencode`|opencode.*sdk|AbortSignal|signal:' {}Repository: stablyai/orca
Length of output: 1068
🏁 Script executed:
#!/bin/sh
set -eu
# Inspect the relevant section with surrounding context.
sed -n '380,560p' src/main/opencode/hook-service.ts
printf '\n--- eventQueue references ---\n'
rg -n -C3 'eventQueue|post\(' src/main/opencode/hook-service.ts
printf '\n--- session lookup / timeout references in repo ---\n'
rg -n -C3 'AbortSignal|signal:|timeout|setTimeout|Promise\.race|withTimeout|deadline' src/main/opencode src | sed -n '1,220p'Repository: stablyai/orca
Length of output: 23711
🏁 Script executed:
#!/bin/sh
set -eu
printf '%s\n' '--- resolveRootSessionID callsites ---'
rg -n -C4 'resolveRootSessionID\(' src/main/opencode/hook-service.ts
printf '\n%s\n' '--- post() implementation around timeout handling ---'
sed -n '640,760p' src/main/opencode/hook-service.ts
printf '\n%s\n' '--- any helper that wraps client.* with timeout ---'
rg -n -C3 'timeoutMs|Promise\.race|withTimeout|AbortSignal|signal:' src/main/opencode/hook-service.ts src/main/opencodeRepository: stablyai/orca
Length of output: 7401
Add a deadline to root-session lookups before they enter the queue. resolveRootSessionID() awaits client.session.get()/list() inside the global eventQueue, so a hung SDK call stalls later factory/lifecycle work. Use the same short timeout pattern as post() or skip the lookup on timeout.
| '__orca_materialize_wsl_opencode() {', | ||
| ` local __orca_plugin_hash='${pluginHash}'`, | ||
| ' local __orca_home="${HOME:-}"', | ||
| ' local __orca_source_dir=""', | ||
| ' local __orca_mirror_source_dir=""', | ||
| ' local __orca_source_key="default"', | ||
| ' local __orca_default_config_dir=""', | ||
| ' local __orca_source_real=""', | ||
| ' local __orca_default_real=""', | ||
| ' local __orca_overlay_root=""', | ||
| ' local __orca_overlay_dir=""', | ||
| ' local __orca_plugins_dir=""', | ||
| ' local __orca_entry=""', | ||
| ' local __orca_name=""', | ||
| ' local __orca_target=""', | ||
| ' local __orca_tmp=""', | ||
| ' local __orca_link_value=""', | ||
| ' local __orca_instance_key=""', |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟠 Major | ⚡ Quick win
🧩 Analysis chain
🏁 Script executed:
#!/bin/sh
set -eu
for name in ksh ksh93; do
shell=$(command -v "$name" 2>/dev/null || true)
[ -n "$shell" ] || continue
printf '%s: ' "$shell"
if "$shell" -c 'f() { local value=1; }; f' >/dev/null 2>&1; then
echo 'local supported'
else
echo 'local unsupported'
fi
done
rg -n -C2 'local __orca_|AVAILABLE_POSIX_SHELLS' \
src/main/pty/wsl-opencode-shell-materializer.ts \
src/main/pty/wsl-opencode-shell-materializer.test.tsRepository: stablyai/orca
Length of output: 3814
🏁 Script executed:
#!/bin/sh
set -eu
printf '\n== materializer ==\n'
sed -n '1,220p' src/main/pty/wsl-opencode-shell-materializer.ts
printf '\n== tests ==\n'
sed -n '1,220p' src/main/pty/wsl-opencode-shell-materializer.test.ts
printf '\n== ksh mentions ==\n'
rg -n '\bksh\b|ksh93|typeset|local ' src/main/ptyRepository: stablyai/orca
Length of output: 20536
🏁 Script executed:
#!/bin/sh
set -eu
nl -ba src/main/pty/wsl-opencode-shell-materializer.ts | sed -n '1,220p'Repository: stablyai/orca
Length of output: 187
🌐 Web query:
ksh93 local builtin function scope documentation
💡 Result:
In ksh93, local variables are supported exclusively within functions defined using the function name syntax [1][2]. To create a local variable, use the typeset special built-in command inside such a function [1][3]. The following key points summarize the behavior and scope of local variables in ksh93: Scope and Mechanism: Ksh93 employs static scoping for variables declared with typeset [4][5]. This means that local variables are confined to the function in which they are defined and are not visible to functions called by that function [5]. This behavior differs from older shell implementations (like ksh88) that used dynamic scoping [4][5]. Function Declaration Syntax: 1. Function name syntax (e.g., function myfunc {... }): These functions run in a separate execution environment that supports local variables created via typeset [1][2]. 2. Name syntax (e.g., myfunc {... }): These functions execute in the caller's environment and do not support local variables [1][2]. Variables modified or created within these functions are global to the shell [6][7]. Passing Data: Because ksh93 uses static scoping, called functions do not automatically inherit access to a parent's local variables [5]. To share specific variables with called functions, you can pass them as arguments or utilize namerefs (created with typeset -n) [1][8]. When a nameref is assigned to a variable name, it allows the called function to access and modify the parent's variable [8]. Summary Table: Function Definition Style | Local Variables Supported | Scope Model --- | --- | --- function name {... } | Yes (via typeset) | Static name {... } | No | Global Note: The typeset command is a special built-in that must be used within the function body to establish the local scope [1][7]. If a variable is assigned a value without being declared by typeset inside a function defined with the function keyword, it remains a global variable [7].
Citations:
- 1: https://manpages.ubuntu.com/manpages/trusty/man1/ksh93.1.html
- 2: https://www.ibm.com/docs/en/aix/7.3.0?topic=shell-enhanced-korn-ksh93
- 3: https://man.omnios.org/man1/ksh93
- 4: https://unix.stackexchange.com/questions/375156/typeset-in-ksh93-not-working-as-expected
- 5: https://lists.endsoftwarepatents.org/archive/html/bug-bash/2018-03/msg00034.html
- 6: https://illumos.org/man/1/ksh
- 7: https://stackoverflow.com/questions/12000949/scope-of-variables-in-ksh
- 8: https://manpages.debian.org/man/1/ksh93
🏁 Script executed:
#!/bin/sh
set -eu
rg -n '\bksh93?\b|KSH|Korn|shells? support|login-shell' \
src test . \
--glob '!**/dist/**' --glob '!**/build/**' --glob '!**/node_modules/**' | sed -n '1,200p'Repository: stablyai/orca
Length of output: 8616
Make the shell declarations portable or exclude ksh explicitly. __orca_materialize_wsl_opencode() uses local throughout, which breaks in shells that don’t support it; if ksh93 is part of the login-shell path, switch these bindings to typeset/portable assignments.
📍 Affects 2 files
src/main/pty/wsl-opencode-shell-materializer.ts#L74-L91(this comment)src/main/pty/wsl-opencode-shell-materializer.test.ts#L34-L44
| // Keep labels above those floating surfaces instead of hidden behind them. | ||
| className={cn( | ||
| 'pointer-events-none z-[90] w-fit origin-(--radix-tooltip-content-transform-origin) animate-in rounded-md bg-foreground px-3 py-1.5 text-xs text-balance text-background fade-in-0 zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95', | ||
| 'pointer-events-none z-[90] w-fit origin-(--radix-tooltip-content-transform-origin) animate-in rounded-md bg-foreground px-3 py-1.5 text-xs text-balance text-background fade-in-0 zoom-in-95 motion-reduce:animate-none data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95', |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win
🧩 Analysis chain
🏁 Script executed:
rg -n 'motion-reduce:animate-none|data-\[state=closed\]:animate-out' src/renderer/src/components/ui/tooltip.tsxRepository: stablyai/orca
Length of output: 634
🏁 Script executed:
#!/bin/bash
set -euo pipefail
printf '## tooltip.tsx\n'
cat -n src/renderer/src/components/ui/tooltip.tsx | sed -n '1,120p'
printf '\n## tailwind-related files\n'
fd -a -t f 'tailwind*|postcss*|vite*|css' src/renderer -E 'node_modules' | sed -n '1,120p'
printf '\n## search for animate-out / motion-reduce / tailwindcss-animate\n'
rg -n 'animate-out|motion-reduce:animate-none|tailwindcss-animate|prefers-reduced-motion' src/renderer -g '!**/node_modules/**'Repository: stablyai/orca
Length of output: 17108
🏁 Script executed:
#!/bin/bash
set -euo pipefail
python3 - <<'PY'
from pathlib import Path
p = Path('src/renderer/src/components/ui/tooltip.tsx')
print('exists', p.exists())
PYRepository: stablyai/orca
Length of output: 163
🏁 Script executed:
#!/bin/bash
set -euo pipefail
sed -n '1,220p' src/renderer/src/components/ui/tooltip.tsxRepository: stablyai/orca
Length of output: 2401
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo '## tooltip.tsx'
sed -n '1,120p' src/renderer/src/components/ui/tooltip.tsx
echo
echo '## tailwind config candidates'
fd -a -t f 'tailwind.config.*|postcss.config.*|vite.config.*' . | sed -n '1,80p'
echo
echo '## motion-reduce / data-state usage'
rg -n 'motion-reduce:animate-none|data-\[state=closed\]:animate-out|prefers-reduced-motion|tailwindcss-animate' . -g '!**/node_modules/**' -g '!**/dist/**' -g '!**/build/**'Repository: stablyai/orca
Length of output: 13487
🏁 Script executed:
#!/bin/bash
set -euo pipefail
printf '## generated motion-reduce utilities\n'
rg -n --context 2 'motion-reduce\\:animate-none|data-\\\[state\\=closed\\]\\:animate-out|data-\\\[state\\=open\\]\\:animate-in' src/renderer/src/assets/main.css
printf '\n## nearby reduced-motion block\n'
sed -n '1470,1515p' src/renderer/src/assets/main.css
printf '\n## nearby animation block\n'
sed -n '1,120p' src/renderer/src/assets/main.cssRepository: stablyai/orca
Length of output: 188
🏁 Script executed:
#!/bin/bash
set -euo pipefail
printf '## motion and animation rules in main.css\n'
rg -n 'prefers-reduced-motion|animate-none|animate-out|animate-in|fade-out-0|zoom-out-95|fade-in-0|zoom-in-95' src/renderer/src/assets/main.css
printf '\n## around the reduced-motion sections\n'
for line in 1470 1550 1670 2190 2270 2620 3120; do
start=$((line-12))
end=$((line+28))
printf '\n--- main.css:%s-%s ---\n' "$start" "$end"
sed -n "${start},${end}p" src/renderer/src/assets/main.css
doneRepository: stablyai/orca
Length of output: 7246
🏁 Script executed:
#!/bin/bash
set -euo pipefail
printf '## package.json excerpts\n'
sed -n '1,220p' package.json
printf '\n## renderer/package.json if present\n'
if [ -f src/renderer/package.json ]; then
sed -n '1,220p' src/renderer/package.json
fi
printf '\n## tailwind-related config files\n'
fd -a -t f 'tailwind.config.*|postcss.config.*|electron.vite.config.*|vite.config.*' . | sed -n '1,80p'Repository: stablyai/orca
Length of output: 14904
Disable the closed-state animation under reduced motion motion-reduce:animate-none only covers the base tooltip animation; data-[state=closed]:animate-out still applies on dismiss. Add a reduced-motion override for the closed state too.
| if (theme === 'light' && expectedStatus === 'inactive') { | ||
| const contrast = await readInactiveOpenCodeMarkContrast(identity) | ||
| expect(contrast.opacity).toBe('1') | ||
| expect(contrast.color).toBe(contrast.mutedColor) | ||
| expect(contrast.ratio).toBeGreaterThanOrEqual(3) | ||
| } |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win
Validate inactive icon contrast in dark mode too.
The theme guard leaves dark-mode OpenCode branding contrast untested even though both themes run.
Proposed fix
- if (theme === 'light' && expectedStatus === 'inactive') {
+ if (expectedStatus === 'inactive') {📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| if (theme === 'light' && expectedStatus === 'inactive') { | |
| const contrast = await readInactiveOpenCodeMarkContrast(identity) | |
| expect(contrast.opacity).toBe('1') | |
| expect(contrast.color).toBe(contrast.mutedColor) | |
| expect(contrast.ratio).toBeGreaterThanOrEqual(3) | |
| } | |
| if (expectedStatus === 'inactive') { | |
| const contrast = await readInactiveOpenCodeMarkContrast(identity) | |
| expect(contrast.opacity).toBe('1') | |
| expect(contrast.color).toBe(contrast.mutedColor) | |
| expect(contrast.ratio).toBeGreaterThanOrEqual(3) | |
| } |
| // Why: unread is a passive notification over successful completion; | ||
| // destructive interruption must keep its newer, more specific red glyph. | ||
| await seedAgentStatus(orcaPage, primaryPaneKey, worktreeId, tabId, DONE) | ||
| await orcaPage.evaluate((targetTabId) => { | ||
| window.__store?.getState().markTerminalTabUnread(targetTabId) | ||
| }, tabId) | ||
| const tab = terminalTab(orcaPage, tabId) | ||
| const bell = tab.locator('[data-testid="tab-activity-bell"]') | ||
| await expect(bell).toBeVisible() | ||
| await expect(bell).toHaveAttribute('data-unread-kind', 'terminal-activity') | ||
| await expectSingleAccessibleLabel(tab, 'OpenCode · Unread terminal activity') | ||
| expectFixedGeometry(await readLeadingGeometry(tab, bell), quietGeometry) | ||
| await captureEvidence(tab, worktreeRow(orcaPage, worktreeId), theme, 'unread-terminal') | ||
|
|
||
| await orcaPage.evaluate((targetPaneKey) => { | ||
| window.__store?.getState().markAgentCompletionPaneUnread(targetPaneKey) | ||
| }, primaryPaneKey) | ||
| await expect(bell).toHaveAttribute('data-unread-kind', 'agent-completion') | ||
| await expectSingleAccessibleLabel(tab, 'OpenCode · Unread agent completion') | ||
| expectFixedGeometry(await readLeadingGeometry(tab, bell), quietGeometry) | ||
| await captureEvidence(tab, worktreeRow(orcaPage, worktreeId), theme, 'unread-completion') |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win
Assert that unread notifications preserve the sidebar’s Done outcome.
Screenshots are optional and unverified, so a regression that rewrites the sidebar status after either unread action would still pass.
Proposed fix
await expectSingleAccessibleLabel(tab, 'OpenCode · Unread terminal activity')
+ await expectCardLabel(statusSlot(orcaPage, worktreeId), 'Done')
expectFixedGeometry(await readLeadingGeometry(tab, bell), quietGeometry)
await expect(bell).toHaveAttribute('data-unread-kind', 'agent-completion')
await expectSingleAccessibleLabel(tab, 'OpenCode · Unread agent completion')
+ await expectCardLabel(statusSlot(orcaPage, worktreeId), 'Done')📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| // Why: unread is a passive notification over successful completion; | |
| // destructive interruption must keep its newer, more specific red glyph. | |
| await seedAgentStatus(orcaPage, primaryPaneKey, worktreeId, tabId, DONE) | |
| await orcaPage.evaluate((targetTabId) => { | |
| window.__store?.getState().markTerminalTabUnread(targetTabId) | |
| }, tabId) | |
| const tab = terminalTab(orcaPage, tabId) | |
| const bell = tab.locator('[data-testid="tab-activity-bell"]') | |
| await expect(bell).toBeVisible() | |
| await expect(bell).toHaveAttribute('data-unread-kind', 'terminal-activity') | |
| await expectSingleAccessibleLabel(tab, 'OpenCode · Unread terminal activity') | |
| expectFixedGeometry(await readLeadingGeometry(tab, bell), quietGeometry) | |
| await captureEvidence(tab, worktreeRow(orcaPage, worktreeId), theme, 'unread-terminal') | |
| await orcaPage.evaluate((targetPaneKey) => { | |
| window.__store?.getState().markAgentCompletionPaneUnread(targetPaneKey) | |
| }, primaryPaneKey) | |
| await expect(bell).toHaveAttribute('data-unread-kind', 'agent-completion') | |
| await expectSingleAccessibleLabel(tab, 'OpenCode · Unread agent completion') | |
| expectFixedGeometry(await readLeadingGeometry(tab, bell), quietGeometry) | |
| await captureEvidence(tab, worktreeRow(orcaPage, worktreeId), theme, 'unread-completion') | |
| // Why: unread is a passive notification over successful completion; | |
| // destructive interruption must keep its newer, more specific red glyph. | |
| await seedAgentStatus(orcaPage, primaryPaneKey, worktreeId, tabId, DONE) | |
| await orcaPage.evaluate((targetTabId) => { | |
| window.__store?.getState().markTerminalTabUnread(targetTabId) | |
| }, tabId) | |
| const tab = terminalTab(orcaPage, tabId) | |
| const bell = tab.locator('[data-testid="tab-activity-bell"]') | |
| await expect(bell).toBeVisible() | |
| await expect(bell).toHaveAttribute('data-unread-kind', 'terminal-activity') | |
| await expectSingleAccessibleLabel(tab, 'OpenCode · Unread terminal activity') | |
| await expectCardLabel(statusSlot(orcaPage, worktreeId), 'Done') | |
| expectFixedGeometry(await readLeadingGeometry(tab, bell), quietGeometry) | |
| await captureEvidence(tab, worktreeRow(orcaPage, worktreeId), theme, 'unread-terminal') | |
| await orcaPage.evaluate((targetPaneKey) => { | |
| window.__store?.getState().markAgentCompletionPaneUnread(targetPaneKey) | |
| }, primaryPaneKey) | |
| await expect(bell).toHaveAttribute('data-unread-kind', 'agent-completion') | |
| await expectSingleAccessibleLabel(tab, 'OpenCode · Unread agent completion') | |
| await expectCardLabel(statusSlot(orcaPage, worktreeId), 'Done') | |
| expectFixedGeometry(await readLeadingGeometry(tab, bell), quietGeometry) | |
| await captureEvidence(tab, worktreeRow(orcaPage, worktreeId), theme, 'unread-completion') |
|
Moving this PR to draft because it has been untouched for more than 80 days (latest recorded PR activity or commit timestamp: 2026-07-18T09:07:55Z; checked on 2026-10-27). This keeps the review queue focused on active work while preserving the proposal. Please mark it ready for review when work resumes. |
Why
OpenCode emits lifecycle events quickly and does not await plugin event-handler promises. Orca previously treated those events too independently, so concurrent roots, child sessions, recoverable preflight errors, synthetic
noReplymessages, or stale unread state could make the pane, tab, and sidebar disagree.This PR makes the whole path truthful—from OpenCode's event stream, through native/WSL/SSH transport, to every rendered status surface. The implementation was checked against local
~/projects/opencodeat anomalyco/opencode@45cd8d7 (v1.18.3).Fresh Electron evidence
These were generated by the committed E2E against this exact commit after the final rebase. They are stored as hidden upload-ref artifacts and are not in the PR diff.
What changed, ELI5
1. One ordered scoreboard for the whole OpenCode process
ELI5: OpenCode can have several games happening at once—multiple root sessions, child agents, and more than one plugin factory—but Orca has one scoreboard for the pane. The new process-wide coordinator queues events in arrival order, rolls children into their root, and publishes the highest-priority real state instead of letting the last async callback win.
Evidence:
src/main/opencode/hook-service.tshook-plugin-status-transitions.test.tsparentID.2. Waiting, blocked, interrupted, done, and recovered are different outcomes
ELI5: “Please answer,” “OpenCode failed,” “you stopped it,” and “it finished” should not all look like the same dot. Orca now preserves each meaning, keeps permission/question requests FIFO, clears only the request that was answered, and makes a newer interrupted/error state outrank stale success or unread.
Evidence:
hook-plugin-authoritative-recovery.test.ts,agent-hook-listener.test.tsworktree-status.test.ts,WorktreeCardStatusSlot.test.tsx,terminal-tab-activity-status.test.ts3. Recoverable preflight errors no longer flash a false failure
ELI5: OpenCode sometimes says “this attachment had a problem” and then keeps preparing the prompt. Orca now holds that provisional error until an authoritative event proves whether the turn recovered. Only source-proven terminal failures—such as a missing model, agent, or command—use the bounded fallback.
Synthetic move/warp
noReplyparts are also ignored as work, and recovery settlement is deferred just enough to avoid an idle/done flash before a real user part or canonical busy event.Evidence:
noReply, and “no idle between error and ordinary prompt” assertions inhook-plugin-authoritative-recovery.test.ts.noReplyhandling.4. Streaming previews are bounded and human-speed
ELI5: OpenCode sends many tiny “the answer grew by another character” updates. Orca now combines them into a short preview at a human-readable cadence, while still posting the user prompt immediately and flushing the final assistant text before completion. This keeps status useful without flooding the main process and renderer.
Evidence:
hook-plugin-message-part-throttle.test.tscovers throttling, prompt immediacy, bounded previews, final flush, and multi-session isolation.5. Every visual surface uses the same truthful vocabulary
ELI5: The color language is now consistent everywhere:
Blocked and interrupted no longer collapse into generic attention or successful green. Tool/request context stays on live rows and stale tool text disappears on completion.
Evidence:
docs/STYLEGUIDE.mdsrc/renderer/src/assets/main.cssAgentStateDot.tsx,StatusIndicator.tsx,worktree-status.ts6. Tab icons stay aligned and tell the truth about provider ownership
ELI5: A split tab is a little team. The pane with the most important current state owns the status—not merely the pane you clicked last. If winning panes disagree about whether they are Codex or OpenCode, Orca stops guessing and uses provider-neutral copy. A fixed 28 px leading lane means the title never jumps as states change.
Generic terminal bytes now say Unread terminal activity; only a real completion marker says Unread agent completion. Live/blocked/interrupted state replaces an older bell.
Evidence:
terminal-tab-activity-status.ts,use-terminal-tab-status-presentation.ts, andTerminalTabLeadingIcon.tsx7. Sidebar cards and rows agree with the tab
ELI5: The workspace card now repeats the same real outcome instead of replacing it with review/branch chrome or a less specific dot. Hover details use documented tooltips, unread overlays do not widen the lane, and compact/full agent rows share the same semantics.
Evidence:
WorktreeCardStatusSlot.tsx,worktree-agent-activity-summary.ts,DashboardAgentRow.tsx8. WSL gets a guest-native OpenCode plugin without breaking user config
ELI5: A Windows path cannot be handed to Linux and expected to work. Orca now waits for WSL startup, discovers the guest-side OpenCode config, creates a guest-native overlay, mirrors user config/plugins safely, and gives each Orca relay instance a stable namespace. If materialization is unsafe or unavailable, the terminal still opens.
Bash/zsh wrappers run after startup files; modern fish uses
--init-commandafterconfig.fishwith atomic NUL-delimited environment transfer. Daemon and direct PTY paths preserve process-env-only guestOPENCODE_CONFIG_DIR. SSH remains provider-scoped and never receives WSL assumptions.Evidence:
wsl-opencode-shell-materializer.ts,wsl-opencode-shell-materializer-block.ts,wsl-hook-relay-instance-key.ts,wsl-login-shell-command.ts, and PTY/daemon/relay wiring.OPENCODE_CONFIG_DIRloading, additive config path order, and plugin-directory scanning.9. The OpenCode mark is canonical and accessible
ELI5: Orca uses OpenCode's real logo shape, not an imitation. It only swaps the fill to
currentColorso it remains visible in both themes. Tooltips and ARIA names expose the exact provider + state without duplicate announcements.Evidence:
0 0 512 512viewBox.0 0 512 512geometry, and the expected dark semantic token values.Verification
pnpm test: 2,983 test files passed, 7 skipped; 31,823 tests passed, 53 skipped.pnpm typecheck: passed.pnpm build: passed (relay for Linux/macOS/Windows x64+arm64, WSL relay, CLI, Electron, web, and macOS native sidecars).oxlint --deny-warnings: passed.oxfmt --check: passed.git diff --check: passed.The repo-wide lint/format aggregate still reports only existing
maindebt outside this diff: the knownskill-freshness-group.tsxexhaustiveness case, releasedorca-clisnapshot-history drift, pre-existing lint warnings, and 30 pre-existing format files. All 86 changed files pass their scoped gates.Honest upstream/platform boundaries
ENVstartup still run exactly once.noReply: OpenCode's public API permits arbitrary non-syntheticpromptAsync({ noReply: true }), but its plugin events omitnoReply. That is observationally identical to a slow normal prompt, so Orca conservatively stays working rather than inventing a false-idle timeout. Built-in move and warp are synthetic and fully covered; a general solution needs upstream event provenance.Diff
One commit, 86 files, 6,998 insertions / 558 deletions. Most of the size is exhaustive lifecycle, renderer, WSL, and Electron regression coverage.