@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 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.67",
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",