Skip to content

fix(native-chat): keep a delayed composer commit from crashing on a destroyed prompt editor - #31736

Draft
OrcaWin wants to merge 2 commits into
mainfrom
crash/s46-chat-editor-focus-before-mount
Draft

OrcaWin wants to merge 2 commits into
mainfrom
crash/s46-chat-editor-focus-before-mount

Conversation

@OrcaWin

@OrcaWin OrcaWin commented Oct 22, 2026 •

Copy link
Copy Markdown
Collaborator
Files Added Deleted Net
Test 1 $\color{#1a7f37}{\Huge{\mathbf{+}}}$​123 0 $\color{#1a7f37}{\Huge{\mathbf{+}}}$​123
Prod 1 $\color{#1a7f37}{\Huge{\mathbf{+}}}$​27 $\color{#cf222e}{\Huge{\mathbf{−}}}$​7 $\color{#1a7f37}{\Huge{\mathbf{+}}}$​20

ELI5

The chat box at the bottom of an agent chat is built from a text-editor widget. When a new chat opened, Orca tried to put the cursor in that box right away. Sometimes the box was shown a little late (React waits up to about 300ms before revealing a loading section), and by then the editor library had already thrown the unused editor away. Orca then told the thrown-away editor to take focus, the editor library threw an error, and the whole tab fell into the error screen. Now Orca checks first: if the editor has been thrown away, it skips that one attempt. The focus logic already retries on the next few frames, and by then a fresh editor is in place, so the cursor still ends up in the chat box.

The same thrown-away editor could also crash a different way: when one chat box tried to grab the cursor, Orca asked every other chat box on screen "is the cursor inside you?", and asking a thrown-away editor that question threw the same error. The same check now answers "no" for a thrown-away editor, so that path is covered too.

What Changed

Before: opening a new chat or agent session could sometimes replace the tab with the error screen ("Something went wrong"). This shows up in crash report 4f1f0de8 on 1.4.237:

Error: [tiptap error]: The editor view is not available. Cannot access view['focus']. The editor may not be mounted yet.
  at focus            (NativeChatPromptEditor handle:  d.view.focus() ...)          chunk 131:1263
  at focus            (use-native-chat-typed-insertion.ts wrapper)                   chunk 132:59097
  at reclaim          (use-native-chat-composer-reveal-focus.ts useLayoutEffect)    client 73:188558
  -> caught by the terminal.workbench error boundary

Crash report 55eb9500 on 1.4.237 (found in crash Scan-48) hits the same destroyed editor through a sibling composer:

Error: [tiptap error]: The editor view is not available. Cannot access view['dom']. The editor may not be mounted yet.
  at contains         (NativeChatPromptEditor handle)
  at contains         (use-native-chat-typed-insertion.ts wrapper)
  at Array.some       ([...mountedComposers].some(c => c.ownerRef.current?.contains(active)))
  at reclaim          (use-native-chat-composer-reveal-focus.ts, layout-effect commit)

After: the new chat opens normally, and the cursor lands in the composer.

Mechanism (src/renderer/src/components/native-chat/NativeChatPromptEditor.tsx):

  • The editor library's useEditor destroys any editor whose mount effect has not run within 1ms of the editor being created. React can delay committing a Suspense reveal by up to about 300ms, so the composer's handle can be attached to an editor that is already destroyed. In the same commit, the new-chat reveal-focus layout effect calls focus(), and that reads editor.view. On a destroyed editor, editor.view is a stub that throws on most keys.
  • A new liveView(editor) returns null when the editor is destroyed. focus, contains and isCaretOnVisualEdge go through it and do nothing (or return false) when no view exists.
  • The handle's disabled getter now returns editor.isDestroyed || !editor.isEditable. Before, it reported a dead editor as usable, because the stub says editable: true. With this change the composer wrapper's focus() returns false, so the existing reveal-focus retry loop (6 frames) tries again and focuses the replacement editor that useEditor builds on the next render.
  • insertText, insertSkill, removeImageReferences and insertFileReferences return early on a destroyed editor, because they would otherwise throw a different error about a null schema or command manager.

Why

The crash is a timing gap between the editor library's 1ms destroy timer and React's delayed Suspense commit. Neither side is wrong by its own rules. The narrowest fix is to make the prompt handle honest about a dead editor, which follows the existing !editor.isDestroyed guards in components/editor. Options I rejected:

  • Postponing focus with a timer or extra deferral. That adds new delayed work, and the delayed focus could fire on the wrong tab later.
  • Catching the error in the reveal-focus hook. That hides the real state, and the other handle methods (insert, paste) would still throw.
  • Patching the editor library's destroy timing. That is third-party internals and fragile across upgrades.

Scan-54 update (2026-10-26, rebased on main 8851890)

Three more of these crashes on 1.4.241 (the current release) in one day, on macOS and Windows: 86cba930, 68477af0, ef36bda1. The stack is identical: reveal-focus reclaim, then composer focus(), then editor.view.focus(), then [tiptap error] ... Cannot access view['focus'], caught by terminal.workbench.

  • Rebase: one conflict. Main's comment cleanup ([comment audit] Complete repository-wide comment cleanup #33249) reworded the focus comment. I kept main's wording and kept the liveView guard.
  • Repro on current main: I swapped NativeChatPromptEditor.tsx to origin/main and both delayed-commit tests fail with the production error [tiptap error]: The editor view is not available. Cannot access view['focus']. With this branch, 2/2 pass.
  • Gates: pnpm tc:web passes. The native-chat suite passes (381 files, 4268 tests). oxlint, oxfmt and check:code-quality:changed are clean.
  • Adversarial review after the rebase: 1 round, CLEAN (no P0/P1). The reviewer checked every key on tiptap's destroyed-view Proxy. Only focus, dom and endOfTextblock throw, and all three go through liveView. The retry lands on the replacement editor.

Scan-49 update (re-verified on current main, no code change)

Description

Orca crash report (Slack #crash channel), Orca 1.4.237: the same [tiptap error]: The editor view is not available. Cannot access view['focus'] crash in an agent-session tab, about 18s after start, caught by the terminal.workbench error boundary.

Root cause (unchanged): the reveal-focus hook calls the composer handle's focus() synchronously in a useLayoutEffect once composerReady is set and a focus intent is pending for the tab. A Suspense reveal can delay the commit (throttled to about 300ms). useEditor destroys an editor whose mount effect did not run within 1ms of creation. So the commit gives the imperative handle a destroyed editor whose editorView is null, and editor.view.focus() hits tiptap's view proxy, which throws.

Frame mapping from the extracted v1.4.237 arm64 app.asar. The released sourcemaps zip has no renderer maps, so the frames were mapped by reading the minified code:

  • OnboardingInlineCommandTerminal-C2cRmP37.js:131:1263 is focus:()=>{d.view.focus(),d.isEditable||d.view.dom.focus()}, the handle focus() in NativeChatPromptEditor.tsx.
  • :132:59097 is focus:(0,Q.useCallback)(()=>{let e=s();return e?.focus(),e!==null},[s]), the focus callback in use-native-chat-typed-insertion.ts.
  • client-B0x6L90P.js:73:188558/188640 is the reveal-focus useLayoutEffect r(), called through NJ?.tabId===e&&(NJ.cancel(),r()). That is use-native-chat-composer-reveal-focus.ts:109-122.

None of these files (NativeChatPromptEditor.tsx, use-native-chat-composer-reveal-focus.ts, use-native-chat-typed-insertion.ts) changed on origin/main since the PR base f0450469e42, so main still has the crash.

Fix evidence

Run in the PR worktree at head 252c8b9e95a:

  1. Main's file with the PR test (fail before):
git checkout origin/main -- src/renderer/src/components/native-chat/NativeChatPromptEditor.tsx
pnpm test src/renderer/src/components/native-chat/NativeChatPromptEditor.delayed-commit.test.tsx
 × focuses a new chat whose composer commits after a delayed Suspense reveal
 × reclaims focus past a sibling composer that committed a destroyed editor
   Error: [tiptap error]: The editor view is not available. Cannot access view['focus']. The editor may not be mounted yet.
 Tests  2 failed (2)
  1. The PR's file (pass after):
git checkout HEAD -- src/renderer/src/components/native-chat/NativeChatPromptEditor.tsx
pnpm test src/renderer/src/components/native-chat/NativeChatPromptEditor.delayed-commit.test.tsx
 Tests  2 passed (2)
  1. git merge-tree --write-tree origin/main HEAD exits 0, so the PR still merges cleanly onto current main (5d32f372e76). No rebase was needed, and nothing new was pushed.

In tiptap core 3.31.3, isDestroyed returns this.editorView?.isDestroyed ?? true. It is true for both the never-mounted and the destroyed case, so the liveView(editor) guard covers this report.

ELI5

The chat box's text editor sometimes gets thrown away and rebuilt when the screen shows up a little late. The app tried to put the cursor into the old editor after it was thrown away, and that crashed the tab. The fix first checks whether the editor is still alive. If it isn't, the app skips the focus, and the fresh editor that appears a moment later gets focus normally.

Trade-offs

No new trade-offs since Scan-48. When the handle still points at a destroyed editor, focus, insert and contains calls do nothing instead of throwing. On the rare delayed-reveal path, an initial auto-focus or a typed or pasted insertion aimed at the dead editor is dropped instead of crashing. The existing reclaim pass then focuses the re-rendered live editor, and the PR's tests cover that.

Adversarial review

Scan-49: 1 loop, final CLEAN.

  • Loop 1 (clean): no P0 or P1 problems, and the reviewer could not refute the fix.
    • Root cause: in tiptap 3.31.3, EditorInstanceManager.scheduleDestroy destroys the editor 1ms after creation unless its effect has run. The destroyed editor.view proxy then throws on focus and dom.
    • Repro: with main's file swapped in, both tests fail with the exact production error. With the fix, both pass.
    • Recovery: focus still lands. disabled returns true for a destroyed editor, so the wrapper's focus() returns false and the claim is retried for up to 6 frames. Meanwhile refreshEditorInstance creates a fresh editor, and a later retry focuses it.
    • Early returns: they fire only when editorView is null, never on a live editor. The unguarded paths only touch proxy keys that don't throw.
    • Scope: renderer-only, with no IPC, platform, SSH or mixed-version surface.
    • Gates: native-chat tests 4078 passed (372 files), pnpm tc:web clean, oxlint clean on the changed files.
    • Minor P2 notes (not blocking): while the dead editor is committed (1 to 2 renders), acceptsText and focus report the composer as unavailable. A draft written to the dead editor during that window is not carried over to the fresh editor. This already happens on main and is not introduced by this PR.

Linked crash reports

No issue.

  • 4f1f0de8, Orca 1.4.237: Cannot access view['focus'] via reclaim -> focus (found in crash Scan-46).
  • 55eb9500, Orca 1.4.237: Cannot access view['dom'] via reclaim -> Array.some -> contains on a sibling composer (found in crash Scan-48). No new production change was needed: the liveView() guard in contains() already covers it, and so does the editingSiblingComposer check in claim(), which uses the same contains. This update adds a test that proves it.

Fix evidence

The Scan-48 update adds a second real-code test to NativeChatPromptEditor.delayed-commit.test.tsx: reclaims focus past a sibling composer that committed a destroyed editor. It renders two real composers (real NativeChatPromptEditor + useNativeChatTypedInsertion + useNativeChatComposerRevealFocus, no mocks) inside a Suspense boundary whose lazy sibling resolves after 10ms. React's 300ms retry throttle delays the commit, so useEditor has already destroyed both editors. A pending focus request for the second composer's tab runs reclaim(), and the mountedComposers.some(... contains(active)) check reaches the first composer's dead editor.

With origin/main's NativeChatPromptEditor.tsx swapped in (git show origin/main:<file> > <file>), verbatim:

 × focuses a new chat whose composer commits after a delayed Suspense reveal
   + "Error: [tiptap error]: The editor view is not available. Cannot access view['focus']. The editor may not be mounted yet.",
 × reclaims focus past a sibling composer that committed a destroyed editor 372ms
   + "Error: [tiptap error]: The editor view is not available. Cannot access view['dom']. The editor may not be mounted yet.",
 Tests  2 failed (2)

The sibling test failed 4 of 4 runs on main, with the exact message and call path from report 55eb9500.

With this PR's NativeChatPromptEditor.tsx (liveView guard), verbatim:

 Tests  2 passed (2)

This passed 3 of 3 runs. The sibling test also asserts that document.activeElement is the reclaiming composer's textbox.

Test isolation note: the first draft of the sibling test passed falsely on main when it ran after the focus test. happy-dom left document.activeElement on the previous test's detached textbox, so waitFor finished before anything committed. Instrumenting showed that neither contains nor focus was called. The focus check is now scoped to the test's own container.

Visual Proof

N/A. This prevents an error screen in a timing window that is hard to trigger by hand. The proof is the automated test below: it fails with the exact production error before the fix and passes after.

Testing

Scan-48 update: the branch is rebased onto origin/main (it was 193 commits behind) and force-pushed with lease. Head is 252c8b9e95a. Native-chat suite: Test Files 384 passed / Tests 4205 passed. pnpm tc:web: No type errors in 15,625 files. oxlint on both changed files: clean. The pre-commit oxlint, react-doctor and oxfmt hooks also passed.

Original fix evidence (Scan-46). The new test src/renderer/src/components/native-chat/NativeChatPromptEditor.delayed-commit.test.tsx uses only real code. It renders NativeChatPromptEditor, useNativeChatTypedInsertion and useNativeChatComposerRevealFocus with a pending new-chat focus request, inside a Suspense boundary held by a lazy sibling. Nothing is mocked to throw. React's own Suspense throttle creates the delayed commit.

Before the fix (verbatim):

 FAIL  |node| src/renderer/src/components/native-chat/NativeChatPromptEditor.delayed-commit.test.tsx > focuses a new chat whose composer commits after a delayed Suspense reveal
AssertionError: expected [ Array(1) ] to deeply equal []
- Expected
+ Received
- []
+ [
+   "Error: [tiptap error]: The editor view is not available. Cannot access view['focus']. The editor may not be mounted yet.",
+ ]
 ❯ src/renderer/src/components/native-chat/NativeChatPromptEditor.delayed-commit.test.tsx:94:30
 Test Files  1 failed (1)
      Tests  1 failed (1)

After the fix (verbatim, 5 of 5 runs passed):

 Test Files  1 passed (1)
      Tests  1 passed (1)

The test asserts that no uncaught error occurred and that document.activeElement is inside the composer, so focus really lands.

Revert check: with the fix removed, the test failed 3 of 3 runs with the same Cannot access view['focus'] error. A separate review pass that swapped main's NativeChatPromptEditor.tsx back in got the same result.

Other checks:

  • Full native-chat suite: Test Files 370 passed (370) / Tests 4018 passed (4018)
  • pnpm tc:web: No type errors in 15,445 files
  • oxlint on changed files: clean
  • pnpm run check:code-quality:changed: passed, 0 new findings

Platforms: macOS (unit tests). The change is renderer-only and does not depend on the platform. It does not touch SSH or remote paths.

  • I manually tested these changes locally
  • Automated tests added/updated, or explained why not below

Trade-offs

  • For less than a frame, before React swaps in the fresh editor, programmatic focus, insert and paste on the composer do nothing instead of crashing. Focus is retried, so it still lands. A paste in that window would be dropped, but the composer isn't visible or usable then.
  • isCaretOnVisualEdge returns false when no view exists, so arrow-key prompt recall can't fire then. That can only happen before the view exists.
  • The test uses real timers and React's ~300ms Suspense throttle, so it takes about 0.4s and depends on that React behavior. It was stable across repeated runs.
  • Scan-48 update: no new trade-offs. As before, a composer whose committed editor was destroyed reports contains = false and does not take focus until the next render swaps in a live editor.
  • I did not reproduce the crash in a running app. The repro is the real-code test, plus a trace of the shipped 1.4.237 bundle's call chain.

Adversarial review

Scan-48 update (sibling-composer test): 1 loop, final CLEAN.

  • Loop 1 (clean): found no P0 or P1 problems.

Original fix (Scan-46): 1 loop, clean = true.

  • Loop 1 (clean): found no P0 or P1 problems and could not refute the fix. It confirmed the root cause (a delayed Suspense commit lands a prompt editor that useEditor has already destroyed, and the view stub throws on focus). It also confirmed that disabled now makes the composer wrapper's focus() return false, so the 6-frame reveal-focus retry focuses the replacement editor. No new deferred work was added, so focus can't land on the wrong tab later. The reviewer swapped main's file back in, got the exact crash message, then restored the file. The reviewer re-ran the prompt-editor tests (17 passed), tc:web, the code-quality gate and oxlint, all clean. One minor non-blocking note on the command paths.

Agent skill upstream boundary

  • Not applicable, or this change copies or mechanically translates no upstream skill-installer source, tests, fixtures, registry entries, path tables, comments, or documentation.

Notes

Ensure no issues in: Security, Cross-platoform support (Linux, Windows, Mac), Remote SSH, Mobile, general backwards compatibility, performance

Renderer-only guard: no wire, IPC or storage changes, no platform-specific code, and no performance cost (one boolean check per handle call).

Checklist

  • This PR is small and focused
  • I explained what changed and why (ELI5, the user-facing before/after, the mechanism, and why over the alternatives)
  • Before/after screenshots or videos attached for UI changes, or N/A with reason
  • Self-reviewed for correctness, security, and performance
  • Cross-platform, SSH/remote, and path/shortcut impact considered (or N/A)
  • pnpm lint, pnpm typecheck, pnpm test, and pnpm build pass (or CI will cover; local preferred)

@OrcaWin
OrcaWin force-pushed the crash/s46-chat-editor-focus-before-mount branch from a609728 to 252c8b9 Compare October 23, 2026 00:08
@OrcaWin

OrcaWin commented Oct 25, 2026

Copy link
Copy Markdown
Collaborator Author

Crash scan 52 found another sighting on v1.4.240 (report b40f5f19, macOS 27 arm64, boundary terminal.workbench): [tiptap error]: The editor view is not available. Cannot access view['dom']. It is thrown from the handle's contains, called by the focus-reclaim mountedComposers.some(...contains(active)) in use-native-chat-composer-reveal-focus.ts:201. That is the sibling-composer path this PR guards with liveView(), covered by test commit 252c8b9. main still has editor.view.dom.contains(node), so this crash keeps happening until the PR merges.

m4air added 2 commits October 26, 2026 07:04
…estroyed prompt editor

A Suspense retry commits up to 300ms after it renders. useEditor destroys an editor
whose mount effect has not run 1ms after creating it, so that commit hands the prompt
handle a destroyed editor, and the new-chat focus claim (a layout effect) threw
"The editor view is not available. Cannot access view['focus']". The handle now
reports a destroyed editor as disabled and skips view reads and edits on it; the next
render swaps in the fresh editor and the reveal-focus frame retry focuses it.
@OrcaWin
OrcaWin force-pushed the crash/s46-chat-editor-focus-before-mount branch from 252c8b9 to ff6173d Compare October 26, 2026 14:08

This branch has not been deployed

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant