@astrofoundry/pi-astro 0.14.1 → 0.14.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
|
@@ -25,33 +25,63 @@ interface FakeCtx {
|
|
|
25
25
|
model: { id: string; contextWindow: number } | undefined;
|
|
26
26
|
sessionManager: { getEntries: () => unknown[] };
|
|
27
27
|
ui: {
|
|
28
|
-
|
|
29
|
-
onTerminalInput: ReturnType<typeof vi.fn>;
|
|
28
|
+
custom: ReturnType<typeof vi.fn>;
|
|
30
29
|
};
|
|
31
30
|
}
|
|
32
31
|
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
32
|
+
interface CapturedComponent {
|
|
33
|
+
invalidate(): void;
|
|
34
|
+
render(width: number): string[];
|
|
35
|
+
handleInput?(data: string): void;
|
|
36
|
+
dispose?(): void;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
interface CapturedFactory {
|
|
40
|
+
options: unknown;
|
|
41
|
+
done: (result: unknown) => void;
|
|
42
|
+
component: CapturedComponent | null;
|
|
43
|
+
settle: Promise<unknown>;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function makeCtx(overrides: Partial<FakeCtx> = {}): FakeCtx & { _factory: CapturedFactory | null } {
|
|
47
|
+
const state: { factory: CapturedFactory | null } = { factory: null };
|
|
48
|
+
const customMock = vi.fn(
|
|
49
|
+
(
|
|
50
|
+
factory: (
|
|
51
|
+
tui: { requestRender: () => void },
|
|
52
|
+
theme: unknown,
|
|
53
|
+
kb: unknown,
|
|
54
|
+
done: (result: unknown) => void,
|
|
55
|
+
) => CapturedComponent,
|
|
56
|
+
options: unknown,
|
|
57
|
+
) => {
|
|
58
|
+
let resolveSettle: (r: unknown) => void = () => {};
|
|
59
|
+
const settle = new Promise<unknown>((resolve) => {
|
|
60
|
+
resolveSettle = resolve;
|
|
61
|
+
});
|
|
62
|
+
const cap: CapturedFactory = {
|
|
63
|
+
options,
|
|
64
|
+
done: (r: unknown) => resolveSettle(r),
|
|
65
|
+
component: null,
|
|
66
|
+
settle,
|
|
67
|
+
};
|
|
68
|
+
state.factory = cap;
|
|
69
|
+
const tui = { requestRender: vi.fn() };
|
|
70
|
+
cap.component = factory(tui, {}, {}, cap.done);
|
|
71
|
+
return cap.settle;
|
|
72
|
+
},
|
|
73
|
+
);
|
|
41
74
|
return {
|
|
42
75
|
hasUI: true,
|
|
43
76
|
cwd: "/Users/astro/proj",
|
|
44
77
|
model: { id: "anthropic/claude-sonnet-4-6", contextWindow: 1_000_000 },
|
|
45
78
|
sessionManager: { getEntries: () => [] },
|
|
46
|
-
ui: {
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
},
|
|
50
|
-
get _terminalListener() {
|
|
51
|
-
return state.listener;
|
|
79
|
+
ui: { custom: customMock },
|
|
80
|
+
get _factory() {
|
|
81
|
+
return state.factory;
|
|
52
82
|
},
|
|
53
83
|
...overrides,
|
|
54
|
-
} as FakeCtx & {
|
|
84
|
+
} as FakeCtx & { _factory: CapturedFactory | null };
|
|
55
85
|
}
|
|
56
86
|
|
|
57
87
|
describe("astro-welcome extension", () => {
|
|
@@ -72,75 +102,71 @@ describe("astro-welcome extension", () => {
|
|
|
72
102
|
return { pi, mod };
|
|
73
103
|
}
|
|
74
104
|
|
|
75
|
-
it("registers session_start and before_agent_start
|
|
105
|
+
it("registers session_start and before_agent_start handlers", async () => {
|
|
76
106
|
const { pi } = await install();
|
|
77
107
|
expect(pi.handlers.session_start).toBeDefined();
|
|
78
108
|
expect(pi.handlers.before_agent_start).toBeDefined();
|
|
79
109
|
});
|
|
80
110
|
|
|
81
|
-
it("
|
|
111
|
+
it("session_start opens a centered overlay via ctx.ui.custom", async () => {
|
|
82
112
|
const { pi } = await install();
|
|
83
113
|
const ctx = makeCtx();
|
|
84
|
-
await pi.handlers.session_start({
|
|
85
|
-
expect(ctx.ui.
|
|
86
|
-
|
|
114
|
+
await pi.handlers.session_start({}, ctx);
|
|
115
|
+
expect(ctx.ui.custom).toHaveBeenCalledTimes(1);
|
|
116
|
+
const opts = ctx._factory!.options as { overlay: boolean; overlayOptions: { anchor: string } };
|
|
117
|
+
expect(opts.overlay).toBe(true);
|
|
118
|
+
expect(opts.overlayOptions.anchor).toBe("center");
|
|
87
119
|
});
|
|
88
120
|
|
|
89
121
|
it("does nothing when ctx.hasUI is false", async () => {
|
|
90
122
|
const { pi } = await install();
|
|
91
123
|
const ctx = makeCtx({ hasUI: false });
|
|
92
|
-
await pi.handlers.session_start({
|
|
93
|
-
expect(ctx.ui.
|
|
124
|
+
await pi.handlers.session_start({}, ctx);
|
|
125
|
+
expect(ctx.ui.custom).not.toHaveBeenCalled();
|
|
94
126
|
});
|
|
95
127
|
|
|
96
|
-
it("
|
|
128
|
+
it("auto-dismisses after 6 seconds via done(undefined)", async () => {
|
|
97
129
|
const { pi } = await install();
|
|
98
130
|
const ctx = makeCtx();
|
|
99
|
-
await pi.handlers.session_start({
|
|
131
|
+
await pi.handlers.session_start({}, ctx);
|
|
132
|
+
const settle = ctx._factory!.settle;
|
|
100
133
|
vi.advanceTimersByTime(6_000);
|
|
101
|
-
|
|
102
|
-
expect(calls.length).toBe(2);
|
|
103
|
-
expect(calls[1][0]).toBeUndefined();
|
|
134
|
+
await expect(settle).resolves.toBeUndefined();
|
|
104
135
|
});
|
|
105
136
|
|
|
106
|
-
it("
|
|
137
|
+
it("dismisses on first handleInput keystroke", async () => {
|
|
107
138
|
const { pi } = await install();
|
|
108
139
|
const ctx = makeCtx();
|
|
109
|
-
await pi.handlers.session_start({
|
|
110
|
-
const
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
expect(result).toBeUndefined();
|
|
114
|
-
expect(ctx.ui.setHeader).toHaveBeenLastCalledWith(undefined);
|
|
140
|
+
await pi.handlers.session_start({}, ctx);
|
|
141
|
+
const settle = ctx._factory!.settle;
|
|
142
|
+
ctx._factory!.component!.handleInput!("x");
|
|
143
|
+
await expect(settle).resolves.toBeUndefined();
|
|
115
144
|
});
|
|
116
145
|
|
|
117
|
-
it("
|
|
146
|
+
it("dismisses on before_agent_start (external dismiss)", async () => {
|
|
118
147
|
const { pi } = await install();
|
|
119
148
|
const ctx = makeCtx();
|
|
120
|
-
await pi.handlers.session_start({
|
|
149
|
+
await pi.handlers.session_start({}, ctx);
|
|
150
|
+
const settle = ctx._factory!.settle;
|
|
121
151
|
await pi.handlers.before_agent_start({ prompt: "hi" }, ctx);
|
|
122
|
-
expect(
|
|
152
|
+
await expect(settle).resolves.toBeUndefined();
|
|
123
153
|
});
|
|
124
154
|
|
|
125
|
-
it("dismiss is idempotent — second trigger does not
|
|
155
|
+
it("dismiss is idempotent — second trigger does not throw", async () => {
|
|
126
156
|
const { pi } = await install();
|
|
127
157
|
const ctx = makeCtx();
|
|
128
|
-
await pi.handlers.session_start({
|
|
129
|
-
|
|
130
|
-
|
|
158
|
+
await pi.handlers.session_start({}, ctx);
|
|
159
|
+
ctx._factory!.component!.handleInput!("a");
|
|
160
|
+
await ctx._factory!.settle;
|
|
161
|
+
expect(() => ctx._factory!.component!.handleInput!("b")).not.toThrow();
|
|
131
162
|
vi.advanceTimersByTime(6_000);
|
|
132
|
-
expect(ctx.ui.setHeader.mock.calls.length).toBe(before);
|
|
133
163
|
});
|
|
134
164
|
|
|
135
|
-
it("renders
|
|
165
|
+
it("renders all stats and the dismiss hint in the overlay component", async () => {
|
|
136
166
|
const { pi } = await install();
|
|
137
167
|
const ctx = makeCtx();
|
|
138
|
-
await pi.handlers.session_start({
|
|
139
|
-
const
|
|
140
|
-
tui: unknown,
|
|
141
|
-
theme: unknown,
|
|
142
|
-
) => { render: (w: number) => string[] };
|
|
143
|
-
const lines = factory({}, {}).render(120);
|
|
168
|
+
await pi.handlers.session_start({}, ctx);
|
|
169
|
+
const lines = ctx._factory!.component!.render(120);
|
|
144
170
|
const joined = lines.join("\n");
|
|
145
171
|
expect(joined).toContain("ASTRO PI");
|
|
146
172
|
expect(joined).toContain("anthropic/claude-sonnet-4-6");
|
|
@@ -158,11 +184,8 @@ describe("astro-welcome extension", () => {
|
|
|
158
184
|
],
|
|
159
185
|
},
|
|
160
186
|
});
|
|
161
|
-
await pi.handlers.session_start({
|
|
162
|
-
const
|
|
163
|
-
|
|
164
|
-
theme: unknown,
|
|
165
|
-
) => { render: (w: number) => string[] };
|
|
166
|
-
expect(factory({}, {}).render(120).join("\n")).toContain("caveman: ultra");
|
|
187
|
+
await pi.handlers.session_start({}, ctx);
|
|
188
|
+
const lines = ctx._factory!.component!.render(120);
|
|
189
|
+
expect(lines.join("\n")).toContain("caveman: ultra");
|
|
167
190
|
});
|
|
168
191
|
});
|
|
@@ -1,11 +1,15 @@
|
|
|
1
1
|
import type { ExtensionAPI, ExtensionContext } from "@mariozechner/pi-coding-agent";
|
|
2
2
|
import { composeWelcome, type WelcomeStats } from "./compose.ts";
|
|
3
|
+
import { ART_HEIGHT, ART_WIDTH } from "./art.ts";
|
|
3
4
|
|
|
4
5
|
const DISMISS_AFTER_MS = 6_000;
|
|
6
|
+
const OVERLAY_WIDTH = Math.max(ART_WIDTH + 30, 70);
|
|
7
|
+
const OVERLAY_HEIGHT = ART_HEIGHT + 7;
|
|
5
8
|
|
|
6
9
|
function readCavemanLevel(ctx: ExtensionContext): string | undefined {
|
|
7
|
-
|
|
8
|
-
|
|
10
|
+
const entries = ctx.sessionManager.getEntries();
|
|
11
|
+
for (let i = entries.length - 1; i >= 0; i--) {
|
|
12
|
+
const entry = entries[i];
|
|
9
13
|
if (entry.type === "custom" && entry.customType === "caveman-mode") {
|
|
10
14
|
const data = entry.data as { level?: string } | undefined;
|
|
11
15
|
return data?.level ?? undefined;
|
|
@@ -14,74 +18,74 @@ function readCavemanLevel(ctx: ExtensionContext): string | undefined {
|
|
|
14
18
|
return undefined;
|
|
15
19
|
}
|
|
16
20
|
|
|
17
|
-
function modelDisplayShort(modelId: string | undefined): string | undefined {
|
|
18
|
-
if (!modelId) return undefined;
|
|
19
|
-
const trimmed = modelId.trim();
|
|
20
|
-
if (trimmed.length === 0) return undefined;
|
|
21
|
-
return trimmed;
|
|
22
|
-
}
|
|
23
|
-
|
|
24
21
|
export default function astroWelcomeExtension(pi: ExtensionAPI): void {
|
|
25
|
-
let
|
|
26
|
-
let timer: ReturnType<typeof setTimeout> | null = null;
|
|
27
|
-
let unsubscribeInput: (() => void) | null = null;
|
|
28
|
-
let activeCtx: ExtensionContext | null = null;
|
|
29
|
-
|
|
30
|
-
function dismiss(): void {
|
|
31
|
-
if (dismissed) return;
|
|
32
|
-
dismissed = true;
|
|
33
|
-
if (timer !== null) {
|
|
34
|
-
clearTimeout(timer);
|
|
35
|
-
timer = null;
|
|
36
|
-
}
|
|
37
|
-
if (unsubscribeInput !== null) {
|
|
38
|
-
try {
|
|
39
|
-
unsubscribeInput();
|
|
40
|
-
} catch {
|
|
41
|
-
/* ignore unsubscribe errors */
|
|
42
|
-
}
|
|
43
|
-
unsubscribeInput = null;
|
|
44
|
-
}
|
|
45
|
-
try {
|
|
46
|
-
activeCtx?.ui.setHeader(undefined);
|
|
47
|
-
} catch {
|
|
48
|
-
/* ignore header clear errors */
|
|
49
|
-
}
|
|
50
|
-
activeCtx = null;
|
|
51
|
-
}
|
|
22
|
+
let externalDismiss: (() => void) | null = null;
|
|
52
23
|
|
|
53
24
|
pi.on("session_start", async (_event, ctx) => {
|
|
54
25
|
if (!ctx.hasUI) return;
|
|
55
|
-
dismissed = false;
|
|
56
|
-
activeCtx = ctx;
|
|
57
|
-
const seed = Date.now() & 0xffff_ffff;
|
|
58
26
|
|
|
27
|
+
const seed = Date.now() & 0xffff_ffff;
|
|
59
28
|
const stats: WelcomeStats = {
|
|
60
|
-
modelDisplay:
|
|
29
|
+
modelDisplay: ctx.model?.id,
|
|
61
30
|
contextWindow: ctx.model?.contextWindow,
|
|
62
31
|
extensionCount: pi.getAllTools().length,
|
|
63
32
|
cavemanLevel: readCavemanLevel(ctx),
|
|
64
33
|
};
|
|
65
34
|
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
render(width: number): string[] {
|
|
69
|
-
return composeWelcome(width, seed, stats);
|
|
70
|
-
},
|
|
71
|
-
}));
|
|
35
|
+
let dismissed = false;
|
|
36
|
+
let timer: ReturnType<typeof setTimeout> | null = null;
|
|
72
37
|
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
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
|
+
};
|
|
51
|
+
|
|
52
|
+
timer = setTimeout(dismiss, DISMISS_AFTER_MS);
|
|
53
|
+
externalDismiss = dismiss;
|
|
54
|
+
|
|
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
|
+
if (timer !== null) {
|
|
67
|
+
clearTimeout(timer);
|
|
68
|
+
timer = null;
|
|
69
|
+
}
|
|
70
|
+
externalDismiss = null;
|
|
71
|
+
},
|
|
72
|
+
};
|
|
73
|
+
},
|
|
74
|
+
{
|
|
75
|
+
overlay: true,
|
|
76
|
+
overlayOptions: {
|
|
77
|
+
anchor: "center",
|
|
78
|
+
width: OVERLAY_WIDTH,
|
|
79
|
+
maxHeight: OVERLAY_HEIGHT,
|
|
80
|
+
},
|
|
81
|
+
},
|
|
82
|
+
)
|
|
83
|
+
.catch(() => {
|
|
84
|
+
/* overlay close errors are not fatal */
|
|
78
85
|
});
|
|
79
|
-
} catch {
|
|
80
|
-
/* terminal input listener not supported in this mode */
|
|
81
|
-
}
|
|
82
86
|
});
|
|
83
87
|
|
|
84
88
|
pi.on("before_agent_start", async () => {
|
|
85
|
-
|
|
89
|
+
externalDismiss?.();
|
|
86
90
|
});
|
|
87
91
|
}
|