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:
can1357
2026-08-20 05:51:53 +02:00
parent e8d7c0a493
commit bd5764aad4
13 changed files with 176 additions and 29 deletions
+4 -3
View File
@@ -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"],
+2 -2
View File
@@ -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");
});