pi-idle 1.0.2 → 1.0.4

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.
Files changed (3) hide show
  1. package/package.json +11 -2
  2. package/pi-idle.ts +14 -0
  3. package/test.ts +42 -11
package/package.json CHANGED
@@ -1,8 +1,8 @@
1
1
  {
2
2
  "name": "pi-idle",
3
- "version": "1.0.2",
3
+ "version": "1.0.4",
4
4
  "description": "Pi extension: shows ✓ in terminal title when idle, spinner (◰◳◲◱) while working, with context-usage percentage beside the checkmark",
5
- "keywords": ["idle", "done", "spinner", "pi-extension", "pi", "pi-package"],
5
+ "keywords": ["idle", "done", "spinner", "pi-extension", "pi-package"],
6
6
  "type": "module",
7
7
  "license": "MIT",
8
8
  "repository": {
@@ -13,6 +13,15 @@
13
13
  "test": "vitest run",
14
14
  "test:watch": "vitest"
15
15
  },
16
+ "engines": {
17
+ "node": ">=22.0.0"
18
+ },
19
+ "pi": {
20
+ "extensions": ["./pi-idle.ts"]
21
+ },
22
+ "peerDependencies": {
23
+ "@earendil-works/pi-coding-agent": "*"
24
+ },
16
25
  "devDependencies": {
17
26
  "vitest": "^3.2.4"
18
27
  }
package/pi-idle.ts CHANGED
@@ -92,6 +92,9 @@ export default function (pi: ExtensionAPI) {
92
92
  // ── Lifecycle hooks ────────────────────────────────────────
93
93
 
94
94
  pi.on("session_start", async (_event, ctx) => {
95
+ // Schedule after microtask so pi's init-based updateTerminalTitle()
96
+ // fires first, then we overwrite it with the checkmark.
97
+ await Promise.resolve();
95
98
  showDone(ctx);
96
99
  });
97
100
 
@@ -101,6 +104,17 @@ export default function (pi: ExtensionAPI) {
101
104
  }
102
105
  });
103
106
 
107
+ pi.on("agent_start", async (_event, ctx) => {
108
+ // agent_start always fires after input for every user prompt;
109
+ // backstop in case the input handler missed a non-interactive source.
110
+ startSpinner(ctx);
111
+ });
112
+
113
+ pi.on("turn_start", async (_event, ctx) => {
114
+ // Multi-turn agent: keep spinner running between turns.
115
+ startSpinner(ctx);
116
+ });
117
+
104
118
  pi.on("agent_end", async (_event, ctx) => {
105
119
  showDone(ctx);
106
120
  });
package/test.ts CHANGED
@@ -3,16 +3,18 @@
3
3
  *
4
4
  * Tests:
5
5
  * 1. Module loads and exports a default function
6
- * 2. All 4 lifecycle handlers are registered
7
- * 3. `session_start` → plain checkmark in title
6
+ * 2. All 6 lifecycle handlers are registered
7
+ * 3. `session_start` → plain checkmark in title (after microtask yield)
8
8
  * 4. `input` (interactive) → spinner starts in title
9
9
  * 5. `input` (non-interactive) → no spinner
10
- * 6. `agent_end` → checkmark restored, spinner stopped
11
- * 7. `session_shutdown` → plain base title
12
- * 8. Context ≤50% no indicator in title
13
- * 9. Context >50% [N%] in title
14
- * 10. Context ≥90% → ![N%]! in title
15
- * 11. Context nullno indicator
10
+ * 6. `agent_start` → spinner starts in title
11
+ * 7. `turn_start` → spinner starts in title (multi-turn)
12
+ * 8. `agent_end`checkmark restored, spinner stopped
13
+ * 9. `session_shutdown`plain base title
14
+ * 10. Context ≤50% → no indicator in title
15
+ * 11. Context >50%[N%] in title
16
+ * 12. Context ≥90% → ![N%]! in title
17
+ * 13. Context null → no indicator
16
18
  */
17
19
 
18
20
  import { describe, it, expect, vi, beforeEach } from "vitest";
@@ -33,7 +35,7 @@ function createMockPi(): ExtensionAPI & { _handlers: Map<string, Function> } {
33
35
 
34
36
  function createMockCtx(overrides?: Partial<ExtensionContext>): ExtensionContext {
35
37
  const theme = {
36
- fg: vi.fn((_color: string, text: string) => `«${_color}:${text}»`),
38
+ fg: vi.fn((_color: string, text: string) => `\x1b[32m${text}\x1b[0m`),
37
39
  };
38
40
  return {
39
41
  ui: {
@@ -55,7 +57,7 @@ describe("pi-idle.ts module", () => {
55
57
  expect(typeof mod.default).toBe("function");
56
58
  });
57
59
 
58
- it("registers all four lifecycle handlers", async () => {
60
+ it("registers all six lifecycle handlers", async () => {
59
61
  const mockPi = createMockPi();
60
62
  const mod = await import("./pi-idle.ts");
61
63
  mod.default(mockPi as unknown as ExtensionAPI);
@@ -65,6 +67,8 @@ describe("pi-idle.ts module", () => {
65
67
  );
66
68
  expect(events.has("session_start")).toBe(true);
67
69
  expect(events.has("input")).toBe(true);
70
+ expect(events.has("agent_start")).toBe(true);
71
+ expect(events.has("turn_start")).toBe(true);
68
72
  expect(events.has("agent_end")).toBe(true);
69
73
  expect(events.has("session_shutdown")).toBe(true);
70
74
  });
@@ -81,11 +85,13 @@ describe("extension handlers", () => {
81
85
  mod.default(mockPi as unknown as ExtensionAPI);
82
86
  });
83
87
 
84
- it("session_start: ≤50% context → no indicator in title", async () => {
88
+ it("session_start: ≤50% context → no indicator in title (after microtask yield)", async () => {
85
89
  const ctx = createMockCtx(); // 25% ≤ 50%
86
90
  const handler = mockPi._handlers.get("session_start")!;
87
91
  await handler({ reason: "startup" }, ctx);
88
92
 
93
+ // session_start yields via await Promise.resolve() to survive pi's
94
+ // init-based updateTerminalTitle(); make sure the microtask has run.
89
95
  expect(ctx.ui.setTitle).toHaveBeenCalledWith("✓ π - pi-idle");
90
96
  expect(ctx.ui.setStatus).not.toHaveBeenCalled();
91
97
  });
@@ -114,6 +120,31 @@ describe("extension handlers", () => {
114
120
  expect(ctx.ui.setStatus).not.toHaveBeenCalled();
115
121
  });
116
122
 
123
+ it("agent_start starts spinner in title", async () => {
124
+ const ctx = createMockCtx();
125
+ const handler = mockPi._handlers.get("agent_start")!;
126
+ await handler({}, ctx);
127
+
128
+ await new Promise((r) => setTimeout(r, 150));
129
+
130
+ expect(ctx.ui.setTitle).toHaveBeenCalled();
131
+ const firstCall = (ctx.ui.setTitle as ReturnType<typeof vi.fn>).mock.calls[0][0] as string;
132
+ expect(firstCall).toMatch(/^[◰◳◲◱] π - pi-idle$/);
133
+ expect(ctx.ui.setStatus).not.toHaveBeenCalled();
134
+ });
135
+
136
+ it("turn_start starts spinner in title (multi-turn)", async () => {
137
+ const ctx = createMockCtx();
138
+ const handler = mockPi._handlers.get("turn_start")!;
139
+ await handler({ turnIndex: 1, timestamp: Date.now() }, ctx);
140
+
141
+ await new Promise((r) => setTimeout(r, 150));
142
+
143
+ expect(ctx.ui.setTitle).toHaveBeenCalled();
144
+ const firstCall = (ctx.ui.setTitle as ReturnType<typeof vi.fn>).mock.calls[0][0] as string;
145
+ expect(firstCall).toMatch(/^[◰◳◲◱] π - pi-idle$/);
146
+ });
147
+
117
148
  it("agent_end restores checkmark in title", async () => {
118
149
  const ctx = createMockCtx(); // 25% ≤ 50%
119
150
  const handler = mockPi._handlers.get("agent_end")!;