Merge PR #2519: classify HTML export theme defaults by status-line luminance
This commit is contained in:
@@ -80,6 +80,10 @@
|
||||
|
||||
- Added the `mnemopi.embeddingVariant` setting (`en` | `multilingual`) selecting a stronger SOTA local embedding model — `en` → `BAAI/bge-base-en-v1.5` (768d), `multilingual` → `intfloat/multilingual-e5-large` (1024d). Resolution precedence is `mnemopi.embeddingModel` setting > `MNEMOPI_EMBEDDING_MODEL` env > variant default, so the documented env override is still honored. Changing the active model wipes and rebuilds stored embeddings on the next writable start ([#2476](https://github.com/can1357/oh-my-pi/issues/2476))
|
||||
|
||||
### 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
|
||||
|
||||
|
||||
@@ -2532,17 +2532,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<Record<string, string>> {
|
||||
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<string, string> = {};
|
||||
@@ -2560,8 +2581,9 @@ export async function getResolvedThemeColors(themeName?: string): Promise<Record
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a theme is a "light" theme by analyzing its background color luminance.
|
||||
* Loads theme JSON synchronously (built-in or custom file) and resolves userMessageBg.
|
||||
* Check if a theme is a "light" theme by analyzing its status-line background
|
||||
* luminance. Loads theme JSON synchronously (built-in or custom file on disk)
|
||||
* for callers in synchronous flows (settings migration, setup wizard).
|
||||
*/
|
||||
export function isLightTheme(themeName?: string): boolean {
|
||||
const name = themeName ?? "dark";
|
||||
@@ -2578,13 +2600,7 @@ export function isLightTheme(themeName?: string): boolean {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
try {
|
||||
const resolved = resolveVarRefs(themeJson.colors.userMessageBg, themeJson.vars ?? {});
|
||||
const luminance = colorLuma(resolved);
|
||||
return luminance !== undefined && luminance > 0.5;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
return isLightThemeJson(themeJson);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
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 +20,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");
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user