Skip to content

feat(web): searchable font dropdowns for the three font roles (PR 2/2 for #2921) - #6

Draft
btli wants to merge 6 commits into
polly/font-loader-catalogfrom
polly/font-dropdown-settings
Draft

feat(web): searchable font dropdowns for the three font roles (PR 2/2 for #2921)#6
btli wants to merge 6 commits into
polly/font-loader-catalogfrom
polly/font-dropdown-settings

Conversation

@btli

@btli btli commented Jul 20, 2026

Copy link
Copy Markdown
Owner

Stacked PR (2 of 2). Base branch is polly/font-loader-catalog (PR omnigent-ai#2923), not main. Review this on top of PR 1's catalog + loader. Merge omnigent-ai#2923 first.

Related issue

Closes omnigent-ai#2921

Summary

The Settings → Appearance font-family controls were free-text inputs that only set a font name — nothing loaded the font, so picking a family the OS didn't have installed silently fell back. Building on PR 1's curated catalog + on-demand webfont loader, this PR replaces those inputs with searchable dropdowns, one per interface font role:

  • Font family (sans / UI chrome) → catalog sans category, applied via --ui-font-family.
  • Fixed width font (monospace chrome — file paths, hashes, inline code, log rows) → catalog fixedWidth category, applied via a new --ui-mono-font-family variable the font-mono utility reads. This role didn't have its own persisted preference before, so a parallel fixedWidthFontPreferences module was added mirroring the existing ui/code pattern.
  • Code font family (editor + terminal, incl. Nerd Fonts) → catalog code category, applied via the existing code-font pub/sub.

Each combobox previews every option in its own face (the loader fetches catalog faces on open), loads the selected webfont so it renders without a local install, and keeps backward compatibility: a previously-typed custom family that isn't in the catalog is still honored — shown as its own selectable row, plus a free-text "Use "…"" escape hatch in the search. All existing localStorage keys are unchanged; the new fixed-width preference restores on boot in main.tsx / embed.tsx.

Built with the repo's existing Popover + cmdk Command primitives (no new UI deps).

Test Plan

All gates run from web/ on Node 20 (Node 26 breaks jsdom localStorage):

  • npm run type-check → clean
  • npm run lint (oxlint) → no new findings in changed files (pre-existing errors in untouched files only)
  • npm run test (vitest) → 4286 passed / 3 expected-fail / 2 skipped
  • npm run build → built OK

New / updated unit tests:

  • FontFamilyCombobox.test.tsx — renders every font in a category (and only that category), selection calls onChange + triggers the loader, default row clears, out-of-catalog custom row surfaces, free-text escape hatch applies a typed family.
  • fixedWidthFontPreferences.test.ts — read/write/normalize/apply round-trips under the dedicated key + --ui-mono-font-family.
  • SettingsPage.test.tsx — rewrote the old free-text-input tests for the three comboboxes: default → catalog select persists + applies to the right CSS var, "default" row restores, Nerd Fonts listed, custom sans family honored, free-text fallback.

Demo

Three font controls in Appearance (Font family / Fixed width font / Code font family):

Three font controls

Searchable code-font dropdown — each option previewed in its own loaded face, incl. Nerd Fonts:

Code font dropdown open

Custom free-text fallback — typing a family the catalog doesn't know offers a "Use "…"" row:

Custom font fallback

Type of change

  • Bug fix
  • Feature
  • UI / frontend change
  • Refactor / chore
  • Docs
  • Test / CI
  • Breaking change

Test coverage

  • Unit tests added / updated
  • Integration tests added / updated
  • E2E tests added / updated
  • Manual verification completed
  • Existing tests cover this change
  • Not applicable

Coverage notes

Manual verification: ran the vite dev server and drove the Appearance page in a browser — confirmed all three dropdowns render with correct default labels, the code-font dropdown lists IDE + Nerd fonts previewed in-face, and typing a non-catalog family ("Comic Code") surfaces the "Use "…"" fallback row (screenshots above). Automated coverage is via the vitest suites listed in the Test Plan.

Changelog

Appearance settings now offer searchable font pickers for the UI, fixed-width, and code fonts — selected fonts load on demand so IDE and Nerd Fonts render without a local install

@github-actions github-actions Bot added the size/XL Pull request size: XL label Jul 20, 2026
@btli
btli force-pushed the polly/font-dropdown-settings branch from b3a4770 to b3d8f4e Compare July 21, 2026 16:45
btli and others added 5 commits July 21, 2026 09:50
The Settings font controls only set a font NAME; nothing loaded the
family, so picking a Nerd Font or any non-installed IDE font silently
rendered the fallback stack. Add the loader half of the fix (the
dropdown is PR 2):

- fontCatalog.ts: a curated registry in three roles — sans (UI/chrome),
  fixedWidth (mono UI), and code (editor/terminal) — each entry carrying
  a stable id, label, CSS family, category, and delivery metadata
  (bundled | google-css2 | self-hosted).
- webFontLoader.ts: a deduplicated loader that injects a keyless Google
  CSS2 <link> or explicit @font-face rules and awaits readiness via
  document.fonts.load(). No-ops for bundled/system families; never
  injects the same stylesheet twice; concurrent loads share one promise.
- Wire the loader into the preference-application path: applyUiFontFamily
  and writeCodeFontFamily kick a catalog load; after the code font lands,
  re-emit so Monaco re-measures (remeasureFonts) and xterm refits. Restore
  on boot from main.tsx and embed.tsx.

Self-hosted fonts (Cascadia Code via Fontsource CDN, four Nerd Font Mono
variants via a pinned ryanoasis/nerd-fonts tag) are lazy-loaded on demand
— not eagerly imported — so the bundle stays lean.

The free-text inputs still work unchanged: a typed name that matches a
catalog family now loads; anything else is left to the OS as before.

Co-authored-by: omnigent <noreply@omnigent.ai>
Signed-off-by: Bryan Li <bryan.li@gmail.com>
Address cross-vendor review of the font loader/catalog:

1. Google readiness raced the stylesheet load: document.fonts.load() ran
   before the injected <link>'s @font-face rules were registered, so it
   could resolve empty and fire a premature remeasure. injectStylesheet
   now awaits the link's `load` event (rejects on `error`, removing the
   dead node) BEFORE querying document.fonts; an empty/failed FontFaceSet
   result is treated as not-ready (false), not success.

2. The 8s timeout reported success and left a dangling timer. Removed it:
   readiness is now signalled ONLY by genuine document.fonts.load
   settlement (itself bounded by spec), and neither consumer blocks on it
   (UI fires-and-forgets via font-display: swap; the code font chains a
   .then that re-emits only on a true result). So a slow font remeasures
   exactly when it truly arrives and a fast one leaves no timer.

3. Dedup was keyed by catalog id, so the two IBM Plex Mono entries (same
   URL, different ids) injected twice and returned different promises.
   Added fontLoadKey(entry) — the canonical resource URL / @font-face src
   — and keyed both the in-flight map and the DOM guard by it. A failed
   load evicts its cache entry so a later attempt retries.

4. getFontByFamily always returned the fixedWidth IBM Plex Mono entry.
   Added getFontsByFamily (all matches) and an optional category arg to
   getFontByFamily so a per-role lookup resolves the right entry; the
   code-font path now passes "code". Single-arg behavior is unchanged.

Tests: readiness ordering (no fonts query until <link> load; empty/errored
= not ready), cross-id IBM Plex Mono dedup (one injection, shared promise),
retry-after-failure, fontLoadKey resource identity, and category-aware
family resolution.

Co-authored-by: omnigent <noreply@omnigent.ai>
Signed-off-by: Bryan Li <bryan.li@gmail.com>
Round-2 regression from the dedup/retry fix: on a fontsReady() failure the
in-flight promise was evicted but the injected <link>/<style> stayed in the
DOM, so the injection guard matched the dead node and a retry never
re-fetched. For self-hosted faces the errored FontFace also lingered in
document.fonts, so document.fonts.load kept returning the rejected face.

- fontsReady now returns a three-way outcome: `ready` | `failed` |
  `unsupported`. `failed` (rejected load OR empty/not-ready result) is
  retryable; `unsupported` (no document.fonts API) is NOT, so we don't
  thrash reinjecting when the API is simply missing.
- On `failed`, removeInjected(key) drops this attempt's node(s) BEFORE
  evicting the cache entry, so a later call reinjects and re-fetches (and,
  for self-hosted, mints a fresh FontFace). On `unsupported` the node is
  left in place (CSS may still paint via font-display) and only the cache
  entry is evicted.
- Track injected nodes in a key→nodes Map instead of a quoted attribute
  selector: the key is a URL (`:?&;@`), and such attribute selectors match
  unreliably across engines (jsdom fails them), which would have silently
  broken the dedup/removal guards.

Tests: fonts.load rejection and empty/not-ready results both remove the
node and let a retry reinject + re-query (google-css2 and self-hosted),
genuine success after a prior failure, and the unsupported-API case
leaving the node untouched.

Co-authored-by: omnigent <noreply@omnigent.ai>
Signed-off-by: Bryan Li <bryan.li@gmail.com>
PR-1 review follow-ups on the font loader/catalog work, shaping the
shared surface PR 2's font dropdowns will consume:

- Extract the CSS-var-backed font-family core into
  cssFontFamilyPreference.ts (normalizeFontFamily, readStoredFontFamily,
  writeStoredFontFamily, createCssFontFamilyPreference threading a loader
  category) and refactor uiFontPreferences onto it. codeFontPreferences
  keeps its specialized remeasure/refit pub/sub path.
- Add a googleFont() catalog helper and collapse the repeated Google
  CSS2 entries; family names unchanged.
- Export UI_FONT_FAMILY_FALLBACK so callers/tests stop repeating the
  var(--font-sans) literal (mirrors CODE_FONT_FAMILY_FALLBACK).
- resetFontLoaderForTests now removes the DOM nodes it injected.
- Centralize boot restore in restoreFontPreferences(), called from
  main.tsx and embed.tsx.

Co-authored-by: Isaac
Signed-off-by: Bryan Li <bryan.li@gmail.com>
restoreFontPreferences applied the UI and code font roles on boot but
omitted the fixed-width (monospace chrome) role, so a saved fixed-width
family was neither applied nor fetched until the next Settings change.

Add the CSS-variable-backed fixed-width preference module (shared
factory: --ui-mono-font-family, omnigent:fixed-width-font-family, the
fixedWidth catalog category) and its .font-mono rule, then wire it into
restoreFontPreferences alongside UI and code so all three roles restore
on boot. Extend the restore test to assert the fixed-width family is
read, its CSS var applied, and its webfont load kicked off.

Co-authored-by: Isaac
Signed-off-by: Bryan Li <bryan.li@gmail.com>
@btli
btli force-pushed the polly/font-loader-catalog branch from 9544303 to d3dac7f Compare July 21, 2026 17:32
The Appearance font-family controls were free-text inputs that only set a
family name; nothing loaded the font, so picking one the OS lacked silently
fell back. Building on the PR-1 catalog + webfont loader, replace them with
searchable comboboxes — one per interface font role:

- Font family (sans / UI chrome) → catalog `sans`, --ui-font-family.
- Fixed width (monospace chrome) → catalog `fixedWidth`, new
  --ui-mono-font-family read by the `font-mono` utility.
- Code font (editor + terminal) → catalog `code` (incl. Nerd Fonts),
  code-font pub/sub.

Each control previews every option in its own face, loads the webfont on
selection so it renders without a local install, and keeps a free-text
escape hatch plus an out-of-catalog custom row so a previously-typed family
stays honored. localStorage keys are unchanged; a parallel
fixedWidthFontPreferences module mirrors the ui/code pattern and restores on
boot in main.tsx / embed.tsx.

Co-authored-by: omnigent <noreply@omnigent.ai>
Signed-off-by: Bryan Li <bryan.li@gmail.com>
@btli
btli force-pushed the polly/font-dropdown-settings branch from b3d8f4e to 1aa568b Compare July 21, 2026 17:46
btli pushed a commit that referenced this pull request Jul 23, 2026
…mnigent-ai#2997)

* perf(scheduled-tasks): fix unbounded queries in scheduled-task store

Three unbounded DB reads could cause excessive load as the task table grows:

- Issue #5: `list()` fetched all workspace tasks then filtered in Python.
  Add `owner_user_id` parameter to `list()` (ABC + SQLAlchemy) so the
  WHERE clause uses the existing `ix_scheduled_tasks_owner_user_id` index.
  Update the route to pass `owner_id` directly instead of post-filtering.

- Issue #6: `list_runs()` returned every historical run for a task with no
  LIMIT. Add a `limit: int = 100` keyword parameter (ABC + SQLAlchemy) and
  apply `.limit(limit)` to the query.

- Issue #10: `list_active_all_workspaces()` had no cap on rows returned at
  scheduler boot. Apply a hard `.limit(10_000)` to prevent unbounded load.

Signed-off-by: Tomu Hirata <tomu.hirata@gmail.com>

* fix(scheduled-tasks): paginate list_runs and arm all tasks at boot instead of silent caps

Problem A: GET /scheduled-tasks/{id}/runs silently truncated run history at
100 rows with no pagination. Replace the bare limit with cursor pagination:
list_runs now returns (runs, next_cursor) and takes after_id; the endpoint
accepts limit (1-1000) and after, and returns {runs, next_cursor}. Run ids are
random UUIDs, so the keyset resolves the cursor row's scheduled_at and compares
the full (scheduled_at, id) tuple under the DESC order — an id-only cursor
would skip/repeat rows on scheduled_at ties.

Problem B: scheduler boot (list_active_all_workspaces) capped at 10k rows, so
tasks beyond the cap silently never armed. Chose the complete-pagination
approach over a loud-warning cap: the method now keyset-pages internally by
(workspace_id, created_at, id) in 10k batches and returns ALL active tasks, so
every task is armed at boot. Full pagination is strictly correct (no task ever
left un-armed) and the boot scan is a rare, one-shot cost.

Signed-off-by: Tomu Hirata <tomu.hirata@gmail.com>

---------

Signed-off-by: Tomu Hirata <tomu.hirata@gmail.com>
@btli
btli force-pushed the polly/font-loader-catalog branch 2 times, most recently from 8fa66de to 662ac11 Compare August 21, 2026 05:40
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size/XL Pull request size: XL

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant