Skip to content

fix: allow disabling WebGL terminal renderer to prevent garbled text - #3795

Closed
tomqiaozc wants to merge 2 commits into
ParadevAI:mainfrom
tomqiaozc:fix/terminal-webgl-renderer-preference
Closed

tomqiaozc wants to merge 2 commits into
ParadevAI:mainfrom
tomqiaozc:fix/terminal-webgl-renderer-preference

Conversation

@tomqiaozc

@tomqiaozc tomqiaozc commented Apr 27, 2026 •

Copy link
Copy Markdown

Summary

  • Adds a localStorage-based preference (terminal-renderer-type) to let users choose between WebGL and DOM rendering for the terminal
  • Fixes both v1 (helpers.ts) and v2 (terminal-addons.ts) terminal initialization paths
  • Users experiencing garbled characters can now run localStorage.setItem("terminal-renderer-type", "dom") in DevTools instead of launching the entire app with --disable-gpu

Problem

The xterm.js WebGL addon's glyph texture atlas can silently become corrupted on some machines/GPUs, causing characters to render as wrong glyphs. The existing onContextLoss fallback only fires on complete context loss — partial texture corruption is not detected, so the DOM fallback never triggers.

The only current workaround is --disable-gpu, which disables all GPU acceleration (window compositing, CSS animations, etc.), which is too broad.

Solution

Read a localStorage preference before initializing suggestedRendererType. If terminal-renderer-type is set to "dom", WebGL loading is skipped entirely. The default behavior (undefined → try WebGL first) is unchanged for users not affected.

This is a minimal, non-breaking change. A settings UI toggle can be added in a follow-up.

How to use

In DevTools console:

// Disable WebGL rendering
localStorage.setItem("terminal-renderer-type", "dom");
// Re-enable WebGL rendering
localStorage.removeItem("terminal-renderer-type");

Then restart the app (or open a new terminal tab).

Test plan

  • Verify default behavior unchanged: new installs still use WebGL
  • Set localStorage.setItem("terminal-renderer-type", "dom") → restart → confirm no garbled text
  • Set localStorage.setItem("terminal-renderer-type", "webgl") → restart → confirm WebGL is used
  • Remove the key → restart → confirm fallback to auto behavior

Fixes #3794

🤖 Generated with Claude Code


Summary by cubic

Adds a localStorage preference to disable the terminal’s WebGL renderer so users with garbled text can force the DOM renderer without --disable-gpu. Default behavior stays the same for everyone else.

  • Bug Fixes
    • Read localStorage key terminal-renderer-type and honor "dom" to skip WebGL; any other value (including "webgl") is ignored.
    • Apply the check in both init paths (helpers.ts and terminal-addons.ts).
    • Workaround for xterm.js WebGL glyph atlas corruption by using DOM when "dom" is set.

Written for commit b4b59d5. Summary will update on new commits. Review in cubic

Summary by CodeRabbit

  • New Features
    • Terminal renderer preference (DOM or WebGL) is now applied at startup by reading the local preference.
    • Previously selected renderer type persists across sessions and is applied automatically.
    • The app still falls back to the alternative renderer if compatibility or runtime errors occur.

The xterm.js WebGL addon can silently corrupt its glyph texture atlas,
causing terminal characters to render as wrong glyphs. The existing
fallback only triggers on complete WebGL context loss, missing partial
corruption. This adds a localStorage-based preference check so users
can set `terminal-renderer-type` to `"dom"` to bypass WebGL without
needing `--disable-gpu` (which disables all GPU acceleration).

Fixes ParadevAI#3794

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@coderabbitai

coderabbitai Bot commented Apr 27, 2026 •

Copy link
Copy Markdown
Contributor
📝 Walkthrough

Walkthrough

Two terminal initialization modules now pre-read localStorage["terminal-renderer-type"] and set suggestedRendererType = "dom" when the stored value is "dom", causing subsequent terminal creation to skip WebGL initialization; prior runtime fallback behavior remains unchanged otherwise.

Changes

Cohort / File(s) Summary
Terminal Renderer Preference Persistence
apps/desktop/src/renderer/lib/terminal/terminal-addons.ts, apps/desktop/src/renderer/screens/main/.../Terminal/helpers.ts
Both modules now initialize suggestedRendererType from localStorage["terminal-renderer-type"] and force "dom" when that value equals "dom", causing existing WebGL setup to be skipped up-front; existing WebGL error fallback still sets suggestedRendererType = "dom".

Estimated code review effort

🎯 2 (Simple) | ⏱️ ~10 minutes

Poem

🐰 I nibble bytes and stash a key,
localStorage hums, "let DOM be me."
No scrambled glyphs to make me frown,
I hop from WebGL and settle down. 🥕

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title 'fix: allow disabling WebGL terminal renderer to prevent garbled text' clearly and concisely summarizes the main change—adding a way to disable WebGL to fix garbled character rendering.
Description check ✅ Passed The PR description provides comprehensive coverage with clear sections including problem statement, solution, implementation details, usage instructions, and test plan. All required template elements are addressed.
Linked Issues check ✅ Passed The PR fully implements the objectives from linked issue #3794: adds localStorage-based renderer preference, applies it to both initialization paths (helpers.ts and terminal-addons.ts), and provides users an escape hatch without disabling all GPU acceleration.
Out of Scope Changes check ✅ Passed All changes are directly within scope of issue #3794. The modifications to terminal-addons.ts and helpers.ts exclusively address the localStorage preference for controlling the terminal renderer, with no unrelated changes detected.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.

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

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

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

greptile-apps Bot commented Apr 27, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

Adds a localStorage key (terminal-renderer-type) that is read once at module load to pre-seed suggestedRendererType in both the v1 (helpers.ts) and v2 (terminal-addons.ts) terminal initialisation paths. Setting the key to \"dom\" correctly bypasses WebGL entirely; the try/catch and value-allowlist guard against unavailable/malformed storage.

Confidence Score: 5/5

Safe to merge — the "dom" opt-out path is correct and default behaviour is unchanged

The only finding is P2: the "webgl" localStorage value is functionally identical to undefined (both attempt WebGL, both fall back to DOM on failure). This does not affect the primary use-case or existing users. All remaining feedback is style/documentation only.

No files require special attention

Important Files Changed

Filename Overview
apps/desktop/src/renderer/lib/terminal/terminal-addons.ts Adds IIFE-based localStorage read to initialise suggestedRendererType; core "dom" opt-out logic is correct, "webgl" value is functionally a no-op (P2 style note)
apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/TabsContent/Terminal/helpers.ts Applies the identical localStorage-preference initialisation to the v1 terminal path; same P2 note about "webgl" being equivalent to undefined

Sequence Diagram

sequenceDiagram
    participant LS as localStorage
    participant Mod as Module init (IIFE)
    participant RAF as requestAnimationFrame
    participant WGL as WebglAddon
    participant DOM as DOM renderer

    Mod->>LS: getItem("terminal-renderer-type")
    LS-->>Mod: "dom" | "webgl" | null
    Note over Mod: suggestedRendererType = "dom" | "webgl" | undefined

    Note over RAF: New terminal opened
    RAF->>RAF: suggestedRendererType === "dom"?
    alt "dom" (or disposed)
        RAF-->>DOM: skip WebGL, use DOM renderer
    else "webgl" or undefined
        RAF->>WGL: new WebglAddon() + loadAddon()
        alt WebGL succeeds
            WGL-->>RAF: WebGL renderer active
            WGL->>WGL: onContextLoss → dispose WebGL
        else WebGL throws
            RAF->>Mod: suggestedRendererType = "dom"
            RAF-->>DOM: fallback to DOM renderer
        end
    end
Loading
Prompt To Fix All With AI
This is a comment left during a code review.
Path: apps/desktop/src/renderer/lib/terminal/terminal-addons.ts
Line: 19-25

Comment:
**`"webgl"` preference is functionally identical to `undefined`**

Setting `terminal-renderer-type` to `"webgl"` initializes `suggestedRendererType` to `"webgl"`, but the only branch that guards on this variable is `if (… suggestedRendererType === "dom") return` (line 55). Both `"webgl"` and `undefined` bypass that guard and attempt WebGL — so the two values are indistinguishable at runtime.

More importantly, if WebGL fails (the `catch` on line 66), `suggestedRendererType` is overwritten to `"dom"` in memory regardless of what localStorage says, so the user's explicit `"webgl"` preference is silently discarded for the remainder of the session. A user who sets `"webgl"` to "re-enable" WebGL on a broken machine will still end up on DOM after the first failure — exactly the same outcome as `undefined`.

Consider either documenting this limitation in a comment, or accepting only `"dom"` as a valid preference value (the only value that meaningfully changes behaviour). The same pattern applies to `helpers.ts`.

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

Reviews (1): Last reviewed commit: "fix: allow users to disable WebGL termin..." | Re-trigger Greptile

Comment on lines +19 to +25
let suggestedRendererType: "webgl" | "dom" | undefined = (() => {
try {
const pref = localStorage.getItem("terminal-renderer-type");
if (pref === "dom" || pref === "webgl") return pref;
} catch {}
return undefined;
})();

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.

P2 "webgl" preference is functionally identical to undefined

Setting terminal-renderer-type to "webgl" initializes suggestedRendererType to "webgl", but the only branch that guards on this variable is if (… suggestedRendererType === "dom") return (line 55). Both "webgl" and undefined bypass that guard and attempt WebGL — so the two values are indistinguishable at runtime.

More importantly, if WebGL fails (the catch on line 66), suggestedRendererType is overwritten to "dom" in memory regardless of what localStorage says, so the user's explicit "webgl" preference is silently discarded for the remainder of the session. A user who sets "webgl" to "re-enable" WebGL on a broken machine will still end up on DOM after the first failure — exactly the same outcome as undefined.

Consider either documenting this limitation in a comment, or accepting only "dom" as a valid preference value (the only value that meaningfully changes behaviour). The same pattern applies to helpers.ts.

Prompt To Fix With AI
This is a comment left during a code review.
Path: apps/desktop/src/renderer/lib/terminal/terminal-addons.ts
Line: 19-25

Comment:
**`"webgl"` preference is functionally identical to `undefined`**

Setting `terminal-renderer-type` to `"webgl"` initializes `suggestedRendererType` to `"webgl"`, but the only branch that guards on this variable is `if (… suggestedRendererType === "dom") return` (line 55). Both `"webgl"` and `undefined` bypass that guard and attempt WebGL — so the two values are indistinguishable at runtime.

More importantly, if WebGL fails (the `catch` on line 66), `suggestedRendererType` is overwritten to `"dom"` in memory regardless of what localStorage says, so the user's explicit `"webgl"` preference is silently discarded for the remainder of the session. A user who sets `"webgl"` to "re-enable" WebGL on a broken machine will still end up on DOM after the first failure — exactly the same outcome as `undefined`.

Consider either documenting this limitation in a comment, or accepting only `"dom"` as a valid preference value (the only value that meaningfully changes behaviour). The same pattern applies to `helpers.ts`.

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

@coderabbitai coderabbitai Bot left a comment

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.

🧹 Nitpick comments (2)
apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/TabsContent/Terminal/helpers.ts (1)

64-73: Extract the localStorage preference reader to a shared helper.

The IIFE at lines 67-73 is byte-identical to the one in apps/desktop/src/renderer/lib/terminal/terminal-addons.ts (lines 19-25). Since the helper is used in 2+ places, it should be promoted to a shared module so the parsing/validation logic lives in one place — otherwise a future tweak (e.g., adding a "canvas" value, normalizing case, or migrating to a different storage key) has to be applied in two spots and can silently drift.

A good home would be alongside the existing terminal utilities, e.g. apps/desktop/src/renderer/lib/terminal/renderer-preference.ts:

♻️ Proposed shared helper + call-site updates

New file apps/desktop/src/renderer/lib/terminal/renderer-preference.ts:

export type TerminalRendererType = "webgl" | "dom";

/**
 * Read the user's persisted terminal renderer preference.
 * Returns `undefined` if unset, invalid, or localStorage is unavailable.
 */
export function getStoredRendererPreference():
  | TerminalRendererType
  | undefined {
  try {
    const pref = localStorage.getItem("terminal-renderer-type");
    if (pref === "dom" || pref === "webgl") return pref;
  } catch {}
  return undefined;
}

Then in this file:

-// Once WebGL fails, skip it for all subsequent terminals (VS Code pattern).
-// Users can also set localStorage "terminal-renderer-type" to "dom" to
-// permanently disable WebGL (workaround for GPU texture atlas corruption).
-let suggestedRendererType: "webgl" | "dom" | undefined = (() => {
-	try {
-		const pref = localStorage.getItem("terminal-renderer-type");
-		if (pref === "dom" || pref === "webgl") return pref;
-	} catch {}
-	return undefined;
-})();
+// Once WebGL fails, skip it for all subsequent terminals (VS Code pattern).
+// Users can also set localStorage "terminal-renderer-type" to "dom" to
+// permanently disable WebGL (workaround for GPU texture atlas corruption).
+let suggestedRendererType: TerminalRendererType | undefined =
+	getStoredRendererPreference();

(plus the matching change in terminal-addons.ts).

As per coding guidelines: "If a utility is used once, nest it under the parent's directory. If used 2+ times, promote it to the highest shared parent's directory or top-level components/."

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/TabsContent/Terminal/helpers.ts`
around lines 64 - 73, The IIFE that sets suggestedRendererType is duplicated and
should be extracted into a shared helper; create a new exported function
getStoredRendererPreference (returning "webgl" | "dom" | undefined) alongside
the terminal utilities, move the localStorage read/validation logic there, and
replace the IIFE in this file (and the matching one in terminal-addons.ts) by
calling getStoredRendererPreference to set suggestedRendererType so
parsing/validation lives in one place.
apps/desktop/src/renderer/lib/terminal/terminal-addons.ts (1)

16-25: Same dedup applies here.

This block is identical to the one in apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/TabsContent/Terminal/helpers.ts (lines 67-73). Once the shared getStoredRendererPreference() helper is extracted (see comment on that file), this initializer should call it as well so both terminal init paths stay in lockstep on parsing/validation rules.

Note: each file still keeps its own module-scoped suggestedRendererType for the runtime WebGL-fallback latch, which is intentional — only the localStorage read needs to be shared.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/desktop/src/renderer/lib/terminal/terminal-addons.ts` around lines 16 -
25, Extract the localStorage parsing logic into a shared helper
getStoredRendererPreference() (as noted in the other file) and replace the
module initializer for suggestedRendererType in this file so it calls that
helper instead of duplicating the code; keep the module-level
suggestedRendererType variable name and its type ("webgl" | "dom" | undefined)
but set it by invoking getStoredRendererPreference(), ensuring the same
validation rules (accept only "dom" or "webgl") are used in both places.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@apps/desktop/src/renderer/lib/terminal/terminal-addons.ts`:
- Around line 16-25: Extract the localStorage parsing logic into a shared helper
getStoredRendererPreference() (as noted in the other file) and replace the
module initializer for suggestedRendererType in this file so it calls that
helper instead of duplicating the code; keep the module-level
suggestedRendererType variable name and its type ("webgl" | "dom" | undefined)
but set it by invoking getStoredRendererPreference(), ensuring the same
validation rules (accept only "dom" or "webgl") are used in both places.

In
`@apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/TabsContent/Terminal/helpers.ts`:
- Around line 64-73: The IIFE that sets suggestedRendererType is duplicated and
should be extracted into a shared helper; create a new exported function
getStoredRendererPreference (returning "webgl" | "dom" | undefined) alongside
the terminal utilities, move the localStorage read/validation logic there, and
replace the IIFE in this file (and the matching one in terminal-addons.ts) by
calling getStoredRendererPreference to set suggestedRendererType so
parsing/validation lives in one place.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: a1ad4bf5-2620-4e59-a2f5-b693a0a85135

📥 Commits

Reviewing files that changed from the base of the PR and between 2183bcf and 8800b17.

📒 Files selected for processing (2)
  • apps/desktop/src/renderer/lib/terminal/terminal-addons.ts
  • apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/TabsContent/Terminal/helpers.ts

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

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.

No issues found across 2 files

"webgl" preference was functionally identical to undefined — the only
guard checks for "dom", and if WebGL fails at runtime the catch block
overwrites to "dom" regardless of localStorage. Simplify to only
recognize "dom" and document the limitation.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

@coderabbitai coderabbitai Bot left a comment

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.

🧹 Nitpick comments (1)
apps/desktop/src/renderer/lib/terminal/terminal-addons.ts (1)

16-25: Consider extracting the duplicated renderer-type initializer into a shared helper.

The exact same IIFE (including the comment block) is duplicated in apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/TabsContent/Terminal/helpers.ts (lines 64-73). Both v1 and v2 terminals will need to be kept in sync if the preference key, accepted values, or fallback logic ever changes (e.g., adding a settings UI as hinted in the PR objectives). A small shared util such as getInitialSuggestedRendererType() co-located under apps/desktop/src/renderer/lib/terminal/ would centralize the contract while preserving the independent module-level suggestedRendererType variables in each file.

♻️ Example extraction

New file apps/desktop/src/renderer/lib/terminal/renderer-preference.ts:

// Users can set localStorage "terminal-renderer-type" to "dom" to permanently
// disable WebGL (workaround for GPU texture atlas corruption). Only "dom" is
// meaningful — any other value (including "webgl") behaves like the default.
export function getInitialSuggestedRendererType(): "dom" | undefined {
	try {
		if (localStorage.getItem("terminal-renderer-type") === "dom") return "dom";
	} catch {}
	return undefined;
}

Then in this file:

-// Once WebGL fails, skip it for all subsequent runtimes (VS Code pattern).
-// Users can set localStorage "terminal-renderer-type" to "dom" to permanently
-// disable WebGL (workaround for GPU texture atlas corruption). Only "dom" is
-// meaningful — any other value (including "webgl") behaves like the default.
-let suggestedRendererType: "webgl" | "dom" | undefined = (() => {
-	try {
-		if (localStorage.getItem("terminal-renderer-type") === "dom") return "dom";
-	} catch {}
-	return undefined;
-})();
+// Once WebGL fails, skip it for all subsequent runtimes (VS Code pattern).
+let suggestedRendererType: "webgl" | "dom" | undefined =
+	getInitialSuggestedRendererType();
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/desktop/src/renderer/lib/terminal/terminal-addons.ts` around lines 16 -
25, The duplicated IIFE that initializes suggestedRendererType should be
extracted into a single helper (e.g., getInitialSuggestedRendererType) colocated
under the terminal lib and both modules should call that helper to initialize
their module-level suggestedRendererType; keep the existing explanatory comment
in the new helper, implement the function to return "dom" | undefined by reading
localStorage with try/catch, and replace the IIFE in this file (symbol:
suggestedRendererType) and the other helper file (the duplicate block in
WorkspaceView/ContentView/TabsContent/Terminal/helpers.ts) to call
getInitialSuggestedRendererType().
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@apps/desktop/src/renderer/lib/terminal/terminal-addons.ts`:
- Around line 16-25: The duplicated IIFE that initializes suggestedRendererType
should be extracted into a single helper (e.g., getInitialSuggestedRendererType)
colocated under the terminal lib and both modules should call that helper to
initialize their module-level suggestedRendererType; keep the existing
explanatory comment in the new helper, implement the function to return "dom" |
undefined by reading localStorage with try/catch, and replace the IIFE in this
file (symbol: suggestedRendererType) and the other helper file (the duplicate
block in WorkspaceView/ContentView/TabsContent/Terminal/helpers.ts) to call
getInitialSuggestedRendererType().

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 81223a31-f751-42c3-ac50-3127290e2e3c

📥 Commits

Reviewing files that changed from the base of the PR and between 8800b17 and b4b59d5.

📒 Files selected for processing (2)
  • apps/desktop/src/renderer/lib/terminal/terminal-addons.ts
  • apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/TabsContent/Terminal/helpers.ts
🚧 Files skipped from review as they are similar to previous changes (1)
  • apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/TabsContent/Terminal/helpers.ts

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.

Terminal renders garbled characters (WebGL texture atlas corruption)

2 participants