Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion docs/skills/pr-review-decision.md
Original file line number Diff line number Diff line change
Expand Up @@ -215,7 +215,7 @@ Time-ordered list with owner + dependency. Strike through items that completed a
- Hyperlinks: full URLs, never bare `#11`
```

Use full markdown links for every PR / commit reference: `[PR #11](https://github.com/<org>/<repo>/pull/11)`, not `PR #11`. Same for table cells (`[#11](url)`) and English-prose mentions (`taken in [#12](url)`). To avoid nested-bracket bugs when running replace*all, scrub any pre-existing `[PR #11 - title](url)` style links to `[Pull request 11 — title](url)` \_first*, then replace_all `PR #11` → linked form.
Use full markdown links for every PR / commit reference: `[PR #11](https://github.com/<org>/<repo>/pull/11)`, not `PR #11`. Same for table cells (`[#11](url)`) and English-prose mentions (`taken in [#12](url)`). To avoid nested-bracket bugs when running `replace_all`, scrub any pre-existing `[PR #11 - title](url)` style links to `[Pull request 11 — title](url)` first, then run `replace_all` on `PR #11` → linked form.

### Step 6 — Bilingual comment drafting

Expand Down
32 changes: 17 additions & 15 deletions src/tui/app.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ import {
getTuiThemeNames,
hasTuiTheme,
mergeTuiThemes,
resolveTuiTranscriptRailColor,
resolveTuiTheme,
type StepCliTuiThemeColors,
type StepCliTuiThemeName,
Expand Down Expand Up @@ -1095,7 +1096,7 @@ export function StepCliTuiScreen(props: StepCliTuiScreenProps) {
setStatus({
tone: "accent",
label: "Theme",
detail: `Current ${themeName}. Available: ${availableThemesLabel}. Use /theme <name>.`,
detail: `Current ${themeName}. Available: ${availableThemesLabel}. Use /theme <name> or /theme ${DEFAULT_TUI_THEME_NAME} to recover the default theme.`,
});
return true;
}
Expand All @@ -1104,7 +1105,7 @@ export function StepCliTuiScreen(props: StepCliTuiScreenProps) {
setStatus({
tone: "warning",
label: "Theme",
detail: `Unknown theme "${requestedThemeName}". Available: ${availableThemesLabel}.`,
detail: `Unknown theme "${requestedThemeName}". Available: ${availableThemesLabel}. Use /theme ${DEFAULT_TUI_THEME_NAME} to recover the default theme.`,
});
return false;
}
Expand Down Expand Up @@ -1501,7 +1502,7 @@ const TranscriptPane = React.memo(function TranscriptPane(input: {
{summaryLines.length > 0 ? (
<box flexDirection="column" marginBottom={1}>
<text fg={input.theme.foreground}>
<span bg={input.theme.systemBadge} fg={input.theme.warning}>
<span bg={input.theme.systemBadge} fg={input.theme.foreground}>
{" "}
SUMMARY{" "}
</span>
Expand Down Expand Up @@ -2157,42 +2158,43 @@ function resolveTranscriptBadgeStyle(
textColor: string;
railColor: string;
} {
const railColor = resolveTuiTranscriptRailColor(theme);
switch (tone) {
case "accent":
return {
backgroundColor: theme.userBadge,
textColor: theme.accent,
railColor: theme.accent,
textColor: theme.foreground,
railColor,
};
case "brand":
return {
backgroundColor: theme.assistantBadge,
textColor: theme.brand,
railColor: theme.brand,
textColor: theme.foreground,
railColor,
};
case "success":
return {
backgroundColor: theme.toolBadge,
textColor: theme.success,
railColor: theme.success,
textColor: theme.foreground,
railColor,
};
case "warning":
return {
backgroundColor: theme.systemBadge,
textColor: theme.warning,
railColor: theme.warning,
textColor: theme.foreground,
railColor,
};
case "danger":
return {
backgroundColor: theme.systemBadge,
textColor: theme.danger,
railColor: theme.danger,
textColor: theme.foreground,
railColor,
};
case "muted":
return {
backgroundColor: theme.systemBadge,
textColor: theme.muted,
railColor: theme.line,
textColor: theme.foreground,
railColor,
};
}
}
Expand Down
106 changes: 106 additions & 0 deletions src/tui/theme.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
import { describe, expect, it } from "vitest";
import { getBuiltinTuiThemes, resolveTuiTranscriptRailColor } from "./theme.js";

function hexToRgb(hex: string): [number, number, number] {
const normalized = hex.trim().replace(/^#/, "");
const expanded =
normalized.length === 3
? normalized
.split("")
.map((character) => `${character}${character}`)
.join("")
: normalized.slice(0, 6);

return [
Number.parseInt(expanded.slice(0, 2), 16) / 255,
Number.parseInt(expanded.slice(2, 4), 16) / 255,
Number.parseInt(expanded.slice(4, 6), 16) / 255,
];
}

function linearize(channel: number): number {
return channel <= 0.03928
? channel / 12.92
: ((channel + 0.055) / 1.055) ** 2.4;
}

function relativeLuminance(hex: string): number {
const [red, green, blue] = hexToRgb(hex).map(linearize);
return 0.2126 * red + 0.7152 * green + 0.0722 * blue;
}

function contrastRatio(left: string, right: string): number {
const [lighter, darker] = [
relativeLuminance(left),
relativeLuminance(right),
].sort((first, second) => second - first);
return (lighter + 0.05) / (darker + 0.05);
}

describe("builtin TUI themes", () => {
const themes = getBuiltinTuiThemes();

it("keeps foreground and muted text readable on core surfaces", () => {
for (const theme of themes) {
expect(
contrastRatio(theme.colors.foreground, theme.colors.canvas),
`${theme.name} foreground vs canvas`,
).toBeGreaterThanOrEqual(7);
expect(
contrastRatio(theme.colors.foreground, theme.colors.panel),
`${theme.name} foreground vs panel`,
).toBeGreaterThanOrEqual(7);
expect(
contrastRatio(theme.colors.foreground, theme.colors.inputBackground),
`${theme.name} foreground vs inputBackground`,
).toBeGreaterThanOrEqual(7);
expect(
contrastRatio(theme.colors.muted, theme.colors.canvas),
`${theme.name} muted vs canvas`,
).toBeGreaterThanOrEqual(4.5);
expect(
contrastRatio(theme.colors.muted, theme.colors.panel),
`${theme.name} muted vs panel`,
).toBeGreaterThanOrEqual(4.5);
expect(
contrastRatio(theme.colors.foreground, theme.colors.selection),
`${theme.name} foreground vs selection`,
).toBeGreaterThanOrEqual(4.5);
}
});

it("keeps transcript rails and badges visible against panel surfaces", () => {
for (const theme of themes) {
const transcriptRailColor = resolveTuiTranscriptRailColor(theme.colors);

expect(
contrastRatio(transcriptRailColor, theme.colors.panel),
`${theme.name} transcript rail vs panel`,
).toBeGreaterThanOrEqual(3);
expect(
contrastRatio(transcriptRailColor, theme.colors.panelAlt),
`${theme.name} transcript rail vs panelAlt`,
).toBeGreaterThanOrEqual(3);
expect(
contrastRatio(transcriptRailColor, theme.colors.inputBackground),
`${theme.name} transcript rail vs inputBackground`,
).toBeGreaterThanOrEqual(3);
expect(
contrastRatio(theme.colors.foreground, theme.colors.assistantBadge),
`${theme.name} badge foreground vs assistantBadge`,
).toBeGreaterThanOrEqual(4.5);
expect(
contrastRatio(theme.colors.foreground, theme.colors.userBadge),
`${theme.name} badge foreground vs userBadge`,
).toBeGreaterThanOrEqual(4.5);
expect(
contrastRatio(theme.colors.foreground, theme.colors.toolBadge),
`${theme.name} badge foreground vs toolBadge`,
).toBeGreaterThanOrEqual(4.5);
expect(
contrastRatio(theme.colors.foreground, theme.colors.systemBadge),
`${theme.name} badge foreground vs systemBadge`,
).toBeGreaterThanOrEqual(4.5);
}
});
});
22 changes: 15 additions & 7 deletions src/tui/theme.ts
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ export type StepCliTuiThemeName = string;
const BUILTIN_TUI_THEME_COLORS = {
default: {
foreground: "#eef6ff",
muted: "#7d93ab",
muted: "#8eaac8",
accent: "#62d8ff",
brand: "#4da3ff",
success: "#58d6a6",
Expand All @@ -59,15 +59,15 @@ const BUILTIN_TUI_THEME_COLORS = {
panelAlt: "#112033",
inputBackground: "#1b2431",
selection: "#173556",
line: "#234462",
line: "#688fb1",
assistantBadge: "#102846",
userBadge: "#0b223d",
toolBadge: "#0d2b36",
systemBadge: "#17273a",
},
sage: {
foreground: "#eef4df",
muted: "#818274",
muted: "#a7b7a2",
accent: "#bde038",
brand: "#a3ab78",
success: "#bde038",
Expand All @@ -78,7 +78,7 @@ const BUILTIN_TUI_THEME_COLORS = {
panelAlt: "#16515c",
inputBackground: "#18363b",
selection: "#245258",
line: "#506266",
line: "#8ca7aa",
assistantBadge: "#1a3940",
userBadge: "#17343a",
toolBadge: "#233d35",
Expand All @@ -97,7 +97,7 @@ const BUILTIN_TUI_THEME_COLORS = {
panelAlt: "#2b1d3d",
inputBackground: "#251c35",
selection: "#382353",
line: "#5a4675",
line: "#8874b0",
assistantBadge: "#2a1846",
userBadge: "#113a37",
toolBadge: "#3c3712",
Expand All @@ -116,7 +116,7 @@ const BUILTIN_TUI_THEME_COLORS = {
panelAlt: "#0c2a62",
inputBackground: "#0f2446",
selection: "#163972",
line: "#1c4d8e",
line: "#5a8fda",
assistantBadge: "#0a2752",
userBadge: "#0a3448",
toolBadge: "#093b3b",
Expand All @@ -135,7 +135,7 @@ const BUILTIN_TUI_THEME_COLORS = {
panelAlt: "#0c1a2d",
inputBackground: "#102033",
selection: "#18304d",
line: "#35516e",
line: "#607d9d",
assistantBadge: "#0a1c32",
userBadge: "#0e2739",
toolBadge: "#122631",
Expand Down Expand Up @@ -211,6 +211,14 @@ export function resolveTuiTheme(
);
}

export function resolveTuiTranscriptRailColor(
colors: StepCliTuiThemeColors,
): string {
// Transcript rails are structural markers, so keep them tied to the
// contrast-checked line color instead of tone colors that may be too dark.
return colors.line;
}

export function isValidTuiThemeName(value: string): boolean {
return /^[a-z0-9]+(?:[-_][a-z0-9]+)*$/i.test(value.trim());
}
Expand Down
Loading