@astrofoundry/pi-astro 0.14.0 → 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,82 +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("
|
|
82
|
-
const { pi } = await install();
|
|
83
|
-
const ctx = makeCtx();
|
|
84
|
-
await pi.handlers.session_start({ reason: "startup" }, ctx);
|
|
85
|
-
expect(ctx.ui.setHeader).toHaveBeenCalledTimes(1);
|
|
86
|
-
expect(ctx.ui.onTerminalInput).toHaveBeenCalledTimes(1);
|
|
87
|
-
});
|
|
88
|
-
|
|
89
|
-
it("does nothing when reason is not 'startup' (e.g. reload, resume)", async () => {
|
|
111
|
+
it("session_start opens a centered overlay via ctx.ui.custom", async () => {
|
|
90
112
|
const { pi } = await install();
|
|
91
113
|
const ctx = makeCtx();
|
|
92
|
-
await pi.handlers.session_start({
|
|
93
|
-
expect(ctx.ui.
|
|
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");
|
|
94
119
|
});
|
|
95
120
|
|
|
96
121
|
it("does nothing when ctx.hasUI is false", async () => {
|
|
97
122
|
const { pi } = await install();
|
|
98
123
|
const ctx = makeCtx({ hasUI: false });
|
|
99
|
-
await pi.handlers.session_start({
|
|
100
|
-
expect(ctx.ui.
|
|
124
|
+
await pi.handlers.session_start({}, ctx);
|
|
125
|
+
expect(ctx.ui.custom).not.toHaveBeenCalled();
|
|
101
126
|
});
|
|
102
127
|
|
|
103
|
-
it("
|
|
128
|
+
it("auto-dismisses after 6 seconds via done(undefined)", async () => {
|
|
104
129
|
const { pi } = await install();
|
|
105
130
|
const ctx = makeCtx();
|
|
106
|
-
await pi.handlers.session_start({
|
|
131
|
+
await pi.handlers.session_start({}, ctx);
|
|
132
|
+
const settle = ctx._factory!.settle;
|
|
107
133
|
vi.advanceTimersByTime(6_000);
|
|
108
|
-
|
|
109
|
-
expect(calls.length).toBe(2);
|
|
110
|
-
expect(calls[1][0]).toBeUndefined();
|
|
134
|
+
await expect(settle).resolves.toBeUndefined();
|
|
111
135
|
});
|
|
112
136
|
|
|
113
|
-
it("
|
|
137
|
+
it("dismisses on first handleInput keystroke", async () => {
|
|
114
138
|
const { pi } = await install();
|
|
115
139
|
const ctx = makeCtx();
|
|
116
|
-
await pi.handlers.session_start({
|
|
117
|
-
const
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
expect(result).toBeUndefined();
|
|
121
|
-
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();
|
|
122
144
|
});
|
|
123
145
|
|
|
124
|
-
it("
|
|
146
|
+
it("dismisses on before_agent_start (external dismiss)", async () => {
|
|
125
147
|
const { pi } = await install();
|
|
126
148
|
const ctx = makeCtx();
|
|
127
|
-
await pi.handlers.session_start({
|
|
149
|
+
await pi.handlers.session_start({}, ctx);
|
|
150
|
+
const settle = ctx._factory!.settle;
|
|
128
151
|
await pi.handlers.before_agent_start({ prompt: "hi" }, ctx);
|
|
129
|
-
expect(
|
|
152
|
+
await expect(settle).resolves.toBeUndefined();
|
|
130
153
|
});
|
|
131
154
|
|
|
132
|
-
it("dismiss is idempotent — second trigger does not
|
|
155
|
+
it("dismiss is idempotent — second trigger does not throw", async () => {
|
|
133
156
|
const { pi } = await install();
|
|
134
157
|
const ctx = makeCtx();
|
|
135
|
-
await pi.handlers.session_start({
|
|
136
|
-
|
|
137
|
-
|
|
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();
|
|
138
162
|
vi.advanceTimersByTime(6_000);
|
|
139
|
-
expect(ctx.ui.setHeader.mock.calls.length).toBe(before);
|
|
140
163
|
});
|
|
141
164
|
|
|
142
|
-
it("renders
|
|
165
|
+
it("renders all stats and the dismiss hint in the overlay component", async () => {
|
|
143
166
|
const { pi } = await install();
|
|
144
167
|
const ctx = makeCtx();
|
|
145
|
-
await pi.handlers.session_start({
|
|
146
|
-
const
|
|
147
|
-
tui: unknown,
|
|
148
|
-
theme: unknown,
|
|
149
|
-
) => { render: (w: number) => string[] };
|
|
150
|
-
const lines = factory({}, {}).render(120);
|
|
168
|
+
await pi.handlers.session_start({}, ctx);
|
|
169
|
+
const lines = ctx._factory!.component!.render(120);
|
|
151
170
|
const joined = lines.join("\n");
|
|
152
171
|
expect(joined).toContain("ASTRO PI");
|
|
153
172
|
expect(joined).toContain("anthropic/claude-sonnet-4-6");
|
|
@@ -165,11 +184,8 @@ describe("astro-welcome extension", () => {
|
|
|
165
184
|
],
|
|
166
185
|
},
|
|
167
186
|
});
|
|
168
|
-
await pi.handlers.session_start({
|
|
169
|
-
const
|
|
170
|
-
|
|
171
|
-
theme: unknown,
|
|
172
|
-
) => { render: (w: number) => string[] };
|
|
173
|
-
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");
|
|
174
190
|
});
|
|
175
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,75 +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
|
-
pi.on("session_start", async (
|
|
54
|
-
if (event.reason !== "startup") return;
|
|
24
|
+
pi.on("session_start", async (_event, ctx) => {
|
|
55
25
|
if (!ctx.hasUI) return;
|
|
56
|
-
dismissed = false;
|
|
57
|
-
activeCtx = ctx;
|
|
58
|
-
const seed = Date.now() & 0xffff_ffff;
|
|
59
26
|
|
|
27
|
+
const seed = Date.now() & 0xffff_ffff;
|
|
60
28
|
const stats: WelcomeStats = {
|
|
61
|
-
modelDisplay:
|
|
29
|
+
modelDisplay: ctx.model?.id,
|
|
62
30
|
contextWindow: ctx.model?.contextWindow,
|
|
63
31
|
extensionCount: pi.getAllTools().length,
|
|
64
32
|
cavemanLevel: readCavemanLevel(ctx),
|
|
65
33
|
};
|
|
66
34
|
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
render(width: number): string[] {
|
|
70
|
-
return composeWelcome(width, seed, stats);
|
|
71
|
-
},
|
|
72
|
-
}));
|
|
35
|
+
let dismissed = false;
|
|
36
|
+
let timer: ReturnType<typeof setTimeout> | null = null;
|
|
73
37
|
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
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 */
|
|
79
85
|
});
|
|
80
|
-
} catch {
|
|
81
|
-
/* terminal input listener not supported in this mode */
|
|
82
|
-
}
|
|
83
86
|
});
|
|
84
87
|
|
|
85
88
|
pi.on("before_agent_start", async () => {
|
|
86
|
-
|
|
89
|
+
externalDismiss?.();
|
|
87
90
|
});
|
|
88
91
|
}
|