@astrofoundry/pi-astro 0.13.0 → 0.13.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.
|
@@ -43,6 +43,7 @@ interface FakeCtx {
|
|
|
43
43
|
ui: {
|
|
44
44
|
notify: ReturnType<typeof vi.fn>;
|
|
45
45
|
setFooter: ReturnType<typeof vi.fn>;
|
|
46
|
+
setWidget: ReturnType<typeof vi.fn>;
|
|
46
47
|
};
|
|
47
48
|
cwd: string;
|
|
48
49
|
model: { id: string } | undefined;
|
|
@@ -56,6 +57,7 @@ function makeCtx(overrides: Partial<FakeCtx> = {}, oauth = false): FakeCtx {
|
|
|
56
57
|
ui: {
|
|
57
58
|
notify: vi.fn(),
|
|
58
59
|
setFooter: vi.fn(),
|
|
60
|
+
setWidget: vi.fn(),
|
|
59
61
|
},
|
|
60
62
|
cwd: "/Users/astro/proj",
|
|
61
63
|
model: { id: "anthropic/claude-sonnet-4-6" },
|
|
@@ -123,6 +125,55 @@ describe("astro-footer extension", () => {
|
|
|
123
125
|
expect(pi.commands.has("footer")).toBe(true);
|
|
124
126
|
expect(pi.handlers.session_start).toBeDefined();
|
|
125
127
|
expect(pi.handlers.tool_result).toBeDefined();
|
|
128
|
+
expect(pi.handlers.before_agent_start).toBeDefined();
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
it("registers a last-prompt widget below the editor on activate", async () => {
|
|
132
|
+
const { pi } = await install();
|
|
133
|
+
const ctx = makeCtx();
|
|
134
|
+
await pi.handlers.session_start({}, ctx);
|
|
135
|
+
const calls = ctx.ui.setWidget.mock.calls;
|
|
136
|
+
const lastPromptCall = calls.find((c) => c[0] === "astro-footer-last-prompt");
|
|
137
|
+
expect(lastPromptCall).toBeDefined();
|
|
138
|
+
expect(lastPromptCall![2]).toEqual({ placement: "belowEditor" });
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
it("last-prompt widget renders nothing before the first prompt", async () => {
|
|
142
|
+
const { pi } = await install();
|
|
143
|
+
const ctx = makeCtx();
|
|
144
|
+
await pi.handlers.session_start({}, ctx);
|
|
145
|
+
const widgetCall = ctx.ui.setWidget.mock.calls.find((c) => c[0] === "astro-footer-last-prompt");
|
|
146
|
+
const factory = widgetCall![1] as (
|
|
147
|
+
tui: unknown,
|
|
148
|
+
theme: unknown,
|
|
149
|
+
) => { render: (w: number) => string[] };
|
|
150
|
+
const lines = factory({}, fakeTheme).render(120);
|
|
151
|
+
expect(lines).toEqual([]);
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
it("last-prompt widget renders the last submitted prompt with arrow prefix", async () => {
|
|
155
|
+
const { pi } = await install();
|
|
156
|
+
const ctx = makeCtx();
|
|
157
|
+
await pi.handlers.session_start({}, ctx);
|
|
158
|
+
await pi.handlers.before_agent_start({ prompt: "fix the auth middleware token expiry check" }, ctx);
|
|
159
|
+
const widgetCall = ctx.ui.setWidget.mock.calls.find((c) => c[0] === "astro-footer-last-prompt");
|
|
160
|
+
const factory = widgetCall![1] as (
|
|
161
|
+
tui: unknown,
|
|
162
|
+
theme: unknown,
|
|
163
|
+
) => { render: (w: number) => string[] };
|
|
164
|
+
const lines = factory({}, fakeTheme).render(120);
|
|
165
|
+
expect(lines).toHaveLength(1);
|
|
166
|
+
expect(lines[0]).toContain("↳");
|
|
167
|
+
expect(lines[0]).toContain("fix the auth middleware");
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
it("/footer off clears the last-prompt widget too", async () => {
|
|
171
|
+
const { pi } = await install();
|
|
172
|
+
const ctx = makeCtx();
|
|
173
|
+
await pi.commands.get("footer")!.handler("off", ctx);
|
|
174
|
+
const widgetCalls = ctx.ui.setWidget.mock.calls;
|
|
175
|
+
const cleared = widgetCalls.find((c) => c[0] === "astro-footer-last-prompt" && c[1] === undefined);
|
|
176
|
+
expect(cleared).toBeDefined();
|
|
126
177
|
});
|
|
127
178
|
|
|
128
179
|
it("session_start activates the footer via ctx.ui.setFooter", async () => {
|
|
@@ -4,6 +4,7 @@ import type {
|
|
|
4
4
|
ReadonlyFooterDataProvider,
|
|
5
5
|
} from "@mariozechner/pi-coding-agent";
|
|
6
6
|
import type { AssistantMessage } from "@mariozechner/pi-ai";
|
|
7
|
+
import type { Theme } from "@mariozechner/pi-coding-agent";
|
|
7
8
|
import { truncateToWidth, visibleWidth } from "@mariozechner/pi-tui";
|
|
8
9
|
import { resolvePathMode, type PathMode } from "./format.ts";
|
|
9
10
|
import { GitStatusTracker } from "./git-status.ts";
|
|
@@ -27,6 +28,17 @@ import {
|
|
|
27
28
|
} from "./segments.ts";
|
|
28
29
|
|
|
29
30
|
const SUBAGENT_STATUS_KEY = "subagents";
|
|
31
|
+
const LAST_PROMPT_WIDGET_KEY = "astro-footer-last-prompt";
|
|
32
|
+
|
|
33
|
+
function renderLastPromptLine(theme: Theme, lastPrompt: string, width: number): string[] {
|
|
34
|
+
const prefix = ` ${theme.fg("dim", "↳")} `;
|
|
35
|
+
const available = width - visibleWidth(prefix);
|
|
36
|
+
if (available < 10) return [];
|
|
37
|
+
const collapsed = lastPrompt.replace(/\s+/g, " ").trim();
|
|
38
|
+
if (collapsed.length === 0) return [];
|
|
39
|
+
const truncated = truncateToWidth(collapsed, available, "…");
|
|
40
|
+
return [truncateToWidth(`${prefix}${theme.fg("dim", truncated)}`, width, "…")];
|
|
41
|
+
}
|
|
30
42
|
|
|
31
43
|
interface UsageTotals {
|
|
32
44
|
input: number;
|
|
@@ -162,6 +174,7 @@ export default function astroFooterExtension(pi: ExtensionAPI): void {
|
|
|
162
174
|
let autoCompact = true;
|
|
163
175
|
let activeRequestRender: (() => void) | null = null;
|
|
164
176
|
let timeTickHandle: ReturnType<typeof setInterval> | null = null;
|
|
177
|
+
let lastPrompt = "";
|
|
165
178
|
|
|
166
179
|
const gitTracker = new GitStatusTracker(() => {
|
|
167
180
|
activeRequestRender?.();
|
|
@@ -184,6 +197,17 @@ export default function astroFooterExtension(pi: ExtensionAPI): void {
|
|
|
184
197
|
sessionStartedAt = Date.now();
|
|
185
198
|
autoCompact = isAutoCompactEnabled(ctx.cwd);
|
|
186
199
|
gitTracker.refresh(ctx.cwd, true);
|
|
200
|
+
ctx.ui.setWidget(
|
|
201
|
+
LAST_PROMPT_WIDGET_KEY,
|
|
202
|
+
(_tui, theme) => ({
|
|
203
|
+
invalidate() {},
|
|
204
|
+
render(width: number): string[] {
|
|
205
|
+
if (!lastPrompt) return [];
|
|
206
|
+
return renderLastPromptLine(theme, lastPrompt, width);
|
|
207
|
+
},
|
|
208
|
+
}),
|
|
209
|
+
{ placement: "belowEditor" },
|
|
210
|
+
);
|
|
187
211
|
ctx.ui.setFooter((tui, theme, footerData) => {
|
|
188
212
|
activeRequestRender = () => tui.requestRender();
|
|
189
213
|
startTimeTicker();
|
|
@@ -215,9 +239,15 @@ export default function astroFooterExtension(pi: ExtensionAPI): void {
|
|
|
215
239
|
}
|
|
216
240
|
|
|
217
241
|
pi.on("session_start", async (_event, ctx) => {
|
|
242
|
+
lastPrompt = "";
|
|
218
243
|
if (enabled) activate(ctx);
|
|
219
244
|
});
|
|
220
245
|
|
|
246
|
+
pi.on("before_agent_start", async (event) => {
|
|
247
|
+
lastPrompt = event.prompt ?? "";
|
|
248
|
+
activeRequestRender?.();
|
|
249
|
+
});
|
|
250
|
+
|
|
221
251
|
pi.on("tool_result", async (event, ctx) => {
|
|
222
252
|
if (!enabled) return;
|
|
223
253
|
if (event.toolName === "edit" || event.toolName === "write" || event.toolName === "bash") {
|
|
@@ -256,6 +286,7 @@ export default function astroFooterExtension(pi: ExtensionAPI): void {
|
|
|
256
286
|
enabled = false;
|
|
257
287
|
stopTimeTicker();
|
|
258
288
|
ctx.ui.setFooter(undefined);
|
|
289
|
+
ctx.ui.setWidget(LAST_PROMPT_WIDGET_KEY, undefined);
|
|
259
290
|
ctx.ui.notify("astro-footer: off (default footer restored)", "info");
|
|
260
291
|
return;
|
|
261
292
|
}
|
|
@@ -238,6 +238,18 @@ describe("caveman extension", () => {
|
|
|
238
238
|
expect(result.systemPrompt).not.toContain("**wenyan-full**");
|
|
239
239
|
});
|
|
240
240
|
|
|
241
|
+
it("before_agent_start tail-appends a per-turn reminder naming opinions/debates", async () => {
|
|
242
|
+
const { pi } = await install();
|
|
243
|
+
const ctx = makeCtx();
|
|
244
|
+
await pi.commands.get("caveman")!.handler("ultra", ctx);
|
|
245
|
+
const result = (await pi.handlers.before_agent_start(
|
|
246
|
+
{ systemPrompt: "ORIG" },
|
|
247
|
+
makeCtx(),
|
|
248
|
+
)) as { systemPrompt: string };
|
|
249
|
+
expect(result.systemPrompt).toMatch(/REMINDER: caveman ultra applies to THIS response/);
|
|
250
|
+
expect(result.systemPrompt).toMatch(/opinions, debates, philosophy/);
|
|
251
|
+
});
|
|
252
|
+
|
|
241
253
|
it("before_agent_start keeps only the active level's worked-example bullets", async () => {
|
|
242
254
|
const { pi } = await install();
|
|
243
255
|
const ctx = makeCtx();
|
|
@@ -66,7 +66,12 @@ function filterForLevel(body: string, level: Level): string {
|
|
|
66
66
|
}
|
|
67
67
|
|
|
68
68
|
function buildInjection(level: Level, body: string): string {
|
|
69
|
-
|
|
69
|
+
const filtered = filterForLevel(body, level);
|
|
70
|
+
const reminder =
|
|
71
|
+
`REMINDER: caveman ${level} applies to THIS response, every topic — ` +
|
|
72
|
+
`opinions, debates, philosophy, jokes, casual chat, technical — all the same. ` +
|
|
73
|
+
`No exceptions beyond security warnings and destructive-action confirmations.`;
|
|
74
|
+
return `CAVEMAN MODE ACTIVE — level: ${level}\n\n${filtered}\n\n${reminder}`;
|
|
70
75
|
}
|
|
71
76
|
|
|
72
77
|
function statusText(level: Level): string {
|
package/package.json
CHANGED
package/skills/caveman/SKILL.md
CHANGED
|
@@ -1,115 +1,70 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: caveman
|
|
3
3
|
description: >
|
|
4
|
-
Persistent compressed-output mode.
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
4
|
+
Persistent compressed-output mode. Cuts assistant token usage ~70-75% by
|
|
5
|
+
stripping fillers, hedging, and pleasantries from prose. Applies to every
|
|
6
|
+
reply on every topic — code, debugging, opinions, debates, philosophy, jokes,
|
|
7
|
+
casual chat, explanations. Six intensity levels: lite, full (default), ultra,
|
|
8
|
+
wenyan-lite, wenyan-full, wenyan-ultra. Activate with /caveman or
|
|
9
|
+
/skill:caveman; deactivate with /caveman off, "stop caveman", or "normal mode".
|
|
9
10
|
---
|
|
10
11
|
|
|
11
12
|
# Caveman Mode
|
|
12
13
|
|
|
13
|
-
|
|
14
|
-
verbatim — only redundancy and social padding are removed. Code, error
|
|
15
|
-
strings, command lines, paths, identifiers, and version numbers are reproduced
|
|
16
|
-
unchanged.
|
|
14
|
+
Reply terse like smart caveman. Fluff die. Substance live.
|
|
17
15
|
|
|
18
16
|
## Persistence
|
|
19
17
|
|
|
20
|
-
|
|
21
|
-
drift back to verbose prose mid-conversation, even after long context, after
|
|
22
|
-
context compaction, or after style instructions injected by other extensions.
|
|
23
|
-
Resume caveman immediately after any unavoidable break (security warning,
|
|
24
|
-
destructive-action confirmation, multi-step procedure).
|
|
18
|
+
ACTIVE EVERY RESPONSE. Every reply, every topic. No revert mid-conversation. No filler drift after long context or compaction. Still active if unsure. Off only on: "stop caveman", "normal mode", or `/caveman off`.
|
|
25
19
|
|
|
26
|
-
|
|
27
|
-
Switch off only on explicit user request: "stop caveman", "normal mode", or
|
|
28
|
-
`/caveman off`.
|
|
20
|
+
Topic does not change the rule. Caveman applies to opinions, debates, philosophy, jokes, casual chat, explanations, and technical answers — all the same.
|
|
29
21
|
|
|
30
|
-
##
|
|
22
|
+
## Rules
|
|
31
23
|
|
|
32
|
-
Drop:
|
|
24
|
+
Drop: articles (a/an/the), filler (just/really/basically/actually/simply/very), pleasantries (sure/certainly/of course/happy to/let me/I'd be glad to), hedging (it seems/perhaps/you might want to/I think), restating the question. Fragments OK. Short synonyms (fix not "implement a solution for", big not extensive). Code blocks unchanged. Errors quoted exact. Cause precedes effect.
|
|
33
25
|
|
|
34
|
-
|
|
35
|
-
- Filler adverbs and intensifiers (just, really, basically, actually, simply, very).
|
|
36
|
-
- Pleasantries (sure, certainly, of course, happy to, let me, I'd be glad to).
|
|
37
|
-
- Hedging phrases (it seems, perhaps, you might want to, I think).
|
|
38
|
-
- Restating the question before answering it.
|
|
26
|
+
Sentence pattern: `[thing] [action] [reason]. [next step].`
|
|
39
27
|
|
|
40
|
-
|
|
28
|
+
Anti: "Sure! I'd be happy to help with that. The issue you're seeing is most likely caused by..."
|
|
29
|
+
Yes: "Bug in auth middleware. Token expiry uses `<` not `<=`. Fix:"
|
|
41
30
|
|
|
42
|
-
|
|
43
|
-
- Code blocks verbatim. Error messages quoted exactly.
|
|
44
|
-
- Cause-and-effect order — never reorder steps for terseness.
|
|
45
|
-
- Punctuation needed for parsing (commas separating list items, periods between independent claims).
|
|
46
|
-
|
|
47
|
-
Sentence pattern: `<subject> <action> <reason>. <next step>.`
|
|
48
|
-
|
|
49
|
-
Anti-example: "Sure! I'd be happy to help you with that. The issue you're seeing is most likely caused by..."
|
|
50
|
-
Example: "Bug in auth middleware. Token expiry uses `<` not `<=`. Fix:"
|
|
51
|
-
|
|
52
|
-
## Intensity levels
|
|
31
|
+
## Intensity
|
|
53
32
|
|
|
54
33
|
| Level | Behaviour |
|
|
55
34
|
|---|---|
|
|
56
|
-
| **lite** | Drop
|
|
57
|
-
| **full** | Drop articles.
|
|
58
|
-
| **ultra** | Abbreviate (DB, auth, cfg, req, res, fn, impl).
|
|
59
|
-
| **wenyan-lite** | Classical Chinese register, modern grammar.
|
|
60
|
-
| **wenyan-full** | Full 文言文 — verb precedes object,
|
|
35
|
+
| **lite** | Drop filler + hedging only. Articles + full sentences kept. |
|
|
36
|
+
| **full** | Drop articles. Fragments OK. Short synonyms. Default. |
|
|
37
|
+
| **ultra** | Abbreviate (DB, auth, cfg, req, res, fn, impl). Arrows for causality (X → Y). One word when one word enough. |
|
|
38
|
+
| **wenyan-lite** | Classical Chinese register, modern grammar. |
|
|
39
|
+
| **wenyan-full** | Full 文言文 — verb precedes object, subject often omitted, classical particles (之/乃/為/其). |
|
|
61
40
|
| **wenyan-ultra** | Maximum classical compression. One classical phrase per thought. |
|
|
62
41
|
|
|
63
|
-
###
|
|
42
|
+
### Example — "Why does this React component re-render?"
|
|
64
43
|
|
|
65
|
-
- lite: "Your component re-renders because every render creates a new object reference
|
|
44
|
+
- lite: "Your component re-renders because every render creates a new object reference. Wrap the prop in `useMemo`."
|
|
66
45
|
- full: "New object ref each render. Inline obj prop = new ref = re-render. Wrap in `useMemo`."
|
|
67
46
|
- ultra: "Inline obj prop → new ref → re-render. `useMemo`."
|
|
68
47
|
- wenyan-lite: "組件每次重繪皆生新對象參照。以 `useMemo` 包之。"
|
|
69
48
|
- wenyan-full: "每繪生新參照,故重繪。以 `useMemo` 包之。"
|
|
70
49
|
- wenyan-ultra: "新參照→重繪。`useMemo` 包。"
|
|
71
50
|
|
|
72
|
-
###
|
|
51
|
+
### Example — "Explain database connection pooling."
|
|
73
52
|
|
|
74
|
-
- lite: "Connection pooling reuses already-open connections instead of opening
|
|
53
|
+
- lite: "Connection pooling reuses already-open connections instead of opening one per request, avoiding handshake overhead."
|
|
75
54
|
- full: "Pool reuse open connections. No new handshake per request."
|
|
76
55
|
- ultra: "Pool = reuse conn. Skip handshake → fast under load."
|
|
77
56
|
- wenyan-lite: "連接池重用已開連接,免逐請求重握之耗。"
|
|
78
57
|
- wenyan-full: "池存連接而重用,無逐請求重握之耗。"
|
|
79
58
|
- wenyan-ultra: "池連,免握。"
|
|
80
59
|
|
|
81
|
-
## Auto-
|
|
82
|
-
|
|
83
|
-
Drop caveman compression when ambiguity would harm correctness:
|
|
60
|
+
## Auto-Clarity
|
|
84
61
|
|
|
85
|
-
|
|
86
|
-
- Confirmations for destructive or irreversible operations.
|
|
87
|
-
- Multi-step procedures whose order matters and where fragments would invite misreading.
|
|
88
|
-
- The user repeats a question or asks for clarification.
|
|
62
|
+
Drop caveman ONLY for: security warnings, and confirmations of destructive or irreversible operations. Resume caveman immediately after. Nothing else qualifies — long explanations, multi-point answers, opinions, philosophical debates all stay caveman.
|
|
89
63
|
|
|
90
|
-
|
|
91
|
-
after the clarified step is complete.
|
|
64
|
+
## Boundaries
|
|
92
65
|
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
> **Warning:** the next command permanently deletes every row in `users` and
|
|
96
|
-
> cannot be undone:
|
|
97
|
-
>
|
|
98
|
-
> ```sql
|
|
99
|
-
> DROP TABLE users;
|
|
100
|
-
> ```
|
|
101
|
-
>
|
|
102
|
-
> Verify a current backup exists before running. Caveman resume.
|
|
103
|
-
|
|
104
|
-
## Boundaries (caveman never applies)
|
|
105
|
-
|
|
106
|
-
- Source code, commit messages, PR descriptions, code review comments — write in normal prose.
|
|
107
|
-
- Documentation files (`.md`, `.mdx`, `.rst`) — caveman is for chat, not artifacts.
|
|
108
|
-
- Quoted log lines, stack traces, error messages — reproduce exactly.
|
|
66
|
+
Code, commit messages, PR descriptions: write normal. Quoted log lines, stack traces, error messages: reproduce exactly.
|
|
109
67
|
|
|
110
68
|
## Deactivation
|
|
111
69
|
|
|
112
|
-
|
|
113
|
-
- "stop caveman" / "normal mode" / "disable caveman" — natural-language requests.
|
|
114
|
-
|
|
115
|
-
After deactivation, return to normal prose immediately on the next response.
|
|
70
|
+
`/caveman off`, "stop caveman", or "normal mode" → resume normal prose on the next reply.
|