@astrofoundry/pi-astro 0.12.2 → 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.
@@ -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
- it("registers /footer command and session_start handler", async () => {
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 a single-line footer with model, path, git, and extension statuses", async () => {
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: { input: 1000, output: 200, cost: { total: 0.05 } },
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.ui.setFooter.mock.calls[0][0] as (
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
- getGitBranch: () => "main",
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).toHaveLength(1);
189
- const line = lines[0];
190
- expect(line).toContain("Anthropic: Claude Sonnet 4 6");
191
- expect(line).toContain("proj");
192
- expect(line).toContain("main");
193
- expect(line).toContain("1.2k");
194
- expect(line).toContain("$0.05");
195
- expect(line).toContain("25.0%/200k");
196
- expect(line).toContain("🪨 caveman:full");
197
- });
198
-
199
- it("renders without right side when there's nothing to show", async () => {
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
- getGitBranch: () => null,
212
- getExtensionStatuses: () => new Map(),
213
- getAvailableProviderCount: () => 1,
214
- onBranchChange: () => () => {},
269
+ ...fakeFooterDataDefault,
270
+ getExtensionStatuses: () =>
271
+ new Map([
272
+ ["subagents", "2"],
273
+ ["caveman", "🪨 caveman:full"],
274
+ ]),
215
275
  };
216
- const component = factory({}, fakeTheme, fakeFooterData);
217
- const lines = component.render(80);
218
- expect(lines).toHaveLength(1);
219
- expect(lines[0]).not.toContain("$");
220
- expect(lines[0]).toContain("proj");
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 = { input: 0, output: 0, cost: 0 };
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 buildLine(
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 left = [
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 right: string[] = [];
65
- const tokenSegment = renderTokens(theme, icons, totalTokens);
66
- if (tokenSegment) right.push(tokenSegment);
67
- const costSegment = renderCost(theme, icons, usage.cost);
68
- if (costSegment) right.push(costSegment);
69
- const contextSegment = renderContext(theme, icons, contextUsage
70
- ? { percent: contextUsage.percent, contextWindow: contextUsage.contextWindow }
71
- : null);
72
- if (contextSegment) right.push(contextSegment);
73
- for (const [, value] of statuses) {
74
- if (value) right.push(renderExtensionStatus(theme, value));
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 sep = ` ${theme.fg("border", icons.separator)} `;
78
- const leftStr = left.join(sep);
79
- const rightStr = right.join(sep);
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
- if (rightStr.length === 0) return truncateToWidth(leftStr, width);
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
- const padWidth = Math.max(1, width - visibleWidth(leftStr) - visibleWidth(rightStr));
84
- return truncateToWidth(leftStr + " ".repeat(padWidth) + rightStr, width);
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
- ctx.ui.setFooter((_tui, theme, footerData) => {
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 [buildLine(themeFn, icons, ctx, pi, footerData, width)];
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 ? `astro-footer: on (${useNerd ? "nerd" : "ascii"} icons)` : "astro-footer: off",
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
  }