pi-idle 1.0.4 → 1.0.6

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
@@ -16,6 +16,12 @@ A [pi](https://github.com/earendil-works/pi-coding-agent) extension that puts a
16
16
  - `≤ 50 %` → percentage hidden; `> 50 %` → `[N%]`; `≥ 90 %` → `![N%]!` (high-usage warning).
17
17
  - ANSI colours don't work inside terminal-title OSC sequences, so everything is plain text.
18
18
 
19
+ ## Screenshots
20
+
21
+ | Working | Idle |
22
+ |---|---|
23
+ | ![spinner](assets/spinner.svg) | ![idle](assets/idle.png) |
24
+
19
25
  ## Install
20
26
 
21
27
  From npm:
Binary file
Binary file
@@ -0,0 +1,5 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" width="128" height="128" viewBox="0 0 128 128">
2
+ <text x="64" y="90" font-size="96" font-family="sans-serif" fill="#bd93f9" text-anchor="middle">
3
+ <animate attributeName="textContent" values="◰;◳;◲;◱;◰" keyTimes="0;0.25;0.5;0.75;1" dur="1s" repeatCount="indefinite" calcMode="discrete" />
4
+ </text>
5
+ </svg>
package/package.json CHANGED
@@ -1,8 +1,14 @@
1
1
  {
2
2
  "name": "pi-idle",
3
- "version": "1.0.4",
3
+ "version": "1.0.6",
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-package"],
5
+ "keywords": [
6
+ "idle",
7
+ "done",
8
+ "spinner",
9
+ "pi-extension",
10
+ "pi-package"
11
+ ],
6
12
  "type": "module",
7
13
  "license": "MIT",
8
14
  "repository": {
@@ -17,10 +23,9 @@
17
23
  "node": ">=22.0.0"
18
24
  },
19
25
  "pi": {
20
- "extensions": ["./pi-idle.ts"]
21
- },
22
- "peerDependencies": {
23
- "@earendil-works/pi-coding-agent": "*"
26
+ "extensions": [
27
+ "./pi-idle.ts"
28
+ ]
24
29
  },
25
30
  "devDependencies": {
26
31
  "vitest": "^3.2.4"
package/pi-idle.ts CHANGED
@@ -57,6 +57,8 @@ function getContextIndicator(ctx: ExtensionContext): string {
57
57
  export default function (pi: ExtensionAPI) {
58
58
  let timer: ReturnType<typeof setInterval> | null = null;
59
59
  let frameIndex = 0;
60
+ let spinnerActive = false;
61
+ let currentCtx: ExtensionContext | null = null;
60
62
 
61
63
  // ── Internal helpers ───────────────────────────────────────
62
64
 
@@ -65,7 +67,9 @@ export default function (pi: ExtensionAPI) {
65
67
  clearInterval(timer);
66
68
  timer = null;
67
69
  }
70
+ spinnerActive = false;
68
71
  frameIndex = 0;
72
+ currentCtx = null;
69
73
  }
70
74
 
71
75
  /** Write the idle title (plain text — colours don't work in OSC titles). */
@@ -77,16 +81,35 @@ export default function (pi: ExtensionAPI) {
77
81
  ctx.ui.setTitle(`✓${spacer}${indicator} ${baseTitle}`);
78
82
  }
79
83
 
84
+ function showSpinnerFrame(ctx: ExtensionContext) {
85
+ const frame = SPINNER_FRAMES[frameIndex % SPINNER_FRAMES.length];
86
+ const baseTitle = getBaseTitle(pi);
87
+ ctx.ui.setTitle(`${frame} ${baseTitle}`);
88
+ frameIndex++;
89
+ }
90
+
80
91
  /** Start the spinner in the title. No context percentage — it only appears with the checkmark. */
81
92
  function startSpinner(ctx: ExtensionContext) {
82
- stopSpinner();
93
+ // Don't restart if already spinning — avoids race conditions and reduces CPU
94
+ if (spinnerActive) {
95
+ frameIndex = 0;
96
+ currentCtx = ctx;
97
+ return;
98
+ }
99
+
100
+ spinnerActive = true;
101
+ currentCtx = ctx;
102
+ frameIndex = 0;
103
+
104
+ // Show first frame immediately so user sees spinner right away.
105
+ showSpinnerFrame(ctx);
83
106
 
84
107
  timer = setInterval(() => {
85
- const frame = SPINNER_FRAMES[frameIndex % SPINNER_FRAMES.length];
86
- const baseTitle = getBaseTitle(pi);
87
- ctx.ui.setTitle(`${frame} ${baseTitle}`);
88
- frameIndex++;
89
- }, 120);
108
+ if (currentCtx) {
109
+ showSpinnerFrame(currentCtx);
110
+ }
111
+ }, 2000);
112
+ (timer as ReturnType<typeof setInterval> & { unref?: () => void }).unref?.();
90
113
  }
91
114
 
92
115
  // ── Lifecycle hooks ────────────────────────────────────────
@@ -98,29 +121,29 @@ export default function (pi: ExtensionAPI) {
98
121
  showDone(ctx);
99
122
  });
100
123
 
101
- pi.on("input", async (event, ctx) => {
124
+ pi.on("input", (event, ctx) => {
102
125
  if (event.source === "interactive") {
103
126
  startSpinner(ctx);
104
127
  }
105
128
  });
106
129
 
107
- pi.on("agent_start", async (_event, ctx) => {
130
+ pi.on("agent_start", (_event, ctx) => {
108
131
  // agent_start always fires after input for every user prompt;
109
132
  // backstop in case the input handler missed a non-interactive source.
110
133
  startSpinner(ctx);
111
134
  });
112
135
 
113
- pi.on("turn_start", async (_event, ctx) => {
136
+ pi.on("turn_start", (_event, ctx) => {
114
137
  // Multi-turn agent: keep spinner running between turns.
115
138
  startSpinner(ctx);
116
139
  });
117
140
 
118
- pi.on("agent_end", async (_event, ctx) => {
141
+ pi.on("agent_end", (_event, ctx) => {
119
142
  showDone(ctx);
120
143
  });
121
144
 
122
- pi.on("session_shutdown", async (_event, ctx) => {
145
+ pi.on("session_shutdown", (_event, ctx) => {
123
146
  stopSpinner();
124
147
  ctx.ui.setTitle(getBaseTitle(pi));
125
- })
148
+ });
126
149
  }