# How Workspace Themes Are Applied and Cascaded in Craft Agents

> Discover how Craft Agents applies workspace themes in a cascading hierarchy. Learn how app-wide defaults and overrides work, with real time synchronization across windows.

- Repository: [Craft Ai Agents/craft-agents-oss](https://github.com/craft-ai-agents/craft-agents-oss)
- Tags: internals
- Published: 2026-07-06

---

**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`](https://github.com/craft-ai-agents/craft-agents-oss/blob/main/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`](https://github.com/craft-ai-agents/craft-agents-oss/blob/main/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`](https://github.com/craft-ai-agents/craft-agents-oss/blob/main/packages/shared/src/protocol/channels.ts).

## Loading Overrides at Bootstrap

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

```typescript
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`](https://github.com/craft-ai-agents/craft-agents-oss/blob/main/AppearanceSettingsPage.tsx) combines the loaded overrides with the available preset themes:

```typescript
<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:

```typescript
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`](https://github.com/craft-ai-agents/craft-agents-oss/blob/main/packages/ui/src/context/ShikiThemeContext.tsx) retrieves the resolved Shiki syntax-highlighting theme:

```typescript
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`](https://github.com/craft-ai-agents/craft-agents-oss/blob/main/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`](https://github.com/craft-ai-agents/craft-agents-oss/blob/main/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`.