@gonrocca/zero-pi 0.1.66 → 0.1.68

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`. |
@@ -73,6 +73,22 @@ const THINKING_PLAYFUL = [
73
73
  "No rompiendo producción",
74
74
  "Haciendo que compile en la cabeza",
75
75
  "Respirando antes de tocar código",
76
+ "Usando la Fuerza",
77
+ "Afinando el sable láser",
78
+ "Saltando al hiperespacio",
79
+ "Esquivando blasters",
80
+ "Consultando al droide astromecánico",
81
+ "Que el diff te acompañe",
82
+ "Evitando el lado oscuro del scope",
83
+ "No es el bug que estás buscando",
84
+ "Cargando el ki",
85
+ "Entrando en modo Super Saiyajin",
86
+ "Juntando energía para la Genkidama",
87
+ "Buscando las Esferas del Dragón",
88
+ "Entrenando en gravedad 100x",
89
+ "Midiendo poder con el scouter",
90
+ "Preparando un Kamehameha al bug",
91
+ "Teletransportando contexto",
76
92
  ];
77
93
 
78
94
  /** Thinking verbs biased toward SDD work — used once an SDD run is detected. */
@@ -98,6 +114,15 @@ const THINKING_SDD = [
98
114
  "Preguntando lo justo",
99
115
  "Cerrando ambigüedades",
100
116
  "Forjando el spec",
117
+ "Escribiendo el holocrón del spec",
118
+ "Auditando como Consejo Jedi",
119
+ "Manteniendo balance en la Fuerza",
120
+ "Evitando el lado oscuro del scope creep",
121
+ "Buscando el canon del requisito",
122
+ "Entrenando en la Habitación del Tiempo",
123
+ "Cargando Genkidama de tests",
124
+ "Subiendo el poder del build",
125
+ "Invocando a Shenlong por un veredicto pasa",
101
126
  "Maquinando",
102
127
  "Razonando",
103
128
  ];
@@ -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.66",
3
+ "version": "0.1.68",
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",