fix(web): theme terminal foreground and restore backdrop plugin slot

Make Nous Blue terminal text readable without the inversion layer, re-mount
the backdrop plugin slot, and drop unused backdrop CSS vars from theme apply.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Austin Pickett 2026-06-29 08:46:37 -04:00 committed by Teknium
parent 57d98ebed7
commit 10374bb7a2
7 changed files with 44 additions and 39 deletions

View file

@ -486,6 +486,13 @@ export default function App() {
>
<SelectionSwitcher />
<div
aria-hidden
className="pointer-events-none fixed inset-0 z-0"
>
<PluginSlot name="backdrop" />
</div>
<header
className={cn(
"lg:hidden fixed top-0 left-0 right-0 z-40 min-h-14",

View file

@ -317,12 +317,6 @@ function FontSection({ fontChoices, fontId, setFont }: FontSectionProps) {
}
function ThemeSwatch({ theme }: { theme: DashboardTheme }) {
// Inverted themes (Nous Blue / future lens themes) author their palette
// pre-inversion — `#FFAC02` reads as `#0053FD` blue once the foreground-
// difference layer flips the page. The picker can't replay that math
// cheaply, so themes opt-in to an explicit `swatchColors` triplet that
// mirrors the on-screen result. Falls back to the raw palette hexes for
// every other theme so existing dark-theme swatches are untouched.
const [c1, c2, c3] = theme.swatchColors ?? [
theme.palette.background.hex,
theme.palette.midground.hex,

View file

@ -58,14 +58,19 @@ function generateChannelId(scope?: string): string {
// with cream foreground — we intentionally don't pick monokai or a loud
// theme, because the TUI's skin engine already paints the content; the
// terminal chrome just needs to sit quietly inside the dashboard.
// `background` is omitted here — it's supplied dynamically from the active
// theme's `terminalBackground` field so users can control it via YAML themes.
const TERMINAL_THEME_STATIC = {
foreground: "#f0e6d2",
cursor: "#f0e6d2",
cursorAccent: "#0d2626",
selectionBackground: "#f0e6d244",
};
const DEFAULT_TERMINAL_BACKGROUND = "#000000";
const DEFAULT_TERMINAL_FOREGROUND = "#f0e6d2";
function buildTerminalTheme(background: string, foreground: string) {
return {
background,
foreground,
cursor: foreground,
cursorAccent: background,
selectionBackground:
foreground.length === 7 ? `${foreground}44` : foreground,
};
}
/**
* CSS width for xterm font tiers.
@ -193,10 +198,11 @@ export default function ChatPage({ isActive = true }: { isActive?: boolean }) {
);
const { theme } = useTheme();
const terminalBg = theme.terminalBackground ?? "#000000";
const terminalBg = theme.terminalBackground ?? DEFAULT_TERMINAL_BACKGROUND;
const terminalFg = theme.terminalForeground ?? DEFAULT_TERMINAL_FOREGROUND;
const terminalTheme = useMemo(
() => ({ ...TERMINAL_THEME_STATIC, background: terminalBg }),
[terminalBg],
() => buildTerminalTheme(terminalBg, terminalFg),
[terminalBg, terminalFg],
);
// The dashboard keeps ChatPage mounted persistently so the PTY survives tab
@ -897,12 +903,12 @@ export default function ChatPage({ isActive = true }: { isActive?: boolean }) {
}, [isActive]);
// Keep the live xterm theme in sync when the active theme's terminal
// background changes (e.g. user switches to a custom YAML theme mid-session).
// colors change (e.g. user switches to a custom YAML theme mid-session).
useEffect(() => {
const term = termRef.current;
if (!term) return;
term.options.theme = { ...TERMINAL_THEME_STATIC, background: terminalBg };
}, [terminalBg]);
term.options.theme = terminalTheme;
}, [terminalTheme]);
// Layout:
// outer flex column — sits inside the dashboard's content area
@ -1065,7 +1071,7 @@ export default function ChatPage({ isActive = true }: { isActive?: boolean }) {
"bottom-2 right-2 px-2 py-1 text-xs sm:bottom-3 sm:right-3 sm:px-2.5 sm:py-1.5",
"lg:bottom-4 lg:right-4",
)}
style={{ color: TERMINAL_THEME_STATIC.foreground }}
style={{ color: terminalFg }}
>
<span className="inline-flex items-center gap-1.5">
<Copy className="h-3 w-3 shrink-0" />

View file

@ -19,7 +19,8 @@ import React, { Fragment, useEffect, useState } from "react";
* these in their manifest's `slots` field get wired in automatically.
*
* Shell-wide slots:
* - `backdrop` optional full-viewport background decoration
* - `backdrop` optional full-viewport background decoration;
* mounted behind shell chrome at z-0
* - `header-left` injected before the Hermes brand in the top bar
* - `header-right` injected before the theme/language switchers
* - `header-banner` injected below the top nav bar, full-width

View file

@ -82,8 +82,6 @@ function paletteVars(palette: ThemePalette): Record<string, string> {
...layerVars("background", palette.background),
...layerVars("midground", palette.midground),
...layerVars("foreground", palette.foreground),
"--warm-glow": palette.warmGlow,
"--noise-opacity-mul": String(palette.noiseOpacity),
};
}
@ -391,11 +389,15 @@ function applyTheme(theme: DashboardTheme) {
applyCustomCSS(theme.customCSS);
applyLayoutVariant(theme.layoutVariant);
// Terminal background — read by ChatPage via useTheme(); also available as CSS var.
// Terminal colors — read by ChatPage via useTheme(); also available as CSS vars.
root.style.setProperty(
"--theme-terminal-background",
theme.terminalBackground ?? "#000000",
);
root.style.setProperty(
"--theme-terminal-foreground",
theme.terminalForeground ?? "#f0e6d2",
);
// Re-assert the font override last: theme application just rewrote
// --theme-font-sans/-display, so an active override has to win again.

View file

@ -199,6 +199,7 @@ export const nousBlueTheme: DashboardTheme = {
typography: DEFAULT_TYPOGRAPHY,
layout: DEFAULT_LAYOUT,
terminalBackground: "#f5f8fc",
terminalForeground: "#170d02",
seriesColors: {
inputTokenAccent: "#001934",
outputTokenAccent: "#0053fd",

View file

@ -80,7 +80,8 @@ export type ThemeLayoutVariant = "standard" | "cockpit" | "tiled";
* emitted as a CSS var (`--theme-asset-<name>`). Plugin slots and
* shell chrome may consume these via CSS. */
export interface ThemeAssets {
/** Full-viewport background image URL. */
/** Full-viewport background image URL. Exposed as `--theme-asset-bg` for
* the `backdrop` plugin slot or theme `customCSS`. */
bg?: string;
/** Hero render (Gundam, mascot, wallpaper) — for plugin sidebars/overlays. */
hero?: string;
@ -120,13 +121,7 @@ export interface ThemeComponentStyles {
* `--series-input-token` / `--series-output-token` CSS vars consumed
* inline by pages that render input-vs-output token flows. Themes can
* omit either field to inherit the default token defined in
* `index.css` (Hermes-teal `#ffe6cb` for input, `#34d399` for output).
*
* Inverted-lens themes (e.g. Nous Blue) must pre-invert these hex
* values so they read as their intended visual color after the FG
* difference layer flips them (`out = 255 channel`). E.g. to make
* output paint as Nous-blue `#0053FD` on screen, set
* `outputTokenAccent: "#FFAC02"` the difference math reverses it. */
* `index.css` (Hermes-teal `#ffe6cb` for input, `#34d399` for output). */
export interface ThemeSeriesColors {
/** Input-tokens series accent (Analytics chart bars + table values). */
inputTokenAccent?: string;
@ -177,18 +172,17 @@ export interface DashboardTheme {
/** Per-component CSS-var overrides. See `ThemeComponentStyles`. */
componentStyles?: ThemeComponentStyles;
colorOverrides?: ThemeColorOverrides;
/** Data-series accent colors for Analytics/Models token charts.
* See `ThemeSeriesColors` for inversion-aware values. */
/** Data-series accent colors for Analytics/Models token charts. */
seriesColors?: ThemeSeriesColors;
/** Explicit 3-color swatch override for the theme picker. Use when the
* palette's raw hex values don't reflect what users see on screen
* e.g. inverted "lens" themes whose foreground-difference layer flips
* the authored colors to their visual complements. Order matches the
/** Explicit 3-color swatch override for the theme picker. Order matches the
* default swatch cells: [background, midground, warmGlow]. */
swatchColors?: [string, string, string];
/** Background color for the embedded terminal pane (xterm.js).
* Hex string. Defaults to `"#000000"` when absent. */
terminalBackground?: string;
/** Default text/cursor color for the embedded terminal pane (xterm.js).
* Hex string. Defaults to `"#f0e6d2"` when absent. */
terminalForeground?: string;
}
/**