@gonrocca/zero-pi 0.1.67 → 0.1.69
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/README.md +64 -0
- package/extensions/working-phrases.ts +8 -0
- package/extensions/zero-activity-panel.ts +266 -0
- package/package.json +3 -1
package/README.md
CHANGED
|
@@ -116,6 +116,7 @@ into `/forge` for you.
|
|
|
116
116
|
| **ZERO HUD** | OMP-inspired segmented footer for SDD work: phase, model, tokens, cache, cost, diff, context %, and branch. Switch presets with `/zero-hud compact\|minimal\|full\|ascii\|off` or `ZERO_HUD_PRESET`. |
|
|
117
117
|
| **Pretty input box** | OMP-style prompt chrome with rounded corners, side borders, ZERO chips, and compact keyboard hints. |
|
|
118
118
|
| **Pretty code fences** | Markdown code blocks render as bordered panels with language badges instead of raw ```txt / ```json fence lines. |
|
|
119
|
+
| **Live activity panel** | Shows `/forge` phase progress and recent tool cards above the prompt while work is running. |
|
|
119
120
|
| **Working-phrase ticker** | Swaps pi's static `Working...` / `Procesando...` for a large pool of context-aware Spanish/ZERO/SDD phrases + spinner. |
|
|
120
121
|
| **Conversation resume** | Writes `.pi/zero-resume.md` on exit — the restore command + a conversation tail. |
|
|
121
122
|
| **Windows tree-kill** | Aborting a turn kills the whole process tree — no orphaned `claude`. |
|
|
@@ -144,6 +145,69 @@ into `/forge` for you.
|
|
|
144
145
|
| `/zero-diff <slug>` | Preview the logical spec-store merge without writing files. |
|
|
145
146
|
| `/zero-resume` | Write the session handoff note now. |
|
|
146
147
|
|
|
148
|
+
## 🎛️ Visual chrome: HUD, activity panel, prompt box
|
|
149
|
+
|
|
150
|
+
zero-pi ships several visual extensions. They are related, but each one owns a different part of pi's TUI:
|
|
151
|
+
|
|
152
|
+
| Surface | File | Where it appears | Purpose |
|
|
153
|
+
| ------- | ---- | ---------------- | ------- |
|
|
154
|
+
| **ZERO HUD** | `extensions/zero-hud.ts` | Footer/status line at the bottom | Persistent model/tokens/cost/diff/context/branch telemetry. |
|
|
155
|
+
| **Activity panel** | `extensions/zero-activity-panel.ts` | Widget above the prompt while work is running | Live `/forge` phase progress plus recent tool cards. |
|
|
156
|
+
| **Prompt box** | `extensions/zero-pretty-input-box.ts` | The input/editor box | OMP-like rounded box with ZERO chips and key hints. |
|
|
157
|
+
| **Code fences** | `extensions/zero-pretty-code-fences.ts` | Markdown code blocks in chat | Bordered panels with language badges instead of raw ```txt / ```json lines. |
|
|
158
|
+
| **Working phrases** | `extensions/working-phrases.ts` | Tiny loading row (`Procesando… (esc)`) | Rotating ZERO/SDD/Star Wars/DBZ phrases and themed spinner. |
|
|
159
|
+
|
|
160
|
+
### ZERO HUD presets
|
|
161
|
+
|
|
162
|
+
The HUD is the **bottom footer**. It is not the activity panel. Control it with:
|
|
163
|
+
|
|
164
|
+
```txt
|
|
165
|
+
/zero-hud preview
|
|
166
|
+
/zero-hud compact
|
|
167
|
+
/zero-hud minimal
|
|
168
|
+
/zero-hud full
|
|
169
|
+
/zero-hud ascii
|
|
170
|
+
/zero-hud off
|
|
171
|
+
/zero-hud on
|
|
172
|
+
```
|
|
173
|
+
|
|
174
|
+
Example compact HUD:
|
|
175
|
+
|
|
176
|
+
```txt
|
|
177
|
+
ZERO ▸ gpt-5.5 ▸ tok ↑1.1M ↓42K ▸ cost $16.22 ▸ diff +0/-0 ▸ ctx 11%
|
|
178
|
+
```
|
|
179
|
+
|
|
180
|
+
Startup preset can be set with:
|
|
181
|
+
|
|
182
|
+
```bash
|
|
183
|
+
ZERO_HUD_PRESET=full pi
|
|
184
|
+
```
|
|
185
|
+
|
|
186
|
+
Supported presets: `compact` (default), `minimal`, `full`, `ascii`, `off`.
|
|
187
|
+
|
|
188
|
+
### Activity panel vs HUD
|
|
189
|
+
|
|
190
|
+
During `/forge`, the activity panel appears **above the prompt** and summarizes the live run:
|
|
191
|
+
|
|
192
|
+
```txt
|
|
193
|
+
╭─ ZERO activity ───────────────────────────────────────────────────────╮
|
|
194
|
+
│ SDD ✓ clarify › ✓ explore › ⠋ plan › · analyze › · build › · veredicto
|
|
195
|
+
│ Tools ⠋ subagent · ✓ read zero-hud.ts · ✓ bash npm test
|
|
196
|
+
╰────────────────────────────────────────────────────────────────────────╯
|
|
197
|
+
```
|
|
198
|
+
|
|
199
|
+
The activity panel is transient: it clears after the agent finishes. The HUD stays in the footer across normal work.
|
|
200
|
+
|
|
201
|
+
### Applying visual changes after install
|
|
202
|
+
|
|
203
|
+
After `pi install npm:@gonrocca/zero-pi@<version>`, reload the active session:
|
|
204
|
+
|
|
205
|
+
```txt
|
|
206
|
+
/reload
|
|
207
|
+
```
|
|
208
|
+
|
|
209
|
+
If a visual monkeypatch was already loaded in the current process, restarting pi is the safest way to force all visual extensions to reinitialize.
|
|
210
|
+
|
|
147
211
|
### Git / PR / Issues
|
|
148
212
|
|
|
149
213
|
Recommended flow: `/zero-branch <slug>` → `/zero-issue <slug>` → `/forge <slug>` (the orchestrator validates plan artifacts and can create `/zero-checkpoint` before build) → `/zero-git-validate <slug> --for=pr` → `/zero-pr <slug>` → `/zero-archive <slug>`.
|
|
@@ -325,6 +325,13 @@ export default function register(pi?: PiAPI): void {
|
|
|
325
325
|
installSpinner();
|
|
326
326
|
});
|
|
327
327
|
|
|
328
|
+
// Pi can reset extension-owned UI during reload/session rebinds. Reinstall the
|
|
329
|
+
// spinner at the start of every run so the symbol survives those resets.
|
|
330
|
+
pi.on("before_agent_start", (_event, ctx) => {
|
|
331
|
+
capture(ctx);
|
|
332
|
+
installSpinner();
|
|
333
|
+
});
|
|
334
|
+
|
|
328
335
|
pi.on("input", (event, ctx) => {
|
|
329
336
|
capture(ctx);
|
|
330
337
|
const text = (event as { text?: string })?.text ?? "";
|
|
@@ -333,6 +340,7 @@ export default function register(pi?: PiAPI): void {
|
|
|
333
340
|
|
|
334
341
|
pi.on("agent_start", (_event, ctx) => {
|
|
335
342
|
capture(ctx);
|
|
343
|
+
installSpinner();
|
|
336
344
|
start();
|
|
337
345
|
});
|
|
338
346
|
|
|
@@ -0,0 +1,266 @@
|
|
|
1
|
+
// zero-pi — live activity panel for /forge progress and recent tool cards.
|
|
2
|
+
//
|
|
3
|
+
// This extension uses pi's public widget API (ctx.ui.setWidget) to render a
|
|
4
|
+
// compact panel above the input while the agent works. It intentionally avoids
|
|
5
|
+
// replacing pi's built-in tool renderer: the panel is a live dashboard, not a
|
|
6
|
+
// second source of truth.
|
|
7
|
+
|
|
8
|
+
interface Ctx {
|
|
9
|
+
ui?: {
|
|
10
|
+
setWidget?: (key: string, content: string[] | undefined, options?: { placement?: "aboveEditor" | "belowEditor" }) => void;
|
|
11
|
+
};
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
interface PiAPI {
|
|
15
|
+
on(event: string, handler: (event: unknown, ctx: Ctx) => unknown): void;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export const PHASES = ["clarify", "explore", "plan", "analyze", "build", "veredicto"] as const;
|
|
19
|
+
export type Phase = (typeof PHASES)[number];
|
|
20
|
+
export type PhaseState = "pending" | "active" | "done" | "error";
|
|
21
|
+
export type ToolState = "running" | "ok" | "error";
|
|
22
|
+
|
|
23
|
+
export interface ToolCard {
|
|
24
|
+
id: string;
|
|
25
|
+
name: string;
|
|
26
|
+
label: string;
|
|
27
|
+
state: ToolState;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export interface ActivityState {
|
|
31
|
+
sddActive: boolean;
|
|
32
|
+
phases: Record<Phase, PhaseState>;
|
|
33
|
+
tools: ToolCard[];
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const WIDGET_ID = "zero-activity-panel";
|
|
37
|
+
const MAX_TOOLS = 5;
|
|
38
|
+
|
|
39
|
+
const PHASE_FROM_AGENT: Record<string, Phase> = {
|
|
40
|
+
"zero-clarify": "clarify",
|
|
41
|
+
"zero-explore": "explore",
|
|
42
|
+
"zero-plan": "plan",
|
|
43
|
+
"zero-analyze": "analyze",
|
|
44
|
+
"zero-build": "build",
|
|
45
|
+
"zero-veredicto": "veredicto",
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
function esc(code: string, text: string): string {
|
|
49
|
+
return `\x1b[${code}m${text}\x1b[0m`;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const color = {
|
|
53
|
+
dim: (s: string) => esc("38;2;112;99;88", s),
|
|
54
|
+
muted: (s: string) => esc("38;2;184;155;120", s),
|
|
55
|
+
gold: (s: string) => esc("38;2;246;184;90", s),
|
|
56
|
+
coral: (s: string) => esc("38;2;255;107;95", s),
|
|
57
|
+
green: (s: string) => esc("38;2;0;255;138", s),
|
|
58
|
+
cyan: (s: string) => esc("38;2;0;215;255", s),
|
|
59
|
+
magenta: (s: string) => esc("38;2;216;107;255", s),
|
|
60
|
+
red: (s: string) => esc("38;2;255;79;123", s),
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
export function createActivityState(): ActivityState {
|
|
64
|
+
return {
|
|
65
|
+
sddActive: false,
|
|
66
|
+
phases: Object.fromEntries(PHASES.map((phase) => [phase, "pending"])) as Record<Phase, PhaseState>,
|
|
67
|
+
tools: [],
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export function isForgeInput(text: string): boolean {
|
|
72
|
+
return /(?:^|\s)\/forge\b|\bzero[\s-]?sdd\b|\bsdd\b/i.test(text ?? "");
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function collectAgents(args: unknown, out = new Set<string>()): Set<string> {
|
|
76
|
+
if (!args || typeof args !== "object") return out;
|
|
77
|
+
const a = args as Record<string, unknown>;
|
|
78
|
+
if (typeof a.agent === "string") out.add(a.agent);
|
|
79
|
+
for (const key of ["chain", "tasks"] as const) {
|
|
80
|
+
const list = a[key];
|
|
81
|
+
if (!Array.isArray(list)) continue;
|
|
82
|
+
for (const entry of list) {
|
|
83
|
+
if (!entry || typeof entry !== "object") continue;
|
|
84
|
+
const e = entry as Record<string, unknown>;
|
|
85
|
+
if (typeof e.agent === "string") out.add(e.agent);
|
|
86
|
+
const parallel = e.parallel;
|
|
87
|
+
if (Array.isArray(parallel)) {
|
|
88
|
+
for (const p of parallel) collectAgents(p, out);
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
return out;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
export function phaseFromSubagentArgs(args: unknown): Phase | undefined {
|
|
96
|
+
for (const name of collectAgents(args)) {
|
|
97
|
+
const key = name.toLowerCase().split(".").pop() ?? name.toLowerCase();
|
|
98
|
+
const phase = PHASE_FROM_AGENT[key];
|
|
99
|
+
if (phase) return phase;
|
|
100
|
+
}
|
|
101
|
+
return undefined;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
export function markPhaseActive(state: ActivityState, phase: Phase): void {
|
|
105
|
+
state.sddActive = true;
|
|
106
|
+
const activeIndex = PHASES.indexOf(phase);
|
|
107
|
+
for (const [index, name] of PHASES.entries()) {
|
|
108
|
+
if (index < activeIndex && state.phases[name] === "pending") state.phases[name] = "done";
|
|
109
|
+
if (name === phase) state.phases[name] = "active";
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
export function finishActivePhase(state: ActivityState, failed: boolean): void {
|
|
114
|
+
const active = PHASES.find((phase) => state.phases[phase] === "active");
|
|
115
|
+
if (!active) return;
|
|
116
|
+
state.phases[active] = failed ? "error" : "done";
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
export function toolLabel(toolName: string, args?: unknown): string {
|
|
120
|
+
const name = (toolName || "tool").split(/[._-]/)[0] || toolName || "tool";
|
|
121
|
+
if (toolName === "bash" && args && typeof args === "object") {
|
|
122
|
+
const command = String((args as Record<string, unknown>).command ?? "").trim();
|
|
123
|
+
const first = command.split(/\s+/).slice(0, 2).join(" ");
|
|
124
|
+
return first ? `bash ${first}` : "bash";
|
|
125
|
+
}
|
|
126
|
+
if (/read/i.test(toolName) && args && typeof args === "object") {
|
|
127
|
+
const path = String((args as Record<string, unknown>).path ?? "");
|
|
128
|
+
const short = path.split("/").filter(Boolean).pop();
|
|
129
|
+
return short ? `read ${short}` : "read";
|
|
130
|
+
}
|
|
131
|
+
if (/edit|write/i.test(toolName) && args && typeof args === "object") {
|
|
132
|
+
const path = String((args as Record<string, unknown>).path ?? "");
|
|
133
|
+
const short = path.split("/").filter(Boolean).pop();
|
|
134
|
+
return short ? `${name} ${short}` : name;
|
|
135
|
+
}
|
|
136
|
+
if (toolName.includes("__")) {
|
|
137
|
+
const [, server, tool] = toolName.split("__");
|
|
138
|
+
return `${server ?? "mcp"}:${tool ?? "tool"}`;
|
|
139
|
+
}
|
|
140
|
+
return toolName || "tool";
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
export function upsertTool(state: ActivityState, card: ToolCard): void {
|
|
144
|
+
const existing = state.tools.findIndex((tool) => tool.id === card.id);
|
|
145
|
+
if (existing >= 0) state.tools.splice(existing, 1);
|
|
146
|
+
state.tools.unshift(card);
|
|
147
|
+
state.tools = state.tools.slice(0, MAX_TOOLS);
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
function phaseGlyph(state: PhaseState): string {
|
|
151
|
+
switch (state) {
|
|
152
|
+
case "active": return color.cyan("⠋");
|
|
153
|
+
case "done": return color.green("✓");
|
|
154
|
+
case "error": return color.red("✗");
|
|
155
|
+
default: return color.dim("·");
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
function toolGlyph(state: ToolState): string {
|
|
160
|
+
switch (state) {
|
|
161
|
+
case "running": return color.cyan("⠋");
|
|
162
|
+
case "ok": return color.green("✓");
|
|
163
|
+
case "error": return color.red("✗");
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
export function renderActivityPanel(state: ActivityState): string[] {
|
|
168
|
+
if (!state.sddActive && state.tools.length === 0) return [];
|
|
169
|
+
const rule = (n: number) => color.coral("─".repeat(n));
|
|
170
|
+
const border = rule(72);
|
|
171
|
+
const header = `${color.coral("╭─")} ${color.gold("ZERO activity")} ${rule(54)}${color.coral("╮")}`;
|
|
172
|
+
const phaseLine = PHASES
|
|
173
|
+
.map((phase) => `${phaseGlyph(state.phases[phase])} ${state.phases[phase] === "active" ? color.cyan(phase) : color.muted(phase)}`)
|
|
174
|
+
.join(color.dim(" › "));
|
|
175
|
+
const toolLine = state.tools.length === 0
|
|
176
|
+
? color.dim("sin tools todavía")
|
|
177
|
+
: state.tools.map((tool) => `${toolGlyph(tool.state)} ${color.muted(tool.label)}`).join(color.dim(" · "));
|
|
178
|
+
return [
|
|
179
|
+
header,
|
|
180
|
+
`${color.coral("│")} ${color.magenta("SDD ")} ${phaseLine}`,
|
|
181
|
+
`${color.coral("│")} ${color.magenta("Tools")} ${toolLine}`,
|
|
182
|
+
`${color.coral("╰")}${border}${color.coral("╯")}`,
|
|
183
|
+
];
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
let registered = false;
|
|
187
|
+
|
|
188
|
+
export default function register(pi?: PiAPI): void {
|
|
189
|
+
if (!pi || typeof pi.on !== "function") return;
|
|
190
|
+
if (registered) return;
|
|
191
|
+
registered = true;
|
|
192
|
+
|
|
193
|
+
const state = createActivityState();
|
|
194
|
+
let ui: Ctx["ui"] | undefined;
|
|
195
|
+
|
|
196
|
+
const capture = (ctx: Ctx): void => {
|
|
197
|
+
if (ctx?.ui?.setWidget) ui = ctx.ui;
|
|
198
|
+
};
|
|
199
|
+
|
|
200
|
+
const draw = (): void => {
|
|
201
|
+
try {
|
|
202
|
+
const lines = renderActivityPanel(state);
|
|
203
|
+
ui?.setWidget?.(WIDGET_ID, lines.length > 0 ? lines : undefined, { placement: "aboveEditor" });
|
|
204
|
+
} catch {
|
|
205
|
+
// Visual sugar must never break the session.
|
|
206
|
+
}
|
|
207
|
+
};
|
|
208
|
+
|
|
209
|
+
const clear = (): void => {
|
|
210
|
+
state.sddActive = false;
|
|
211
|
+
state.tools = [];
|
|
212
|
+
for (const phase of PHASES) state.phases[phase] = "pending";
|
|
213
|
+
try { ui?.setWidget?.(WIDGET_ID, undefined); } catch { /* ignore */ }
|
|
214
|
+
};
|
|
215
|
+
|
|
216
|
+
pi.on("session_start", (_event, ctx) => {
|
|
217
|
+
capture(ctx);
|
|
218
|
+
draw();
|
|
219
|
+
});
|
|
220
|
+
|
|
221
|
+
pi.on("input", (event, ctx) => {
|
|
222
|
+
capture(ctx);
|
|
223
|
+
const text = String((event as { text?: unknown })?.text ?? "");
|
|
224
|
+
if (isForgeInput(text)) {
|
|
225
|
+
state.sddActive = true;
|
|
226
|
+
draw();
|
|
227
|
+
}
|
|
228
|
+
});
|
|
229
|
+
|
|
230
|
+
pi.on("tool_execution_start", (event, ctx) => {
|
|
231
|
+
capture(ctx);
|
|
232
|
+
const e = event as { toolCallId?: string; toolName?: string; args?: unknown };
|
|
233
|
+
const phase = e.toolName === "subagent" ? phaseFromSubagentArgs(e.args) : undefined;
|
|
234
|
+
if (phase) markPhaseActive(state, phase);
|
|
235
|
+
upsertTool(state, {
|
|
236
|
+
id: e.toolCallId ?? `${Date.now()}`,
|
|
237
|
+
name: e.toolName ?? "tool",
|
|
238
|
+
label: toolLabel(e.toolName ?? "tool", e.args),
|
|
239
|
+
state: "running",
|
|
240
|
+
});
|
|
241
|
+
draw();
|
|
242
|
+
});
|
|
243
|
+
|
|
244
|
+
pi.on("tool_execution_end", (event, ctx) => {
|
|
245
|
+
capture(ctx);
|
|
246
|
+
const e = event as { toolCallId?: string; toolName?: string; isError?: boolean };
|
|
247
|
+
const id = e.toolCallId ?? "";
|
|
248
|
+
const idx = state.tools.findIndex((tool) => tool.id === id);
|
|
249
|
+
if (idx >= 0) state.tools[idx].state = e.isError ? "error" : "ok";
|
|
250
|
+
if (e.toolName === "subagent") finishActivePhase(state, Boolean(e.isError));
|
|
251
|
+
draw();
|
|
252
|
+
});
|
|
253
|
+
|
|
254
|
+
pi.on("agent_end", (_event, ctx) => {
|
|
255
|
+
capture(ctx);
|
|
256
|
+
// Leave the completed panel visible briefly in the next render cycle, then clear.
|
|
257
|
+
setTimeout(clear, 1800).unref?.();
|
|
258
|
+
});
|
|
259
|
+
|
|
260
|
+
for (const lifecycle of ["session_shutdown", "session_before_switch"]) {
|
|
261
|
+
pi.on(lifecycle, (_event, ctx) => {
|
|
262
|
+
capture(ctx);
|
|
263
|
+
clear();
|
|
264
|
+
});
|
|
265
|
+
}
|
|
266
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@gonrocca/zero-pi",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.69",
|
|
4
4
|
"description": "zero-pi — an installable layer for pi (pi.dev): the zero spec-driven development workflow (clarify → explore → plan → analyze → build → veredicto) with automatic clarify/analyze gates, per-phase model autotune, and token-efficient batched builds. Adds capability to pi without modifying pi.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"keywords": [
|
|
@@ -28,6 +28,7 @@
|
|
|
28
28
|
"./extensions/zero-hud.ts",
|
|
29
29
|
"./extensions/zero-pretty-code-fences.ts",
|
|
30
30
|
"./extensions/zero-pretty-input-box.ts",
|
|
31
|
+
"./extensions/zero-activity-panel.ts",
|
|
31
32
|
"./extensions/working-phrases.ts",
|
|
32
33
|
"./extensions/win-tree-kill.ts",
|
|
33
34
|
"./extensions/sdd-agents.ts",
|
|
@@ -59,6 +60,7 @@
|
|
|
59
60
|
"extensions/zero-hud.ts",
|
|
60
61
|
"extensions/zero-pretty-code-fences.ts",
|
|
61
62
|
"extensions/zero-pretty-input-box.ts",
|
|
63
|
+
"extensions/zero-activity-panel.ts",
|
|
62
64
|
"extensions/zero-statusline.ts",
|
|
63
65
|
"extensions/working-phrases.ts",
|
|
64
66
|
"extensions/win-tree-kill.ts",
|