# PRODUCT.md — Per-tab theme overrides driven by directory and launch configurations Issue: https://github.com/warpdotdev/warp/issues/478 Related: https://github.com/warpdotdev/warp/issues/2618 (set warp theme in launch configuration) ## Summary The Warp theme is a single global value today (`appearance.themes.theme` in `settings.toml`); switching it affects every open tab at once. Users have asked for years (`#478`, 55+ upvotes; `#2618`) for tabs to render with different themes when they represent different contexts — different projects, local vs. remote machines, production vs. development. This spec covers a focused first cut of per-tab theme overrides driven by **three** sources, in priority order: 1. A user-visible **manual** override on a tab (set via the launch configuration YAML or via a right-click menu). 2. A **directory-pattern** auto-match: the user maps directory paths to themes in `settings.toml`, and tabs whose active pane's cwd matches a pattern render with the mapped theme. This is the path most users in the issue thread describe (`pyronaur`, `janderegg`, `milopersic`): "I `cd` into project A, my theme should change." 3. A **launch-configuration window-level default** that themes every tab a given launch configuration opens unless the tab itself has another override. The global theme remains the fallback when none of the three sources apply. Window chrome (title bar, sidebar, settings views, the tab strip) continues to follow the global theme so windows holding mixed-theme tabs remain visually coherent at the window level. This spec deliberately scopes out automatic theming triggered by SSH host, hostname, runtime escape codes, or shell hooks. Those appear in `#478` discussion and are listed as follow-ups that consume the override field this spec introduces. Figma: none provided. ## Goals / Non-goals In-scope surfaces: - A new settings map `appearance.themes.directory_overrides` whose keys are directory paths (tilde-expanded) and whose values are theme identifiers. Matching uses longest-prefix-wins. - The launch-configuration YAML schema gains an optional `theme:` field at the tab level and at the window level. - A persisted per-tab override that survives session restore. - A right-click tab menu entry for **Pin theme** (manually override the active tab's theme to a chosen theme) and **Reset theme** (clear a manual override; cwd-pattern matching may then reapply). Out of scope: - SSH-host-driven, hostname-driven, or `whoami`-driven theming (`stevenchanin`, `pyronaur`, `zethon`, `janderegg`). - Escape-code or shell-hook protocols for runtime theme switching (`yatharth`, for Claude-Code session signaling). - Per-pane theming. Panes inside a tab continue to share one theme. - Per-tab wallpaper or graphics (`scottaw66`, `SheepDomination`). - Changes to the global theme storage path (`appearance.themes.theme`), the theme picker UI, or custom-theme loading. Overrides reuse the existing theme identifier type. ## Resolution order A tab's effective theme is determined by walking these layers and returning the first hit: 1. **Menu pin**, if any. Set by the right-click "Pin theme" menu and cleared by the right-click "Reset theme" menu. Persists across sessions. 2. **Launch-configuration manual pin**, if any. Set by a tab-level `theme:` in the launch configuration that opened (or restored) the tab. Cleared only by an explicit "Forget launch config theme" menu entry — `Reset theme` does **not** clear this layer (per Zach's v4 review). Persists across sessions. 3. **Directory match**, if any. The active pane's current working directory is matched against `appearance.themes.directory_overrides`; if a key is a prefix of the cwd, the longest such key wins. 4. **Launch-configuration window-level default**, if the tab was opened from a launch configuration with a window-level `theme:` and no closer override applies. Persists across sessions. 5. **Global theme** as derived from `ThemeSettings` and the system theme, exactly as today. If none of the override sources resolve to a known theme, behavior is bit-for-bit identical to today. ## Behavior ### Directory-pattern overrides 1. `settings.toml` accepts a new section `[appearance.themes.directory_overrides]` whose entries map a directory path to a theme identifier. Example: ```toml [appearance.themes.directory_overrides] "~/Work/medone" = "Dark City" "~/Work/bondwise" = "Solarized Dark" "~/Work/checkpt" = "Dracula" ``` Theme values are the same string form accepted by the global `appearance.themes.theme` (the parser tolerates both display names like `"Dark City"` and snake-case like `"dark_city"`; see #14 below). 2. Keys are tilde-expanded to absolute paths at match time. Trailing slashes are normalized away. Symlinks are not resolved — the cwd as the shell reports it is what's matched. Path normalization rules, per platform: - **Linux:** matching is case-sensitive (matches the filesystem semantics on standard ext4/xfs). Path separators are `/`. - **macOS:** matching is case-insensitive (matches the default HFS+/APFS case-insensitive setting). Path separators are `/`. - **Windows:** matching is case-insensitive. Both `/` and `\` are accepted as separators in `directory_overrides` keys and are normalized internally to a single canonical form before comparison. Drive letters are normalized to uppercase (`c:\Work` and `C:\Work` are equivalent keys; the second-written one wins per TOML duplicate-key semantics). Tilde expands to `%USERPROFILE%`. Component-boundary matching (the rule in the previous paragraph that prevents `~/Work/medone` from matching `~/Work/medone-archive`) uses the platform's path-component definition — on Windows that means the boundary follows either `\` or `/` after normalization. 3. Match resolution: a key matches if it is a prefix of the active pane's cwd at a path-component boundary. `~/Work/medone` matches both `~/Work/medone` and `~/Work/medone/apps/admin-api`, but does **not** match `~/Work/medone-archive` (no component boundary). When multiple keys match, the longest one wins (most specific). 4. The cwd evaluated for a tab is the cwd of the **focused pane** in that tab. A tab whose focused pane is in a non-shell context (notebook, settings view, etc.) has no cwd and falls through directory matching. 5. When a tab's active pane changes cwd (because the user ran `cd`, opened a subdirectory, or moved focus to a pane in a different cwd), directory matching re-runs. If the new cwd matches a different key, the tab immediately re-renders with the new theme. If it matches no key, the tab falls through to the next layer in the resolution order. 6. Adding, editing, or removing entries in `directory_overrides` while Warp is running re-evaluates every open tab. Tabs whose effective theme changes redraw; tabs whose effective theme is unchanged do not. 7. A theme name in `directory_overrides` that does not resolve to a known theme is treated the same way an unknown launch-configuration theme is (#11): a warning is logged identifying the offending key, the entry is skipped for matching purposes, and the rest of the map continues to work. 7a. **`directory_overrides` is stored locally and never synced to Warp's cloud.** Directory paths can encode employer, customer, and project names (`~/Work///...`); cloud-syncing the keys would push that organizational context off-machine. Users on multiple machines who want shared themes today set them per-machine. An opt-in cloud-sync mode is a candidate follow-up. The global theme setting (`appearance.themes.theme`) and per-tab pins set via the right-click menu remain user-controllable surfaces; only this map is local-only. 7b. **Diagnostic output never contains raw `directory_overrides` keys.** Local logs are routinely shared with Warp support and copied into bug reports, so even local diagnostics can leak path keys. The invariant is: any warning or telemetry emitted by the `directory_overrides` machinery refers to an offending entry by a short non-cryptographic hash of its key plus the offending value (the theme name, which is non-sensitive). For example, a warning that today might say `directory_overrides: "~/Work/AcmeCorp/2026": unknown theme "Drakula"` instead reads `directory_overrides[hash=8a3f9c]: unknown theme "Drakula"`. The user can grep their `settings.toml` for the bad theme name to find the offending row. Hashes are stable for the same key but contain no path information. ### Launch-configuration overrides 8. A launch configuration YAML may include `theme:` on any tab entry. Accepted values are theme identifiers in the form documented in #14. Omitting the field leaves the tab to fall through the resolution order. 9. A launch configuration YAML may include `theme:` on any window entry. Tabs in that window with no tab-level `theme:` and no directory match inherit this window-level value (per the resolution order: #3 sits below #2). 10. Saving a window's current state to a launch configuration preserves every theme override that was *not* derived from directory matching, so that reopening the saved configuration produces the same effective themes (modulo cwd matching, which re-runs against the current `directory_overrides`). Specifically, for each tab the *preserved override* is the first non-empty value of, in priority order: the tab's menu pin (layer #1), the tab's launch-configuration manual pin (layer #2), or the tab's window-level default (layer #4). Tabs whose effective theme came only from directory matching (layer #3) have no preserved override. The save rule is then: - If every tab in the window has the same preserved override `Some(X)` and no tab has a manual pin different from the others, the saved YAML emits a single window-level `theme: X` and no per-tab `theme:` fields. - Otherwise, each tab whose preserved override is `Some(Y)` emits a per-tab `theme: Y`; tabs with no preserved override emit no `theme:` field; window-level `theme:` is omitted. - Directory-matched themes never appear in saved YAML — directory matching is settings-level and re-applies on next open. This means a launch configuration that originally opened with a window-level `theme:` round-trips correctly: every tab inherited the value into its `window_default` slot, the save rule sees a shared preserved override, and emits the same window-level `theme:` again. ### YAML / settings format 11. An unknown theme identifier — anywhere it appears — never causes a file-level load failure. The deserializer accepts any string; the resolver runs at apply time and falls back to the next resolution layer for the affected entry only. Other tabs in the same launch configuration, and other entries in the same `directory_overrides` map, are unaffected. Each unknown name produces exactly one logged warning per load. 12. Custom themes referenced by an override behave identically to custom themes used as the global theme — loaded from the user's themes directory, fail-soft to the next layer if the file is missing, same trust/validation rules as the global theme loader. 13. Overrides persist per-tab through session restore, by source: - **Menu pin** (layer #1) — kept on relaunch. - **Launch-configuration manual pin** (layer #2) — kept on relaunch. The launch configuration that set it is not necessarily reopened on restore, so the value travels with the tab. - **Launch-configuration window-level default** (layer #4) — kept on relaunch, same reason as #2. - **Directory match** (layer #3) — not stored; recomputed on relaunch from the current `directory_overrides` and the restored cwd. Editing `directory_overrides` between sessions therefore takes effect on the next launch. 14. The accepted form for any theme reference (in `directory_overrides`, in launch-config tab `theme:`, in launch-config window `theme:`) is a single string. Both the human-readable display form (`"Dark City"`, `"Solarized Dark"`, `"Dracula"`) and the snake-case form (`"dark_city"`, `"solarized_dark"`, `"dracula"`) are accepted. Matching is case-insensitive on whitespace-stripped input. Custom themes are referenced by their custom-theme name, same as today's global setting. ### Rendering scope 15. When a tab has an effective override (from any layer), the override applies to: the terminal cell foreground/background, the ANSI 16-color palette used by the terminal grid, and any in-tab UI surfaces whose colors are derived from the active theme (block backgrounds, command output styling, accent colors). The window chrome (title bar, sidebar, settings views, the tab strip itself) continues to follow the global theme. 16. Switching tabs is instant — no flash, no progressive paint. Only the rendering of the newly-active tab reflects its (possibly different) theme; inactive tabs do not redraw on switch. 17. Changing the global theme updates every tab whose effective theme falls through to the global layer. Tabs with overrides at any higher-priority layer are unaffected. ### User affordances 18. The right-click tab context menu gains three entries, alongside the existing per-tab attributes: - **Pin theme...** — opens a submenu listing available themes. Choosing one sets a *menu pin* on the tab (resolution layer #1), which wins over every other layer including a launch-config manual pin. Visible at all times. - **Reset theme** — clears only the menu pin (layer #1). The tab falls through to the launch-config manual pin / directory match / window default / global layers in that order. Visible only when the tab has a menu pin. - **Forget launch config theme** — clears only the launch-config manual pin (layer #2). The tab falls through to the directory match / window default / global layers. Visible only when the tab has a launch-config manual pin. Splitting "Reset theme" from "Forget launch config theme" means a user who pinned a different theme via the menu can clear that pin without unintentionally also discarding what their launch configuration originally set. (Per Zach's v4 review.) 19. The existing per-tab `color:` field on a tab template (the small colored indicator next to the tab title) is independent of the new theme override. Both can be set; both are honored. 20. The feature applies on every supported platform (macOS, Linux, Windows). It is **gated behind a feature flag** named `appearance.themes.per_tab_overrides` (per Zach's v4 review): - Initial release ships with the flag **off** in stable and **on** in dev/preview, so the rollout can soak with internal users before reaching the broader install base. - When the flag is off the feature is invisible: the right-click menu entries are hidden, `directory_overrides` matching does not run (the settings group is still parseable so users who set up the map under preview do not lose data), and launch-configuration `theme:` fields are deserialized but ignored. The user-visible behavior is bit-for-bit identical to today. - The default flips to on in stable in a follow-up release, after telemetry on the preview/dev cohort confirms no regressions in render performance, settings parsing, or session restore. - An empty `directory_overrides` map (the default) plus no launch-config theme fields plus no pinned themes — even with the flag on — is bit-for-bit identical to current behavior. ### Accessibility 21. The override does not change any text content, accessible labels, or focus order. Screen readers continue to report tab titles and contents identically. The three new right-click menu entries have accessible labels "Pin theme" (with a submenu of theme names), "Reset theme", and "Forget launch config theme"; each is announced as a menu item, and each follows the menu's existing visibility rules (Reset theme appears only when the tab has a menu pin; Forget launch config theme appears only when the tab has a launch-config manual pin). ## User-visible failure modes - **Unknown theme name** — anywhere it appears, the entry is skipped at apply time, a one-line warning is written to the Warp log identifying the source (launch configuration filename + tab title or index for launch-config sources; **redacted entry identifier** for `directory_overrides` sources, never the raw path key — see #7b), and the rest of the configuration loads normally. - **Custom theme file missing** — same fallback the global theme uses today: tab opens with the next-layer theme, warning logged. - **Two `directory_overrides` keys are equivalent after tilde expansion** — last-write-wins per TOML semantics; a warning is logged identifying the duplicate. - **A pane's cwd is unavailable** (non-shell pane content) — that pane contributes no cwd to directory matching; if it is the focused pane the tab falls through to the window/global layers. ## Open questions (Resolved in v4 review and folded into the spec — kept here as a record of the design choices made.) - ~~`directory_overrides` keys: glob vs. prefix matching.~~ Resolved: prefix matching only (Zach v4: "i think prefix-match is fine to start"). Globs are a follow-up. - ~~"Reset theme" scope: clear all manual layers, or only menu pins.~~ Resolved: only menu pins (Zach v4: "i don't think it should" clear launch-config pins). A separate "Forget launch config theme" entry exists for the launch-config layer. Reflected in behaviors #18 and in the resolution order. - Should saving a launch configuration emit `directory_overrides` entries so themes travel with the launch config? Spec keeps the two surfaces independent — launch configs carry only manual pins; directory matching is settings-level and does not round-trip through saved launch configs. A future "shareable theme bundle" could compose them. Open for product-team input.