fix: sync opencode tui compatibility slice#73
Conversation
|
Note Reviews pausedIt 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 Use the following commands to manage reviews:
Use the checkboxes below for quick actions:
📝 WalkthroughWalkthroughRefactors 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
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
Estimated code review effort🎯 4 (Complex) | ⏱️ ~75 minutes Suggested labels
🚥 Pre-merge checks | ✅ 2 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (2 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches🧪 Generate unit tests (beta)
Comment |
Dependency Review✅ No vulnerabilities or license issues or OpenSSF Scorecard issues found.Scanned FilesNone |
There was a problem hiding this comment.
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.
|
@coderabbitai review |
✅ Actions performedReview triggered.
|
There was a problem hiding this comment.
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 | 🟠 MajorDefault toast duration was dropped, which can auto-dismiss toasts immediately.
At Line 59,
durationis now read directly fromoptionswithout normalization. Callers liketoast.error(...)omitduration, 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
rejectPathsis untested in this file.
createFetch()/mount()now expose a fetch-failure injection path, but the new test never passesrejectPaths; it only replacesproject.workspace.sync. Either drive the failure throughmount(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 | 🟠 MajorReconnects 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. WithsseMaxRetryAttempts: 0, this permanently disables event reconnections after any transient failure. The backoffsetTimeoutat 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 | 🟠 MajorUnlocked theme mode stops tracking renderer theme changes.
The
CliRenderEvents.THEME_MODEsubscription is commented out. Whenunlock()is called, it syncs once viafree(), but subsequent renderer theme-mode changes won't updatestore.modeunless 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 | 🟠 MajorSerialize
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 | 🟠 MajorPartial initialization:
runtimeis set before try block completes.
runtime = nextis assigned on line 987 before the try block. IfInstance.providethrows,runtimeremains pointing to the partially initializednextobject. Subsequent calls tolist(),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: Prefertmpdir({ config })over inline runtime config blocks in tests.Both test setups duplicate manual
TuiConfig.Infoconstruction. Moving this intotmpdirsetup will reduce drift and keep test fixtures consistent.As per coding guidelines, “Use the
configoption intmpdirto write anopencode.jsonconfig 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.Infoblocks make the suite harder to evolve safely. Consider centralizing viatmpdir({ config })or a local helper to keep test setup uniform.As per coding guidelines, “Use the
configoption intmpdirto write anopencode.jsonconfig 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
📒 Files selected for processing (98)
packages/opencode/src/cli/cmd/tui/app.tsxpackages/opencode/src/cli/cmd/tui/attach.tspackages/opencode/src/cli/cmd/tui/component/dialog-agent.tsxpackages/opencode/src/cli/cmd/tui/component/dialog-command.tsxpackages/opencode/src/cli/cmd/tui/component/dialog-mcp.tsxpackages/opencode/src/cli/cmd/tui/component/dialog-provider.tsxpackages/opencode/src/cli/cmd/tui/component/dialog-session-list.tsxpackages/opencode/src/cli/cmd/tui/component/dialog-session-rename.tsxpackages/opencode/src/cli/cmd/tui/component/dialog-theme-list.tsxpackages/opencode/src/cli/cmd/tui/component/dialog-workspace-create.tsxpackages/opencode/src/cli/cmd/tui/component/error-component.tsxpackages/opencode/src/cli/cmd/tui/component/prompt/autocomplete.tsxpackages/opencode/src/cli/cmd/tui/component/prompt/cwd.tspackages/opencode/src/cli/cmd/tui/component/prompt/index.tsxpackages/opencode/src/cli/cmd/tui/config/cwd.tspackages/opencode/src/cli/cmd/tui/context/kv.tsxpackages/opencode/src/cli/cmd/tui/context/local.tsxpackages/opencode/src/cli/cmd/tui/context/project.tsxpackages/opencode/src/cli/cmd/tui/context/route.tsxpackages/opencode/src/cli/cmd/tui/context/sdk.tsxpackages/opencode/src/cli/cmd/tui/context/sync.tsxpackages/opencode/src/cli/cmd/tui/context/theme.tsxpackages/opencode/src/cli/cmd/tui/context/theme/aura.jsonpackages/opencode/src/cli/cmd/tui/context/theme/ayu.jsonpackages/opencode/src/cli/cmd/tui/context/theme/carbonfox.jsonpackages/opencode/src/cli/cmd/tui/context/theme/catppuccin-frappe.jsonpackages/opencode/src/cli/cmd/tui/context/theme/catppuccin-macchiato.jsonpackages/opencode/src/cli/cmd/tui/context/theme/catppuccin.jsonpackages/opencode/src/cli/cmd/tui/context/theme/cobalt2.jsonpackages/opencode/src/cli/cmd/tui/context/theme/cursor.jsonpackages/opencode/src/cli/cmd/tui/context/theme/dracula.jsonpackages/opencode/src/cli/cmd/tui/context/theme/everforest.jsonpackages/opencode/src/cli/cmd/tui/context/theme/flexoki.jsonpackages/opencode/src/cli/cmd/tui/context/theme/github.jsonpackages/opencode/src/cli/cmd/tui/context/theme/gruvbox.jsonpackages/opencode/src/cli/cmd/tui/context/theme/kanagawa.jsonpackages/opencode/src/cli/cmd/tui/context/theme/lucent-orng.jsonpackages/opencode/src/cli/cmd/tui/context/theme/material.jsonpackages/opencode/src/cli/cmd/tui/context/theme/matrix.jsonpackages/opencode/src/cli/cmd/tui/context/theme/monokai.jsonpackages/opencode/src/cli/cmd/tui/context/theme/nightowl.jsonpackages/opencode/src/cli/cmd/tui/context/theme/nord.jsonpackages/opencode/src/cli/cmd/tui/context/theme/one-dark.jsonpackages/opencode/src/cli/cmd/tui/context/theme/opencode.jsonpackages/opencode/src/cli/cmd/tui/context/theme/orng.jsonpackages/opencode/src/cli/cmd/tui/context/theme/osaka-jade.jsonpackages/opencode/src/cli/cmd/tui/context/theme/palenight.jsonpackages/opencode/src/cli/cmd/tui/context/theme/rosepine.jsonpackages/opencode/src/cli/cmd/tui/context/theme/solarized.jsonpackages/opencode/src/cli/cmd/tui/context/theme/synthwave84.jsonpackages/opencode/src/cli/cmd/tui/context/theme/tokyonight.jsonpackages/opencode/src/cli/cmd/tui/context/theme/vercel.jsonpackages/opencode/src/cli/cmd/tui/context/theme/vesper.jsonpackages/opencode/src/cli/cmd/tui/context/theme/zenburn.jsonpackages/opencode/src/cli/cmd/tui/event.tspackages/opencode/src/cli/cmd/tui/feature-plugins/home/tips-view.tsxpackages/opencode/src/cli/cmd/tui/feature-plugins/sidebar/footer.tsxpackages/opencode/src/cli/cmd/tui/feature-plugins/system/plugins.tsxpackages/opencode/src/cli/cmd/tui/plugin/api.tsxpackages/opencode/src/cli/cmd/tui/plugin/runtime.tspackages/opencode/src/cli/cmd/tui/routes/home.tsxpackages/opencode/src/cli/cmd/tui/routes/session/dialog-fork-from-timeline.tsxpackages/opencode/src/cli/cmd/tui/routes/session/dialog-message.tsxpackages/opencode/src/cli/cmd/tui/routes/session/index.tsxpackages/opencode/src/cli/cmd/tui/routes/session/permission.tsxpackages/opencode/src/cli/cmd/tui/routes/session/question.tsxpackages/opencode/src/cli/cmd/tui/routes/session/sidebar.tsxpackages/opencode/src/cli/cmd/tui/routes/session/subagent-footer.tsxpackages/opencode/src/cli/cmd/tui/thread.tspackages/opencode/src/cli/cmd/tui/ui/dialog-confirm.tsxpackages/opencode/src/cli/cmd/tui/ui/dialog-export-options.tsxpackages/opencode/src/cli/cmd/tui/ui/dialog-select.tsxpackages/opencode/src/cli/cmd/tui/ui/dialog.tsxpackages/opencode/src/cli/cmd/tui/ui/toast.tsxpackages/opencode/src/cli/cmd/tui/util/binary.tspackages/opencode/src/cli/cmd/tui/util/clipboard.tspackages/opencode/src/cli/cmd/tui/util/editor.tspackages/opencode/src/cli/cmd/tui/util/revert-diff.tspackages/opencode/src/cli/cmd/tui/util/selection.tspackages/opencode/src/cli/cmd/tui/util/signal.tspackages/opencode/src/cli/cmd/tui/util/terminal.tspackages/opencode/src/cli/cmd/tui/win32.tspackages/opencode/src/cli/cmd/tui/worker.tspackages/opencode/test/cli/tui/_mock-tui-runtime.tspackages/opencode/test/cli/tui/dialog-session-list.test.tsxpackages/opencode/test/cli/tui/dialog-workspace-create.test.tspackages/opencode/test/cli/tui/plugin-add.test.tspackages/opencode/test/cli/tui/plugin-install.test.tspackages/opencode/test/cli/tui/plugin-lifecycle.test.tspackages/opencode/test/cli/tui/plugin-loader-entrypoint.test.tspackages/opencode/test/cli/tui/plugin-loader-pure.test.tspackages/opencode/test/cli/tui/plugin-loader.test.tspackages/opencode/test/cli/tui/plugin-toggle.test.tspackages/opencode/test/cli/tui/revert-diff.test.tspackages/opencode/test/cli/tui/sync-provider.test.tsxpackages/opencode/test/cli/tui/terminal.test.tspackages/opencode/test/cli/tui/theme-store.test.tspackages/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
There was a problem hiding this comment.
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 | 🟠 MajorPrevent unhandled rejection during fatal-exit teardown.
handleExitcan reject, and Line 29 fires it as fire-and-forget. IfonBeforeExit/onExitfails, 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 }andas { 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
📒 Files selected for processing (19)
packages/opencode/src/cli/cmd/tui/component/dialog-session-list.tsxpackages/opencode/src/cli/cmd/tui/component/dialog-session-rename.tsxpackages/opencode/src/cli/cmd/tui/component/dialog-workspace-create.tsxpackages/opencode/src/cli/cmd/tui/component/error-component.tsxpackages/opencode/src/cli/cmd/tui/component/prompt/index.tsxpackages/opencode/src/cli/cmd/tui/context/kv.tsxpackages/opencode/src/cli/cmd/tui/context/route.tsxpackages/opencode/src/cli/cmd/tui/plugin/runtime.tspackages/opencode/src/cli/cmd/tui/routes/session/dialog-message.tsxpackages/opencode/src/cli/cmd/tui/routes/session/index.tsxpackages/opencode/src/cli/cmd/tui/routes/session/permission.tsxpackages/opencode/src/cli/cmd/tui/routes/session/question.tsxpackages/opencode/src/cli/cmd/tui/routes/session/subagent-footer.tsxpackages/opencode/src/cli/cmd/tui/thread.tspackages/opencode/src/cli/cmd/tui/util/terminal.tspackages/opencode/test/cli/tui/_mock-tui-runtime.tspackages/opencode/test/cli/tui/dialog-workspace-create.test.tspackages/opencode/test/cli/tui/plugin-lifecycle.test.tspackages/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
|
Note Autofix is a beta feature. Expect some limitations and changes as we gather feedback and continue to improve it. Fixes Applied SuccessfullyFixed 1 file(s) based on 1 unresolved review comment. Files modified:
Commit: The changes have been pushed to the Time taken: |
Fixed 1 file(s) based on 1 unresolved review comment. Co-authored-by: CodeRabbit <noreply@coderabbit.ai>
There was a problem hiding this comment.
Actionable comments posted: 5
♻️ Duplicate comments (1)
packages/opencode/src/cli/cmd/tui/plugin/runtime.ts (1)
973-981:⚠️ Potential issue | 🟠 MajorPreserve
loadErrorpropagation even when no runtime state exists.At Line 975, the early return suppresses
loadErrorcaptured at Lines 966-971 whenruntimeis undefined. That hides initialization failures fromdispose()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
formatListPermissionTitlereturns 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
📒 Files selected for processing (14)
packages/opencode/src/cli/cmd/tui/component/error-component.tsxpackages/opencode/src/cli/cmd/tui/context/sdk.tsxpackages/opencode/src/cli/cmd/tui/context/sync.tsxpackages/opencode/src/cli/cmd/tui/context/theme.tsxpackages/opencode/src/cli/cmd/tui/plugin/runtime.tspackages/opencode/src/cli/cmd/tui/routes/session/permission.tsxpackages/opencode/src/cli/cmd/tui/ui/toast.tsxpackages/opencode/test/cli/tui/error-component.test.tspackages/opencode/test/cli/tui/permission-title.test.tspackages/opencode/test/cli/tui/plugin-lifecycle.test.tspackages/opencode/test/cli/tui/sync-provider.test.tsxpackages/opencode/test/cli/tui/theme-provider.test.tsxpackages/opencode/test/cli/tui/toast.test.tspackages/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
Summary
Why
This lands the remaining
sync/opencode-tui-compatwork 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
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
devbranchFollow-up
pr-title-lintworkflow permission hotfix merged via fix: grant pr title lint status permission #76 to unblock the advisory title check on this PR.Summary by CodeRabbit
New Features
Bug Fixes
UI/UX Improvements