From 5ce0d15b66dd267a99900f0d5f02806d88928ce5 Mon Sep 17 00:00:00 2001 From: Mario Zechner Date: Tue, 31 Mar 2026 14:34:59 +0200 Subject: [PATCH] fix(coding-agent): resolve theme export variables closes #2707 --- packages/coding-agent/CHANGELOG.md | 4 + .../src/modes/interactive/theme/theme.ts | 14 +-- .../coding-agent/test/theme-export.test.ts | 104 ++++++++++++++++++ 3 files changed, 113 insertions(+), 9 deletions(-) create mode 100644 packages/coding-agent/test/theme-export.test.ts diff --git a/packages/coding-agent/CHANGELOG.md b/packages/coding-agent/CHANGELOG.md index f528c057..0b203157 100644 --- a/packages/coding-agent/CHANGELOG.md +++ b/packages/coding-agent/CHANGELOG.md @@ -75,6 +75,10 @@ After runtime replacement, use `runtimeHost.session` as the new live session and - Added label timestamps to the session tree with a `Shift+T` toggle in `/tree`, smart date formatting, and timestamp preservation through branching ([#2691](https://github.com/badlogic/pi-mono/pull/2691) by [@w-winter](https://github.com/w-winter)) +### Fixed + +- Fixed theme `export` colors to resolve theme variables the same way as `colors`, so `/export` HTML backgrounds now honor entries like `pageBg: "base"` instead of requiring inline hex values ([#2707](https://github.com/badlogic/pi-mono/issues/2707)) + ## [0.64.0] - 2026-03-29 ### New Features diff --git a/packages/coding-agent/src/modes/interactive/theme/theme.ts b/packages/coding-agent/src/modes/interactive/theme/theme.ts index 498c3131..48d0e72a 100644 --- a/packages/coding-agent/src/modes/interactive/theme/theme.ts +++ b/packages/coding-agent/src/modes/interactive/theme/theme.ts @@ -915,16 +915,12 @@ export function getThemeExportColors(themeName?: string): { if (!exportSection) return {}; const vars = themeJson.vars ?? {}; - const resolve = (value: string | number | undefined): string | undefined => { + const resolve = (value: ColorValue | undefined): string | undefined => { if (value === undefined) return undefined; - if (typeof value === "number") return ansi256ToHex(value); - if (value.startsWith("$")) { - const resolved = vars[value]; - if (resolved === undefined) return undefined; - if (typeof resolved === "number") return ansi256ToHex(resolved); - return resolved; - } - return value; + const resolved = resolveVarRefs(value, vars); + if (typeof resolved === "number") return ansi256ToHex(resolved); + if (resolved === "") return undefined; + return resolved; }; return { diff --git a/packages/coding-agent/test/theme-export.test.ts b/packages/coding-agent/test/theme-export.test.ts new file mode 100644 index 00000000..84dd649a --- /dev/null +++ b/packages/coding-agent/test/theme-export.test.ts @@ -0,0 +1,104 @@ +import { mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from "fs"; +import { tmpdir } from "os"; +import { join } from "path"; +import { afterEach, beforeEach, describe, expect, it } from "vitest"; +import { getThemeExportColors } from "../src/modes/interactive/theme/theme.js"; + +type ThemeFile = { + name: string; + vars?: Record; + colors: Record; + export?: { + pageBg?: string | number; + cardBg?: string | number; + infoBg?: string | number; + }; +}; + +describe("getThemeExportColors", () => { + let tempRoot: string; + let previousAgentDir: string | undefined; + + beforeEach(() => { + tempRoot = mkdtempSync(join(tmpdir(), "pi-theme-export-")); + previousAgentDir = process.env.PI_CODING_AGENT_DIR; + process.env.PI_CODING_AGENT_DIR = join(tempRoot, "agent"); + mkdirSync(join(process.env.PI_CODING_AGENT_DIR, "themes"), { recursive: true }); + }); + + afterEach(() => { + rmSync(tempRoot, { recursive: true, force: true }); + if (previousAgentDir === undefined) { + delete process.env.PI_CODING_AGENT_DIR; + } else { + process.env.PI_CODING_AGENT_DIR = previousAgentDir; + } + }); + + it("resolves export variable references using the same syntax as colors", () => { + const darkTheme = JSON.parse( + readFileSync(new URL("../src/modes/interactive/theme/dark.json", import.meta.url), "utf-8"), + ) as ThemeFile; + + const customTheme: ThemeFile = { + ...darkTheme, + name: "custom-export-vars", + vars: { + ...(darkTheme.vars ?? {}), + pageBgVar: "#112233", + pageBgAlias: "pageBgVar", + infoBgVar: "#445566", + cardBgVar: "#223344", + }, + export: { + pageBg: "pageBgAlias", + cardBg: "cardBgVar", + infoBg: "infoBgVar", + }, + }; + + writeFileSync( + join(process.env.PI_CODING_AGENT_DIR!, "themes", "custom-export-vars.json"), + JSON.stringify(customTheme, null, 2), + ); + + expect(getThemeExportColors("custom-export-vars")).toEqual({ + pageBg: "#112233", + cardBg: "#223344", + infoBg: "#445566", + }); + }); + + it("resolves recursive vars and converts 256-color export values to hex", () => { + const darkTheme = JSON.parse( + readFileSync(new URL("../src/modes/interactive/theme/dark.json", import.meta.url), "utf-8"), + ) as ThemeFile; + + const customTheme: ThemeFile = { + ...darkTheme, + name: "custom-export-recursive", + vars: { + ...(darkTheme.vars ?? {}), + deepPageBg: "#abcdef", + pageBgAlias: "deepPageBg", + cardBgAnsi: 24, + }, + export: { + pageBg: "pageBgAlias", + cardBg: "cardBgAnsi", + infoBg: "", + }, + }; + + writeFileSync( + join(process.env.PI_CODING_AGENT_DIR!, "themes", "custom-export-recursive.json"), + JSON.stringify(customTheme, null, 2), + ); + + expect(getThemeExportColors("custom-export-recursive")).toEqual({ + pageBg: "#abcdef", + cardBg: "#005f87", + infoBg: undefined, + }); + }); +});