# How the Craft-Agents Theme System and Cascading Configuration Work

> Understand the Craft-Agents theme system and cascading configuration. Explore how layered architecture, preset themes, and global overrides create final CSS variables for ultimate control.

- Repository: [Craft Docs/craft-agents-oss](https://github.com/lukilabs/craft-agents-oss)
- Tags: deep-dive
- Published: 2026-04-18

---

**Craft-Agents uses a layered architecture where preset themes provide base colors, workspace-specific IDs override the global selection, and app-level user tweaks in [`theme.json`](https://github.com/lukilabs/craft-agents-oss/blob/main/theme.json) are merged last to produce the final CSS variables.**

The `lukilabs/craft-agents-oss` repository implements a sophisticated **Craft-Agents theme system and cascading configuration** that separates visual styling from behavioral defaults. Both systems follow a strict precedence model where values defined at more specific layers override those from general layers, ensuring that workspace-level preferences take priority over global defaults while user color overrides always apply last.

## Theme System Architecture

The theme system distributes configuration across five distinct layers, ranging from bundled preset files to runtime user overrides.

### Preset Themes and Bundled Assets

Complete color themes ship as JSON files (e.g., [`dracula.json`](https://github.com/lukilabs/craft-agents-oss/blob/main/dracula.json), [`nord.json`](https://github.com/lukilabs/craft-agents-oss/blob/main/nord.json)) bundled within `apps/electron/resources/themes/`. When the application launches, `ensurePresetThemes()` in [`src/packages/shared/src/config/storage.ts`](https://github.com/lukilabs/craft-agents-oss/blob/main/src/packages/shared/src/config/storage.ts) (lines 1015‑1045) synchronizes these bundled assets to the user directory at `~/.craft-agent/themes/`, preserving any user edits unless the file is corrupted.

```typescript
export function ensurePresetThemes(): void {
  if (presetsInitialized) return;
  presetsInitialized = true;
  const themesDir = getAppThemesDir();
  if (!existsSync(themesDir)) mkdirSync(themesDir, { recursive: true });
  const bundledThemesDir = getBundledAssetsDir('themes');
  // copy each *.json preserving custom files
}

```

The system validates preset integrity using `isValidThemeFile()` from [`src/packages/shared/src/config/validators.ts`](https://github.com/lukilabs/craft-agents-oss/blob/main/src/packages/shared/src/config/validators.ts) before loading.

### App-Level Theme Overrides

Users define granular color overrides and visual modes in `~/.craft-agent/theme.json`. This file supports **semantic colours** (`background`, `foreground`, `accent`), **surface colours** (`paper`, `navigator`, `input`, `popover`), **mode** selection (`solid` or `scenic`), and optional `backgroundImage` paths.

The function `loadAppTheme()` (lines 727‑779 in [`src/packages/shared/src/config/storage.ts`](https://github.com/lukilabs/craft-agents-oss/blob/main/src/packages/shared/src/config/storage.ts)) handles persistence:

```typescript
export function loadAppTheme(): ThemeOverrides | undefined {
  const themePath = getAppThemePath();
  if (!existsSync(themePath)) return undefined;
  // reads and parses theme.json
}

```

### Workspace-Specific Theme Selection

Workspaces can declare a specific preset ID in their local [`config.json`](https://github.com/lukilabs/craft-agents-oss/blob/main/config.json) under `defaults.colorTheme`. This allows different projects to use different base palettes without changing the global application preference.

The workspace API exposes:
- `getWorkspaceColorTheme(rootPath)` (lines 429‑441 in [`src/packages/shared/src/workspaces/storage.ts`](https://github.com/lukilabs/craft-agents-oss/blob/main/src/packages/shared/src/workspaces/storage.ts))
- `setWorkspaceColorTheme(rootPath, themeId)` (lines 442‑466)

If a workspace omits `colorTheme`, the system falls back to the global selection via `getColorTheme()` (lines 1224‑1241 in [`src/packages/shared/src/config/storage.ts`](https://github.com/lukilabs/craft-agents-oss/blob/main/src/packages/shared/src/config/storage.ts)).

## Cascading Configuration Resolution

The merging logic implements a **deep-merge strategy** where specificity determines the winner.

### The Resolution Stack

When the UI requests the final theme, the resolution follows this precedence (highest to lowest):

1. **App-level overrides** ([`theme.json`](https://github.com/lukilabs/craft-agents-oss/blob/main/theme.json)) – user-defined colors and mode
2. **Selected preset theme** – base palette from workspace or global selection
3. **Default theme** – hard-coded `DEFAULT_THEME` in [`theme.ts`](https://github.com/lukilabs/craft-agents-oss/blob/main/theme.ts)

Workspace configuration determines *which* preset loads, but does not directly override individual colors; those always come from [`theme.json`](https://github.com/lukilabs/craft-agents-oss/blob/main/theme.json).

### Deep Merge Logic in theme.ts

The core function `resolveTheme()` in [`src/packages/shared/src/config/theme.ts`](https://github.com/lukilabs/craft-agents-oss/blob/main/src/packages/shared/src/config/theme.ts) (lines 135‑139) coordinates the merge:

```typescript
export function resolveTheme(app?: ThemeOverrides): ThemeOverrides {
  // No workspace cascading – app-level only
  return mergeThemes(undefined, app) || {};
}

```

The internal `mergeThemes()` function (lines 97‑129) performs a recursive deep merge where defined keys in the override object replace those in the base. This handles nested structures like `colors.background` or `surfaces.paper`.

### CSS Generation with themeToCSS

After resolution, `themeToCSS()` (lines 173‑244) converts the theme object into CSS custom properties:

1. Determines dark mode status and applies `theme.dark` overrides if applicable
2. Emits variables: `--background`, `--foreground`, `--accent`, etc.
3. Calculates RGB variants for shadow effects
4. Sets `--theme-mode` to `solid` or `scenic`
5. For scenic mode, injects the `backgroundImage` as a data URL via `resolveThemeBackgroundImage()` (lines 1216‑1248 in [`storage.ts`](https://github.com/lukilabs/craft-agents-oss/blob/main/storage.ts))

## Configuration Storage and Persistence

### File Locations and Loading Functions

| Setting | Storage Path | Loader Function |
|---------|-------------|-----------------|
| App theme overrides | `~/.craft-agent/theme.json` | `loadAppTheme()` (storage.ts:727) |
| Global theme selection | `~/.craft-agent/config.json` → `colorTheme` | `getColorTheme()` (storage.ts:1224) |
| Workspace theme ID | [`workspace/config.json`](https://github.com/lukilabs/craft-agents-oss/blob/main/workspace/config.json) → `defaults.colorTheme` | `getWorkspaceColorTheme()` (workspaces/storage.ts:429) |
| Preset theme files | `~/.craft-agent/themes/*.json` | `loadPresetThemes()` (storage.ts:1556) |

### Validation and Reset Mechanisms

The system guards against invalid data through `isValidThemeFile()` in [`validators.ts`](https://github.com/lukilabs/craft-agents-oss/blob/main/validators.ts), which checks JSON structure and required color fields.

Users can restore a preset to its original state using `resetPresetTheme()` (lines 1289‑1314):

```typescript
export function resetPresetTheme(themeId: string): void {
  const bundledPath = getBundledThemePath(themeId);
  const userPath = getAppThemePath(themeId);
  if (existsSync(bundledPath)) {
    copyFileSync(bundledPath, userPath);
  }
}

```

## Practical Implementation in the Renderer

### The useTheme Hook

The Electron renderer composes all layers in [`src/apps/electron/src/renderer/hooks/useTheme.ts`](https://github.com/lukilabs/craft-agents-oss/blob/main/src/apps/electron/src/renderer/hooks/useTheme.ts). This hook orchestrates the cascade by loading the workspace-specific preset ID, falling back to the global theme, then applying user overrides.

### Code Example: Loading the Effective Theme

```typescript
import { loadAppTheme, getColorTheme, loadPresetTheme, resolveTheme } from '@craft-agent/shared/config';
import { getWorkspaceColorTheme } from '@craft-agent/shared/workspaces/storage';

// Assume we already know the active workspace rootPath
const workspaceRoot = '/home/user/.craft-agent/workspaces/my-workspace';

const workspacePresetId = getWorkspaceColorTheme(workspaceRoot) ?? getColorTheme(); // cascade
const preset = loadPresetTheme(workspacePresetId);
const appOverrides = loadAppTheme();

const finalTheme = resolveTheme({ ...(preset?.theme ?? {}), ...appOverrides });
const css = themeToCSS(finalTheme, /* isDark */ false);
console.log(css);   // → CSS custom properties for the renderer

```

*(File references: `loadAppTheme` – [`src/packages/shared/src/config/storage.ts`](https://github.com/lukilabs/craft-agents-oss/blob/main/src/packages/shared/src/config/storage.ts) lines 727‑779; `loadPresetTheme` – [`src/packages/shared/src/config/storage.ts`](https://github.com/lukilabs/craft-agents-oss/blob/main/src/packages/shared/src/config/storage.ts) lines 1258‑1264; `resolveTheme` – [`src/packages/shared/src/config/theme.ts`](https://github.com/lukilabs/craft-agents-oss/blob/main/src/packages/shared/src/config/theme.ts) lines 135‑139)*

## Summary

- **Craft-Agents** separates preset themes from user overrides, allowing workspaces to reference specific color palettes while individual users retain control over granular colors and modes.
- **Five layers** define the cascade: default theme → preset files → global theme ID → workspace theme ID → app-level [`theme.json`](https://github.com/lukilabs/craft-agents-oss/blob/main/theme.json) overrides.
- **Deep merge logic** in [`theme.ts`](https://github.com/lukilabs/craft-agents-oss/blob/main/theme.ts) combines selected presets with user overrides, producing a final theme object converted to CSS variables by `themeToCSS()`.
- **Persistence** spans multiple files: `~/.craft-agent/theme.json` for overrides, [`config.json`](https://github.com/lukilabs/craft-agents-oss/blob/main/config.json) for global selection, and per-workspace [`config.json`](https://github.com/lukilabs/craft-agents-oss/blob/main/config.json) for workspace-specific presets.
- **Validation and reset** functions ensure data integrity and allow users to restore bundled presets to their original state.

## Frequently Asked Questions

### How does Craft-Agents decide which theme to display when a workspace has no colorTheme set?

When a workspace configuration lacks a `colorTheme` property, the system falls back to the global application setting via `getColorTheme()` in [`src/packages/shared/src/config/storage.ts`](https://github.com/lukilabs/craft-agents-oss/blob/main/src/packages/shared/src/config/storage.ts). This function checks the app's [`config.json`](https://github.com/lukilabs/craft-agents-oss/blob/main/config.json) for a stored `colorTheme` value, and if missing, returns the default specified in [`config-defaults.json`](https://github.com/lukilabs/craft-agents-oss/blob/main/config-defaults.json). The workspace then uses whatever preset ID is returned by this global lookup.

### Can I override specific colors from a preset theme without creating a new preset file?

Yes. Individual color overrides are handled through `~/.craft-agent/theme.json`, which is loaded by `loadAppTheme()` in [`src/packages/shared/src/config/storage.ts`](https://github.com/lukilabs/craft-agents-oss/blob/main/src/packages/shared/src/config/storage.ts). This file accepts partial theme definitions—such as only `background` or `accent` colors—which are deep-merged with the selected preset theme via `mergeThemes()` in [`theme.ts`](https://github.com/lukilabs/craft-agents-oss/blob/main/theme.ts). The overrides in [`theme.json`](https://github.com/lukilabs/craft-agents-oss/blob/main/theme.json) always take precedence over the preset's base colors, allowing you to tweak specific values without modifying bundled theme files.

### What is the difference between solid mode and scenic mode in Craft-Agents themes?

**Solid mode** displays the standard color palette without background imagery, using only the defined semantic and surface colors (background, foreground, paper, etc.). **Scenic mode** enables background images specified via the `backgroundImage` property in [`theme.json`](https://github.com/lukilabs/craft-agents-oss/blob/main/theme.json) or a preset file. When `mode` is set to `scenic`, the system calls `resolveThemeBackgroundImage()` in [`src/packages/shared/src/config/storage.ts`](https://github.com/lukilabs/craft-agents-oss/blob/main/src/packages/shared/src/config/storage.ts) to convert the image path to a data URL, which `themeToCSS()` then injects as the `--background-image` CSS variable. This allows translucent UI elements to render over a custom backdrop image instead of a solid color.

### How do I reset a preset theme to its original bundled version if I have modified it?

Use the `resetPresetTheme()` function exported from [`src/packages/shared/src/config/storage.ts`](https://github.com/lukilabs/craft-agents-oss/blob/main/src/packages/shared/src/config/storage.ts) (lines 1289‑1314). This utility locates the original bundled theme file in `apps/electron/resources/themes/` and copies it over the user-edited version in `~/.craft-agent/themes/`, restoring the preset to its factory state. This is particularly useful when `isValidThemeFile()` detects that manual edits have broken the JSON structure or removed required color fields, as the reset ensures a valid base theme for the cascading system to build upon.