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:
- Workspace-specific theme — If the active workspace has a stored override in the
workspaceThemesmap, that theme ID is applied immediately. - App-wide default theme — If no workspace-specific entry exists, the system falls back to the global
colorThemevalue. - System default — If the global
colorThemeis 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
ThemeContextresolver checks theworkspaceThemesmap first, falling back to the globalcolorThemeonly 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 viawindow.electronAPI.setWorkspaceColorTheme. - IPC synchronization — The
WORKSPACE_THEME_CHANGEDchannel 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
useThemehook provides the resolved effective theme to all UI components, including specialized contexts likeShikiThemeContextfor 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:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →