feat(coding-agent): integrated subscription status icons and advisor tracking
- Added theme icons and formatting for subscription status and advisor tracking across status lines and footers. - Implemented `isAdvisorUsingSubscription` and `isUsingSubscription` methods to check active models against OAuth usage. - Updated status line model segments to display the new advisor icon instead of text badges. - Added comprehensive tests covering subscription and advisor costs across multiple font presets.
This commit is contained in:
@@ -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`.
|
||||
|
||||
@@ -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));
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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<string>([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 };
|
||||
},
|
||||
|
||||
@@ -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": "@",
|
||||
|
||||
@@ -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"],
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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.
|
||||
*/
|
||||
|
||||
@@ -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.
|
||||
*/
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -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");
|
||||
});
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user