@astrofoundry/pi-astro 0.14.2 → 0.14.4

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.
@@ -1,98 +1,92 @@
1
1
  import { fgBg, PALETTE, type RGB } from "./colors.ts";
2
2
 
3
- const _ = "·";
4
- const S = "S";
5
- const X = "X";
6
- const A = "A";
7
- const Y = "Y";
8
- const W = "W";
9
- const M = "M";
10
- const D = "D";
11
- const G = "G";
12
- const T = "T";
13
- const E = "E";
14
- const F = "F";
15
- const O = "O";
16
- const R = "R";
17
-
18
- type PaletteKey = typeof _ | typeof S | typeof X | typeof A | typeof Y | typeof W | typeof M | typeof D | typeof G | typeof T | typeof E | typeof F | typeof O | typeof R;
3
+ type PaletteKey =
4
+ | "·"
5
+ | "S"
6
+ | "X"
7
+ | "A"
8
+ | "Y"
9
+ | "L"
10
+ | "H"
11
+ | "B"
12
+ | "V"
13
+ | "K"
14
+ | "W"
15
+ | "M"
16
+ | "D"
17
+ | "G"
18
+ | "T"
19
+ | "E"
20
+ | "F"
21
+ | "O"
22
+ | "R";
19
23
 
20
24
  const PIXEL_COLOR: Record<PaletteKey, RGB | null> = {
21
- [_]: null,
22
- [S]: PALETTE.suitLight,
23
- [X]: PALETTE.suitMid,
24
- [A]: PALETTE.suitShadow,
25
- [Y]: PALETTE.suitTrim,
26
- [W]: PALETTE.visorLow,
27
- [M]: PALETTE.visorMid,
28
- [D]: PALETTE.visorTop,
29
- [G]: PALETTE.backpack,
30
- [T]: PALETTE.earthBlue,
31
- [E]: PALETTE.earthGreen,
32
- [F]: PALETTE.flameYellow,
33
- [O]: PALETTE.flameOrange,
34
- [R]: PALETTE.flameRed,
25
+ "·": null,
26
+ S: PALETTE.suitLight,
27
+ X: PALETTE.suitMid,
28
+ A: PALETTE.suitShadow,
29
+ Y: PALETTE.suitTrim,
30
+ L: PALETTE.suitGoldHigh,
31
+ H: PALETTE.suitGoldShadow,
32
+ B: PALETTE.flagBlue,
33
+ V: PALETTE.voidBlack,
34
+ K: PALETTE.outline,
35
+ W: PALETTE.visorLow,
36
+ M: PALETTE.visorMid,
37
+ D: PALETTE.visorTop,
38
+ G: PALETTE.backpack,
39
+ T: PALETTE.earthBlue,
40
+ E: PALETTE.earthGreen,
41
+ F: PALETTE.flameYellow,
42
+ O: PALETTE.flameOrange,
43
+ R: PALETTE.flameRed,
35
44
  };
36
45
 
37
- const ASTRONAUT_FLOATING: readonly string[] = [
38
- "··········SSSSSSSSSS··········",
39
- "·······SSSSXXXXXXXXSSSS·······",
40
- "·····SSXXMMMMMMMMMMMMXXSS·····",
41
- "····SXMMDDDDDDDDDDDDDDMMSS····",
42
- "···SXMDDDDWWDDDDDDWWDDDDMMS···",
43
- "···SXMDDWWWWDDDDDDWWWWDDMMS···",
44
- "···SXMDDDDDDDDDDDDDDDDDDMMS···",
45
- "···SXMDDDDDDDDWWDDDDDDDDMMS···",
46
- "····SXMMDDDDDDDDDDDDDDMMSS····",
47
- "·····SSXXMMMMMMMMMMMMXXSS·····",
48
- "·······SSXXXXXXXXXXXXSS·······",
49
- "··········SXAAAAAAXSS·········",
50
- "·······SSSSSSSSSSSSSSSS·······",
51
- "······SSSSGGGGSSSSGGGGSSS·····",
52
- "·····SSSSGGGGSSAAYAASSGGSSS···",
53
- "·····SSGGGGSSSAYYYAASSGGGGS···",
54
- "·····SSGGGSSSSAYAYASSSSGGGS···",
55
- "·····SSGGGSSSSAAAASSSSSGGGS···",
56
- "·····SSGGGSSSSSSSSSSSSGGGS····",
57
- "······SSSSSSSSSSSSSSSSSS······",
58
- "········SSAAAAAASSSSSSAAAA····",
59
- "········SSSAAAASSSSAAAAS······",
60
- "········SXXSSSSSSSXXXSS·······",
61
- "·········SSSS·····SSSS········",
62
- "··········SSS······SSS········",
63
- "··········SS········SS········",
64
- ];
65
-
66
- const ASTRONAUT_FLAG: readonly string[] = [
67
- "··········SSSSSSSSSS··········",
68
- "·······SSSSXXXXXXXXSSSS·······",
69
- "·····SSXXMMMMMMMMMMMMXXSS·····",
70
- "····SXMMDDDDDDDDDDDDDDMMSS····",
71
- "···SXMDDDDWWDDDDDDWWDDDDMMS···",
72
- "···SXMDDWWWWDDDDDDWWWWDDMMS···",
73
- "···SXMDDDDDDDDDDDDDDDDDDMMS···",
74
- "···SXMDDDDDDDDWWDDDDDDDDMMS···",
75
- "····SXMMDDDDDDDDDDDDDDMMSS····",
76
- "·····SSXXMMMMMMMMMMMMXXSS·····",
77
- "·······SSXXXXXXXXXXXXSS·······",
78
- "··········SXAAAAAAXSS·········",
79
- "······YYYYYSSSSSSSSSSSSSS·····",
80
- "······YYYYYSSGGGGSSSGGGGSSS···",
81
- "······YYYYYSSGGGGSSSSGGGSS····",
82
- "······YYYYYSSGGGSSSSSSGGGS····",
83
- "······YYYYYSSGGSSSSSSSSGGS····",
84
- "········SSSSSSSSSSSSSSSSSS····",
85
- "········SSAAAAAASSSAAAASS·····",
86
- "········SSSAAAASSSAAAAAS······",
87
- "········SXXSSSSSSSXXXSS·······",
88
- "·········SSSS·····SSSS········",
89
- "··········SSS······SSS········",
90
- "··········SS········SS········",
91
- "·························E····",
92
- "·························TT···",
46
+ const ASTRONAUT_PORTRAIT: readonly string[] = [
47
+ "········································",
48
+ "···················YY···················",
49
+ "·················YYYYYY·················",
50
+ "··············YYYYYYYYYYYY··············",
51
+ "············YYYYYYYYYYYYYYYY············",
52
+ "··········YYYYYYYYYYYYYYYYYYYY··········",
53
+ "·········YYYYYYYYYYYYYYYYYYYYYY·········",
54
+ "········YYYYYYYYYYYYYYYYYYYYYYYY········",
55
+ "········YYYYYYKKKKKKKKKKKKYYYYYY········",
56
+ "········YYYYKKKKKKKKKKKKKKKKYYYY········",
57
+ "········YYYKKKKKKKKKKKKKKKKKKYYY········",
58
+ "········YYYKKKKKKKKKKKKKKKKKKYYY········",
59
+ "········YYYKKKKKKKKKKKKKKKKKKYYY········",
60
+ "········YYYKKKKKKKKKKKKKKKKKKYYY········",
61
+ "········YYYKKKKKKKKKKKKKKKKKKYYY········",
62
+ "········YYYYKKKKKKKKKKKKKKKKYYYY········",
63
+ "········YYYYYYKKKKKKKKKKKKYYYYYY········",
64
+ "········YYYYYYYYYYYYYYYYYYYYYYYY········",
65
+ "·········YYYYYYYYYYYYYYYYYYYYYY·········",
66
+ "··········YYYYYYYYYYYYYYYYYYYY··········",
67
+ "············YYYYYYYYYYYYYYYY············",
68
+ "··············YYYYYYYYYYYY··············",
69
+ "················YYYYYYYY················",
70
+ "················YYYYYYYY················",
71
+ "·············YYYYYYYYYYYYYY·············",
72
+ "··········YYYYYYYYYYYYYYYYYYYY··········",
73
+ "········YYYYYYYYYYYYYYYYYYYYYYYY········",
74
+ "······YYYYYYYYYYYYYYYYYYYYYYYYYYYY······",
75
+ "·····YYYYYYYYYYYYYYYYYYYYYYYYYYYYYY·····",
76
+ "····YYYYYYYYYYYYYYYYYYYYYYYBSYYYYYYY····",
77
+ "····YYYYYYYYYYYYYYYYYYYYYBSBBYYYYYYY····",
78
+ "····YYYYYYYYYYYYYYYYYYYBBBBBBYYYYYYY····",
79
+ "····YYYYYYYYYYYYYYYYYBBSBBBBBYYYYYYY····",
80
+ "····YYYYYYYYYYYYYYYBBBBBBBBBBYYYYYYY····",
81
+ "····YYYYYYYYYYYYYBBSBBBBBBBBBYYYYYYY····",
82
+ "····YYYYYYYYYYYBBBBBBBBBBBBBBYYYYYYY····",
83
+ "····YYYYYYYYYBBSBBBBBBBBBBBBBYYYYYYY····",
84
+ "····YYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYY····",
85
+ "···YYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYY···",
86
+ "··YYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYY··",
93
87
  ];
94
88
 
95
- export const VARIANTS = [ASTRONAUT_FLOATING, ASTRONAUT_FLAG] as const;
89
+ export const VARIANTS = [ASTRONAUT_PORTRAIT] as const;
96
90
 
97
91
  export function pickVariant(seed: number): readonly string[] {
98
92
  const idx = Math.abs(seed) % VARIANTS.length;
@@ -121,5 +115,5 @@ export function renderArt(grid: readonly string[]): string[] {
121
115
  return rows;
122
116
  }
123
117
 
124
- export const ART_WIDTH = ASTRONAUT_FLOATING[0].length;
125
- export const ART_HEIGHT = Math.ceil(ASTRONAUT_FLOATING.length / 2);
118
+ export const ART_WIDTH = ASTRONAUT_PORTRAIT[0].length;
119
+ export const ART_HEIGHT = Math.ceil(ASTRONAUT_PORTRAIT.length / 2);
@@ -16,10 +16,16 @@ export const PALETTE = {
16
16
  visorTop: [40, 80, 180] as const,
17
17
  visorMid: [80, 160, 240] as const,
18
18
  visorLow: [180, 220, 255] as const,
19
- suitLight: [240, 240, 230] as const,
19
+ suitLight: [255, 255, 255] as const,
20
20
  suitMid: [200, 200, 195] as const,
21
21
  suitShadow: [120, 120, 130] as const,
22
- suitTrim: [240, 200, 60] as const,
22
+ suitTrim: [254, 209, 0] as const,
23
+ suitGoldHigh: [255, 220, 110] as const,
24
+ suitGoldShadow: [200, 150, 30] as const,
25
+ flagBlue: [0, 51, 160] as const,
26
+ voidBlack: [10, 12, 30] as const,
27
+ outline: [22, 32, 64] as const,
28
+ highlight: [255, 255, 255] as const,
23
29
  backpack: [80, 80, 90] as const,
24
30
  earthBlue: [50, 110, 200] as const,
25
31
  earthGreen: [70, 160, 100] as const,
@@ -6,6 +6,7 @@ const baseStats: WelcomeStats = {
6
6
  contextWindow: 1_000_000,
7
7
  extensionCount: 9,
8
8
  cavemanLevel: undefined,
9
+ pkgVersion: "0.14.2",
9
10
  };
10
11
 
11
12
  describe("composeWelcome", () => {
@@ -48,6 +49,16 @@ describe("composeWelcome", () => {
48
49
  expect(() => composeWelcome(40, 1, baseStats)).not.toThrow();
49
50
  });
50
51
 
52
+ it("renders the version subtitle when pkgVersion is provided", () => {
53
+ const lines = composeWelcome(120, 1, baseStats);
54
+ expect(lines.join("\n")).toContain("v0.14.2");
55
+ });
56
+
57
+ it("omits the version row when pkgVersion is undefined", () => {
58
+ const lines = composeWelcome(120, 1, { ...baseStats, pkgVersion: undefined });
59
+ expect(lines.join("\n")).not.toMatch(/v\d+\.\d+\.\d+/);
60
+ });
61
+
51
62
  it("produces deterministic output for the same seed", () => {
52
63
  const a = composeWelcome(120, 99, baseStats);
53
64
  const b = composeWelcome(120, 99, baseStats);
@@ -9,6 +9,7 @@ export interface WelcomeStats {
9
9
  contextWindow: number | undefined;
10
10
  extensionCount: number;
11
11
  cavemanLevel: string | undefined;
12
+ pkgVersion: string | undefined;
12
13
  }
13
14
 
14
15
  const TITLE = "★ ASTRO PI ★";
@@ -69,10 +70,12 @@ class CompositeMask implements Mask {
69
70
  }
70
71
 
71
72
  export function composeWelcome(width: number, seed: number, stats: WelcomeStats): string[] {
72
- const totalRows = ART_HEIGHT + 7;
73
+ const hasVersion = !!stats.pkgVersion;
74
+ const versionRow = 1;
75
+ const artStartRow = hasVersion ? 2 : 1;
76
+ const totalRows = artStartRow + ART_HEIGHT + 6;
73
77
  const safeWidth = Math.max(40, width);
74
78
  const artStartCol = Math.max(0, Math.floor((safeWidth - ART_WIDTH) / 2));
75
- const artStartRow = 1;
76
79
 
77
80
  const mask = new CompositeMask(totalRows, safeWidth);
78
81
  mask.occupyArt(artStartRow, artStartCol, ART_HEIGHT, ART_WIDTH);
@@ -81,6 +84,13 @@ export function composeWelcome(width: number, seed: number, stats: WelcomeStats)
81
84
  const titlePlaced = centerInLine(titleLine, safeWidth);
82
85
  mask.occupy(0, titlePlaced.col, visibleWidth(titleLine));
83
86
 
87
+ let versionPlaced: { line: string; col: number } | null = null;
88
+ if (hasVersion) {
89
+ const versionLine = fg(PALETTE.hint, `v${stats.pkgVersion}`);
90
+ versionPlaced = centerInLine(versionLine, safeWidth);
91
+ mask.occupy(versionRow, versionPlaced.col, visibleWidth(versionLine));
92
+ }
93
+
84
94
  const taglineRaw = pickMessage(seed);
85
95
  const taglineLine = fg(PALETTE.tagline, taglineRaw);
86
96
  const taglineRow = artStartRow + ART_HEIGHT + 1;
@@ -106,6 +116,7 @@ export function composeWelcome(width: number, seed: number, stats: WelcomeStats)
106
116
  let line = stars[r] ?? " ".repeat(safeWidth);
107
117
 
108
118
  if (r === 0) line = overlay(line, titlePlaced.line, safeWidth);
119
+ if (versionPlaced && r === versionRow) line = overlay(line, versionPlaced.line, safeWidth);
109
120
  if (r === taglineRow) line = overlay(line, taglinePlaced.line, safeWidth);
110
121
  if (r === statsRow) line = overlay(line, statsPlaced.line, safeWidth);
111
122
  if (r === hintRow) line = overlay(line, hintPlaced.line, safeWidth);
@@ -23,7 +23,7 @@ interface FakeCtx {
23
23
  hasUI: boolean;
24
24
  cwd: string;
25
25
  model: { id: string; contextWindow: number } | undefined;
26
- sessionManager: { getEntries: () => unknown[] };
26
+ sessionManager: { getEntries: () => unknown[]; getBranch: () => unknown[] };
27
27
  ui: {
28
28
  custom: ReturnType<typeof vi.fn>;
29
29
  };
@@ -75,7 +75,7 @@ function makeCtx(overrides: Partial<FakeCtx> = {}): FakeCtx & { _factory: Captur
75
75
  hasUI: true,
76
76
  cwd: "/Users/astro/proj",
77
77
  model: { id: "anthropic/claude-sonnet-4-6", contextWindow: 1_000_000 },
78
- sessionManager: { getEntries: () => [] },
78
+ sessionManager: { getEntries: () => [], getBranch: () => [] },
79
79
  ui: { custom: customMock },
80
80
  get _factory() {
81
81
  return state.factory;
@@ -108,10 +108,12 @@ describe("astro-welcome extension", () => {
108
108
  expect(pi.handlers.before_agent_start).toBeDefined();
109
109
  });
110
110
 
111
- it("session_start opens a centered overlay via ctx.ui.custom", async () => {
111
+ it("session_start opens a centered overlay via ctx.ui.custom (after a short delay)", async () => {
112
112
  const { pi } = await install();
113
113
  const ctx = makeCtx();
114
114
  await pi.handlers.session_start({}, ctx);
115
+ expect(ctx.ui.custom).not.toHaveBeenCalled();
116
+ vi.advanceTimersByTime(100);
115
117
  expect(ctx.ui.custom).toHaveBeenCalledTimes(1);
116
118
  const opts = ctx._factory!.options as { overlay: boolean; overlayOptions: { anchor: string } };
117
119
  expect(opts.overlay).toBe(true);
@@ -122,6 +124,35 @@ describe("astro-welcome extension", () => {
122
124
  const { pi } = await install();
123
125
  const ctx = makeCtx({ hasUI: false });
124
126
  await pi.handlers.session_start({}, ctx);
127
+ vi.advanceTimersByTime(100);
128
+ expect(ctx.ui.custom).not.toHaveBeenCalled();
129
+ });
130
+
131
+ it("does not show welcome a second time within the same process", async () => {
132
+ const { pi } = await install();
133
+ const ctx1 = makeCtx();
134
+ await pi.handlers.session_start({}, ctx1);
135
+ vi.advanceTimersByTime(100);
136
+ ctx1._factory!.component!.handleInput!("x");
137
+ await ctx1._factory!.settle;
138
+ const ctx2 = makeCtx();
139
+ await pi.handlers.session_start({}, ctx2);
140
+ vi.advanceTimersByTime(100);
141
+ expect(ctx2.ui.custom).not.toHaveBeenCalled();
142
+ });
143
+
144
+ it("skips welcome when session already has assistant activity (e.g. /reload mid-session)", async () => {
145
+ const { pi } = await install();
146
+ const ctx = makeCtx({
147
+ sessionManager: {
148
+ getBranch: () => [
149
+ { type: "message", message: { role: "assistant" } },
150
+ ],
151
+ getEntries: () => [],
152
+ },
153
+ });
154
+ await pi.handlers.session_start({}, ctx);
155
+ vi.advanceTimersByTime(100);
125
156
  expect(ctx.ui.custom).not.toHaveBeenCalled();
126
157
  });
127
158
 
@@ -129,6 +160,7 @@ describe("astro-welcome extension", () => {
129
160
  const { pi } = await install();
130
161
  const ctx = makeCtx();
131
162
  await pi.handlers.session_start({}, ctx);
163
+ vi.advanceTimersByTime(100);
132
164
  const settle = ctx._factory!.settle;
133
165
  vi.advanceTimersByTime(6_000);
134
166
  await expect(settle).resolves.toBeUndefined();
@@ -138,24 +170,36 @@ describe("astro-welcome extension", () => {
138
170
  const { pi } = await install();
139
171
  const ctx = makeCtx();
140
172
  await pi.handlers.session_start({}, ctx);
173
+ vi.advanceTimersByTime(100);
141
174
  const settle = ctx._factory!.settle;
142
175
  ctx._factory!.component!.handleInput!("x");
143
176
  await expect(settle).resolves.toBeUndefined();
144
177
  });
145
178
 
146
- it("dismisses on before_agent_start (external dismiss)", async () => {
179
+ it("dismisses on before_agent_start (external dismiss after mount)", async () => {
147
180
  const { pi } = await install();
148
181
  const ctx = makeCtx();
149
182
  await pi.handlers.session_start({}, ctx);
183
+ vi.advanceTimersByTime(100);
150
184
  const settle = ctx._factory!.settle;
151
185
  await pi.handlers.before_agent_start({ prompt: "hi" }, ctx);
152
186
  await expect(settle).resolves.toBeUndefined();
153
187
  });
154
188
 
189
+ it("before_agent_start fired during the 100ms mount-delay cancels the overlay entirely", async () => {
190
+ const { pi } = await install();
191
+ const ctx = makeCtx();
192
+ await pi.handlers.session_start({}, ctx);
193
+ await pi.handlers.before_agent_start({ prompt: "fast" }, ctx);
194
+ vi.advanceTimersByTime(200);
195
+ expect(ctx.ui.custom).not.toHaveBeenCalled();
196
+ });
197
+
155
198
  it("dismiss is idempotent — second trigger does not throw", async () => {
156
199
  const { pi } = await install();
157
200
  const ctx = makeCtx();
158
201
  await pi.handlers.session_start({}, ctx);
202
+ vi.advanceTimersByTime(100);
159
203
  ctx._factory!.component!.handleInput!("a");
160
204
  await ctx._factory!.settle;
161
205
  expect(() => ctx._factory!.component!.handleInput!("b")).not.toThrow();
@@ -166,6 +210,7 @@ describe("astro-welcome extension", () => {
166
210
  const { pi } = await install();
167
211
  const ctx = makeCtx();
168
212
  await pi.handlers.session_start({}, ctx);
213
+ vi.advanceTimersByTime(100);
169
214
  const lines = ctx._factory!.component!.render(120);
170
215
  const joined = lines.join("\n");
171
216
  expect(joined).toContain("ASTRO PI");
@@ -182,9 +227,11 @@ describe("astro-welcome extension", () => {
182
227
  getEntries: () => [
183
228
  { type: "custom", customType: "caveman-mode", data: { level: "ultra" } },
184
229
  ],
230
+ getBranch: () => [],
185
231
  },
186
232
  });
187
233
  await pi.handlers.session_start({}, ctx);
234
+ vi.advanceTimersByTime(100);
188
235
  const lines = ctx._factory!.component!.render(120);
189
236
  expect(lines.join("\n")).toContain("caveman: ultra");
190
237
  });
@@ -1,10 +1,12 @@
1
1
  import type { ExtensionAPI, ExtensionContext } from "@mariozechner/pi-coding-agent";
2
2
  import { composeWelcome, type WelcomeStats } from "./compose.ts";
3
3
  import { ART_HEIGHT, ART_WIDTH } from "./art.ts";
4
+ import { readPackageVersion } from "./version.ts";
4
5
 
5
6
  const DISMISS_AFTER_MS = 6_000;
7
+ const MOUNT_DELAY_MS = 100;
6
8
  const OVERLAY_WIDTH = Math.max(ART_WIDTH + 30, 70);
7
- const OVERLAY_HEIGHT = ART_HEIGHT + 7;
9
+ const OVERLAY_HEIGHT = ART_HEIGHT + 8;
8
10
 
9
11
  function readCavemanLevel(ctx: ExtensionContext): string | undefined {
10
12
  const entries = ctx.sessionManager.getEntries();
@@ -18,74 +20,106 @@ function readCavemanLevel(ctx: ExtensionContext): string | undefined {
18
20
  return undefined;
19
21
  }
20
22
 
23
+ function hasSessionActivity(ctx: ExtensionContext): boolean {
24
+ for (const entry of ctx.sessionManager.getBranch()) {
25
+ if (entry.type === "message" && entry.message.role === "assistant") return true;
26
+ }
27
+ return false;
28
+ }
29
+
21
30
  export default function astroWelcomeExtension(pi: ExtensionAPI): void {
31
+ let shownThisProcess = false;
22
32
  let externalDismiss: (() => void) | null = null;
33
+ let dismissedBeforeMount = false;
34
+ let pendingMount: ReturnType<typeof setTimeout> | null = null;
23
35
 
24
36
  pi.on("session_start", async (_event, ctx) => {
37
+ if (shownThisProcess) return;
25
38
  if (!ctx.hasUI) return;
39
+ if (hasSessionActivity(ctx)) return;
40
+ shownThisProcess = true;
26
41
 
27
42
  const seed = Date.now() & 0xffff_ffff;
43
+ let pkgVersion: string | undefined;
44
+ try {
45
+ pkgVersion = readPackageVersion();
46
+ } catch {
47
+ pkgVersion = undefined;
48
+ }
28
49
  const stats: WelcomeStats = {
29
50
  modelDisplay: ctx.model?.id,
30
51
  contextWindow: ctx.model?.contextWindow,
31
52
  extensionCount: pi.getAllTools().length,
32
53
  cavemanLevel: readCavemanLevel(ctx),
54
+ pkgVersion,
33
55
  };
34
56
 
35
- let dismissed = false;
36
- let timer: ReturnType<typeof setTimeout> | null = null;
57
+ dismissedBeforeMount = false;
37
58
 
38
- ctx.ui
39
- .custom(
40
- (tui, _theme, _kb, done) => {
41
- const dismiss = (): void => {
42
- if (dismissed) return;
43
- dismissed = true;
44
- if (timer !== null) {
45
- clearTimeout(timer);
46
- timer = null;
47
- }
48
- externalDismiss = null;
49
- done(undefined);
50
- };
59
+ pendingMount = setTimeout(() => {
60
+ pendingMount = null;
61
+ if (dismissedBeforeMount) return;
51
62
 
52
- timer = setTimeout(dismiss, DISMISS_AFTER_MS);
53
- externalDismiss = dismiss;
63
+ let dismissed = false;
64
+ let timer: ReturnType<typeof setTimeout> | null = null;
54
65
 
55
- return {
56
- invalidate(): void {
57
- tui.requestRender();
58
- },
59
- render(width: number): string[] {
60
- return composeWelcome(width, seed, stats);
61
- },
62
- handleInput(): void {
63
- dismiss();
64
- },
65
- dispose(): void {
66
+ ctx.ui
67
+ .custom(
68
+ (tui, _theme, _kb, done) => {
69
+ const dismiss = (): void => {
70
+ if (dismissed) return;
71
+ dismissed = true;
66
72
  if (timer !== null) {
67
73
  clearTimeout(timer);
68
74
  timer = null;
69
75
  }
70
76
  externalDismiss = null;
77
+ done(undefined);
78
+ };
79
+
80
+ timer = setTimeout(dismiss, DISMISS_AFTER_MS);
81
+ externalDismiss = dismiss;
82
+
83
+ return {
84
+ invalidate(): void {
85
+ tui.requestRender();
86
+ },
87
+ render(width: number): string[] {
88
+ return composeWelcome(width, seed, stats);
89
+ },
90
+ handleInput(): void {
91
+ dismiss();
92
+ },
93
+ dispose(): void {
94
+ if (timer !== null) {
95
+ clearTimeout(timer);
96
+ timer = null;
97
+ }
98
+ externalDismiss = null;
99
+ },
100
+ };
101
+ },
102
+ {
103
+ overlay: true,
104
+ overlayOptions: {
105
+ anchor: "center",
106
+ width: OVERLAY_WIDTH,
107
+ maxHeight: OVERLAY_HEIGHT,
71
108
  },
72
- };
73
- },
74
- {
75
- overlay: true,
76
- overlayOptions: {
77
- anchor: "center",
78
- width: OVERLAY_WIDTH,
79
- maxHeight: OVERLAY_HEIGHT,
80
109
  },
81
- },
82
- )
83
- .catch(() => {
84
- /* overlay close errors are not fatal */
85
- });
110
+ )
111
+ .catch(() => {
112
+ /* overlay close errors are not fatal */
113
+ });
114
+ }, MOUNT_DELAY_MS);
86
115
  });
87
116
 
88
117
  pi.on("before_agent_start", async () => {
118
+ if (pendingMount !== null) {
119
+ clearTimeout(pendingMount);
120
+ pendingMount = null;
121
+ dismissedBeforeMount = true;
122
+ }
89
123
  externalDismiss?.();
90
124
  });
91
125
  }
@@ -0,0 +1,12 @@
1
+ import * as fs from "node:fs";
2
+ import * as path from "node:path";
3
+ import { fileURLToPath } from "node:url";
4
+
5
+ const extensionDir = path.dirname(fileURLToPath(import.meta.url));
6
+ const packageJsonPath = path.resolve(extensionDir, "..", "..", "package.json");
7
+
8
+ export function readPackageVersion(): string | undefined {
9
+ const raw = fs.readFileSync(packageJsonPath, "utf8");
10
+ const pkg = JSON.parse(raw) as { version?: unknown };
11
+ return typeof pkg.version === "string" ? pkg.version : undefined;
12
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@astrofoundry/pi-astro",
3
- "version": "0.14.2",
3
+ "version": "0.14.4",
4
4
  "description": "Personal pi customizations (extensions, skills, prompts, themes) for the pi coding agent.",
5
5
  "keywords": [
6
6
  "pi-package"