[PR #12411] fix(tui): add isDestroyed guards to prevent EditBuffer crash on session switch #14204

Open
opened 2026-02-16 18:19:01 -05:00 by yindo · 0 comments
Owner

Original Pull Request: https://github.com/anomalyco/opencode/pull/12411

State: open
Merged: No


Problem

When switching sessions via leader key + keybind, the TUI crashes with:

Error: EditBuffer is destroyed
    at guard (@opentui/core/index.js:332:17)
    at getText (@opentui/core/index.js:371:10)
    at focus (@opentui/core/index.js:6769:32)
    at <anonymous> (src/cli/cmd/tui/context/keybind.tsx:53:19)

Root Cause

The KeybindProvider captures a reference to the currently focused Renderable when the leader key is activated. During session navigation, the old session's components are destroyed (including their EditBuffers), but the captured reference still attempts to call .focus() on the destroyed renderable. The focus() method internally calls editBuffer.getText()guard() → throws "EditBuffer is destroyed".

Three code paths in keybind.tsx called focus.focus() without checking isDestroyed:

  1. Leader deactivation (line 44) — synchronous, no guard
  2. setImmediate callback (line 59) — deferred execution, no guard
  3. setTimeout callback (line 37) — already had the guard

Additionally, two other components had the same unguarded pattern:

  • prompt/index.tsxPromptRef.focus() and visibility effect
  • question.tsxqueueMicrotask in textarea ref callback

Fix

Added isDestroyed guards before every .focus() call on captured/deferred renderables, consistent with the existing pattern used in dialog.tsx:75, dialog-select.tsx:256, dialog-export-options.tsx:72, and dialog-prompt.tsx:31.

Changes

File Location Change
keybind.tsx:44 Leader deactivation Added !focus.isDestroyed to condition
keybind.tsx:59 setImmediate callback Added !focus.isDestroyed to condition
prompt/index.tsx:351 PromptRef.focus() Added if (input.isDestroyed) return early guard
prompt/index.tsx:379 Visibility createEffect Added input && !input.isDestroyed to condition
question.tsx:384 queueMicrotask in ref Added if (val.isDestroyed) return early guard

Testing

  • All 47 keybind tests pass
  • Typecheck clean (pre-existing unrelated type errors only)
  • Manual reproduction on old version and fixed version: working on it, will update this PR when ready
**Original Pull Request:** https://github.com/anomalyco/opencode/pull/12411 **State:** open **Merged:** No --- ## Problem When switching sessions via leader key + keybind, the TUI crashes with: ``` Error: EditBuffer is destroyed at guard (@opentui/core/index.js:332:17) at getText (@opentui/core/index.js:371:10) at focus (@opentui/core/index.js:6769:32) at <anonymous> (src/cli/cmd/tui/context/keybind.tsx:53:19) ``` ## Root Cause The `KeybindProvider` captures a reference to the currently focused `Renderable` when the leader key is activated. During session navigation, the old session's components are destroyed (including their `EditBuffer`s), but the captured reference still attempts to call `.focus()` on the destroyed renderable. The `focus()` method internally calls `editBuffer.getText()` → `guard()` → throws "EditBuffer is destroyed". Three code paths in `keybind.tsx` called `focus.focus()` without checking `isDestroyed`: 1. **Leader deactivation** (line 44) — synchronous, no guard 2. **`setImmediate` callback** (line 59) — deferred execution, no guard 3. **`setTimeout` callback** (line 37) — already had the guard ✅ Additionally, two other components had the same unguarded pattern: - `prompt/index.tsx` — `PromptRef.focus()` and visibility effect - `question.tsx` — `queueMicrotask` in textarea ref callback ## Fix Added `isDestroyed` guards before every `.focus()` call on captured/deferred renderables, consistent with the existing pattern used in `dialog.tsx:75`, `dialog-select.tsx:256`, `dialog-export-options.tsx:72`, and `dialog-prompt.tsx:31`. ### Changes | File | Location | Change | |------|----------|--------| | `keybind.tsx:44` | Leader deactivation | Added `!focus.isDestroyed` to condition | | `keybind.tsx:59` | `setImmediate` callback | Added `!focus.isDestroyed` to condition | | `prompt/index.tsx:351` | `PromptRef.focus()` | Added `if (input.isDestroyed) return` early guard | | `prompt/index.tsx:379` | Visibility `createEffect` | Added `input && !input.isDestroyed` to condition | | `question.tsx:384` | `queueMicrotask` in ref | Added `if (val.isDestroyed) return` early guard | ## Testing - All 47 keybind tests pass - Typecheck clean (pre-existing unrelated type errors only) - Manual reproduction on old version and fixed version: working on it, will update this PR when ready ⏳
yindo added the pull-request label 2026-02-16 18:19:01 -05:00
Sign in to join this conversation.
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: anomalyco/opencode#14204