Move the tiptap composer behind a first-focus handoff - #2390
Conversation
…oxShell The tiptap/prosemirror graph (261.9 KB brotli) sits in SplitWorkspaceRoute's static closure because the composer chrome and the editor live in one module. Step 1 of the first-focus handoff (plan 008): extract the closed-state look — frame, placeholder, draft preview, action row, send button — into PromptBoxShell with zero tiptap imports. The shell's preview surface is a `.ProseMirror`-shaped contenteditable=false div, so the existing placeholder and compact-truncation CSS in app.css applies unchanged; draft text and mention pills render from the controlled value, so a saved draft shows immediately. Shared chrome (PromptSubmitButton, frame classes, chrome-target selector) and the editorless prompt-action append (new prompt-action-draft.ts) move out of PromptBoxInternal, which now imports them — the dependency is one-way so future features land in the internal module. PromptBoxInternal also grows the takeFocusOnCreate handoff prop the wrapper will use in step 2. A Ladle story renders the shell beside the mounted editor's closed state for the parity check (empty, saved draft, mention pill, compact). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Step 2 of plan 008. PromptBox renders the shell until the first compose intent — a tap/focus on the text region, a paste or drop, a plus-menu action, or a programmatic focus request (promptBoxRef.focusEnd(), a focusEndKey change, desktop autoFocus) — then dynamically imports PromptBoxInternal and mounts it for good. While the chunk loads, an invisible interim textarea overlaid on the text region owns the focus: the tap focuses it natively (so the soft keyboard opens inside the user's gesture) and every keystroke and paste flushes straight into the controlled draft, which the shell preview echoes. Empty drafts render the interim text natively (native caret and IME preview); continuing drafts stay invisible behind the preview echo plus a caret affordance. The swap defers during IME composition so no composition is cut; at swap the editor mounts with the full draft and takes over focus (takeFocusOnCreate) with the caret at the end. The chunk also prefetches at idle and on pointerenter/focus so the handoff is normally mount-only. FollowUpPromptBox and NewThreadPromptBox mount PromptBox instead of PromptBoxInternal; the FollowUpPromptBox suite mocks the new boundary. New handoff tests fail against the eager architecture (7/8; the autoFocus parity test is trivially green there) and pass after. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Step 3 of plan 008. Every @tiptap/* and prosemirror-* package observed in the graph moves into onDemandPackages gated on PromptBoxInternal.tsx (the dynamic-import target of the PromptBox handoff), so any new static edge into the editor graph fails check:bundle no matter which chunk it comes from. The same canonical package names join SplitWorkspaceRoute's forbiddenPackages, mirroring how katex is listed in both. Route closure budgets re-ratchet to 10% above the measured closure: 2,268,430 -> 1,765,479 raw and 605,332 -> 489,779 brotli. Measured closure after the handoff: 1,604,980 raw / 445,253 brotli, down from 2018.4 KB raw / 538.5 KB brotli before it. The editor gate chunk is 487,251 raw / 121,126 brotli, fetched at idle or on first compose intent instead of before route content paints. The plan's ~240 KB brotli estimate attributed the whole 1.07 MB workspace-checkout-display chunk to tiptap; the chunk also carried ~550 KB of unrelated app modules (ansi-to-html/entities and timeline code) that stay eager, so the realized closure saving is 451 KB raw / 104 KB brotli plus the budget headroom reclaimed by the ratchet (503 KB raw / 116 KB brotli). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
|
Closing this PR for rework after an adversarial review of the #2385–#2393 set. Method: two independent multi-agent review passes over a worktree at this PR head; every finding went to three independent refuters (code trace, a throwaway experiment against the real code, and an impact judge), and only findings that survived at least two of three are listed. The branch stays as-is; please reopen this PR or open a new one when the blocking items are addressed. Blocking
Fix together with the above (each confirmed by experiment)
Low
Open riskNo one verified on a device that the iOS keyboard stays up across the swap; the refuters could only run desktop WebKit. Test that on a phone before investing in the tail above; if the keyboard drops, the swap must happen synchronously inside the tap gesture. Suggested salvageThe design is right and the bundle win is real (measured 104 KB brotli off the route closure on the merged tree). Fix the loader, the IME guard, and chrome stability; add coarse-pointer tests; then reopen after a device test.
|
What was wrong
The tiptap/prosemirror editor graph sat in
SplitWorkspaceRoute's static closure: 487 KB raw / 121 KB brotli parsed and compiled before route content paints on every thread, compose, and plugin-panel page, for an editor the user has not tapped.bundle-budget.jsonalready named the intended fix — "allowed until it moves behind a first-focus handoff".What changed
PromptBoxShell.tsx(new): the composer's exact closed-state chrome with zero tiptap imports. Its preview is a.ProseMirror-shapedcontenteditable="false"surface, so existing placeholder/compact-preview CSS applies unchanged; a saved draft shows immediately.PromptBox.tsx(new): renders the shell until the first compose intent (tap/focus, paste, drop, plus-menu action, programmatic focus request, desktop autoFocus), then dynamically imports and permanently mountsPromptBoxInternal. An invisible interim textarea owns the interim: the tap focuses it natively (soft keyboard opens in-gesture), keystrokes and pastes flush live into the draft, the swap defers during IME composition (never cutting a composition), and the editor takes focus at mount. The chunk prefetches at idle / pointerenter, so the handoff is normally mount-only. Manualimport()instead ofReact.lazyso the swap can wait out IME composition.bundle-budget.json: all 32 observed@tiptap/*/prosemirror-*packages gated onPromptBoxInternal.tsxinonDemandPackages; route closure budgets re-ratcheted 2,268,430→1,765,479 raw, 605,332→489,779 brotli so regression fails CI.Honest sizing note: the measured closure saving is 451 KB raw / 104 KB brotli — the old 1.07 MB chunk was only ~49% tiptap; the rest is unrelated code that stays eager.
How you verified
PromptBox.test.tsx, 8 tests) fails 7/8 against the eager architecture and passes 8/8 after: draft survives first tap, keystrokes/paste before mount land in the editor, file paste attaches,focusEnd()/focusEndKeyrealize and focus, Enter submits from the shell.@bb/appsuite 3,350 passed.check:bundlegreen: closure 538.5→434.8 KB brotli;why-eagerreports 0 eagerly reachable tiptap/prosemirror modules; a simulated static edge fails the check.