Skip to content

fix(desktop): recover terminal from non-monospace font crash (#3513)#3554

Merged
Kitenite merged 5 commits intomainfrom
fix/3513-font-crash
Apr 18, 2026
Merged

fix(desktop): recover terminal from non-monospace font crash (#3513)#3554
Kitenite merged 5 commits intomainfrom
fix/3513-font-crash

Conversation

@Kitenite
Copy link
Copy Markdown
Collaborator

@Kitenite Kitenite commented Apr 18, 2026

Summary

Fixes #3513. Setting the terminal font to a proportional family like "Inter" blanked the main window on next launch — xterm.js can't lay out cells against a non-monospace font, and because the bad value is persisted to SQLite the crash repeated on every relaunch with no way back into settings (reporter had to hand-edit ~/.superset/local.db to recover).

Two changes:

  • Sanitize on read (sanitizeTerminalFontFamily in apps/desktop/src/renderer/lib/terminal/appearance/index.ts). When the primary family of the stored CSS value isn't monospace (decided by canvas measurement of iiiiii vs MMMMMM), fall back to DEFAULT_TERMINAL_FONT_FAMILY. Wired into both the v1 (Terminal.tsx) and v2 (useTerminalAppearance) apply paths. This is the recovery path for users already stuck.
  • Restrict the picker (FontFamilyCombobox.tsx). For variant === "terminal", hide the "Other" (proportional) group, the CommandEmpty "Use ''" escape hatch, and the free-form "Custom" group — so new selections can only come from the Nerd Fonts / Monospace groups.

Test plan

  • bun test src/renderer/lib/terminal/appearance/appearance.test.ts — 6 new unit tests cover null/empty, generic monospace pass-through, monospace font, proportional font fallback (quoted + bare), and canvas-unavailable path.
  • bun run typecheck clean.
  • bun run lint clean.
  • Manual: set terminal_font_family = 'Inter' in ~/.superset/local.db before launch — app should start normally and use the default terminal font.
  • Manual: open Settings → Appearance, confirm the Terminal Font picker no longer lists "Other" fonts (e.g. Inter, Arial) and no longer offers "Use ''" / Custom entry for free-form input.
  • Manual: Editor Font picker still shows "Other" + custom entry (regression guard — variant-scoped change).

Summary by cubic

Recover from desktop launch crash caused by non‑monospace terminal fonts and prevent it from happening again. Fixes #3513.

  • Bug Fixes

    • Sanitize the stored terminal font on read: validate the actual CSS primary family (not the first concrete entry); if it isn’t monospace or the stack uses proportional generics, fall back to DEFAULT_TERMINAL_FONT_FAMILY. Append ", monospace" when missing so the OS monospace is used if the chosen font isn’t installed. Applied in both v1 and v2 terminal paths.
    • Restrict the Terminal Font picker: hide “Other” and custom free‑form input for the terminal variant to enforce monospace fonts.
  • Refactors

    • Replace the box‑drawing terminal preview with a simple shell session to show column alignment clearly.
    • Show the “Nerd Fonts” group in the editor font picker as well.

Written for commit 8404892. Summary will update on new commits.

Summary by CodeRabbit

  • Bug Fixes

    • Terminal font input is now validated: invalid or proportional primary families fall back to the default and emit warnings; monospace stacks are preserved and a monospace fallback is appended when needed.
  • User Interface

    • Terminal font selector hides custom entries and the "Other" group for terminal variant; Nerd Fonts remain visible.
    • Terminal font preview text updated for a clearer example transcript.
  • Tests

    • Added tests covering validation, monospace detection, measurement behavior, and error cases.

Setting the terminal font to a proportional family like "Inter" blanked
the app on next launch — the bad value persisted in SQLite and xterm
couldn't lay out cells on reload, leaving no way back into settings.

- Sanitize the stored family on read: if the primary family isn't
  monospace (per canvas measurement), fall back to the default terminal
  font so a poisoned DB value can never blank the renderer.
- Hide the "Other" group and custom free-form entry in the terminal
  font picker so new selections are restricted to monospace candidates.
@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Apr 18, 2026

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 0bee69e7-a175-470e-80b5-88851d30e2e3

📥 Commits

Reviewing files that changed from the base of the PR and between 5b9936d and 8404892.

📒 Files selected for processing (2)
  • apps/desktop/src/renderer/lib/terminal/appearance/appearance.test.ts
  • apps/desktop/src/renderer/lib/terminal/appearance/index.ts
✅ Files skipped from review due to trivial changes (1)
  • apps/desktop/src/renderer/lib/terminal/appearance/appearance.test.ts
🚧 Files skipped from review as they are similar to previous changes (1)
  • apps/desktop/src/renderer/lib/terminal/appearance/index.ts

📝 Walkthrough

Walkthrough

Adds a sanitization utility, sanitizeTerminalFontFamily, that normalizes and validates persisted terminal CSS font-family values (including canvas-based monospace detection and caching), wires it into terminal rendering and settings UI, and adds tests and preview text updates.

Changes

Cohort / File(s) Summary
Font Sanitization Implementation
apps/desktop/src/renderer/lib/terminal/appearance/index.ts
New exported sanitizeTerminalFontFamily(cssValue); parses font-family lists, performs canvas-based monospace detection with caching and permissive fallback, normalizes stacks, and warns when falling back to the default.
Font Sanitization Tests
apps/desktop/src/renderer/lib/terminal/appearance/appearance.test.ts
New Bun test suite exercising empty/null input, generic monospace acceptance, non-monospace generic fallback, mixed stacks, canvas-driven detection (equal vs proportional widths via stubCanvas), measureText error path, and cleanup of the canvas stub.
Terminal Appearance Integration
apps/desktop/src/renderer/routes/.../useTerminalAppearance.ts, apps/desktop/src/renderer/screens/.../Terminal/Terminal.tsx
Replaced prior direct fallback to DEFAULT_TERMINAL_FONT_FAMILY with calls to sanitizeTerminalFontFamily when deriving/applying terminal fontFamily; removed unused default import.
Settings UI: Font Selection
apps/desktop/src/renderer/.../FontFamilyCombobox.tsx
Disabled freeform/custom font entry for the terminal variant (allowCustomEntry gated), always show “Nerd Fonts”, and hide the “Other” group when variant is terminal.
Font Preview Text Update
apps/desktop/src/renderer/.../FontPreview/FontPreview.tsx
Updated TERMINAL_PREVIEW sample text for the terminal variant to a simplified terminal transcript; rendering logic unchanged.

Sequence Diagram(s)

sequenceDiagram
    participant UI as Settings UI
    participant Sanitizer as sanitizeTerminalFontFamily
    participant Doc as document / Canvas
    participant Cache as MonospaceCache
    participant Terminal as Terminal Renderer

    UI->>Sanitizer: provide font-family string
    Sanitizer->>Cache: check cached result (by lowercased name)
    alt cache hit
        Cache-->>Sanitizer: cached verdict
    else cache miss
        Sanitizer->>Doc: create canvas & set font
        Doc-->>Sanitizer: measureText widths / or throw
        Sanitizer->>Cache: store detection result
    end
    Sanitizer-->>UI: return normalized font-family (with monospace fallback if needed)
    UI->>Terminal: apply sanitized fontFamily
    Terminal-->>UI: render/update terminal
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~22 minutes

Poem

🐰 I sniffed the stacks and tapped the keys,

I measured lines with tiny pleas,
When spacing broke, I tucked a tail — monospace snug and neat,
Now terminals breathe easy, paws on repeat.

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 71.43% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title accurately summarizes the main change: recovery from terminal font crash caused by non-monospace fonts.
Description check ✅ Passed The description comprehensively covers the bug fix, implementation details, test plan, and links to the fixed issue (#3513).
Linked Issues check ✅ Passed The PR fully addresses issue #3513 requirements: sanitizes stored terminal font values, prevents non-monospace fonts through UI restrictions, appends monospace fallback, and provides recovery for affected users.
Out of Scope Changes check ✅ Passed All changes are directly related to issue #3513: font sanitization logic, picker UI restrictions, font preview updates for clarity, and comprehensive test coverage.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/3513-font-crash

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@greptile-apps
Copy link
Copy Markdown

greptile-apps Bot commented Apr 18, 2026

Greptile Summary

This PR fixes a startup crash (#3513) where persisting a proportional font (e.g. "Inter") as the terminal font family caused xterm.js to fail cell layout, blanking the main window on every subsequent launch with no UI escape route.

Two complementary defences are introduced:

  • Read-time sanitization (sanitizeTerminalFontFamily in appearance/index.ts): uses canvas measureText to heuristically confirm the primary CSS family is monospace before applying it; falls back to DEFAULT_TERMINAL_FONT_FAMILY for proportional fonts. Wired into both the v1 (Terminal.tsx) and v2 (useTerminalAppearance) code paths, and backed by six targeted unit tests.
  • Write-time restriction (FontFamilyCombobox.tsx): for variant === \"terminal\", the "Other" (proportional) font group and all free-form custom entry escape hatches are hidden, preventing new poisoned values from entering the DB.

Key observations:

  • The canvas monospace heuristic (|narrow − wide| < 1px) is sound for named fonts but an all-generic proportional value such as \"cursive\" bypasses the check because parsePrimaryFontFamily returns null for generic-only lists and the sanitizer passes the raw value through — same root class of bug.
  • FontFamilyCombobox gates the "Nerd Fonts" group exclusively on variant === \"terminal\", which also hides Nerd Fonts from the editor picker; the PR description only mentions restricting the terminal "Other" group, so this scope may be unintentional.
  • Test isolation is handled carefully: the canvas-throws test uses a unique font name (\"UnmeasurableFont-ABC-123\") to avoid the module-level monospaceCheckCache masking the error path.

Confidence Score: 4/5

Safe to merge — the crash recovery path is solid and well-tested; two minor gaps (generic proportional bypass, Nerd Fonts in editor) are worth addressing but don't affect the primary bug fix.

The core fix — sanitizing persisted terminal font families and restricting the picker — is correct, complete, and covered by targeted unit tests. The two P2 issues (all-generic proportional bypass and potential Nerd Fonts regression in the editor) are edge cases that don't impact the primary crash scenario reported in #3513. Both could be addressed in a follow-up without blocking the merge.

appearance/index.ts (generic-family bypass in sanitizeTerminalFontFamily) and FontFamilyCombobox.tsx (Nerd Fonts gating scope).

Important Files Changed

Filename Overview
apps/desktop/src/renderer/lib/terminal/appearance/index.ts Core sanitization logic: adds sanitizeTerminalFontFamily with canvas-based monospace detection and caching — but all-generic proportional families (cursive, fantasy) bypass the guard because parsePrimaryFontFamily returns null for generic-only lists.
apps/desktop/src/renderer/lib/terminal/appearance/appearance.test.ts Well-structured unit tests covering null/empty, generic monospace pass-through, monospace font, proportional font fallback (quoted + bare), and canvas-unavailable path; uses unique font name to avoid module-level cache poisoning.
apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/hooks/usePaneRegistry/components/TerminalPane/hooks/useTerminalAppearance/useTerminalAppearance.ts v2 appearance hook correctly wraps font family lookup with sanitizeTerminalFontFamily before passing to xterm.
apps/desktop/src/renderer/routes/_authenticated/settings/appearance/components/AppearanceSettings/components/FontSettingSection/components/FontFamilyCombobox/FontFamilyCombobox.tsx Adds variant prop to gate off the "Other" font group and free-form custom entry for terminal pickers; Nerd Fonts are also gated to terminal-only which may unintentionally hide them from the editor picker.
apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/TabsContent/Terminal/Terminal.tsx v1 terminal correctly applies sanitizeTerminalFontFamily before passing font family to v1TerminalCache.updateAppearance.

Flowchart

%%{init: {'theme': 'neutral'}}%%
flowchart TD
    DB[(SQLite DB\nterminal_font_family)]
    DB --> |fontSettings query| SAN
    SAN["sanitizeTerminalFontFamily(cssValue)"]
    SAN --> NULL{null / empty?}
    NULL -- yes --> DEF[Return DEFAULT_TERMINAL_FONT_FAMILY]
    NULL -- no --> PARSE["parsePrimaryFontFamily(cssValue)"]
    PARSE --> PRIMQ{primary found?}
    PRIMQ -- no, all-generic --> RAW["Return cssValue as-is\n⚠️ cursive/fantasy not caught"]
    PRIMQ -- yes --> MONO["isFontFamilyMonospace(primary)"]
    MONO --> CACHE{In cache?}
    CACHE -- yes --> HIT[Return cached result]
    CACHE -- no --> CANVAS[Canvas measureText\niiiiii vs MMMMMM]
    CANVAS --> MEASURE{width diff < 1px?}
    MEASURE -- yes --> PASS[Return cssValue ✓]
    MEASURE -- no --> WARN[console.warn + Return DEFAULT]
    CANVAS --> EX{Exception or no document?}
    EX -- yes --> TRUST[Return true / pass through]
    PASS --> V1[Terminal.tsx v1]
    DEF --> V1
    WARN --> V1
    PASS --> V2[useTerminalAppearance v2]
    DEF --> V2
    WARN --> V2
Loading
Prompt To Fix All With AI
This is a comment left during a code review.
Path: apps/desktop/src/renderer/lib/terminal/appearance/index.ts
Line: 143-145

Comment:
**Generic proportional families bypass the sanitizer**

When the stored CSS value consists entirely of generic families (e.g. `"cursive"`, `"fantasy"`, or `"cursive, monospace"`), `parsePrimaryFontFamily` returns `null` (it skips generics when finding the "primary" family). The sanitizer then returns the raw value as-is, even though `cursive` and `fantasy` are proportional and would cause the same xterm crash the PR is fixing.

```ts
const primary = parsePrimaryFontFamily(cssValue);
if (!primary) return cssValue;  // passes through "cursive", "fantasy", etc.
```

A straightforward fix is to check whether the raw value is a known safe generic before passing it through:

```
const SAFE_GENERIC_MONOSPACE = new Set(["monospace", "ui-monospace"]);

const primary = parsePrimaryFontFamily(cssValue);
if (primary === null) {
  // All-generic list — only safe if every resolved family is monospace-generic
  const allSafe = cssValue
    .split(",")
    .map((s) => s.trim().toLowerCase())
    .every((f) => SAFE_GENERIC_MONOSPACE.has(f));
  return allSafe ? cssValue : DEFAULT_TERMINAL_FONT_FAMILY;
}
```

While a user storing `"cursive"` by hand is unlikely once the picker is restricted, the sanitizer's stated goal is to recover any poisoned DB value — proportional generic families are the same class of problem.

How can I resolve this? If you propose a fix, please make it concise.

---

This is a comment left during a code review.
Path: apps/desktop/src/renderer/routes/_authenticated/settings/appearance/components/AppearanceSettings/components/FontSettingSection/components/FontFamilyCombobox/FontFamilyCombobox.tsx
Line: 152-154

Comment:
**Nerd Fonts hidden from the editor picker — intentional?**

The conditional gates Nerd Fonts exclusively on `variant === "terminal"`:

```tsx
{variant === "terminal" && renderGroup("Nerd Fonts", nerdFonts)}
{renderGroup("Monospace", monoFonts)}
{variant !== "terminal" && renderGroup("Other", otherFonts)}
```

This means the editor font picker only shows Monospace + Other — Nerd Fonts never appear there. Nerd Fonts are monospace and widely used in editors for powerline/git-blame symbols. Was it a deliberate decision to exclude them from the editor, or were they meant to be available in both pickers (and only `"Other"` should be restricted to terminal)?

The PR description mentions hiding `"Other"` for terminal, but is silent on hiding Nerd Fonts from the editor.

How can I resolve this? If you propose a fix, please make it concise.

Reviews (1): Last reviewed commit: "fix(desktop): recover terminal from non-..." | Re-trigger Greptile

Comment on lines +143 to +145
const primary = parsePrimaryFontFamily(cssValue);
if (!primary) return cssValue;
if (isFontFamilyMonospace(primary)) return cssValue;
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Generic proportional families bypass the sanitizer

When the stored CSS value consists entirely of generic families (e.g. "cursive", "fantasy", or "cursive, monospace"), parsePrimaryFontFamily returns null (it skips generics when finding the "primary" family). The sanitizer then returns the raw value as-is, even though cursive and fantasy are proportional and would cause the same xterm crash the PR is fixing.

const primary = parsePrimaryFontFamily(cssValue);
if (!primary) return cssValue;  // passes through "cursive", "fantasy", etc.

A straightforward fix is to check whether the raw value is a known safe generic before passing it through:

const SAFE_GENERIC_MONOSPACE = new Set(["monospace", "ui-monospace"]);

const primary = parsePrimaryFontFamily(cssValue);
if (primary === null) {
  // All-generic list — only safe if every resolved family is monospace-generic
  const allSafe = cssValue
    .split(",")
    .map((s) => s.trim().toLowerCase())
    .every((f) => SAFE_GENERIC_MONOSPACE.has(f));
  return allSafe ? cssValue : DEFAULT_TERMINAL_FONT_FAMILY;
}

While a user storing "cursive" by hand is unlikely once the picker is restricted, the sanitizer's stated goal is to recover any poisoned DB value — proportional generic families are the same class of problem.

Prompt To Fix With AI
This is a comment left during a code review.
Path: apps/desktop/src/renderer/lib/terminal/appearance/index.ts
Line: 143-145

Comment:
**Generic proportional families bypass the sanitizer**

When the stored CSS value consists entirely of generic families (e.g. `"cursive"`, `"fantasy"`, or `"cursive, monospace"`), `parsePrimaryFontFamily` returns `null` (it skips generics when finding the "primary" family). The sanitizer then returns the raw value as-is, even though `cursive` and `fantasy` are proportional and would cause the same xterm crash the PR is fixing.

```ts
const primary = parsePrimaryFontFamily(cssValue);
if (!primary) return cssValue;  // passes through "cursive", "fantasy", etc.
```

A straightforward fix is to check whether the raw value is a known safe generic before passing it through:

```
const SAFE_GENERIC_MONOSPACE = new Set(["monospace", "ui-monospace"]);

const primary = parsePrimaryFontFamily(cssValue);
if (primary === null) {
  // All-generic list — only safe if every resolved family is monospace-generic
  const allSafe = cssValue
    .split(",")
    .map((s) => s.trim().toLowerCase())
    .every((f) => SAFE_GENERIC_MONOSPACE.has(f));
  return allSafe ? cssValue : DEFAULT_TERMINAL_FONT_FAMILY;
}
```

While a user storing `"cursive"` by hand is unlikely once the picker is restricted, the sanitizer's stated goal is to recover any poisoned DB value — proportional generic families are the same class of problem.

How can I resolve this? If you propose a fix, please make it concise.

Comment on lines +152 to +154
{variant === "terminal" && renderGroup("Nerd Fonts", nerdFonts)}
{renderGroup("Monospace", monoFonts)}
{renderGroup("Other", otherFonts)}
{variant !== "terminal" && renderGroup("Other", otherFonts)}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Nerd Fonts hidden from the editor picker — intentional?

The conditional gates Nerd Fonts exclusively on variant === "terminal":

{variant === "terminal" && renderGroup("Nerd Fonts", nerdFonts)}
{renderGroup("Monospace", monoFonts)}
{variant !== "terminal" && renderGroup("Other", otherFonts)}

This means the editor font picker only shows Monospace + Other — Nerd Fonts never appear there. Nerd Fonts are monospace and widely used in editors for powerline/git-blame symbols. Was it a deliberate decision to exclude them from the editor, or were they meant to be available in both pickers (and only "Other" should be restricted to terminal)?

The PR description mentions hiding "Other" for terminal, but is silent on hiding Nerd Fonts from the editor. Was it intentional to hide Nerd Fonts from the editor font picker? They are monospace and commonly used in code editors too — the PR description only mentions restricting the terminal picker's "Other" group.

Prompt To Fix With AI
This is a comment left during a code review.
Path: apps/desktop/src/renderer/routes/_authenticated/settings/appearance/components/AppearanceSettings/components/FontSettingSection/components/FontFamilyCombobox/FontFamilyCombobox.tsx
Line: 152-154

Comment:
**Nerd Fonts hidden from the editor picker — intentional?**

The conditional gates Nerd Fonts exclusively on `variant === "terminal"`:

```tsx
{variant === "terminal" && renderGroup("Nerd Fonts", nerdFonts)}
{renderGroup("Monospace", monoFonts)}
{variant !== "terminal" && renderGroup("Other", otherFonts)}
```

This means the editor font picker only shows Monospace + Other — Nerd Fonts never appear there. Nerd Fonts are monospace and widely used in editors for powerline/git-blame symbols. Was it a deliberate decision to exclude them from the editor, or were they meant to be available in both pickers (and only `"Other"` should be restricted to terminal)?

The PR description mentions hiding `"Other"` for terminal, but is silent on hiding Nerd Fonts from the editor. Was it intentional to hide Nerd Fonts from the editor font picker? They are monospace and commonly used in code editors too — the PR description only mentions restricting the terminal picker's "Other" group.

How can I resolve this? If you propose a fix, please make it concise.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Good catch, but out of scope for this PR — that gate was pre-existing, not introduced here. git blame on this line predates this branch; this PR only adds the variant !== "terminal" gate on the Other group below it.

Two reasons to defer: (1) editor users who want a specific Nerd Font can still type it in the search box and hit 'Use …' — the custom-entry path is intentionally left open for the editor variant. (2) The Superset editor font only applies to diff views and file editors, where Nerd Font icon glyphs aren't actually rendered, so the UX upside is modest. Happy to revisit in a follow-up.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@apps/desktop/src/renderer/lib/terminal/appearance/appearance.test.ts`:
- Around line 70-72: Add a regression test to ensure proportional generic
families are not trusted: in appearance.test.ts add a test (e.g. "falls back for
proportional generic families") that calls
sanitizeTerminalFontFamily("sans-serif") and sanitizeTerminalFontFamily("serif")
and asserts both results equal the terminal fallback value by comparing them to
sanitizeTerminalFontFamily("") (or whatever the existing function returns for
the default/fallback). Place the test alongside the existing "trusts all-generic
monospace values without canvas" test and reference the
sanitizeTerminalFontFamily function to locate the code under test.

In `@apps/desktop/src/renderer/lib/terminal/appearance/index.ts`:
- Around line 90-92: parsePrimaryFontFamily currently lets an all-generic stack
slip through via the caller fallback; instead explicitly detect and reject
stacks composed only of generic families: in parsePrimaryFontFamily (the
function using GENERIC_FONT_FAMILIES and families.find(...)), add a check like
if (families.length && families.every(f =>
GENERIC_FONT_FAMILIES.has(f.toLowerCase()))) return null so pure-generic inputs
(e.g., "sans-serif") are treated as invalid and won't be returned/used by the
caller.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: aa888184-bd39-4b90-93ba-5f4578beeab2

📥 Commits

Reviewing files that changed from the base of the PR and between aa23ae3 and 4d23df4.

📒 Files selected for processing (5)
  • apps/desktop/src/renderer/lib/terminal/appearance/appearance.test.ts
  • apps/desktop/src/renderer/lib/terminal/appearance/index.ts
  • apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/hooks/usePaneRegistry/components/TerminalPane/hooks/useTerminalAppearance/useTerminalAppearance.ts
  • apps/desktop/src/renderer/routes/_authenticated/settings/appearance/components/AppearanceSettings/components/FontSettingSection/components/FontFamilyCombobox/FontFamilyCombobox.tsx
  • apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/TabsContent/Terminal/Terminal.tsx

Comment thread apps/desktop/src/renderer/lib/terminal/appearance/index.ts Outdated
Copy link
Copy Markdown
Contributor

@cubic-dev-ai cubic-dev-ai Bot left a comment

Choose a reason for hiding this comment

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

1 issue found across 5 files

Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/hooks/usePaneRegistry/components/TerminalPane/hooks/useTerminalAppearance/useTerminalAppearance.ts">

<violation number="1" location="apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/hooks/usePaneRegistry/components/TerminalPane/hooks/useTerminalAppearance/useTerminalAppearance.ts:24">
P2: Sanitization still lets all-generic non-monospace font stacks through, so the terminal can still be launched with a proportional font.</violation>
</file>

Reply with feedback, questions, or to request a fix. Tag @cubic-dev-ai to re-run a review.

const theme = terminalTheme ?? fallbackTheme;
const fontFamily =
fontSettings?.terminalFontFamily || DEFAULT_TERMINAL_FONT_FAMILY;
const fontFamily = sanitizeTerminalFontFamily(
Copy link
Copy Markdown
Contributor

@cubic-dev-ai cubic-dev-ai Bot Apr 18, 2026

Choose a reason for hiding this comment

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

P2: Sanitization still lets all-generic non-monospace font stacks through, so the terminal can still be launched with a proportional font.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/hooks/usePaneRegistry/components/TerminalPane/hooks/useTerminalAppearance/useTerminalAppearance.ts, line 24:

<comment>Sanitization still lets all-generic non-monospace font stacks through, so the terminal can still be launched with a proportional font.</comment>

<file context>
@@ -21,8 +21,9 @@ export function useTerminalAppearance(): TerminalAppearance {
 		const theme = terminalTheme ?? fallbackTheme;
-		const fontFamily =
-			fontSettings?.terminalFontFamily || DEFAULT_TERMINAL_FONT_FAMILY;
+		const fontFamily = sanitizeTerminalFontFamily(
+			fontSettings?.terminalFontFamily,
+		);
</file context>
Fix with Cubic

Follow-up on #3554 review. sanitizeTerminalFontFamily previously passed
any all-generic CSS value through untouched (e.g. "cursive", "sans-serif",
"monospace, sans-serif") because parsePrimaryFontFamily returns null when
no concrete family is present — same blank-window crash class as the
"Inter" report.

Refactor the sanitizer to inspect the full family list: when no concrete
primary exists, only trust the value if every entry is a monospace
generic; otherwise fall back to the default. Add regression tests.
- Always append "monospace" to the sanitized terminal font stack when it
  doesn't already end with one. Mirrors VS Code's behavior in
  src/vs/workbench/contrib/terminal/browser/terminalConfigurationService.ts
  so that if the configured primary isn't installed on this machine, the
  browser falls back to the OS monospace generic instead of a proportional
  default.
- Swap the terminal font preview from a box-drawing layout (which rendered
  as broken in proportional fonts and used tofu glyphs) to a shell session
  that demonstrates column alignment naturally.
- Drop a couple of narrating comments flagged in simplify review.
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@apps/desktop/src/renderer/lib/terminal/appearance/index.ts`:
- Around line 141-171: The current code picks the first non-generic entry as
`primary`, which lets a persisted value like `sans-serif, "JetBrains Mono"` slip
through; instead validate the actual CSS primary (the first family token in
`families`, i.e. `families[0]`) — if the first is a generic, apply the existing
"all-mono" check using `MONOSPACE_GENERIC_FAMILIES` and fall back to
`DEFAULT_TERMINAL_FONT_FAMILY` or return `cssValue` accordingly; if the first is
non-generic, call `isFontFamilyMonospace` on that actual primary and fall back
to `DEFAULT_TERMINAL_FONT_FAMILY` on failure; finally preserve the existing
logic that appends a monospace generic tail (`MONOSPACE_GENERIC_FAMILIES`) to
`cssValue` when missing.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: db92d0b7-05d2-4a7c-bd0d-170aa87e56b4

📥 Commits

Reviewing files that changed from the base of the PR and between a0c584d and b2e6a04.

📒 Files selected for processing (3)
  • apps/desktop/src/renderer/lib/terminal/appearance/appearance.test.ts
  • apps/desktop/src/renderer/lib/terminal/appearance/index.ts
  • apps/desktop/src/renderer/routes/_authenticated/settings/appearance/components/AppearanceSettings/components/FontSettingSection/components/FontPreview/FontPreview.tsx
✅ Files skipped from review due to trivial changes (1)
  • apps/desktop/src/renderer/routes/_authenticated/settings/appearance/components/AppearanceSettings/components/FontSettingSection/components/FontPreview/FontPreview.tsx
🚧 Files skipped from review as they are similar to previous changes (1)
  • apps/desktop/src/renderer/lib/terminal/appearance/appearance.test.ts

Comment on lines +141 to +171
const primary = families.find(
(f) => !GENERIC_FONT_FAMILIES.has(f.toLowerCase()),
);
if (!primary) {
// All-generic stack (e.g. "monospace", "cursive, fantasy"). Only trust it
// when every entry is a monospace generic — otherwise a value like
// "sans-serif" would still blank the terminal.
const allMono = families.every((f) =>
MONOSPACE_GENERIC_FAMILIES.has(f.toLowerCase()),
);
if (allMono) return cssValue;
console.warn(
`[terminal] Font stack "${cssValue}" has no monospace family; falling back to default terminal font.`,
);
return DEFAULT_TERMINAL_FONT_FAMILY;
}

if (!isFontFamilyMonospace(primary)) {
console.warn(
`[terminal] Font "${primary}" is not monospace; falling back to default terminal font.`,
);
return DEFAULT_TERMINAL_FONT_FAMILY;
}
// Ensure a generic monospace tail — if the configured primary isn't
// installed on this machine, the browser falls back to the OS monospace
// generic instead of a proportional default (mirrors VS Code's behavior
// in src/vs/workbench/contrib/terminal/browser/terminalConfigurationService.ts).
const hasMonoTail = families.some((f) =>
MONOSPACE_GENERIC_FAMILIES.has(f.toLowerCase()),
);
return hasMonoTail ? cssValue : `${cssValue}, monospace`;
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Validate the actual CSS primary family, not the first non-generic entry.

Line 141 skips leading generics, so a persisted value like sans-serif, "JetBrains Mono" passes validation because "JetBrains Mono" is monospace. CSS will still render sans-serif first, leaving the proportional primary in place and potentially preserving the crash path.

🐛 Proposed fix
-	const primary = families.find(
-		(f) => !GENERIC_FONT_FAMILIES.has(f.toLowerCase()),
-	);
-	if (!primary) {
-		// All-generic stack (e.g. "monospace", "cursive, fantasy"). Only trust it
-		// when every entry is a monospace generic — otherwise a value like
-		// "sans-serif" would still blank the terminal.
-		const allMono = families.every((f) =>
-			MONOSPACE_GENERIC_FAMILIES.has(f.toLowerCase()),
-		);
-		if (allMono) return cssValue;
+	const primary = families[0];
+	const primaryKey = primary.toLowerCase();
+
+	if (GENERIC_FONT_FAMILIES.has(primaryKey)) {
+		if (MONOSPACE_GENERIC_FAMILIES.has(primaryKey)) return cssValue;
 		console.warn(
 			`[terminal] Font stack "${cssValue}" has no monospace family; falling back to default terminal font.`,
 		);
 		return DEFAULT_TERMINAL_FONT_FAMILY;
 	}
 
 	if (!isFontFamilyMonospace(primary)) {
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const primary = families.find(
(f) => !GENERIC_FONT_FAMILIES.has(f.toLowerCase()),
);
if (!primary) {
// All-generic stack (e.g. "monospace", "cursive, fantasy"). Only trust it
// when every entry is a monospace generic — otherwise a value like
// "sans-serif" would still blank the terminal.
const allMono = families.every((f) =>
MONOSPACE_GENERIC_FAMILIES.has(f.toLowerCase()),
);
if (allMono) return cssValue;
console.warn(
`[terminal] Font stack "${cssValue}" has no monospace family; falling back to default terminal font.`,
);
return DEFAULT_TERMINAL_FONT_FAMILY;
}
if (!isFontFamilyMonospace(primary)) {
console.warn(
`[terminal] Font "${primary}" is not monospace; falling back to default terminal font.`,
);
return DEFAULT_TERMINAL_FONT_FAMILY;
}
// Ensure a generic monospace tail — if the configured primary isn't
// installed on this machine, the browser falls back to the OS monospace
// generic instead of a proportional default (mirrors VS Code's behavior
// in src/vs/workbench/contrib/terminal/browser/terminalConfigurationService.ts).
const hasMonoTail = families.some((f) =>
MONOSPACE_GENERIC_FAMILIES.has(f.toLowerCase()),
);
return hasMonoTail ? cssValue : `${cssValue}, monospace`;
const primary = families[0];
const primaryKey = primary.toLowerCase();
if (GENERIC_FONT_FAMILIES.has(primaryKey)) {
if (MONOSPACE_GENERIC_FAMILIES.has(primaryKey)) return cssValue;
console.warn(
`[terminal] Font stack "${cssValue}" has no monospace family; falling back to default terminal font.`,
);
return DEFAULT_TERMINAL_FONT_FAMILY;
}
if (!isFontFamilyMonospace(primary)) {
console.warn(
`[terminal] Font "${primary}" is not monospace; falling back to default terminal font.`,
);
return DEFAULT_TERMINAL_FONT_FAMILY;
}
// Ensure a generic monospace tail — if the configured primary isn't
// installed on this machine, the browser falls back to the OS monospace
// generic instead of a proportional default (mirrors VS Code's behavior
// in src/vs/workbench/contrib/terminal/browser/terminalConfigurationService.ts).
const hasMonoTail = families.some((f) =>
MONOSPACE_GENERIC_FAMILIES.has(f.toLowerCase()),
);
return hasMonoTail ? cssValue : `${cssValue}, monospace`;
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/desktop/src/renderer/lib/terminal/appearance/index.ts` around lines 141
- 171, The current code picks the first non-generic entry as `primary`, which
lets a persisted value like `sans-serif, "JetBrains Mono"` slip through; instead
validate the actual CSS primary (the first family token in `families`, i.e.
`families[0]`) — if the first is a generic, apply the existing "all-mono" check
using `MONOSPACE_GENERIC_FAMILIES` and fall back to
`DEFAULT_TERMINAL_FONT_FAMILY` or return `cssValue` accordingly; if the first is
non-generic, call `isFontFamilyMonospace` on that actual primary and fall back
to `DEFAULT_TERMINAL_FONT_FAMILY` on failure; finally preserve the existing
logic that appends a monospace generic tail (`MONOSPACE_GENERIC_FAMILIES`) to
`cssValue` when missing.

Nerd Fonts are monospace — the terminal-only gate was pre-existing
special-casing and reviewers pointed out it hides a widely-used class
of fonts from users picking an editor font. Drop the gate.
…crete entry

Addresses the coderabbit review on b2e6a04. The sanitizer previously
skipped leading generics when picking the primary to measure, so a value
like `sans-serif, "JetBrains Mono"` passed validation because the later
concrete entry was monospace — but CSS resolves the first generic
(sans-serif) and the terminal still renders proportional.

Switch to validating families[0] (the actual CSS primary): if it's a
monospace generic, trust the stack; if it's a proportional generic, fall
back; if it's concrete, canvas-measure it. Add regression tests.
@Kitenite Kitenite merged commit 56e6652 into main Apr 18, 2026
7 checks passed
@Kitenite Kitenite deleted the fix/3513-font-crash branch April 18, 2026 19:39
@github-actions
Copy link
Copy Markdown
Contributor

🧹 Preview Cleanup Complete

The following preview resources have been cleaned up:

  • ✅ Neon database branch
  • ⚠️ Electric Fly.io app

Thank you for your contribution! 🎉

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.

[bug] Changing terminal font to Inter crashes the app into a blank window — no way to recover from inside the app

1 participant