@gonrocca/zero-pi 0.1.11 → 0.1.12

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.
@@ -1,295 +1,295 @@
1
- // zero-pi — hybrid working-phrase ticker and themed spinner.
2
- //
3
- // Pi shows a single static "Working..." line while the agent is busy. This
4
- // extension replaces it with a context-aware, rotating phrase:
5
- //
6
- // • a tool-specific line while a tool runs ("Leyendo archivos…")
7
- // • an SDD-phase line while a zero sub-agent runs ("Construyendo…")
8
- // • a rotation of playful verbs while the model thinks ("Maquinando…")
9
- //
10
- // It also installs a braille spinner tinted with the active theme.
11
- //
12
- // Built only on pi's public extension API — no pi internals are touched. Every
13
- // handler is defensive: visual sugar must never break an interactive session.
14
-
15
- /** A pi theme — only the colouring helper is used here. */
16
- interface Theme {
17
- fg(color: string, text: string): string;
18
- }
19
-
20
- /** The slice of pi's UI API this extension drives. */
21
- interface PiUI {
22
- theme: Theme;
23
- setWorkingMessage(message?: string): void;
24
- setWorkingIndicator(options?: { frames: string[]; intervalMs?: number }): void;
25
- }
26
-
27
- interface Ctx {
28
- ui: PiUI;
29
- }
30
-
31
- /** The subset of the extension API this extension subscribes to. */
32
- interface PiAPI {
33
- on(event: string, handler: (event: unknown, ctx: Ctx) => unknown): void;
34
- }
35
-
36
- // ── Phrase pools ───────────────────────────────────────────────────────────
37
-
38
- /** Playful verbs shown while the model is thinking on ordinary work. */
39
- const THINKING_PLAYFUL = [
40
- "Maquinando",
41
- "Rumiando",
42
- "Pensando",
43
- "Cocinando",
44
- "Tramando",
45
- "Conjurando",
46
- "Cavilando",
47
- "Hilando ideas",
48
- "Procesando",
49
- "Razonando",
50
- "Atando cabos",
51
- "Calibrando",
52
- ];
53
-
54
- /** Thinking verbs biased toward SDD work — used once an SDD run is detected. */
55
- const THINKING_SDD = [
56
- "Explorando ideas",
57
- "Trazando el plan",
58
- "Puliendo el diseño",
59
- "Revisando supuestos",
60
- "Ordenando las tareas",
61
- "Cuadrando el spec",
62
- "Maquinando",
63
- "Razonando",
64
- ];
65
-
66
- /** SDD phase labels keyed by the zero sub-agent that owns the phase. */
67
- const SDD_PHASE: Record<string, string> = {
68
- "zero-explore": "Explorando el código",
69
- "zero-plan": "Planeando la solución",
70
- "zero-build": "Construyendo la implementación",
71
- "zero-veredicto": "Revisando el veredicto",
72
- };
73
-
74
- /** Tool-name patterns → the phrase shown while that kind of tool runs. */
75
- const TOOL_RULES: Array<[RegExp, string]> = [
76
- [/^(read|cat|view|open)/i, "Leyendo archivos"],
77
- [/(multi.?edit|str.?replace|edit|patch|apply)/i, "Aplicando cambios"],
78
- [/(write|create)/i, "Escribiendo archivos"],
79
- [/(bash|shell|exec|run|terminal|cmd)/i, "Ejecutando comandos"],
80
- [/(grep|search|ripgrep|^rg)/i, "Buscando en el código"],
81
- [/(glob|find|^ls$|list|tree)/i, "Rastreando archivos"],
82
- [/(web|fetch|http|url|browse|curl)/i, "Navegando la web"],
83
- [/(subagent|custom-agent|^task$|agent)/i, "Coordinando sub-agentes"],
84
- [/(todo|plan)/i, "Ordenando el plan"],
85
- ];
86
-
87
- // ── Pure helpers (exported for tests) ──────────────────────────────────────
88
-
89
- /** Resolve the phrase for a tool by its name; falls back to the raw name. */
90
- export function toolPhrase(toolName: string): string {
91
- const name = (toolName ?? "").trim();
92
- if (name === "") return "Trabajando";
93
- // MCP tools come through as `mcp__server__tool` — name the server, not pi.
94
- if (name.includes("__") || /^mcp[._-]/i.test(name)) return "Consultando un MCP";
95
- for (const [pattern, phrase] of TOOL_RULES) {
96
- if (pattern.test(name)) return phrase;
97
- }
98
- return `Usando ${name}`;
99
- }
100
-
101
- /**
102
- * If a sub-agent invocation targets a zero SDD phase agent, return that phase's
103
- * label; otherwise return `null`. Accepts the single, chain, and parallel
104
- * shapes of the `subagent` tool's arguments.
105
- */
106
- export function sddPhase(args: unknown): string | null {
107
- const names = new Set<string>();
108
- const collect = (value: unknown): void => {
109
- if (typeof value === "string") names.add(value);
110
- };
111
- if (args && typeof args === "object") {
112
- const a = args as Record<string, unknown>;
113
- collect(a.agent);
114
- for (const key of ["chain", "tasks"] as const) {
115
- const list = a[key];
116
- if (Array.isArray(list)) {
117
- for (const entry of list) {
118
- if (entry && typeof entry === "object") {
119
- const e = entry as Record<string, unknown>;
120
- collect(e.agent);
121
- if (Array.isArray(e.parallel)) {
122
- for (const p of e.parallel) {
123
- if (p && typeof p === "object") collect((p as Record<string, unknown>).agent);
124
- }
125
- }
126
- }
127
- }
128
- }
129
- }
130
- }
131
- for (const name of names) {
132
- const key = name.toLowerCase().split(".").pop() ?? name;
133
- if (SDD_PHASE[key]) return SDD_PHASE[key];
134
- }
135
- return null;
136
- }
137
-
138
- /** Whether a piece of user input signals an SDD run is starting. */
139
- export function looksLikeSdd(text: string): boolean {
140
- return /\/forge\b|\bforge\b|\bsdd\b|zero[\s-]?sdd/i.test(text ?? "");
141
- }
142
-
143
- /** Pick a thinking phrase, avoiding an immediate repeat of `previous`. */
144
- export function pickThinking(previous: string | undefined, sddBias: boolean): string {
145
- const pool = sddBias ? THINKING_SDD : THINKING_PLAYFUL;
146
- const choices = pool.length > 1 ? pool.filter((p) => p !== previous) : pool;
147
- return choices[Math.floor(Math.random() * choices.length)];
148
- }
149
-
150
- // ── Spinner ────────────────────────────────────────────────────────────────
151
-
152
- /** Braille spinner glyphs, in rotation order. */
153
- const SPIN_GLYPHS = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"];
154
-
155
- /** A per-frame colour cycle that makes the spinner gently breathe. */
156
- const SPIN_COLORS = ["dim", "muted", "accent", "accent", "accent", "muted"];
157
-
158
- /** Build the themed spinner frames. */
159
- export function spinnerFrames(theme: Theme): string[] {
160
- return SPIN_GLYPHS.map((glyph, i) => {
161
- const color = SPIN_COLORS[i % SPIN_COLORS.length];
162
- try {
163
- return theme.fg(color, glyph);
164
- } catch {
165
- return glyph;
166
- }
167
- });
168
- }
169
-
170
- // ── Extension entry point ──────────────────────────────────────────────────
171
-
172
- export default function register(pi?: PiAPI): void {
173
- if (!pi || typeof pi.on !== "function") return;
174
-
175
- // The last UI handle seen on any event — the rotation timer uses it.
176
- let ui: PiUI | undefined;
177
- // The phrase-rotation interval; defined only while the agent is busy.
178
- let timer: ReturnType<typeof setInterval> | undefined;
179
- // Active tools, keyed by tool-call id, in start order. The most recently
180
- // started tool's phrase wins the indicator.
181
- const activeTools = new Map<string, string>();
182
- // The phrase the model-thinking rotation last produced.
183
- let lastThinking: string | undefined;
184
- // Whether the most recent prompt looks like an SDD run.
185
- let sddBias = false;
186
-
187
- /** The phrase to show right now, given current state. */
188
- const currentPhrase = (): string => {
189
- if (activeTools.size > 0) {
190
- // Last inserted entry — most recently started tool.
191
- let phrase = "Trabajando";
192
- for (const value of activeTools.values()) phrase = value;
193
- return phrase;
194
- }
195
- lastThinking = pickThinking(lastThinking, sddBias);
196
- return lastThinking;
197
- };
198
-
199
- /** Push the current phrase into pi's working indicator. */
200
- const render = (): void => {
201
- if (!ui) return;
202
- try {
203
- ui.setWorkingMessage(`${currentPhrase()}… (esc)`);
204
- } catch {
205
- // Indicator failures must never surface.
206
- }
207
- };
208
-
209
- /** Begin the rotation if it is not already running. */
210
- const start = (): void => {
211
- render();
212
- if (timer) return;
213
- timer = setInterval(render, 2400);
214
- // Don't keep the process alive purely for the ticker.
215
- (timer as { unref?: () => void }).unref?.();
216
- };
217
-
218
- /** Stop the rotation and restore pi's default working message. */
219
- const stop = (): void => {
220
- if (timer) {
221
- clearInterval(timer);
222
- timer = undefined;
223
- }
224
- activeTools.clear();
225
- try {
226
- ui?.setWorkingMessage();
227
- } catch {
228
- // ignore
229
- }
230
- };
231
-
232
- /** Install the themed spinner once a UI handle is available. */
233
- const installSpinner = (): void => {
234
- if (!ui) return;
235
- try {
236
- ui.setWorkingIndicator({ frames: spinnerFrames(ui.theme), intervalMs: 90 });
237
- } catch {
238
- // A spinner failure is harmless — pi keeps its default.
239
- }
240
- };
241
-
242
- const capture = (ctx: Ctx): void => {
243
- if (ctx && ctx.ui) ui = ctx.ui;
244
- };
245
-
246
- pi.on("session_start", (_event, ctx) => {
247
- capture(ctx);
248
- installSpinner();
249
- });
250
-
251
- pi.on("input", (event, ctx) => {
252
- capture(ctx);
253
- const text = (event as { text?: string })?.text ?? "";
254
- if (text) sddBias = looksLikeSdd(text);
255
- });
256
-
257
- pi.on("agent_start", (_event, ctx) => {
258
- capture(ctx);
259
- start();
260
- });
261
-
262
- pi.on("message_update", (_event, ctx) => {
263
- capture(ctx);
264
- });
265
-
266
- pi.on("tool_execution_start", (event, ctx) => {
267
- capture(ctx);
268
- const e = event as { toolCallId?: string; toolName?: string; args?: unknown };
269
- const id = e.toolCallId ?? `${activeTools.size}`;
270
- const phrase = sddPhase(e.args) ?? toolPhrase(e.toolName ?? "");
271
- activeTools.set(id, phrase);
272
- sddBias = sddBias || sddPhase(e.args) !== null;
273
- render();
274
- });
275
-
276
- pi.on("tool_execution_end", (event, ctx) => {
277
- capture(ctx);
278
- const id = (event as { toolCallId?: string }).toolCallId;
279
- if (id !== undefined) activeTools.delete(id);
280
- render();
281
- });
282
-
283
- pi.on("agent_end", (_event, ctx) => {
284
- capture(ctx);
285
- stop();
286
- });
287
-
288
- // Tidy up if the session is torn down mid-run.
289
- for (const lifecycle of ["session_shutdown", "session_before_switch"]) {
290
- pi.on(lifecycle, (_event, ctx) => {
291
- capture(ctx);
292
- stop();
293
- });
294
- }
295
- }
1
+ // zero-pi — hybrid working-phrase ticker and themed spinner.
2
+ //
3
+ // Pi shows a single static "Working..." line while the agent is busy. This
4
+ // extension replaces it with a context-aware, rotating phrase:
5
+ //
6
+ // • a tool-specific line while a tool runs ("Leyendo archivos…")
7
+ // • an SDD-phase line while a zero sub-agent runs ("Construyendo…")
8
+ // • a rotation of playful verbs while the model thinks ("Maquinando…")
9
+ //
10
+ // It also installs a braille spinner tinted with the active theme.
11
+ //
12
+ // Built only on pi's public extension API — no pi internals are touched. Every
13
+ // handler is defensive: visual sugar must never break an interactive session.
14
+
15
+ /** A pi theme — only the colouring helper is used here. */
16
+ interface Theme {
17
+ fg(color: string, text: string): string;
18
+ }
19
+
20
+ /** The slice of pi's UI API this extension drives. */
21
+ interface PiUI {
22
+ theme: Theme;
23
+ setWorkingMessage(message?: string): void;
24
+ setWorkingIndicator(options?: { frames: string[]; intervalMs?: number }): void;
25
+ }
26
+
27
+ interface Ctx {
28
+ ui: PiUI;
29
+ }
30
+
31
+ /** The subset of the extension API this extension subscribes to. */
32
+ interface PiAPI {
33
+ on(event: string, handler: (event: unknown, ctx: Ctx) => unknown): void;
34
+ }
35
+
36
+ // ── Phrase pools ───────────────────────────────────────────────────────────
37
+
38
+ /** Playful verbs shown while the model is thinking on ordinary work. */
39
+ const THINKING_PLAYFUL = [
40
+ "Maquinando",
41
+ "Rumiando",
42
+ "Pensando",
43
+ "Cocinando",
44
+ "Tramando",
45
+ "Conjurando",
46
+ "Cavilando",
47
+ "Hilando ideas",
48
+ "Procesando",
49
+ "Razonando",
50
+ "Atando cabos",
51
+ "Calibrando",
52
+ ];
53
+
54
+ /** Thinking verbs biased toward SDD work — used once an SDD run is detected. */
55
+ const THINKING_SDD = [
56
+ "Explorando ideas",
57
+ "Trazando el plan",
58
+ "Puliendo el diseño",
59
+ "Revisando supuestos",
60
+ "Ordenando las tareas",
61
+ "Cuadrando el spec",
62
+ "Maquinando",
63
+ "Razonando",
64
+ ];
65
+
66
+ /** SDD phase labels keyed by the zero sub-agent that owns the phase. */
67
+ const SDD_PHASE: Record<string, string> = {
68
+ "zero-explore": "Explorando el código",
69
+ "zero-plan": "Planeando la solución",
70
+ "zero-build": "Construyendo la implementación",
71
+ "zero-veredicto": "Revisando el veredicto",
72
+ };
73
+
74
+ /** Tool-name patterns → the phrase shown while that kind of tool runs. */
75
+ const TOOL_RULES: Array<[RegExp, string]> = [
76
+ [/^(read|cat|view|open)/i, "Leyendo archivos"],
77
+ [/(multi.?edit|str.?replace|edit|patch|apply)/i, "Aplicando cambios"],
78
+ [/(write|create)/i, "Escribiendo archivos"],
79
+ [/(bash|shell|exec|run|terminal|cmd)/i, "Ejecutando comandos"],
80
+ [/(grep|search|ripgrep|^rg)/i, "Buscando en el código"],
81
+ [/(glob|find|^ls$|list|tree)/i, "Rastreando archivos"],
82
+ [/(web|fetch|http|url|browse|curl)/i, "Navegando la web"],
83
+ [/(subagent|custom-agent|^task$|agent)/i, "Coordinando sub-agentes"],
84
+ [/(todo|plan)/i, "Ordenando el plan"],
85
+ ];
86
+
87
+ // ── Pure helpers (exported for tests) ──────────────────────────────────────
88
+
89
+ /** Resolve the phrase for a tool by its name; falls back to the raw name. */
90
+ export function toolPhrase(toolName: string): string {
91
+ const name = (toolName ?? "").trim();
92
+ if (name === "") return "Trabajando";
93
+ // MCP tools come through as `mcp__server__tool` — name the server, not pi.
94
+ if (name.includes("__") || /^mcp[._-]/i.test(name)) return "Consultando un MCP";
95
+ for (const [pattern, phrase] of TOOL_RULES) {
96
+ if (pattern.test(name)) return phrase;
97
+ }
98
+ return `Usando ${name}`;
99
+ }
100
+
101
+ /**
102
+ * If a sub-agent invocation targets a zero SDD phase agent, return that phase's
103
+ * label; otherwise return `null`. Accepts the single, chain, and parallel
104
+ * shapes of the `subagent` tool's arguments.
105
+ */
106
+ export function sddPhase(args: unknown): string | null {
107
+ const names = new Set<string>();
108
+ const collect = (value: unknown): void => {
109
+ if (typeof value === "string") names.add(value);
110
+ };
111
+ if (args && typeof args === "object") {
112
+ const a = args as Record<string, unknown>;
113
+ collect(a.agent);
114
+ for (const key of ["chain", "tasks"] as const) {
115
+ const list = a[key];
116
+ if (Array.isArray(list)) {
117
+ for (const entry of list) {
118
+ if (entry && typeof entry === "object") {
119
+ const e = entry as Record<string, unknown>;
120
+ collect(e.agent);
121
+ if (Array.isArray(e.parallel)) {
122
+ for (const p of e.parallel) {
123
+ if (p && typeof p === "object") collect((p as Record<string, unknown>).agent);
124
+ }
125
+ }
126
+ }
127
+ }
128
+ }
129
+ }
130
+ }
131
+ for (const name of names) {
132
+ const key = name.toLowerCase().split(".").pop() ?? name;
133
+ if (SDD_PHASE[key]) return SDD_PHASE[key];
134
+ }
135
+ return null;
136
+ }
137
+
138
+ /** Whether a piece of user input signals an SDD run is starting. */
139
+ export function looksLikeSdd(text: string): boolean {
140
+ return /\/forge\b|\bforge\b|\bsdd\b|zero[\s-]?sdd/i.test(text ?? "");
141
+ }
142
+
143
+ /** Pick a thinking phrase, avoiding an immediate repeat of `previous`. */
144
+ export function pickThinking(previous: string | undefined, sddBias: boolean): string {
145
+ const pool = sddBias ? THINKING_SDD : THINKING_PLAYFUL;
146
+ const choices = pool.length > 1 ? pool.filter((p) => p !== previous) : pool;
147
+ return choices[Math.floor(Math.random() * choices.length)];
148
+ }
149
+
150
+ // ── Spinner ────────────────────────────────────────────────────────────────
151
+
152
+ /** Braille spinner glyphs, in rotation order. */
153
+ const SPIN_GLYPHS = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"];
154
+
155
+ /** A per-frame colour cycle that makes the spinner gently breathe. */
156
+ const SPIN_COLORS = ["dim", "muted", "accent", "accent", "accent", "muted"];
157
+
158
+ /** Build the themed spinner frames. */
159
+ export function spinnerFrames(theme: Theme): string[] {
160
+ return SPIN_GLYPHS.map((glyph, i) => {
161
+ const color = SPIN_COLORS[i % SPIN_COLORS.length];
162
+ try {
163
+ return theme.fg(color, glyph);
164
+ } catch {
165
+ return glyph;
166
+ }
167
+ });
168
+ }
169
+
170
+ // ── Extension entry point ──────────────────────────────────────────────────
171
+
172
+ export default function register(pi?: PiAPI): void {
173
+ if (!pi || typeof pi.on !== "function") return;
174
+
175
+ // The last UI handle seen on any event — the rotation timer uses it.
176
+ let ui: PiUI | undefined;
177
+ // The phrase-rotation interval; defined only while the agent is busy.
178
+ let timer: ReturnType<typeof setInterval> | undefined;
179
+ // Active tools, keyed by tool-call id, in start order. The most recently
180
+ // started tool's phrase wins the indicator.
181
+ const activeTools = new Map<string, string>();
182
+ // The phrase the model-thinking rotation last produced.
183
+ let lastThinking: string | undefined;
184
+ // Whether the most recent prompt looks like an SDD run.
185
+ let sddBias = false;
186
+
187
+ /** The phrase to show right now, given current state. */
188
+ const currentPhrase = (): string => {
189
+ if (activeTools.size > 0) {
190
+ // Last inserted entry — most recently started tool.
191
+ let phrase = "Trabajando";
192
+ for (const value of activeTools.values()) phrase = value;
193
+ return phrase;
194
+ }
195
+ lastThinking = pickThinking(lastThinking, sddBias);
196
+ return lastThinking;
197
+ };
198
+
199
+ /** Push the current phrase into pi's working indicator. */
200
+ const render = (): void => {
201
+ if (!ui) return;
202
+ try {
203
+ ui.setWorkingMessage(`${currentPhrase()}… (esc)`);
204
+ } catch {
205
+ // Indicator failures must never surface.
206
+ }
207
+ };
208
+
209
+ /** Begin the rotation if it is not already running. */
210
+ const start = (): void => {
211
+ render();
212
+ if (timer) return;
213
+ timer = setInterval(render, 2400);
214
+ // Don't keep the process alive purely for the ticker.
215
+ (timer as { unref?: () => void }).unref?.();
216
+ };
217
+
218
+ /** Stop the rotation and restore pi's default working message. */
219
+ const stop = (): void => {
220
+ if (timer) {
221
+ clearInterval(timer);
222
+ timer = undefined;
223
+ }
224
+ activeTools.clear();
225
+ try {
226
+ ui?.setWorkingMessage();
227
+ } catch {
228
+ // ignore
229
+ }
230
+ };
231
+
232
+ /** Install the themed spinner once a UI handle is available. */
233
+ const installSpinner = (): void => {
234
+ if (!ui) return;
235
+ try {
236
+ ui.setWorkingIndicator({ frames: spinnerFrames(ui.theme), intervalMs: 90 });
237
+ } catch {
238
+ // A spinner failure is harmless — pi keeps its default.
239
+ }
240
+ };
241
+
242
+ const capture = (ctx: Ctx): void => {
243
+ if (ctx && ctx.ui) ui = ctx.ui;
244
+ };
245
+
246
+ pi.on("session_start", (_event, ctx) => {
247
+ capture(ctx);
248
+ installSpinner();
249
+ });
250
+
251
+ pi.on("input", (event, ctx) => {
252
+ capture(ctx);
253
+ const text = (event as { text?: string })?.text ?? "";
254
+ if (text) sddBias = looksLikeSdd(text);
255
+ });
256
+
257
+ pi.on("agent_start", (_event, ctx) => {
258
+ capture(ctx);
259
+ start();
260
+ });
261
+
262
+ pi.on("message_update", (_event, ctx) => {
263
+ capture(ctx);
264
+ });
265
+
266
+ pi.on("tool_execution_start", (event, ctx) => {
267
+ capture(ctx);
268
+ const e = event as { toolCallId?: string; toolName?: string; args?: unknown };
269
+ const id = e.toolCallId ?? `${activeTools.size}`;
270
+ const phrase = sddPhase(e.args) ?? toolPhrase(e.toolName ?? "");
271
+ activeTools.set(id, phrase);
272
+ sddBias = sddBias || sddPhase(e.args) !== null;
273
+ render();
274
+ });
275
+
276
+ pi.on("tool_execution_end", (event, ctx) => {
277
+ capture(ctx);
278
+ const id = (event as { toolCallId?: string }).toolCallId;
279
+ if (id !== undefined) activeTools.delete(id);
280
+ render();
281
+ });
282
+
283
+ pi.on("agent_end", (_event, ctx) => {
284
+ capture(ctx);
285
+ stop();
286
+ });
287
+
288
+ // Tidy up if the session is torn down mid-run.
289
+ for (const lifecycle of ["session_shutdown", "session_before_switch"]) {
290
+ pi.on(lifecycle, (_event, ctx) => {
291
+ capture(ctx);
292
+ stop();
293
+ });
294
+ }
295
+ }