diff --git a/packages/coding-agent/CHANGELOG.md b/packages/coding-agent/CHANGELOG.md index 03ec72a40..909f74e2d 100644 --- a/packages/coding-agent/CHANGELOG.md +++ b/packages/coding-agent/CHANGELOG.md @@ -8,6 +8,7 @@ - Fixed llama.cpp discovery to prefer per-model `/v1/models` `meta.n_ctx`/`meta.n_ctx_train` values, refresh selected models after lazy load, and bypass fresh-cache reuse so server restarts update context windows. ([#3310](https://github.com/can1357/oh-my-pi/issues/3310)) - Fixed `task.maxConcurrency: 0` serializing subagent spawns instead of running them unbounded. The settings UI labels `0` as "Unlimited", but the session-scoped spawn `Semaphore` clamped `max` via `Math.max(1, max)`, so the second subagent body in a batch always waited for the first to release the seat. The constructor now treats `max <= 0` (and any non-finite input) as unbounded via `Number.POSITIVE_INFINITY`, matching the eval `parallel()`/`pipeline()` worker-pool semantics ([#3305](https://github.com/can1357/oh-my-pi/issues/3305)). - Fixed MCP tool calls forwarding empty optional placeholder arguments (`""` and `{}`) to `tools/call`; optional placeholders are now omitted while required fields and meaningful falsy values are preserved. ([#3302](https://github.com/can1357/oh-my-pi/issues/3302)) +- Fixed the welcome `Tip:` line rendering with hardcoded `#b48cff` / `#9ccfff` pastels plus a manual `\x1b[2m` dim, so any light theme dropped the body to ~1.5:1 contrast (well under WCAG AA). `renderWelcomeTip` in `packages/coding-agent/src/modes/components/welcome.ts` now paints the label through `theme.fg("customMessageLabel", …)` and the body through `theme.fg("muted", …)` (no manual dim), so the line tracks the active theme and stays legible on light backgrounds. ([#3337](https://github.com/can1357/oh-my-pi/issues/3337)) ## [16.1.16] - 2026-06-23 diff --git a/packages/coding-agent/src/modes/components/welcome.ts b/packages/coding-agent/src/modes/components/welcome.ts index cc1fadf4a..f4fedf2af 100644 --- a/packages/coding-agent/src/modes/components/welcome.ts +++ b/packages/coding-agent/src/modes/components/welcome.ts @@ -95,24 +95,23 @@ export function renderWelcomeTip(tip: string, boxWidth: number, phase = 0): stri const wrappedBody = wrapTextWithAnsi(replaceTabs(body), bodyBudget); if (wrappedBody.length === 0) return []; - const encoding: ColorEncoding = TERMINAL.trueColor ? "ansi-16m" : "ansi-256"; - const purple = Bun.color("#b48cff", encoding) ?? ""; - const lightBlue = Bun.color("#9ccfff", encoding) ?? ""; - const italic = "\x1b[3m"; - const dim = "\x1b[2m"; - const reset = "\x1b[0m"; + // Pull both colors from the active theme so the line stays readable on light + // themes; the previous hardcoded `#b48cff` / `#9ccfff` pastels (plus a manual + // `\x1b[2m` dim on the body) dropped to ~1.5:1 contrast on a white background. const continuationIndent = padding(labelWidth); + const styledLabel = theme.fg("customMessageLabel", label); - const lines = wrappedBody.map((line, index) => - index === 0 - ? ` ${italic}${purple}${label}${dim}${lightBlue}${line}${reset}` - : ` ${italic}${continuationIndent}${dim}${lightBlue}${line}${reset}`, - ); + const lines = wrappedBody.map((line, index) => { + const styledBody = theme.fg("muted", line); + const content = index === 0 ? `${styledLabel}${styledBody}` : `${continuationIndent}${styledBody}`; + return ` ${theme.italic(content)}`; + }); if (isNew) { // Append the rainbow tag to the final body line when it fits within the // box; otherwise drop it onto its own indented continuation line so the // styled glyphs never overflow or reflow the wrapped body. + const encoding: ColorEncoding = TERMINAL.trueColor ? "ansi-16m" : "ansi-256"; const tag = renderNewTag(phase, encoding); const tagWidth = 1 + visibleWidth(NEW_TAG_TEXT); // 1 = space separator const lastLine = lines[lines.length - 1]; @@ -393,8 +392,8 @@ export class WelcomeComponent implements Component { } /** - * Render the per-instance tip line: a purple "Tip:" label followed by the - * tip body in dimmed light blue, the whole line italicized. Returns `[]` + * Render the per-instance tip line: the `customMessageLabel`-themed `Tip:` + * label followed by a `muted` body, the whole line italicized. Returns `[]` * when no tip is available or the box is too narrow to be useful. */ #renderTip(boxWidth: number): string[] { diff --git a/packages/coding-agent/test/welcome-tip.test.ts b/packages/coding-agent/test/welcome-tip.test.ts index 12429ac2f..810a0cbb5 100644 --- a/packages/coding-agent/test/welcome-tip.test.ts +++ b/packages/coding-agent/test/welcome-tip.test.ts @@ -1,8 +1,12 @@ -import { describe, expect, it } from "bun:test"; +import { beforeAll, describe, expect, it } from "bun:test"; import { renderWelcomeTip } from "@oh-my-pi/pi-coding-agent/modes/components/welcome"; +import { initTheme, setTheme, theme } from "@oh-my-pi/pi-coding-agent/modes/theme/theme"; import { visibleWidth } from "@oh-my-pi/pi-tui"; - describe("renderWelcomeTip", () => { + beforeAll(async () => { + await initTheme(false); + }); + it("wraps long tips under the label instead of truncating", () => { const tip = "Next time you see spaghetti try creating a TTSR rule that prevents this pattern before it spreads"; const width = 44; @@ -57,4 +61,33 @@ describe("renderWelcomeTip", () => { expect(plain).not.toContain("NEW!"); expect(plain).toContain("Tip: Plain old tip"); }); + + it("derives label and body colors from the active theme, with no manual dim layer", async () => { + // Regression for #3337: hardcoded #b48cff/#9ccfff plus a manual `\x1b[2m` + // dropped the body to ~1.5:1 contrast on any light-theme background. + await setTheme("dark"); + const darkLabelAnsi = theme.getFgAnsi("customMessageLabel"); + const darkMutedAnsi = theme.getFgAnsi("muted"); + const dark = renderWelcomeTip("Welcome aboard friend", 60).join("\n"); + + await setTheme("light"); + const lightLabelAnsi = theme.getFgAnsi("customMessageLabel"); + const lightMutedAnsi = theme.getFgAnsi("muted"); + const light = renderWelcomeTip("Welcome aboard friend", 60).join("\n"); + + // Each theme paints with its own tokens. + expect(dark).toContain(darkLabelAnsi); + expect(dark).toContain(darkMutedAnsi); + expect(light).toContain(lightLabelAnsi); + expect(light).toContain(lightMutedAnsi); + + // Switching themes must change the emitted bytes — the previous bug + // produced byte-identical output for both. + expect(dark).not.toBe(light); + + // The manual `\x1b[2m` dim is gone; muted/label tokens carry their own + // theme-tuned luminance. + expect(dark).not.toContain("\x1b[2m"); + expect(light).not.toContain("\x1b[2m"); + }); });