How Workspace Themes Are Applied and Cascaded in Craft Agents

Craft Agents applies workspace themes through a hierarchical cascade where workspace-specific overrides take precedence over app-wide defaults, with changes persisted via Electron IPC and synchronized across renderer windows in real-time.

The craft-ai-agents/craft-agents-oss repository implements a hierarchical theme management system that allows individual workspaces to override the global application appearance while maintaining consistency across the Electron-based desktop environment. Each workspace maintains its own color theme preferences, which are resolved through a specific cascade algorithm and persisted to disk for persistence across application restarts.

Theme State Architecture

The theme system centers on the ThemeContext provider located in packages/ui/src/context/ThemeContext.tsx. This context manages the global state for theming, including the current mode (system | light | dark), the selected color theme, UI font settings, and the currently active workspace ID.

The context exposes critical state mutators including setMode, setColorTheme, setFont, and specifically setWorkspaceColorTheme for handling workspace-specific overrides. It also provides diagnostic properties such as themeLoadError and themeResolvedFrom to help trace the origin of the currently applied theme.

The Cascade Resolution Logic

The cascade follows a strict priority order to resolve the effective theme for any given workspace:

  1. Workspace-specific theme — If the active workspace has a stored override in the workspaceThemes map, that theme ID is applied immediately.
  2. App-wide default theme — If no workspace-specific entry exists, the system falls back to the global colorTheme value.
  3. System default — If the global colorTheme is set to 'default', the resolver ultimately falls back to the system color scheme ('light' or 'dark').

This resolution happens inside the ThemeContext provider, which maintains the workspaceThemes map populated at bootstrap via window.electronAPI.getAllWorkspaceThemes.

Persisting Workspace Theme Overrides

When users select a theme for a specific workspace through the AppearanceSettingsPage at apps/electron/src/renderer/pages/settings/AppearanceSettingsPage.tsx, the application distinguishes between immediate and deferred updates.

Active Workspace Updates

If the user modifies the theme for the currently visible workspace (matching activeWorkspaceId), the UI calls setWorkspaceColorTheme directly on the context. This updates the in-memory state instantly, providing immediate visual feedback without requiring IPC round-trips.

Cross-Workspace Persistence

For non-active workspaces, the handleWorkspaceThemeChange function sends the change via Electron IPC using window.electronAPI.setWorkspaceColorTheme. The main process stores the configuration on disk under ~/.craft-agent/workspaces/<id>/config.json and broadcasts the change to all renderer windows via the WORKSPACE_THEME_CHANGED channel defined in packages/shared/src/protocol/channels.ts.

Loading Overrides at Bootstrap

At application startup, the renderer process requests all stored workspace themes:

useEffect(() => {
  const loadWorkspaceThemes = async () => {
    if (!window.electronAPI?.getAllWorkspaceThemes) return;
    const themes = await window.electronAPI.getAllWorkspaceThemes();
    setWorkspaceThemes(themes);
  };
  loadWorkspaceThemes();
}, []);

This populates the local workspaceThemes state, ensuring the settings page displays the correct dropdown values for each workspace before user interaction.

Implementing the Theme Selector

The workspace-specific theme selector in AppearanceSettingsPage.tsx combines the loaded overrides with the available preset themes:

<SettingsMenuSelect
  value={hasCustomTheme ? wsTheme : 'default'}
  onValueChange={(value) => handleWorkspaceThemeChange(workspace.id, value)}
  options={[
    { 
      value: 'default', 
      label: appDefaultLabel
        ? t("settings.appearance.useDefaultWithTheme", { theme: appDefaultLabel })
        : t("settings.appearance.useDefault") 
    },
    ...presetThemes
      .filter(t => t.id !== 'default')
      .map(t => ({ value: t.id, label: t.theme.name || t.id })),
  ]}
/>

The change handler distinguishes between immediate and persisted updates:

const handleWorkspaceThemeChange = useCallback(
  async (workspaceId: string, value: string) => {
    const themeId = value === 'default' ? null : value;

    // Immediate update for the currently active workspace
    if (workspaceId === activeWorkspaceId) {
      setWorkspaceColorTheme(themeId);
    } else {
      // Persist for other workspaces via IPC
      await window.electronAPI?.setWorkspaceColorTheme?.(workspaceId, themeId);
    }

    // Keep UI in sync
    setWorkspaceThemes(prev => ({
      ...prev,
      [workspaceId]: themeId ?? undefined,
    }));
  },
  [activeWorkspaceId, setWorkspaceColorTheme],
);

Consuming Resolved Themes in Components

Components access the effective theme through the useTheme hook. For example, the CodeBlock component in packages/ui/src/context/ShikiThemeContext.tsx retrieves the resolved Shiki syntax-highlighting theme:

const { shikiTheme } = useTheme();   // gets the effective theme after cascade

This ensures that code blocks render with the correct theme regardless of whether the workspace uses a specific override or falls back to the application default.

Summary

  • Workspace themes override app defaults — The ThemeContext resolver checks the workspaceThemes map first, falling back to the global colorTheme only when no workspace-specific value exists.
  • Immediate updates for active workspaces — Changes to the currently visible workspace apply instantly via setWorkspaceColorTheme, while other workspaces persist via window.electronAPI.setWorkspaceColorTheme.
  • IPC synchronization — The WORKSPACE_THEME_CHANGED channel broadcasts updates across all renderer windows, ensuring consistency in multi-window scenarios.
  • Disk persistence — Theme overrides are stored in individual workspace configuration files at ~/.craft-agent/workspaces/<id>/config.json.
  • Component integration — The useTheme hook provides the resolved effective theme to all UI components, including specialized contexts like ShikiThemeContext for syntax highlighting.

Frequently Asked Questions

How do I set a different theme for each workspace?

Navigate to the appearance settings in apps/electron/src/renderer/pages/settings/AppearanceSettingsPage.tsx and select a theme from the dropdown associated with each workspace. The application stores this preference separately from the global theme, and the ThemeContext provider automatically applies the workspace-specific value when that workspace becomes active.

What happens if I delete a workspace theme override?

Setting a workspace theme to 'default' (or null) removes the specific override. The cascade logic immediately falls back to the app-wide colorTheme value, and if that is also 'default', the system ultimately uses the operating system's color scheme preference.

How does the application handle theme changes across multiple windows?

When you change a theme for a non-active workspace, the main process emits the WORKSPACE_THEME_CHANGED event on the IPC channel defined in packages/shared/src/protocol/channels.ts. All renderer windows listen for this broadcast and update their internal workspaceThemes maps accordingly, ensuring synchronized state across the entire application.

Where are workspace theme preferences stored?

Workspace theme overrides are persisted on disk at ~/.craft-agent/workspaces/<id>/config.json by the main Electron process. The renderer retrieves these values at startup via window.electronAPI.getAllWorkspaceThemes and maintains them in the React state managed by ThemeContext.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →