@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.
- package/extensions/astro-welcome/art.ts +83 -89
- package/extensions/astro-welcome/colors.ts +8 -2
- package/extensions/astro-welcome/compose.test.ts +11 -0
- package/extensions/astro-welcome/compose.ts +13 -2
- package/extensions/astro-welcome/index.test.ts +51 -4
- package/extensions/astro-welcome/index.ts +76 -42
- package/extensions/astro-welcome/version.ts +12 -0
- package/package.json +1 -1
|
@@ -1,98 +1,92 @@
|
|
|
1
1
|
import { fgBg, PALETTE, type RGB } from "./colors.ts";
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
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
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
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
|
|
38
|
-
"
|
|
39
|
-
"
|
|
40
|
-
"
|
|
41
|
-
"
|
|
42
|
-
"
|
|
43
|
-
"
|
|
44
|
-
"
|
|
45
|
-
"
|
|
46
|
-
"
|
|
47
|
-
"
|
|
48
|
-
"
|
|
49
|
-
"
|
|
50
|
-
"
|
|
51
|
-
"
|
|
52
|
-
"
|
|
53
|
-
"
|
|
54
|
-
"
|
|
55
|
-
"
|
|
56
|
-
"
|
|
57
|
-
"
|
|
58
|
-
"
|
|
59
|
-
"
|
|
60
|
-
"
|
|
61
|
-
"
|
|
62
|
-
"
|
|
63
|
-
"··········
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
"
|
|
68
|
-
"
|
|
69
|
-
"
|
|
70
|
-
"····
|
|
71
|
-
"
|
|
72
|
-
"
|
|
73
|
-
"
|
|
74
|
-
"
|
|
75
|
-
"····
|
|
76
|
-
"
|
|
77
|
-
"
|
|
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 = [
|
|
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 =
|
|
125
|
-
export const ART_HEIGHT = Math.ceil(
|
|
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: [
|
|
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: [
|
|
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
|
|
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 +
|
|
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
|
-
|
|
36
|
-
let timer: ReturnType<typeof setTimeout> | null = null;
|
|
57
|
+
dismissedBeforeMount = false;
|
|
37
58
|
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
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
|
-
|
|
53
|
-
|
|
63
|
+
let dismissed = false;
|
|
64
|
+
let timer: ReturnType<typeof setTimeout> | null = null;
|
|
54
65
|
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
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
|
-
|
|
84
|
-
|
|
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
|
+
}
|