perf: tag the center column instead of a :has() selector; stop re-stringifying the editor doc per keystroke

The bottom-push rule anchored on #root :has(> [data-slot=conversation]):
a :has() match cache invalidates on every #root subtree mutation, and
the streaming chat mutates that subtree constantly — every layout-push
frame (the bottom drag writes --dsh-sidebar-height per frame) paid a
whole-document selector re-evaluation. The shell's locator (which
already holds the measured column node for the ResizeObserver) now
tags it with [data-dsh-center-col]; the rule evaluates once per node.
The tag retargets with the ref on boot swap / HMR node replacement and
is removed on unmount; the drag lane pins exactly-one-tagged-node and
its identity as the conversation slot wrapper's parent. The dead
[data-pane] branch (renamed away in alpha hosts) is gone.

TextEditor: the CodeMirror updateListener re-stringified the WHOLE
document into React state on every keystroke (O(docLength) per key,
re-rendering the shell per key). The listener now only flips the dirty
flag; the preview's draft is snapshotted from the live view when
entering preview (or on a content swap), and save() already reads the
live document.
This commit is contained in:
Menghuan1918
2026-09-01 16:01:34 +08:00
parent c7d58c9b25
commit cf542d6318
6 changed files with 63 additions and 47 deletions
+18 -6
View File
@@ -114,11 +114,13 @@ export function TextEditor(props: FileViewerProps) {
}, [content])
// Create the CodeMirror editor once the content is loaded. The view owns
// the document; React only tracks dirty/draft state through the update
// listener. For markdown the view stays mounted while previewing (hidden),
// so unsaved edits survive the preview/edit toggle. The theme + syntax
// colors live in a compartment so a scheme flip reconfigures only that
// part — the document, undo history and scroll position survive.
// the document; React only tracks dirty state through the update listener
// (the draft — the preview's text — is snapshotted from the live view on
// entering preview, not re-stringified per keystroke). For markdown the
// view stays mounted while previewing (hidden), so unsaved edits survive
// the preview/edit toggle. The theme + syntax colors live in a compartment
// so a scheme flip reconfigures only that part — the document, undo
// history and scroll position survive.
useEffect(() => {
if (content === undefined) return
const host = hostRef.current
@@ -139,7 +141,6 @@ export function TextEditor(props: FileViewerProps) {
...(language !== null ? [language] : []),
CodeMirrorView.updateListener.of((update) => {
if (update.docChanged) {
setDraft(update.state.doc.toString())
setDirty(true)
}
}),
@@ -226,6 +227,17 @@ export function TextEditor(props: FileViewerProps) {
if (mode === 'edit') viewRef.current?.requestMeasure()
}, [mode])
// Snapshot the live document into the draft whenever the preview needs
// it: entering preview (the markdown preview renders `draft ?? content`)
// and a content swap (the view was just re-created above, so the read is
// the new document — matching the reset-to-null of a clean tab). The
// updateListener used to re-stringify the WHOLE document on every
// keystroke (O(docLength) per key) for a draft only preview reads.
useEffect(() => {
const view = viewRef.current
setDraft(view === null ? null : view.state.doc.toString())
}, [mode, content])
const save = (): void => {
const view = viewRef.current
if (view === null || savingRef.current) return