Skip to content

feat: add browserless deterministic SVG export to record export - #171

Open
ThomasK33 wants to merge 21 commits into
mainfrom
feat/record-export-svg
Open

feat: add browserless deterministic SVG export to record export#171
ThomasK33 wants to merge 21 commits into
mainfrom
feat/record-export-svg

Conversation

@ThomasK33

Copy link
Copy Markdown
Member

Problem

agent-tty's only visual artifacts are Playwright-produced PNGs and WebM videos: every visual export needs Chromium, and WebM additionally doesn't render inline on GitHub. There is no visual artifact that is cheap to produce in CI, diffable, or hash-stable.

Mechanism

record export --format svg [--animate] — the first renderer-grade artifact with zero Playwright/ffmpeg footprint:

  • A new grid-frame replay helper (src/replay/gridFrames.ts) builds validated replay input from the session's event log and replays it through the libghostty-vt backend (in-process WASM, no browser), capturing snapshot({ includeCells: true }) at output/resize boundaries. Same-timestamp events are coalesced and consecutive identical frames are de-duplicated with merged hold durations. Still export captures only the final frame; if the optional @coder/libghostty-vt-node package is unavailable the command fails with a clear error rather than falling back to a browser.
  • A pure SVG renderer (src/export/svg.ts) groups consecutive same-style cells into <text> runs pinned to the grid with textLength, draws per-cell backgrounds/underlines and a translucent block cursor, and uses fixed cell metrics (8.4×18 px at the pinned fontSize 14) so output is deterministic — byte-identical across repeated exports — with no timestamps, random IDs, or environment-dependent content. Animation uses ID-free SMIL discrete visibility with key times derived purely from recorded event-log timing (--timing is rejected for svg). Only the two locked profiles (reference-dark/reference-light) are supported; no new themes.
  • Wired through the existing artifact plumbing: format enum in RecordExportResultSchema, .svg via recordingFilename, artifact kind recording with metadata (frameCount, animated, profileName, renderProfileHash, rendererBackend, event counts), and .svg classification in the review-bundle tooling. Docs updated in docs/USAGE.md.

Idea credit: adopted from the tcut review (docs/notes/tcut-review.md, §3.3) — reimplemented for agent-tty's replay/profile model, no code copied, no new dependencies.

How to verify

SID=$(agent-tty create --json -- /bin/sh -c 'printf "\033[1;33mHELLO\033[0m plain\n"' | jq -r .result.sessionId)
agent-tty wait "$SID" --exit --json
agent-tty record export "$SID" --format svg --out /tmp/a.svg --json
agent-tty record export "$SID" --format svg --out /tmp/b.svg --json
sha256sum /tmp/a.svg /tmp/b.svg    # byte-identical
agent-tty record export "$SID" --format svg --animate --out /tmp/anim.svg --json

Dogfood proof (isolated AGENT_TTY_HOME): repeated still exports byte-identical (29bfb9253a6b…, 704 bytes; a larger colorful session: dc369920f3f5…, 11,226 bytes twice); animated export of an 11-frame session reports durationMs: 4601, rendererBackend: libghostty-vt. The rendered SVG shows correct bold/ANSI-color runs and the cursor block.

Validation run: npm run typecheck, npm run lint, npm run format, unit suites (svg, gridFrames, record-export, protocol/golden/review-bundle/artifact-storage), and test/integration/record-export.test.ts with the native package available (all svg cases executed). New tests fail without the change; native-dependent integration cases skip cleanly when the optional package is missing.

Note: the CI audit step currently fails on every fresh run due to new upstream advisories; the fix is in the separate overrides-bump chore PR (#168).


Generated with mux • Model: anthropic:claude-fable-5 • Thinking: xhigh

aube audit --audit-level high (run by CI) reports 8 high advisories in
transitive dev dependencies: brace-expansion (<5.0.9), js-yaml (<4.3.1),
nanoid (<3.3.18), and postcss (<=8.5.17). Bump the existing overrides
block to the fixed versions; each package resolves to a single version
in the tree, so the bumps are mechanical.
- src/export/svg.ts: pure grid-frame -> SVG renderer (style runs, XML
  escaping, fixed 8.4x18 cell metrics, SMIL discrete animation)
- src/replay/gridFrames.ts: offline libghostty-vt replay capturing
  de-duplicated styled grid frames with recorded hold durations
- record export --format svg (+ --animate, --timing rejected for svg)
- schema/messages format enum, recording filename .svg extension,
  review-bundle .svg classification, USAGE.md docs
…ites

- unit tests for renderGridFramesToSvg determinism, style runs, XML
  escaping, cursor block, SMIL hold key times
- unit tests for captureGridFrames coalescing, de-duplication, recorded
  holds, final mode, and native-unavailable export error
- record export command tests for svg envelope, --animate/--timing
  validation
- native-gated integration tests incl. byte-identical repeated exports
- protocol/golden/artifact-path/review-bundle enum extensions
@ThomasK33

Copy link
Copy Markdown
Member Author

@codex review

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 3e37952d6e

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread src/export/svg.ts
The aube repository moved from endevco/aube to jdx/aube. GitHub
attestation lookups via the old path now find nothing, so mise
2026.8.x fails 'mise install --locked' with 'Lockfile requires
github-attestations provenance ... but verification was not performed',
breaking every CI job at setup. Regenerate the aube lockfile entries
with current mise (mise lock aube): URLs now point at jdx/aube and the
unverifiable provenance pins are dropped. All artifact sha256 checksums
are unchanged, so the pinned binaries are identical.
Cursor-forward output (e.g. ESC[10C) surfaces untouched columns as
unstyled empty cells; merging them into a matching default-style text
run re-anchored the run at the gap's first column and stretched the
following glyphs across the gap via textLength.

Zero-style empty cells now break the current run so the next glyph
anchors at its true column, while styled wide-glyph spacers keep
extending their run's cellCount/textLength. Added an invariant that
emitted runs cover strictly advancing, non-overlapping columns within
the row.
@ThomasK33
ThomasK33 changed the base branch from main to chore/fix-audit-advisories August 21, 2026 11:01
@ThomasK33

Copy link
Copy Markdown
Member Author

@codex review

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 4ed3a7bda7

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread src/export/svg.ts Outdated
Comment thread src/export/svg.ts
Comment thread src/replay/gridFrames.ts Outdated
- Preserve wide-glyph spans across styles: SnapshotCellSchema gains an
  optional width (>=2, leading cell only), the libghostty-vt backend
  propagates the native span (spacers stay width-free), and svg run
  grouping consumes a wide glyph's trailing spacers regardless of
  styling, so default-style CJK keeps its full textLength while
  zero-style gap padding still breaks runs.
- Embed fonts in standalone SVGs: always inline the 21 KB JetBrains
  Mono latin subset as a woff2 data URI; inline the 2.5 MB Symbols
  Nerd Font only when a rendered frame contains a Private Use Area
  glyph. Deterministic (base64 of checked-in assets) with an SIL OFL
  1.1 attribution comment.
- Replace quadratic replay in captureGridFrames with an event cursor:
  each boundary feeds only the not-yet-applied validated event suffix,
  so every event is fed exactly once.
@ThomasK33

Copy link
Copy Markdown
Member Author

@codex review

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: c215cdd7f4

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread src/replay/gridFrames.ts
Comment thread src/export/svg.ts Outdated
Comment thread src/export/svg.ts
- Timeline spans the full event log: leading non-visual gaps surface as
  a pre-visual (blank) frame held until the first visual boundary, and
  trailing non-visual events extend the final frame's hold to the last
  event timestamp (plus the final viewing hold). Model documented in
  captureGridFrames.
- Honor hidden cursors when known: SvgGridFrame gains optional
  cursorVisible and the renderer skips the cursor block when false. The
  libghostty-vt native snapshot does not expose DECTCEM visibility
  (verified against the module's TerminalSnapshot shape), so captured
  frames leave it unset and keep drawing the cursor; documented.
- Embed the Symbols Nerd Font whenever a rendered code point escapes
  the pinned JetBrains Mono latin subset (exact cmap coverage dumped
  offline and hardcoded), not just PUA glyphs, so symbols like U+26A1
  no longer fall back to environment fonts. Over-embedding for glyphs
  covered by neither face (e.g. CJK) is accepted and documented.
@ThomasK33

Copy link
Copy Markdown
Member Author

@codex review

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: b725d76037

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread src/cli/commands/record-export.ts Outdated
A running-but-silent session (e.g. sleep) has a valid empty event log;
svg export previously failed its >=1-event invariant with EXPORT_ERROR
while asciicast already supports header-only exports for this state.

captureGridFrames now synthesizes a single blank frame from the
manifest's initial dimensions (cursor at 0,0, final viewing hold)
without booting the native backend — backends reject snapshot() before
the first replayed event. capturedAtSeq follows the empty-log
convention (0, matching asciicast); frameCount 1, event counts 0,
rendererBackend 'none'. Output stays deterministic.
@ThomasK33

Copy link
Copy Markdown
Member Author

@codex review

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 0edf33d440

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread src/export/svg.ts
Glyphs outside both bundled faces (notably CJK and most emoji) render
via the viewer's monospace fallback, mirroring the reference
ghostty-web renderer's own system-font fallback. Documented in
docs/USAGE.md and at the embedding predicate: no CJK face is bundled by
the pinned profiles and glyph-path conversion would require a
font-shaping dependency. Layout stays deterministic under fallback
fonts because text runs are grid-pinned via textLength.
@ThomasK33

Copy link
Copy Markdown
Member Author

@codex review

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: c4f53a7288

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread src/export/svg.ts Outdated
A mixed-width run (e.g. A + CJK + B) merged into one <text> scales
natural advances uniformly via textLength, so a fallback font whose
wide-glyph advance ratio differs shifts interior glyphs off their
columns even though the run endpoint stays correct.

Runs are now homogeneous in cell width: each wide glyph forms its own
single-glyph run spanning its trailing spacers, while width-1 glyphs
keep merging by style. Within a homogeneous-width run a monospace
fallback has equal natural advances, so uniform scaling lands every
glyph exactly on its column. Gap and spacer semantics unchanged.
@ThomasK33

Copy link
Copy Markdown
Member Author

@codex review

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: d30294e6f7

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread src/cli/commands/record-export.ts
Still and animated SVG exports at the same seq both resolved to
recording-<seq>-svg.svg: the second export overwrote the first file
while the artifact manifest kept both entries, leaving one entry's
sha256/bytes/animated metadata pointing at the other export's content.

recordingFilename now accepts an optional variant label appended
before the extension; record export passes 'animated' for animated SVG
so it resolves to recording-<seq>-svg-animated.svg while still SVG and
the existing asciicast/webm names stay byte-identical. The default-path
seq invariant is unchanged.
@ThomasK33

Copy link
Copy Markdown
Member Author

@codex review

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: c624435dda

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread src/export/svg.ts
Comment thread src/export/svg.ts Outdated
With 6-decimal rounding, a 1 ms hold inside a timeline longer than
~16m40s serialized both frame boundaries to the same keyTime, giving
the frame a zero-length interval and silently dropping a recorded
transition. keyTimes now serialize with 12 decimals (millisecond
resolution up to 10^12 ms timelines) with trailing zeros trimmed for
canonical output, and renderVisibilityAnimation asserts serialized
boundaries stay strictly increasing for positive holds.

Also documents in gridFrames.ts that captured frames omit
cursorVisible because the native snapshot exposes no DECTCEM field.
@ThomasK33

Copy link
Copy Markdown
Member Author

@codex review

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 1ab6654e80

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread src/export/svg.ts Outdated
A background-styled empty width-1 cell between same-style glyphs (e.g.
colored field padding) matched the run's style and merged, stretching
the run across its column so the following glyph drifted off position.

Every empty cell that is not a wide-glyph trailing spacer now breaks
the current text run so the next glyph re-anchors at its true column.
Backgrounds are grouped independently of text runs via
groupRowIntoBackgroundRuns: styled empties still shade their columns,
and adjacent same-bg cells merge into one continuous rect regardless
of other styling differences.
@ThomasK33

Copy link
Copy Markdown
Member Author

@codex review

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 2727b55720

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread src/cli/commands/record-export.ts Outdated
Same-seq SVG exports with different --profile values resolved to the
same default path: the second export overwrote the first file while
the artifact manifest appended a second entry with stale
sha256/bytes/renderProfileHash pointing at replaced content.

The SVG filename variant now carries the resolved profile name
(sanitized by recordingFilename), so defaults become
recording-<seq>-svg-<profile>.svg and
recording-<seq>-svg-<profile>-animated.svg. asciicast/webm naming is
unchanged; webm's pre-existing profile collision is out of scope.
@ThomasK33

Copy link
Copy Markdown
Member Author

@codex review

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: b55a1c5486

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread src/export/svg.ts Outdated
A width-1 symbols-face glyph (e.g. U+E0A0) beside same-style latin
text merged into one run; the two embedded faces have different
natural advances, so uniform textLength scaling drifted interior
glyphs off their columns.

Each glyph is classified against the pinned latin subset coverage
(latin vs symbols/fallback path) and runs now break when the
classification changes, exactly like the width-boundary split. Glyphs
covered by neither bundled face (CJK etc.) classify as fallback too —
both resolve past the latin face, sharing advance behavior. Every run
is now homogeneous in (style, width, face), so uniform scaling anchors
each glyph to its column under monospace faces.
@ThomasK33

Copy link
Copy Markdown
Member Author

@codex review

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 7e84625308

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread src/export/svg.ts Outdated
Math.max(...frames.map(...)) and lines.push(...renderFrameElements())
argument-spread per-frame arrays; past V8's argument limit (~125k
elements, reachable by long recordings or dense frames) they throw a
RangeError. Canvas dimensions now come from a loop and frame elements
are appended one-by-one. Array-literal spreads elsewhere are
iteration-based and unaffected.
@ThomasK33

Copy link
Copy Markdown
Member Author

@codex review

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: e5a7863859

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread src/replay/gridFrames.ts
Animated capture keeps every distinct frame's dense styled grid in
memory until rendering; within the supported 50 MB event-log limit a
recording can produce enough distinct frames to exhaust the heap.

captureGridFrames now enforces a retained-cell budget (default
MAX_ANIMATED_GRID_CELLS = 20M cells, ~10k distinct 80x24 frames,
adapting to terminal size; override-able via the maxGridCells option
for tests). Exceeding it fails fast with EXPORT_ERROR suggesting a
still SVG or WebM export — frames are never silently dropped because
exported artifacts are evidence. Documented in docs/USAGE.md.
@ThomasK33

Copy link
Copy Markdown
Member Author

@codex review

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: f5e8e3c854

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread src/replay/gridFrames.ts Outdated
For an animated recording that resizes from a larger grid down,
renderGridFramesToSvg sizes the canvas to the per-frame maxima of
cols/rows, but captureGridFrames reported only the final frame's
dimensions — record export then published width/height in CLI JSON and
the artifact manifest smaller than the actual viewBox.

capture.cols/rows now carry the same per-frame maxima the renderer
sizes from (documented on GridFrameCapture). Still exports capture a
single frame, so their dimensions are unchanged; the blank-synthesized
empty-log capture is likewise unaffected.
@ThomasK33

Copy link
Copy Markdown
Member Author

@codex review

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 29ac24b21c

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread src/export/svg.ts Outdated
Base automatically changed from chore/fix-audit-advisories to main August 21, 2026 13:34
The two-bucket face classification merged adjacent width-1 glyphs
where one resolves to the embedded Symbols Nerd Font and the other to
the viewer's system fallback (e.g. alpha + U+26A1): different real
fonts with different natural advances under one aggregate textLength
drifted interior glyphs.

GlyphFace now has three buckets — latin, symbols, system — with
symbols classified by the pinned SymbolsNerdFontMono-Regular.ttf cmap
coverage (dumped offline from the checked-in asset's format-12 cmap:
10,410 code points in 28 ranges, hardcoded with regeneration notes).
Run grouping already breaks on face change. The embedding predicate is
narrowed to glyphs the symbols face actually covers: system-fallback
glyphs (CJK etc.) gain nothing from the 2.5 MB payload and no longer
trigger it.
@ThomasK33

Copy link
Copy Markdown
Member Author

@codex review

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: e9cce6c948

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread src/export/svg.ts
Adjacent system-fallback glyphs (covered by neither bundled face) can
resolve to different system fonts per script (e.g. Greek + Hebrew), so
even a same-bucket merged run scales unequal natural advances under
one aggregate textLength and drifts interior glyphs.

System-classified glyphs never merge and close their run immediately
(wide system glyphs close via the existing spacer branch), so every
system glyph is pinned individually via its own x + textLength and
cannot drift regardless of which face the viewer picks. Latin and
symbols runs keep merging: each is a single known monospace face with
uniform advances. Guarantee documented: runs are homogeneous in
(style, width, face) and system-fallback glyphs are always singleton
runs.
@ThomasK33

Copy link
Copy Markdown
Member Author

@codex review

@chatgpt-codex-connector

Copy link
Copy Markdown

Codex Review: Didn't find any major issues. What shall we delve into next?

Reviewed commit: 39cc13755f

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

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