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
@@ -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");
});