feat(web): searchable font dropdowns for the three font roles (PR 2/2 for #2921) - #6
Draft
btli wants to merge 6 commits into
Draft
feat(web): searchable font dropdowns for the three font roles (PR 2/2 for #2921)#6btli wants to merge 6 commits into
btli wants to merge 6 commits into
Conversation
btli
force-pushed
the
polly/font-dropdown-settings
branch
from
July 21, 2026 16:45
b3a4770 to
b3d8f4e
Compare
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
force-pushed
the
polly/font-loader-catalog
branch
from
July 21, 2026 17:32
9544303 to
d3dac7f
Compare
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
force-pushed
the
polly/font-dropdown-settings
branch
from
July 21, 2026 17:46
b3d8f4e to
1aa568b
Compare
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
force-pushed
the
polly/font-loader-catalog
branch
2 times, most recently
from
August 21, 2026 05:40
8fa66de to
662ac11
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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:
sanscategory, applied via--ui-font-family.fixedWidthcategory, applied via a new--ui-mono-font-familyvariable thefont-monoutility reads. This role didn't have its own persisted preference before, so a parallelfixedWidthFontPreferencesmodule was added mirroring the existing ui/code pattern.codecategory, 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
localStoragekeys are unchanged; the new fixed-width preference restores on boot inmain.tsx/embed.tsx.Built with the repo's existing
Popover+cmdkCommandprimitives (no new UI deps).Test Plan
All gates run from
web/on Node 20 (Node 26 breaks jsdom localStorage):npm run type-check→ cleannpm 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 skippednpm run build→ built OKNew / updated unit tests:
FontFamilyCombobox.test.tsx— renders every font in a category (and only that category), selection callsonChange+ 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):
Searchable code-font dropdown — each option previewed in its own loaded face, incl. Nerd Fonts:
Custom free-text fallback — typing a family the catalog doesn't know offers a "Use "…"" row:
Type of change
Test coverage
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