Skip to content

Phase 4: typed notes - #15

Merged
XrxcGH merged 151 commits into
mainfrom
phase-4
Oct 3, 2026
Merged

XrxcGH merged 151 commits into
mainfrom
phase-4

Conversation

@XrxcGH

@XrxcGH XrxcGH commented Oct 3, 2026 •

Copy link
Copy Markdown
Owner

Summary

Phase 4 of DEVELOPMENT.md: typed notes. Text boxes sit anywhere on a freeform page or stack in a document flow, with rich formatting, Markdown shortcuts, images, tables, code, paste from other apps, and spell check. This branch also brings in Phase 3 (document model and storage) from main, so main gets both.

Outcome: a 20-page note types at 13.8 to 14.4 ms painted p95 on the Surface Laptop Studio 2, inside the 16 ms gate. The outline and squiggle-heavy conditions are still over budget; see the follow-ups.

What shipped

  • Page view: static rendering with an editor pool that mounts in place, a zoomable, pannable viewport with touch and pen gestures, object mode with move, resize, and reading order, and keep-below for inserted blocks
  • Writing: headings, lists, checklists, quotes, 11 callout types, links, and every mark, with Markdown shortcuts as you type, the slash menu, the formatting bar, text styles, folding, outline moves, AutoCorrect, and date helpers
  • Images: paste, drag, or the file picker, with resize, crop, and alt text
  • Tables and code: header rows, column widths, keyboard cells; code blocks with idle syntax highlighting
  • Paste: Word, OneNote, Google Docs, Excel, VS Code, web pages, and plain text, through one sanitizer, with Markdown paste and PDF line joining
  • Spell check through the Windows spelling service, F7, read aloud, page history with compare and partial restore, and an Editing section in Settings
  • Sync: batched flushes 150 to 300 ms after typing, splice edits, and undo through the core

Exit-gate results

  • Typing benchmark (npm run perf:typing, 14 conditions, 100 keys each), recorded in docs/perf/phase-4.md: the 20-page note at its start, middle, and end is 14.3, 14.4, and 13.8 ms p95. The callout, freeform page, table cell, code block, and 50% zoom also pass.
  • Microbenchmarks (unit suite, every pull request): Phase 4's plugins cost 0.33 ms a key p95 in the 20-page note and 0.50 ms in the outline, against 1.5 ms. A flush takes 1.2 ms.
  • Motion: touch pan at 103 fps with no dropped frames; Ctrl+wheel and pinch never drop two frames in a row. Wheel scrolling had one 3-frame hitch.
  • Paste corpus: 12 cases from Word, OneNote, web pages (including hostile HTML), plain text, PDF, Markdown, Google Docs, Excel, and VS Code, all matching their expected Markdown.
  • UI suite: page screens pass axe at every size class in both themes. New keyboard, touch and pen, and end-to-end walk specs cover writing and formatting a page; static and mounted text read the same to screen readers.
  • Gates: tokens, typecheck, lint, format, checks, Clippy, rustfmt, build, and bundle limits pass. Unit 1,277 passed; UI 149 passed. Components 443 of 444: the language picker's filter test times out on this machine at 100% load from other jobs, though none of its code changed here.

Fixed at the gate

  • 4bf48e7, fc58752: typing an address straight after Ctrl+K put its first letter into the note over the selected words
  • 4c9f7ee: a one-finger touch pan stopped after its first step (lost implicit pointer capture)
  • 80184bb: textblocks had no contain: paint, and an :nth-child(of) rule restyled every block (17.5 to 14 ms p95)
  • b1967bc: fold buttons were redrawn on every key in a heading or list item (22.8 to 14.3 ms at the note's start)
  • f364955: squiggles were re-added on every key, repainting the page (17 ms of paint a key, now 8.5)
  • c0832ff: math in static text was invisible, to eyes and screen readers, until an editor mounted
  • d1f6398: merged main; kept both content security policy changes in tauri.conf.json

Review fixes

Four high findings from the pre-merge review, merged from p4-review-fixes in 3b69571:

  • e6943e7: a pasted attribute could override a schema parse rule; every pasted attribute now goes through its own parse rule
  • 897e306: the page service numbered a page's edits even when the core refused one; it now numbers an edit only when the core accepts it
  • 9e00b8e: text sync marked unsent text as sent after a refused change; the change now stays unsent until it reaches the core
  • 8c74489: the core bridge treated an unreadable pages.json as empty and could replace it; it never replaces an unreadable page map now, and it checks page arguments

The same branch also reads a link's scheme as a browser does before checking it (f1bc893), keeps a clicked link from navigating the window (d9b98b2), and encodes the page and asset IDs in an image's address (276acd8).

Known follow-ups

  • The 20-page outline types at 36 ms p95: Chromium's text insertion, layout, and hover hit testing over 3,600 list boxes. Undo there re-parses the whole outline (38 ms).
  • 200 squiggles in view: 18.4 ms p95, from Chromium re-validating every highlight after each change.
  • Bursts, the screen reader condition, and 200% zoom pass or fail by load; script per key is 5.6 to 7 ms against 4.5.
  • Not yet measured: the 4-core reference laptop, the screen measure, typing with the real core, a UI Automation client, memory, and page-open budgets.
  • Not done: the WebDriver versions of the keyboard and touch-pen specs (no tauri-driver on this machine; the UI suite covers the flows), the manual checklist run (docs/testing/keyboard-and-screen-reader.md), page screenshot baselines, and spikes S3 and S5 on hardware.
  • Static task items have no checkbox and static callouts have no name; an expected-failure test tracks both.
  • Owner decisions: the raised bundle limits (start-up 180 KB, page chunk 225 KB), which Phase 4 flags turn on in beta, and ADRs 0020 to 0026, still Proposed with what each awaits.

🤖 Generated with Claude Code

XrxcGH and others added 30 commits September 30, 2026 11:17
The Phase 4 editor parses OpenNote Markdown (CommonMark plus extensions,
SPEC 7.1) with markdown-it, as the Phase 4 design chooses. markdown-it 15
ships its own types. @tiptap/core and @tiptap/pm move from
devDependencies to dependencies, because the editor schema ships in the
app.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The schema is built with Tiptap's getSchema from extensions, so the editor
can mount the same extension list later. It holds exactly what OpenNote
Markdown 1 can write (SPEC 7.2 to 7.4): paragraphs, headings, lists with
task state on the item, quotes, callouts with a title node, code blocks,
math atoms, images, and the marks in SPEC 7.7's nesting order.

Element metadata (SPEC 6.6) lives in node attributes that the Markdown
never carries. Parse rules read tags and data attributes only, so they
form the paste allowlist.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
parseTextBlock reads the whole CommonMark syntax with markdown-it, plus
the dialect of SPEC 7: == highlights, task items, callouts with a fold
and a title, math atoms, and the allowed HTML tags, which become marks
only when they pair and nest. Setext headings, indented code, reference
links, and other HTML map to their canonical equivalents, and any string
gives a valid document.

serializeTextBlock writes SPEC 7.7's canonical form. Text is escaped as
one line of code points (SPEC 7.6). Marks nest in the canonical order,
close and reopen on overlap, and move edge whitespace outside. A
delimiter that would not open or close under CommonMark's flanking rules
becomes an HTML tag. The writer also checks each paragraph against the
reader and falls back to tags when CommonMark's matching rules would
pair the delimiters differently.

Two lists of one kind side by side would read back as one list, so the
writer joins them, and joinListsDeep does the same for a document.

Fixtures, SPEC B.5 escape pairs, and fast-check properties cover the
round trips in both directions and the stability of writing any input.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
SPEC 6.6 keeps element data in the text block's ids, tags, styles, and
checked fields, in document order, and the Markdown never holds it. A
document holds it as node attributes. applyElementData reads the fields
into attributes: extra IDs are ignored and later elements stay without
one. extractElementData writes them back, mints IDs for elements that
have none or repeat one, and orders the maps by key.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
SPEC 2.4 IDs are 26 lowercase Crockford base32 characters: a 48-bit
millisecond time and 80 random bits. The editor needs them for text
element IDs, table rows and columns, and the tables that a paste makes.
The time and the random source are parameters so tests can fix them.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The parser tests and the paste tests both compare documents as compact
outlines, so the helper moves to its own file.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
sanitizePaste turns one clipboard paste into pieces for the editor:
text documents and table blocks. It is pure. It reads no clipboard,
loads no image, and never touches the page.

HTML is parsed into an inert document and classified by its source.
Normalizers then rebuild what each program hides in styles. Word's
mso-list paragraphs become nested lists and its character styles become
marks. OneNote's To Do images become task items, and Google Docs'
wrapper and sibling lists are fixed. Web pages lose page chrome, hidden
text, scripts, colors, and fonts, and relative addresses are resolved
against the source page. Only what the editor schema's parse rules
allow survives, so script, data, and vscript links and local file
images never reach the page.

Tables become table blocks with new IDs and inline Markdown cells.
Images become asset requests, and resolveImages swaps each source for
its asset, or for a link or the description when an import fails.
Plain text is read as Markdown when it looks like it, with GFM tables,
or else as a paragraph for each line. Text from a PDF gets a separate
joined version. HTML over 5 MB falls back to plain text.

Small captured clipboard samples live in paste/fixtures and are skipped
by CHECKS, because real clipboard HTML has long lines.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The parallel packages build against these decisions from day one, so they
land as Proposed before the code: the typed notes editor, highlighting
with lowlight, the page viewport, the keyboard model, and text box
layout. ADRs 0023 and 0025 wait for their spikes, as the plan says.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Tiptap packages declare exact peer versions of each other, so all of
them move to dependencies at one exact pin, with the table, subscript,
superscript, extensions, and suggestion packages the editor needs.
markdown-it is pinned, and lowlight and highlight.js arrive for the lazy
highlighting chunk, as ARCHITECTURE.md section 3.1 lists.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
editor/ never imports state, services, features, or the shell; it gets
what it needs through EditorHost, like ui/ does through props. A fixture
proves the rule fails. A feature's flags.ts also becomes public like its
index.ts, because flags load at start-up, before the feature's chunk.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The early core lived in features/editor; the parallel packages look for
editor/schema, editor/markdown, and features/page/paste, so the files
move there with their history. On top of it, the editor skeleton of
PLAN.md section 3.8 lands:

- schema: the table schema, the specs as data, and static DOM in slices
- markdown/index.ts: the fixed API, with the cache, the splice, the
  range re-parse, and the worker as stubs that WP1 replaces
- meta.ts and host.ts: the transaction meta keys and the EditorHost
- extensions/kit.ts: one factory per extension file, each a stub that
  its owner replaces, and the editorExtensions registry

A test builds each kit's schema and requires it to equal textSchema and
tableSchema. The serializer takes nodes from any schema instance, since
each editor builds its own.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The parallel packages build against these from day one (PLAN.md
sections 3.5 and 3.12, change P2-10):

- services/pages: the PageService types, a memory implementation with
  per-client undo, Phase 3's typing groups, and frames for other
  clients, the shared contract suite, deterministic page fixtures, and
  the Tauri adapter over the core's page commands
- core/client.ts: the client Phase 3 planned for the app bridge, with
  invoke injected from platform/tauri (P3-8), one request in flight
  per page, and the envelope and frame decoders
- platform: the pages, spelling, clipboard, images, and speech clients
  replace Phase 2's placeholders, each a Tauri wrapper and a web fake;
  ?fixture=sampler makes every web page the sampler

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
PLAN.md section 4.1's page skeleton: every seam of sections 3.6 to 3.10
exists with a working stub, so each package's tests run from day one.

- flags.ts: Phase 4's flags, joined to Phase 2's list at start-up
- keys.ts: every Phase 4 command's keys in both shortcut sets, checked
  against Phase 2's rules; registerPageCommand reads them
- registries.ts, register.ts, and one registrations file per package
- seams/index.ts: the only path Phase 5 imports
- a static viewport, a block layer, a pool that mounts every text block
  at open, and a sync queue that sends setText after each change
- PageView.tsx mounts them behind page.editor: an empty page opens with
  a text box that joins page.json when typed in, and the empty state
  has a small illustration of a window at dusk with a candle and plant
- one strings namespace per package

Page zoom moves into registrations/page.ts, and its reset command takes
the plan's ID, page.zoom100, with Ctrl+Alt+= and its neighbors.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
PLAN.md section 3.15's helpers, so each package tests its feature end
to end from day one:

- builders.ts: document builders over textSchema, and md(), which
  reads a selection marked with brackets out of Markdown
- harness.ts: renderPage mounts the real page assembly over the memory
  service, types with real keys in browser mode, and reads back what
  the service holds; expectCommand runs a command on a marked selection
- fixtures.ts: the sampler, the short note, ADR 0005's 20-page note in
  three shapes, eight freeform boxes, and the 500-block budget page

Smoke tests in each job: the unit harness test, the PageView component
test that types into an empty page and finds it in the page service,
and the Playwright spec that does the same in the production build and
opens ?fixture=sampler. ESLint treats the helpers as test code.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Phase 3 built the core but not the app bridge it planned, so typed text
had nowhere to go in the desktop app. core_bridge.rs starts the core on
the first page_open and serves page_open, page_apply, page_undo,
page_redo, page_save_now, and page_close, and flushes and stops the
core on exit. Until the storage-backed notes service lands, the
interface's pages live in one notebook of the bridge's own, mapped by
the interface's page ID. page_handle serves the image commands (P3-8).

Every other command of PLAN.md section 3.13 is a stub that answers
notImplemented: clipboard facts and file tokens, image import and
renditions, spelling, and speech. Each is listed in lib.rs, the
command list, and the capability file, and the windows crate gains the
features of ARCHITECTURE.md section 3.2. A test restarts the core and
finds the text still there.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Mounting the page pulled Tiptap, ProseMirror, and markdown-it into the
start-up bundle, which doubled it. The page body now loads lazily, and
the sync registration reaches the shown queue through a module that
needs no editor code, so start-up stays at 160 KB gzipped (P2-11).

The bundle budget gains ARCHITECTURE.md section 24.4's chunks: the page
at 170 KB, and the Markdown worker, highlighting core, each grammar,
history, read aloud, text styles, and reading order. The specific
entries come first, since the check takes the first key that matches.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Ctrl+/ shows every key a person can rely on, and Phase 4 adds the keys
the browser and ProseMirror own (P2-12): Tab in a text box, Escape and
then Tab to leave it, Enter and Shift+Enter, the clipboard keys, plain
paste, the slash menu, and the arrows, Shift, Space, Enter, and F2 with
boxes selected.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The typing gate needs a home from day one (ARCHITECTURE.md section
24.5). tests/perf/typing measures keydown to the next frame, as ADR
0005's spike did, with 100 keys 120 ms apart after 10 warm-up keys, on
the short fixture in the test build. The perf-typing CI job runs it and
keeps the numbers; WP8 adds the conditions, throttling, and the gate.
Here it reads 7.4 ms at the median and 9.5 ms at the 95th percentile.

Its own preview server and port keep it apart from Playwright runs in
other worktrees. The WebView2 harness in spikes/harness stays there
until WP8 ports it to the real app.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
PLAN.md section 4.4 asks that text typed into a page in the desktop app
come back after a restart, through Phase 3's core. The new smoke spec
types into Mitosis, closes the app, starts it again on the same
profile, and finds the text. It runs in the e2e-smoke job, where the
drivers are installed, and skips elsewhere like the other specs.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The sampler read as one gray column: every highlight was honey, colored
text and sizes were ignored, and callouts were plain paragraphs. Text
now takes BRAND.md's palette, quietly: highlighters are the pens'
colors washed out on paper, pen colors and sizes apply, bullets are
clay and checked items moss, and each callout is a soft card tinted by
its type with the color on its edge and beside its title. The title
names the type in words, so color is never the only signal, and forced
colors draw them in CanvasText.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The serializer now keeps an entry for every node it visits: the text its
parent joins, the text without its own prefixes, and where each child
starts and ends. ProseMirror shares unchanged nodes between states, so a
flush after a keystroke writes one paragraph and re-joins cached strings
along one path, and renumbering a list rewrites only items whose marker
width changed. The spans feed the range re-parse that comes next.

Types are compared by name and the read-back check builds its expected
nodes in textSchema, so editor and table schemas no longer go through a
JSON copy first. warmCache now fills the cache. A property test checks
cached and uncached text are equal across random edit sequences.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
A table block stores SPEC 6.3's data (columns with IDs and widths, rows
whose cells are keyed by column ID), while its editor holds one table
document. mapping.ts converts between the two, keeps IDs by position,
memoizes each cell's Markdown by paragraph node so a flush serializes
only edited cells, and normalizes untrusted data from the core so every
row has a cell for every column and every ID is unique.

The table commands are pure changes to that data, so a moved row keeps
its ID and new rows and columns get fresh ones. Property tests cover the
round trip, the normalizer, and random command sequences.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
The page now scrolls in its own viewport: a sizer that makes the scroll
bars match the zoomed world, the world in page units scaled by
--page-zoom, and the underlay Phase 5 draws into. The camera keeps
scroll and zoom without layout reads, a ResizeObserver keeps the
viewport rectangle, and listeners run once per frame during gestures
and once when they settle. Ctrl+wheel and touchpad pinch zoom around the
pointer with data-gesture set until 150 ms after the last input, the
three zoom commands step through the fixed zoom list, and Fit page to
width joins them. The world grows to fit the flow column and never
shrinks while the page is open. Each page's scroll and zoom are saved in
device state and restored when it opens again.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
The contract suite now holds real cases in contractCases.ts, free of any
test framework, so the same cases can run in Vitest and inside the real
app against Phase 3's core: edits with order keys and preconditions,
UTF-8 splice offsets, whole-or-nothing batches, locks, page fields,
undo grouping by Phase 3's rules, frames to other clients, failed undo
checks, and versions.

The memory service now follows the core: each open page is a client
with its own undo stacks, grouped by the rules of
crates/core/src/ops/undo/group.rs, steps that refuse to undo blocks
another client changed since, frames for the other clients, read-only
pages, changes on disk, and versions kept per save.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Task items get a native checkbox named by the item's text, Enter starts
an unchecked task, and Backspace drops the box before leaving the list.
Adjacent lists of one kind join after each local edit, since canonical
Markdown can't keep them apart. Callouts are notes named by type and
title, with a type menu and a real fold button that changes the
Markdown. Math atoms show their source and edit it in a small field.
Inline images show a chip and never load an outside source. A custom
text color is lightened on dark pages.

Adds the shared command helpers and the editor test helpers the later
WP4 commits build on.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Undo, redo, and frames from other windows hand a text block its new
Markdown. reparseRange now finds the splice, walks down through the
lists, items, quotes, and callouts that hold all of it using each cache
entry's child spans, and parses only the children it touches, widened
by one sibling. Writing the result again and comparing it with the new
Markdown proves it equals a full parse; when a level can't prove it,
the level above tries, and otherwise the caller parses the whole block.
Unchanged children at either end keep their identity and cache entries.

A property test checks the range result against a full parse for random
documents and random splices, and that adding a word to any paragraph
never needs a full parse.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
parseInWorker now starts one module worker on first need and resolves
with the document it sends back as JSON. Where there are no workers, or
the worker fails to start or to parse, the block is parsed on the main
thread instead, so callers always get a document. An aborted signal
rejects with its reason. MAIN_THREAD_PARSE_LIMIT (64 KB) tells callers
when a full parse belongs in the worker.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
The page never decodes an image to learn its size (Phase 4 ARCHITECTURE.md
section 12.2). probe.rs reads each format's header for the pixel size and
EXIF orientation (orientations 5 to 8 swap the sides, so the stored size
is the shown size), SVG sizes come from width, height, or viewBox, and on
Windows a WIC header read confirms the size. image_import and
image_import_clip check the declared type and the 50 MB cap, then store
the file through the core's PageHandle::import_asset on a blocking thread.

HEIC and TIFF are refused unless page.heicImport is on, when convert.rs
turns them into PNG or JPEG with WIC, upright. The opennote-asset scheme
serves an open page's assets through PageHandle::asset_bytes, so the
WebView never opens an asset file itself. Fixtures cover every format and
all eight orientations.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
image_import_url saves an image from a web paste into the page (Phase 4
ARCHITECTURE.md sections 12.2 and 15.5). It uses ureq with Windows' TLS
stack, as the updater does: 20 seconds, 50 MB, image/* answers only, at
most 3 redirects and never to another scheme, no cookies, no Referer, and
no proxy. Loopback, link-local, private, and other non-public addresses
are refused when the name resolves, so a pasted page can't reach into the
local network. Tests run against a local server.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
XrxcGH and others added 15 commits October 3, 2026 06:28
Each key in a paragraph with squiggles dropped its ranges from the
spelling highlight and added equal ones. Chromium repaints everything
with a custom highlight when a highlight's ranges change: 17 ms of paint
a key with 200 squiggles in view. Typing already moves the live ranges
with the text, so show() now keeps the ranges when they match. Paint
falls to about 8 ms; the rest is Chromium revalidating every highlight
marker after any DOM change, recorded in docs/perf/phase-4.md.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
The typing benchmark ran one key loop on the short note. It now runs
every typing condition of ARCHITECTURE.md section 24.2 that a browser
can reach: the 20-page note at its start, middle, and end; the outline;
the callout; the freeform page; a table cell and a code block after the
note; 200 squiggles in view; bursts at 30 keys a second; the screen
reader flag with Chromium's accessibility tree on; and 50% and 200%
zoom. Each reports painted p50, p95, and maximum, script per key, and
long animation frames.

The motion mode pans and zooms the freeform page by wheel, Ctrl+wheel,
touch drag, and pinch, and counts dropped frames against 60 Hz.

OPENNOTE_TYPING_GATE=1 holds painted p95 to 16 ms, a baseline file holds
it to main's within 10%, and calibrated throttling matches Phase 2's.
CI keeps recording the numbers, with time for the longer run.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
ARCHITECTURE.md section 24.1 gives Phase 4's plugins 1.5 ms of each
key, checked on every pull request. On the 20-page note and the 20-page
outline, the new unit test times a key's transaction against the full
editor state and against a state with no plugins, and holds the
difference to that budget. It also times the serializer cold, a flush
(serialize, splice, and the request's JSON), and the range re-parse that
applies undo frames. A fixed benchmark scales the budgets on slower
machines, as Phase 2's perf tests do.

The outline is one top-level list, so its undo frames fall back to a
full parse; the test holds that to undo's whole 50 ms budget and
docs/perf/phase-4.md lists it as a follow-up.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
ARCHITECTURE.md section 26.2 lists prose with blank lines and a single
URL among the plain-text sources. Prose keeps its paragraphs and its
inline address as text; a lone URL stays text rather than being parsed
as Markdown, as section 13 requires.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Typing an address straight after Ctrl+K put its first letter into the
note, over the selected words, because the link popover loads and takes
focus a few tasks later. Walking the app on the web build turned it up:
"Visit site", Ctrl+K, "example.com" became "Visit e" with a link to
xample.com.

The command now holds printable keys, Backspace, and Enter from the
moment Ctrl+K runs, and the popover starts its address field with them.
Holding stops when the popover opens, fails, or after two seconds.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Static text renders math as an empty element with its source in an
attribute, so display and inline math were invisible, and silent to
screen readers, until the block's editor mounted. Generated content now
shows the same $$ fences and source, in the math view's style.

A new accessibility spec compares the sampler's tree static and mounted.
Apart from the editor's own controls the two now read the same. Two gaps
remain for the schema's owner and sit in an expected-failure test: static
task items have no checkbox and static callouts have no name.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
tests/ui/screens/page.ts registers the sampler page, static and with its
editor mounted, so the cross-screen axe suite checks every node, mark,
callout type, table, code block, and image at each size class in both
themes. All 12 runs pass.

page.walk types with Markdown shortcuts, italicizes with the formatting
bar, pastes HTML, adds a code block, an image from the file picker, and
a table, then undoes and redoes, checking the Markdown the page service
holds at each step.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Keys typed at full speed after Ctrl+K still lost their first letters:
the popover added the held letters through state, and the next key's
change event read the field's old, empty value and replaced them. The
held letters now go straight into the field, then into state.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
The keyboard specs of ARCHITECTURE.md section 26.3 on the web build:
every Markdown shortcut, with undo right after one keeping the typed
characters; then marks, a link typed straight after Ctrl+K, heading
levels and Normal, an outline move, the date helper, and AutoCorrect
with its one-step undo. Each checks the Markdown the page service holds.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
A one-finger pan moved the page by its first step only. Touch and pen
pointers start captured by the element they pressed; when the gesture
tool moved the capture to the viewport, that element got
lostpointercapture, and the router, listening on the viewport in the
capture phase, took it as a cancel and ended the gesture. Synthetic
events in the component tests never capture, so they missed it; touch
sent through the DevTools Protocol showed it.

The router now cancels only when the viewport itself loses the capture.
A component test fires lostpointercapture on a pressed child mid-pan.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
page.touch-pen sends real touch and pen input through the DevTools
Protocol: a tap places the caret in a text box, a one-finger drag pans
without making a box, a pinch zooms, and a pen drag on empty page draws
a marquee without scrolling. Long presses need Windows' gesture
recognizer and stay with the desktop spec and the manual checklist.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
ARCHITECTURE.md section 30 asks for both as Phase 4 documentation.
The help page explains the keys between blocks: arrows across a
document's blocks, selecting blocks, Escape then Tab out of a text box,
Tab inside text, outline moves, and page zoom. The checklist holds what
needs a person: Narrator and NVDA, touch and pen on a Surface, IMEs and
other text entry, AltGr layouts, and contrast themes, with a table for
each run. Phase 4's run has not happened yet.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Kept both content security policy changes: main's connect-src for Tauri's IPC over fetch, and Phase 4's opennote-asset localhost image source.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
docs/perf/phase-4.md holds the exit gate run on the Surface Laptop
Studio 2: every typing condition the web build can reach, two runs (one
at 99% processor load from other jobs), the motion gestures, and the
microbenchmarks. The 20-page note types at 13.8 to 14.4 ms painted p95,
inside the 16 ms gate. The page lists what the run fixed, what is still
over budget with the cause of each (the outline, squiggles in view,
outline undo, script per key), and what still needs the reference
laptop or the desktop app.

ADRs 0020 and 0022 now point at the measured numbers in their status.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
The walk read the page service right after typing in a table cell, but
the words arrive in the next flush, 150 to 300 ms later, and later still
on a busy machine. It now polls for them, before undo and after redo.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Comment thread app/src/features/page/paste/corpus.test.ts Fixed
XrxcGH and others added 12 commits October 3, 2026 08:59
The status table now marks typed notes done with this pull request's
link, shows Phases 5 to 14 as in progress and being wired into the app,
and notes the first beta build on branch beta. A new section lists what
the app does today, the quick start lists the test commands in
package.json, and the repository map covers the editor, the page view,
the page service, the typing benchmark, docs/perf, and ADRs 0020 to
0026.

docs/README.md now links the Phase 4 performance record, the keyboard
and screen reader checklist, and the block-navigation help page. The ADR
index lists the seven Phase 4 records it was missing.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
The WIC agreement test decoded every fixture and compared its size with
the header reader's. Windows ships the WebP decoder as an optional
extension, which the CI runner lacks, so WIC answered None for the WebP
fixtures and the test failed there. With no codec there is no answer to
compare, so the test now skips only a missing answer for a WebP file.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
The popover's chunk loaded on the first Ctrl+K. On a slow runner that took
longer than the keys typed after it, which were all held for the address
field, so the link never saved. The chunk now loads in idle time with the
formatting commands, and the popover mounts synchronously once loaded, so
the held keys reach its field before the next key arrives.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
The page holds an image before the view has finished with it. The view
then selects the image and takes the focus, so a click and the slash menu's
keys sent in between were lost on a slow runner. The walk now waits for
the image's toolbar, which shows once the view is done.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
The benchmark pressed Enter at the end of the 20-page note and typed at
once. If the Enter had not landed, a block shortcut such as the code fence
could not apply, and the run waited two minutes for a code block. It now
waits until the caret is in an empty paragraph, and both waits fail within
15 seconds and say where the caret was.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
CodeQL flagged the corpus test's table cell helper, which escaped a pipe
but not a backslash, so a backslash before a pipe would read as the pipe's
own escape. The helper now escapes both in one pass. No expected table in
the corpus holds a backslash, so no expected file changes.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
The spec ended the WebDriver session right after the text showed in the
box. That stops the app at once, before the page's flush to the core,
which is 150 to 300 ms after the last key, so the text was lost on
restart as in a crash. The spec now closes the window as the close button
does. Rust holds that close until the interface has flushed the page, so
the process leaves only after the text is with the core. The assertion
also says what the page held.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
The typing benchmark focused the text box after it had collected the
box's text nodes. Focus can mount the editor, which replaces those nodes,
so the caret fell back to the page title and the next keys went there.
It now focuses first. The language picker tests typed through real key
events into a popover that was still settling, and timed out at 15 s on
loaded runners. They now set the filter's value directly, as the arrow and
Enter steps already drove their keys.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
A command used before its chunk loads runs after the keys typed behind
it, so on a slow runner a strike-through, a checkbox toggle, and a list
move came out wrong in the keyboard spec. The idle warm-up now loads the
formatting commands, the link popover, the slash menu, folds, and the
merge command together, and sets data-page-commands on the root when all
are in. The keyboard and walk specs wait for it before they type.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
The code condition failed on CI with the caret still in a heading after
Enter. A text box exists as an empty wrapper until its text is drawn in
idle time, and the old search for the longest contenteditable text box
then found only the page title, so the caret went into the title.
Delaying idle time by 3 s reproduced the exact CI message.

openFixture now waits for the page's commands (data-page-commands) and
for every text block to be drawn. longestBox searches the text blocks,
not the title. placeCaret waits for the press to mount the box's editor
and says so when the caret and focus are not in the box afterwards. The
caret context also reports whether the caret is in the title.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
later() started its module load with void load(), so a load that
rejected was an unhandled rejection. In a unit test that finishes before
the idle load does, the load runs after the environment is torn down.
Vitest then fails the run with an EnvironmentTeardownError even though
every test passed, as the ubuntu checks job did on code/commands.test.ts.

The loads are optional warm-ups: the chunk that needs the module first
imports it too. A failed one is now ignored, as the editor's own warm-up
already does. A test covers a load that rejects.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
@XrxcGH
XrxcGH merged commit 74a7204 into main Oct 3, 2026
15 of 16 checks passed
@XrxcGH
XrxcGH deleted the phase-4 branch October 3, 2026 20:49
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.

2 participants