Perf/typing hotpath main#96
Open
luca-chen198 wants to merge 38 commits into
Open
Conversation
Prints one line per keystroke with a per-phase breakdown (wiki, backtick, parse, activeTok, latexMap, restyle, codeSel, overscroll) plus the document length, so typing costs that scale with file size become visible. Deep notes count wasted layout fragments and re-rendered tables. Debug-only (compiled out in Release; silence with env MD_PERF=0). Remove after the perf work lands. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Six-task TDD plan to make per-keystroke cost O(1) in document length, based on the multi-agent audit + measured PerfTrace baseline (139k doc ~12.8ms/key). Note: /docs is gitignored in this repo; force-added onto this feature branch per request. Drop before merging to main if the convention should hold. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…ance Every keystroke re-rendered every inactive table to a fresh NSImage (7-17ms with 10 tables). Table pixels depend only on source, font, colors and appearance, so identical keys now reuse the cached image. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
makeStorageState rescanned and rebuilt the whole document per keystroke once any [[ existed (3.4ms at 139k chars). Edits that provably cannot touch link syntax now splice into the previous storage string in O(edit + links); anything ambiguous falls back to the full rebuild. DEBUG samples every 64th keystroke against the full rebuild. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
components(separatedBy:) allocated a whole-document substring array per keystroke just to count fences. One allocation-free scan replaces it, and textDidChange bridges tv.string exactly once. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…cument head Enumerating from documentRange.location with .ensuresLayout laid out every fragment above the viewport on each keystroke - O(caret position). Starting at viewportRange keeps the walk O(viewport). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The parse cache compared the whole document string on every call - O(doc) per keystroke AND per caret move. A generation counter bumped on every storage edit makes hits O(1), with the string compare kept as a correctness fallback. BlockParser now reuses the UTF-16 buffer the tokenizer already extracted instead of re-extracting it. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Adversarial review of the rebase onto main (clipboard pipeline) confirmed: 1. Smart-input interceptors (auto-pair, ->-arrow, Tab indent, list-exit, $$-wrap) suppress the typed keystroke and perform a different programmatic edit, but the suppressed edit's pendingEditedRange was left as the wiki splice descriptor - silently diverging the storage form (and the saved file) from the display. Refresh the descriptor for programmatic edits too. 2. The table-image cache keyed colors by NSColor description, which is not an identity (named dynamic colors collide, unnamed never hit) and omitted mutedText/highlightColor/latex inputs renderTable draws with. Key on appearance-resolved sRGB components of every color used, plus the latex renderer type. 3. IME composition updates (setMarkedText) mutate the storage without textDidChange while shouldChangeTextIn's own parse re-caches the pre-edit string at the current generation - same-length updates then restyled from a stale parse. Bump the generation in setMarkedText. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…ick census Live profiling decomposed the remaining ~6.5ms parse at 139k chars: 59% was one prefix/suffix diff scan run TWICE (BlockParser and the tokenizer each did their own), plus a full-document String == in the parse-cache verify that cost ~6ms per keystroke on its own. - DocumentParseState: per-editor buffer + blocks + tokens evolve under ONE edit descriptor; steady-state typing splices the UTF-16 buffer in O(edit) and re-tokenizes only the touched block window. Trust gate: exactly one proposed edit per cycle; anything else (interceptor substitutions, IME commits, WT batches) falls back to a single shared scan, and a sampled DEBUG assert re-extracts every 64th keystroke. - The state publishes each result into the static BlockParser/tokenizer memos, so the restyle's DocumentAST.parse hits via memcmp instead of re-splicing against a one-keystroke-stale cache (~1.5ms saved). - parsedDocument's verify compares via NSString.isEqual (byte compare) instead of bridged String == (~6ms -> <1ms), and the selection-change pre-parse hands the pending descriptor through. - Backtick census updates from the edited window only: the greedy count is exactly the sum of floor(runLen/3) over maximal backtick runs, so expanding the window through adjacent backticks composes exactly. Reseeded per document; IME compositions force a full rescan. - Table image cache: resolve theme colors once per theme+appearance (identity-memoized), not six times per table per keystroke. Fuzz suite: after every random edit (descriptor-driven, scan-driven, widened descriptors) tokens must equal a from-scratch parse; census composition fuzzed incl. cross-boundary fence merges. Measured at 139k chars (Debug): total 10.5 -> 3.6-4.8ms per keystroke (parse 6.0 -> 0.7-0.95, backtick 0.95 -> 0.00, restyle 3.5 -> 2.0-2.4). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…ote) Debug-only PerfTrace additions for the table upward-shift hunt; remove with PerfTrace after sign-off. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…ettle before caret reveals Reverts the viewport-scoped walk (1ea2712). Every fragment's Y is the sum of the heights above it, so leaving above/below-viewport fragments estimate-only broke the invariant that visible geometry is settled by draw time: TextKit estimates an invalidated table paragraph as a single text line (~250pt short), which made content shift upward while typing, made the height measure bistable (frame pumping 5231<->4735 per keystroke), and fed the caret reveal wrong out-of-view verdicts that actively scrolled up. Live-diagnosed via docH/scrollY/caret traces. The reveal keeps one hardening from that hunt: an out-of-view verdict is re-checked against layout settled up to the caret before scrolling, so genuine jumps (search hits) land on the correct target. Steady-state cost of the head walk is an enumeration over already-laid fragments (~1ms at 139k chars); the big hot-path wins (parse splice, census, table image cache) are layout-neutral and unaffected. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
scrollJump watcher, FULL-RESTYLE/REBUILD/BIG-APPLY prints, reveal print. Debug-only; remove with PerfTrace after sign-off. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…derivations Live profiling of the residual restyle cost (2-2.5ms at 139k, 6-9ms in table docs) found the work was AROUND the already-scoped styler, not in it: - The five NSImage passes (block/inline LaTeX, image links/embeds, tables) walked EVERY document token per keystroke even though attribute application clips to the candidate paragraphs. StylingContext now carries the scope bounds and each pass skips tokens outside them. Table-doc styleTables: 3.6-9ms -> 0.35-0.9ms (out-of-scope tables skip the render lookup and anchor build entirely). - DocumentAST.parse's scoped-block filter became a sorted sweep over candidates instead of scanning every candidate per block (it went quadratic in formula-rich docs). - Table parse + content-hash memoized (tableMeta) instead of recomputed for every table every keystroke. - ParsedDocument gained a version stamp (bumped only on FRESH parses); computeActiveTokenIndices is memoized on (version, selection, suppressed) — it ran 3x/keystroke on identical inputs, now ~0ms. - updateCodeBlockSelection takes the indexed code blocks from the classification pass (no per-call full-token filter) and dedupes identical emits. Key uses the FULL active-token set: a caret move into a standalone block above a code block toggles that block's height and shifts the code block — same version/scroll/width — so keying on only the code intersection would leave the copy-button overlay stale (adversarial review, confirmed 2/2). - editedTableParagraphs filters the classified table tokens with an early exit; TextStylingService.normalize dedupes in one O(n) pass. Measured (Debug): 139k doc ~4.5 -> ~3ms/keystroke; table doc styleTables ~9 -> <1ms. All 206 tests green. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…; strip session diagnostics textDidChange bumped parseGeneration a second time per keystroke (after shouldChangeTextIn's bump), which forced parsedDocument off its O(1) generation hit onto the O(doc) byte-compare VERIFY (~0.7ms at 139k). For a trusted length-changing edit the length alone already invalidated the pre-edit cache and the selection-change re-parsed the post-edit text at the current generation, so the bump is skipped and the hit is O(1). Same-length and untracked edits still bump so the byte-compare keeps catching same-length content changes; undo/IME/bypass paths unaffected (adversarial review: only an over-strict DEBUG assert was flagged, removed). Measured (Debug): 139k doc parse 0.7 -> 0.0ms, total ~3 -> ~2.3ms/keystroke (short-file ~1.1ms). Also removes the temporary height/flicker/parse-timing diagnostics added during the investigation; PerfTrace itself stays. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Feed the styler's five NSImage passes (block/inline LaTeX, image embeds, image links, tables) a pre-classified per-kind indexed-token bundle (ClassifiedStyleTokens) built once in the parse classification, so each pass binary-searches its own small array to the restyle scope instead of walking every document token via enumerate+filter every keystroke. - MarkdownStyler.StylingContext: IndexedToken, ClassifiedStyleTokens, scoped() binary search over the location-sorted per-kind arrays. - Latex/Images passes iterate ctx.scoped(ctx.<kind>Indexed); styleTables iterates the classified table array (still a full occurrence scan). - Raise table metadata/image cache caps above a document's table count so a table-dense doc stops thrashing (Belady-pessimal FIFO) each keystroke. - Viewport-cull updateCodeBlockSelection to the visible range. - Scope the textDidChange latex/image restyle candidates to the edit. Retains the per-keystroke PerfTrace timing splits (styleAttributes / restyle split / styleTables substring+meta) as WIP diagnostics on this branch. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…full-rebuild verifiers behind MD_PERF_VERIFY The printed per-keystroke totals were misleading in two ways: - The keystroke's real work starts in shouldChangeTextIn (pre-edit parse, smart-input interceptors — every space/Enter walks this path) and the first post-edit parse runs in the mid-edit selection change; both ran BEFORE PerfTrace.begin, so textDidChange's 'parse' span showed only the O(1) cache hit. The frame now opens in shouldChangeTextIn, begin() continues an already-open recent frame, and preParse/smartInput/ selStates/selParse/selRestyle spans attach to it. - Three sampled every-64th-keystroke verifier asserts (wiki splice, backtick census, spliced parse buffer) each ran full O(doc) rebuilds synchronously in the keystroke turn — periodic spikes in the very numbers under investigation. They are now opt-in via MD_PERF_VERIFY=1. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…e, not an O(doc) scan per space/Enter
Every space/Enter/Tab/>/[/(/{ fell through the list handler's fast path
into `textView.string.contains("`")` — a full bridge copy + scan of the
document — and, once any backtick existed anywhere, a full tokenizer pass
via MarkdownDetection.isInsideCodeBlock(location:in:). All of it BEFORE
PerfTrace.begin, so it never showed up in the printed totals.
The coordinator already parses the pre-edit document in shouldChangeTextIn;
it now threads parsed.codeTokens down (MarkdownInputHandler → MarkdownLists)
and the handler binary answer costs O(#code tokens). Direct callers without
a parse keep the old derivation (isInsideCodeBlock: nil fallback).
Also: the Enter-at-fence-opener completion counted ``` occurrences via
components(separatedBy:) — an O(doc) substring-array allocation — and a
bridged .contains for the closing check; both are now allocation-free
NSString range(of:) scans.
Behavior locked by ListHandlerCodeContextTests (7 characterization tests
through the production delegate path).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…bridge per delegate event Two O(doc)-per-keystroke costs in the delegate callbacks, both memcpy-class but unconditional: - shouldChangeTextIn bumped parseGeneration first and parsed the pre-edit text after, so the parse always missed the O(1) generation check and fell onto the O(doc) NSString.isEqual byte-compare. The text at that point is still pre-edit — identical to what the previous cycle cached — so parsing BEFORE the bump O(1)-hits. The bump (and the descriptor refresh) still runs for every proposed edit, including programmatic/WT/raw ones. - shouldChangeTextIn and textViewDidChangeSelection re-read tv.string at ~15 separate sites per keystroke; each read is an O(doc) copy of the mutable backing store (the 40c8508 hoist only covered textDidChange). Both now bridge once and pass the hoisted string/NSString through (updateSelectionStates takes it as a parameter; the wiki snap-back branch keeps its explicit re-reads because it mutates the text). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
… edit-scoped token lookups textViewDidChangeSelection mapped EVERY latex/blockLatex/imageEmbed token in the document to its paragraph on every selection change — including the mid-keystroke one whose restyle is skipped anyway — and fed them all into each caret-move restyle, widening scopeBounds to the whole document and defeating the styler's per-pass culling (O(#formulas) per caret move; the formula-rich perf docs hit this hard). - Candidates are now built only when the restyle actually runs. - The formula map covers only tokens inside the caret/previous-caret paragraphs (rendered↔raw flips of entered/left tokens were already handled by tokenRestyleParagraphs). - The edit-scoped token lookups in textDidChange (latexMap, edited tables) and the styler's scope culling now share one binary-searched slice helper (MarkdownStyler.scopedSlice) instead of walking each per-kind array linearly from the document head. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…-restyle buffer re-extraction Every restyle ran MarkdownASTStyler → DocumentAST.parse → BlockParser.parse with no buffer, so even a cache HIT first allocated a doc-length [unichar], getCharacters-copied the whole document, and memcmp'd it against the seeded cache — O(doc) per keystroke that grew with file size and showed up inside the 'restyle' span. ParsedDocument now carries the block list its tokens were derived from (DocumentParseState.currentBlocks), and the restyle hands it down as precomputedBlocks (restyleTextView → TextStylingService → MarkdownStyler → MarkdownASTStyler → DocumentAST.parse), skipping BlockParser.parse entirely. Direct callers without a parse (initial load, appearance change, tests) keep the derive-it-here path. restyleParagraphs also bridged tv.string twice per call; hoisted to one. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…te for out-of-scope unique tables styleTables visited EVERY table in the document per keystroke: a full source substring, a dictionary lookup keyed by that string (hashing all its bytes even on a hit), and a (token.range, .spellingState: 0) emission — which TextStylingService applies UNCLIPPED, so every keystroke wrote attributes over every table in the document (attribute edits can also invalidate TextKit-2 layout fragments, coupling this into the ensureVisibleLayout head-walk). Equal content implies equal source length, so a table whose length is unique in the document can never affect a duplicate's occurrence index. Inactive, out-of-scope, unique-length tables — in a typical document, all of them while typing prose — are now skipped before any per-table work. Same-length tables stay on the full path to keep duplicate sourceIDs stable; the PERF note reports skipped=N. Locked by TableScopeSkipTests (skip, in-scope emission, duplicate bookkeeping). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
A smart-input interceptor (Enter-in-list, auto-pair, Tab indent, '->' substitution, $$-wrap, [[-completion, fence auto-close) suppresses the typed key and performs one programmatic edit. That left pendingEditCount at 2, so textDidChange distrusted the descriptor and fell back to a full O(doc) backtick rescan plus a descriptorless O(doc) diff parse — on some of the most common keystrokes there are. The suppressed keystroke never applies, so performEdit / insertTextProgrammatically now drop its pending count before the programmatic edit registers; shouldChangeTextIn refreshes the descriptor for that edit (60faf0b), which describes the applied transition exactly, and the cycle stays a trusted single tracked edit. InterceptorTrustTests pin the trust flag per interceptor (via a Debug-only debugLastEditWasTrusted diagnostic); the pre-existing InterceptorStorageSyncTests prove storage correctness on the now-trusted path. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…full-reparsing every keystroke Typing INSIDE a fenced code or $$ block was the biggest remaining per-keystroke cliff: incrementalParse unconditionally bailed whenever the ±1-block window contained a fencedCode/blockLatex block, so every keystroke in a code block ran computeBlocks over the whole document plus a fullTokens pass over every block. The bail is redundant with the existing guards: the ±3 delimiter check (old AND new buffer) already bails on any edit that creates, destroys, or touches a ```/$$ pairing, and an edit that un-closes a block by adding trailing chars on its closer line makes the reparsed block reach the window end — the trailing guard bails there. Interior edits reparse the whole opaque block inside the window, which is sound because windows are whole blocks. Pinned by FenceInteriorIncrementalTests: direct splice equivalence for fence + $$ interiors, the un-closing edge, and a 4-seed fence-heavy differential fuzz (300 random edits each) against the from-scratch parse, on top of the existing ParseIncrementalEquivalenceTests suite. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…ilar-tables doc skipped nothing Live measurement (682k chars, 533 near-identical tables) showed skipped=0 with substring+meta at 3.35ms/keystroke: the unique-length heuristic never fires when tables share length classes, which generated/templated tables almost always do. A sourceID is only CONSUMED by a table that renders this pass (inactive + in scope), and equal content implies equal length — so only tables sharing a length with a RENDERING table need their hash for occurrence stability. Typing prose renders no table, so now every table skips (styledRanges and the unclipped spellingState writes drop with it). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…ate split The incremental machinery itself showed up at selParse=6.77ms in a 682k-char document: the affected-block window in BlockParser.incrementalParse and the touched-block scan in incrementalTokens walked the block list linearly — O(#blocks) client code per keystroke (tens of thousands of blocks at 100k words, -Onone). Blocks tile in order, so both are binary searches now. Also: PerfTrace note with the parse-state split (buffer / blocks / tokens, splice-vs-FULL mode, block+token counts) so the next live measurement pinpoints what remains — the O(#tokens) widen/prefix/suffix passes are the known Phase-3 (relative ranges) target. Covered by the existing differential fuzz suites (ParseIncremental- EquivalenceTests + FenceInteriorIncrementalTests, 250-300 random edits per seed against ground truth). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…rs flipped past the ±3 window Adversarial review of a479348 found (and reproduced standalone) a real divergence: isBlockLatexOpen matches the TRIMMED line prefix, so an edit in the leading whitespace of an indented ' $$' opener flips its pairing while sitting more than 3 chars from the literal $$ — the old ±3 guard passed, the window splice kept the stale block, and the wrong parse persisted through the seeded caches (both insert and delete directions). hasBlockDelimiter now expands the scan to the full LINES the edit touches (delimiters are line-classified), with a capped boundary walk that reports a delimiter when the cap is hit (conservative full parse). Pinned by two regression tests with the exact repro; the fence-heavy fuzz doc gained indented $$ blocks. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…lbacks + overscroll detail + other= The 682k live frames grew to ~47ms with ~32ms outside every span. New DEBUG-only accounting to pin it down: - PerfTrace.accumulate sums repeated in-AppKit work under one +label (caret reveal, spell-checker callbacks) and end() prints other= (total minus everything measured). - scrollRangeToVisible and its doc-start→caret settle layout accumulate as +reveal / +revealSettle — the settle is O(caret depth) REAL layout and runs only on out-of-view verdicts, invisible until now. - setSpellingState accumulates as +spellCb (full-document contains scans per checker callback). - recalcOverscroll notes fullLayout/hChanged/osChanged per keystroke — a persistent fullLayout=1 with flipping heights is the bistable-height loop paying a FULL document ensureLayout inside the overscroll span. - selActive/selAuto/selCtx spans + classify note close the selection- change accounting gap. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…scan other=30ms survives with the engine back at the fast state — the cost sits BETWEEN our delegate callbacks, not in them. @shouldOut/@selIn/@selOut/ @didin checkpoints (offsets from frame start) locate which AppKit gap eats it: edit application (shouldOut→selIn), or the didChangeText machinery (selOut→didIn). The wide-table presence scan (a full attribute-run walk on every restyle when the doc has NO wide table) gets a ⏱ stamp when >0.3ms — it runs between keystrokes and contributes felt lag without appearing in any frame. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…mulated spans The checkpoints located the missing ~25ms precisely: between shouldOut and selIn — AppKit's edit application (replaceCharacters → processEditing → TextKit-2 sync/layout). Our only code in that window besides O(1) hooks is the layout-fragment provider, which pays a font-metrics lookup + paragraph style + NSDictionary per created fragment; if invalidation makes TextKit recreate hundreds of fragments per keystroke, that's the gap. +fragProv=…(×n) will show it. Accumulated spans now print call counts. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…n, fix CI cache collision - Reduce the five typing-perf test files to their main behaviors (fence/$$ interior splice + the indented-$$ regression; interceptor trust flag; table scope-skip; precomputed blocks; list-handler code context). The removed edge/variant cases and the redundant fuzz are covered by the pre-existing ParseIncrementalEquivalenceTests + InterceptorStorageSyncTests. - Fix the failing CI test (TableImageCacheTests.secondRequestIsServedFromCache): the old TableScopeSkipTests rendered the same '| alpha | beta |' table, warming the shared static tableImageCache so the cache test's first request was no longer a fresh render under parallel CI ordering. The trimmed table tests use unique sources (kappa/lambda, mu/nu). - Delete the implementation plan (docs/superpowers/plans/2026-07-07-typing-perf-hotpath.md). 217 tests green. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…g sideways Long sentences in a cell used to force the whole table wide (one line per cell), pushing every table into the horizontal-scroll overlay. Now, when the natural column widths exceed the available container width, columns share the width proportionally (floored at ~3.5em each, never above their natural width) and cells word-wrap onto extra lines — Obsidian-style. Rows size individually to their tallest wrapped cell. - renderTable/tableImage take availableWidth; it is part of the image cache key (layout depends on it), so appearance/width changes render fresh while repeated same-width restyles stay cached. - Tables whose per-column floors genuinely don't fit (many columns) still render wider than the container and keep the scrollable-overlay path. - Known limit: without a reading column, a window resize doesn't re-render already-styled non-wide tables until their next restyle (the fixed-width reading column — Nodes — is unaffected). TableWrappingTests pin: wraps to width, height grows when narrower, small tables keep natural width, width is a cache-key dimension. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…f growing sideways" This reverts commit 3e8cdfb.
…of growing sideways" This reverts commit be098b9.
…, horizontal scroll when minimums don't fit Refines the cell-wrapping layout per the CSS automatic table layout algorithm (W3C 17.5.2.2): column min = widest whitespace-separated segment (measured per segment — a too-narrow boundingRect emergency- breaks INSIDE words and understates the minimum), column max = one-line width. Available width is distributed proportionally to (max−min) above the minimums; when even the minimums don't fit (many-column tables), the table stays wide and the horizontal-scroll overlay takes over. Demo gains a Tables section showing both cases. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
… dot under rendered tables Live-diagnosed (🔴 SPELL value=1 inTable=true APPLIED text="Setapp"): the checker painted red squiggles onto words in table SOURCE, which is collapsed to a ~1pt line under the rendered image — showing up as a stray red dot below the table. Tables were the one rendered token kind missing from isInsideSpellcheckSuppressedToken (code/latex/links were covered); the per-keystroke doc-wide spellingState scrub that used to hide this went away with the table-skip optimization. The suspected typing-lag from the earlier attempt at this fix was since proven to be TextKit-internal element shifting, unrelated to this path (+spellCb accumulate shows the callback cost directly if ever in doubt). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The overlay is a control surface (rendered table image + horizontal scroller), not text, but the text view's tracking areas are not occlusion-aware — super kept setting the I-beam through the overlay on every mouse move. Mirrors the existing task-checkbox suppression branch (skip super entirely; setting the arrow after it flickers). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The one-line .table fix stays (6efe6b7); the temp 🔴 SPELL logging served its purpose (live-proved the squiggle-on-collapsed-table-source cause). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…croller Only the scroller strip is a control surface — over the rendered table image the normal text-cursor behavior stays. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
No description provided.