Skip to content

perf(native-chat): read the composer's text and caret straight from the editor - #28064

Merged
brennanb2025 merged 3 commits into
mainfrom
brennanb2025/perf-composer-direct
Oct 13, 2026
Merged

brennanb2025 merged 3 commits into
mainfrom
brennanb2025/perf-composer-direct

Conversation

@brennanb2025

@brennanb2025 brennanb2025 commented Oct 12, 2026 •

Copy link
Copy Markdown
Contributor
Files Added Deleted Net
Test 3 $\color{#1a7f37}{\Huge{\mathbf{+}}}$​256 $\color{#cf222e}{\Huge{\mathbf{−}}}$​3 $\color{#1a7f37}{\Huge{\mathbf{+}}}$​253
Prod 4 $\color{#1a7f37}{\Huge{\mathbf{+}}}$​87 $\color{#cf222e}{\Huge{\mathbf{−}}}$​82 $\color{#1a7f37}{\Huge{\mathbf{+}}}$​5

ELI5

When you type or move the cursor in a long chat draft, the composer has to know two things: the text of your draft, and where your cursor sits in it. Until now it got those by redrawing a full "character → spot in the editor" chart of the whole draft every time anything asked, several times per keypress. This PR stops drawing that chart at all and asks the editor directly instead, the way rich-text composers usually do. In the final large-draft comparison, typing used less than half the renderer work, and caret keys also used less.

What Changed

What you saw (STA-7911): in a long native chat draft (we test with about 55,000 characters and a few skill and file pills), every keypress did noticeably more work than it needed to. In the final same-host comparison, main spent 11.86 ms of renderer task time per typed character and 5.59–6.43 ms per caret key (medians). These timings are from the measured Mac only; Windows and Linux latency was not measured.

Why it happened: the composer gives the rest of the chat code a textarea-shaped view of the editor (its text, the caret as a character offset, "put the caret at offset N"). Every one of those reads rebuilt a list with one entry per character of the draft, plus the whole text one character at a time. That happened 8 times per typed character and 3 times per cursor key.

What is different now: typing and moving the cursor in a long draft require less renderer work (table below). Supported composer behavior stays the same: text, pills, cursor positions, line breaks and emoji, including modifier-selected paragraph replacement.

Mechanism:

  • native-chat-prompt-document.ts: promptTextMap (the per-character list) is gone. The text is ProseMirror's own doc.textBetween(…, '\n', promptLeafText), the same read the clipboard copy already used (a hard break reads as a newline, a pill as its token). The caret offset is the length of the text before the caret. Going the other way (offset → editor position, used only when code sets the selection, inserts a skill pill, or replaces the draft) walks the draft's blocks and pills once, not its characters. An offset inside a pill's token snaps to the pill's start, or to its end when a replacement must swallow the whole pill, exactly as before.
  • NativeChatPromptEditor.tsx: the textarea-shaped view answers from the editor through those functions. The "caret is on the first/last line" check for prompt recall uses the text before and after the caret instead of the per-character list.
  • NativeChatComposer.tsx: each change or selection event reads the text and caret once instead of twice.

Why

This follows the common pattern: the composer reads the editor directly instead of rebuilding a text copy. Direct reads still walk document nodes and construct strings; they avoid the per-character position array. The direct reads reuse editor nodes and avoid constructing an entry for every draft character. The final production-bundle comparison below measures the remaining work as well as the removed map builds.

Alternatives considered:

  • Cache the per-character list per document (perf(native-chat): stop rebuilding the composer's text map on every keypress in long drafts #27784). That works and is about as fast on cursor keys, but it keeps the per-character list and adds a module-wide cache to make it affordable. Removing the list removes the need for the cache, and each edit avoids building the list once. Measured direct/cached ranges overlap, so this review does not establish a speed advantage over the cache.
  • Drop the textarea-shaped view and move every caller to editor positions. The draft string and character caret are what is saved per pane, sent, recalled, and parsed for /, $ and @ pickers. Moving about ten modules and their tests to editor positions would broaden this focused change across persistence, send, recall and insertion; it is not required to remove the per-character map. The view stays; only how it answers changes.
  • Skip the selection callback when an edit callback will follow. Rejected: pasting identical text over a selection changes the selection but the editor sends no edit callback, so the caret would go stale.

Paragraph selection parity: Cmd-click on macOS or Ctrl-click elsewhere can select a whole paragraph. Review found that a direct prefix read initially missed the newline before that selection, so redirected typing could join two lines. The final implementation uses the editor’s resolved position to include that separator, preserving the old behavior without rebuilding a character map. The regression tests select paragraphs through the actual mouse handler and exercise the production insertion hook, including an empty preceding line and emoji.

Intended edge difference: a pill with an empty token reads as zero characters, correcting the old off-by-one caret count. Normal pill creation always provides a token; behavior for empty-token restored documents is not claimed identical.

Linked Issue

Linear: STA-7911. Supersedes #27784 (same problem; this removes the per-character list instead of caching it).

Visual Proof

Final reviewed head e217dbb was rebuilt and measured against the original base b40554b2dc4 and that same base with #27784's cached-map patch. Nine hidden production-build runs, interleaved main/cache/direct three times, used the original workload on the same Mac laptop: 55,061 UTF-16 characters, 715 paragraphs, five skill pills and three file pills. A stand-in Codex executable opens the native chat; no prompt is sent. DevTools sends 100 presses per individual key at 60 ms intervals.

The table records renderer main-thread busy time from each keydown's task to the next, including other renderer work in that window. It is not a time-to-paint measurement. p50/p95 are medians of the three per-run percentiles; parentheses show the runs' p50 min–max. Build/source/loaded-chunk hashes, raw or bounded extracted trace/coverage captures and PID cleanup records are retained with the measurement artifacts.

Key main p50 ms (range) #27784 p50 ms (range) this PR p50 ms (range) main p95 #27784 p95 this PR p95 map builds: main / cache / direct
Typing a character 11.86 (8.77–12.52) 5.75 (4.67–6.36) 5.16 (4.66–6.12) 14.13 11.82 8.49 8 / 1 / 0
Arrow Left 5.59 (4.88–6.26) 3.48 (3.39–4.03) 3.65 (3.35–3.82) 6.91 3.92 4.63 3 / 0 / 0
Arrow Right 5.69 (4.87–5.88) 3.48 (3.44–3.71) 3.37 (3.09–3.56) 6.75 4.18 4.29 3 / 0 / 0
Arrow Up 6.25 (4.96–6.43) 3.82 (3.69–3.88) 4.04 (3.30–4.09) 7.85 4.82 5.17 3 / 0 / 0
Arrow Down 6.43 (5.19–6.74) 3.96 (3.58–4.11) 4.02 (3.27–4.20) 7.56 7.38 5.37 3 / 0 / 0
Home / End 5.94 (4.96–6.59) 3.53 (3.50–3.64) 3.49 (2.98–4.01) 6.95 4.47 4.43 3 / 0 / 0
Cmd+Left / Cmd+Right 6.29 (5.00–6.37) 3.71 (3.32–3.75) 3.73 (3.35–4.03) 7.53 4.94 5.46 3 / 0 / 0

Typing p50 was 5.16 ms versus main's 11.86 ms (56.5% lower) and the cached comparison's 5.75 ms. Some cursor medians and p95 values are higher than the cached comparison, but every direct/cached per-run p50 and p95 range overlaps. These samples support comparable cached-build costs. The shared M5 Pro host had substantial ordinary background activity; small timing differences should be read within that spread. Windows/Linux performance was not measured.

Map builds are exact separate V8 call counts: 8 / 1 / 0 per typed character and 3 / 0 / 0 per caret key. Direct editor text reads remain: seven per typed character and two or three per caret key. These read document nodes and strings; draft JSON serialization and existing picker scans also remain. The complete key path is not traversal-free.

Final composer benchmark

Hidden rendered Electron QA on the M4Air completed at the final head: 30/31 initial assertions and 13/13 continuation assertions passed. Real modifier-click paragraph replacement, pills, hard breaks, emoji, wrapped recall, redirected typing, equal-content paste, trusted file/image drops, undo and two concurrently mounted composers were exercised. Dictation-style contenteditable insertion passed; live speech capture was not exercised. The initial synthetic image-paste miss did not populate the native clipboard used by the save bridge; trusted drops passed. Screenshots are attached to the final review status comment. This is fixture-based end-to-end build evidence, not packaged-app or live-agent evidence.

Testing

  • Final fix: 281 tests in 28 files passed, covering document/editor parity, pills, breaks, emoji, recall, redirected typing, composition, draft restore, attachments, dictation target/segments, undo and separate editors.

  • The new modifier-click paragraph-selection tests failed before the fix and passed after it, including an empty preceding line and UTF-16 text.

  • Fresh independent review: 40 additional tests and 585-document base comparisons passed, including 7,701 document positions, paragraph selection endpoints and 34,664 replacement comparisons. Identical-text replacement still publishes a changed selection when the editor emits no update callback.

  • tc:web passed at the final head; the original full native TypeScript baseline passed. Pre-push orca-ci-checks --no-typecheck ran all 23 applicable steps: 21 passed; the two known local failures were unchanged script imports and local Bun 1.3.14 unable to execute the configured bun check step. No unrelated source/toolchain patch was made. Final-head hosted CI passed all 13 applicable checks, including static analysis/typecheck, five test shards, relay integration and macOS/Windows packaging; 17 jobs were skipped and provide no runtime evidence.

  • Final nine-run production benchmark completed with exact build identity and preserved operation counts. Renderer/platform, native microphone and full live-agent/SSH evidence limits are stated separately from unit results.

  • Automated tests added/updated

  • Hidden end-to-end keyboard benchmark completed on the same laptop

  • Remote rendered flow QA complete

Review

Two independent review rounds, an architecture challenge, dedicated performance pass and final readiness review completed. One reachable paragraph-selection regression was fixed in e217dbb; the fresh follow-up found no remaining source correction. The direct-read design and existing composer contract were retained. Final-head CI and remote rendered QA completed; remaining evidence limits are listed below. No source finding remains open or deferred.

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

Renderer-only: no IPC, wire contract, paths, shortcuts or execution-host routing changed. No application text/index cache or new stored state was added. Draft-document serialization and existing picker scans remain, and are included in the key-window measurements. Actual Windows/Linux/SSH runtime, microphone capture, paint latency and heap behavior were not measured by this review.

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)

…he editor

Every composer read (text, caret, selection) rebuilt a per-character text-to-position
map of the whole draft: 8 times per typed character and 3 per caret key. Reads now ask
the editor directly (ProseMirror textBetween) and convert a text offset to an editor
position with one node-level walk only when a caller sets the selection or edits.

STA-7911
@brennanb2025
brennanb2025 marked this pull request as ready for review October 13, 2026 13:15
@brennanb2025

brennanb2025 commented Oct 13, 2026 •

Copy link
Copy Markdown
Contributor Author

Long native-chat drafts rebuilt a character-to-editor-position table repeatedly while typing or moving the caret. This PR removes that table and reads the editor directly, reducing measured renderer work while preserving the existing draft/caret contract; it replaces #27784 without changing that PR.

The full review loop is complete at e217dbb: two independent rounds, an architecture challenge, a performance pass and readiness review. The direct-read design was retained. One P2 regression was fixed: after Cmd/Ctrl-click selected a paragraph and focus moved to the transcript, redirected typing could delete the preceding newline (abc\ndef became abcX). It now produces abc\nX; real mouse-handler/insertion tests failed before the fix and passed after it. Nothing remains open or deferred. Empty-token restored pills intentionally count as zero characters; normal pill creation supplies a token.

Verification:

  • 281 tests in 28 files, final renderer typecheck and fresh independent parity checks passed. Final-head push CI and the automatic ready-for-review CI passed; the final aggregate has 26 successful and 33 skipped check records, with no failures or pending jobs. The two local pre-push static-check failures were baseline script-import/toolchain issues; hosted static analysis passed.
  • Nine interleaved production-build measurements on the original host gave typing p50 medians of 11.86 ms main / 5.75 ms cached / 5.16 ms direct. All direct/cached p50 and p95 ranges overlap, and some cursor medians are higher than the cache. This supports comparable costs to the cache, with no statistically established speed advantage over it. The per-character table and application cache are gone; full/prefix text reads, draft serialization and picker scans remain. The whole keypress path is not traversal-free, and these numbers measure renderer busy time rather than paint latency.
  • Hidden M4Air Electron QA: 30/31 initial assertions plus 13/13 continuation assertions passed. Real modifier-click paragraph replacement, skill/file pills, hard breaks, emoji, wrapped recall, redirected typing, equal-content paste, trusted file/image drops and undo passed. Two full composers were mounted concurrently in separate tabs; editing and undo in one preserved the other's value and editor instance. Dictation-style production contenteditable insertion passed for a selection and beside a pill.
  • The initial synthetic image-paste assertion produced no attachment. This is a harness coverage miss: the save bridge reads the native clipboard, which the forged DOM paste did not populate. Successful trusted file/image drops verify their production drop route; they do not verify native clipboard paste. Live microphone capture/final-transcript delivery, native IME/focus, packaged-app/live-agent flows and Windows/Linux/SSH runtime were not exercised. Remote QA used the final branch build; base parity was checked locally rather than through a second remote rendered run.

All six owned remote Electron rigs have actual graceful-exit receipts. The standalone QA driver exited, its owned shell terminals closed, the QA checkout was removed from Orca and the M4Air slot released. Local benchmark captures remain bounded; process absence without an exit receipt was treated as unverifiable, not proof of exit.

The PR is ready for human review and remains unmerged. Screenshots below show the final branch fixture; assertions provide the selection and instance-isolation evidence.

File attachment alongside text:

File attachment and draft

Image attachment preview alongside text:

Image drop and draft

@coderabbitai

coderabbitai Bot commented Oct 13, 2026

Copy link
Copy Markdown
Contributor

Review in Change Stack →

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration
  • Configuration used: Repository UI
  • Review profile: CHILL
  • Plan: Advanced
  • Run ID: 4ac65f77-e9b5-4a89-b3e9-fee209e83b65
📥 Commits

Reviewing files that changed from the base of the PR and between 0ec645f and e217dbb.

📒 Files selected for processing (7)
  • src/renderer/src/components/native-chat/NativeChatComposer.tsx
  • src/renderer/src/components/native-chat/NativeChatPromptEditor.paragraph-selection.test.tsx
  • src/renderer/src/components/native-chat/NativeChatPromptEditor.test.tsx
  • src/renderer/src/components/native-chat/NativeChatPromptEditor.tsx
  • src/renderer/src/components/native-chat/native-chat-prompt-document.test.ts
  • src/renderer/src/components/native-chat/native-chat-prompt-document.ts
  • src/renderer/src/components/native-chat/native-chat-prompt-editor.test-support.ts

Included review availability: This review used your included allowance. Your plan provides up to 10 included reviews per hour; 8 remain after this review.

📝 Walkthrough

The prompt document helpers now serialize prompt text and map between text offsets and document positions. The prompt editor uses these helpers for values, replacements, selections, and persistence. New tests cover text mapping, paragraph selection, file-reference edits, and editor isolation. Composer event handlers now calculate and update caret positions directly.

Priority: ⬇️ Low

Merge Risk: ⚪ Minimal · up to e217d

No actionable issue identified; the change is mergeable after normal checks.

Pre-merge checks · Passed 4 · Failed 1

Failed Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage Warning Docstring coverage is 38.46% which is insufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 13 functions across 7 files. Write docstrings for the functions missing them to satisfy the coverage threshold.
Passed Passed checks (4 passed)
Check name Status Explanation
Title check Passed The title clearly and concisely describes the main change: reading composer text and caret directly from the editor to improve native-chat performance.
Description check Passed The description is complete and directly matches the template. It explains the user impact, implementation, rationale, alternatives, linked issue reference, visual proof, testing, review status, limit…
Linked Issues check Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check Passed Check skipped because no linked issues were found for this pull request.
  • Fix all pre-merge checks with AI
✨ Finishing Touches 💡 1
📝 Generate docstrings 💡
  • Commit to this branch
  • Create a new PR
  • Autofix · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@brennanb2025
brennanb2025 merged commit a36b2ac into main Oct 13, 2026
59 checks passed
@brennanb2025
brennanb2025 deleted the brennanb2025/perf-composer-direct branch October 13, 2026 21:37
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