diff --git a/docs/superpowers/plans/2026-08-24-custom-colours-phase-1-catalog-config.md b/docs/superpowers/plans/2026-08-24-custom-colours-phase-1-catalog-config.md index 2f910d9..dbb9073 100644 --- a/docs/superpowers/plans/2026-08-24-custom-colours-phase-1-catalog-config.md +++ b/docs/superpowers/plans/2026-08-24-custom-colours-phase-1-catalog-config.md @@ -4,7 +4,7 @@ **Goal:** Add the complete colour preset catalogue and canonical persisted `ColorSettings` contract, with Pi as the default. -**Architecture:** Keep palette constants, validation, normalization, cloning, and lookup in one new core module. Extend the shared config type and existing config load/save path so later phases consume one required, normalized settings object. +**Architecture:** Keep palette constants, validation, normalization, and lookup in one new core module. Reuse the shared semantic-role contract and native `structuredClone`; extend the existing config load/save path so later phases consume one required, normalized settings object. **Tech Stack:** TypeScript 6, Node.js `>=24.15.0`, Vitest 4, and the existing extension-owned `ConfigStore`. @@ -19,7 +19,7 @@ - Modify only `/Users/lanh/Developer/pi-vault/pi-status`; Pi and Atelier repositories are read-only references. - Add no dependency, runtime palette fetch, graphical picker, CSS parser, import/export, or Pi global-theme mutation. - Use `Pi` (`pi`) as the default preset. `NO_COLOR` is an environment override, never a preset. -- Persist only uppercase `#RRGGBB` Custom values and retain exactly 14 editable semantic roles. +- Accept case-insensitive hex input, persist only lowercase `#rrggbb` values, and retain exactly 14 editable semantic roles. - Fixed and Custom presets emit truecolour; do not add a 256-colour conversion path. - Dashboard uses draft colours; installed surfaces change only after persistence succeeds. - Preserve malformed-file overwrite refusal and renderer plain-text fallbacks. @@ -38,6 +38,7 @@ This phase is complete when every preset has a typed, pinned catalogue entry; mi - Modify: `src/shared/types.ts` - Modify: `src/core/config.ts` - Test: `tests/core/config.test.ts` +- Test fixtures: `tests/index.test.ts`, `tests/index-save.test.ts`, `tests/index-sidebar-layout.test.ts`, `tests/core/resolve-footer.test.ts`, `tests/core/runtime-state.test.ts`, `tests/core/sidebar-layout.test.ts`, `tests/tui/dashboard-render.test.ts`, `tests/tui/dashboard-state.test.ts`, `tests/tui/dashboard.test.ts`, `tests/tui/sidebar-render.test.ts`, and `tests/tui/sidebar.test.ts` --- @@ -53,7 +54,7 @@ This phase is complete when every preset has a typed, pinned catalogue entry; mi **Interfaces:** - Produces shared `PALETTE_ROLES`, `PaletteRole`, `ColorPreset`, `FixedColorPreset`, `HexColor`, `ColorPalette`, and `ColorSettings`. -- Produces `COLOR_PRESET_IDS`, `ATELIER_COLORS`, `FIXED_COLOR_PALETTES`, `DEFAULT_COLOR_SETTINGS`, `isHexColor`, `normalizeHexColor`, `getFixedColorPalette`, `cloneColorSettings`, and `normalizeColorSettings` from `src/core/colors.ts`. +- Produces `COLOR_PRESET_IDS`, `ATELIER_COLORS`, `FIXED_COLOR_PALETTES`, `DEFAULT_COLOR_SETTINGS`, `isHexColor`, `normalizeHexColor`, `getFixedColorPalette`, and `normalizeColorSettings` from `src/core/colors.ts`. - Produces required `PiStatusConfig.colors: ColorSettings` for later tasks. - [ ] **Step 1: Write failing configuration tests** @@ -66,11 +67,161 @@ import { COLOR_PRESET_IDS, DEFAULT_COLOR_SETTINGS, FIXED_COLOR_PALETTES, + getFixedColorPalette, normalizeColorSettings, + normalizeHexColor, } from "../../src/core/colors.ts"; -import { PALETTE_ROLES } from "../../src/shared/types.ts"; +import { + PALETTE_ROLES, + type FixedColorPreset, +} from "../../src/shared/types.ts"; + +const EXPECTED_FIXED_VALUES: Record = { + atelier: [ + "#b18cff", + "#d4d4d4", + "#808080", + "#666666", + "#6ea8fe", + "#ff9f43", + "#6ea8fe", + "#b18cff", + "#7dd3fc", + "#ff9f43", + "#6ea8fe", + "#b18cff", + "#ff9f43", + "#ff5d73", + ], + "catppuccin-mocha": [ + "#cba6f7", + "#cdd6f4", + "#a6adc8", + "#6c7086", + "#a6e3a1", + "#fab387", + "#89b4fa", + "#cba6f7", + "#89dceb", + "#fab387", + "#89b4fa", + "#f5c2e7", + "#f9e2af", + "#f38ba8", + ], + "catppuccin-latte": [ + "#8839ef", + "#4c4f69", + "#6c6f85", + "#9ca0b0", + "#40a02b", + "#fe640b", + "#1e66f5", + "#8839ef", + "#04a5e5", + "#fe640b", + "#1e66f5", + "#ea76cb", + "#df8e1d", + "#d20f39", + ], + dracula: [ + "#bd93f9", + "#f8f8f2", + "#6272a4", + "#44475a", + "#50fa7b", + "#ffb86c", + "#8be9fd", + "#bd93f9", + "#8be9fd", + "#ffb86c", + "#8be9fd", + "#ff79c6", + "#f1fa8c", + "#ff5555", + ], + "dracula-alucard": [ + "#644ac9", + "#1f1f1f", + "#6c664b", + "#6c664b", + "#14710a", + "#a34d14", + "#036a96", + "#644ac9", + "#036a96", + "#a34d14", + "#036a96", + "#a3144d", + "#846e15", + "#cb3a2a", + ], + "tokyonight-moon": [ + "#c099ff", + "#c8d3f5", + "#636da6", + "#545c7e", + "#c3e88d", + "#ff966c", + "#82aaff", + "#c099ff", + "#86e1fc", + "#ff966c", + "#82aaff", + "#fca7ea", + "#ffc777", + "#ff757f", + ], + "tokyonight-day": [ + "#9854f1", + "#3760bf", + "#848cb5", + "#8990b3", + "#587539", + "#b15c00", + "#2e7de9", + "#9854f1", + "#007197", + "#b15c00", + "#2e7de9", + "#7847bd", + "#8c6c3e", + "#f52a65", + ], +}; describe("config — colours", () => { + it("pins preset and role order", () => { + expect(COLOR_PRESET_IDS).toEqual([ + "pi", + "atelier", + "catppuccin-mocha", + "catppuccin-latte", + "dracula", + "dracula-alucard", + "tokyonight-moon", + "tokyonight-day", + "custom", + ]); + expect(PALETTE_ROLES).toEqual([ + "accent", + "primary", + "muted", + "dim", + "ready", + "working", + "input", + "output", + "cache", + "cost", + "context", + "menu", + "warning", + "error", + ]); + }); + it("defaults missing colours to the live Pi preset", () => { const first = loadConfig({ agentDir: "/agent-a", @@ -91,20 +242,27 @@ describe("config — colours", () => { }); it("repairs malformed settings role by role", () => { - expect( - normalizeColorSettings({ - preset: "custom", - custom: { accent: "#abcdef", error: "broken", unknown: "#000000" }, - }), - ).toMatchObject({ + const input = { + preset: "custom", + custom: { accent: "#AbCdEf", error: "broken", unknown: "#000000" }, + }; + const normalized = normalizeColorSettings(input); + expect(normalized).toEqual({ preset: "custom", customInitialized: true, - custom: { accent: "#ABCDEF", error: ATELIER_COLORS.error }, + custom: { ...ATELIER_COLORS, accent: "#abcdef" }, }); + expect(normalized.custom).not.toBe(input.custom); expect(normalizeColorSettings({ preset: "none" }).preset).toBe("pi"); expect(normalizeColorSettings({ preset: "unknown" }).preset).toBe("pi"); }); + it("accepts every hex letter case and canonicalizes to lowercase", () => { + expect(normalizeHexColor("#ABCDEF", ATELIER_COLORS.accent)).toBe("#abcdef"); + expect(normalizeHexColor("#abcdef", ATELIER_COLORS.accent)).toBe("#abcdef"); + expect(normalizeHexColor("#AbCdEf", ATELIER_COLORS.accent)).toBe("#abcdef"); + }); + it.each(COLOR_PRESET_IDS)("round-trips the %s preset", (preset) => { const store = new MemoryConfigStore(); const value = { @@ -125,27 +283,39 @@ describe("config — colours", () => { ); }); - it("pins every fixed palette to 14 complete values", () => { - for (const palette of Object.values(FIXED_COLOR_PALETTES)) { - expect(Object.keys(palette)).toEqual(PALETTE_ROLES); - expect(Object.values(palette)).toHaveLength(14); - expect( - Object.values(palette).every((value) => /^#[0-9A-F]{6}$/.test(value)), - ).toBe(true); + it("pins every value and freezes the fixed catalogue", () => { + expect(Object.isFrozen(FIXED_COLOR_PALETTES)).toBe(true); + for (const [preset, values] of Object.entries(EXPECTED_FIXED_VALUES)) { + const palette = FIXED_COLOR_PALETTES[preset as FixedColorPreset]; + expect(PALETTE_ROLES.map((role) => palette[role])).toEqual(values); + expect(Object.isFrozen(palette)).toBe(true); } + expect(getFixedColorPalette("pi")).toBeUndefined(); + expect(getFixedColorPalette("custom")).toBeUndefined(); }); -}); -``` -Add exact assertions for one distinctive role from every sourced preset: + it("canonicalizes colours before saving", () => { + const store = new MemoryConfigStore(); + saveConfig( + { + ...config, + colors: { + preset: "custom", + custom: { accent: "#ABCDEF", error: "broken", unknown: "#000000" }, + customInitialized: "yes", + } as unknown as PiStatusConfig["colors"], + }, + { agentDir: "/agent", store }, + ); -```ts -expect(FIXED_COLOR_PALETTES["catppuccin-mocha"].accent).toBe("#CBA6F7"); -expect(FIXED_COLOR_PALETTES["catppuccin-latte"].primary).toBe("#4C4F69"); -expect(FIXED_COLOR_PALETTES.dracula.error).toBe("#FF5555"); -expect(FIXED_COLOR_PALETTES["dracula-alucard"].primary).toBe("#1F1F1F"); -expect(FIXED_COLOR_PALETTES["tokyonight-moon"].cache).toBe("#86E1FC"); -expect(FIXED_COLOR_PALETTES["tokyonight-day"].primary).toBe("#3760BF"); + const written = JSON.parse(store.read(getConfigPath("/agent")) as string); + expect(written.colors).toEqual({ + preset: "custom", + custom: { ...ATELIER_COLORS, accent: "#abcdef" }, + customInitialized: true, + }); + }); +}); ``` - [ ] **Step 2: Run the configuration test and verify RED** @@ -199,9 +369,11 @@ export type ColorSettings = { custom: ColorPalette; customInitialized: boolean; }; + +export type SidebarSegmentRole = PaletteRole; ``` -Add `colors: ColorSettings` to `PiStatusConfig`. +Replace the existing `SidebarSegmentRole` union with the alias above, then add `colors: ColorSettings` to `PiStatusConfig`. Do not retain a second copy of the same 14 roles. - [ ] **Step 4: Create the immutable preset catalogue** @@ -221,20 +393,20 @@ export const COLOR_PRESET_IDS = [ ] as const satisfies readonly ColorPreset[]; export const ATELIER_COLORS: Readonly = Object.freeze({ - accent: "#B18CFF", - primary: "#D4D4D4", + accent: "#b18cff", + primary: "#d4d4d4", muted: "#808080", dim: "#666666", - ready: "#6EA8FE", - working: "#FF9F43", - input: "#6EA8FE", - output: "#B18CFF", - cache: "#7DD3FC", - cost: "#FF9F43", - context: "#6EA8FE", - menu: "#B18CFF", - warning: "#FF9F43", - error: "#FF5D73", + ready: "#6ea8fe", + working: "#ff9f43", + input: "#6ea8fe", + output: "#b18cff", + cache: "#7dd3fc", + cost: "#ff9f43", + context: "#6ea8fe", + menu: "#b18cff", + warning: "#ff9f43", + error: "#ff5d73", }); export const FIXED_COLOR_PALETTES: Readonly< @@ -243,102 +415,102 @@ export const FIXED_COLOR_PALETTES: Readonly< atelier: ATELIER_COLORS, // Catppuccin palette 1.8.0: https://github.com/catppuccin/palette/blob/main/palette.json "catppuccin-mocha": Object.freeze({ - accent: "#CBA6F7", - primary: "#CDD6F4", - muted: "#A6ADC8", - dim: "#6C7086", - ready: "#A6E3A1", - working: "#FAB387", - input: "#89B4FA", - output: "#CBA6F7", - cache: "#89DCEB", - cost: "#FAB387", - context: "#89B4FA", - menu: "#F5C2E7", - warning: "#F9E2AF", - error: "#F38BA8", + accent: "#cba6f7", + primary: "#cdd6f4", + muted: "#a6adc8", + dim: "#6c7086", + ready: "#a6e3a1", + working: "#fab387", + input: "#89b4fa", + output: "#cba6f7", + cache: "#89dceb", + cost: "#fab387", + context: "#89b4fa", + menu: "#f5c2e7", + warning: "#f9e2af", + error: "#f38ba8", }), "catppuccin-latte": Object.freeze({ - accent: "#8839EF", - primary: "#4C4F69", - muted: "#6C6F85", - dim: "#9CA0B0", - ready: "#40A02B", - working: "#FE640B", - input: "#1E66F5", - output: "#8839EF", - cache: "#04A5E5", - cost: "#FE640B", - context: "#1E66F5", - menu: "#EA76CB", - warning: "#DF8E1D", - error: "#D20F39", + accent: "#8839ef", + primary: "#4c4f69", + muted: "#6c6f85", + dim: "#9ca0b0", + ready: "#40a02b", + working: "#fe640b", + input: "#1e66f5", + output: "#8839ef", + cache: "#04a5e5", + cost: "#fe640b", + context: "#1e66f5", + menu: "#ea76cb", + warning: "#df8e1d", + error: "#d20f39", }), // Dracula Classic/Alucard: https://github.com/dracula/dracula-theme#color-palette dracula: Object.freeze({ - accent: "#BD93F9", - primary: "#F8F8F2", - muted: "#6272A4", - dim: "#44475A", - ready: "#50FA7B", - working: "#FFB86C", - input: "#8BE9FD", - output: "#BD93F9", - cache: "#8BE9FD", - cost: "#FFB86C", - context: "#8BE9FD", - menu: "#FF79C6", - warning: "#F1FA8C", - error: "#FF5555", + accent: "#bd93f9", + primary: "#f8f8f2", + muted: "#6272a4", + dim: "#44475a", + ready: "#50fa7b", + working: "#ffb86c", + input: "#8be9fd", + output: "#bd93f9", + cache: "#8be9fd", + cost: "#ffb86c", + context: "#8be9fd", + menu: "#ff79c6", + warning: "#f1fa8c", + error: "#ff5555", }), "dracula-alucard": Object.freeze({ - accent: "#644AC9", - primary: "#1F1F1F", - muted: "#6C664B", - dim: "#6C664B", - ready: "#14710A", - working: "#A34D14", - input: "#036A96", - output: "#644AC9", - cache: "#036A96", - cost: "#A34D14", - context: "#036A96", - menu: "#A3144D", - warning: "#846E15", - error: "#CB3A2A", + accent: "#644ac9", + primary: "#1f1f1f", + muted: "#6c664b", + dim: "#6c664b", + ready: "#14710a", + working: "#a34d14", + input: "#036a96", + output: "#644ac9", + cache: "#036a96", + cost: "#a34d14", + context: "#036a96", + menu: "#a3144d", + warning: "#846e15", + error: "#cb3a2a", }), // TokyoNight: https://github.com/folke/tokyonight.nvim/tree/main/lua/tokyonight/colors "tokyonight-moon": Object.freeze({ - accent: "#C099FF", - primary: "#C8D3F5", - muted: "#636DA6", - dim: "#545C7E", - ready: "#C3E88D", - working: "#FF966C", - input: "#82AAFF", - output: "#C099FF", - cache: "#86E1FC", - cost: "#FF966C", - context: "#82AAFF", - menu: "#FCA7EA", - warning: "#FFC777", - error: "#FF757F", + accent: "#c099ff", + primary: "#c8d3f5", + muted: "#636da6", + dim: "#545c7e", + ready: "#c3e88d", + working: "#ff966c", + input: "#82aaff", + output: "#c099ff", + cache: "#86e1fc", + cost: "#ff966c", + context: "#82aaff", + menu: "#fca7ea", + warning: "#ffc777", + error: "#ff757f", }), "tokyonight-day": Object.freeze({ - accent: "#9854F1", - primary: "#3760BF", - muted: "#848CB5", - dim: "#8990B3", + accent: "#9854f1", + primary: "#3760bf", + muted: "#848cb5", + dim: "#8990b3", ready: "#587539", - working: "#B15C00", - input: "#2E7DE9", - output: "#9854F1", + working: "#b15c00", + input: "#2e7de9", + output: "#9854f1", cache: "#007197", - cost: "#B15C00", - context: "#2E7DE9", - menu: "#7847BD", - warning: "#8C6C3E", - error: "#F52A65", + cost: "#b15c00", + context: "#2e7de9", + menu: "#7847bd", + warning: "#8c6c3e", + error: "#f52a65", }), }); ``` @@ -366,13 +538,13 @@ export function normalizeHexColor( value: unknown, fallback: HexColor, ): HexColor { - return isHexColor(value) ? (value.toUpperCase() as HexColor) : fallback; + return isHexColor(value) ? (value.toLowerCase() as HexColor) : fallback; } export function getFixedColorPalette( preset: ColorPreset, ): Readonly | undefined { - return preset in FIXED_COLOR_PALETTES + return Object.hasOwn(FIXED_COLOR_PALETTES, preset) ? FIXED_COLOR_PALETTES[preset as FixedColorPreset] : undefined; } @@ -407,27 +579,21 @@ export function normalizeColorSettings(input: unknown): ColorSettings { customInitialized: value.customInitialized === true || preset === "custom", }; } - -export function cloneColorSettings(value: ColorSettings): ColorSettings { - return { - preset: value.preset, - custom: { ...value.custom }, - customInitialized: value.customInitialized, - }; -} ``` - [ ] **Step 6: Wire colours through config load, clone, and save** -In `src/core/config.ts`, import the colour helpers. Add an independent clone of `DEFAULT_COLOR_SETTINGS` to `DEFAULT_CONFIG` and `cloneDefaultConfig`; add `colors: normalizeColorSettings(input.colors)` to `normalizeConfig`; write `colors: cloneColorSettings(config.colors)` in `saveConfig`. +In `src/core/config.ts`, import `DEFAULT_COLOR_SETTINGS` and `normalizeColorSettings`. Add `colors: structuredClone(DEFAULT_COLOR_SETTINGS)` to `DEFAULT_CONFIG`, `colors: structuredClone(DEFAULT_CONFIG.colors)` to `cloneDefaultConfig`, `colors: normalizeColorSettings(input.colors)` to `normalizeConfig`, and `colors: normalizeColorSettings(config.colors)` to `saveConfig`. Saving must normalize rather than merely copy so JavaScript callers and cast values cannot persist uppercase, mixed-case, partial, or unknown colour data. -Update every typed `PiStatusConfig` fixture reported by TypeScript with: +Update the typed `PiStatusConfig` fixtures listed in the file map with this import and field: ```ts -colors: cloneColorSettings(DEFAULT_COLOR_SETTINGS), +import { DEFAULT_COLOR_SETTINGS } from "../src/core/colors.ts"; + +colors: structuredClone(DEFAULT_COLOR_SETTINGS), ``` -Do not make `colors` optional or add casts to hide missing fixtures. +Use `../src/core/colors.ts` in the three root `tests/index*.test.ts` files and `../../src/core/colors.ts` in `tests/core/*` and `tests/tui/*`. In `tests/core/config.test.ts`, also add `colors` to the exact filesystem default, serialized-config, and published-key expectations. Do not make `colors` optional or add casts to hide missing fixtures. - [ ] **Step 7: Run focused verification and reach GREEN** @@ -435,15 +601,16 @@ Run: ```bash pnpm exec vitest run tests/core/config.test.ts -pnpm exec tsc --noEmit +pnpm check +git diff --check ``` -Expected: PASS with no missing `colors` fixtures. +Expected: the focused test passes, then all formatting, lint, typecheck, and 925+ tests pass, and `git diff --check` reports no whitespace errors. - [ ] **Step 8: Commit the catalogue checkpoint when authorized** ```bash -git add src/core/colors.ts src/shared/types.ts src/core/config.ts tests/core/config.test.ts +git add src/core/colors.ts src/shared/types.ts src/core/config.ts tests/core/config.test.ts tests/index.test.ts tests/index-save.test.ts tests/index-sidebar-layout.test.ts tests/core/resolve-footer.test.ts tests/core/runtime-state.test.ts tests/core/sidebar-layout.test.ts tests/tui/dashboard-render.test.ts tests/tui/dashboard-state.test.ts tests/tui/dashboard.test.ts tests/tui/sidebar-render.test.ts tests/tui/sidebar.test.ts git commit -m "feat: add colour preset catalogue" ``` diff --git a/docs/superpowers/plans/2026-08-24-custom-colours-phase-2-theme-surfaces.md b/docs/superpowers/plans/2026-08-24-custom-colours-phase-2-theme-surfaces.md index 0eb465c..6153ea3 100644 --- a/docs/superpowers/plans/2026-08-24-custom-colours-phase-2-theme-surfaces.md +++ b/docs/superpowers/plans/2026-08-24-custom-colours-phase-2-theme-surfaces.md @@ -19,7 +19,7 @@ - Modify only `/Users/lanh/Developer/pi-vault/pi-status`; Pi and Atelier repositories are read-only references. - Add no dependency, runtime palette fetch, graphical picker, CSS parser, import/export, or Pi global-theme mutation. - Use `Pi` (`pi`) as the default preset. `NO_COLOR` is an environment override, never a preset. -- Persist only uppercase `#RRGGBB` Custom values and retain exactly 14 editable semantic roles. +- Accept case-insensitive hex input, persist only lowercase `#rrggbb` values, and retain exactly 14 editable semantic roles. - Fixed and Custom presets emit truecolour; do not add a 256-colour conversion path. - Dashboard uses draft colours; installed surfaces change only after persistence succeeds. - Preserve malformed-file overwrite refusal and renderer plain-text fallbacks. diff --git a/docs/superpowers/plans/2026-08-24-custom-colours-phase-3-dashboard.md b/docs/superpowers/plans/2026-08-24-custom-colours-phase-3-dashboard.md index 1d8040a..ff392df 100644 --- a/docs/superpowers/plans/2026-08-24-custom-colours-phase-3-dashboard.md +++ b/docs/superpowers/plans/2026-08-24-custom-colours-phase-3-dashboard.md @@ -19,7 +19,7 @@ - Modify only `/Users/lanh/Developer/pi-vault/pi-status`; Pi and Atelier repositories are read-only references. - Add no dependency, runtime palette fetch, graphical picker, CSS parser, import/export, or Pi global-theme mutation. - Use `Pi` (`pi`) as the default preset. `NO_COLOR` is an environment override, never a preset. -- Persist only uppercase `#RRGGBB` Custom values and retain exactly 14 editable semantic roles. +- Accept case-insensitive hex input, persist only lowercase `#rrggbb` values, and retain exactly 14 editable semantic roles. - Fixed and Custom presets emit truecolour; do not add a 256-colour conversion path. - Dashboard uses draft colours; installed surfaces change only after persistence succeeds. - Preserve malformed-file overwrite refusal and renderer plain-text fallbacks. @@ -233,12 +233,12 @@ In `tests/tui/dashboard.test.ts`, activate Accent and verify: ```ts expect(renderText()).toContain("Edit accent colour"); -component.handleInput("#010203"); +component.handleInput("#AbCdEf"); component.handleInput("\r"); -expect(component.getState().draft.colors.custom.accent).toBe("#010203"); +expect(component.getState().draft.colors.custom.accent).toBe("#abcdef"); ``` -Add invalid-submit and Escape tests. Invalid submit must leave the dialog open and draft unchanged, and notify `Colour must use #RRGGBB` with `warning` once per submit. +Add invalid-submit and Escape tests. Invalid submit must leave the dialog open and draft unchanged, and notify `Colour must use # followed by 6 hex digits` with `warning` once per submit. - [ ] **Step 5: Run Dashboard render/component tests and verify RED** @@ -250,7 +250,7 @@ Expected: FAIL because preset rendering and the colour dialog do not exist. - [ ] **Step 6: Implement labels, draft preview, and Input dialog** -In `src/tui/dashboard-render.ts`, add the exact labels above and render Colours as an adjustable `↔` row. Render each Custom row with its role, uppercase value, and a sample through `theme.fg(role, "●")`. +In `src/tui/dashboard-render.ts`, add the exact labels above and render Colours as an adjustable `↔` row. Render each Custom row with its role, lowercase value, and a sample through `theme.fg(role, "●")`. Extend `DashboardDialog`: @@ -273,7 +273,7 @@ Handle `edit_color` by seeding `Input` with the current value. On submit: ```ts if (!isHexColor(value)) { - this.warn("Colour must use #RRGGBB"); + this.warn("Colour must use # followed by 6 hex digits"); return; } this.dispatch({ diff --git a/docs/superpowers/plans/2026-08-24-custom-colours-phase-4-docs-verification.md b/docs/superpowers/plans/2026-08-24-custom-colours-phase-4-docs-verification.md index e4dfb09..e12993c 100644 --- a/docs/superpowers/plans/2026-08-24-custom-colours-phase-4-docs-verification.md +++ b/docs/superpowers/plans/2026-08-24-custom-colours-phase-4-docs-verification.md @@ -19,7 +19,7 @@ - Modify only `/Users/lanh/Developer/pi-vault/pi-status`; Pi and Atelier repositories are read-only references. - Add no dependency, runtime palette fetch, graphical picker, CSS parser, import/export, or Pi global-theme mutation. - Use `Pi` (`pi`) as the default preset. `NO_COLOR` is an environment override, never a preset. -- Persist only uppercase `#RRGGBB` Custom values and retain exactly 14 editable semantic roles. +- Accept case-insensitive hex input, persist only lowercase `#rrggbb` values, and retain exactly 14 editable semantic roles. - Fixed and Custom presets emit truecolour; do not add a 256-colour conversion path. - Dashboard uses draft colours; installed surfaces change only after persistence succeeds. - Preserve malformed-file overwrite refusal and renderer plain-text fallbacks. @@ -114,7 +114,7 @@ In `README.md`: - List all nine labels in Dashboard order. - Group Catppuccin Mocha/Latte, Dracula/Alucard, and Tokyo Night Moon/Day as explicit dark/light choices. - Link the three official palette sources used by the local constants. -- Document first-use Custom seeding, 14 `#RRGGBB` roles, uppercase persistence, and truecolour requirements. +- Document first-use Custom seeding, 14 `#rrggbb` roles, case-insensitive input, lowercase persistence, and truecolour requirements. - State that `NO_COLOR` disables styling across Dashboard, Statusbar, and Sidebar without changing the saved selection. In `CHANGELOG.md`, add above released entries: @@ -162,7 +162,7 @@ Launch the local extension in Pi and open `/statusline` at approximately `120x30 3. All nine labels cycle in the documented order with wraparound. 4. Each fixed dark/light preset previews and saves consistently across all surfaces. 5. First Custom entry clones the selected fixed palette, or Atelier from Pi; later switches preserve it. -6. Custom exposes 14 scrollable roles; lowercase input saves uppercase, invalid input remains open with one warning per submit, and Escape cancels. +6. Custom exposes 14 scrollable roles; uppercase, lowercase, and mixed-case input saves lowercase, invalid input remains open with one warning per submit, and Escape cancels. 7. Draft changes recolour Dashboard immediately while installed surfaces wait for confirmed Save. 8. Failed Save leaves installed colours unchanged and Dashboard dirty. 9. `NO_COLOR` removes ANSI styling without changing the saved preset. diff --git a/docs/superpowers/plans/2026-08-24-custom-colours.md b/docs/superpowers/plans/2026-08-24-custom-colours.md index 60d52f6..d18d3e8 100644 --- a/docs/superpowers/plans/2026-08-24-custom-colours.md +++ b/docs/superpowers/plans/2026-08-24-custom-colours.md @@ -4,7 +4,7 @@ **Goal:** Add Pi-synchronized, sourced fixed, and fully editable 14-role Custom colour presets to `/statusline`, with Pi as the default across Dashboard, Statusbar, and Sidebar. -**Architecture:** Persist one normalized `ColorSettings` object in `PiStatusConfig`. A local immutable catalogue supplies seven fixed palettes, while one render-time `createStatusLineTheme` adapter either delegates to Pi's live theme, emits fixed/Custom truecolour ANSI, or returns identity under `NO_COLOR`. Dashboard resolves draft settings; installed Footer and Sidebar resolve committed runtime settings. +**Architecture:** Persist one normalized `ColorSettings` object in `PiStatusConfig`. A local immutable catalogue supplies seven fixed palettes, reuses the shared semantic-role contract, and relies on native `structuredClone`; one render-time `createStatusLineTheme` adapter either delegates to Pi's live theme, emits fixed/Custom truecolour ANSI, or returns identity under `NO_COLOR`. Dashboard resolves draft settings; installed Footer and Sidebar resolve committed runtime settings. **Tech Stack:** TypeScript 6, Node.js `>=24.15.0`, Pi TUI `Input`, 24-bit ANSI colour sequences, Vitest 4, Biome, and the existing extension-owned `ConfigStore`. @@ -15,7 +15,7 @@ - Modify only `/Users/lanh/Developer/pi-vault/pi-status`; Pi and Atelier repositories are read-only references. - Add no dependency, runtime palette fetch, graphical picker, CSS parser, import/export, or Pi global-theme mutation. - Use `Pi` (`pi`) as the default preset. `NO_COLOR` is an environment override, never a preset. -- Persist only uppercase `#RRGGBB` Custom values and retain exactly 14 editable semantic roles. +- Accept case-insensitive hex input, persist only lowercase `#rrggbb` values, and retain exactly 14 editable semantic roles. - Fixed and Custom presets emit truecolour; do not add a 256-colour conversion path. - Dashboard uses draft colours; installed surfaces change only after persistence succeeds. - Preserve malformed-file overwrite refusal and renderer plain-text fallbacks. @@ -26,7 +26,7 @@ ## File map -- Create `src/core/colors.ts`: preset IDs, sourced fixed constants, validation, normalization, cloning, and fixed-palette lookup. +- Create `src/core/colors.ts`: preset IDs, sourced fixed constants, validation, normalization, and fixed-palette lookup; use native `structuredClone` where an exact copy is needed. - Modify `src/shared/types.ts` and `src/core/config.ts`: shared contracts and canonical persistence. - Modify `src/tui/theme.ts`: the only production resolver for Pi, fixed, Custom, and `NO_COLOR` styling. - Modify Sidebar rendering/controller files: direct semantic-role painting and committed render-time resolution. @@ -39,6 +39,8 @@ ### Task 1: Add the preset catalogue and canonical persistence +**Executable source of truth:** `docs/superpowers/plans/2026-08-24-custom-colours-phase-1-catalog-config.md`. Its complete palette-value fixture, canonical-save regression, typed-fixture file map, and verification commands supersede the abbreviated examples below. + **Files:** - Create: `src/core/colors.ts` @@ -49,7 +51,7 @@ **Interfaces:** - Produces shared `PALETTE_ROLES`, `PaletteRole`, `ColorPreset`, `FixedColorPreset`, `HexColor`, `ColorPalette`, and `ColorSettings`. -- Produces `COLOR_PRESET_IDS`, `ATELIER_COLORS`, `FIXED_COLOR_PALETTES`, `DEFAULT_COLOR_SETTINGS`, `isHexColor`, `normalizeHexColor`, `getFixedColorPalette`, `cloneColorSettings`, and `normalizeColorSettings` from `src/core/colors.ts`. +- Produces `COLOR_PRESET_IDS`, `ATELIER_COLORS`, `FIXED_COLOR_PALETTES`, `DEFAULT_COLOR_SETTINGS`, `isHexColor`, `normalizeHexColor`, `getFixedColorPalette`, and `normalizeColorSettings` from `src/core/colors.ts`. - Produces required `PiStatusConfig.colors: ColorSettings` for later tasks. - [ ] **Step 1: Write failing configuration tests** @@ -90,12 +92,12 @@ describe("config — colours", () => { expect( normalizeColorSettings({ preset: "custom", - custom: { accent: "#abcdef", error: "broken", unknown: "#000000" }, + custom: { accent: "#AbCdEf", error: "broken", unknown: "#000000" }, }), ).toMatchObject({ preset: "custom", customInitialized: true, - custom: { accent: "#ABCDEF", error: ATELIER_COLORS.error }, + custom: { accent: "#abcdef", error: ATELIER_COLORS.error }, }); expect(normalizeColorSettings({ preset: "none" }).preset).toBe("pi"); expect(normalizeColorSettings({ preset: "unknown" }).preset).toBe("pi"); @@ -126,22 +128,22 @@ describe("config — colours", () => { expect(Object.keys(palette)).toEqual(PALETTE_ROLES); expect(Object.values(palette)).toHaveLength(14); expect( - Object.values(palette).every((value) => /^#[0-9A-F]{6}$/.test(value)), + Object.values(palette).every((value) => /^#[0-9a-f]{6}$/.test(value)), ).toBe(true); } }); }); ``` -Add exact assertions for one distinctive role from every sourced preset: +The executable Phase 1 plan replaces these smoke assertions with one complete role-ordered expected-value table for all seven fixed palettes: ```ts -expect(FIXED_COLOR_PALETTES["catppuccin-mocha"].accent).toBe("#CBA6F7"); -expect(FIXED_COLOR_PALETTES["catppuccin-latte"].primary).toBe("#4C4F69"); -expect(FIXED_COLOR_PALETTES.dracula.error).toBe("#FF5555"); -expect(FIXED_COLOR_PALETTES["dracula-alucard"].primary).toBe("#1F1F1F"); -expect(FIXED_COLOR_PALETTES["tokyonight-moon"].cache).toBe("#86E1FC"); -expect(FIXED_COLOR_PALETTES["tokyonight-day"].primary).toBe("#3760BF"); +expect(FIXED_COLOR_PALETTES["catppuccin-mocha"].accent).toBe("#cba6f7"); +expect(FIXED_COLOR_PALETTES["catppuccin-latte"].primary).toBe("#4c4f69"); +expect(FIXED_COLOR_PALETTES.dracula.error).toBe("#ff5555"); +expect(FIXED_COLOR_PALETTES["dracula-alucard"].primary).toBe("#1f1f1f"); +expect(FIXED_COLOR_PALETTES["tokyonight-moon"].cache).toBe("#86e1fc"); +expect(FIXED_COLOR_PALETTES["tokyonight-day"].primary).toBe("#3760bf"); ``` - [ ] **Step 2: Run the configuration test and verify RED** @@ -195,9 +197,11 @@ export type ColorSettings = { custom: ColorPalette; customInitialized: boolean; }; + +export type SidebarSegmentRole = PaletteRole; ``` -Add `colors: ColorSettings` to `PiStatusConfig`. +Replace the existing `SidebarSegmentRole` union with the alias above, then add `colors: ColorSettings` to `PiStatusConfig`. Do not retain a second copy of the same 14 roles. - [ ] **Step 4: Create the immutable preset catalogue** @@ -217,20 +221,20 @@ export const COLOR_PRESET_IDS = [ ] as const satisfies readonly ColorPreset[]; export const ATELIER_COLORS: Readonly = Object.freeze({ - accent: "#B18CFF", - primary: "#D4D4D4", + accent: "#b18cff", + primary: "#d4d4d4", muted: "#808080", dim: "#666666", - ready: "#6EA8FE", - working: "#FF9F43", - input: "#6EA8FE", - output: "#B18CFF", - cache: "#7DD3FC", - cost: "#FF9F43", - context: "#6EA8FE", - menu: "#B18CFF", - warning: "#FF9F43", - error: "#FF5D73", + ready: "#6ea8fe", + working: "#ff9f43", + input: "#6ea8fe", + output: "#b18cff", + cache: "#7dd3fc", + cost: "#ff9f43", + context: "#6ea8fe", + menu: "#b18cff", + warning: "#ff9f43", + error: "#ff5d73", }); export const FIXED_COLOR_PALETTES: Readonly< @@ -239,102 +243,102 @@ export const FIXED_COLOR_PALETTES: Readonly< atelier: ATELIER_COLORS, // Catppuccin palette 1.8.0: https://github.com/catppuccin/palette/blob/main/palette.json "catppuccin-mocha": Object.freeze({ - accent: "#CBA6F7", - primary: "#CDD6F4", - muted: "#A6ADC8", - dim: "#6C7086", - ready: "#A6E3A1", - working: "#FAB387", - input: "#89B4FA", - output: "#CBA6F7", - cache: "#89DCEB", - cost: "#FAB387", - context: "#89B4FA", - menu: "#F5C2E7", - warning: "#F9E2AF", - error: "#F38BA8", + accent: "#cba6f7", + primary: "#cdd6f4", + muted: "#a6adc8", + dim: "#6c7086", + ready: "#a6e3a1", + working: "#fab387", + input: "#89b4fa", + output: "#cba6f7", + cache: "#89dceb", + cost: "#fab387", + context: "#89b4fa", + menu: "#f5c2e7", + warning: "#f9e2af", + error: "#f38ba8", }), "catppuccin-latte": Object.freeze({ - accent: "#8839EF", - primary: "#4C4F69", - muted: "#6C6F85", - dim: "#9CA0B0", - ready: "#40A02B", - working: "#FE640B", - input: "#1E66F5", - output: "#8839EF", - cache: "#04A5E5", - cost: "#FE640B", - context: "#1E66F5", - menu: "#EA76CB", - warning: "#DF8E1D", - error: "#D20F39", + accent: "#8839ef", + primary: "#4c4f69", + muted: "#6c6f85", + dim: "#9ca0b0", + ready: "#40a02b", + working: "#fe640b", + input: "#1e66f5", + output: "#8839ef", + cache: "#04a5e5", + cost: "#fe640b", + context: "#1e66f5", + menu: "#ea76cb", + warning: "#df8e1d", + error: "#d20f39", }), // Dracula Classic/Alucard: https://github.com/dracula/dracula-theme#color-palette dracula: Object.freeze({ - accent: "#BD93F9", - primary: "#F8F8F2", - muted: "#6272A4", - dim: "#44475A", - ready: "#50FA7B", - working: "#FFB86C", - input: "#8BE9FD", - output: "#BD93F9", - cache: "#8BE9FD", - cost: "#FFB86C", - context: "#8BE9FD", - menu: "#FF79C6", - warning: "#F1FA8C", - error: "#FF5555", + accent: "#bd93f9", + primary: "#f8f8f2", + muted: "#6272a4", + dim: "#44475a", + ready: "#50fa7b", + working: "#ffb86c", + input: "#8be9fd", + output: "#bd93f9", + cache: "#8be9fd", + cost: "#ffb86c", + context: "#8be9fd", + menu: "#ff79c6", + warning: "#f1fa8c", + error: "#ff5555", }), "dracula-alucard": Object.freeze({ - accent: "#644AC9", - primary: "#1F1F1F", - muted: "#6C664B", - dim: "#6C664B", - ready: "#14710A", - working: "#A34D14", - input: "#036A96", - output: "#644AC9", - cache: "#036A96", - cost: "#A34D14", - context: "#036A96", - menu: "#A3144D", - warning: "#846E15", - error: "#CB3A2A", + accent: "#644ac9", + primary: "#1f1f1f", + muted: "#6c664b", + dim: "#6c664b", + ready: "#14710a", + working: "#a34d14", + input: "#036a96", + output: "#644ac9", + cache: "#036a96", + cost: "#a34d14", + context: "#036a96", + menu: "#a3144d", + warning: "#846e15", + error: "#cb3a2a", }), // TokyoNight: https://github.com/folke/tokyonight.nvim/tree/main/lua/tokyonight/colors "tokyonight-moon": Object.freeze({ - accent: "#C099FF", - primary: "#C8D3F5", - muted: "#636DA6", - dim: "#545C7E", - ready: "#C3E88D", - working: "#FF966C", - input: "#82AAFF", - output: "#C099FF", - cache: "#86E1FC", - cost: "#FF966C", - context: "#82AAFF", - menu: "#FCA7EA", - warning: "#FFC777", - error: "#FF757F", + accent: "#c099ff", + primary: "#c8d3f5", + muted: "#636da6", + dim: "#545c7e", + ready: "#c3e88d", + working: "#ff966c", + input: "#82aaff", + output: "#c099ff", + cache: "#86e1fc", + cost: "#ff966c", + context: "#82aaff", + menu: "#fca7ea", + warning: "#ffc777", + error: "#ff757f", }), "tokyonight-day": Object.freeze({ - accent: "#9854F1", - primary: "#3760BF", - muted: "#848CB5", - dim: "#8990B3", + accent: "#9854f1", + primary: "#3760bf", + muted: "#848cb5", + dim: "#8990b3", ready: "#587539", - working: "#B15C00", - input: "#2E7DE9", - output: "#9854F1", + working: "#b15c00", + input: "#2e7de9", + output: "#9854f1", cache: "#007197", - cost: "#B15C00", - context: "#2E7DE9", - menu: "#7847BD", - warning: "#8C6C3E", - error: "#F52A65", + cost: "#b15c00", + context: "#2e7de9", + menu: "#7847bd", + warning: "#8c6c3e", + error: "#f52a65", }), }); ``` @@ -362,13 +366,13 @@ export function normalizeHexColor( value: unknown, fallback: HexColor, ): HexColor { - return isHexColor(value) ? (value.toUpperCase() as HexColor) : fallback; + return isHexColor(value) ? (value.toLowerCase() as HexColor) : fallback; } export function getFixedColorPalette( preset: ColorPreset, ): Readonly | undefined { - return preset in FIXED_COLOR_PALETTES + return Object.hasOwn(FIXED_COLOR_PALETTES, preset) ? FIXED_COLOR_PALETTES[preset as FixedColorPreset] : undefined; } @@ -403,24 +407,16 @@ export function normalizeColorSettings(input: unknown): ColorSettings { customInitialized: value.customInitialized === true || preset === "custom", }; } - -export function cloneColorSettings(value: ColorSettings): ColorSettings { - return { - preset: value.preset, - custom: { ...value.custom }, - customInitialized: value.customInitialized, - }; -} ``` - [ ] **Step 6: Wire colours through config load, clone, and save** -In `src/core/config.ts`, import the colour helpers. Add an independent clone of `DEFAULT_COLOR_SETTINGS` to `DEFAULT_CONFIG` and `cloneDefaultConfig`; add `colors: normalizeColorSettings(input.colors)` to `normalizeConfig`; write `colors: cloneColorSettings(config.colors)` in `saveConfig`. +In `src/core/config.ts`, import `DEFAULT_COLOR_SETTINGS` and `normalizeColorSettings`. Add `colors: structuredClone(DEFAULT_COLOR_SETTINGS)` to `DEFAULT_CONFIG`, `colors: structuredClone(DEFAULT_CONFIG.colors)` to `cloneDefaultConfig`, `colors: normalizeColorSettings(input.colors)` to `normalizeConfig`, and `colors: normalizeColorSettings(config.colors)` to `saveConfig`. Saving must normalize rather than merely copy so JavaScript callers and cast values cannot persist uppercase, mixed-case, partial, or unknown colour data. Update every typed `PiStatusConfig` fixture reported by TypeScript with: ```ts -colors: cloneColorSettings(DEFAULT_COLOR_SETTINGS), +colors: structuredClone(DEFAULT_COLOR_SETTINGS), ``` Do not make `colors` optional or add casts to hide missing fixtures. @@ -431,15 +427,16 @@ Run: ```bash pnpm exec vitest run tests/core/config.test.ts -pnpm exec tsc --noEmit +pnpm check +git diff --check ``` -Expected: PASS with no missing `colors` fixtures. +Expected: the focused test passes, then formatting, lint, typecheck, and the full test suite pass, and `git diff --check` reports no whitespace errors. - [ ] **Step 8: Commit the catalogue checkpoint when authorized** ```bash -git add src/core/colors.ts src/shared/types.ts src/core/config.ts tests/core/config.test.ts +git add src/core/colors.ts src/shared/types.ts src/core/config.ts tests/core/config.test.ts tests/index.test.ts tests/index-save.test.ts tests/index-sidebar-layout.test.ts tests/core/resolve-footer.test.ts tests/core/runtime-state.test.ts tests/core/sidebar-layout.test.ts tests/tui/dashboard-render.test.ts tests/tui/dashboard-state.test.ts tests/tui/dashboard.test.ts tests/tui/sidebar-render.test.ts tests/tui/sidebar.test.ts git commit -m "feat: add colour preset catalogue" ``` @@ -968,12 +965,12 @@ In `tests/tui/dashboard.test.ts`, activate Accent and verify: ```ts expect(renderText()).toContain("Edit accent colour"); -component.handleInput("#010203"); +component.handleInput("#AbCdEf"); component.handleInput("\r"); -expect(component.getState().draft.colors.custom.accent).toBe("#010203"); +expect(component.getState().draft.colors.custom.accent).toBe("#abcdef"); ``` -Add invalid-submit and Escape tests. Invalid submit must leave the dialog open and draft unchanged, and notify `Colour must use #RRGGBB` with `warning` once per submit. +Add invalid-submit and Escape tests. Invalid submit must leave the dialog open and draft unchanged, and notify `Colour must use # followed by 6 hex digits` with `warning` once per submit. - [ ] **Step 5: Run Dashboard render/component tests and verify RED** @@ -985,7 +982,7 @@ Expected: FAIL because preset rendering and the colour dialog do not exist. - [ ] **Step 6: Implement labels, draft preview, and Input dialog** -In `src/tui/dashboard-render.ts`, add the exact labels above and render Colours as an adjustable `↔` row. Render each Custom row with its role, uppercase value, and a sample through `theme.fg(role, "●")`. +In `src/tui/dashboard-render.ts`, add the exact labels above and render Colours as an adjustable `↔` row. Render each Custom row with its role, lowercase value, and a sample through `theme.fg(role, "●")`. Extend `DashboardDialog`: @@ -1008,7 +1005,7 @@ Handle `edit_color` by seeding `Input` with the current value. On submit: ```ts if (!isHexColor(value)) { - this.warn("Colour must use #RRGGBB"); + this.warn("Colour must use # followed by 6 hex digits"); return; } this.dispatch({ @@ -1119,7 +1116,7 @@ In `README.md`: - List all nine labels in Dashboard order. - Group Catppuccin Mocha/Latte, Dracula/Alucard, and Tokyo Night Moon/Day as explicit dark/light choices. - Link the three official palette sources used by the local constants. -- Document first-use Custom seeding, 14 `#RRGGBB` roles, uppercase persistence, and truecolour requirements. +- Document first-use Custom seeding, 14 `#rrggbb` roles, case-insensitive input, lowercase persistence, and truecolour requirements. - State that `NO_COLOR` disables styling across Dashboard, Statusbar, and Sidebar without changing the saved selection. In `CHANGELOG.md`, add above released entries: @@ -1167,7 +1164,7 @@ Launch the local extension in Pi and open `/statusline` at approximately `120x30 3. All nine labels cycle in the documented order with wraparound. 4. Each fixed dark/light preset previews and saves consistently across all surfaces. 5. First Custom entry clones the selected fixed palette, or Atelier from Pi; later switches preserve it. -6. Custom exposes 14 scrollable roles; lowercase input saves uppercase, invalid input remains open with one warning per submit, and Escape cancels. +6. Custom exposes 14 scrollable roles; uppercase, lowercase, and mixed-case input saves lowercase, invalid input remains open with one warning per submit, and Escape cancels. 7. Draft changes recolour Dashboard immediately while installed surfaces wait for confirmed Save. 8. Failed Save leaves installed colours unchanged and Dashboard dirty. 9. `NO_COLOR` removes ANSI styling without changing the saved preset. diff --git a/docs/superpowers/specs/2026-08-24-custom-colours-design.md b/docs/superpowers/specs/2026-08-24-custom-colours-design.md index 6f52b0a..443d850 100644 --- a/docs/superpowers/specs/2026-08-24-custom-colours-design.md +++ b/docs/superpowers/specs/2026-08-24-custom-colours-design.md @@ -86,20 +86,20 @@ The default persisted shape is: "colors": { "preset": "pi", "custom": { - "accent": "#B18CFF", - "primary": "#D4D4D4", + "accent": "#b18cff", + "primary": "#d4d4d4", "muted": "#808080", "dim": "#666666", - "ready": "#6EA8FE", - "working": "#FF9F43", - "input": "#6EA8FE", - "output": "#B18CFF", - "cache": "#7DD3FC", - "cost": "#FF9F43", - "context": "#6EA8FE", - "menu": "#B18CFF", - "warning": "#FF9F43", - "error": "#FF5D73" + "ready": "#6ea8fe", + "working": "#ff9f43", + "input": "#6ea8fe", + "output": "#b18cff", + "cache": "#7dd3fc", + "cost": "#ff9f43", + "context": "#6ea8fe", + "menu": "#b18cff", + "warning": "#ff9f43", + "error": "#ff5d73" }, "customInitialized": false } @@ -111,7 +111,7 @@ The default persisted shape is: - Missing or non-object `colors` becomes `{ preset: "pi", custom: ATELIER_COLORS, customInitialized: false }`. - Only the nine declared IDs are valid. Unknown values, including the obsolete design-only value `none`, normalize to `pi`. - A Custom colour is valid only when it matches `/^#[0-9a-f]{6}$/i`. -- Valid values save in uppercase `#RRGGBB` form. +- Valid values are accepted case-insensitively and save in lowercase `#rrggbb` form. - A missing or invalid role falls back independently to the corresponding Atelier value. - Unknown Custom keys are ignored. - `customInitialized` is true only for persisted `true`, or when a manually supplied valid preset is `custom`; other values normalize to false. @@ -121,7 +121,7 @@ The default persisted shape is: ### Custom initialization - The first Dashboard transition into Custom while `customInitialized` is false copies the currently selected fixed palette. -- Entering Custom from Pi copies Atelier because Pi exposes live styling operations, not a stable `#RRGGBB` palette. +- Entering Custom from Pi copies Atelier because Pi exposes live styling operations, not a stable `#rrggbb` palette. - The transition sets `customInitialized` to true in the draft. It is therefore a persisted draft change even if the user cycles back to another preset before Save. - Once initialized, Custom values survive preset changes, Save, and restart. @@ -241,8 +241,8 @@ The Colours row displays the current label. Left/Right cycles through the catalo Each Custom row displays the role name, canonical hex value, and a sample painted with that exact role. Enter or Space opens the existing Pi TUI `Input`, seeded with the current value. Escape cancels. Submit validates the complete input: -- Valid input is uppercased, stored in `draft.colors.custom[role]`, and closes the dialog. -- Invalid input keeps the dialog open and reports `Colour must use #RRGGBB` once per submit attempt. +- Valid input is lowercased, stored in `draft.colors.custom[role]`, and closes the dialog. +- Invalid input keeps the dialog open and reports `Colour must use # followed by 6 hex digits` once per submit attempt. The Settings viewport scrolls to keep the selected role visible. @@ -272,7 +272,7 @@ Dashboard resolves its theme from `state.draft.colors` on every render, so prese - Legacy/missing configs default to Pi and an independent uninitialized Custom palette. - Every preset round-trips; unknown and `none` normalize to Pi. -- Lowercase hex saves uppercase; invalid/missing roles fall back independently. +- Uppercase, lowercase, and mixed-case hex save lowercase; invalid/missing roles fall back independently. - Unknown keys are omitted and palette objects are defensively copied. - All seven fixed palettes match their pinned source-token values. diff --git a/src/core/colors.ts b/src/core/colors.ts new file mode 100644 index 0000000..fab4db1 --- /dev/null +++ b/src/core/colors.ts @@ -0,0 +1,189 @@ +import { + PALETTE_ROLES, + type ColorPalette, + type ColorPreset, + type ColorSettings, + type FixedColorPreset, + type HexColor, +} from "../shared/types.ts"; + +export const COLOR_PRESET_IDS = [ + "pi", + "atelier", + "catppuccin-mocha", + "catppuccin-latte", + "dracula", + "dracula-alucard", + "tokyonight-moon", + "tokyonight-day", + "custom", +] as const satisfies readonly ColorPreset[]; + +export const ATELIER_COLORS: Readonly = Object.freeze({ + accent: "#b18cff", + primary: "#d4d4d4", + muted: "#808080", + dim: "#666666", + ready: "#6ea8fe", + working: "#ff9f43", + input: "#6ea8fe", + output: "#b18cff", + cache: "#7dd3fc", + cost: "#ff9f43", + context: "#6ea8fe", + menu: "#b18cff", + warning: "#ff9f43", + error: "#ff5d73", +}); + +export const FIXED_COLOR_PALETTES: Readonly>> = + Object.freeze({ + atelier: ATELIER_COLORS, + // Catppuccin palette 1.8.0: https://github.com/catppuccin/palette/blob/main/palette.json + "catppuccin-mocha": Object.freeze({ + accent: "#cba6f7", + primary: "#cdd6f4", + muted: "#a6adc8", + dim: "#6c7086", + ready: "#a6e3a1", + working: "#fab387", + input: "#89b4fa", + output: "#cba6f7", + cache: "#89dceb", + cost: "#fab387", + context: "#89b4fa", + menu: "#f5c2e7", + warning: "#f9e2af", + error: "#f38ba8", + }), + "catppuccin-latte": Object.freeze({ + accent: "#8839ef", + primary: "#4c4f69", + muted: "#6c6f85", + dim: "#9ca0b0", + ready: "#40a02b", + working: "#fe640b", + input: "#1e66f5", + output: "#8839ef", + cache: "#04a5e5", + cost: "#fe640b", + context: "#1e66f5", + menu: "#ea76cb", + warning: "#df8e1d", + error: "#d20f39", + }), + // Dracula Classic/Alucard: https://github.com/dracula/dracula-theme#color-palette + dracula: Object.freeze({ + accent: "#bd93f9", + primary: "#f8f8f2", + muted: "#6272a4", + dim: "#44475a", + ready: "#50fa7b", + working: "#ffb86c", + input: "#8be9fd", + output: "#bd93f9", + cache: "#8be9fd", + cost: "#ffb86c", + context: "#8be9fd", + menu: "#ff79c6", + warning: "#f1fa8c", + error: "#ff5555", + }), + "dracula-alucard": Object.freeze({ + accent: "#644ac9", + primary: "#1f1f1f", + muted: "#6c664b", + dim: "#6c664b", + ready: "#14710a", + working: "#a34d14", + input: "#036a96", + output: "#644ac9", + cache: "#036a96", + cost: "#a34d14", + context: "#036a96", + menu: "#a3144d", + warning: "#846e15", + error: "#cb3a2a", + }), + // TokyoNight: https://github.com/folke/tokyonight.nvim/tree/main/lua/tokyonight/colors + "tokyonight-moon": Object.freeze({ + accent: "#c099ff", + primary: "#c8d3f5", + muted: "#636da6", + dim: "#545c7e", + ready: "#c3e88d", + working: "#ff966c", + input: "#82aaff", + output: "#c099ff", + cache: "#86e1fc", + cost: "#ff966c", + context: "#82aaff", + menu: "#fca7ea", + warning: "#ffc777", + error: "#ff757f", + }), + "tokyonight-day": Object.freeze({ + accent: "#9854f1", + primary: "#3760bf", + muted: "#848cb5", + dim: "#8990b3", + ready: "#587539", + working: "#b15c00", + input: "#2e7de9", + output: "#9854f1", + cache: "#007197", + cost: "#b15c00", + context: "#2e7de9", + menu: "#7847bd", + warning: "#8c6c3e", + error: "#f52a65", + }), + }); + +export const DEFAULT_COLOR_SETTINGS: Readonly = Object.freeze({ + preset: "pi", + custom: ATELIER_COLORS, + customInitialized: false, +}); + +const PRESETS = new Set(COLOR_PRESET_IDS); + +export function isHexColor(value: unknown): value is HexColor { + return typeof value === "string" && /^#[0-9a-f]{6}$/i.test(value); +} + +export function normalizeHexColor(value: unknown, fallback: HexColor): HexColor { + return isHexColor(value) ? (value.toLowerCase() as HexColor) : fallback; +} + +export function getFixedColorPalette(preset: ColorPreset): Readonly | undefined { + return Object.hasOwn(FIXED_COLOR_PALETTES, preset) + ? FIXED_COLOR_PALETTES[preset as FixedColorPreset] + : undefined; +} + +export function normalizeColorSettings(input: unknown): ColorSettings { + const value = + input && typeof input === "object" && !Array.isArray(input) + ? (input as { + preset?: unknown; + custom?: unknown; + customInitialized?: unknown; + }) + : {}; + const preset = + typeof value.preset === "string" && PRESETS.has(value.preset as ColorPreset) + ? (value.preset as ColorPreset) + : "pi"; + const custom = + value.custom && typeof value.custom === "object" && !Array.isArray(value.custom) + ? (value.custom as Record) + : {}; + return { + preset, + custom: Object.fromEntries( + PALETTE_ROLES.map((role) => [role, normalizeHexColor(custom[role], ATELIER_COLORS[role])]), + ) as ColorPalette, + customInitialized: value.customInitialized === true || preset === "custom", + }; +} diff --git a/src/core/config.ts b/src/core/config.ts index c57b71c..647420b 100644 --- a/src/core/config.ts +++ b/src/core/config.ts @@ -25,6 +25,7 @@ import { type StatusLineSegmentId, type StatusLineZones, } from "../shared/types.ts"; +import { DEFAULT_COLOR_SETTINGS, normalizeColorSettings } from "./colors.ts"; import { isPersistedSidebarSegmentId, SIDEBAR_LAYOUT_MAX_ASSIGNMENTS, @@ -47,6 +48,7 @@ export const DEFAULT_CONFIG: PiStatusConfig = { completionNotifications: false, sidebarPanelLayout: cloneSidebarPanelLayout(DEFAULT_SIDEBAR_PANEL_LAYOUT), sidebarHiddenSegments: [], + colors: structuredClone(DEFAULT_COLOR_SETTINGS), }; function cloneDefaultConfig(): PiStatusConfig { @@ -59,6 +61,7 @@ function cloneDefaultConfig(): PiStatusConfig { completionNotifications: DEFAULT_CONFIG.completionNotifications, sidebarPanelLayout: cloneSidebarPanelLayout(DEFAULT_CONFIG.sidebarPanelLayout), sidebarHiddenSegments: [...DEFAULT_CONFIG.sidebarHiddenSegments], + colors: structuredClone(DEFAULT_CONFIG.colors), }; } @@ -265,6 +268,7 @@ function normalizeConfig(input: Record): PiStatusConfig { completionNotifications: input.completionNotifications === true, sidebarPanelLayout: layout.sidebarPanelLayout, sidebarHiddenSegments: layout.sidebarHiddenSegments, + colors: normalizeColorSettings(input.colors), }; } @@ -354,6 +358,7 @@ export function saveConfig( completionNotifications: config.completionNotifications, sidebarPanelLayout: sidebar.sidebarPanelLayout, sidebarHiddenSegments: sidebar.sidebarHiddenSegments.filter(isPersistedSidebarSegmentId), + colors: normalizeColorSettings(config.colors), }; store.write(path, `${JSON.stringify(next, null, 2)}\n`); return { path }; diff --git a/src/shared/types.ts b/src/shared/types.ts index 46542fb..1fc0d0f 100644 --- a/src/shared/types.ts +++ b/src/shared/types.ts @@ -73,21 +73,45 @@ export function isSidebarPanelId(value: unknown): value is SidebarPanelId { export type SidebarSegmentPersistence = "stable" | "session"; export type SidebarSegmentPriority = "required" | "important" | "normal" | "optional"; -export type SidebarSegmentRole = - | "accent" - | "primary" - | "muted" - | "dim" - | "ready" - | "working" - | "input" - | "output" - | "cache" - | "context" - | "cost" - | "menu" - | "warning" - | "error"; + +export const PALETTE_ROLES = [ + "accent", + "primary", + "muted", + "dim", + "ready", + "working", + "input", + "output", + "cache", + "cost", + "context", + "menu", + "warning", + "error", +] as const; + +export type PaletteRole = (typeof PALETTE_ROLES)[number]; +export type ColorPreset = + | "pi" + | "atelier" + | "catppuccin-mocha" + | "catppuccin-latte" + | "dracula" + | "dracula-alucard" + | "tokyonight-moon" + | "tokyonight-day" + | "custom"; +export type FixedColorPreset = Exclude; +export type HexColor = `#${string}`; +export type ColorPalette = Record; +export type ColorSettings = { + preset: ColorPreset; + custom: ColorPalette; + customInitialized: boolean; +}; + +export type SidebarSegmentRole = PaletteRole; export interface SidebarSegmentSpan { text: string; @@ -206,6 +230,7 @@ export type PiStatusConfig = { completionNotifications: boolean; sidebarPanelLayout: SidebarPanelLayout; sidebarHiddenSegments: string[]; + colors: ColorSettings; }; export const KNOWN_SEGMENTS: readonly StatusLineSegmentId[] = [ diff --git a/tests/core/config.test.ts b/tests/core/config.test.ts index 4102786..a85df47 100644 --- a/tests/core/config.test.ts +++ b/tests/core/config.test.ts @@ -18,13 +18,24 @@ import { normalizeZones, saveConfig, } from "../../src/core/config.ts"; +import { + ATELIER_COLORS, + COLOR_PRESET_IDS, + DEFAULT_COLOR_SETTINGS, + FIXED_COLOR_PALETTES, + getFixedColorPalette, + normalizeColorSettings, + normalizeHexColor, +} from "../../src/core/colors.ts"; import { BUILTIN_SIDEBAR_PANEL_IDS, type ConfigStore, DEFAULT_SIDEBAR_PANEL_LAYOUT, DEFAULT_ZONES, + PALETTE_ROLES, SIDEBAR_BUILTIN_ASSIGNMENTS, STATUS_LINE_ZONE_ORDER, + type FixedColorPreset, type PiStatusConfig, type SidebarPanelLayout, } from "../../src/shared/types.ts"; @@ -48,8 +59,248 @@ const config: PiStatusConfig = { completionNotifications: false, sidebarPanelLayout: expectedDefault(), sidebarHiddenSegments: [], + colors: structuredClone(DEFAULT_COLOR_SETTINGS), +}; + +const EXPECTED_FIXED_VALUES: Record = { + atelier: [ + "#b18cff", + "#d4d4d4", + "#808080", + "#666666", + "#6ea8fe", + "#ff9f43", + "#6ea8fe", + "#b18cff", + "#7dd3fc", + "#ff9f43", + "#6ea8fe", + "#b18cff", + "#ff9f43", + "#ff5d73", + ], + "catppuccin-mocha": [ + "#cba6f7", + "#cdd6f4", + "#a6adc8", + "#6c7086", + "#a6e3a1", + "#fab387", + "#89b4fa", + "#cba6f7", + "#89dceb", + "#fab387", + "#89b4fa", + "#f5c2e7", + "#f9e2af", + "#f38ba8", + ], + "catppuccin-latte": [ + "#8839ef", + "#4c4f69", + "#6c6f85", + "#9ca0b0", + "#40a02b", + "#fe640b", + "#1e66f5", + "#8839ef", + "#04a5e5", + "#fe640b", + "#1e66f5", + "#ea76cb", + "#df8e1d", + "#d20f39", + ], + dracula: [ + "#bd93f9", + "#f8f8f2", + "#6272a4", + "#44475a", + "#50fa7b", + "#ffb86c", + "#8be9fd", + "#bd93f9", + "#8be9fd", + "#ffb86c", + "#8be9fd", + "#ff79c6", + "#f1fa8c", + "#ff5555", + ], + "dracula-alucard": [ + "#644ac9", + "#1f1f1f", + "#6c664b", + "#6c664b", + "#14710a", + "#a34d14", + "#036a96", + "#644ac9", + "#036a96", + "#a34d14", + "#036a96", + "#a3144d", + "#846e15", + "#cb3a2a", + ], + "tokyonight-moon": [ + "#c099ff", + "#c8d3f5", + "#636da6", + "#545c7e", + "#c3e88d", + "#ff966c", + "#82aaff", + "#c099ff", + "#86e1fc", + "#ff966c", + "#82aaff", + "#fca7ea", + "#ffc777", + "#ff757f", + ], + "tokyonight-day": [ + "#9854f1", + "#3760bf", + "#848cb5", + "#8990b3", + "#587539", + "#b15c00", + "#2e7de9", + "#9854f1", + "#007197", + "#b15c00", + "#2e7de9", + "#7847bd", + "#8c6c3e", + "#f52a65", + ], }; +describe("config — colours", () => { + it("pins preset and role order", () => { + expect(COLOR_PRESET_IDS).toEqual([ + "pi", + "atelier", + "catppuccin-mocha", + "catppuccin-latte", + "dracula", + "dracula-alucard", + "tokyonight-moon", + "tokyonight-day", + "custom", + ]); + expect(PALETTE_ROLES).toEqual([ + "accent", + "primary", + "muted", + "dim", + "ready", + "working", + "input", + "output", + "cache", + "cost", + "context", + "menu", + "warning", + "error", + ]); + }); + + it("defaults missing colours to the live Pi preset", () => { + const first = loadConfig({ + agentDir: "/agent-a", + store: new MemoryConfigStore(), + }); + const second = loadConfig({ + agentDir: "/agent-b", + store: new MemoryConfigStore(), + }); + + expect(first.colors).toEqual(DEFAULT_COLOR_SETTINGS); + expect(first.colors).toEqual({ + preset: "pi", + custom: ATELIER_COLORS, + customInitialized: false, + }); + expect(first.colors.custom).not.toBe(second.colors.custom); + }); + + it("repairs malformed settings role by role", () => { + const input = { + preset: "custom", + custom: { accent: "#AbCdEf", error: "broken", unknown: "#000000" }, + }; + const normalized = normalizeColorSettings(input); + expect(normalized).toEqual({ + preset: "custom", + customInitialized: true, + custom: { ...ATELIER_COLORS, accent: "#abcdef" }, + }); + expect(normalized.custom).not.toBe(input.custom); + expect(normalizeColorSettings({ preset: "none" }).preset).toBe("pi"); + expect(normalizeColorSettings({ preset: "unknown" }).preset).toBe("pi"); + }); + + it("accepts every hex letter case and canonicalizes to lowercase", () => { + expect(normalizeHexColor("#ABCDEF", ATELIER_COLORS.accent)).toBe("#abcdef"); + expect(normalizeHexColor("#abcdef", ATELIER_COLORS.accent)).toBe("#abcdef"); + expect(normalizeHexColor("#AbCdEf", ATELIER_COLORS.accent)).toBe("#abcdef"); + }); + + it.each(COLOR_PRESET_IDS)("round-trips the %s preset", (preset) => { + const store = new MemoryConfigStore(); + const value = { + ...config, + colors: { + preset, + custom: { ...ATELIER_COLORS }, + customInitialized: preset === "custom", + }, + }; + + saveConfig(value, { agentDir: "/agent", store }); + + const written = JSON.parse(store.read(getConfigPath("/agent")) as string); + expect(written.colors).toEqual(value.colors); + expect(loadConfig({ agentDir: "/agent", store }).colors).toEqual(value.colors); + }); + + it("pins every value and freezes the fixed catalogue", () => { + expect(Object.isFrozen(FIXED_COLOR_PALETTES)).toBe(true); + for (const [preset, values] of Object.entries(EXPECTED_FIXED_VALUES)) { + const palette = FIXED_COLOR_PALETTES[preset as FixedColorPreset]; + expect(PALETTE_ROLES.map((role) => palette[role])).toEqual(values); + expect(Object.isFrozen(palette)).toBe(true); + } + expect(getFixedColorPalette("pi")).toBeUndefined(); + expect(getFixedColorPalette("custom")).toBeUndefined(); + }); + + it("canonicalizes colours before saving", () => { + const store = new MemoryConfigStore(); + saveConfig( + { + ...config, + colors: { + preset: "custom", + custom: { accent: "#ABCDEF", error: "broken", unknown: "#000000" }, + customInitialized: "yes", + } as unknown as PiStatusConfig["colors"], + }, + { agentDir: "/agent", store }, + ); + + const written = JSON.parse(store.read(getConfigPath("/agent")) as string); + expect(written.colors).toEqual({ + preset: "custom", + custom: { ...ATELIER_COLORS, accent: "#abcdef" }, + customInitialized: true, + }); + }); +}); + describe("config — normalization", () => { it("normalizes segments: dedupes, rejects unknowns and non-strings", () => { expect( @@ -510,6 +761,7 @@ describe("config — direct extension file", () => { completionNotifications: false, sidebarPanelLayout: expectedDefault(), sidebarHiddenSegments: [], + colors: structuredClone(DEFAULT_COLOR_SETTINGS), }); expect(store.accessPaths).toEqual([path]); expect(store.accessPaths.some((accessed) => accessed.includes("settings.json"))).toBe(false); @@ -546,6 +798,7 @@ describe("config — direct extension file", () => { completionNotifications: false, sidebarPanelLayout: expectedDefault(), sidebarHiddenSegments: [], + colors: structuredClone(DEFAULT_COLOR_SETTINGS), }); }, ); @@ -688,6 +941,7 @@ describe("config — filesystem", () => { completionNotifications: false, sidebarPanelLayout: expectedDefault(), sidebarHiddenSegments: [], + colors: structuredClone(DEFAULT_COLOR_SETTINGS), }); vi.mocked(mkdtempSync).mockClear(); expect(saveConfig(config)).toEqual({ path }); @@ -788,6 +1042,7 @@ describe("config — completion notifications", () => { expect(written).toEqual({ ...config, completionNotifications: true }); expect(Object.keys(written).sort()).toEqual( [ + "colors", "completionNotifications", "extensionSegments", "extensionStatusZone", diff --git a/tests/core/resolve-footer.test.ts b/tests/core/resolve-footer.test.ts index aa0f592..502c444 100644 --- a/tests/core/resolve-footer.test.ts +++ b/tests/core/resolve-footer.test.ts @@ -5,6 +5,7 @@ import { DEFAULT_ZONES, type PiStatusConfig, } from "../../src/shared/types.ts"; +import { DEFAULT_COLOR_SETTINGS } from "../../src/core/colors.ts"; import type { ThemeLike } from "../../src/tui/render.ts"; function makeInput(overrides?: Partial): SnapshotInput { @@ -40,6 +41,7 @@ function footerConfig(overrides: Partial = {}): PiStatusConfig { segments: [...panel.segments], })), sidebarHiddenSegments: [], + colors: structuredClone(DEFAULT_COLOR_SETTINGS), ...overrides, }; } diff --git a/tests/core/runtime-state.test.ts b/tests/core/runtime-state.test.ts index b1cc97c..8281dfd 100644 --- a/tests/core/runtime-state.test.ts +++ b/tests/core/runtime-state.test.ts @@ -6,6 +6,7 @@ import { SIDEBAR_BUILTIN_ASSIGNMENTS, type PiStatusConfig, } from "../../src/shared/types.ts"; +import { DEFAULT_COLOR_SETTINGS } from "../../src/core/colors.ts"; const defaultConfig: PiStatusConfig = { statusbarEnabled: true, @@ -25,6 +26,7 @@ const defaultConfig: PiStatusConfig = { segments: [...(SIDEBAR_BUILTIN_ASSIGNMENTS as Record)[id]], })), sidebarHiddenSegments: [], + colors: structuredClone(DEFAULT_COLOR_SETTINGS), }; function stubCtx(cwd = "/test"): ExtensionContext { @@ -97,6 +99,7 @@ describe("RuntimeStateMachine", () => { segments: [...(SIDEBAR_BUILTIN_ASSIGNMENTS as Record)[id]], })), sidebarHiddenSegments: [], + colors: structuredClone(DEFAULT_COLOR_SETTINGS), }; sm.update({ type: "config_reload", config: newConfig }); expect(sm.snapshot().config).toEqual(newConfig); diff --git a/tests/core/sidebar-layout.test.ts b/tests/core/sidebar-layout.test.ts index fd82a6d..2e72359 100644 --- a/tests/core/sidebar-layout.test.ts +++ b/tests/core/sidebar-layout.test.ts @@ -22,6 +22,7 @@ import { type PiStatusConfig, type SidebarCatalogEntry, } from "../../src/shared/types.ts"; +import { DEFAULT_COLOR_SETTINGS } from "../../src/core/colors.ts"; function config(overrides: Partial = {}): PiStatusConfig { return { @@ -36,6 +37,7 @@ function config(overrides: Partial = {}): PiStatusConfig { segments: [...entry.segments], })), sidebarHiddenSegments: [], + colors: structuredClone(DEFAULT_COLOR_SETTINGS), ...overrides, }; } diff --git a/tests/index-save.test.ts b/tests/index-save.test.ts index d501248..2a1667e 100644 --- a/tests/index-save.test.ts +++ b/tests/index-save.test.ts @@ -6,6 +6,7 @@ import { SIDEBAR_BUILTIN_ASSIGNMENTS, type PiStatusConfig, } from "../src/shared/types.ts"; +import { DEFAULT_COLOR_SETTINGS } from "../src/core/colors.ts"; import type { StatusLineDashboardComponent } from "../src/tui/dashboard.ts"; import { noTheme } from "../src/tui/theme.ts"; import { isDashboardDirty, selectableRows } from "../src/tui/dashboard-state.ts"; @@ -61,6 +62,7 @@ function config(): PiStatusConfig { segments: [...(SIDEBAR_BUILTIN_ASSIGNMENTS as Record)[id]], })), sidebarHiddenSegments: [], + colors: structuredClone(DEFAULT_COLOR_SETTINGS), }; } diff --git a/tests/index-sidebar-layout.test.ts b/tests/index-sidebar-layout.test.ts index 013b193..2f483d1 100644 --- a/tests/index-sidebar-layout.test.ts +++ b/tests/index-sidebar-layout.test.ts @@ -7,6 +7,7 @@ import { type PiStatusConfig, type SidebarPanelId, } from "../src/shared/types.ts"; +import { DEFAULT_COLOR_SETTINGS } from "../src/core/colors.ts"; import { SIDEBAR_PANEL_CHANNEL, SIDEBAR_PANEL_PROTOCOL_VERSION, @@ -43,6 +44,7 @@ function configWithModelIn(panelId: SidebarPanelId): PiStatusConfig { segments: id === panelId ? ["builtin:model"] : [], })), sidebarHiddenSegments: [], + colors: structuredClone(DEFAULT_COLOR_SETTINGS), }; } @@ -60,6 +62,7 @@ function configWithPanelsVisible(panels: SidebarPanelId[]): PiStatusConfig { segments: id === "agent" ? ["builtin:model"] : [], })), sidebarHiddenSegments: [], + colors: structuredClone(DEFAULT_COLOR_SETTINGS), }; } diff --git a/tests/index.test.ts b/tests/index.test.ts index 912e5f7..3f6b8af 100644 --- a/tests/index.test.ts +++ b/tests/index.test.ts @@ -6,6 +6,7 @@ import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-a import createExtension from "../src/index.ts"; import { BUILTIN_SIDEBAR_PANEL_IDS, SIDEBAR_BUILTIN_ASSIGNMENTS } from "../src/shared/types.ts"; import type { SidebarCatalogEntry, SidebarEffectiveLayout } from "../src/shared/types.ts"; +import { DEFAULT_COLOR_SETTINGS } from "../src/core/colors.ts"; import type { StatusLineDashboardComponent } from "../src/tui/dashboard.ts"; let agentDir: string; @@ -663,6 +664,7 @@ describe("extension wiring", () => { segments: [...(SIDEBAR_BUILTIN_ASSIGNMENTS as Record)[id]], })), sidebarHiddenSegments: ["tool:read"], + colors: structuredClone(DEFAULT_COLOR_SETTINGS), }); }); diff --git a/tests/tui/dashboard-render.test.ts b/tests/tui/dashboard-render.test.ts index 5ea4329..3eef70b 100644 --- a/tests/tui/dashboard-render.test.ts +++ b/tests/tui/dashboard-render.test.ts @@ -8,6 +8,7 @@ import { type PiStatusConfig, type StatusLineZones, } from "../../src/shared/types.ts"; +import { DEFAULT_COLOR_SETTINGS } from "../../src/core/colors.ts"; import type { DashboardTool } from "../../src/tui/tool-controls.ts"; import type { SessionDetails } from "../../src/tui/session-actions.ts"; import { @@ -42,6 +43,7 @@ function config(overrides: Partial = {}): PiStatusConfig { segments: [...(SIDEBAR_BUILTIN_ASSIGNMENTS as Record)[id]], })), sidebarHiddenSegments: [], + colors: structuredClone(DEFAULT_COLOR_SETTINGS), ...overrides, }; } diff --git a/tests/tui/dashboard-state.test.ts b/tests/tui/dashboard-state.test.ts index 89925b2..3c50d8b 100644 --- a/tests/tui/dashboard-state.test.ts +++ b/tests/tui/dashboard-state.test.ts @@ -8,6 +8,7 @@ import { type SidebarPanelId, type StatusLineZones, } from "../../src/shared/types.ts"; +import { DEFAULT_COLOR_SETTINGS } from "../../src/core/colors.ts"; import type { DashboardTool } from "../../src/tui/tool-controls.ts"; import type { SessionDetails } from "../../src/tui/session-actions.ts"; import { @@ -43,6 +44,7 @@ function config(overrides: Partial = {}): PiStatusConfig { segments: [...(SIDEBAR_BUILTIN_ASSIGNMENTS as Record)[id]], })), sidebarHiddenSegments: [], + colors: structuredClone(DEFAULT_COLOR_SETTINGS), ...overrides, }; } diff --git a/tests/tui/dashboard.test.ts b/tests/tui/dashboard.test.ts index 5a50c01..eec5132 100644 --- a/tests/tui/dashboard.test.ts +++ b/tests/tui/dashboard.test.ts @@ -7,6 +7,7 @@ import { SIDEBAR_BUILTIN_ASSIGNMENTS, type PiStatusConfig, } from "../../src/shared/types.ts"; +import { DEFAULT_COLOR_SETTINGS } from "../../src/core/colors.ts"; import { openStatusLineDashboard, StatusLineDashboardComponent } from "../../src/tui/dashboard.ts"; import { isDashboardDirty, selectableRows } from "../../src/tui/dashboard-state.ts"; import type { FooterRenderInput } from "../../src/tui/render.ts"; @@ -31,6 +32,7 @@ function config(): PiStatusConfig { segments: [...(SIDEBAR_BUILTIN_ASSIGNMENTS as Record)[id]], })), sidebarHiddenSegments: [], + colors: structuredClone(DEFAULT_COLOR_SETTINGS), }; } diff --git a/tests/tui/sidebar-render.test.ts b/tests/tui/sidebar-render.test.ts index bd8b688..b63ca2b 100644 --- a/tests/tui/sidebar-render.test.ts +++ b/tests/tui/sidebar-render.test.ts @@ -14,6 +14,7 @@ import { buildSidebarSnapshot, renderSidebarLines } from "../../src/tui/sidebar- import { noTheme, type StatusLineTheme } from "../../src/tui/theme.ts"; import { buildSidebarSegmentCatalog } from "../../src/tui/sidebar-segments.ts"; import { seedSidebarEffectiveLayout } from "../../src/core/sidebar-layout.ts"; +import { DEFAULT_COLOR_SETTINGS } from "../../src/core/colors.ts"; import { withDefaults } from "../helpers.ts"; type SidebarRenderFixtureInput = Parameters[0] & { @@ -53,6 +54,7 @@ function makeInput(overrides: Partial = {}): SidebarR completionNotifications: false, sidebarPanelLayout: [...DEFAULT_SIDEBAR_PANEL_LAYOUT], sidebarHiddenSegments: [], + colors: structuredClone(DEFAULT_COLOR_SETTINGS), }, persisted: true, branchEntryCount: 3, diff --git a/tests/tui/sidebar.test.ts b/tests/tui/sidebar.test.ts index 9514232..1fb3bc6 100644 --- a/tests/tui/sidebar.test.ts +++ b/tests/tui/sidebar.test.ts @@ -5,6 +5,7 @@ import type { PiStatusConfig } from "../../src/shared/types.ts"; import { DEFAULT_SIDEBAR_PANEL_LAYOUT, DEFAULT_ZONES } from "../../src/shared/types.ts"; import { createSidebarController } from "../../src/tui/sidebar.ts"; import { seedSidebarEffectiveLayout } from "../../src/core/sidebar-layout.ts"; +import { DEFAULT_COLOR_SETTINGS } from "../../src/core/colors.ts"; import { buildSidebarSegmentCatalog } from "../../src/tui/sidebar-segments.ts"; import { buildSidebarSnapshot, type SidebarSnapshot } from "../../src/tui/sidebar-render.ts"; import { @@ -51,6 +52,7 @@ const FIXED_CONFIG: PiStatusConfig = { completionNotifications: false, sidebarPanelLayout: [...DEFAULT_SIDEBAR_PANEL_LAYOUT], sidebarHiddenSegments: [], + colors: structuredClone(DEFAULT_COLOR_SETTINGS), }; function sidebarView(snapshot = FIXED_SNAPSHOT) {