diff --git a/packages/coding-agent/CHANGELOG.md b/packages/coding-agent/CHANGELOG.md index 62716c0ea..fa2c230b7 100644 --- a/packages/coding-agent/CHANGELOG.md +++ b/packages/coding-agent/CHANGELOG.md @@ -4,15 +4,15 @@ ### Added -- Added `composer.shape` setting (`/settings` → Appearance → Composer) to customize the editor's visual layout, including live layout previews in settings and the setup wizard +- Added `composer.shape` setting (`/settings` → Appearance → Composer) to customize the editor's visual layout — rounded box (default), Claude Code rules with the right status group chipped onto the top rule, upstream-pi rules, or borderless — with live layout previews in settings and a new setup-wizard scene. Non-box shapes render the status bar as a plain bottom line (no powerline caps or background) that yields its row to the autocomplete menu. +- Added `statusLine.contextLine` (Context-Reactive Line): the gauge line between the status groups tracks context usage — `off` (solid accent), `percentage` (used/unused split), `annotated` (default; adds boundary markers where speculative compaction starts and auto-compaction fires), or `embedded` (annotated plus in-gauge percentage/window labels). - Added `omp ps` for inspecting and controlling daemon-broker supervised processes from outside the harness: an interactive alt-screen monitor on TTYs (live table, info/logs views, stop/kill/restart, all-scopes toggle) plus static `--plain`/`--json` listings and `info`/`logs`/`stop`/`kill`/`restart` subactions with `--all`, `--dir`, and `--global` scope selectors. Brokers now record their project directory in `scope.json` so runtime scopes can be mapped back to projects offline. - Added `qwenTemplateReasoningEffort` to the `models.yml` `compat` schema, so the auto-enabled Qwen 3.8+ template effort dialect (`chat_template_kwargs.reasoning_effort`) can be switched off per provider/model for strict local servers that reject unknown `chat_template_kwargs`. - Added `tokenizer` to custom model and `modelOverrides` configuration. It overrides the catalog-resolved local tokenizer family for a model when a proxy serves a known model id with a different tokenizer. - Added `extendedContext` setting (`/settings` → Context → General, default on). When off, models with a premium long-context price tier (OpenAI GPT-5.6 Sol/Terra/Luna bill 2x input / 1.5x output above 272K input tokens, on both the API and subscription Codex) are capped at the standard-pricing threshold — they appear as 272K again and compaction fires before a request crosses into premium billing. Toggling mid-session re-clamps or restores the active model's window immediately. Anthropic Claude 4.6+ serves its full 1M window at standard pricing, so no Anthropic model is affected. - Added click-to-toggle and drag-to-reorder controls for list-valued `/settings` editors. - Added `compaction.asyncEnabled` (Async Compaction, default on): when context enters the band just below the compaction threshold, maintenance speculatively summarizes in the background off a branch snapshot (first configured LLM-backed method — remote, handoff, or soft — isolated from the live turn by a side session id) and holds the armed result; crossing the threshold then splices it in instantly instead of blocking on a summarization round-trip. Armed results are invalidated by branch changes, reset boundaries, model switches that strand provider-native replay payloads, and context growth past `keepRecentTokens` (which re-speculates). The status line pulses the auto-compact icon while a speculation runs and holds it in accent once a result is armed. -- Added auto-backgrounding for eval cells, mirroring the bash policy: with `eval.autoBackground.enabled` (default off), a cell still running after `eval.autoBackground.thresholdMs` (default 60s) — or when a queued user/peer message steers mid-wait — converts into a background job that keeps executing on the kernel and delivers its result automatically. Both settings inherit the RPC-host default treatment alongside their bash counterparts. - +- Added `icon.subscription` and `icon.advisor` symbol theme tokens. In Nerd Font mode, subscription spend renders with `󰙺` (`\u{f067a}`) and advisor spend renders with `` (`\uea70`); in Unicode mode, advisor spend renders with `👁` (e.g. `󰙺 2.67 +  󰙺 0.41` in Nerd Font mode, `S2.67 + 👁 S0.41` in Unicode mode, and `S2.67 + S0.41 (adv)` in ASCII mode). ### Changed - The context-reactive status line gained an Embedded mode that absorbs configured context segments into in-gauge percentage/window labels. Annotated and Embedded gauges use `󰕝` for the async-speculation boundary and `󰁨` for the compaction boundary under the Nerd Font symbol preset; Unicode and ASCII keep their existing boundary ticks. @@ -30,6 +30,7 @@ ### Fixed +- Fixed GitHub `file_read` failing on image and binary responses that GitHub CLI attempted to transform; it now requests uncompressed JSON content, returns supported images as image blocks, and identifies unavailable or non-text files with a view URL. - Fixed subagent structured returns being silently corrupted by the tool-argument repair layer: the `yield` tool's parameters embed the caller's output schema under an `anyOf` wrapper, and lossy repairs fired on union-branch guesses — JSON-stringifying object payloads into string-typed fields (parents received `summary: "{\"purge\":13,…}"` instead of prose) and deleting unrecognized keys — bypassing yield's own validate-and-retry loop. The repair layer (pi-ai) now restricts union-branch diagnoses to lossless repairs, so mismatches surface as retryable schema errors and accepted payloads arrive verbatim. - Fixed the `yield` tool bouncing common weak-caller envelope shapes with `result must be an object containing either data or error` retries (the dominant structured-output failure in Gemini-flash subagent traces): `type: "result"` with the `result` wrapper omitted entirely now finalizes as the documented last-turn yield, top-level `data`/`error` payloads missing the wrapper are salvaged, and `result` or `data` sent as a JSON-encoded string is parsed losslessly (mirroring executor finalization) before consuming a schema retry. - Fixed the `yield` tool's instructions teaching weak callers the wrong call shape for structured tasks: the description led with `Pass type:"result" to finalize; when data is omitted, your last assistant turn becomes the raw final result` before ever stating the `result: { data }` wrapper, producing wrapper-less `{type:"result"}` punts and top-level `data:` payloads in Gemini-flash traces. The description (now `prompts/tools/yield.md`) and the subagent system prompt lead with the wrapper contract and only advertise last-turn extraction when no output schema is declared; a schema-bound last-turn finalize with no accumulated incremental sections is rejected in-band as a retryable error instead of terminating the child into an uncorrectable post-mortem `schema_violation`. diff --git a/packages/coding-agent/src/modes/components/footer.ts b/packages/coding-agent/src/modes/components/footer.ts index ff8732e13..4e3bd21d5 100644 --- a/packages/coding-agent/src/modes/components/footer.ts +++ b/packages/coding-agent/src/modes/components/footer.ts @@ -157,18 +157,30 @@ export class FooterComponent implements Component { // Show billing summary with subscription and premium-request indicators const usingSubscription = state.model ? this.session.modelRegistry.isUsingOAuth(state.model) : false; + const { auto: autoIcon, subscription: subscriptionIcon } = theme.icon; const normalizedPremiumRequests = Math.round((totalPremiumRequests + Number.EPSILON) * 100) / 100; if (totalCost || usingSubscription || normalizedPremiumRequests) { const billingParts: string[] = []; - if (totalCost) billingParts.push(`$${totalCost.toFixed(3)}`); + if (totalCost) { + const formatted = totalCost.toFixed(3); + if (usingSubscription) { + const spend = + theme.getSymbolPreset() === "nerd" && subscriptionIcon + ? `${subscriptionIcon} ${formatted}` + : `S${formatted}`; + billingParts.push(spend); + } else { + billingParts.push(`$${formatted}`); + } + } else if (usingSubscription) { + billingParts.push(theme.getSymbolPreset() === "nerd" && subscriptionIcon ? subscriptionIcon : "(sub)"); + } if (normalizedPremiumRequests) billingParts.push(`★ ${formatNumber(normalizedPremiumRequests)}`); - if (usingSubscription) billingParts.push("(sub)"); if (billingParts.length > 0) statsParts.push(billingParts.join(" ")); } - // Colorize context percentage based on usage let contextPercentStr: string; - const autoIndicator = this.#autoCompactEnabled ? " (auto)" : ""; + const autoIndicator = this.#autoCompactEnabled && autoIcon ? ` ${autoIcon}` : ""; const contextPercentDisplay = `${formatContextUsage(contextPercentValue, contextWindow, contextTokens)}${autoIndicator}`; if (contextUsage && contextPercentValue !== null) { const color = getContextUsageThemeColor(getContextUsageLevel(contextPercentValue, contextWindow)); diff --git a/packages/coding-agent/src/modes/components/status-line/component.ts b/packages/coding-agent/src/modes/components/status-line/component.ts index 1716dc437..57058b2f5 100644 --- a/packages/coding-agent/src/modes/components/status-line/component.ts +++ b/packages/coding-agent/src/modes/components/status-line/component.ts @@ -2005,7 +2005,9 @@ export class StatusLineComponent implements Component { } } - const usedCount = Math.min(scaleWidth, Math.max(0, Math.round((clampedPct / 100) * scaleWidth))); + // At least one accent cell: a fresh session still shows the session-accent + // line starting at the left instead of a fully dim bar. + const usedCount = Math.min(scaleWidth, Math.max(1, Math.round((clampedPct / 100) * scaleWidth))); const unusedColor = theme.getFgAnsi("border"); // Boundary markers are only meaningful when auto-compaction can fire and diff --git a/packages/coding-agent/src/modes/components/status-line/segments.ts b/packages/coding-agent/src/modes/components/status-line/segments.ts index c35ca5f03..32b91419a 100644 --- a/packages/coding-agent/src/modes/components/status-line/segments.ts +++ b/packages/coding-agent/src/modes/components/status-line/segments.ts @@ -3,7 +3,7 @@ import * as path from "node:path"; import { ThinkingLevel } from "@oh-my-pi/pi-agent-core"; import { TERMINAL } from "@oh-my-pi/pi-tui"; import { formatDuration, formatNumber, getProjectDir, pathIsWithin, relativePathWithinRoot } from "@oh-my-pi/pi-utils"; -import { type ThemeColor, theme } from "../../../modes/theme/theme"; +import { type Theme, type ThemeColor, theme } from "../../../modes/theme/theme"; import { shortenPath, TRUNCATE_LENGTHS, truncateToWidth } from "../../../tools/render-utils"; import { fileHyperlink } from "../../../tui/hyperlink"; import { getSessionAccentAnsi, getSessionAccentHex } from "../../../utils/session-color"; @@ -49,6 +49,24 @@ function stripDisplayRoot(pwd: string): string { function normalizePremiumRequests(value: number): number { return Math.round((value + Number.EPSILON) * 100) / 100; } +function formatSpend(amount: number, usingSubscription: boolean, uiTheme: Theme): string { + const formatted = amount.toFixed(2); + if (!usingSubscription) return `$${formatted}`; + if (uiTheme.getSymbolPreset() === "nerd") { + const icon = uiTheme.icon.subscription; + return icon ? `${icon} ${formatted}` : `S${formatted}`; + } + return `S${formatted}`; +} + +function formatAdvisorSpend(amount: number, usingSubscription: boolean, uiTheme: Theme): string { + const spend = formatSpend(amount, usingSubscription, uiTheme); + const icon = uiTheme.icon.advisor; + if (icon && icon !== "(adv)") { + return `${icon} ${spend}`; + } + return `${spend} (adv)`; +} const SCRATCH_ROOTS: readonly string[] = (() => { const roots = new Set([os.tmpdir(), path.join(os.homedir(), "tmp")]); @@ -130,8 +148,8 @@ const modelSegment: StatusLineSegment = { const modelIcon = compact ? thinkingGlyph(thinkingDisplay) : theme.icon.model; // Fast-mode icon and thinking-level suffix trail the model name and are - // colored together with it as `statusLineModel`. The advisor "++" badge - // sits between the name and that tail, so it reads as a distinct marker. + // colored together with it as `statusLineModel`. The advisor symbol sits + // between the name and that tail, so it reads as a distinct marker. // theme.fg resets only the fg, so the spans are concatenated (not // nested) to keep each color intact. let tail = ""; @@ -145,14 +163,15 @@ const modelSegment: StatusLineSegment = { // `statusLineModel` is aliased to `accent` in many themes, so the badge // uses status colors to stay visibly distinct from the model name color. let content = theme.fg("statusLineModel", withIcon(modelIcon, modelName)); - // Advisor "++" badge, colored by the worst status in the roster: + // Advisor symbol, colored by the worst status in the roster: // success = all running, warning = quota-exhausted, error = failed, // dim = everything paused/no-model. Per-advisor detail lives in // `/advisor status`. // Optional chaining: lightweight session doubles (test mocks) that don't // implement getAdvisorStatusOverview skip the badge instead of crashing. + const advisorIcon = theme.icon.advisor; const advisorStats = ctx.session.getAdvisorStatusOverview?.(); - if (advisorStats?.configured && advisorStats.advisors.length > 0) { + if (advisorIcon && advisorStats?.configured && advisorStats.advisors.length > 0) { const statuses = advisorStats.advisors.map(a => a.status); const badgeColor = statuses.includes("error") ? "error" @@ -161,7 +180,7 @@ const modelSegment: StatusLineSegment = { : statuses.includes("running") ? "success" : "dim"; - content += theme.fg(badgeColor, "++"); + content += theme.fg(badgeColor, ` ${advisorIcon}`); } if (tail) { content += theme.fg("statusLineModel", tail); @@ -437,16 +456,26 @@ const costSegment: StatusLineSegment = { const normalizedPremiumRequests = normalizePremiumRequests(premiumRequests); const state = ctx.session.state; const usingSubscription = state.model ? (ctx.session.modelRegistry?.isUsingOAuth(state.model) ?? false) : false; + const advisorUsingSubscription = ctx.session.isAdvisorUsingSubscription?.() ?? false; if (!cost && !advisorCost && !usingSubscription && !normalizedPremiumRequests) { return { content: "", visible: false }; } const billingParts: string[] = []; - if (cost) billingParts.push(`$${cost.toFixed(2)}`); + if (cost) { + billingParts.push(formatSpend(cost, usingSubscription, theme)); + } else if (usingSubscription) { + billingParts.push( + theme.getSymbolPreset() === "nerd" && theme.icon.subscription ? theme.icon.subscription : "(sub)", + ); + } if (normalizedPremiumRequests) billingParts.push(`★ ${formatNumber(normalizedPremiumRequests)}`); - if (usingSubscription) billingParts.push("(sub)"); - if (advisorCost) billingParts.push(`${billingParts.length ? "+ " : ""}$${advisorCost.toFixed(2)} (adv)`); + if (advisorCost) { + const prefix = billingParts.length ? "+ " : ""; + billingParts.push(`${prefix}${formatAdvisorSpend(advisorCost, advisorUsingSubscription, theme)}`); + } + if (billingParts.length === 0) return { content: "", visible: false }; return { content: theme.fg("statusLineCost", billingParts.join(" ")), visible: true }; }, diff --git a/packages/coding-agent/src/modes/theme/symbols.ts b/packages/coding-agent/src/modes/theme/symbols.ts index c793b85c0..64a02fff2 100644 --- a/packages/coding-agent/src/modes/theme/symbols.ts +++ b/packages/coding-agent/src/modes/theme/symbols.ts @@ -89,6 +89,8 @@ export type SymbolKey = | "icon.tokens" | "icon.context" | "icon.cost" + | "icon.subscription" + | "icon.advisor" | "icon.time" | "icon.pi" | "icon.ghost" @@ -305,6 +307,8 @@ const UNICODE_SYMBOLS: SymbolMap = { "icon.tokens": "🪙", "icon.context": "◫", "icon.cost": "💲", + "icon.subscription": "(sub)", + "icon.advisor": "👁", "icon.time": "⏱", "icon.pi": "π", "icon.ghost": "👻", @@ -588,6 +592,10 @@ const NERD_SYMBOLS: SymbolMap = { "icon.context": "\ue70f", // pick:  | alt: $ ¢ "icon.cost": "\uf155", + // pick: 󰙺 (nf-md-currency_usd_off) + "icon.subscription": "\u{f067a}", + // pick:  (nf-cod-eye) + "icon.advisor": "\uea70", // pick:  | alt: ◷ ◴ "icon.time": "\uf017", // pick:  | alt: π ∏ ∑ @@ -839,6 +847,8 @@ const ASCII_SYMBOLS: SymbolMap = { "icon.tokens": "tok:", "icon.context": "ctx:", "icon.cost": "$", + "icon.subscription": "(sub)", + "icon.advisor": "(adv)", "icon.time": "t:", "icon.pi": "pi", "icon.ghost": "@", diff --git a/packages/coding-agent/src/modes/theme/theme-class.ts b/packages/coding-agent/src/modes/theme/theme-class.ts index c3b83efdf..57b26bd6d 100644 --- a/packages/coding-agent/src/modes/theme/theme-class.ts +++ b/packages/coding-agent/src/modes/theme/theme-class.ts @@ -501,6 +501,8 @@ export class Theme { tokens: this.#symbols["icon.tokens"], context: this.#symbols["icon.context"], cost: this.#symbols["icon.cost"], + subscription: this.#symbols["icon.subscription"], + advisor: this.#symbols["icon.advisor"], time: this.#symbols["icon.time"], pi: this.#symbols["icon.pi"], ghost: this.#symbols["icon.ghost"], diff --git a/packages/coding-agent/src/sdk.ts b/packages/coding-agent/src/sdk.ts index 6dcd83283..8d0196938 100644 --- a/packages/coding-agent/src/sdk.ts +++ b/packages/coding-agent/src/sdk.ts @@ -3378,8 +3378,8 @@ async function createAgentSessionScoped(options: CreateAgentSessionOptions): Pro // Owned only when this session created the manager; subagents receive a // parent's manager via `options.mcpManager` and MUST NOT disconnect it. const ownedMcpManager = options.mcpManager ? undefined : mcpManager; - // A resumed session already has advisor turns on disk; without this the status - // line would restart its `(adv)` total at zero for the rest of the session. + // A resumed session already has advisor turns on disk; without this its + // status-line cost total would restart at zero for the rest of the session. const initialAdvisorCosts = await loadAdvisorTranscriptCosts(sessionManager.getSessionFile()); session = new AgentSession({ advisorWatchdogPrompt, diff --git a/packages/coding-agent/src/session/agent-session.ts b/packages/coding-agent/src/session/agent-session.ts index c762593b8..6f562c384 100644 --- a/packages/coding-agent/src/session/agent-session.ts +++ b/packages/coding-agent/src/session/agent-session.ts @@ -9467,6 +9467,10 @@ export class AgentSession { getAdvisorCost(): number { return this.#advisors.getAdvisorCost(); } + /** Return whether any active or configured advisor is running on an OAuth/subscription model. */ + isAdvisorUsingSubscription(): boolean { + return this.#advisors.isUsingSubscription(); + } /** * Return structured advisor stats for the status command and TUI panel. */ diff --git a/packages/coding-agent/src/session/session-advisors.ts b/packages/coding-agent/src/session/session-advisors.ts index ca3c0400a..95f1bb815 100644 --- a/packages/coding-agent/src/session/session-advisors.ts +++ b/packages/coding-agent/src/session/session-advisors.ts @@ -1679,6 +1679,14 @@ export class SessionAdvisors { for (const advisorCost of this.#advisorCosts.values()) cost += advisorCost; return cost; } + /** Return whether any active or configured advisor is running on an OAuth/subscription model. */ + isUsingSubscription(): boolean { + if (this.#advisors.length > 0) { + return this.#advisors.some(a => this.#host.modelRegistry.isUsingOAuth(a.model)); + } + const sel = resolveAdvisorRoleSelection(this.#host.settings, this.#host.modelRegistry.getAvailable()); + return sel ? this.#host.modelRegistry.isUsingOAuth(sel.model) : false; + } /** * Return structured advisor stats for the status command and TUI panel. */ diff --git a/packages/coding-agent/test/modes/components/status-line/component.test.ts b/packages/coding-agent/test/modes/components/status-line/component.test.ts index 3bdb4c832..e36acc59c 100644 --- a/packages/coding-agent/test/modes/components/status-line/component.test.ts +++ b/packages/coding-agent/test/modes/components/status-line/component.test.ts @@ -1,6 +1,7 @@ import { beforeAll, describe, expect, it } from "bun:test"; import { Settings } from "../../../../src/config/settings"; import { StatusLineComponent } from "../../../../src/modes/components/status-line/component"; +import { loadTheme } from "../../../../src/modes/theme/loader"; import { getThemeByName, setThemeInstance } from "../../../../src/modes/theme/theme"; import type { AgentSession } from "../../../../src/session/agent-session"; @@ -19,7 +20,8 @@ function makeSessionWithLastMessage( cost = 0, advisorCost = 0, usingSubscription = false, - }: { cost?: number; advisorCost?: number; usingSubscription?: boolean } = {}, + advisorUsingSubscription = false, + }: { cost?: number; advisorCost?: number; usingSubscription?: boolean; advisorUsingSubscription?: boolean } = {}, ) { return { messages: lastMessage ? [lastMessage] : [], @@ -57,6 +59,7 @@ function makeSessionWithLastMessage( advisors: advisorCost > 0 ? [{ name: "test", status: "running" as const }] : [], }), getAdvisorCost: () => advisorCost, + isAdvisorUsingSubscription: () => advisorUsingSubscription, isFastModeActive: () => false, configuredThinkingLevel: () => undefined, modelRegistry: { @@ -101,7 +104,7 @@ describe("StatusLineComponent", () => { const stripped = border.content.replace(/\x1b\[[0-9;]*m/g, ""); expect(stripped).toContain("Prewalk"); }); - it("renders primary and advisor costs separately", () => { + it("renders primary and advisor costs separately with subscription indicator in Unicode preset", () => { const statusLine = new StatusLineComponent( makeSessionWithLastMessage(null, false, { cost: 2.67, @@ -111,7 +114,42 @@ describe("StatusLineComponent", () => { ); const stripped = statusLine.getTopBorder(WIDE_ENOUGH_FOR_COST_SEGMENT).content.replace(/\x1b\[[0-9;]*m/g, ""); - expect(stripped).toContain("$2.67 (sub) + $0.41 (adv)"); + expect(stripped).toContain("S2.67 + 👁 $0.41"); + }); + + it("renders advisor cost with subscription prefix when advisor is on subscription in Unicode preset", () => { + const statusLine = new StatusLineComponent( + makeSessionWithLastMessage(null, false, { + cost: 2.67, + advisorCost: 0.41, + usingSubscription: true, + advisorUsingSubscription: true, + }) as unknown as AgentSession, + ); + + const stripped = statusLine.getTopBorder(WIDE_ENOUGH_FOR_COST_SEGMENT).content.replace(/\x1b\[[0-9;]*m/g, ""); + expect(stripped).toContain("S2.67 + 👁 S0.41"); + }); + + it("renders ASCII preset fallback with (adv) for advisor costs", async () => { + const baseTheme = await getThemeByName("dark"); + if (!baseTheme) throw new Error("theme unavailable"); + const asciiTheme = await loadTheme("dark", { symbolPresetOverride: "ascii" }); + setThemeInstance(asciiTheme); + try { + const statusLine = new StatusLineComponent( + makeSessionWithLastMessage(null, false, { + cost: 2.67, + advisorCost: 0.41, + usingSubscription: true, + advisorUsingSubscription: true, + }) as unknown as AgentSession, + ); + const stripped = statusLine.getTopBorder(WIDE_ENOUGH_FOR_COST_SEGMENT).content.replace(/\x1b\[[0-9;]*m/g, ""); + expect(stripped).toContain("S2.67 + S0.41 (adv)"); + } finally { + setThemeInstance(baseTheme); + } }); it("omits advisor cost when the advisor has never been active", () => { @@ -123,7 +161,28 @@ describe("StatusLineComponent", () => { ); const stripped = statusLine.getTopBorder(WIDE_ENOUGH_FOR_COST_SEGMENT).content.replace(/\x1b\[[0-9;]*m/g, ""); - expect(stripped).toContain("$2.67 (sub)"); + expect(stripped).toContain("S2.67"); expect(stripped).not.toContain("(adv)"); }); + + it("renders Nerd Font symbols for subscription and advisor costs", async () => { + const baseTheme = await getThemeByName("dark"); + if (!baseTheme) throw new Error("theme unavailable"); + const nerdTheme = await loadTheme("dark", { symbolPresetOverride: "nerd" }); + setThemeInstance(nerdTheme); + try { + const statusLine = new StatusLineComponent( + makeSessionWithLastMessage(null, false, { + cost: 2.67, + advisorCost: 0.41, + usingSubscription: true, + advisorUsingSubscription: true, + }) as unknown as AgentSession, + ); + const stripped = statusLine.getTopBorder(WIDE_ENOUGH_FOR_COST_SEGMENT).content.replace(/\x1b\[[0-9;]*m/g, ""); + expect(stripped).toContain("\u{f067a} 2.67 + \uea70 \u{f067a} 0.41"); + } finally { + setThemeInstance(baseTheme); + } + }); }); diff --git a/packages/coding-agent/test/status-line-model.test.ts b/packages/coding-agent/test/status-line-model.test.ts index 97b658ba6..d3392e58e 100644 --- a/packages/coding-agent/test/status-line-model.test.ts +++ b/packages/coding-agent/test/status-line-model.test.ts @@ -59,10 +59,10 @@ function createModelContext(advisorActive: boolean): SegmentContext { } describe("status line model segment advisor badge", () => { - it("appends a success-colored ++ badge when all advisors run", () => { + it("appends a success-colored advisor symbol when all advisors run", () => { const rendered = renderSegment("model", createModelContext(true)); expect(rendered.content).toContain("Test Model"); - expect(rendered.content).toContain(theme.fg("success", "++")); + expect(rendered.content).toContain(theme.fg("success", ` ${theme.icon.advisor}`)); }); it("colors the badge by the worst roster status", () => { @@ -74,7 +74,7 @@ describe("status line model segment advisor badge", () => { { name: "b", status: "quota_exhausted" }, ], }); - expect(renderSegment("model", ctx).content).toContain(theme.fg("warning", "++")); + expect(renderSegment("model", ctx).content).toContain(theme.fg("warning", ` ${theme.icon.advisor}`)); ctx.session.getAdvisorStatusOverview = () => ({ configured: true, advisors: [ @@ -82,13 +82,13 @@ describe("status line model segment advisor badge", () => { { name: "b", status: "quota_exhausted" }, ], }); - expect(renderSegment("model", ctx).content).toContain(theme.fg("error", "++")); + expect(renderSegment("model", ctx).content).toContain(theme.fg("error", ` ${theme.icon.advisor}`)); }); it("omits the badge when the advisor is inactive", () => { const rendered = renderSegment("model", createModelContext(false)); expect(rendered.content).toContain("Test Model"); - expect(rendered.content).not.toContain("++"); + expect(rendered.content).not.toContain(theme.icon.advisor); }); }); diff --git a/packages/coding-agent/test/theme-nerd-symbols.test.ts b/packages/coding-agent/test/theme-nerd-symbols.test.ts index d2855258e..b309d267c 100644 --- a/packages/coding-agent/test/theme-nerd-symbols.test.ts +++ b/packages/coding-agent/test/theme-nerd-symbols.test.ts @@ -39,3 +39,23 @@ it("uses the Nerd Fonts v3 Material Design session icon", async () => { const theme = await getThemeByName(customThemeName); expect(theme?.symbol("icon.session")).toBe("\u{f0051}"); }); + +it("resolves subscription and advisor icons in Nerd Font mode", async () => { + originalAgentDir = getAgentDir(); + originalAgentDirEnv = process.env.PI_CODING_AGENT_DIR; + tempAgentDir = await fs.mkdtemp(path.join(os.tmpdir(), "omp-nerd-symbols-")); + setAgentDir(tempAgentDir); + + const dark = await Bun.file(DARK_THEME_PATH).json(); + const customThemeName = "nerd-annotation-symbols"; + await Bun.write( + path.join(getCustomThemesDir(), `${customThemeName}.json`), + JSON.stringify({ ...dark, name: customThemeName, symbols: { ...dark.symbols, preset: "nerd" } }), + ); + + const theme = await getThemeByName(customThemeName); + expect(theme?.symbol("icon.subscription")).toBe("\u{f067a}"); + expect(theme?.symbol("icon.advisor")).toBe("\uea70"); + expect(theme?.icon.subscription).toBe("\u{f067a}"); + expect(theme?.icon.advisor).toBe("\uea70"); +}); diff --git a/packages/tui/CHANGELOG.md b/packages/tui/CHANGELOG.md index 371c04d1e..d8b9d940c 100644 --- a/packages/tui/CHANGELOG.md +++ b/packages/tui/CHANGELOG.md @@ -4,7 +4,7 @@ ### Added -- Added support for customizable editor border styles including box, claude, pi, and borderless +- Added composer border styles (box, claude, pi, borderless) as per-style `ComposerStyle` objects (`getComposerStyle`) owning chrome geometry, top/bottom/row rendering, and status-bar attachment metadata — shared by the editor and host previews so they cannot drift - Added support for warning risk notes and row markers in settings lists ## [17.3.8] - 2026-08-19