@astrofoundry/pi-astro 0.12.1 → 0.13.0
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/README.md +1 -1
- package/extensions/astro-agents/index.test.ts +7 -1
- package/extensions/astro-agents/index.ts +41 -26
- package/extensions/astro-footer/README.md +49 -27
- package/extensions/astro-footer/format.test.ts +61 -1
- package/extensions/astro-footer/format.ts +48 -0
- package/extensions/astro-footer/git-status.test.ts +45 -0
- package/extensions/astro-footer/git-status.ts +82 -0
- package/extensions/astro-footer/icons.ts +24 -3
- package/extensions/astro-footer/index.test.ts +168 -47
- package/extensions/astro-footer/index.ts +150 -33
- package/extensions/astro-footer/segments.test.ts +116 -24
- package/extensions/astro-footer/segments.ts +76 -17
- package/extensions/astro-footer/settings.test.ts +85 -0
- package/extensions/astro-footer/settings.ts +43 -0
- package/extensions/astro-snake/README.md +0 -10
- package/extensions/astro-snake/component.ts +0 -11
- package/package.json +1 -1
|
@@ -7,6 +7,7 @@ interface FakePi {
|
|
|
7
7
|
handlers: Record<string, Handler>;
|
|
8
8
|
commands: Map<string, { handler: CommandHandler; getArgumentCompletions?: (p: string) => unknown }>;
|
|
9
9
|
getThinkingLevel: ReturnType<typeof vi.fn>;
|
|
10
|
+
getSessionName: ReturnType<typeof vi.fn>;
|
|
10
11
|
on: (event: string, h: Handler) => void;
|
|
11
12
|
registerCommand: (
|
|
12
13
|
name: string,
|
|
@@ -18,13 +19,14 @@ interface FakePi {
|
|
|
18
19
|
) => void;
|
|
19
20
|
}
|
|
20
21
|
|
|
21
|
-
function makePi(): FakePi {
|
|
22
|
+
function makePi(sessionName?: string): FakePi {
|
|
22
23
|
const handlers: Record<string, Handler> = {};
|
|
23
24
|
const commands = new Map<string, { handler: CommandHandler; getArgumentCompletions?: (p: string) => unknown }>();
|
|
24
25
|
return {
|
|
25
26
|
handlers,
|
|
26
27
|
commands,
|
|
27
28
|
getThinkingLevel: vi.fn(() => "medium"),
|
|
29
|
+
getSessionName: vi.fn(() => sessionName),
|
|
28
30
|
on: (event, h) => {
|
|
29
31
|
handlers[event] = h;
|
|
30
32
|
},
|
|
@@ -44,11 +46,12 @@ interface FakeCtx {
|
|
|
44
46
|
};
|
|
45
47
|
cwd: string;
|
|
46
48
|
model: { id: string } | undefined;
|
|
49
|
+
modelRegistry: { isUsingOAuth: ReturnType<typeof vi.fn> };
|
|
47
50
|
sessionManager: { getBranch: () => unknown[] };
|
|
48
51
|
getContextUsage: () => { tokens: number | null; contextWindow: number; percent: number | null } | undefined;
|
|
49
52
|
}
|
|
50
53
|
|
|
51
|
-
function makeCtx(overrides: Partial<FakeCtx> = {}): FakeCtx {
|
|
54
|
+
function makeCtx(overrides: Partial<FakeCtx> = {}, oauth = false): FakeCtx {
|
|
52
55
|
return {
|
|
53
56
|
ui: {
|
|
54
57
|
notify: vi.fn(),
|
|
@@ -56,18 +59,29 @@ function makeCtx(overrides: Partial<FakeCtx> = {}): FakeCtx {
|
|
|
56
59
|
},
|
|
57
60
|
cwd: "/Users/astro/proj",
|
|
58
61
|
model: { id: "anthropic/claude-sonnet-4-6" },
|
|
62
|
+
modelRegistry: { isUsingOAuth: vi.fn(() => oauth) },
|
|
59
63
|
sessionManager: { getBranch: () => [] },
|
|
60
64
|
getContextUsage: () => undefined,
|
|
61
65
|
...overrides,
|
|
62
66
|
};
|
|
63
67
|
}
|
|
64
68
|
|
|
69
|
+
const fakeTheme = { fg: (_role: string, text: string) => text };
|
|
70
|
+
|
|
71
|
+
const fakeFooterDataDefault = {
|
|
72
|
+
getGitBranch: () => "main",
|
|
73
|
+
getExtensionStatuses: () => new Map<string, string>(),
|
|
74
|
+
getAvailableProviderCount: () => 1,
|
|
75
|
+
onBranchChange: () => () => {},
|
|
76
|
+
};
|
|
77
|
+
|
|
65
78
|
describe("astro-footer extension", () => {
|
|
66
79
|
const origEnv = { ...process.env };
|
|
67
80
|
|
|
68
81
|
beforeEach(() => {
|
|
69
82
|
vi.resetModules();
|
|
70
83
|
delete process.env.ASTRO_FOOTER_NERD_FONTS;
|
|
84
|
+
delete process.env.ASTRO_FOOTER_PATH;
|
|
71
85
|
delete process.env.TERM_PROGRAM;
|
|
72
86
|
delete process.env.TERM;
|
|
73
87
|
delete process.env.LC_TERMINAL;
|
|
@@ -86,17 +100,29 @@ describe("astro-footer extension", () => {
|
|
|
86
100
|
return await import("./index.ts");
|
|
87
101
|
}
|
|
88
102
|
|
|
89
|
-
async function install(): Promise<{ pi: FakePi; mod: Awaited<ReturnType<typeof load>> }> {
|
|
103
|
+
async function install(sessionName?: string): Promise<{ pi: FakePi; mod: Awaited<ReturnType<typeof load>> }> {
|
|
90
104
|
const mod = await load();
|
|
91
|
-
const pi = makePi();
|
|
105
|
+
const pi = makePi(sessionName);
|
|
92
106
|
mod.default(pi as unknown as Parameters<typeof mod.default>[0]);
|
|
93
107
|
return { pi, mod };
|
|
94
108
|
}
|
|
95
109
|
|
|
96
|
-
|
|
110
|
+
function getFactory(ctx: FakeCtx): (tui: unknown, theme: unknown, footerData: unknown) => {
|
|
111
|
+
render: (w: number) => string[];
|
|
112
|
+
dispose?: () => void;
|
|
113
|
+
} {
|
|
114
|
+
return ctx.ui.setFooter.mock.calls[0][0] as (
|
|
115
|
+
tui: unknown,
|
|
116
|
+
theme: unknown,
|
|
117
|
+
footerData: unknown,
|
|
118
|
+
) => { render: (w: number) => string[]; dispose?: () => void };
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
it("registers /footer command and lifecycle handlers", async () => {
|
|
97
122
|
const { pi } = await install();
|
|
98
123
|
expect(pi.commands.has("footer")).toBe(true);
|
|
99
124
|
expect(pi.handlers.session_start).toBeDefined();
|
|
125
|
+
expect(pi.handlers.tool_result).toBeDefined();
|
|
100
126
|
});
|
|
101
127
|
|
|
102
128
|
it("session_start activates the footer via ctx.ui.setFooter", async () => {
|
|
@@ -150,7 +176,7 @@ describe("astro-footer extension", () => {
|
|
|
150
176
|
);
|
|
151
177
|
});
|
|
152
178
|
|
|
153
|
-
it("renders
|
|
179
|
+
it("renders row 1 with model/path/git, tokens split, cost, ctx — and row 2 with statuses", async () => {
|
|
154
180
|
process.env.ASTRO_FOOTER_NERD_FONTS = "0";
|
|
155
181
|
const { pi } = await install();
|
|
156
182
|
const ctx = makeCtx({
|
|
@@ -160,7 +186,13 @@ describe("astro-footer extension", () => {
|
|
|
160
186
|
type: "message",
|
|
161
187
|
message: {
|
|
162
188
|
role: "assistant",
|
|
163
|
-
usage: {
|
|
189
|
+
usage: {
|
|
190
|
+
input: 1234,
|
|
191
|
+
output: 567,
|
|
192
|
+
cacheRead: 0,
|
|
193
|
+
cacheWrite: 0,
|
|
194
|
+
cost: { total: 0.05 },
|
|
195
|
+
},
|
|
164
196
|
},
|
|
165
197
|
},
|
|
166
198
|
],
|
|
@@ -168,55 +200,144 @@ describe("astro-footer extension", () => {
|
|
|
168
200
|
getContextUsage: () => ({ tokens: 50, contextWindow: 200_000, percent: 25 }),
|
|
169
201
|
});
|
|
170
202
|
await pi.handlers.session_start({}, ctx);
|
|
171
|
-
const factory = ctx
|
|
172
|
-
tui: unknown,
|
|
173
|
-
theme: unknown,
|
|
174
|
-
footerData: unknown,
|
|
175
|
-
) => { render: (w: number) => string[] };
|
|
176
|
-
const fakeTheme = {
|
|
177
|
-
fg: (_role: string, text: string) => text,
|
|
178
|
-
bold: (text: string) => text,
|
|
179
|
-
};
|
|
203
|
+
const factory = getFactory(ctx);
|
|
180
204
|
const fakeFooterData = {
|
|
181
|
-
|
|
205
|
+
...fakeFooterDataDefault,
|
|
182
206
|
getExtensionStatuses: () => new Map([["caveman", "🪨 caveman:full"]]),
|
|
183
|
-
getAvailableProviderCount: () => 1,
|
|
184
|
-
onBranchChange: () => () => {},
|
|
185
207
|
};
|
|
186
|
-
const component = factory({}, fakeTheme, fakeFooterData);
|
|
208
|
+
const component = factory({ requestRender: () => {} }, fakeTheme, fakeFooterData);
|
|
187
209
|
const lines = component.render(200);
|
|
188
|
-
expect(lines).
|
|
189
|
-
const
|
|
190
|
-
expect(
|
|
191
|
-
expect(
|
|
192
|
-
expect(
|
|
193
|
-
expect(
|
|
194
|
-
expect(
|
|
195
|
-
expect(
|
|
196
|
-
expect(
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
210
|
+
expect(lines.length).toBeGreaterThanOrEqual(1);
|
|
211
|
+
const row1 = lines[0];
|
|
212
|
+
expect(row1).toContain("Anthropic: Claude Sonnet 4 6");
|
|
213
|
+
expect(row1).toContain("proj");
|
|
214
|
+
expect(row1).toContain("main");
|
|
215
|
+
expect(row1).toContain("↑");
|
|
216
|
+
expect(row1).toContain("1.2k");
|
|
217
|
+
expect(row1).toContain("↓");
|
|
218
|
+
expect(row1).toContain("567");
|
|
219
|
+
expect(row1).toContain("$0.05");
|
|
220
|
+
expect(row1).toContain("25.0%/200k");
|
|
221
|
+
expect(lines.length).toBe(2);
|
|
222
|
+
expect(lines[1]).toContain("🪨 caveman:full");
|
|
223
|
+
component.dispose?.();
|
|
224
|
+
});
|
|
225
|
+
|
|
226
|
+
it("OAuth-authed model shows (sub) instead of dollar cost", async () => {
|
|
227
|
+
process.env.ASTRO_FOOTER_NERD_FONTS = "0";
|
|
228
|
+
const { pi } = await install();
|
|
229
|
+
const ctx = makeCtx(
|
|
230
|
+
{
|
|
231
|
+
sessionManager: {
|
|
232
|
+
getBranch: () => [
|
|
233
|
+
{
|
|
234
|
+
type: "message",
|
|
235
|
+
message: {
|
|
236
|
+
role: "assistant",
|
|
237
|
+
usage: { input: 100, output: 50, cacheRead: 0, cacheWrite: 0, cost: { total: 0 } },
|
|
238
|
+
},
|
|
239
|
+
},
|
|
240
|
+
],
|
|
241
|
+
},
|
|
242
|
+
},
|
|
243
|
+
true,
|
|
244
|
+
);
|
|
245
|
+
await pi.handlers.session_start({}, ctx);
|
|
246
|
+
const component = getFactory(ctx)({ requestRender: () => {} }, fakeTheme, fakeFooterDataDefault);
|
|
247
|
+
const row1 = component.render(200)[0];
|
|
248
|
+
expect(row1).toContain("(sub)");
|
|
249
|
+
component.dispose?.();
|
|
250
|
+
});
|
|
251
|
+
|
|
252
|
+
it("renders single line when row 2 is empty", async () => {
|
|
253
|
+
process.env.ASTRO_FOOTER_NERD_FONTS = "0";
|
|
254
|
+
const { pi } = await install();
|
|
255
|
+
const ctx = makeCtx();
|
|
256
|
+
await pi.handlers.session_start({}, ctx);
|
|
257
|
+
const component = getFactory(ctx)({ requestRender: () => {} }, fakeTheme, fakeFooterDataDefault);
|
|
258
|
+
const lines = component.render(200);
|
|
259
|
+
expect(lines.length).toBe(1);
|
|
260
|
+
component.dispose?.();
|
|
261
|
+
});
|
|
262
|
+
|
|
263
|
+
it("subagent count from extension status appears on row 2 and is excluded from generic statuses", async () => {
|
|
200
264
|
process.env.ASTRO_FOOTER_NERD_FONTS = "0";
|
|
201
265
|
const { pi } = await install();
|
|
202
266
|
const ctx = makeCtx();
|
|
203
267
|
await pi.handlers.session_start({}, ctx);
|
|
204
|
-
const factory = ctx.ui.setFooter.mock.calls[0][0] as (
|
|
205
|
-
tui: unknown,
|
|
206
|
-
theme: unknown,
|
|
207
|
-
footerData: unknown,
|
|
208
|
-
) => { render: (w: number) => string[] };
|
|
209
|
-
const fakeTheme = { fg: (_role: string, text: string) => text };
|
|
210
268
|
const fakeFooterData = {
|
|
211
|
-
|
|
212
|
-
getExtensionStatuses: () =>
|
|
213
|
-
|
|
214
|
-
|
|
269
|
+
...fakeFooterDataDefault,
|
|
270
|
+
getExtensionStatuses: () =>
|
|
271
|
+
new Map([
|
|
272
|
+
["subagents", "2"],
|
|
273
|
+
["caveman", "🪨 caveman:full"],
|
|
274
|
+
]),
|
|
215
275
|
};
|
|
216
|
-
const component =
|
|
217
|
-
const lines = component.render(
|
|
218
|
-
expect(lines).
|
|
219
|
-
|
|
220
|
-
expect(
|
|
276
|
+
const component = getFactory(ctx)({ requestRender: () => {} }, fakeTheme, fakeFooterData);
|
|
277
|
+
const lines = component.render(200);
|
|
278
|
+
expect(lines.length).toBe(2);
|
|
279
|
+
const row2 = lines[1];
|
|
280
|
+
expect(row2).toContain("2");
|
|
281
|
+
expect(row2).toContain("🪨 caveman:full");
|
|
282
|
+
expect(row2).not.toMatch(/\bsubagents\s*:\s*2\b/);
|
|
283
|
+
component.dispose?.();
|
|
284
|
+
});
|
|
285
|
+
|
|
286
|
+
it("session name shows on row 2 when set", async () => {
|
|
287
|
+
process.env.ASTRO_FOOTER_NERD_FONTS = "0";
|
|
288
|
+
const { pi } = await install("before-rewrite");
|
|
289
|
+
const ctx = makeCtx();
|
|
290
|
+
await pi.handlers.session_start({}, ctx);
|
|
291
|
+
const component = getFactory(ctx)({ requestRender: () => {} }, fakeTheme, fakeFooterDataDefault);
|
|
292
|
+
const lines = component.render(200);
|
|
293
|
+
expect(lines.length).toBe(2);
|
|
294
|
+
expect(lines[1]).toContain("before-rewrite");
|
|
295
|
+
component.dispose?.();
|
|
296
|
+
});
|
|
297
|
+
|
|
298
|
+
it("cache read/write totals show on row 2 when present", async () => {
|
|
299
|
+
process.env.ASTRO_FOOTER_NERD_FONTS = "0";
|
|
300
|
+
const { pi } = await install();
|
|
301
|
+
const ctx = makeCtx({
|
|
302
|
+
sessionManager: {
|
|
303
|
+
getBranch: () => [
|
|
304
|
+
{
|
|
305
|
+
type: "message",
|
|
306
|
+
message: {
|
|
307
|
+
role: "assistant",
|
|
308
|
+
usage: {
|
|
309
|
+
input: 100,
|
|
310
|
+
output: 50,
|
|
311
|
+
cacheRead: 8500,
|
|
312
|
+
cacheWrite: 2100,
|
|
313
|
+
cost: { total: 0.01 },
|
|
314
|
+
},
|
|
315
|
+
},
|
|
316
|
+
},
|
|
317
|
+
],
|
|
318
|
+
},
|
|
319
|
+
});
|
|
320
|
+
await pi.handlers.session_start({}, ctx);
|
|
321
|
+
const component = getFactory(ctx)({ requestRender: () => {} }, fakeTheme, fakeFooterDataDefault);
|
|
322
|
+
const lines = component.render(200);
|
|
323
|
+
expect(lines.length).toBe(2);
|
|
324
|
+
const row2 = lines[1];
|
|
325
|
+
expect(row2).toContain("cR");
|
|
326
|
+
expect(row2).toContain("8.5k");
|
|
327
|
+
expect(row2).toContain("cW");
|
|
328
|
+
expect(row2).toContain("2.1k");
|
|
329
|
+
component.dispose?.();
|
|
330
|
+
});
|
|
331
|
+
|
|
332
|
+
it("ASTRO_FOOTER_PATH=full shows the full home-tilded path", async () => {
|
|
333
|
+
process.env.ASTRO_FOOTER_NERD_FONTS = "0";
|
|
334
|
+
process.env.ASTRO_FOOTER_PATH = "full";
|
|
335
|
+
const { pi } = await install();
|
|
336
|
+
const ctx = makeCtx({ cwd: `${process.env.HOME}/wb/github/astrofoundry/pi-astro` });
|
|
337
|
+
await pi.handlers.session_start({}, ctx);
|
|
338
|
+
const component = getFactory(ctx)({ requestRender: () => {} }, fakeTheme, fakeFooterDataDefault);
|
|
339
|
+
const row1 = component.render(200)[0];
|
|
340
|
+
expect(row1).toContain("~/wb/github/astrofoundry/pi-astro");
|
|
341
|
+
component.dispose?.();
|
|
221
342
|
});
|
|
222
343
|
});
|
|
@@ -5,8 +5,12 @@ import type {
|
|
|
5
5
|
} from "@mariozechner/pi-coding-agent";
|
|
6
6
|
import type { AssistantMessage } from "@mariozechner/pi-ai";
|
|
7
7
|
import { truncateToWidth, visibleWidth } from "@mariozechner/pi-tui";
|
|
8
|
+
import { resolvePathMode, type PathMode } from "./format.ts";
|
|
9
|
+
import { GitStatusTracker } from "./git-status.ts";
|
|
8
10
|
import { detectNerdFonts, iconsFor, type IconSet } from "./icons.ts";
|
|
11
|
+
import { isAutoCompactEnabled } from "./settings.ts";
|
|
9
12
|
import {
|
|
13
|
+
renderCache,
|
|
10
14
|
renderContext,
|
|
11
15
|
renderCost,
|
|
12
16
|
renderExtensionStatus,
|
|
@@ -14,19 +18,36 @@ import {
|
|
|
14
18
|
renderModel,
|
|
15
19
|
renderPath,
|
|
16
20
|
renderPi,
|
|
21
|
+
renderSessionName,
|
|
22
|
+
renderSessionTime,
|
|
23
|
+
renderSubagents,
|
|
17
24
|
renderThinking,
|
|
18
25
|
renderTokens,
|
|
19
26
|
type ThemeFn,
|
|
20
27
|
} from "./segments.ts";
|
|
21
28
|
|
|
29
|
+
const SUBAGENT_STATUS_KEY = "subagents";
|
|
30
|
+
|
|
22
31
|
interface UsageTotals {
|
|
23
32
|
input: number;
|
|
24
33
|
output: number;
|
|
25
34
|
cost: number;
|
|
35
|
+
cacheRead: number;
|
|
36
|
+
cacheWrite: number;
|
|
37
|
+
paidTurns: number;
|
|
38
|
+
totalTurns: number;
|
|
26
39
|
}
|
|
27
40
|
|
|
28
41
|
function collectUsage(ctx: ExtensionContext): UsageTotals {
|
|
29
|
-
const totals: UsageTotals = {
|
|
42
|
+
const totals: UsageTotals = {
|
|
43
|
+
input: 0,
|
|
44
|
+
output: 0,
|
|
45
|
+
cost: 0,
|
|
46
|
+
cacheRead: 0,
|
|
47
|
+
cacheWrite: 0,
|
|
48
|
+
paidTurns: 0,
|
|
49
|
+
totalTurns: 0,
|
|
50
|
+
};
|
|
30
51
|
for (const entry of ctx.sessionManager.getBranch()) {
|
|
31
52
|
if (entry.type !== "message") continue;
|
|
32
53
|
if (entry.message.role !== "assistant") continue;
|
|
@@ -34,71 +55,160 @@ function collectUsage(ctx: ExtensionContext): UsageTotals {
|
|
|
34
55
|
totals.input += msg.usage.input;
|
|
35
56
|
totals.output += msg.usage.output;
|
|
36
57
|
totals.cost += msg.usage.cost.total;
|
|
58
|
+
totals.cacheRead += msg.usage.cacheRead;
|
|
59
|
+
totals.cacheWrite += msg.usage.cacheWrite;
|
|
60
|
+
totals.totalTurns++;
|
|
61
|
+
if (msg.usage.cost.total > 0) totals.paidTurns++;
|
|
37
62
|
}
|
|
38
63
|
return totals;
|
|
39
64
|
}
|
|
40
65
|
|
|
41
|
-
function
|
|
66
|
+
function readSubagentCount(footerData: ReadonlyFooterDataProvider): number {
|
|
67
|
+
const value = footerData.getExtensionStatuses().get(SUBAGENT_STATUS_KEY);
|
|
68
|
+
if (!value) return 0;
|
|
69
|
+
const parsed = Number.parseInt(value, 10);
|
|
70
|
+
return Number.isFinite(parsed) && parsed > 0 ? parsed : 0;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function buildLines(
|
|
42
74
|
theme: ThemeFn,
|
|
43
75
|
icons: IconSet,
|
|
44
76
|
ctx: ExtensionContext,
|
|
45
77
|
pi: ExtensionAPI,
|
|
46
78
|
footerData: ReadonlyFooterDataProvider,
|
|
79
|
+
gitTracker: GitStatusTracker,
|
|
80
|
+
pathMode: PathMode,
|
|
81
|
+
sessionStartedAt: number,
|
|
82
|
+
autoCompact: boolean,
|
|
47
83
|
width: number,
|
|
48
|
-
): string {
|
|
84
|
+
): string[] {
|
|
49
85
|
const usage = collectUsage(ctx);
|
|
50
|
-
const totalTokens = usage.input + usage.output;
|
|
51
86
|
const contextUsage = ctx.getContextUsage();
|
|
52
87
|
const thinkingLevel = pi.getThinkingLevel();
|
|
53
88
|
const branch = footerData.getGitBranch();
|
|
89
|
+
const gitCounts = gitTracker.get();
|
|
54
90
|
const statuses = footerData.getExtensionStatuses();
|
|
91
|
+
const subagentCount = readSubagentCount(footerData);
|
|
92
|
+
const sessionName = pi.getSessionName();
|
|
93
|
+
const usingSubscription =
|
|
94
|
+
ctx.model !== undefined && ctx.modelRegistry.isUsingOAuth(ctx.model) && usage.totalTurns > 0;
|
|
95
|
+
|
|
96
|
+
const sep = ` ${theme.fg("border", icons.separator)} `;
|
|
55
97
|
|
|
56
|
-
const
|
|
98
|
+
const row1Left = [
|
|
57
99
|
renderPi(theme, icons),
|
|
58
100
|
renderModel(theme, icons, ctx.model?.id),
|
|
59
101
|
renderThinking(theme, icons, thinkingLevel),
|
|
60
|
-
renderPath(theme, icons, ctx.cwd),
|
|
61
|
-
renderGit(theme, icons, branch),
|
|
102
|
+
renderPath(theme, icons, ctx.cwd, pathMode),
|
|
103
|
+
renderGit(theme, icons, branch, gitCounts),
|
|
62
104
|
].filter((s): s is string => Boolean(s));
|
|
63
105
|
|
|
64
|
-
const
|
|
65
|
-
const tokenSegment = renderTokens(theme, icons,
|
|
66
|
-
if (tokenSegment)
|
|
67
|
-
const costSegment = renderCost(theme, icons, usage.cost);
|
|
68
|
-
if (costSegment)
|
|
69
|
-
const contextSegment = renderContext(
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
106
|
+
const row1Right: string[] = [];
|
|
107
|
+
const tokenSegment = renderTokens(theme, icons, usage.input, usage.output);
|
|
108
|
+
if (tokenSegment) row1Right.push(tokenSegment);
|
|
109
|
+
const costSegment = renderCost(theme, icons, { costUsd: usage.cost, isSubscription: usingSubscription });
|
|
110
|
+
if (costSegment) row1Right.push(costSegment);
|
|
111
|
+
const contextSegment = renderContext(
|
|
112
|
+
theme,
|
|
113
|
+
icons,
|
|
114
|
+
contextUsage
|
|
115
|
+
? {
|
|
116
|
+
percent: contextUsage.percent,
|
|
117
|
+
contextWindow: contextUsage.contextWindow,
|
|
118
|
+
autoCompact,
|
|
119
|
+
}
|
|
120
|
+
: null,
|
|
121
|
+
);
|
|
122
|
+
if (contextSegment) row1Right.push(contextSegment);
|
|
76
123
|
|
|
77
|
-
const
|
|
78
|
-
const
|
|
79
|
-
|
|
124
|
+
const row2Items: string[] = [];
|
|
125
|
+
const subagentSegment = renderSubagents(theme, icons, subagentCount);
|
|
126
|
+
if (subagentSegment) row2Items.push(subagentSegment);
|
|
127
|
+
const elapsedMs = Math.max(0, Date.now() - sessionStartedAt);
|
|
128
|
+
const sessionTimeSegment = renderSessionTime(theme, icons, elapsedMs);
|
|
129
|
+
if (sessionTimeSegment) row2Items.push(sessionTimeSegment);
|
|
130
|
+
const sessionNameSegment = renderSessionName(theme, icons, sessionName);
|
|
131
|
+
if (sessionNameSegment) row2Items.push(sessionNameSegment);
|
|
132
|
+
const cacheSegment = renderCache(theme, icons, usage.cacheRead, usage.cacheWrite);
|
|
133
|
+
if (cacheSegment) row2Items.push(cacheSegment);
|
|
134
|
+
for (const [key, value] of statuses) {
|
|
135
|
+
if (!value || key === SUBAGENT_STATUS_KEY) continue;
|
|
136
|
+
row2Items.push(renderExtensionStatus(theme, value));
|
|
137
|
+
}
|
|
80
138
|
|
|
81
|
-
|
|
139
|
+
const row1Left2 = row1Left.join(sep);
|
|
140
|
+
const row1Right2 = row1Right.join(sep);
|
|
141
|
+
let row1: string;
|
|
142
|
+
if (row1Right2.length === 0) {
|
|
143
|
+
row1 = truncateToWidth(row1Left2, width);
|
|
144
|
+
} else {
|
|
145
|
+
const padWidth = Math.max(1, width - visibleWidth(row1Left2) - visibleWidth(row1Right2));
|
|
146
|
+
row1 = truncateToWidth(row1Left2 + " ".repeat(padWidth) + row1Right2, width);
|
|
147
|
+
}
|
|
82
148
|
|
|
83
|
-
|
|
84
|
-
|
|
149
|
+
if (row2Items.length === 0) return [row1];
|
|
150
|
+
const row2 = truncateToWidth(row2Items.join(sep), width);
|
|
151
|
+
return [row1, row2];
|
|
85
152
|
}
|
|
86
153
|
|
|
154
|
+
const REFRESH_INTERVAL_MS = 30_000;
|
|
155
|
+
|
|
87
156
|
export default function astroFooterExtension(pi: ExtensionAPI): void {
|
|
88
157
|
const useNerd = detectNerdFonts();
|
|
89
158
|
const icons = iconsFor(useNerd);
|
|
90
|
-
|
|
159
|
+
const pathMode = resolvePathMode();
|
|
91
160
|
let enabled = true;
|
|
161
|
+
let sessionStartedAt = Date.now();
|
|
162
|
+
let autoCompact = true;
|
|
163
|
+
let activeRequestRender: (() => void) | null = null;
|
|
164
|
+
let timeTickHandle: ReturnType<typeof setInterval> | null = null;
|
|
165
|
+
|
|
166
|
+
const gitTracker = new GitStatusTracker(() => {
|
|
167
|
+
activeRequestRender?.();
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
function startTimeTicker(): void {
|
|
171
|
+
if (timeTickHandle !== null) return;
|
|
172
|
+
timeTickHandle = setInterval(() => {
|
|
173
|
+
activeRequestRender?.();
|
|
174
|
+
}, REFRESH_INTERVAL_MS);
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
function stopTimeTicker(): void {
|
|
178
|
+
if (timeTickHandle === null) return;
|
|
179
|
+
clearInterval(timeTickHandle);
|
|
180
|
+
timeTickHandle = null;
|
|
181
|
+
}
|
|
92
182
|
|
|
93
183
|
function activate(ctx: ExtensionContext): void {
|
|
94
|
-
|
|
184
|
+
sessionStartedAt = Date.now();
|
|
185
|
+
autoCompact = isAutoCompactEnabled(ctx.cwd);
|
|
186
|
+
gitTracker.refresh(ctx.cwd, true);
|
|
187
|
+
ctx.ui.setFooter((tui, theme, footerData) => {
|
|
188
|
+
activeRequestRender = () => tui.requestRender();
|
|
189
|
+
startTimeTicker();
|
|
95
190
|
const themeFn: ThemeFn = {
|
|
96
191
|
fg: (role, text) => theme.fg(role, text),
|
|
97
192
|
};
|
|
98
193
|
return {
|
|
99
194
|
invalidate() {},
|
|
195
|
+
dispose() {
|
|
196
|
+
stopTimeTicker();
|
|
197
|
+
activeRequestRender = null;
|
|
198
|
+
},
|
|
100
199
|
render(width: number): string[] {
|
|
101
|
-
return
|
|
200
|
+
return buildLines(
|
|
201
|
+
themeFn,
|
|
202
|
+
icons,
|
|
203
|
+
ctx,
|
|
204
|
+
pi,
|
|
205
|
+
footerData,
|
|
206
|
+
gitTracker,
|
|
207
|
+
pathMode,
|
|
208
|
+
sessionStartedAt,
|
|
209
|
+
autoCompact,
|
|
210
|
+
width,
|
|
211
|
+
);
|
|
102
212
|
},
|
|
103
213
|
};
|
|
104
214
|
});
|
|
@@ -108,6 +218,13 @@ export default function astroFooterExtension(pi: ExtensionAPI): void {
|
|
|
108
218
|
if (enabled) activate(ctx);
|
|
109
219
|
});
|
|
110
220
|
|
|
221
|
+
pi.on("tool_result", async (event, ctx) => {
|
|
222
|
+
if (!enabled) return;
|
|
223
|
+
if (event.toolName === "edit" || event.toolName === "write" || event.toolName === "bash") {
|
|
224
|
+
gitTracker.refresh(ctx.cwd);
|
|
225
|
+
}
|
|
226
|
+
});
|
|
227
|
+
|
|
111
228
|
pi.registerCommand("footer", {
|
|
112
229
|
description: "Toggle astro-footer. Usage: /footer [on|off|status].",
|
|
113
230
|
getArgumentCompletions: (prefix) => {
|
|
@@ -122,7 +239,9 @@ export default function astroFooterExtension(pi: ExtensionAPI): void {
|
|
|
122
239
|
const sub = args.trim().toLowerCase() || "status";
|
|
123
240
|
if (sub === "status") {
|
|
124
241
|
ctx.ui.notify(
|
|
125
|
-
enabled
|
|
242
|
+
enabled
|
|
243
|
+
? `astro-footer: on (${useNerd ? "nerd" : "ascii"} icons, path=${pathMode})`
|
|
244
|
+
: "astro-footer: off",
|
|
126
245
|
"info",
|
|
127
246
|
);
|
|
128
247
|
return;
|
|
@@ -135,14 +254,12 @@ export default function astroFooterExtension(pi: ExtensionAPI): void {
|
|
|
135
254
|
}
|
|
136
255
|
if (sub === "off") {
|
|
137
256
|
enabled = false;
|
|
257
|
+
stopTimeTicker();
|
|
138
258
|
ctx.ui.setFooter(undefined);
|
|
139
259
|
ctx.ui.notify("astro-footer: off (default footer restored)", "info");
|
|
140
260
|
return;
|
|
141
261
|
}
|
|
142
|
-
ctx.ui.notify(
|
|
143
|
-
`astro-footer: unknown subcommand "${sub}". Use on|off|status.`,
|
|
144
|
-
"warning",
|
|
145
|
-
);
|
|
262
|
+
ctx.ui.notify(`astro-footer: unknown subcommand "${sub}". Use on|off|status.`, "warning");
|
|
146
263
|
},
|
|
147
264
|
});
|
|
148
265
|
}
|