Skip to content

fix(tui): contain plugin dialogs that throw while rendering - #62110

Merged
Tarquinen merged 1 commit into
devfrom
plugin-dialog-boundary
Nov 5, 2026
Merged

Tarquinen merged 1 commit into
devfrom
plugin-dialog-boundary

Conversation

@Tarquinen

@Tarquinen Tarquinen commented Nov 5, 2026 •

Copy link
Copy Markdown
Member

Issue for this PR

Closes #61101

Type of change

  • Bug fix
  • New feature
  • Refactor / code improvement
  • Documentation

What does this PR do?

Problem: If a plugin dialog throws while it renders, the whole TUI goes down with the "OpenCode crashed" screen. That's what happens with /supabase from opencode-supabase, which still uses v1 theme names (undefined is not an object (evaluating 'e.length')). Plugin routes and slots are already wrapped in PluginBoundary, but content passed to ctx.ui.dialog.show was rendered without it.

Change: ctx.ui.dialog.show content is now wrapped in the same PluginBoundary, using where="dialog". A crash shows the usual error toast (<plugin> crashed in dialog: ...) and closes the dialog. Without the close, an empty dialog would stay open and keep focus away from the prompt. The plugin's theme names still need to be fixed in its own repo.

How did you verify your code works?

  • Ran the real dev TUI with a throwaway local plugin whose dialog throws during render. On dev it hits the crash screen. On this branch it shows the error toast, the dialog closes, and the prompt still works (screenshots below).
  • Added a test in packages/tui/test/plugin-slot-output.test.tsx with a fixture plugin whose dialog throws. It fails without the fix ("OpenCode crashed"), and also fails if the dialog isn't closed (focus stays stuck). With the fix it passes: bun test test/plugin-slot-output.test.tsx --rerun-each 10 → 20 pass.
  • bun test test/plugin-select.test.tsx test/plugin-toast.test.tsx test/plugin-keymap.test.tsx → 23 pass
  • bun typecheck in packages/tui → no errors
  • bun run check at the repo root → 35/35 tasks successful

Screenshots / recordings

Before:

before

After:

after

Visual

<svg xmlns="http://www.w3.org/2000/svg" width="240" height="72" viewBox="0 0 240 72" fill="none" stroke="currentColor">
  <style>
    .dlg { animation: dlg 4s ease-in-out infinite; transform-origin: 120px 30px; }
    .toast { animation: toast 4s ease-in-out infinite; }
    @keyframes dlg { 0%, 15% { opacity: 0; transform: scale(.9); } 25%, 45% { opacity: .9; transform: scale(1); } 55%, 100% { opacity: 0; transform: scale(.95); } }
    @keyframes toast { 0%, 50% { opacity: 0; transform: translateX(12px); } 60%, 90% { opacity: 1; transform: translateX(0); } 100% { opacity: 0; } }
    @media (prefers-reduced-motion: reduce) { .dlg, .toast { animation: none; } .dlg { opacity: 0; } }
  </style>
  <rect x="4" y="4" width="232" height="64" rx="6" opacity=".35"/>
  <rect x="14" y="52" width="150" height="10" rx="3" opacity=".5"/>
  <g class="dlg">
    <rect x="70" y="14" width="100" height="32" rx="4" opacity=".8"/>
    <path d="M110 24 l20 12 M130 24 l-20 12" stroke-width="2"/>
  </g>
  <g class="toast">
    <rect x="170" y="10" width="60" height="22" rx="3" fill="currentColor" fill-opacity=".12"/>
    <circle cx="180" cy="21" r="3" fill="currentColor" stroke="none"/>
    <path d="M188 18 h34 M188 24 h24" opacity=".7"/>
  </g>
</svg>

Checklist

  • I have tested my changes locally
  • I have not included unrelated changes in this PR

Added by the opencode bot (AI-generated).

Related: #62051

@opencodejr opencodejr 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.

Looks good. I reproduced #61101 from source with the issue's script: on the base, opening the plugin dialog brings up the "OpenCode crashed" screen. With this PR the TUI stays up, the repro-dialog crashed in dialog: … toast appears, the dialog closes and the prompt still takes input. The error in that script comes from an effect, not the render itself, so that case is covered too. The new test and the nearby plugin tests pass, and tsc -b is clean in packages/tui.

Public API is unchanged. PluginBoundary is now exported and takes an optional onCrash, but both are internal to the TUI. Plugins see no change, except that a crashing ctx.ui.dialog.show dialog now closes instead of crashing the app.

No concerns.

Related: #62051

Before

After

@Tarquinen
Tarquinen merged commit 7452716 into dev Nov 5, 2026
16 checks passed
@Tarquinen
Tarquinen deleted the plugin-dialog-boundary branch November 5, 2026 16:12
cevr added a commit to cevr/opencode that referenced this pull request Nov 6, 2026
Brings upstream's performance work (part rows anomalyco#58916, stale tool calls
anomalyco#61421, skip decoding a completed assistant anomalyco#61159, OpenTUI 0.8.1 anomalyco#60913,
stable keymap commands anomalyco#59294, history flash anomalyco#59236, plugin dialog
containment anomalyco#62110) with the rest of dev, as one merge rather than
cherry-picks so later syncs stay merges against a shared history.

Conflicts and how they were resolved:
- core/src/config/normalize.ts: upstream decodes every experimental field
  generically; took upstream (the fork's shell_background_after is a field
  of ConfigExperimental.Info, so the loop decodes it).
- core/src/job.ts: kept the fork's undelivered/awaitDelivered beside
  upstream's backgroundJobs.
- core/src/session/background.ts: kept the fork's awaitQuiet beside
  upstream's list.
- core/src/tool.ts: kept the fork's isRuntime beside upstream's commit.
- core/src/shell.ts: upstream's "File" entry type with the fork's
  rotated .out.1 cleanup.
- client/src/promise/generated/types.ts: both, regenerated.
- core/test/tool-shell.test.ts: the fork's handle-shaped tests, with
  upstream's withTempDir import for its new tests.
@amrkmn amrkmn mentioned this pull request Nov 7, 2026
1 task
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.

TUI crash: undefined is not an object (evaluating 'e.length')

1 participant