Skip to content

fix: sync opencode tui compatibility slice#73

Merged
Astro-Han merged 21 commits into
devfrom
codex/tui-compat-sync
Apr 20, 2026
Merged

fix: sync opencode tui compatibility slice#73
Astro-Han merged 21 commits into
devfrom
codex/tui-compat-sync

Conversation

@Astro-Han

@Astro-Han Astro-Han commented Apr 20, 2026

Copy link
Copy Markdown
Owner

Summary

  • sync the remaining opencode TUI compatibility slice onto PawWork-owned config and server bridges
  • restore workspace-aware runtime and session compatibility while simplifying session deletion to a plain error toast path
  • trim out-of-scope TUI product surfaces from this slice, including sound/logo animation and upsell/workspace-recovery UI

Why

This lands the remaining sync/opencode-tui-compat work for issue #27 while keeping PR 7 inside the planned compatibility boundary instead of pulling extra TUI product changes along with the upstream intake.

Related Issue

Part of #27

How To Verify

cd packages/opencode

bun test test/config/tui.test.ts \
  test/cli/tui/sync-provider.test.tsx \
  test/cli/tui/thread.test.ts \
  test/cli/tui/theme-store.test.ts \
  test/cli/tui/plugin-add.test.ts \
  test/cli/tui/plugin-install.test.ts \
  test/cli/tui/plugin-lifecycle.test.ts \
  test/cli/tui/plugin-loader-entrypoint.test.ts \
  test/cli/tui/plugin-loader-pure.test.ts \
  test/cli/tui/plugin-loader.test.ts \
  test/cli/tui/plugin-toggle.test.ts

bun test test/cli/tui/dialog-session-list.test.tsx

Screenshots or Recordings

N/A, this PR aligns existing TUI behavior and removes out-of-scope UI surfaces rather than introducing new visible flows.

Checklist

  • I ran the relevant verification steps
  • I tested visible changes manually when needed
  • I am targeting the dev branch

Follow-up

Summary by CodeRabbit

  • New Features

    • Workspace adaptor discovery with async loading and selection.
    • Added /share tip and container run tip for quick sharing and containerized use.
    • Fast-boot startup mode and new session refresh action.
  • Bug Fixes

    • Safer session deletion with clearer error reporting and recovery.
    • Session creation now retries transient server failures and surfaces better errors.
    • Improved terminal background detection and dynamic theme-mode toggle labels.
  • UI/UX Improvements

    • Diff line-number colors adjusted across themes for better visibility.
    • Branding updated from PawWork → OpenCode.

@coderabbitai

coderabbitai Bot commented Apr 20, 2026

Copy link
Copy Markdown
Contributor

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

Refactors TUI imports and terminal color helpers; changes route prompt field; passes explicit config into plugin runtime init; adds utilities (binary search, revert-diff, fade-in signal); tightens async handling (explicit fire-and-forget/catches), improves SSE backoff and sync flows, updates many theme tokens, and expands tests. (≤50 words)

Changes

Cohort / File(s) Summary
App / Attach
packages/opencode/src/cli/cmd/tui/app.tsx, packages/opencode/src/cli/cmd/tui/attach.ts
Switched Clipboard/Selection to namespace imports; replaced local terminal-bg logic with getTerminalBackgroundColor; TuiPluginRuntime.init now receives { api, config }; removed Instance.provide wrapper in attach.
Plugin runtime & API
packages/opencode/src/cli/cmd/tui/plugin/runtime.ts, packages/opencode/src/cli/cmd/tui/plugin/api.tsx
Converted runtime to top-level exports and ConfigPlugin.* types; init accepts { api, config }; load no longer calls TuiConfig.get; adjusted load/dispose error handling and route param rename initialPromptprompt.
Contexts & routing
packages/opencode/src/cli/cmd/tui/context/...
route.tsx, local.tsx, sdk.tsx, sync.tsx, project.tsx, cwd.ts
Renamed route field to prompt; RouteProvider.init accepts { initialRoute? }; added CurrentWorkingDirectory and parseModel; SDK SSE loop uses explicit exponential backoff and abort-aware delays; sync.bootstrap accepts { fatal? } and version guard; added session.refresh().
Utilities (new/changed)
packages/opencode/src/cli/cmd/tui/util/...
binary.ts, signal.ts, revert-diff.ts, terminal.ts, clipboard.ts, selection.ts, editor.ts
Added Binary.search/insert, createFadeIn, getRevertDiffFiles; refactored terminal color parsing and exposed backgroundModeFromResponse/consumeColorResponseBuffer; re-exported clipboard helpers; converted Selection/Editor to top-level exports and adjusted cleanup/promise handling.
Prompt & prompt UI
packages/opencode/src/cli/cmd/tui/component/*, component/prompt/index.tsx
Added null-safety guards, cross-remount prompt stashing, animated fade utilities, centralized async paste handling, and made many async calls explicit fire-and-forget with catch-toasts; adjusted agent/model selection logic and UI fallbacks.
Session routes & sidebar
packages/opencode/src/cli/cmd/tui/routes/*
session, home, index.tsx, dialog-*
Reworked session init to async/await and route.prompt; replaced local revert parsing with getRevertDiffFiles; tightened tool typings; added workspace-aware sidebar status; made SDK calls explicit void + .catch.
Theme changes
packages/opencode/src/cli/cmd/tui/context/theme.tsx, .../theme/*.json
Tweaked theme generator and many theme JSON files; standardized/updated numerous diffLineNumber tokens and a few computed diffs; disabled renderer theme-mode event registration.
Feature plugins & UI copy
packages/opencode/src/cli/cmd/tui/feature-plugins/*, system/plugins.tsx, sidebar/footer.tsx, home/tips-view.tsx
Rebranded copy (PawWork→OpenCode), updated tips/footer text, and made plugin install/activate/deactivate promise usage explicit (fire-and-forget).
KV, Win32, thread, event, worker
packages/opencode/src/cli/cmd/tui/context/kv.tsx, win32.ts, thread.ts, event.ts, worker.ts
Typed Filesystem.readJson invocation; writeJson calls fire-and-forget with catch logging; narrowed Win32 stdin typing; removed unused imports; minor ordering/formatting tweaks.
UI & components (small)
packages/opencode/src/cli/cmd/tui/ui/*.tsx, component/*.tsx
Minor import cleanups, keyboard handler early-return on evt.defaultPrevented, optional chaining to avoid crashes, toast/error helper centralization, and small handler signature tweaks.
Tests & helpers
packages/opencode/test/cli/tui/*, .../_mock-tui-runtime.ts
Updated tests to pass explicit config into TuiPluginRuntime.init({ api, config }); added/updated many tests (dialog session-list, workspace-create retry, revert-diff, terminal color parsing, sync-provider, plugin lifecycle, toast, error-component, theme-provider); added mockTuiRuntime helper.

Sequence Diagram(s)

sequenceDiagram
  participant SDK as SDK Context
  participant Server as SSE Server
  participant Timer as Backoff Timer
  participant Handler as Event Handler

  SDK->>Server: request sdk.global.event(sseMaxRetryAttempts: 0)
  Note over SDK,Server: stream events via async iterator
  Server-->>SDK: events.stream yields events
  SDK->>Handler: handleEvent(event)
  alt stream ends or throws
    SDK->>Timer: compute exponential backoff (capped)
    Timer-->>SDK: await delay (abort-aware)
    SDK->>Server: retry sdk.global.event(...)
  end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~75 minutes

Suggested labels

enhancement, P2, app

🐰 Hopping through the refactor maze,
Namespaces set, and prompts find new ways,
Plugins sip config from a shared cup,
Themes refreshed and tests warm up,
OpenCode nibbles forward—code carrots up! 🥕

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 2.35% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Title check ✅ Passed The title 'fix: sync opencode tui compatibility slice' clearly summarizes the main change—syncing TUI compatibility code—and directly relates to the PR's primary objective of completing the compatibility slice work for issue #27.
Description check ✅ Passed The PR description is comprehensive and follows the template structure with all required sections: Summary explaining the changes, Why providing context for issue #27, Related Issue linking to #27, How To Verify with specific test commands, Screenshots noting N/A appropriately, and a completed Checklist.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch codex/tui-compat-sync

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

@github-actions

Copy link
Copy Markdown

Dependency Review

✅ No vulnerabilities or license issues or OpenSSF Scorecard issues found.

Scanned Files

None

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Code Review

This pull request performs a major refactoring of the TUI, rebranding the application to OpenCode and migrating TUI-specific settings to a dedicated tui.json configuration file. It enhances the prompt component with animations and state persistence, improves session management with better deletion workflows, and introduces support for workspace adaptors. Additionally, it implements exponential backoff for SSE connections and refactors terminal utility functions. Feedback focuses on refining the session list refresh logic to include local sessions, adding a retry limit to prevent infinite loops during workspace creation, ensuring background sync promises handle rejections, and robustly buffering terminal data when querying background colors.

Comment thread packages/opencode/src/cli/cmd/tui/component/dialog-session-list.tsx Outdated
Comment thread packages/opencode/src/cli/cmd/tui/component/dialog-workspace-create.tsx Outdated
Comment thread packages/opencode/src/cli/cmd/tui/context/sync.tsx
Comment thread packages/opencode/src/cli/cmd/tui/util/terminal.ts
@Astro-Han

Copy link
Copy Markdown
Owner Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Apr 20, 2026

Copy link
Copy Markdown
Contributor
✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

Repository owner deleted a comment from coderabbitai Bot Apr 20, 2026

@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: 18

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (6)
packages/opencode/src/cli/cmd/tui/ui/toast.tsx (1)

59-64: ⚠️ Potential issue | 🟠 Major

Default toast duration was dropped, which can auto-dismiss toasts immediately.

At Line 59, duration is now read directly from options without normalization. Callers like toast.error(...) omit duration, so the timeout can fire effectively right away and hide the toast before users can read it.

💡 Proposed fix
     show(options: ToastOptions) {
-      const { duration, ...currentToast } = options
+      const { duration = 3000, ...currentToast } = options
       setStore("currentToast", currentToast)
       if (timeoutHandle) clearTimeout(timeoutHandle)
-      timeoutHandle = setTimeout(() => {
+      timeoutHandle = setTimeout(() => {
         setStore("currentToast", null)
-      }, duration).unref()
+      }, duration)
+      timeoutHandle.unref?.()
     },
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/opencode/src/cli/cmd/tui/ui/toast.tsx` around lines 59 - 64, The
toast auto-dismiss timeout is reading duration directly from options and can be
undefined (causing immediate dismissal); update the handler in the toast display
code to normalize duration (e.g., use a DEFAULT_TOAST_DURATION constant or
fallback like duration ?? DEFAULT_TOAST_DURATION and ensure it's a positive
number) before calling setTimeout; locate the toast creation logic around the
variables options, duration, setStore("currentToast", ...), and timeoutHandle,
and replace the destructuring or timeout argument so missing caller-supplied
durations (e.g., toast.error calls) use the default instead of hiding
immediately.
packages/opencode/test/cli/tui/sync-provider.test.tsx (1)

90-107: ⚠️ Potential issue | 🟡 Minor

rejectPaths is untested in this file.

createFetch()/mount() now expose a fetch-failure injection path, but the new test never passes rejectPaths; it only replaces project.workspace.sync. Either drive the failure through mount(log, { rejectPaths: [...] }) or drop the unused branch, otherwise this new helper logic is not covered.

Also applies to: 185-203, 307-331

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/opencode/test/cli/tui/sync-provider.test.tsx` around lines 90 - 107,
The new createFetch helper exposes a rejectPaths injection but the tests never
use it; either update the failing-test(s) to drive the failure via mount(log, {
rejectPaths: ['/...'] }) (so createFetch will throw for that pathname) or remove
the unused rejectPaths branch. Specifically, in the tests that currently stub
project.workspace.sync, pass the rejectPaths array into mount (or the test
harness that calls createFetch) so the forced fetch error path is exercised
(reference createFetch, mount, and project.workspace.sync to locate the change).
packages/opencode/src/cli/cmd/tui/context/sdk.tsx (1)

77-101: ⚠️ Potential issue | 🟠 Major

Reconnects die on the first SSE setup error.

sdk.global.event() at line 82 lacks error handling, so a rejection bypasses the retry loop entirely and falls into the outer .catch(() => {}) handler. With sseMaxRetryAttempts: 0, this permanently disables event reconnections after any transient failure. The backoff setTimeout at line 99 is also not abort-aware—if cleanup happens during the sleep, the timer completes after up to 30 seconds before the loop can exit, leaving dangling event loop timers.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/opencode/src/cli/cmd/tui/context/sdk.tsx` around lines 77 - 101, The
SSE setup can reject and escape the retry loop; wrap the sdk.global.event(...)
call in a try/catch inside the while loop (around the call at sdk.global.event)
and on error log/handle it and continue to the backoff path instead of letting
it bubble to the outer .catch; replace the current sleep with an abort-aware
wait by assigning setTimeout to the existing timer variable and/or using the
abort.signal to reject the sleep so the loop can exit immediately when
abort.signal or ctrl.signal is aborted; ensure timer is cleared on both success
and abort, and keep existing flush(), queue, handleEvent, retryDelay and
maxRetryDelay logic intact so reconnection attempts continue after transient
failures.
packages/opencode/src/cli/cmd/tui/context/theme.tsx (1)

396-410: ⚠️ Potential issue | 🟠 Major

Unlocked theme mode stops tracking renderer theme changes.

The CliRenderEvents.THEME_MODE subscription is commented out. When unlock() is called, it syncs once via free(), but subsequent renderer theme-mode changes won't update store.mode unless a manual refresh occurs (SIGUSR2 signal or explicit action).

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/opencode/src/cli/cmd/tui/context/theme.tsx` around lines 396 - 410,
The theme renderer subscription was commented out so store.mode no longer
updates on renderer changes; re-enable the listener by restoring the
renderer.on(CliRenderEvents.THEME_MODE, handle) subscription after the handle
definition so theme changes propagate automatically (the handle already guards
with store.lock), and keep the existing renderer.off(CliRenderEvents.THEME_MODE,
handle) in onCleanup; ensure this behavior complements unlock()/free() which
perform a one-time sync but rely on the active subscription for subsequent
updates.
packages/opencode/src/cli/cmd/tui/context/sync.tsx (1)

357-449: ⚠️ Potential issue | 🟠 Major

Serialize bootstrap() results so older workspace loads cannot win the race.

bootstrap() can run concurrently from the workspace watcher, server.instance.disposed, and explicit calls. An older invocation still writes provider/config/session state after a newer workspace switch, so the store can snap back to stale workspace data.

Suggested fix
-    async function bootstrap(input: { fatal?: boolean } = {}) {
+    let bootstrapVersion = 0
+
+    async function bootstrap(input: { fatal?: boolean } = {}) {
+      const version = ++bootstrapVersion
       const fatal = input.fatal ?? true
       const workspace = project.workspace.current()
       if (workspace !== syncedWorkspace) {
         fullSyncedSessions.clear()
         syncedWorkspace = workspace
@@
           ]).then((responses) => {
+            if (version !== bootstrapVersion) return
             const providers = responses[0]
             const providerList = responses[1]
             const consoleState = responses[2]
             const agents = responses[3]
             const config = responses[4]
@@
         .then(() => {
           if (store.status !== "complete") setStore("status", "partial")
           // non-blocking
           void Promise.all([
@@
-          ]).then(() => {
+          ]).then(() => {
+            if (version !== bootstrapVersion) return
             setStore("status", "complete")
           }).catch((error) => {
             Log.Default.error("tui bootstrap non-blocking sync failed", {
               workspace,
               error: errorData(error),
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/opencode/src/cli/cmd/tui/context/sync.tsx` around lines 357 - 449,
bootstrap can run concurrently and older invocations can overwrite a newer
workspace's store; fix by serializing/applying only the latest run's results: at
the start of bootstrap generate a unique run id (or increment a module-level
bootstrapCounter), assign it to a module-level latestBootstrapId, capture it as
localRunId, and use the captured workspace variable; before any setStore/batch
that applies responses (including the non-blocking Promise.all.then), check that
latestBootstrapId === localRunId (or that syncedWorkspace === workspace) and
abort applying results if mismatched; update the places that clear
fullSyncedSessions/syncedWorkspace accordingly so they only run when the run is
still current.
packages/opencode/src/cli/cmd/tui/plugin/runtime.ts (1)

975-1028: ⚠️ Potential issue | 🟠 Major

Partial initialization: runtime is set before try block completes.

runtime = next is assigned on line 987 before the try block. If Instance.provide throws, runtime remains pointing to the partially initialized next object. Subsequent calls to list(), activatePlugin(), etc. will operate on this incomplete state without any indication that initialization failed.

🐛 Proposed fix: only set runtime on success
   const next: RuntimeState = {
     directory: cwd,
     api,
     slots,
     plugins: [],
     plugins_by_id: new Map(),
     pending: new Map(),
   }
-  runtime = next
   try {
     await Instance.provide({
       directory: cwd,
       fn: async () => {
         // ... existing code ...
       },
     })
+    runtime = next
   } catch (error) {
     fail("failed to load tui plugins", { directory: cwd, error })
   }
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/opencode/src/cli/cmd/tui/plugin/runtime.ts` around lines 975 - 1028,
The code sets the module-level runtime variable to a partially initialized
RuntimeState (next) before Instance.provide completes, risking consumers seeing
a broken state if provide throws; move the assignment so runtime is only set
after Instance.provide finishes successfully (i.e., remove or delay runtime =
next before the try and assign runtime = next after the await
Instance.provide(...) completes), keeping the rest of load (creating next,
calling Instance.provide, loading internal/external plugins,
applyInitialPluginEnabledState, and activating plugins) unchanged; refer to the
load function, the runtime variable, the local next RuntimeState, and
Instance.provide to locate where to change the assignment.
🧹 Nitpick comments (2)
packages/opencode/test/cli/tui/plugin-toggle.test.ts (1)

42-54: Prefer tmpdir({ config }) over inline runtime config blocks in tests.

Both test setups duplicate manual TuiConfig.Info construction. Moving this into tmpdir setup will reduce drift and keep test fixtures consistent.

As per coding guidelines, “Use the config option in tmpdir to write an opencode.json config file during test setup by passing a partial Config.Info object.”

Also applies to: 119-131

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/opencode/test/cli/tui/plugin-toggle.test.ts` around lines 42 - 54,
Tests currently construct TuiConfig.Info inline (the const config:
TuiConfig.Info block) causing duplication; instead, update the tmpdir setup
calls in this test (and the similar block at lines 119-131) to pass a config
option to tmpdir, e.g. tmpdir({ config: { plugin: [[tmp.extra.spec, { marker:
tmp.extra.marker }]], plugin_enabled: { "demo.toggle": false }, plugin_origins:
[...] } }), so the opencode.json is written by the fixture; remove the inline
TuiConfig.Info construction and any manual writes, keeping references to
tmp.extra.spec, tmp.extra.marker, and tmp.path.plugin_origins intact.
packages/opencode/test/cli/tui/plugin-loader-entrypoint.test.ts (1)

47-56: Refactor repeated config scaffolding into a shared fixture path.

These repeated inline TuiConfig.Info blocks make the suite harder to evolve safely. Consider centralizing via tmpdir({ config }) or a local helper to keep test setup uniform.

As per coding guidelines, “Use the config option in tmpdir to write an opencode.json config file during test setup by passing a partial Config.Info object.”

Also applies to: 108-117, 170-179, 232-241, 290-299, 355-364, 402-411, 459-468

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/opencode/test/cli/tui/plugin-loader-entrypoint.test.ts` around lines
47 - 56, The repeated inline TuiConfig.Info test scaffolding should be
centralized using the tmpdir helper's config option instead of duplicating
objects; replace each inline config (the object with plugin and plugin_origins
referencing tmp.extra.spec/tmp.extra.marker and tmp.path) by calling tmpdir({
config: <partial TuiConfig.Info> }) so tmpdir writes the opencode.json (or tui
config) during setup, and update tests that reference tmp.path/tui.json to
depend on the tmpdir-produced config; locate occurrences by the TuiConfig.Info
literal and replace them with the shared tmpdir({ config }) fixture creation to
keep setup uniform.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@packages/opencode/src/cli/cmd/tui/component/dialog-session-list.tsx`:
- Around line 159-177: The delete flow currently clears toDelete only if the
initial delete succeeds and the subsequent sync/refresh/refetch steps also
succeed; move or add a guaranteed cleanup so setToDelete(undefined) always runs
after a successful sdk.client.session.delete (regardless of sync.session.refresh
or refetch failures). Specifically, keep the existing try/catch around
sdk.client.session.delete and deleteError handling, then ensure
setToDelete(undefined) is invoked in a finally-style path (or after awaiting the
refresh/refetch wrapped with its own try/catch) so that even if
sync.session.refresh() or refetch() rejects, deleteError is handled and the
confirmation state is cleared; reference functions: sdk.client.session.delete,
deleteError, setToDelete, sync.session.refresh, refetch, and search.

In `@packages/opencode/src/cli/cmd/tui/component/dialog-session-rename.tsx`:
- Line 22: The call to sdk.client.session.update is fire-and-forget and lacks
localized error handling; attach a .catch handler to sdk.client.session.update
(which is invoked with sessionID: props.session and title: value) to call
toast.error with the error message (e.g., "Failed to rename session:
<err.message>") so failures surface to the user, and then call dialog.clear() as
before; reference sdk.client.session.update, props.session, value, toast.error,
and dialog.clear when making this change.

In `@packages/opencode/src/cli/cmd/tui/component/dialog-workspace-create.tsx`:
- Around line 175-199: The code double-notifies workspace create failures
because sdk.client.experimental.workspace.create's .catch currently calls
toast.show and then the subsequent !workspace branch also toasts; remove the
toast from the .catch handler so the catch only logs the error (using log.error
and errorData) and returns undefined, leaving the single user-facing toast in
the following !workspace branch (references:
sdk.client.experimental.workspace.create, .catch handler, toast.show, result,
workspace, setCreating, log.error, errorData).
- Around line 121-137: The fetch handler currently accepts any JSON and calls
setAdaptors(res) which can crash rendering; update the async IIFE to check the
fetch response status and the parsed JSON shape before setting state: first use
sdk.fetch(url) and inspect the Response (e.g., check response.ok) and only parse
JSON if ok, then verify the parsed value is an array of adaptors
(Array.isArray(parsed) and optionally validate required adaptor fields) before
calling setAdaptors(parsed); on any failure (non-ok response, parse error, or
non-array result) show the existing toast error and do not call setAdaptors.

In `@packages/opencode/src/cli/cmd/tui/component/error-component.tsx`:
- Around line 59-61: The clipboard copy promise started in the error UI
(Clipboard.copy(issueURL.toString()) then setCopied(true)) is unhandled on
failure; wrap the copy call to handle errors (either await in an async handler
with try/catch or append .catch(...)) so failures don't produce unhandled
promise rejections and optionally log or surface the error; modify the code
around Clipboard.copy and the setCopied call in the ErrorComponent (or its click
handler) to catch errors and handle them gracefully.

In `@packages/opencode/src/cli/cmd/tui/component/prompt/index.tsx`:
- Line 83: The global variable stashed currently holds one { prompt: PromptInfo;
cursor: number } and leaks drafts across Prompt instances; replace it with a
keyed stash (e.g., Map<string, { prompt: PromptInfo; cursor:number }>) keyed by
a stable prompt identifier (use PromptInfo.id or another unique identity
available on PromptInfo). Update all places that read/write stashed (the stash
save/restore logic near the current stashed usage and the other block around the
restore/clear code) to use stash.get(id)/stash.set(id, value) and ensure you
clear the specific key on commit/submit/unmount to avoid cross-prompt pollution.
Use the existing symbol names (stashed -> stashedMap or promptStash, PromptInfo,
and the existing save/restore handlers) so callers are updated consistently.

In `@packages/opencode/src/cli/cmd/tui/context/kv.tsx`:
- Around line 46-47: The call to Filesystem.writeJson(filePath, store) is fired
with void and no error handling, so rejected promises are unhandled; update the
persistence in the function that calls setStore to either await
Filesystem.writeJson(filePath, store) inside a try/catch or attach a .catch(...)
handler to it and surface/log the error (use the existing logger or UI error
method) so failures to write (filePath, store) are not silently ignored; ensure
any user-facing state remains consistent if the write fails.

In `@packages/opencode/src/cli/cmd/tui/context/route.tsx`:
- Around line 26-33: The init function's use of JSON.parse on
process.env["OPENCODE_ROUTE"] can throw and crash startup; update init (the
block creating the store via createStore<Route>) to wrap parsing of
OPENCODE_ROUTE in a try/catch: attempt to parse only when
process.env["OPENCODE_ROUTE"] is truthy, and if JSON.parse throws, swallow the
error and fall back to the default route ({ type: "home" }) (preserve behavior
when props.initialRoute is provided). Ensure references to init, createStore,
initialRoute, and OPENCODE_ROUTE are updated accordingly.

In `@packages/opencode/src/cli/cmd/tui/plugin/runtime.ts`:
- Around line 961-973: The dispose function currently awaits the load promise
stored in loaded (task) without handling rejections, which can abort cleanup and
leave runtime state inconsistent; wrap the await of task in a try/catch or
try/finally so that regardless of whether task resolves or rejects you still
clear loaded and dir, capture and preserve the error, set runtime to undefined,
and run the plugin teardown loop (calling deactivatePluginEntry for each plugin
from [...state.plugins].reverse()); after cleanup rethrow the original error if
one occurred so callers still see the rejection. Ensure references: function
dispose, variable loaded/task, variable dir, variable runtime/state, and
function deactivatePluginEntry are used to locate and implement the fix.

In `@packages/opencode/src/cli/cmd/tui/routes/session/dialog-message.tsx`:
- Around line 32-35: The fire-and-forget call to sdk.client.session.revert({
sessionID: props.sessionID, messageID: msg.id }) can produce unhandled promise
rejections; update the call site in dialog-message.tsx to append a terminal
.catch handler (e.g., .catch(() => {}) or .catch(err =>
processLogger?.error('revert failed', err))) so errors from
sdk.client.session.revert(...) are consumed; specifically modify the void
sdk.client.session.revert({...}) invocation to include the .catch(...) chain to
mirror other usages like in pages/session.tsx.

In `@packages/opencode/src/cli/cmd/tui/routes/session/index.tsx`:
- Around line 177-201: The effect can apply results after the user navigates to
a different session; to prevent stale mutations, capture the current
route.sessionID at the start (e.g., const currentSession = route.sessionID) and
after each await (after sdk.client.session.get, after sync.bootstrap, and after
sync.session.sync) check if route.sessionID !== currentSession and bail out
early if so; also, where possible pass an AbortSignal to network calls
(sdk.client.session.get) so in-flight requests can be cancelled when the route
changes, and avoid calling project.workspace.set, sync.bootstrap,
sync.session.sync, or scroll.scrollBy if the session changed.

In `@packages/opencode/src/cli/cmd/tui/routes/session/permission.tsx`:
- Around line 187-190: The three calls to sdk.client.permission.reply (e.g., the
one using props.request.id) currently use void to ignore the promise and lack
rejection handling; update each call (the blocks at the shown locations) to
append a .catch(...) that restores the UI stage via setStore("stage",
"permission") and surfaces/errors reports (e.g., call the existing error
reporting helper or set appropriate store values) so the UI isn’t left stale if
the reply fails; ensure you reference sdk.client.permission.reply,
props.request.id and setStore in the catch handler for consistent behavior
across all three sites.

In `@packages/opencode/src/cli/cmd/tui/routes/session/question.tsx`:
- Around line 48-51: The fire-and-forget RPC calls using void
sdk.client.question.reply({ requestID: props.request.id, answers }) should not
discard rejections; update the call sites (e.g., sdk.client.question.reply in
this file and the other similar usages around lines 55 and 70) to append an
explicit .catch(handler) that surfaces errors to the user (for example by
calling the app's toast/error notification function) and logs the error for
debugging; ensure the handler provides a concise user-facing message (e.g.,
"Failed to submit answer") and still logs the original error object for
diagnostics.

In `@packages/opencode/src/cli/cmd/tui/routes/session/subagent-footer.tsx`:
- Line 62: Remove the unused useTerminalDimensions hook: delete its import from
the top of the file and remove the standalone invocation useTerminalDimensions()
inside the subagent-footer component (the hook is not assigned to a variable and
its return value isn’t used); also verify there are no remaining references to
useTerminalDimensions in this component so unused-import lint errors are
resolved.

In `@packages/opencode/src/cli/cmd/tui/thread.ts`:
- Line 239: Remove the leftover temporary comment "// scratch" from the file
packages/opencode/src/cli/cmd/tui/thread.ts; locate the stray marker (the line
containing exactly "// scratch") and delete it so the source contains no unused
scratch annotations before merging.

In `@packages/opencode/src/cli/cmd/tui/util/terminal.ts`:
- Around line 61-82: The handler in colors() currently parses each incoming
Buffer chunk independently and can match partial OSC 10/11/4 payloads split
across chunks, causing incorrect background/foreground/paletteColors; fix by
accumulating chunks into a local buffer string and only extracting complete OSC
sequences that end with BEL (\x07) or ST (\x1b\\) before parsing; in handler
(and any surrounding colors() state) append data.toString() to a pendingBuffer,
use a regex that captures full sequences like
/\x1b\](?:10|11|4);([^\x07\x1b]*?)(?:\x07|\x1b\\)/g to iterate complete matches,
call parse(...) to set background/foreground/paletteColors (using index for OSC
4) and keep any trailing partial data in pendingBuffer for the next chunk.

In `@packages/opencode/test/cli/tui/_mock-tui-runtime.ts`:
- Around line 8-30: Before overwriting process.env.OPENCODE_PLUGIN_META_FILE,
capture its current value (e.g., const previousPluginMeta =
process.env.OPENCODE_PLUGIN_META_FILE) and then use that saved value inside the
restore() function of _mock-tui-runtime: instead of always deleting
OPENCODE_PLUGIN_META_FILE, restore the saved value (set
process.env.OPENCODE_PLUGIN_META_FILE = previousPluginMeta if defined, otherwise
delete it). Update restore() alongside the existing mocks (cwd.mockRestore(),
get.mockRestore(), wait.mockRestore()) so the environment is returned to its
original state.

In `@packages/opencode/test/cli/tui/terminal.test.ts`:
- Around line 9-11: The BEL-terminated OSC 11 response test currently only
asserts backgroundModeFromResponse("\x1b]11;`#ffffff`\x07") is not undefined;
change it to assert the concrete expected mode (e.g.,
expect(backgroundModeFromResponse("\x1b]11;`#ffffff`\x07")).toBe("light")) so the
parser returns the correct semantic mode for the `#ffffff` color; update the test
in terminal.test.ts referencing the backgroundModeFromResponse function to check
the exact value rather than just non-undefined.

---

Outside diff comments:
In `@packages/opencode/src/cli/cmd/tui/context/sdk.tsx`:
- Around line 77-101: The SSE setup can reject and escape the retry loop; wrap
the sdk.global.event(...) call in a try/catch inside the while loop (around the
call at sdk.global.event) and on error log/handle it and continue to the backoff
path instead of letting it bubble to the outer .catch; replace the current sleep
with an abort-aware wait by assigning setTimeout to the existing timer variable
and/or using the abort.signal to reject the sleep so the loop can exit
immediately when abort.signal or ctrl.signal is aborted; ensure timer is cleared
on both success and abort, and keep existing flush(), queue, handleEvent,
retryDelay and maxRetryDelay logic intact so reconnection attempts continue
after transient failures.

In `@packages/opencode/src/cli/cmd/tui/context/sync.tsx`:
- Around line 357-449: bootstrap can run concurrently and older invocations can
overwrite a newer workspace's store; fix by serializing/applying only the latest
run's results: at the start of bootstrap generate a unique run id (or increment
a module-level bootstrapCounter), assign it to a module-level latestBootstrapId,
capture it as localRunId, and use the captured workspace variable; before any
setStore/batch that applies responses (including the non-blocking
Promise.all.then), check that latestBootstrapId === localRunId (or that
syncedWorkspace === workspace) and abort applying results if mismatched; update
the places that clear fullSyncedSessions/syncedWorkspace accordingly so they
only run when the run is still current.

In `@packages/opencode/src/cli/cmd/tui/context/theme.tsx`:
- Around line 396-410: The theme renderer subscription was commented out so
store.mode no longer updates on renderer changes; re-enable the listener by
restoring the renderer.on(CliRenderEvents.THEME_MODE, handle) subscription after
the handle definition so theme changes propagate automatically (the handle
already guards with store.lock), and keep the existing
renderer.off(CliRenderEvents.THEME_MODE, handle) in onCleanup; ensure this
behavior complements unlock()/free() which perform a one-time sync but rely on
the active subscription for subsequent updates.

In `@packages/opencode/src/cli/cmd/tui/plugin/runtime.ts`:
- Around line 975-1028: The code sets the module-level runtime variable to a
partially initialized RuntimeState (next) before Instance.provide completes,
risking consumers seeing a broken state if provide throws; move the assignment
so runtime is only set after Instance.provide finishes successfully (i.e.,
remove or delay runtime = next before the try and assign runtime = next after
the await Instance.provide(...) completes), keeping the rest of load (creating
next, calling Instance.provide, loading internal/external plugins,
applyInitialPluginEnabledState, and activating plugins) unchanged; refer to the
load function, the runtime variable, the local next RuntimeState, and
Instance.provide to locate where to change the assignment.

In `@packages/opencode/src/cli/cmd/tui/ui/toast.tsx`:
- Around line 59-64: The toast auto-dismiss timeout is reading duration directly
from options and can be undefined (causing immediate dismissal); update the
handler in the toast display code to normalize duration (e.g., use a
DEFAULT_TOAST_DURATION constant or fallback like duration ??
DEFAULT_TOAST_DURATION and ensure it's a positive number) before calling
setTimeout; locate the toast creation logic around the variables options,
duration, setStore("currentToast", ...), and timeoutHandle, and replace the
destructuring or timeout argument so missing caller-supplied durations (e.g.,
toast.error calls) use the default instead of hiding immediately.

In `@packages/opencode/test/cli/tui/sync-provider.test.tsx`:
- Around line 90-107: The new createFetch helper exposes a rejectPaths injection
but the tests never use it; either update the failing-test(s) to drive the
failure via mount(log, { rejectPaths: ['/...'] }) (so createFetch will throw for
that pathname) or remove the unused rejectPaths branch. Specifically, in the
tests that currently stub project.workspace.sync, pass the rejectPaths array
into mount (or the test harness that calls createFetch) so the forced fetch
error path is exercised (reference createFetch, mount, and
project.workspace.sync to locate the change).

---

Nitpick comments:
In `@packages/opencode/test/cli/tui/plugin-loader-entrypoint.test.ts`:
- Around line 47-56: The repeated inline TuiConfig.Info test scaffolding should
be centralized using the tmpdir helper's config option instead of duplicating
objects; replace each inline config (the object with plugin and plugin_origins
referencing tmp.extra.spec/tmp.extra.marker and tmp.path) by calling tmpdir({
config: <partial TuiConfig.Info> }) so tmpdir writes the opencode.json (or tui
config) during setup, and update tests that reference tmp.path/tui.json to
depend on the tmpdir-produced config; locate occurrences by the TuiConfig.Info
literal and replace them with the shared tmpdir({ config }) fixture creation to
keep setup uniform.

In `@packages/opencode/test/cli/tui/plugin-toggle.test.ts`:
- Around line 42-54: Tests currently construct TuiConfig.Info inline (the const
config: TuiConfig.Info block) causing duplication; instead, update the tmpdir
setup calls in this test (and the similar block at lines 119-131) to pass a
config option to tmpdir, e.g. tmpdir({ config: { plugin: [[tmp.extra.spec, {
marker: tmp.extra.marker }]], plugin_enabled: { "demo.toggle": false },
plugin_origins: [...] } }), so the opencode.json is written by the fixture;
remove the inline TuiConfig.Info construction and any manual writes, keeping
references to tmp.extra.spec, tmp.extra.marker, and tmp.path.plugin_origins
intact.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 1e510ca0-3efd-418d-a772-5bfc202299cf

📥 Commits

Reviewing files that changed from the base of the PR and between 8caeb99 and 175d2a3.

📒 Files selected for processing (98)
  • packages/opencode/src/cli/cmd/tui/app.tsx
  • packages/opencode/src/cli/cmd/tui/attach.ts
  • packages/opencode/src/cli/cmd/tui/component/dialog-agent.tsx
  • packages/opencode/src/cli/cmd/tui/component/dialog-command.tsx
  • packages/opencode/src/cli/cmd/tui/component/dialog-mcp.tsx
  • packages/opencode/src/cli/cmd/tui/component/dialog-provider.tsx
  • packages/opencode/src/cli/cmd/tui/component/dialog-session-list.tsx
  • packages/opencode/src/cli/cmd/tui/component/dialog-session-rename.tsx
  • packages/opencode/src/cli/cmd/tui/component/dialog-theme-list.tsx
  • packages/opencode/src/cli/cmd/tui/component/dialog-workspace-create.tsx
  • packages/opencode/src/cli/cmd/tui/component/error-component.tsx
  • packages/opencode/src/cli/cmd/tui/component/prompt/autocomplete.tsx
  • packages/opencode/src/cli/cmd/tui/component/prompt/cwd.ts
  • packages/opencode/src/cli/cmd/tui/component/prompt/index.tsx
  • packages/opencode/src/cli/cmd/tui/config/cwd.ts
  • packages/opencode/src/cli/cmd/tui/context/kv.tsx
  • packages/opencode/src/cli/cmd/tui/context/local.tsx
  • packages/opencode/src/cli/cmd/tui/context/project.tsx
  • packages/opencode/src/cli/cmd/tui/context/route.tsx
  • packages/opencode/src/cli/cmd/tui/context/sdk.tsx
  • packages/opencode/src/cli/cmd/tui/context/sync.tsx
  • packages/opencode/src/cli/cmd/tui/context/theme.tsx
  • packages/opencode/src/cli/cmd/tui/context/theme/aura.json
  • packages/opencode/src/cli/cmd/tui/context/theme/ayu.json
  • packages/opencode/src/cli/cmd/tui/context/theme/carbonfox.json
  • packages/opencode/src/cli/cmd/tui/context/theme/catppuccin-frappe.json
  • packages/opencode/src/cli/cmd/tui/context/theme/catppuccin-macchiato.json
  • packages/opencode/src/cli/cmd/tui/context/theme/catppuccin.json
  • packages/opencode/src/cli/cmd/tui/context/theme/cobalt2.json
  • packages/opencode/src/cli/cmd/tui/context/theme/cursor.json
  • packages/opencode/src/cli/cmd/tui/context/theme/dracula.json
  • packages/opencode/src/cli/cmd/tui/context/theme/everforest.json
  • packages/opencode/src/cli/cmd/tui/context/theme/flexoki.json
  • packages/opencode/src/cli/cmd/tui/context/theme/github.json
  • packages/opencode/src/cli/cmd/tui/context/theme/gruvbox.json
  • packages/opencode/src/cli/cmd/tui/context/theme/kanagawa.json
  • packages/opencode/src/cli/cmd/tui/context/theme/lucent-orng.json
  • packages/opencode/src/cli/cmd/tui/context/theme/material.json
  • packages/opencode/src/cli/cmd/tui/context/theme/matrix.json
  • packages/opencode/src/cli/cmd/tui/context/theme/monokai.json
  • packages/opencode/src/cli/cmd/tui/context/theme/nightowl.json
  • packages/opencode/src/cli/cmd/tui/context/theme/nord.json
  • packages/opencode/src/cli/cmd/tui/context/theme/one-dark.json
  • packages/opencode/src/cli/cmd/tui/context/theme/opencode.json
  • packages/opencode/src/cli/cmd/tui/context/theme/orng.json
  • packages/opencode/src/cli/cmd/tui/context/theme/osaka-jade.json
  • packages/opencode/src/cli/cmd/tui/context/theme/palenight.json
  • packages/opencode/src/cli/cmd/tui/context/theme/rosepine.json
  • packages/opencode/src/cli/cmd/tui/context/theme/solarized.json
  • packages/opencode/src/cli/cmd/tui/context/theme/synthwave84.json
  • packages/opencode/src/cli/cmd/tui/context/theme/tokyonight.json
  • packages/opencode/src/cli/cmd/tui/context/theme/vercel.json
  • packages/opencode/src/cli/cmd/tui/context/theme/vesper.json
  • packages/opencode/src/cli/cmd/tui/context/theme/zenburn.json
  • packages/opencode/src/cli/cmd/tui/event.ts
  • packages/opencode/src/cli/cmd/tui/feature-plugins/home/tips-view.tsx
  • packages/opencode/src/cli/cmd/tui/feature-plugins/sidebar/footer.tsx
  • packages/opencode/src/cli/cmd/tui/feature-plugins/system/plugins.tsx
  • packages/opencode/src/cli/cmd/tui/plugin/api.tsx
  • packages/opencode/src/cli/cmd/tui/plugin/runtime.ts
  • packages/opencode/src/cli/cmd/tui/routes/home.tsx
  • packages/opencode/src/cli/cmd/tui/routes/session/dialog-fork-from-timeline.tsx
  • packages/opencode/src/cli/cmd/tui/routes/session/dialog-message.tsx
  • packages/opencode/src/cli/cmd/tui/routes/session/index.tsx
  • packages/opencode/src/cli/cmd/tui/routes/session/permission.tsx
  • packages/opencode/src/cli/cmd/tui/routes/session/question.tsx
  • packages/opencode/src/cli/cmd/tui/routes/session/sidebar.tsx
  • packages/opencode/src/cli/cmd/tui/routes/session/subagent-footer.tsx
  • packages/opencode/src/cli/cmd/tui/thread.ts
  • packages/opencode/src/cli/cmd/tui/ui/dialog-confirm.tsx
  • packages/opencode/src/cli/cmd/tui/ui/dialog-export-options.tsx
  • packages/opencode/src/cli/cmd/tui/ui/dialog-select.tsx
  • packages/opencode/src/cli/cmd/tui/ui/dialog.tsx
  • packages/opencode/src/cli/cmd/tui/ui/toast.tsx
  • packages/opencode/src/cli/cmd/tui/util/binary.ts
  • packages/opencode/src/cli/cmd/tui/util/clipboard.ts
  • packages/opencode/src/cli/cmd/tui/util/editor.ts
  • packages/opencode/src/cli/cmd/tui/util/revert-diff.ts
  • packages/opencode/src/cli/cmd/tui/util/selection.ts
  • packages/opencode/src/cli/cmd/tui/util/signal.ts
  • packages/opencode/src/cli/cmd/tui/util/terminal.ts
  • packages/opencode/src/cli/cmd/tui/win32.ts
  • packages/opencode/src/cli/cmd/tui/worker.ts
  • packages/opencode/test/cli/tui/_mock-tui-runtime.ts
  • packages/opencode/test/cli/tui/dialog-session-list.test.tsx
  • packages/opencode/test/cli/tui/dialog-workspace-create.test.ts
  • packages/opencode/test/cli/tui/plugin-add.test.ts
  • packages/opencode/test/cli/tui/plugin-install.test.ts
  • packages/opencode/test/cli/tui/plugin-lifecycle.test.ts
  • packages/opencode/test/cli/tui/plugin-loader-entrypoint.test.ts
  • packages/opencode/test/cli/tui/plugin-loader-pure.test.ts
  • packages/opencode/test/cli/tui/plugin-loader.test.ts
  • packages/opencode/test/cli/tui/plugin-toggle.test.ts
  • packages/opencode/test/cli/tui/revert-diff.test.ts
  • packages/opencode/test/cli/tui/sync-provider.test.tsx
  • packages/opencode/test/cli/tui/terminal.test.ts
  • packages/opencode/test/cli/tui/theme-store.test.ts
  • packages/opencode/test/cli/tui/thread.test.ts
💤 Files with no reviewable changes (2)
  • packages/opencode/src/cli/cmd/tui/event.ts
  • packages/opencode/src/cli/cmd/tui/worker.ts

Comment thread packages/opencode/src/cli/cmd/tui/component/dialog-session-list.tsx Outdated
Comment thread packages/opencode/src/cli/cmd/tui/component/error-component.tsx
Comment thread packages/opencode/src/cli/cmd/tui/routes/session/subagent-footer.tsx Outdated
Comment thread packages/opencode/src/cli/cmd/tui/thread.ts Outdated
Comment thread packages/opencode/src/cli/cmd/tui/util/terminal.ts Outdated
Comment thread packages/opencode/test/cli/tui/_mock-tui-runtime.ts Outdated
Comment thread packages/opencode/test/cli/tui/terminal.test.ts

@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: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
packages/opencode/src/cli/cmd/tui/component/error-component.tsx (1)

19-25: ⚠️ Potential issue | 🟠 Major

Prevent unhandled rejection during fatal-exit teardown.

handleExit can reject, and Line 29 fires it as fire-and-forget. If onBeforeExit/onExit fails, this can leak an unhandled rejection in the error screen path.

💡 Proposed fix
   const handleExit = async () => {
-    await props.onBeforeExit?.()
-    renderer.setTerminalTitle("")
-    renderer.destroy()
-    win32FlushInputBuffer()
-    await props.onExit()
+    try {
+      await props.onBeforeExit?.()
+      renderer.setTerminalTitle("")
+      renderer.destroy()
+      win32FlushInputBuffer()
+      await props.onExit()
+    } catch {
+      // best-effort exit path from fatal screen; avoid unhandled rejection
+    }
   }

Also applies to: 27-30

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/opencode/src/cli/cmd/tui/component/error-component.tsx` around lines
19 - 25, The handleExit async function can throw and is invoked fire-and-forget,
leading to unhandled rejections; update handleExit (and any other exit
invocations at the same spot) to catch and handle errors from props.onBeforeExit
and props.onExit by wrapping awaits in a try/catch (or surrounding the whole
body in try/catch), ensure renderer.destroy() and win32FlushInputBuffer() still
run in finally if needed, and log or swallow the error instead of letting the
promise reject unobserved; reference the handleExit function,
props.onBeforeExit, props.onExit, renderer.destroy, and win32FlushInputBuffer
when locating the code to modify.
🧹 Nitpick comments (2)
packages/opencode/src/cli/cmd/tui/routes/session/permission.tsx (1)

199-204: Optional: reduce duplication in permission reply callsites.

The void sdk.client.permission.reply(...).catch(...) blocks are now repeated in four places. Consider a small helper to keep behavior consistent and simplify future changes.

Also applies to: 211-217, 466-471, 474-479

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/opencode/src/cli/cmd/tui/routes/session/permission.tsx` around lines
199 - 204, Several callsites repeat the pattern void
sdk.client.permission.reply({...}).catch(error => handleReplyError("Failed to
update permission", error)); extract a small helper (e.g., sendPermissionReply)
that accepts requestID and reply string, calls sdk.client.permission.reply({
reply, requestID }) and attaches the common .catch handler to call
handleReplyError with the same message; then replace the four direct calls with
sendPermissionReply(props.request.id, "always") (and other reply values) to
centralize behavior and reduce duplication.
packages/opencode/src/cli/cmd/tui/routes/session/index.tsx (1)

1913-1929: Type assertions for tool metadata could be tightened.

The as { results?: number } and as { numResults?: number } assertions at lines 1914 and 1923 work, but relying on inline casts is fragile if the tool metadata schema changes.

Consider extracting these types from the tool definitions if available, or adding runtime checks.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/opencode/src/cli/cmd/tui/routes/session/index.tsx` around lines 1913
- 1929, The current inline casts in CodeSearch and WebSearch (metadata as {
results?: number } / { numResults?: number }) are fragile; update these
components to derive metadata types from the tool definitions (use the metadata
type exported or inferred from CodeSearchTool and WebSearchTool) instead of
using ad-hoc assertions, or add a small runtime type guard that checks typeof
metadata.results === 'number' / typeof metadata.numResults === 'number' before
rendering; adjust the props typing for CodeSearch and WebSearch to use the
extracted metadata type (or perform the guard) so the JSX uses a correctly typed
metadata value and avoids brittle casts.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@packages/opencode/src/cli/cmd/tui/routes/session/permission.tsx`:
- Around line 286-292: The title for the "list" permission can end up empty when
data.path is missing; inside the permission === "list" branch (where raw =
data.path, dir = typeof raw === "string" ? raw : "" and title uses
normalizePath(dir)), update the title to fall back to a default label when
normalizePath(dir) is empty (e.g., "List (root)" or "List /") so it never
renders as "List " — modify the return object's title expression to use the
fallback.

---

Outside diff comments:
In `@packages/opencode/src/cli/cmd/tui/component/error-component.tsx`:
- Around line 19-25: The handleExit async function can throw and is invoked
fire-and-forget, leading to unhandled rejections; update handleExit (and any
other exit invocations at the same spot) to catch and handle errors from
props.onBeforeExit and props.onExit by wrapping awaits in a try/catch (or
surrounding the whole body in try/catch), ensure renderer.destroy() and
win32FlushInputBuffer() still run in finally if needed, and log or swallow the
error instead of letting the promise reject unobserved; reference the handleExit
function, props.onBeforeExit, props.onExit, renderer.destroy, and
win32FlushInputBuffer when locating the code to modify.

---

Nitpick comments:
In `@packages/opencode/src/cli/cmd/tui/routes/session/index.tsx`:
- Around line 1913-1929: The current inline casts in CodeSearch and WebSearch
(metadata as { results?: number } / { numResults?: number }) are fragile; update
these components to derive metadata types from the tool definitions (use the
metadata type exported or inferred from CodeSearchTool and WebSearchTool)
instead of using ad-hoc assertions, or add a small runtime type guard that
checks typeof metadata.results === 'number' / typeof metadata.numResults ===
'number' before rendering; adjust the props typing for CodeSearch and WebSearch
to use the extracted metadata type (or perform the guard) so the JSX uses a
correctly typed metadata value and avoids brittle casts.

In `@packages/opencode/src/cli/cmd/tui/routes/session/permission.tsx`:
- Around line 199-204: Several callsites repeat the pattern void
sdk.client.permission.reply({...}).catch(error => handleReplyError("Failed to
update permission", error)); extract a small helper (e.g., sendPermissionReply)
that accepts requestID and reply string, calls sdk.client.permission.reply({
reply, requestID }) and attaches the common .catch handler to call
handleReplyError with the same message; then replace the four direct calls with
sendPermissionReply(props.request.id, "always") (and other reply values) to
centralize behavior and reduce duplication.
🪄 Autofix (Beta)

✅ Autofix completed


ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: b370de29-fd6a-4688-9e2d-59ed54518994

📥 Commits

Reviewing files that changed from the base of the PR and between 175d2a3 and 03a39b1.

📒 Files selected for processing (19)
  • packages/opencode/src/cli/cmd/tui/component/dialog-session-list.tsx
  • packages/opencode/src/cli/cmd/tui/component/dialog-session-rename.tsx
  • packages/opencode/src/cli/cmd/tui/component/dialog-workspace-create.tsx
  • packages/opencode/src/cli/cmd/tui/component/error-component.tsx
  • packages/opencode/src/cli/cmd/tui/component/prompt/index.tsx
  • packages/opencode/src/cli/cmd/tui/context/kv.tsx
  • packages/opencode/src/cli/cmd/tui/context/route.tsx
  • packages/opencode/src/cli/cmd/tui/plugin/runtime.ts
  • packages/opencode/src/cli/cmd/tui/routes/session/dialog-message.tsx
  • packages/opencode/src/cli/cmd/tui/routes/session/index.tsx
  • packages/opencode/src/cli/cmd/tui/routes/session/permission.tsx
  • packages/opencode/src/cli/cmd/tui/routes/session/question.tsx
  • packages/opencode/src/cli/cmd/tui/routes/session/subagent-footer.tsx
  • packages/opencode/src/cli/cmd/tui/thread.ts
  • packages/opencode/src/cli/cmd/tui/util/terminal.ts
  • packages/opencode/test/cli/tui/_mock-tui-runtime.ts
  • packages/opencode/test/cli/tui/dialog-workspace-create.test.ts
  • packages/opencode/test/cli/tui/plugin-lifecycle.test.ts
  • packages/opencode/test/cli/tui/terminal.test.ts
💤 Files with no reviewable changes (1)
  • packages/opencode/src/cli/cmd/tui/routes/session/subagent-footer.tsx
✅ Files skipped from review due to trivial changes (1)
  • packages/opencode/src/cli/cmd/tui/component/dialog-session-list.tsx
🚧 Files skipped from review as they are similar to previous changes (11)
  • packages/opencode/src/cli/cmd/tui/thread.ts
  • packages/opencode/src/cli/cmd/tui/routes/session/question.tsx
  • packages/opencode/src/cli/cmd/tui/component/dialog-session-rename.tsx
  • packages/opencode/src/cli/cmd/tui/routes/session/dialog-message.tsx
  • packages/opencode/test/cli/tui/plugin-lifecycle.test.ts
  • packages/opencode/test/cli/tui/_mock-tui-runtime.ts
  • packages/opencode/src/cli/cmd/tui/context/route.tsx
  • packages/opencode/test/cli/tui/dialog-workspace-create.test.ts
  • packages/opencode/test/cli/tui/terminal.test.ts
  • packages/opencode/src/cli/cmd/tui/component/dialog-workspace-create.tsx
  • packages/opencode/src/cli/cmd/tui/plugin/runtime.ts

Comment thread packages/opencode/src/cli/cmd/tui/routes/session/permission.tsx
@coderabbitai

coderabbitai Bot commented Apr 20, 2026

Copy link
Copy Markdown
Contributor

Note

Autofix is a beta feature. Expect some limitations and changes as we gather feedback and continue to improve it.

Fixes Applied Successfully

Fixed 1 file(s) based on 1 unresolved review comment.

Files modified:

  • packages/opencode/src/cli/cmd/tui/routes/session/permission.tsx

Commit: c48b699acfaf633c56f84140f36dddb00281cd90

The changes have been pushed to the codex/tui-compat-sync branch.

Time taken: 6m 48s

@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

♻️ Duplicate comments (1)
packages/opencode/src/cli/cmd/tui/plugin/runtime.ts (1)

973-981: ⚠️ Potential issue | 🟠 Major

Preserve loadError propagation even when no runtime state exists.

At Line 975, the early return suppresses loadError captured at Lines 966-971 when runtime is undefined. That hides initialization failures from dispose() callers.

💡 Proposed fix
 export async function dispose() {
   const task = loaded
   loaded = undefined
   dir = ""
   let loadError: unknown
   if (task) {
     try {
       await task
     } catch (error) {
       loadError = error
     }
   }
   const state = runtime
   runtime = undefined
-  if (!state) return
+  if (!state) {
+    if (loadError) throw loadError
+    return
+  }
   const queue = [...state.plugins].reverse()
   for (const plugin of queue) {
     await deactivatePluginEntry(state, plugin, false)
   }
   if (loadError) throw loadError
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/opencode/src/cli/cmd/tui/plugin/runtime.ts` around lines 973 - 981,
The early return when runtime is undefined suppresses a previously-captured
loadError; update the dispose logic in runtime.ts so loadError is propagated
even if no state exists — e.g., check and throw loadError before returning when
state is falsy (or change the !state branch to first throw loadError if present,
then return). Refer to the local variables runtime, state and loadError and the
surrounding dispose flow that later iterates plugins and calls
deactivatePluginEntry(state, plugin, false).
🧹 Nitpick comments (2)
packages/opencode/test/cli/tui/toast.test.ts (1)

14-16: Add non-finite input cases (NaN, Infinity) to complete branch coverage.

packages/opencode/src/cli/cmd/tui/ui/toast.tsx (Lines 11-18) explicitly normalizes non-finite values, but this path is not exercised yet.

✅ Suggested test addition
 describe("toast duration", () => {
@@
   test("keeps explicit positive durations", () => {
     expect(normalizeToastDuration(1500)).toBe(1500)
   })
+
+  test("falls back to the default duration when non-finite", () => {
+    expect(normalizeToastDuration(Number.NaN)).toBe(DEFAULT_TOAST_DURATION_MS)
+    expect(normalizeToastDuration(Number.POSITIVE_INFINITY)).toBe(DEFAULT_TOAST_DURATION_MS)
+  })
 })
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/opencode/test/cli/tui/toast.test.ts` around lines 14 - 16, Add unit
tests that exercise the non-finite branches by calling normalizeToastDuration
with NaN and with Infinity (and optionally -Infinity) and asserting the returned
value is a finite, valid duration; specifically, in toast.test.ts import
normalizeToastDuration and add expectations like
Number.isFinite(normalizeToastDuration(NaN)) and
Number.isFinite(normalizeToastDuration(Infinity)) and that those values match
the normalized fallback (e.g., compare to normalizeToastDuration(undefined) or
another known valid normalized value) so the non-finite branches in
normalizeToastDuration are covered.
packages/opencode/test/cli/tui/permission-title.test.ts (1)

1-12: LGTM! Good test coverage for the title formatting fix.

The tests correctly verify that formatListPermissionTitle returns the fallback "List directory" for empty input and properly formats normalized paths like ".". This validates the edge case handling that was flagged in the previous review.

Consider adding tests for additional edge cases if this function handles more complex paths:

💡 Optional: Additional test cases
test("falls back for undefined input", () => {
  expect(formatListPermissionTitle(undefined)).toBe("List directory")
})

test("normalizes relative paths", () => {
  expect(formatListPermissionTitle("src")).toBe("List src")
})
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/opencode/test/cli/tui/permission-title.test.ts` around lines 1 - 12,
Add optional edge-case tests to ensure formatListPermissionTitle handles
undefined and non-empty relative paths: update the test suite in
permission-title.test.ts to call formatListPermissionTitle(undefined) and expect
"List directory", and add another test calling formatListPermissionTitle("src")
(or another relative path) expecting "List src"; reference the existing tests
(formatListPermissionTitle in this file) to mirror structure and assertions so
coverage includes undefined input and normal relative-path formatting.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@packages/opencode/src/cli/cmd/tui/context/sdk.tsx`:
- Around line 78-91: The backoff counter `attempt` is never reset, causing
future reconnects to use large delays; after a successful call to `await
sdk.global.event(...)` (i.e., when `events` is set), reset `attempt` back to 0
so subsequent disconnects start from `retryDelay` again; apply the same change
to the other analogous loop (the block around the second `sdk.global.event`
usage referred to in the comment) and ensure the reset occurs before the loop
continues or waits for the next connection attempt.
- Around line 92-100: The backoff Promise is adding abort.signal and ctrl.signal
listeners each iteration but never removing them when the timeout resolves
normally; modify the Promise in the reconnect sleep (the block creating timer,
stop(), and adding abort.signal.addEventListener and
ctrl.signal.addEventListener) to remove those listeners after the timer resolves
— e.g., ensure the resolve path clears the timeout and also calls
removeEventListener for both abort.signal and ctrl.signal (and clean up any
references to stop) so listeners are not retained; apply the same change to the
duplicate block around the 116-124 region.

In `@packages/opencode/src/cli/cmd/tui/context/sync.tsx`:
- Around line 486-489: The getter ready returns true when OPENCODE_FAST_BOOT is
set but related data getters (e.g., config, provider) are returned unguarded,
letting plugins access incomplete state; update each data property getter (the
same ones alongside the existing vcs property) to mirror vcs's conditional
pattern and only return their underlying store values when store.status !==
"loading" (or otherwise not loading), otherwise return null/undefined so
consumers cannot read incomplete data even when OPENCODE_FAST_BOOT is enabled.

In `@packages/opencode/test/cli/tui/theme-provider.test.tsx`:
- Around line 55-60: Replace the broad await wait(() => onCalls.length > 0) with
a wait for the specific THEME_MODE listener and readiness of the mode accessor:
wait until onCalls contains an entry whose first element equals
CliRenderEvents.THEME_MODE and until mode is assigned/usable, then find the hit
via onCalls.find(call => call[0] === CliRenderEvents.THEME_MODE), assert hit
exists and that hit[1] is a function, invoke it with "light", and finally assert
mode() returns "light"; reference the symbols onCalls,
CliRenderEvents.THEME_MODE, hit, and mode to locate and update the test.

In `@packages/opencode/test/config/config.test.ts`:
- Around line 839-879: Narrow the critical section guarded by withConfigDepsLock
and remove the fixed 1s sleep: convert the test to use it.live(...) (since it
touches real filesystem/locks), acquire withConfigDepsLock only around the parts
that need the shared config dependency (e.g., the setup that writes to
process.env.OPENCODE_CONFIG_DIR and the Instance.provide() call that initializes
Config via Config.Service.use / Config.Service.waitForDependencies), then
release the lock before awaiting background work or asserting Filesystem reads;
delete the setTimeout-based delay and instead wait deterministically for the
condition (e.g., poll Filesystem.exists/readText or await the Instance.provide
scope completion) so the lock isn’t held during that wait and flakes are
avoided.

---

Duplicate comments:
In `@packages/opencode/src/cli/cmd/tui/plugin/runtime.ts`:
- Around line 973-981: The early return when runtime is undefined suppresses a
previously-captured loadError; update the dispose logic in runtime.ts so
loadError is propagated even if no state exists — e.g., check and throw
loadError before returning when state is falsy (or change the !state branch to
first throw loadError if present, then return). Refer to the local variables
runtime, state and loadError and the surrounding dispose flow that later
iterates plugins and calls deactivatePluginEntry(state, plugin, false).

---

Nitpick comments:
In `@packages/opencode/test/cli/tui/permission-title.test.ts`:
- Around line 1-12: Add optional edge-case tests to ensure
formatListPermissionTitle handles undefined and non-empty relative paths: update
the test suite in permission-title.test.ts to call
formatListPermissionTitle(undefined) and expect "List directory", and add
another test calling formatListPermissionTitle("src") (or another relative path)
expecting "List src"; reference the existing tests (formatListPermissionTitle in
this file) to mirror structure and assertions so coverage includes undefined
input and normal relative-path formatting.

In `@packages/opencode/test/cli/tui/toast.test.ts`:
- Around line 14-16: Add unit tests that exercise the non-finite branches by
calling normalizeToastDuration with NaN and with Infinity (and optionally
-Infinity) and asserting the returned value is a finite, valid duration;
specifically, in toast.test.ts import normalizeToastDuration and add
expectations like Number.isFinite(normalizeToastDuration(NaN)) and
Number.isFinite(normalizeToastDuration(Infinity)) and that those values match
the normalized fallback (e.g., compare to normalizeToastDuration(undefined) or
another known valid normalized value) so the non-finite branches in
normalizeToastDuration are covered.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 5cf7bf63-d63b-45a7-a3d1-d366288ae462

📥 Commits

Reviewing files that changed from the base of the PR and between c48b699 and de9aba2.

📒 Files selected for processing (14)
  • packages/opencode/src/cli/cmd/tui/component/error-component.tsx
  • packages/opencode/src/cli/cmd/tui/context/sdk.tsx
  • packages/opencode/src/cli/cmd/tui/context/sync.tsx
  • packages/opencode/src/cli/cmd/tui/context/theme.tsx
  • packages/opencode/src/cli/cmd/tui/plugin/runtime.ts
  • packages/opencode/src/cli/cmd/tui/routes/session/permission.tsx
  • packages/opencode/src/cli/cmd/tui/ui/toast.tsx
  • packages/opencode/test/cli/tui/error-component.test.ts
  • packages/opencode/test/cli/tui/permission-title.test.ts
  • packages/opencode/test/cli/tui/plugin-lifecycle.test.ts
  • packages/opencode/test/cli/tui/sync-provider.test.tsx
  • packages/opencode/test/cli/tui/theme-provider.test.tsx
  • packages/opencode/test/cli/tui/toast.test.ts
  • packages/opencode/test/config/config.test.ts
🚧 Files skipped from review as they are similar to previous changes (4)
  • packages/opencode/src/cli/cmd/tui/ui/toast.tsx
  • packages/opencode/src/cli/cmd/tui/component/error-component.tsx
  • packages/opencode/src/cli/cmd/tui/context/theme.tsx
  • packages/opencode/test/cli/tui/plugin-lifecycle.test.ts

Comment thread packages/opencode/src/cli/cmd/tui/context/sdk.tsx
Comment thread packages/opencode/src/cli/cmd/tui/context/sdk.tsx Outdated
Comment thread packages/opencode/src/cli/cmd/tui/context/sync.tsx
Comment thread packages/opencode/test/cli/tui/theme-provider.test.tsx Outdated
Comment thread packages/opencode/test/config/config.test.ts Outdated
@Astro-Han Astro-Han merged commit cd830af into dev Apr 20, 2026
14 checks passed
@Astro-Han Astro-Han deleted the codex/tui-compat-sync branch April 20, 2026 17:09
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.

1 participant