How the Craft-Agents Theme System and Cascading Configuration Work
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 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, nord.json) bundled within apps/electron/resources/themes/. When the application launches, ensurePresetThemes() in 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.
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 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) handles persistence:
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 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 insrc/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).
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):
- App-level overrides (
theme.json) – user-defined colors and mode - Selected preset theme – base palette from workspace or global selection
- Default theme – hard-coded
DEFAULT_THEMEintheme.ts
Workspace configuration determines which preset loads, but does not directly override individual colors; those always come from theme.json.
Deep Merge Logic in theme.ts
The core function resolveTheme() in src/packages/shared/src/config/theme.ts (lines 135‑139) coordinates the merge:
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:
- Determines dark mode status and applies
theme.darkoverrides if applicable - Emits variables:
--background,--foreground,--accent, etc. - Calculates RGB variants for shadow effects
- Sets
--theme-modetosolidorscenic - For scenic mode, injects the
backgroundImageas a data URL viaresolveThemeBackgroundImage()(lines 1216‑1248 instorage.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 → 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, which checks JSON structure and required color fields.
Users can restore a preset to its original state using resetPresetTheme() (lines 1289‑1314):
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. 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
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 lines 727‑779; loadPresetTheme – src/packages/shared/src/config/storage.ts lines 1258‑1264; resolveTheme – 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.jsonoverrides. - Deep merge logic in
theme.tscombines selected presets with user overrides, producing a final theme object converted to CSS variables bythemeToCSS(). - Persistence spans multiple files:
~/.craft-agent/theme.jsonfor overrides,config.jsonfor global selection, and per-workspaceconfig.jsonfor 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. This function checks the app's config.json for a stored colorTheme value, and if missing, returns the default specified in 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. 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. The overrides in 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 or a preset file. When mode is set to scenic, the system calls resolveThemeBackgroundImage() in 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 (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.
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:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →