From 9e90d23168149c494b1a3770a274c65d05ee86be Mon Sep 17 00:00:00 2001 From: roboomp Date: Sat, 13 Jun 2026 21:58:26 +0000 Subject: [PATCH 1/2] fix(theme): classify HTML export defaults by status-line luminance MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The HTML session export rendered empty text tokens (text, userMessageText, customMessageText, toolTitle) as the dark-theme grey #e5e5e7 on every theme whose name was not literally 'light'. getResolvedThemeColors picked defaultText off name === 'light', and the standalone isLightTheme helper classified on userMessageBg — both diverged from Theme.isLight, which has classified on statusLineBg since porcelain proved userMessageBg unreliable. Factor the status-line luminance into isLightThemeJson(themeJson) and route both helpers through it. defaultText now falls back to #000000 on every light theme (sandstone, limestone, porcelain, light, …) and to #e5e5e7 elsewhere, matching Theme.getColorHex. Regression test extends test/theme-islight.test.ts with the standalone helper and getResolvedThemeColors HTML-default coverage for sandstone + dark. Fixes #2516 --- packages/coding-agent/CHANGELOG.md | 4 ++ .../coding-agent/src/modes/theme/theme.ts | 38 ++++++++++++----- .../coding-agent/test/theme-islight.test.ts | 42 ++++++++++++++++++- 3 files changed, 72 insertions(+), 12 deletions(-) diff --git a/packages/coding-agent/CHANGELOG.md b/packages/coding-agent/CHANGELOG.md index 7fc96cb5e..e7f0e5b5d 100644 --- a/packages/coding-agent/CHANGELOG.md +++ b/packages/coding-agent/CHANGELOG.md @@ -2,6 +2,10 @@ ## [Unreleased] +### Fixed + +- Fixed HTML session export rendering empty text tokens (`text`, `userMessageText`, `customMessageText`, `toolTitle`) as the dark-theme grey `#e5e5e7` on every theme not literally named `light`, making transcripts illegible on custom light themes like `sandstone`, `limestone`, and `porcelain`. `getResolvedThemeColors` and the standalone `isLightTheme` helper now classify against the resolved `statusLineBg` luminance (the same surface `Theme.isLight` uses), so the HTML `defaultText` falls back to `#000000` on light themes and the standalone helper stays in lockstep with `Theme.isLight` ([#2516](https://github.com/can1357/oh-my-pi/issues/2516)). + ## [15.12.5] - 2026-06-13 ### Changed diff --git a/packages/coding-agent/src/modes/theme/theme.ts b/packages/coding-agent/src/modes/theme/theme.ts index a0e2390e1..05cf29e0c 100644 --- a/packages/coding-agent/src/modes/theme/theme.ts +++ b/packages/coding-agent/src/modes/theme/theme.ts @@ -2520,17 +2520,38 @@ function ansi256ToHex(index: number): string { return `#${grayHex}${grayHex}${grayHex}`; } +/** + * Classify a parsed theme JSON as light/dark by the perceived luminance of its + * status-line background. Mirrors {@link Theme.isLight} so the synchronous + * helpers below stay in lockstep with the runtime classifier — see the comment + * on `Theme.statusLineLuminance` for why `statusLineBg` is the source of truth + * (themes like `porcelain` style a dark chat bubble on an otherwise-light + * theme, so `userMessageBg` is unreliable). + */ +function isLightThemeJson(themeJson: ThemeJson): boolean { + try { + const resolved = resolveVarRefs(themeJson.colors.statusLineBg, themeJson.vars ?? {}); + const luminance = colorLuma(resolved); + return luminance !== undefined && luminance > 0.5; + } catch { + return false; + } +} + /** * Get resolved theme colors as CSS-compatible hex strings. * Used by HTML export to generate CSS custom properties. */ export async function getResolvedThemeColors(themeName?: string): Promise> { const name = themeName ?? getDefaultTheme(); - const isLight = name === "light"; const themeJson = await loadThemeJson(name); + const isLight = isLightThemeJson(themeJson); const resolved = resolveThemeColors(themeJson.colors, themeJson.vars); - // Default text color for empty values (terminal uses default fg color) + // Default text color for empty values (terminal uses default fg color). + // Must follow the actual theme appearance — hardcoding `name === "light"` + // makes every custom light theme (sandstone, limestone, porcelain, …) fall + // through to the dark-theme grey and renders the HTML export illegible. const defaultText = isLight ? "#000000" : "#e5e5e7"; const cssColors: Record = {}; @@ -2548,8 +2569,9 @@ export async function getResolvedThemeColors(themeName?: string): Promise 0.5; - } catch { - return false; - } + return isLightThemeJson(themeJson); } /** diff --git a/packages/coding-agent/test/theme-islight.test.ts b/packages/coding-agent/test/theme-islight.test.ts index 7e5af4b10..1a4b703c7 100644 --- a/packages/coding-agent/test/theme-islight.test.ts +++ b/packages/coding-agent/test/theme-islight.test.ts @@ -1,5 +1,9 @@ import { describe, expect, it } from "bun:test"; -import { getThemeByName } from "@oh-my-pi/pi-coding-agent/modes/theme/theme"; +import { + getResolvedThemeColors, + getThemeByName, + isLightTheme, +} from "@oh-my-pi/pi-coding-agent/modes/theme/theme"; describe("Theme.isLight", () => { it("classifies built-in themes by their status-line surface", async () => { @@ -20,3 +24,39 @@ describe("Theme.isLight", () => { expect(dark?.accentSurfaceLuminance).toBeUndefined(); }); }); + +describe("isLightTheme (standalone)", () => { + // Regression for #2516: the standalone helper used to classify on + // userMessageBg, mismatching Theme.isLight (statusLineBg) and the HTML + // export's defaultText. porcelain is the canonical mismatch (dark bubble, + // light status line); sandstone/limestone exercise the custom-light path. + it.each([ + ["sandstone", true], + ["limestone", true], + ["porcelain", true], + ["light", true], + ["dark", false], + ["dark-catppuccin", false], + ])("classifies %s as isLight=%s", (name, expected) => { + expect(isLightTheme(name)).toBe(expected); + }); +}); + +describe("getResolvedThemeColors HTML export defaults", () => { + // Regression for #2516: empty color tokens fell back to #e5e5e7 (the + // dark-theme grey) for every theme not literally named "light", making the + // session transcript text illegible on every custom light theme. + it("uses near-black for empty text tokens on light themes", async () => { + const colors = await getResolvedThemeColors("sandstone"); + expect(colors.text).toBe("#000000"); + expect(colors.userMessageText).toBe("#000000"); + expect(colors.customMessageText).toBe("#000000"); + expect(colors.toolTitle).toBe("#000000"); + }); + + it("uses light grey for empty text tokens on dark themes", async () => { + const colors = await getResolvedThemeColors("dark"); + expect(colors.text).toBe("#e5e5e7"); + expect(colors.userMessageText).toBe("#e5e5e7"); + }); +}); From 70cc306f05fc36922b7f035f6c149a0b17a70305 Mon Sep 17 00:00:00 2001 From: roboomp Date: Sat, 13 Jun 2026 21:58:44 +0000 Subject: [PATCH 2/2] style: bun run fix --- packages/coding-agent/test/theme-islight.test.ts | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/packages/coding-agent/test/theme-islight.test.ts b/packages/coding-agent/test/theme-islight.test.ts index 1a4b703c7..5cf282fea 100644 --- a/packages/coding-agent/test/theme-islight.test.ts +++ b/packages/coding-agent/test/theme-islight.test.ts @@ -1,9 +1,5 @@ import { describe, expect, it } from "bun:test"; -import { - getResolvedThemeColors, - getThemeByName, - isLightTheme, -} from "@oh-my-pi/pi-coding-agent/modes/theme/theme"; +import { getResolvedThemeColors, getThemeByName, isLightTheme } from "@oh-my-pi/pi-coding-agent/modes/theme/theme"; describe("Theme.isLight", () => { it("classifies built-in themes by their status-line surface", async () => {