perf(word-addin): cut streaming and chat-open hot-path costs in the tracked-edits runtime

↗ view on GitHub · Amal · 2026-08-12 · 72254ef3

Three fixes to the same runtime, all about respecting WKWebView's
single main thread: work done per SSE chunk or per Word round-trip
competes directly with paint, scroll handling, and typing.

1. PARSE STREAMED REDLINES ONCE PER FRAME, NOT PER CHUNK
WHY: projectRedlineStream re-parses the accumulated answer from index
zero, so invoking it synchronously on every SSE chunk makes a stream
O(n^2) -- the longer the answer, the more each keystroke-sized chunk
costs, exactly while the transcript is animating.
HOW: the chunk handler now only flags redlineParsePending; the
projection runs inside the same requestAnimationFrame callback that
already coalesces the transcript publish, i.e. once per painted frame
with the latest snapshot. Terminal paths stay exact: success flushes
synchronously and still runs the streamComplete pass, and the
catch/abort path flushes before markIncompleteRedlines, so a sealed
edit in a trailing un-flushed chunk still applies on cancel. A
sendIsCurrent() predicate (currency minus the abort bit) guards the
deferred parse so a rAF firing after a session switch cannot schedule
Word edits under the new generation -- deliberately not the stricter
requestIsCurrent, because an aborted-but-current stream must keep the
edits it already received.

2. STOP THE EDIT CONTROLLER FROM RECREATING handleChat MID-STREAM
WHY: the hook returned a fresh object literal carrying editStateByKey,
and handleChat listed that controller in its deps -- so every
receiving->applying->pending transition recreated handleChat and
re-rendered everything holding it, defeating the message-ref mirroring
built precisely to keep it stable.
HOW: the controller now exposes a useMemo'd streamController
(processLiveRedlines / markIncompleteRedlines / waitForMessageEdits,
all useCallback-stable) separate from editStateByKey. The chat hook
receives only the stable streamController, so handleChat's identity
survives the whole stream; components that render edit state still
consume editStateByKey and re-render on real state changes.

3. RESTORE A CHAT'S TRACKED EDITS IN ONE Word.run BATCH
WHY: opening a chat ran one serialized Word.run (~4 context.sync()
host round-trips) per stored edit behind the global mutation queue --
pane readiness was linear in chat history, and every user action
queued behind the backlog. Each sync is a WKWebView<->host hop.
HOW: restoreTrackedEdits(descriptors) performs one Word.run for the
whole set: all getBookmarkRangeOrNullObject lookups load before one
sync, then items, verification, stale-bookmark deletes, and tracking --
a constant ~4 syncs total. Missing bookmarks are null objects, never
batch failures; per-edit classification (not-found / resolved /
view-only / restored) is preserved verbatim; if Word fails the shared
batch outright, each edit retries sequentially via restoreTrackedEditNow
so one bad object cannot sink the rest.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015U1iPxsADQ2yuksNmDuvvW
Repository open-legal-products/mike
Author Amal <mamalanand3@gmail.com>
Authored
Committed
Parents 80ac1dca
Stats 5 files changed , +301 , -40
Part of Bring the Word add-in chat experience in line with the web app

Capture this commit into my fork

Download a Markdown prompt that tells Claude how to port this exact commit into your working tree. Run it via claude -p < capture-commit-72254ef3.md from inside the repo you want the change in.

⬇ Download capture-commit-72254ef3.md