Skip to content

fix(opencode): make agent status lifecycle and chrome truthful - #9318

Draft
nwparker wants to merge 1 commit into
stablyai:mainfrom
nwparker:nwparker/turbot
Draft

nwparker wants to merge 1 commit into
stablyai:mainfrom
nwparker:nwparker/turbot

Conversation

@nwparker

Copy link
Copy Markdown
Contributor

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 noReply messages, 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/opencode at 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.

Terminal tabs Sidebar cards
Terminal tab light/dark status matrix Sidebar light/dark status matrix

Generic terminal unread versus agent-completion unread

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:

  • Implementation: src/main/opencode/hook-service.ts
  • Exhaustive lifecycle matrix: hook-plugin-status-transitions.test.ts
  • Upstream reason: OpenCode fire-and-forgets plugin event promises, exposes idle / retry / busy, and links child sessions with parentID.
  • Covered cases include concurrent roots, nested children, duplicated/out-of-order idle, retry, compaction, abort, disposal, and per-root timer isolation.

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:

3. 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 noReply parts 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:

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.ts covers throttling, prompt immediacy, bounded previews, final flush, and multi-session isolation.
  • OpenCode publishes whole part snapshots plus incremental deltas.

5. Every visual surface uses the same truthful vocabulary

ELI5: The color language is now consistent everywhere:

  • yellow = working/retrying
  • amber = waiting for the user
  • red = blocked/interrupted/failed
  • green = done or quietly active
  • gray = inactive

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:

  • Design contract: docs/STYLEGUIDE.md
  • Canonical theme tokens: src/renderer/src/assets/main.css
  • Shared primitives: AgentStateDot.tsx, StatusIndicator.tsx, worktree-status.ts
  • Fresh light/dark screenshots above.
  • E2E asserts exact state/copy/style agreement across tab, card, and agent row; minimum status contrast; no console/page errors; and reduced-motion behavior.
  • Calculated minimum graphical contrast across canonical chrome surfaces is 4.47:1 in light mode and 5.00:1 in dark mode.

6. 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, and TerminalTabLeadingIcon.tsx
  • Component and aggregate unit suites cover provider ownership, mixed providers, unread source/type, and priority.
  • Electron E2E measures the 28 px lane and identical title offset in every state, proves one accessible label, checks sibling-owned/mixed-provider split panes, and verifies interruption hides both unread kinds.

7. 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.tsx
  • Focused tests cover destructive precedence, review/branch replacement, compact hover, waiting questions, blocked errors, interruption, retained completion, and tool-line cleanup.
  • The screenshot matrix shows tab/card/row agreement; unread screenshots prove tab-scoped unread leaves the completed sidebar green.

8. 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-command after config.fish with atomic NUL-delimited environment transfer. Daemon and direct PTY paths preserve process-env-only guest OPENCODE_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.
  • Tests cover user config/plugin preservation, concurrency, stale/broken links, traversal/symlink defenses, source isolation, daemon/direct parity, disabled hooks, failure-open behavior, relay namespace agreement, and real ksh/dash startup behavior.
  • OpenCode source: OPENCODE_CONFIG_DIR loading, 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 currentColor so it remains visible in both themes. Tooltips and ARIA names expose the exact provider + state without duplicate announcements.

Evidence:

  • Compared with OpenCode's canonical 512×512 mark.
  • E2E verifies the rendered 12×12 status mark and 0 0 512 512 viewBox.
  • Independent live CDP smoke check against the built Electron app found one accessible OpenCode choice, canonical 0 0 512 512 geometry, 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).
  • Focused OpenCode suite: 83/83 passed.
  • Focused renderer status suite: 64/64 passed.
  • WSL materializer/login focused suites and PTY/daemon/relay suites: passed.
  • Electron status E2E: 1/1 passed, with fresh evidence capture.
  • Changed-file oxlint --deny-warnings: passed.
  • Changed-file oxfmt --check: passed.
  • Changed React Doctor, max-lines ratchet, localization catalog/coverage, reliability manifest, styled-scrollbar, bundled-skill-guide, and git diff --check: passed.
  • Final read-only lifecycle/diff audit: clean.

The repo-wide lint/format aggregate still reports only existing main debt outside this diff: the known skill-freshness-group.tsx exhaustiveness case, released orca-cli snapshot-history drift, pre-existing lint warnings, and 30 pre-existing format files. All 86 changed files pass their scoped gates.

Honest upstream/platform boundaries

  • Shell startup: bash, zsh, and modern fish can enforce the WSL overlay after user startup config. Old fish and ksh/mksh/dash/sh/ash have no safe forced post-login hook; later user config may intentionally win. Orca preserves the user's profile/rc semantics instead of forcing a wrapper that could suppress them. Real ksh/dash tests prove profile and ENV startup still run exactly once.
  • Third-party noReply: OpenCode's public API permits arbitrary non-synthetic promptAsync({ noReply: true }), but its plugin events omit noReply. 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.

@coderabbitai

coderabbitai Bot commented Jul 18, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

📝 Walkthrough

Walkthrough

The 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)

Check name Status Explanation Resolution
Description check ⚠️ Warning The description is detailed but misses required template sections like Summary, Screenshots, Testing checklist, AI Review Report, Security Audit, and Notes. Restructure the description to match the template and add the missing sections, including the testing checklist, AI review report, security audit, and notes.
Docstring Coverage ⚠️ Warning Docstring coverage is 4.48% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (3 passed)
Check name Status Explanation
Title check ✅ Passed The title is concise and accurately summarizes the PR's main change to OpenCode status lifecycle and UI chrome.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

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.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 5

🧹 Nitpick comments (1)
tests/e2e/opencode-agent-status-chrome.spec.ts (1)

446-446: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Build 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.join or 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

📥 Commits

Reviewing files that changed from the base of the PR and between 72d0a40 and 307e000.

📒 Files selected for processing (86)
  • docs/STYLEGUIDE.md
  • src/main/agent-hooks/wsl-hook-relay-instance-key.ts
  • src/main/agent-hooks/wsl-hook-relay-launch.ts
  • src/main/agent-hooks/wsl-hook-relay-manager.test.ts
  • src/main/agent-hooks/wsl-hook-relay-manager.ts
  • src/main/daemon/pty-subprocess.test.ts
  • src/main/daemon/shell-ready.test.ts
  • src/main/daemon/shell-ready.ts
  • src/main/ipc/pty.test.ts
  • src/main/ipc/pty.ts
  • src/main/opencode/hook-plugin-authoritative-recovery.test.ts
  • src/main/opencode/hook-plugin-message-part-throttle.test.ts
  • src/main/opencode/hook-plugin-status-transitions.test.ts
  • src/main/opencode/hook-service.test.ts
  • src/main/opencode/hook-service.ts
  • src/main/providers/local-pty-shell-ready.ts
  • src/main/pty/wsl-opencode-host-materializer.test.ts
  • src/main/pty/wsl-opencode-shell-materializer-block.ts
  • src/main/pty/wsl-opencode-shell-materializer.test.ts
  • src/main/pty/wsl-opencode-shell-materializer.ts
  • src/main/pty/wsl-orca-env.test.ts
  • src/main/pty/wsl-orca-env.ts
  • src/relay/wsl-agent-hook-relay.test.ts
  • src/renderer/src/assets/main.css
  • src/renderer/src/components/AgentStateDot.test.ts
  • src/renderer/src/components/AgentStateDot.tsx
  • src/renderer/src/components/activity/ActivityPrototypePage.tsx
  • src/renderer/src/components/dashboard/DashboardAgentRow.test.tsx
  • src/renderer/src/components/dashboard/DashboardAgentRow.tsx
  • src/renderer/src/components/dashboard/DashboardAgentRowToolStep.tsx
  • src/renderer/src/components/feature-tips/CmdJPaletteFeatureTipVisual.test.tsx
  • src/renderer/src/components/feature-tips/CmdJPaletteFeatureTipVisual.tsx
  • src/renderer/src/components/feature-wall/FeatureTourWorkspaceCard.tsx
  • src/renderer/src/components/feature-wall/TasksAnimatedVisual.tsx
  • src/renderer/src/components/feature-wall/WorkspacesAnimatedVisual.tsx
  • src/renderer/src/components/feature-wall/agents-orchestration/StatusesPage.tsx
  • src/renderer/src/components/feature-wall/agents-orchestration/orchestration-cards.tsx
  • src/renderer/src/components/feature-wall/feature-tour-preview-glyphs.tsx
  • src/renderer/src/components/floating-terminal/FloatingTerminalToggleButton.tsx
  • src/renderer/src/components/sidebar/StatusIndicator.test.ts
  • src/renderer/src/components/sidebar/StatusIndicator.tsx
  • src/renderer/src/components/sidebar/WorktreeActivityStatusIndicator.test.tsx
  • src/renderer/src/components/sidebar/WorktreeCard.compact-hover.test.tsx
  • src/renderer/src/components/sidebar/WorktreeCard.pr-display.test.tsx
  • src/renderer/src/components/sidebar/WorktreeCard.tsx
  • src/renderer/src/components/sidebar/WorktreeCardAgents.test.tsx
  • src/renderer/src/components/sidebar/WorktreeCardMeta.test.tsx
  • src/renderer/src/components/sidebar/WorktreeCardMeta.tsx
  • src/renderer/src/components/sidebar/WorktreeCardStatusSlot.test.tsx
  • src/renderer/src/components/sidebar/WorktreeCardStatusSlot.tsx
  • src/renderer/src/components/sidebar/use-worktree-activity-status.test.tsx
  • src/renderer/src/components/sidebar/use-worktree-activity-status.ts
  • src/renderer/src/components/sidebar/worktree-agent-activity-summary.test.ts
  • src/renderer/src/components/sidebar/worktree-agent-activity-summary.ts
  • src/renderer/src/components/sidebar/worktree-card-compact-agent-row.tsx
  • src/renderer/src/components/sidebar/worktree-card-meta-types.ts
  • src/renderer/src/components/sidebar/worktree-section-activity.test.ts
  • src/renderer/src/components/sidebar/worktree-section-activity.ts
  • src/renderer/src/components/status-bar/StatusBar.tsx
  • src/renderer/src/components/tab-bar/SortableTab.rename-shortcut.test.tsx
  • src/renderer/src/components/tab-bar/SortableTab.tsx
  • src/renderer/src/components/tab-bar/TerminalTabLeadingIcon.test.tsx
  • src/renderer/src/components/tab-bar/TerminalTabLeadingIcon.tsx
  • src/renderer/src/components/tab-bar/tab-title-tooltip.test.tsx
  • src/renderer/src/components/tab-bar/terminal-tab-activity-status.test.ts
  • src/renderer/src/components/tab-bar/terminal-tab-activity-status.ts
  • src/renderer/src/components/tab-bar/use-terminal-tab-status-presentation.ts
  • src/renderer/src/components/ui/tooltip.tsx
  • src/renderer/src/i18n/locales/en.json
  • src/renderer/src/i18n/locales/es.json
  • src/renderer/src/i18n/locales/ja.json
  • src/renderer/src/i18n/locales/ko.json
  • src/renderer/src/i18n/locales/zh.json
  • src/renderer/src/lib/agent-icon-glyphs.tsx
  • src/renderer/src/lib/agent-status.test.ts
  • src/renderer/src/lib/agent-status.ts
  • src/renderer/src/lib/worktree-activity-state.test.ts
  • src/renderer/src/lib/worktree-activity-state.ts
  • src/renderer/src/lib/worktree-status.test.ts
  • src/renderer/src/lib/worktree-status.ts
  • src/shared/agent-hook-listener.test.ts
  • src/shared/agent-hook-listener.ts
  • src/shared/wsl-login-shell-command.test.ts
  • src/shared/wsl-login-shell-command.ts
  • src/shared/wsl-opencode-materializer-contract.ts
  • tests/e2e/opencode-agent-status-chrome.spec.ts

Comment on lines +435 to +482
'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();',

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🩺 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/opencode

Repository: 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.

Comment on lines +74 to +91
'__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=""',

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 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.ts

Repository: 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/pty

Repository: 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:


🏁 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',

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 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.tsx

Repository: 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())
PY

Repository: stablyai/orca

Length of output: 163


🏁 Script executed:

#!/bin/bash
set -euo pipefail
sed -n '1,220p' src/renderer/src/components/ui/tooltip.tsx

Repository: 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.css

Repository: 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
done

Repository: 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.

Comment on lines +393 to +398
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)
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 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.

Suggested change
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)
}

Comment on lines +610 to +630
// 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')

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 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.

Suggested change
// 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')

@OrcaWin

OrcaWin commented Oct 27, 2026

Copy link
Copy Markdown
Collaborator

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.

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants