@astrofoundry/pi-astro 0.11.0 → 0.11.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/extensions/astro-footer/format.test.ts +3 -3
- package/extensions/astro-footer/format.ts +1 -1
- package/extensions/astro-footer/icons.test.ts +8 -4
- package/extensions/astro-footer/icons.ts +6 -6
- package/extensions/astro-footer/index.test.ts +2 -2
- package/extensions/astro-footer/model-name.test.ts +40 -0
- package/extensions/astro-footer/model-name.ts +46 -0
- package/extensions/astro-footer/segments.test.ts +10 -7
- package/extensions/astro-footer/segments.ts +4 -3
- package/package.json +1 -1
|
@@ -53,10 +53,10 @@ describe("formatPercent", () => {
|
|
|
53
53
|
expect(formatPercent(null)).toBe("—");
|
|
54
54
|
});
|
|
55
55
|
|
|
56
|
-
it("rounds to nearest integer percent", () => {
|
|
56
|
+
it("rounds to nearest integer percent (input is 0-100, not 0-1)", () => {
|
|
57
57
|
expect(formatPercent(0)).toBe("0%");
|
|
58
|
-
expect(formatPercent(
|
|
59
|
-
expect(formatPercent(
|
|
58
|
+
expect(formatPercent(45.67)).toBe("46%");
|
|
59
|
+
expect(formatPercent(100)).toBe("100%");
|
|
60
60
|
});
|
|
61
61
|
});
|
|
62
62
|
|
|
@@ -13,7 +13,7 @@ export function formatCost(usd: number): string {
|
|
|
13
13
|
|
|
14
14
|
export function formatPercent(percent: number | null): string {
|
|
15
15
|
if (percent === null) return "—";
|
|
16
|
-
return `${Math.round(percent
|
|
16
|
+
return `${Math.round(percent)}%`;
|
|
17
17
|
}
|
|
18
18
|
|
|
19
19
|
export function formatPathBasename(cwd: string): string {
|
|
@@ -40,15 +40,19 @@ describe("detectNerdFonts", () => {
|
|
|
40
40
|
});
|
|
41
41
|
|
|
42
42
|
describe("iconsFor", () => {
|
|
43
|
-
it("returns nerd-font icons when true", () => {
|
|
43
|
+
it("returns nerd-font icons when true (separator pipe, model diamond, path 'dir')", () => {
|
|
44
44
|
const icons = iconsFor(true);
|
|
45
|
-
expect(icons.separator).toBe("");
|
|
45
|
+
expect(icons.separator).toBe("|");
|
|
46
|
+
expect(icons.model).toBe("◈");
|
|
47
|
+
expect(icons.path).toBe("dir");
|
|
46
48
|
expect(icons.git).toBe("");
|
|
47
49
|
});
|
|
48
50
|
|
|
49
|
-
it("returns ascii icons when false", () => {
|
|
51
|
+
it("returns ascii icons when false (pipe separator, dir prefix, ◈ model)", () => {
|
|
50
52
|
const icons = iconsFor(false);
|
|
51
|
-
expect(icons.separator).toBe("
|
|
53
|
+
expect(icons.separator).toBe("|");
|
|
54
|
+
expect(icons.model).toBe("◈");
|
|
55
|
+
expect(icons.path).toBe("dir");
|
|
52
56
|
expect(icons.git).toBe("git");
|
|
53
57
|
});
|
|
54
58
|
});
|
|
@@ -12,26 +12,26 @@ export interface IconSet {
|
|
|
12
12
|
|
|
13
13
|
const NERD: IconSet = {
|
|
14
14
|
pi: "π",
|
|
15
|
-
model: "",
|
|
15
|
+
model: "◈",
|
|
16
16
|
thinking: "",
|
|
17
|
-
path: "",
|
|
17
|
+
path: "dir",
|
|
18
18
|
git: "",
|
|
19
19
|
tokens: "",
|
|
20
20
|
cost: "",
|
|
21
21
|
context: "",
|
|
22
|
-
separator: "",
|
|
22
|
+
separator: "|",
|
|
23
23
|
};
|
|
24
24
|
|
|
25
25
|
const ASCII: IconSet = {
|
|
26
26
|
pi: "π",
|
|
27
|
-
model: "
|
|
27
|
+
model: "◈",
|
|
28
28
|
thinking: "?",
|
|
29
|
-
path: "
|
|
29
|
+
path: "dir",
|
|
30
30
|
git: "git",
|
|
31
31
|
tokens: "tok",
|
|
32
32
|
cost: "$",
|
|
33
33
|
context: "ctx",
|
|
34
|
-
separator: "
|
|
34
|
+
separator: "|",
|
|
35
35
|
};
|
|
36
36
|
|
|
37
37
|
const NERD_TERM_PROGRAMS = new Set(["iTerm.app", "WezTerm", "ghostty"]);
|
|
@@ -165,7 +165,7 @@ describe("astro-footer extension", () => {
|
|
|
165
165
|
},
|
|
166
166
|
],
|
|
167
167
|
},
|
|
168
|
-
getContextUsage: () => ({ tokens: 50, contextWindow: 200_000, percent:
|
|
168
|
+
getContextUsage: () => ({ tokens: 50, contextWindow: 200_000, percent: 25 }),
|
|
169
169
|
});
|
|
170
170
|
await pi.handlers.session_start({}, ctx);
|
|
171
171
|
const factory = ctx.ui.setFooter.mock.calls[0][0] as (
|
|
@@ -187,7 +187,7 @@ describe("astro-footer extension", () => {
|
|
|
187
187
|
const lines = component.render(120);
|
|
188
188
|
expect(lines).toHaveLength(1);
|
|
189
189
|
const line = lines[0];
|
|
190
|
-
expect(line).toContain("
|
|
190
|
+
expect(line).toContain("Anthropic: Claude Sonnet 4 6");
|
|
191
191
|
expect(line).toContain("proj");
|
|
192
192
|
expect(line).toContain("main");
|
|
193
193
|
expect(line).toContain("1.2k");
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
import { formatModelDisplay } from "./model-name.ts";
|
|
3
|
+
|
|
4
|
+
describe("formatModelDisplay", () => {
|
|
5
|
+
it("formats moonshotai/kimi-k2.6 as MoonshotAI: Kimi K2.6", () => {
|
|
6
|
+
expect(formatModelDisplay("moonshotai/kimi-k2.6")).toBe("MoonshotAI: Kimi K2.6");
|
|
7
|
+
});
|
|
8
|
+
|
|
9
|
+
it("formats anthropic/claude-sonnet-4-6", () => {
|
|
10
|
+
expect(formatModelDisplay("anthropic/claude-sonnet-4-6")).toBe("Anthropic: Claude Sonnet 4 6");
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
it("uppercases known acronyms in the leaf (gpt → GPT)", () => {
|
|
14
|
+
expect(formatModelDisplay("openai/gpt-5.4-mini")).toBe("OpenAI: GPT 5.4 Mini");
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
it("uses the provider mapping (xai → xAI)", () => {
|
|
18
|
+
expect(formatModelDisplay("xai/grok-4")).toBe("xAI: Grok 4");
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
it("falls back to capitalising unknown providers", () => {
|
|
22
|
+
expect(formatModelDisplay("acme/super-model")).toBe("Acme: Super Model");
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
it("handles ids without a provider prefix", () => {
|
|
26
|
+
expect(formatModelDisplay("kimi-k2.6")).toBe("Kimi K2.6");
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
it("returns empty string for empty input", () => {
|
|
30
|
+
expect(formatModelDisplay("")).toBe("");
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
it("trims whitespace", () => {
|
|
34
|
+
expect(formatModelDisplay(" anthropic/claude-haiku-4-5 ")).toBe("Anthropic: Claude Haiku 4 5");
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
it("preserves slashes inside the leaf when the model id has multiple slashes", () => {
|
|
38
|
+
expect(formatModelDisplay("openai/o4/mini")).toBe("OpenAI: O4/mini");
|
|
39
|
+
});
|
|
40
|
+
});
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
const PROVIDER_DISPLAY: Record<string, string> = {
|
|
2
|
+
anthropic: "Anthropic",
|
|
3
|
+
openai: "OpenAI",
|
|
4
|
+
"openai-codex": "OpenAI Codex",
|
|
5
|
+
moonshotai: "MoonshotAI",
|
|
6
|
+
google: "Google",
|
|
7
|
+
meta: "Meta",
|
|
8
|
+
"meta-llama": "Meta Llama",
|
|
9
|
+
mistralai: "MistralAI",
|
|
10
|
+
xai: "xAI",
|
|
11
|
+
deepseek: "DeepSeek",
|
|
12
|
+
perplexity: "Perplexity",
|
|
13
|
+
openrouter: "OpenRouter",
|
|
14
|
+
cohere: "Cohere",
|
|
15
|
+
together: "Together",
|
|
16
|
+
groq: "Groq",
|
|
17
|
+
qwen: "Qwen",
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
const ACRONYMS = new Set(["gpt", "ai", "llm", "api", "moe", "ssm", "vl"]);
|
|
21
|
+
|
|
22
|
+
function capitalize(s: string): string {
|
|
23
|
+
if (s.length === 0) return s;
|
|
24
|
+
return s.charAt(0).toUpperCase() + s.slice(1);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function formatLeafWord(word: string): string {
|
|
28
|
+
if (word.length === 0) return word;
|
|
29
|
+
if (/^\d/.test(word)) return word;
|
|
30
|
+
if (ACRONYMS.has(word.toLowerCase())) return word.toUpperCase();
|
|
31
|
+
return capitalize(word);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export function formatModelDisplay(id: string): string {
|
|
35
|
+
const trimmed = id.trim();
|
|
36
|
+
if (trimmed.length === 0) return "";
|
|
37
|
+
if (!trimmed.includes("/")) {
|
|
38
|
+
return trimmed.split("-").map(formatLeafWord).join(" ");
|
|
39
|
+
}
|
|
40
|
+
const slash = trimmed.indexOf("/");
|
|
41
|
+
const providerRaw = trimmed.slice(0, slash);
|
|
42
|
+
const leaf = trimmed.slice(slash + 1);
|
|
43
|
+
const provider = PROVIDER_DISPLAY[providerRaw.toLowerCase()] ?? capitalize(providerRaw);
|
|
44
|
+
const leafFormatted = leaf.split("-").map(formatLeafWord).join(" ");
|
|
45
|
+
return `${provider}: ${leafFormatted}`;
|
|
46
|
+
}
|
|
@@ -28,10 +28,10 @@ describe("segments", () => {
|
|
|
28
28
|
expect(renderModel(fakeTheme, ICONS, undefined)).toBeNull();
|
|
29
29
|
});
|
|
30
30
|
|
|
31
|
-
it("renderModel
|
|
32
|
-
const out = renderModel(fakeTheme, ICONS, "
|
|
33
|
-
expect(out).toContain("
|
|
34
|
-
expect(out).
|
|
31
|
+
it("renderModel pretty-prints provider + model name in accent colour", () => {
|
|
32
|
+
const out = renderModel(fakeTheme, ICONS, "moonshotai/kimi-k2.6");
|
|
33
|
+
expect(out).toContain("<accent>◈</accent>");
|
|
34
|
+
expect(out).toContain("<accent>MoonshotAI: Kimi K2.6</accent>");
|
|
35
35
|
});
|
|
36
36
|
|
|
37
37
|
it("renderThinking returns null when level missing", () => {
|
|
@@ -87,18 +87,21 @@ describe("segments", () => {
|
|
|
87
87
|
});
|
|
88
88
|
|
|
89
89
|
it("renderContext uses dim colour below 70%", () => {
|
|
90
|
-
const out = renderContext(fakeTheme, ICONS,
|
|
90
|
+
const out = renderContext(fakeTheme, ICONS, 50);
|
|
91
91
|
expect(out).toContain("<dim>");
|
|
92
|
+
expect(out).toContain("50%");
|
|
92
93
|
});
|
|
93
94
|
|
|
94
95
|
it("renderContext uses warning colour at and above 70%", () => {
|
|
95
|
-
const out = renderContext(fakeTheme, ICONS,
|
|
96
|
+
const out = renderContext(fakeTheme, ICONS, 70);
|
|
96
97
|
expect(out).toContain("<warning>");
|
|
98
|
+
expect(out).toContain("70%");
|
|
97
99
|
});
|
|
98
100
|
|
|
99
101
|
it("renderContext uses error colour at and above 90%", () => {
|
|
100
|
-
const out = renderContext(fakeTheme, ICONS,
|
|
102
|
+
const out = renderContext(fakeTheme, ICONS, 95);
|
|
101
103
|
expect(out).toContain("<error>");
|
|
104
|
+
expect(out).toContain("95%");
|
|
102
105
|
});
|
|
103
106
|
|
|
104
107
|
it("renderExtensionStatus uses muted colour", () => {
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type { ThemeColor } from "@mariozechner/pi-coding-agent";
|
|
2
2
|
import type { IconSet } from "./icons.ts";
|
|
3
3
|
import { formatCost, formatPathBasename, formatPercent, formatTokens } from "./format.ts";
|
|
4
|
+
import { formatModelDisplay } from "./model-name.ts";
|
|
4
5
|
|
|
5
6
|
export interface ThemeFn {
|
|
6
7
|
fg: (role: ThemeColor, text: string) => string;
|
|
@@ -30,8 +31,8 @@ export function renderPi(theme: ThemeFn, icons: IconSet): string {
|
|
|
30
31
|
|
|
31
32
|
export function renderModel(theme: ThemeFn, icons: IconSet, modelId: string | undefined): string | null {
|
|
32
33
|
if (!modelId) return null;
|
|
33
|
-
const
|
|
34
|
-
return `${theme.fg("
|
|
34
|
+
const display = formatModelDisplay(modelId);
|
|
35
|
+
return `${theme.fg("accent", icons.model)} ${theme.fg("accent", display)}`.trim();
|
|
35
36
|
}
|
|
36
37
|
|
|
37
38
|
export function renderThinking(theme: ThemeFn, icons: IconSet, level: string | undefined): string | null {
|
|
@@ -67,7 +68,7 @@ export function renderContext(
|
|
|
67
68
|
percent: number | null,
|
|
68
69
|
): string | null {
|
|
69
70
|
if (percent === null) return null;
|
|
70
|
-
const role: ThemeColor = percent >=
|
|
71
|
+
const role: ThemeColor = percent >= 90 ? "error" : percent >= 70 ? "warning" : "dim";
|
|
71
72
|
return `${theme.fg(role, icons.context)} ${theme.fg(role, formatPercent(percent))}`.trim();
|
|
72
73
|
}
|
|
73
74
|
|