Skip to content

Add Claude Sonnet 5 model and theming system - #3912

Closed
thelooter wants to merge 3 commits into
pingdotgg:mainfrom
thelooter:feat/add-sonnet-5
Closed

Add Claude Sonnet 5 model and theming system#3912
thelooter wants to merge 3 commits into
pingdotgg:mainfrom
thelooter:feat/add-sonnet-5

Conversation

@thelooter

@thelooter thelooter commented Jul 12, 2026

Copy link
Copy Markdown

Summary

  • Add Claude Sonnet 5 (claude-sonnet-5) as a built-in model with reasoning-effort and 200k/1M context-window options, gated behind Claude Code ≥ 2.1.197 with an upgrade message for older versions
  • Preserve xhigh effort for Sonnet 5 in normalizeClaudeCliEffort and flag it as a "new" model in the model picker
  • Add a full theming system: built-in palettes, a custom theme editor, and an Appearance settings page (light/dark theme slots + color mode) split out of General settings
  • Support theme import/export/duplicate/delete via clipboard, file, and download, with a theme registry, transport layer, and types
  • Make Shiki syntax highlighting palette-aware by keying the highlighter cache on the active theme and widening DiffWorkerError.themeName to any Shiki theme id
  • Switch CI/release/deploy workflows from Blacksmith runners to GitHub-hosted runners (ubuntu-24.04, macos-26, windows-2025)

Testing

  • ClaudeAdapter.test.ts: preserves xhigh effort for Claude Sonnet 5
  • ProviderRegistry.test.ts: shows Sonnet 5 on supported versions, hides it with the upgrade message on older ones
  • ThemeEditor.test.ts, themes/registry.test.ts, themes/transport.test.ts, diffRendering.syntaxTheme.test.ts: theming registry/editor/transport/syntax-theme behavior
  • CI workflow changes: not run locally — validated by the CI run on this PR
  • Manual UI check of the Appearance settings page and theme switching: Not run

Note

Medium Risk
Large user-facing theming surface and model-selection changes affect defaults and highlighting; CI runner swap may change build times or capacity but not app runtime security.

Overview
Adds Claude Sonnet 5 as a built-in model (reasoning effort, 200k/1M context) gated on Claude Code ≥ 2.1.197, keeps xhigh effort for that slug, updates default sonnet aliases to Sonnet 5, and marks it NEW in the model picker.

Introduces a token-based theming system: built-in palettes (Default, Solarized, Nord, Catppuccin, Rosé Pine, High Contrast), separate light/dark theme slots, a new Appearance settings route with import/export/duplicate/edit, runtime CSS injection with --alphacolor-mix, and palette-aware Shiki highlighting (highlighter cache keyed by theme).

Moves color mode out of General settings into Appearance; scrollbars/noise use theme CSS variables.

CI/release/deploy workflows switch from Blacksmith runners to GitHub-hosted (ubuntu-24.04, macos-26, windows-2025).

Reviewed by Cursor Bugbot for commit 69a39c9. Bugbot is set up for automated code reviews on this repo. Configure here.

Note

Add Claude Sonnet 5 model and a token-based theming system with an Appearance settings page

  • Registers claude-sonnet-5 as a new built-in model (gated on Claude CLI ≥ v2.1.197), preserves xhigh effort for it, and routes the sonnet alias to it instead of claude-sonnet-4-6.
  • Introduces a token-based theming system (registry.ts, builtin.ts) with built-in palettes: Default, Solarized, Nord, High Contrast, Catppuccin flavors, and Rosé Pine variants.
  • Adds a new /settings/appearance page (AppearanceSettingsPanel) for selecting active light/dark themes and color mode, with create/duplicate/edit/export/import/delete of custom themes.
  • Adds a ThemeEditor component (ThemeEditor.tsx) with live preview, per-variant token editing, JSON mode with schema validation, and discard confirmation.
  • Theme transport utilities (transport.ts) handle serialization, file download, clipboard copy, base64 encode/decode, and import with collision policies (add/rename/replace).
  • Syntax highlighting in ChatMarkdown and diff rendering now selects the active palette's Shiki theme id, falling back to Pierre themes.
  • Migrates all CI and release workflow jobs from Blacksmith runners to standard GitHub-hosted runners (ubuntu-24.04, macos-26, windows-2025).
  • Behavioral Change: the sonnet model alias now resolves to claude-sonnet-5; any caller relying on it resolving to claude-sonnet-4-6 will target a different model.
📊 Macroscope summarized 69a39c9. 23 files reviewed, 0 issues evaluated, 0 issues filtered, 0 comments posted

🗂️ Filtered Issues

No issues evaluated.

thelooter and others added 3 commits June 28, 2026 16:04
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
Sonnet 5 (claude-sonnet-5) shipped in Claude Code 2.1.197 as the new
default Sonnet, with native xhigh/max effort and a 1M-token context window.

- Register claude-sonnet-5 in BUILT_IN_MODELS with the low/medium/high
  (default)/xhigh/max effort selector (plus ultracode/ultrathink, matching
  its closest xhigh-native peer Opus 4.8) and a context-window option that
  defaults to 1M.
- Gate it behind Claude Code 2.1.197 via MINIMUM_CLAUDE_SONNET_5_VERSION /
  supportsClaudeSonnet5, wired into getBuiltInClaudeModelsForVersion and the
  version-upgrade-message chain (now newest-first).
- Preserve xhigh (don't downgrade to max) for claude-sonnet-5 in
  normalizeClaudeCliEffort.
- Add sonnet-5 / claude-sonnet-5.0 aliases and repoint the bare "sonnet"
  alias to claude-sonnet-5. Leaves DEFAULT_MODEL_BY_PROVIDER on Sonnet 4.6.
- Highlight claude-sonnet-5 with the "NEW" chip in the picker.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Blacksmith runners aren't available on personal accounts, so CI/release
workflows couldn't run on this fork. Map each Blacksmith label to the
equivalent GitHub-hosted runner, preserving the OS/version and dropping
the Blacksmith-specific vCPU sizing:

  blacksmith-*-ubuntu-2404  -> ubuntu-24.04
  blacksmith-12vcpu-macos-26 -> macos-26
  blacksmith-32vcpu-windows-2025 -> windows-2025

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

coderabbitai Bot commented Jul 12, 2026

Copy link
Copy Markdown

Important

Review skipped

Auto reviews are disabled on this repository. Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: 5389fbcc-b4a1-4335-998e-4529e2c3a90c

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
✨ 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.

@github-actions github-actions Bot added vouch:unvouched PR author is not yet trusted in the VOUCHED list. size:XXL 1,000+ changed lines (additions + deletions). labels Jul 12, 2026
@thelooter

Copy link
Copy Markdown
Author

Sorry, this was meant for my private fork, this shouldn't have been opened here

@thelooter thelooter closed this Jul 12, 2026

@cursor cursor 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.

Cursor Bugbot has reviewed your changes using high effort and found 5 potential issues.

Fix All in Cursor

❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.

Want fixes drafted automatically? Bugbot Autofix can create code changes for findings. A team admin can enable Autofix in the Cursor dashboard.

Reviewed by Cursor Bugbot for commit 69a39c9. Configure here.

const isDark = theme === "dark" || (theme === "system" && systemDark);
document.documentElement.classList.toggle("dark", isDark);
lastAppliedTheme = { theme, systemDark };
restoreActiveThemes();

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.

Palette changes skip CSS apply

High Severity

When setActiveLightTheme or setActiveDarkTheme are called, applyTheme is triggered but returns early because only the active palette ID changes, not the main theme or system dark mode. This prevents restoreActiveThemes from running, so the app's CSS custom properties for the new palette are not applied until a color mode change or page reload.

Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 69a39c9. Configure here.

if (source.id === DEFAULT_THEME_ID) return copy;
if (source.light) copy.light = materializeTokens(source.light);
if (source.dark) copy.dark = materializeTokens(source.dark);
return copy;

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.

Syntax theme lost on share

Medium Severity

The serializeTheme, parseTheme, and duplicateTheme functions don't handle the syntax field. This means themes with custom Shiki mappings lose them when exported, imported, or duplicated, causing code blocks and diffs to fall back to a default highlighting theme.

Additional Locations (1)
Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 69a39c9. Configure here.

}

export function GeneralSettingsPanel() {
const { theme, setTheme } = useTheme();

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.

Appearance restore ignores palettes

Medium Severity

The "Restore defaults" button on the Appearance settings page doesn't fully reset theme settings. It only tracks and resets the overall color mode (theme), but ignores specific light/dark theme palettes (activeLightThemeId, activeDarkThemeId). This can lead to the button being disabled when only palettes are changed, or failing to reset custom palettes when activated.

Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 69a39c9. Configure here.

const style = ensureThemeStyleElement();
const lightCss = tokensToCss(materializeTokens(resolveTokens(lightTheme, "light")));
const darkCss = tokensToCss(materializeTokens(resolveTokens(darkTheme, "dark")));
style.textContent = `:root { ${lightCss} } :root.dark { ${darkCss} }`;

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.

Theme CSS allow-list bypassed

Medium Severity

tokensToCss / applyActiveThemes emit every string entry from resolved token maps, and save/import never strip to THEME_TOKEN_NAMES. The editor claims unknown tokens are ignored, and isValidColorValue still allows ; / }, so imported or edited theme JSON can break out of the injected &lt;style&gt; tag and inject arbitrary CSS.

Additional Locations (2)
Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 69a39c9. Configure here.

const startEditing = useCallback((target: ThemeDefinition) => {
setEditingId(target.id);
setDraft(target);
}, []);

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.

Edit switch drops unsaved draft

Low Severity

startEditing replaces editingId and draft immediately. Clicking Edit on another theme while the current editor has unsaved changes discards that draft with no confirmation, even though Cancel uses a discard dialog for the same risk.

Additional Locations (1)
Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 69a39c9. Configure here.

return BUILT_IN_THEMES.some((theme) => theme.id === id);
}

export function isValidColorValue(value: string): boolean {

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.

🟠 High themes/registry.ts:19

isValidColorValue only rejects a few substrings like javascript: and <script, so values containing CSS delimiters such as red; } body { display: none } :root { pass validation. tokensToCss then interpolates these values verbatim into a <style> element, so importing or JSON-editing a theme with such a value injects arbitrary stylesheet rules that can hide or alter the entire application UI instead of being constrained to a single token value. Consider rejecting any value containing ;, {, }, or < so only a single CSS custom-property value can be produced.

🤖 Copy this AI Prompt to have your agent fix this:
In file @apps/web/src/themes/registry.ts around line 19:

`isValidColorValue` only rejects a few substrings like `javascript:` and `<script`, so values containing CSS delimiters such as `red; } body { display: none } :root {` pass validation. `tokensToCss` then interpolates these values verbatim into a `<style>` element, so importing or JSON-editing a theme with such a value injects arbitrary stylesheet rules that can hide or alter the entire application UI instead of being constrained to a single token value. Consider rejecting any value containing `;`, `{`, `}`, or `<` so only a single CSS custom-property value can be produced.

Comment on lines +177 to +178
sonnet: "claude-sonnet-5",
"sonnet-5": "claude-sonnet-5",

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.

🟠 High src/model.ts:177

Changing the sonnet alias to claude-sonnet-5 makes resolveSelectableModel(..., "sonnet", options) return null on Claude Code versions below 2.1.197: those versions filter Sonnet 5 out of options, and the resolver only resolves an alias whose target is present in options. Previously sonnet resolved to claude-sonnet-4-6, so users on supported-but-older Claude Code versions can no longer resolve the generic sonnet selection even though Sonnet 4.6 remains available. Consider restoring the sonnet alias to claude-sonnet-4-6, or gating it by Claude Code version so the shorthand still resolves for older clients.

-    sonnet: "claude-sonnet-5",
+    sonnet: "claude-sonnet-4-6",
     "sonnet-5": "claude-sonnet-5",
🤖 Copy this AI Prompt to have your agent fix this:
In file @packages/contracts/src/model.ts around lines 177-178:

Changing the `sonnet` alias to `claude-sonnet-5` makes `resolveSelectableModel(..., "sonnet", options)` return `null` on Claude Code versions below `2.1.197`: those versions filter Sonnet 5 out of `options`, and the resolver only resolves an alias whose target is present in `options`. Previously `sonnet` resolved to `claude-sonnet-4-6`, so users on supported-but-older Claude Code versions can no longer resolve the generic `sonnet` selection even though Sonnet 4.6 remains available. Consider restoring the `sonnet` alias to `claude-sonnet-4-6`, or gating it by Claude Code version so the shorthand still resolves for older clients.

Comment on lines +79 to +80
const editing =
editingId !== null && draft !== null ? { source: findTheme(editingId), draft } : null;

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.

🟡 Medium settings/AppearanceSettings.tsx:79

Typing in a token field immediately discards the live preview and reverts the page to the persisted theme. The editing.source value is rebuilt via findTheme(editingId) on every render, which reparses local storage and returns a new object each time. ThemeEditor's cleanup effect is keyed on [source], so every keystroke triggers the cleanup, which calls restoreActiveThemes() and overwrites the in-progress preview that the preview effect just applied. Memoize editing.source so it stays referentially stable while the same theme is being edited.

Suggested change
const editing =
editingId !== null && draft !== null ? { source: findTheme(editingId), draft } : null;
const editingSource = useMemo(() => editingId !== null ? findTheme(editingId) : null, [editingId, themes]);
const editing =
editingId !== null && draft !== null && editingSource !== null ? { source: editingSource, draft } : null;
🤖 Copy this AI Prompt to have your agent fix this:
In file @apps/web/src/components/settings/AppearanceSettings.tsx around lines 79-80:

Typing in a token field immediately discards the live preview and reverts the page to the persisted theme. The `editing.source` value is rebuilt via `findTheme(editingId)` on every render, which reparses local storage and returns a new object each time. `ThemeEditor`'s cleanup effect is keyed on `[source]`, so every keystroke triggers the cleanup, which calls `restoreActiveThemes()` and overwrites the in-progress preview that the preview effect just applied. Memoize `editing.source` so it stays referentially stable while the same theme is being edited.

emitChange();
}, []);

const setActiveLightTheme = useCallback((id: string) => {

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.

🟡 Medium hooks/useTheme.ts:350

setActiveLightTheme and setActiveDarkTheme call setActiveLightThemeId/setActiveDarkThemeId directly without catching errors, unlike setTheme which wraps writeThemePreference in try/catch. If localStorage.setItem throws (quota or security rejection), the exception propagates out of the React event handler — no theme change is applied and no storage error is logged. Consider wrapping these calls in the same error handling used by setTheme, throwing a ThemeStorageError and logging it.

🤖 Copy this AI Prompt to have your agent fix this:
In file @apps/web/src/hooks/useTheme.ts around line 350:

`setActiveLightTheme` and `setActiveDarkTheme` call `setActiveLightThemeId`/`setActiveDarkThemeId` directly without catching errors, unlike `setTheme` which wraps `writeThemePreference` in try/catch. If `localStorage.setItem` throws (quota or security rejection), the exception propagates out of the React event handler — no theme change is applied and no storage error is logged. Consider wrapping these calls in the same error handling used by `setTheme`, throwing a `ThemeStorageError` and logging it.

Comment on lines +27 to +33
export function serializeTheme(theme: ThemeDefinition): string {
const payload: ThemeDefinition = { id: theme.id, name: theme.name };
if (theme.description) payload.description = theme.description;
if (theme.light) payload.light = { ...theme.light };
if (theme.dark) payload.dark = { ...theme.dark };
return JSON.stringify(payload, null, 2);
}

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.

🟡 Medium themes/transport.ts:27

serializeTheme omits the syntax field from the serialized payload, so exporting a theme and then importing it silently drops the Shiki syntax-highlighting configuration. Themes like Solarized, Nord, Catppuccin, and Rosé Pine define palette-specific syntax themes, but after a round-trip through serializeTheme/parseTheme that data is lost and they fall back to the generic syntax theme. The function copies description, light, and dark but never copies syntax. Consider including theme.syntax in the payload when present.

 export function serializeTheme(theme: ThemeDefinition): string {
   const payload: ThemeDefinition = { id: theme.id, name: theme.name };
   if (theme.description) payload.description = theme.description;
   if (theme.light) payload.light = { ...theme.light };
   if (theme.dark) payload.dark = { ...theme.dark };
+  if (theme.syntax) payload.syntax = { ...theme.syntax };
   return JSON.stringify(payload, null, 2);
 }
🤖 Copy this AI Prompt to have your agent fix this:
In file @apps/web/src/themes/transport.ts around lines 27-33:

`serializeTheme` omits the `syntax` field from the serialized payload, so exporting a theme and then importing it silently drops the Shiki syntax-highlighting configuration. Themes like Solarized, Nord, Catppuccin, and Rosé Pine define palette-specific `syntax` themes, but after a round-trip through `serializeTheme`/`parseTheme` that data is lost and they fall back to the generic syntax theme. The function copies `description`, `light`, and `dark` but never copies `syntax`. Consider including `theme.syntax` in the payload when present.

const hasInvalidValue = Object.values(variantTokens).some(
(value) => typeof value === "string" && value.length > 0 && !isValidColorValue(value),
);
const saveDisabled = jsonError !== null || draft.name.trim().length === 0 || hasInvalidValue;

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.

🟡 Medium settings/ThemeEditor.tsx:209

The JSON editor allows the user to change draft.id to the id of an existing theme, and saveDisabled never detects that collision. When the user clicks Save with a duplicate id, updateCustomTheme throws Theme id ... already exists and onSave (in AppearanceSettings) does not catch it, so the editor throws an uncaught runtime error instead of staying open with validation feedback. Consider including id-collision detection in saveDisabled so Save is blocked (or shows an inline error) when the new id already belongs to another theme.

Suggested change
const saveDisabled = jsonError !== null || draft.name.trim().length === 0 || hasInvalidValue;
const saveDisabled = jsonError !== null || draft.name.trim().length === 0 || hasInvalidValue || (draft.id !== source.id && findTheme(draft.id) != null);
Also found in 1 other location(s)

apps/web/src/components/settings/AppearanceSettings.tsx:103

handleSave calls updateCustomTheme without handling its duplicate-ID error. In the exposed JSON editor, a user can change a custom theme's id to any existing built-in or custom ID; the draft remains schema-valid and Save stays enabled, but clicking Save throws from updateCustomTheme with no toast or validation feedback, leaving the save action broken.

🤖 Copy this AI Prompt to have your agent fix this:
In file @apps/web/src/components/settings/ThemeEditor.tsx around line 209:

The JSON editor allows the user to change `draft.id` to the id of an existing theme, and `saveDisabled` never detects that collision. When the user clicks Save with a duplicate id, `updateCustomTheme` throws `Theme id ... already exists` and `onSave` (in `AppearanceSettings`) does not catch it, so the editor throws an uncaught runtime error instead of staying open with validation feedback. Consider including id-collision detection in `saveDisabled` so Save is blocked (or shows an inline error) when the new id already belongs to another theme.

Also found in 1 other location(s):
- apps/web/src/components/settings/AppearanceSettings.tsx:103 -- `handleSave` calls `updateCustomTheme` without handling its duplicate-ID error. In the exposed JSON editor, a user can change a custom theme's `id` to any existing built-in or custom ID; the draft remains schema-valid and Save stays enabled, but clicking Save throws from `updateCustomTheme` with no toast or validation feedback, leaving the save action broken.

{ cause: error },
);
}
if (!isValidTheme(parsed)) {

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.

🟡 Medium themes/transport.ts:45

parseTheme accepts a theme with a non-string description (e.g. {}) because isValidTheme never validates the type of description. The truthy object is stored on the theme at next.description = parsed.description, and when the settings page later renders entry.description as a React child, it throws on object values instead of rejecting the malformed import. Add a typeof candidate.description === "string" check (or otherwise guard description) before trusting it.

🤖 Copy this AI Prompt to have your agent fix this:
In file @apps/web/src/themes/transport.ts around line 45:

`parseTheme` accepts a theme with a non-string `description` (e.g. `{}`) because `isValidTheme` never validates the type of `description`. The truthy object is stored on the theme at `next.description = parsed.description`, and when the settings page later renders `entry.description` as a React child, it throws on object values instead of rejecting the malformed import. Add a `typeof candidate.description === "string"` check (or otherwise guard `description`) before trusting it.

const canGoBack = useCanGoBack();
const [restoreSignal, setRestoreSignal] = useState(0);
const showRestoreDefaults = location.pathname === "/settings/general";
const showRestoreDefaults =

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.

🟡 Medium routes/settings.tsx:40

The RestoreDefaultsButton shown on /settings/appearance is never enabled when the user only changes light/dark palette selections, and when enabled for other dirty settings it still leaves palette selections unrestored. useSettingsRestore only tracks theme and legacy general settings as dirty and only resets those values; activeLightThemeId and activeDarkThemeId are neither checked nor reset. As a result, changing a palette while leaving color mode unchanged leaves the button disabled, and restoring defaults from the Appearance page keeps the non-default palettes applied. Consider including activeLightThemeId and activeDarkThemeId in both the dirty check and the restore logic in useSettingsRestore.

🤖 Copy this AI Prompt to have your agent fix this:
In file @apps/web/src/routes/settings.tsx around line 40:

The `RestoreDefaultsButton` shown on `/settings/appearance` is never enabled when the user only changes light/dark palette selections, and when enabled for other dirty settings it still leaves palette selections unrestored. `useSettingsRestore` only tracks `theme` and legacy general settings as dirty and only resets those values; `activeLightThemeId` and `activeDarkThemeId` are neither checked nor reset. As a result, changing a palette while leaving color mode unchanged leaves the button disabled, and restoring defaults from the Appearance page keeps the non-default palettes applied. Consider including `activeLightThemeId` and `activeDarkThemeId` in both the dirty check and the restore logic in `useSettingsRestore`.

const isDark = theme === "dark" || (theme === "system" && systemDark);
document.documentElement.classList.toggle("dark", isDark);
lastAppliedTheme = { theme, systemDark };
restoreActiveThemes();

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.

🟠 High hooks/useTheme.ts:206

Calling restoreActiveThemes() inside applyTheme reintroduces an unguarded localStorage read: restoreActiveThemes calls getActiveLightThemeId/getActiveDarkThemeId, which read storage without try/catch. When storage access throws (e.g., disabled cookies/private mode — the exact case getStored()/readThemePreference already handle), applyTheme throws instead of falling back to the default theme, so the module-load applyTheme(getStored()) call crashes and the theme hook fails to initialize. Wrap the restoreActiveThemes() call in a try/catch (or guard it in the registry) so storage errors fall back gracefully like the rest of this module.

🤖 Copy this AI Prompt to have your agent fix this:
In file @apps/web/src/hooks/useTheme.ts around line 206:

Calling `restoreActiveThemes()` inside `applyTheme` reintroduces an unguarded `localStorage` read: `restoreActiveThemes` calls `getActiveLightThemeId`/`getActiveDarkThemeId`, which read storage without `try/catch`. When storage access throws (e.g., disabled cookies/private mode — the exact case `getStored()`/`readThemePreference` already handle), `applyTheme` throws instead of falling back to the default theme, so the module-load `applyTheme(getStored())` call crashes and the theme hook fails to initialize. Wrap the `restoreActiveThemes()` call in a `try/catch` (or guard it in the registry) so storage errors fall back gracefully like the rest of this module.

@macroscopeapp

macroscopeapp Bot commented Jul 12, 2026

Copy link
Copy Markdown
Contributor

Approvability

Verdict: Needs human review

9 blocking correctness issues found. This PR introduces a substantial new theming system feature with ~2500+ lines of new code, plus model alias changes. Multiple unresolved review comments identify HIGH severity issues including potential CSS injection vulnerabilities, localStorage crash paths, and breaking behavior for older Claude Code versions. The scope and open issues warrant human review.

You can customize Macroscope's approvability policy. Learn more.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size:XXL 1,000+ changed lines (additions + deletions). vouch:unvouched PR author is not yet trusted in the VOUCHED list.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant