Skip to content

fix: bundle deterministic nerd font fallback for renderer#11

Merged
ThomasK33 merged 1 commit into
mainfrom
agent-terminal-v12b-1
Mar 26, 2026
Merged

fix: bundle deterministic nerd font fallback for renderer#11
ThomasK33 merged 1 commit into
mainfrom
agent-terminal-v12b-1

Conversation

@ThomasK33
Copy link
Copy Markdown
Member

Summary

  • bundle a deterministic Nerd Font symbols fallback alongside JetBrains Mono for the Ghostty Web reference renderer
  • extend render profile metadata and hashing to support ordered bundled font assets
  • preload and verify bundled font assets in the harness, add focused renderer coverage, and commit reviewer-facing dogfood proof bundles including LazyVim screenshots

User-facing / automation-facing behavior changes

  • built-in reference render profiles now use an explicit multi-font stack with bundled fallback symbols
  • renderer startup now verifies bundled font asset identities before registration
  • glyph-heavy screenshots, exports, and LazyVim UI captures render Nerd Font symbols deterministically instead of falling back to host-specific fonts

Validation

  • "/home/coder/.local/bin/mise exec -- npm run typecheck"- "/home/coder/.local/bin/mise exec -- npx vitest run test/unit/renderer/types.test.ts test/unit/renderer/profiles.test.ts test/unit/renderer/ghosttyWebBackend.test.ts test/integration/renderer-backend.test.ts test/e2e/unicode-grid.test.ts"- "/home/coder/.local/bin/mise exec -- npm run format:check"- "/home/coder/.local/bin/mise exec -- npm run lint"- "/home/coder/.local/bin/mise exec -- npm run build"- "/home/coder/.local/bin/mise exec -- npm test"

Design-doc deviations

  • none

Review artifacts

  •       dogfood/20260326-nerd-font-fallback/unicode-grid-reference-dark.png
    
  • dogfood/20260326-nerd-font-fallback/unicode-grid-reference-dark.webm
  • dogfood/20260326-lazyvim-nerd-font-check/lazyvim-dashboard-reference-dark.png
  • dogfood/20260326-lazyvim-nerd-font-check-2/lazyvim-space-reference-dark.png

📋 Implementation Plan

Plan: Deterministic Nerd Font fallback for the reference renderer

Recommendation

Adopt a deterministic bundled fallback-font approach for the Ghostty Web reference renderer:

  • keep JetBrains Mono as the primary text face,
  • add one bundled Nerd Font symbols fallback (prefer a narrow mono/symbols-only asset rather than a full patched family),
  • teach render profiles to describe multiple bundled font assets in order,
  • preload those assets in the harness before ghostty-web initializes,
  • keep custom profiles without bundled assets working as best-effort system-font profiles.

This is the most sensible long-term option for agent-terminal because the project treats the renderer as reference visual truth and relies on reproducible screenshots, WebM exports, and artifact hashes. I am comfortable maintaining explicit bundled assets + strict profile metadata + tests. I am not comfortable making the reference renderer depend on host font discovery, system package state, or browser-permission-driven font APIs.

Context and verified evidence

  • src/renderer/bundledFont.ts currently defines and hashes one bundled font asset: JetBrainsMono-Regular-latin.woff2.
  • src/renderer/types.ts currently models render profiles with fontFamily plus an optional single fontAssetIdentity.
  • src/renderer/profiles.ts uses that single bundled JetBrains Mono asset for both built-in profiles: reference-dark and reference-light.
  • src/renderer/profiles.ts#hashProfile() includes font metadata in the render-profile hash today, so the renderer already has a good reproducibility hook.
  • src/renderer/ghosttyWeb/backend.ts serves a single bundled font route, and src/renderer/ghosttyWeb/harness.html has a single-font loadBundledFont() path that hard-codes JetBrains Mono.
  • Current validation already covers the important renderer layers:
    • test/unit/renderer/profiles.test.ts
    • test/unit/renderer/ghosttyWebBackend.test.ts
    • test/integration/renderer-backend.test.ts
    • test/e2e/unicode-grid.test.ts
    • test/e2e/renderer-slice.test.ts
  • Existing reviewer proof patterns already live under dogfood/, with PNG screenshots, JSON envelopes, and WebM/asciicast artifacts.

Why this option is the best fit

  1. Matches the repo architecture. The renderer is a pinned reference backend; bundled fonts keep rendered output aligned with that model.
  2. Preserves reproducibility. Screenshot differences should come from recorded terminal state or explicit profile changes, not from which fonts happen to be installed on the machine.
  3. Keeps failure modes explicit. If a fallback glyph asset is missing, the renderer can fail fast and surface it clearly instead of silently drifting to host-specific fallback.
  4. Keeps the scope contained. The work stays inside renderer/profile/test codepaths and should not disturb PTY, event log, or replay semantics.
  5. Reduces long-term support burden. A small curated bundled fallback is easier to support than local-font probing or broad font configuration heuristics.

What I am comfortable maintaining long term

  • A small bundled font registry with strict identity checks.
  • Built-in reference profiles that declare an explicit CSS font stack plus bundled font descriptors.
  • Profile hashing that changes intentionally when bundled font assets or ordering change.
  • One dedicated glyph-heavy fixture/test and one dogfood proof bundle for reviewer validation.
  • Best-effort custom profiles that may still rely on system fonts, clearly separate from the reproducible built-in reference profiles.

What I do not recommend taking on now

  • Host/system font discovery as part of the built-in reference path.
  • Browser permission / local-font API dependencies for screenshot correctness.
  • Bundling multiple full Nerd Font families when a smaller mono/symbols fallback should solve the actual tofu problem.
  • Broad user-facing font-management features before the reference renderer path is deterministic.
Why I am rejecting the other options for now

System-font fallback only

Low implementation cost, but screenshots would vary by machine and CI image. That conflicts with the current renderer philosophy and makes artifact review less trustworthy.

Browser/local-font discovery

Potentially clever, but too environment-specific and brittle for a core renderer feature. It also adds browser-permission and API-surface complexity that does not pay for itself here.

Full patched Nerd Font replacement as the main font

That is heavier than necessary. The practical issue is missing icon/symbol glyphs, so a focused fallback font is the smaller and cleaner solution.

Proposed implementation workstreams

1. Add multi-font profile metadata without overcomplicating custom profiles

Files / symbols

  • src/renderer/types.ts
  • src/renderer/profiles.ts
  • any renderer exports that re-export RenderProfileConfig

Plan

  • Extend RenderProfileConfig with an optional ordered bundled-font descriptor list, e.g. fontAssets or similar.
  • Keep fontFamily as the final CSS stack string used by the browser/renderer.
  • Use strict descriptors for each bundled face, such as:
    • family
    • assetIdentity
    • route
    • weight
    • style
  • Preserve a minimal compatibility path: custom profiles that specify only fontFamily and no bundled font descriptors should still validate and behave as best-effort system-font profiles.
  • Update hashProfile() so the ordered bundled font descriptors participate in the canonical hash.

Acceptance criteria

  • Built-in profiles can represent more than one bundled face deterministically.
  • Changing the bundled font list or ordering changes the render-profile hash.
  • Custom profiles without bundled assets still validate and boot.

2. Refactor the single bundled font into a small registry

Files / symbols

  • src/renderer/bundledFont.ts
  • src/renderer/ghosttyWeb/assets/*

Plan

  • Replace the single-font exports with a small registry that can expose multiple bundled font assets.
  • Keep startup assertions and SHA-256 identity checks for every bundled asset.
  • Add one narrow fallback asset for Nerd Font coverage, preferably a mono/symbols-focused file rather than a full family.
  • Keep route names explicit and local to the harness asset server.

Acceptance criteria

  • Renderer code can resolve both the primary face and fallback face from one registry.
  • Every bundled asset is validated at startup and addressable by a stable local route.
  • Repo size growth stays intentional and limited to the chosen fallback asset.

3. Update Ghostty Web asset serving and font preloading

Files / symbols

  • src/renderer/ghosttyWeb/backend.ts
  • src/renderer/ghosttyWeb/harness.html

Plan

  • Update backend asset serving to expose all bundled font routes declared by a profile.
  • Replace the current single-font loadBundledFont() path with a loadBundledFonts() loop that loads every declared bundled face before init().
  • Apply the profile’s final CSS stack to the document and terminal mount after bundled fonts are registered.
  • Add fail-fast checks so the harness errors clearly when a declared bundled font descriptor cannot be resolved.

Acceptance criteria

  • Built-in reference profiles boot successfully with multiple bundled fonts.
  • Glyphs provided by the fallback font render without tofu in screenshots.
  • The renderer remains self-contained and does not need network or host-font discovery for built-in reference profiles.

4. Update built-in reference profiles to use a primary + fallback stack

Files / symbols

  • src/renderer/profiles.ts

Plan

  • Update reference-dark and reference-light to use a CSS stack like:
    • primary JetBrains Mono
    • fallback Nerd Font symbols face
    • terminal-safe generic fallback (monospace)
  • Keep the built-in profiles as the documented reproducible path.
  • Leave custom profiles free to be less deterministic, but do not blur that line in the built-in defaults.

Acceptance criteria

  • Both built-in profiles include bundled fallback font metadata.
  • Built-in screenshots and WebM exports use the new deterministic stack.
  • The built-in profile hash changes intentionally and consistently after the font change.

5. Add targeted tests at the right layers

Files / symbols

  • test/unit/renderer/profiles.test.ts
  • test/unit/renderer/ghosttyWebBackend.test.ts
  • test/integration/renderer-backend.test.ts
  • test/e2e/unicode-grid.test.ts or a new focused glyph fixture/test
  • test/fixtures/apps/*

Plan

  • Unit: validate new bundled-font profile schema, strict descriptor validation, clone behavior, and hash determinism.
  • Unit/backend: validate that all declared bundled font assets are served with correct routes/content-types and that unresolved descriptors fail clearly.
  • Integration: verify the real Playwright-backed renderer boots and screenshots correctly with the multi-font built-in profile.
  • E2E: add a glyph-heavy fixture (or extend unicode-grid) that prints representative Nerd Font icons used in the observed LazyVim/Claude Code workflow.
  • Prefer one focused glyph test over broad snapshot churn.

Acceptance criteria

  • Hashing/profile tests cover the new multi-font metadata.
  • Backend tests prove all bundled font assets are served.
  • Integration/E2E coverage demonstrates that the representative glyphs no longer render as empty boxes.

6. Dogfooding and reviewer proof bundle

Files / symbols

  • dogfood/ new scenario bundle
  • relevant CLI commands via src/cli/main.ts

Plan

  • Create a focused dogfood scenario for glyph rendering in an isolated absolute AGENT_TERMINAL_HOME.
  • Use a reproducible fixture that prints representative Nerd Font glyphs seen in Neovim/LazyVim UI (menu icons, statusline icons, plugin markers, etc.).
  • Capture reviewer-facing proof artifacts:
    1. JSON command envelopes for create/wait/snapshot/screenshot/export
    2. at least one text snapshot proving terminal content is unchanged semantically
    3. at least two PNG screenshots showing the fixed visual rendering
    4. one WebM recording of the replay/export path
    5. optionally one asciicast for text-side inspection
  • Attach screenshots in the final review and keep the WebM in the dogfood bundle for playback.

Dogfooding acceptance criteria

  • Reviewer can see that previously missing glyphs render correctly in attached screenshots.
  • Reviewer can watch a WebM proving the fix survives replay/export, not just live rendering.
  • Text snapshots remain semantically stable while visual artifacts improve.
  • No new renderer failures appear in the dogfood session.

Validation / quality gates

Required automated validation

  • mise run test (or the repo-equivalent split commands if a narrower pass is needed during development)
  • targeted renderer tests if the full suite is too slow during iteration:
    • unit renderer profile/backend tests
    • integration renderer backend tests
    • focused e2e glyph/screenshot test
  • mise run build

Required manual validation

  • Re-run the Neovim/LazyVim-style glyph scenario that originally produced tofu.
  • Confirm the glyphs render correctly in:
    • a live screenshot,
    • an exported WebM,
    • and (if used) the relevant e2e fixture screenshot artifact.
  • Confirm screenshots still look stable across repeated captures with the same profile.

Risks and decision points

Asset choice / licensing

  • Need to choose a small fallback asset that covers the needed glyph ranges without bloating the repo.
  • The exact asset should be decided before coding, but the implementation shape should not depend on a specific font vendor beyond standard metadata.

Visual metrics drift

  • Even a symbols-only fallback can slightly affect alignment or spacing in icon-heavy UIs.
  • The focused glyph fixture and LazyVim dogfood pass should explicitly check for width/alignment regressions, not just absence of tofu.

Hash churn

  • Built-in render-profile hashes will change after this work.
  • That is expected and desirable because the visual rendering contract changes.

Scope control

  • Keep this change about the reference renderer.
  • Do not expand into general font management or cross-platform host font installation work in the same implementation.

Out of scope for the first implementation

  • Installing Nerd Fonts on the host/CI image.
  • Browser-permission-driven local font access.
  • User-configurable font download/management commands.
  • Reworking session/event-log/replay semantics.

Final recommendation for execution

If we implement this, the safest and most maintainable path is:

  1. introduce optional multi-font bundled profile metadata,
  2. bundle one small Nerd Font fallback asset,
  3. load all declared bundled fonts before harness init,
  4. update built-in reference profiles to use the explicit stack,
  5. prove it with focused renderer tests and a dogfood proof bundle.

That gives us better screenshots without sacrificing determinism, which is the key tradeoff I care about most for long-term maintenance in this codebase.


Generated with mux • Model: openai:gpt-5.4 • Thinking: high

@ThomasK33 ThomasK33 force-pushed the agent-terminal-v12b-1 branch from 448c812 to 299e943 Compare March 26, 2026 06:34
@ThomasK33 ThomasK33 merged commit e00b85c into main Mar 26, 2026
2 checks passed
@ThomasK33 ThomasK33 deleted the agent-terminal-v12b-1 branch March 26, 2026 07:04
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