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:
@@ -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");
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user