Skip to content

TypeScript-first theme tokens: replace Rust CSS parser with client-side token system#7218

Closed
aharvard wants to merge 3 commits intomcp-apps-stylingfrom
aharvard/ts-theme-tokens
Closed

TypeScript-first theme tokens: replace Rust CSS parser with client-side token system#7218
aharvard wants to merge 3 commits intomcp-apps-stylingfrom
aharvard/ts-theme-tokens

Conversation

@aharvard
Copy link
Copy Markdown
Collaborator

@aharvard aharvard commented Feb 13, 2026

Summary

This PR replaces the server-side Rust CSS parser (theme_css.rs) with a TypeScript-first theme token system. Instead of parsing and serving CSS from the backend, theme tokens are now defined, built, and applied entirely on the client via theme-tokens.ts.

What changed

Removed (server-side)

  • theme_css.rs — Rust CSS variable parser
  • /config/theme and /config/theme/variables API endpoints
  • regex dependency from goose-server

Added (client-side)

  • ui/desktop/src/theme/theme-tokens.ts — single source of truth for all design tokens (colors, typography, borders, shadows) with light/dark variants
  • applyThemeTokens() — applies tokens as CSS custom properties via style.setProperty() on :root
  • ThemeColorEditor dev tool component for live token editing
  • tokenVersion state in ThemeContext to support localStorage overrides and force re-application

Restructured

  • main.css reorganized into numbered sections:
    1. Tailwind base
    2. Token registration (@theme inline)
    3. Primitives (color palette)
    4. UI aliases (light/dark semantic mappings)
    5. CSS baseline (scrollbar, transitions, etc.)

Migrated legacy class names

  • bg-background-defaultbg-background-primary
  • border-border-defaultborder-border-primary
  • border-border-strongborder-border-secondary
  • bg-background-mutedbg-background-secondary
  • text-text-mutedtext-text-secondary
  • Removed dead accent CSS aliases (no TSX consumers)

Why

The Rust CSS parser was fragile and added unnecessary server round-trips for what is fundamentally a client concern. Moving tokens to TypeScript:

  • Makes them inspectable and overridable at runtime (e.g., MCP host style injection)
  • Eliminates the server dependency for theming
  • Aligns with the MCP Apps styling spec where hosts provide CSS variables directly

⚠️ Known CSS bugs (WIP)

There are a few visual regressions introduced by the main.css refactor that I still need to work through. The token values and/or section 4 alias mappings may need tuning.

image

I will follow up with fixes for these before this is ready to merge.

Move theme token management from server-side Rust (theme_css.rs) to a
TypeScript module (theme-tokens.ts) that serves as the single source of
truth for both Goose desktop styling and MCP app host styles.

Key changes:

Server (goose-server):
- Remove theme_css.rs Rust CSS parser
- Remove /theme/variables and /config/theme API endpoints
- Remove openapi entries for theme endpoints

Theme system (ui/desktop):
- Add theme-tokens.ts with typed light/dark token maps keyed by
  McpUiStyleVariableKey, enforced at compile time
- Add applyThemeTokens() and buildMcpHostStyles() helpers
- ThemeContext now applies tokens via style.setProperty() instead of
  fetching from the server and injecting a <style> tag
- Support localStorage theme-overrides with tokenVersion reactivity
- Expose mcpHostStyles and refreshTokens from ThemeContext
- Pass mcpHostStyles to McpAppRenderer for MCP app theming
- Add tokensUpdated flag to cross-window theme broadcasts

CSS (main.css):
- Restructure into numbered sections (primitives, MCP token registration,
  Goose aliases, light baseline, dark baseline, components)
- Register all McpUiStyleVariableKey tokens in @theme inline for Tailwind
  utility class generation
- Add full light (:root) and dark (.dark) baseline values
- Add scrollbar, sidebar, and reduced-motion styles

Also adds:
- ThemeColorEditor component for live token editing (dev tool)
- PROPOSAL-theme-tokens.md design doc
The accent token aliases (background-accent, border-accent, text-accent,
text-on-accent) were bridging the old class names to the new inverse
tokens. No TSX/TS files reference these utility classes — components
already use the MCP-spec names (bg-background-inverse, text-text-inverse,
etc.) directly. Remove the dead aliases to keep section 4 clean.
Replace old Tailwind utility classes with their MCP-compliant counterparts:
- bg-background-default → bg-background-primary
- border-border-default → border-border-primary
- border-border-strong → border-border-secondary
- bg-background-muted → bg-background-secondary
- text-text-muted → text-text-secondary

These old names had no @theme inline registration, so they resolved
to nothing — causing transparent backgrounds and missing borders
in both light and dark mode (e.g., chat input, speech bubbles,
MCP app containers, recipe form fields).
Copilot AI review requested due to automatic review settings February 13, 2026 22:18
@aharvard
Copy link
Copy Markdown
Collaborator Author

aharvard commented Feb 13, 2026

  • fix main.css style bugs
  • remove PROPOSAL-theme-tokens.md
  • remove ui/desktop/src/components/settings/app/ThemeColorEditor.tsx (this is for my local testing)

@aharvard aharvard requested a review from DOsinga February 13, 2026 22:21
@aharvard
Copy link
Copy Markdown
Collaborator Author

I'm not confident that some of my class name changes that I applied on the JSX side of things are needed. That was kind of some rapid troubleshooting that I might end up reverting. My goal is to clean up the main.css file in such a way that makes it easy to maintain and extend.

@aharvard aharvard marked this pull request as draft February 13, 2026 22:25
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

This PR migrates the theming system from a server-side Rust CSS parser to a TypeScript-first architecture. The change eliminates fragile regex-based CSS parsing and API round-trips, replacing them with type-safe token definitions in theme-tokens.ts that are applied directly to the DOM at runtime. Theme customizations now use localStorage instead of server-side file storage, and MCP apps receive theme variables via the hostContext.styles object using the light-dark() CSS format.

Changes:

  • Removed server-side Rust CSS parser (theme_css.rs) and theme API endpoints (/theme/variables, /theme/save)
  • Added theme-tokens.ts as single source of truth with compiler-enforced type safety via McpUiStyleVariableKey
  • Implemented ThemeColorEditor component for live token editing with localStorage-based persistence
  • Restructured main.css into clearly labeled sections (primitives, MCP token registration, app aliases, baselines)
  • Migrated legacy class names (bg-background-defaultbg-background-primary, etc.)
  • Added cross-window token synchronization via Electron IPC

Reviewed changes

Copilot reviewed 21 out of 21 changed files in this pull request and generated 1 comment.

Show a summary per file
File Description
ui/desktop/src/theme/theme-tokens.ts New source of truth for all theme tokens with light/dark variants and MCP host styles builder
ui/desktop/src/styles/main.css Restructured with clear sections: primitives, token registration, app aliases, baselines
ui/desktop/src/contexts/ThemeContext.tsx Removed API fetch logic, added localStorage override support and token refresh mechanism
ui/desktop/src/components/settings/app/ThemeColorEditor.tsx New interactive theme editor with live preview and localStorage persistence
ui/desktop/src/components/McpApps/McpAppRenderer.tsx Integrated mcpHostStyles from ThemeContext into host context
ui/desktop/src/preload.ts Added tokensUpdated flag to broadcastThemeChange IPC payload
ui/desktop/src/api/*.ts Removed theme API types and functions (getThemeVariables, saveTheme)
ui/desktop/openapi.json Removed /theme/variables and /theme/save endpoints
crates/goose-server/src/theme_css.rs Deleted entire Rust CSS parser module
crates/goose-server/src/routes/config_management.rs Removed theme API route handlers
crates/goose-server/src/openapi.rs Removed theme API OpenAPI definitions
crates/goose-server/src/main.rs Removed theme_css module declaration
crates/goose-server/src/lib.rs Removed theme_css exports
Various TSX files Updated class names from legacy to MCP-compliant semantic tokens
Comments suppressed due to low confidence (1)

ui/desktop/src/components/McpApps/McpAppRenderer.tsx:460

  • The hostContext useMemo uses mcpHostStyles but doesn't list it in the dependency array. While mcpHostStyles is built once at module level and never changes, it's best practice to include all referenced values in the dependency array for clarity and to prevent future bugs if the implementation changes.
  const hostContext = useMemo((): McpUiHostContext => {
    const context: McpUiHostContext = {
      // todo: toolInfo: {}
      theme: resolvedTheme,
      styles: mcpHostStyles,
      // 'standalone' is a Goose-specific display mode (dedicated Electron window)
      // that maps to the spec's inline | fullscreen | pip modes.
      displayMode: displayMode as McpUiDisplayMode,
      availableDisplayModes:
        displayMode === 'standalone' ? [displayMode as McpUiDisplayMode] : AVAILABLE_DISPLAY_MODES,
      // todo: containerDimensions: {} (depends on displayMode)
      locale: navigator.language,
      timeZone: Intl.DateTimeFormat().resolvedOptions().timeZone,
      userAgent: navigator.userAgent,
      platform: 'desktop',
      deviceCapabilities: {
        touch: navigator.maxTouchPoints > 0,
        hover: window.matchMedia('(hover: hover)').matches,
      },
      safeAreaInsets: {
        top: 0,
        right: 0,
        bottom: 0,
        left: 0,
      },
    };

    return context;
  }, [resolvedTheme, displayMode]);

Comment on lines +129 to +132
// Revert live preview
applyThemeTokens(resolvedTheme);
onClose();
}, [resolvedTheme, onClose]);
Copy link

Copilot AI Feb 13, 2026

Choose a reason for hiding this comment

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

The handleCancel function applies default tokens but doesn't restore previously saved localStorage overrides. This could cause a brief flash where saved custom colors revert to defaults before ThemeContext's useEffect reapplies them. Consider reading and applying the saved overrides directly here instead of calling applyThemeTokens.

Suggested change
// Revert live preview
applyThemeTokens(resolvedTheme);
onClose();
}, [resolvedTheme, onClose]);
// Revert live preview to the currently persisted theme tokens (including overrides)
refreshTokens();
onClose();
}, [refreshTokens, onClose]);

Copilot uses AI. Check for mistakes.
aharvard added a commit that referenced this pull request Feb 17, 2026
Combines the TypeScript-first token architecture (PR #7218) with
Spencer's theme picker UX (PR #7216), fully client-side:

- 10 built-in theme presets (Nord, Dracula, Solarized, etc.)
- Preset gallery with tag filtering and one-click apply
- Custom color editor with live preview of real UI components
- Custom themes saved to localStorage (no server round-trips)
- Instant theme switching via style.setProperty() (no page reload)
- ThemeContext gains applyPreset() and activePresetId

Removed:
- regex dependency from goose-server (was for deleted Rust CSS parser)
- PROPOSAL-theme-tokens.md design doc
- Old single-file ThemeColorEditor.tsx (replaced by directory)
@aharvard
Copy link
Copy Markdown
Collaborator Author

Closing in favor of a cleaner two-part PR strategy:

#7275 — theme-tokens — theme token infrastructure + CSS class rename
#7276 — theme-builder — preset gallery & custom color editor

The token system, main.css restructure, and class renames from this PR are all in #7275 with cleaner commit history.

@aharvard aharvard closed this Feb 17, 2026
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.

2 participants