@rol1n/pi-kit 0.0.0-stage → 0.1.1

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 (120) hide show
  1. package/README.md +73 -2
  2. package/ask-u-questions/README.md +26 -0
  3. package/ask-u-questions/dialog-ui.ts +80 -0
  4. package/ask-u-questions/index.ts +92 -0
  5. package/ask-u-questions/multi-choice.ts +258 -0
  6. package/ask-u-questions/single-choice.ts +180 -0
  7. package/ask-u-questions/tool-renderers.ts +67 -0
  8. package/ask-u-questions/types.ts +192 -0
  9. package/continue-signal/README.md +20 -0
  10. package/continue-signal/index.ts +69 -0
  11. package/custom-providers/README.md +20 -0
  12. package/custom-providers/asr/handlers/dashscope.ts +154 -0
  13. package/custom-providers/asr/index.ts +37 -0
  14. package/custom-providers/asr/models.ts +18 -0
  15. package/custom-providers/asr/types.ts +20 -0
  16. package/custom-providers/chat/index.ts +33 -0
  17. package/custom-providers/chat/magpie.ts +258 -0
  18. package/custom-providers/chat/models.ts +111 -0
  19. package/custom-providers/classifier/index.ts +6 -0
  20. package/custom-providers/classifier/models.ts +1 -0
  21. package/custom-providers/image/handlers/dashscope.ts +240 -0
  22. package/custom-providers/image/handlers/openrouter.ts +185 -0
  23. package/custom-providers/image/index.ts +96 -0
  24. package/custom-providers/image/models.ts +71 -0
  25. package/custom-providers/image/types.ts +22 -0
  26. package/custom-providers/index.ts +64 -0
  27. package/custom-providers/types.ts +17 -0
  28. package/image-generate/README.md +25 -0
  29. package/image-generate/files.ts +49 -0
  30. package/image-generate/index.ts +220 -0
  31. package/image-generate/model-selector.ts +150 -0
  32. package/memory-log/README.md +54 -0
  33. package/memory-log/agent/consolidator/prompt.ts +47 -0
  34. package/memory-log/agent/consolidator/tools.ts +160 -0
  35. package/memory-log/agent/cost.ts +32 -0
  36. package/memory-log/agent/index.ts +42 -0
  37. package/memory-log/agent/observer/prompt.ts +73 -0
  38. package/memory-log/agent/observer/tool.ts +73 -0
  39. package/memory-log/commands/compact.ts +35 -0
  40. package/memory-log/commands/consolidate.ts +47 -0
  41. package/memory-log/commands/status.ts +50 -0
  42. package/memory-log/config.ts +253 -0
  43. package/memory-log/debug-log.ts +76 -0
  44. package/memory-log/hooks/compaction-hook.ts +161 -0
  45. package/memory-log/hooks/compaction-trigger.ts +160 -0
  46. package/memory-log/hooks/consolidator-trigger.ts +143 -0
  47. package/memory-log/hooks/observer-trigger.ts +176 -0
  48. package/memory-log/ids.ts +78 -0
  49. package/memory-log/index.ts +149 -0
  50. package/memory-log/ledger/fold.ts +77 -0
  51. package/memory-log/ledger/index.ts +7 -0
  52. package/memory-log/ledger/pool.ts +38 -0
  53. package/memory-log/ledger/progress.ts +181 -0
  54. package/memory-log/ledger/projection.ts +115 -0
  55. package/memory-log/ledger/render.ts +38 -0
  56. package/memory-log/ledger/serialize.ts +161 -0
  57. package/memory-log/ledger/types.ts +201 -0
  58. package/memory-log/memory/index-render.ts +45 -0
  59. package/memory-log/memory/paths.ts +120 -0
  60. package/memory-log/memory/session.ts +64 -0
  61. package/memory-log/runtime.ts +127 -0
  62. package/memory-log/spawn/launch.ts +133 -0
  63. package/memory-log/spawn/runs.ts +83 -0
  64. package/memory-log/tokens.ts +66 -0
  65. package/memory-log/ui/status-controller.ts +225 -0
  66. package/memory-log/ui/timeline.ts +103 -0
  67. package/model-vision/README.md +22 -0
  68. package/multi-harness/README.md +63 -0
  69. package/multi-harness/adapters.mjs +72 -0
  70. package/multi-harness/harness-done.mjs +14 -0
  71. package/multi-harness/index.ts +375 -0
  72. package/multi-harness/ipc.mjs +37 -0
  73. package/multi-harness/opencode-plugin.mjs +99 -0
  74. package/multi-harness/process-runner.mjs +115 -0
  75. package/multi-harness/turn-hook.mjs +29 -0
  76. package/multi-harness/worker-bridge.ts +52 -0
  77. package/oai-subquota/README.md +22 -0
  78. package/oai-subquota/index.ts +286 -0
  79. package/oai-subquota/snapshot.ts +293 -0
  80. package/oai-subquota/ui.ts +183 -0
  81. package/package.json +92 -4
  82. package/prompt-snipaste/README.md +27 -0
  83. package/prompt-snipaste/index.ts +143 -0
  84. package/prompt-snipaste/menu.ts +159 -0
  85. package/prompt-snipaste/prompts/ask-questions.md +8 -0
  86. package/prompt-snipaste/prompts/delegate-exploration.md +7 -0
  87. package/prompt-snipaste/prompts/diagnose-report.md +7 -0
  88. package/prompt-snipaste/prompts/frontend-tech-preset.md +18 -0
  89. package/prompt-snipaste/prompts/orchestrator-mode.md +7 -0
  90. package/prompt-snipaste/prompts/session-kickoff.md +7 -0
  91. package/prompt-snipaste/prompts/verify-not-assume.md +7 -0
  92. package/prompt-snipaste/renderers.ts +87 -0
  93. package/prompt-snipaste/snippets.ts +206 -0
  94. package/prompt-snipaste/types.ts +18 -0
  95. package/screenshot/README.md +14 -0
  96. package/srp-theme/README.md +18 -0
  97. package/srp-theme/footer.ts +193 -0
  98. package/srp-theme/index.ts +172 -0
  99. package/srp-theme/tps-meter.ts +441 -0
  100. package/subagent/README.md +35 -0
  101. package/subagent/bridge.ts +171 -0
  102. package/subagent/index.ts +590 -0
  103. package/subagent/mux.ts +621 -0
  104. package/subagent/session.ts +166 -0
  105. package/subagent/ui.ts +100 -0
  106. package/tui-asr/README.md +21 -0
  107. package/tui-asr/dictation.ts +272 -0
  108. package/tui-asr/index.ts +181 -0
  109. package/tui-asr/input.ts +87 -0
  110. package/tui-asr/model-selector.ts +163 -0
  111. package/tui-asr/status.ts +79 -0
  112. package/web-reach/README.md +19 -0
  113. package/web-reach/html.ts +83 -0
  114. package/web-reach/index.ts +82 -0
  115. package/web-reach/page.ts +214 -0
  116. package/web-reach/rsc.ts +161 -0
  117. package/web-reach/search.ts +85 -0
  118. package/web-reach/transport.ts +35 -0
  119. package/windows-wheel-fix/README.md +12 -0
  120. package/windows-wheel-fix/index.ts +44 -0
@@ -0,0 +1,172 @@
1
+ /**
2
+ * srp-theme — Prompt preview and TPS meter extension.
3
+ *
4
+ * Scheme A (Pure Incremental Mode):
5
+ * Leaves Pi's native Header and Footer untouched so official features
6
+ * (CH cache hit rate, W cache write, auto-compaction indicator, routed models, etc.)
7
+ * work out-of-the-box without maintenance drift.
8
+ *
9
+ * Adds two lightweight incremental UI enhancements:
10
+ * 1. belowEditor widget: Displays the last user prompt in powerline style (↳ <prompt>).
11
+ * 2. Status line item: Real-time streaming TPS meter and sparkline summary.
12
+ */
13
+
14
+ import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
15
+ import type { AutocompleteItem } from "@earendil-works/pi-tui";
16
+ import { getLastLoopInfoFromSession, renderLastPromptLine, renderTimeAndFinishedLine } from "./footer.ts";
17
+ import { safeFallbackTheme, TpsMeter } from "./tps-meter.ts";
18
+
19
+ export { compactPrompt, renderLastPromptLine } from "./footer.ts";
20
+ export { TpsMeter } from "./tps-meter.ts";
21
+
22
+ export default function (pi: ExtensionAPI) {
23
+ let lastUserPrompt = "";
24
+ let footerEnabled = true;
25
+ let lastLoopEndTime: Date | null = null;
26
+ let lastLoopDurationMs: number | null = null;
27
+ let currentLoopStartMs = 0;
28
+ const tpsMeter = new TpsMeter();
29
+
30
+ const installFooter = (ctx: ExtensionContext): void => {
31
+ // Keep Pi's native footer active; this widget is purely incremental.
32
+ ctx.ui.setFooter(undefined);
33
+ ctx.ui.setWidget(
34
+ "srp-footer",
35
+ (_tui, theme) => {
36
+ return {
37
+ invalidate() {},
38
+ dispose() {},
39
+ render(width: number): string[] {
40
+ return [
41
+ ...renderLastPromptLine(lastUserPrompt, width),
42
+ ...renderTimeAndFinishedLine(lastLoopEndTime, lastLoopDurationMs, width, theme),
43
+ ];
44
+ },
45
+ };
46
+ },
47
+ { placement: "belowEditor" },
48
+ );
49
+ };
50
+
51
+ const removeFooter = (ctx: ExtensionContext): void => {
52
+ ctx.ui.setWidget("srp-footer", undefined);
53
+ ctx.ui.setFooter(undefined);
54
+ };
55
+
56
+ pi.on("session_start", (_event, ctx) => {
57
+ lastUserPrompt = "";
58
+ currentLoopStartMs = 0;
59
+ const info = getLastLoopInfoFromSession(ctx);
60
+ lastLoopEndTime = info?.endTime ?? null;
61
+ lastLoopDurationMs = info?.durationMs ?? null;
62
+ footerEnabled = true;
63
+ tpsMeter.enabled = true;
64
+ tpsMeter.reset(ctx);
65
+
66
+ if (ctx.mode === "tui") {
67
+ // Restore native header & footer
68
+ ctx.ui.setHeader(undefined);
69
+ ctx.ui.setFooter(undefined);
70
+ removeFooter(ctx);
71
+ }
72
+ });
73
+
74
+ // Schedule resources_discover after session_start to position widget below powerline widgets
75
+ pi.on("resources_discover", (_event, ctx) => {
76
+ if (footerEnabled && ctx.mode === "tui") {
77
+ installFooter(ctx);
78
+ }
79
+ });
80
+
81
+ // event.prompt is expanded user input. It is absent for custom-message runs
82
+ // (messageType === "custom"), e.g. extension-triggered auto-continue; keep the previous prompt then.
83
+ pi.on("before_agent_start", (event) => {
84
+ if (event.messageType === "user" && typeof event.prompt === "string") {
85
+ lastUserPrompt = event.prompt;
86
+ }
87
+ currentLoopStartMs = Date.now();
88
+ });
89
+
90
+ // TPS monitoring and lifecycle hooks
91
+ pi.on("message_start", (event, ctx) => {
92
+ if (event.message.role !== "assistant") return;
93
+ tpsMeter.onMessageStart(ctx);
94
+ });
95
+
96
+ pi.on("message_update", (event) => {
97
+ if (event.message.role !== "assistant") return;
98
+ tpsMeter.onMessageUpdate(event.assistantMessageEvent as { type: string; delta?: unknown });
99
+ });
100
+
101
+ pi.on("message_end", (event, ctx) => {
102
+ if (event.message.role !== "assistant") return;
103
+ const realOut = (event.message as { usage?: { output?: number } })?.usage?.output;
104
+ tpsMeter.onMessageEnd(realOut, ctx);
105
+ });
106
+
107
+ pi.on("tool_execution_start", (_event, ctx) => {
108
+ tpsMeter.onToolStart(ctx);
109
+ });
110
+
111
+ pi.on("tool_call", (_event, ctx) => {
112
+ tpsMeter.onToolStart(ctx);
113
+ });
114
+
115
+ pi.on("turn_end", (_event, ctx) => {
116
+ tpsMeter.onTurnEnd(ctx);
117
+ });
118
+
119
+ pi.on("agent_end", (_event, ctx) => {
120
+ lastLoopEndTime = new Date();
121
+ if (currentLoopStartMs > 0) {
122
+ lastLoopDurationMs = Math.max(0, Date.now() - currentLoopStartMs);
123
+ }
124
+ tpsMeter.onAgentEnd(ctx);
125
+ });
126
+
127
+ pi.on("agent_settled", (_event, ctx) => {
128
+ lastLoopEndTime = new Date();
129
+ if (currentLoopStartMs > 0) {
130
+ lastLoopDurationMs = Math.max(0, Date.now() - currentLoopStartMs);
131
+ }
132
+ tpsMeter.onAgentSettled(ctx);
133
+ });
134
+
135
+ pi.registerCommand("srp-theme", {
136
+ description: "Toggle SRP prompt widget and TPS meter on/off",
137
+ getArgumentCompletions: (prefix: string): AutocompleteItem[] | null => {
138
+ const candidates: AutocompleteItem[] = [
139
+ { value: "on", label: "on", description: "Enable prompt preview and TPS meter" },
140
+ { value: "off", label: "off", description: "Disable prompt preview and TPS meter" },
141
+ ];
142
+ const filtered = candidates.filter((item) => item.value.startsWith(prefix.trimStart()));
143
+ return filtered.length > 0 ? filtered : null;
144
+ },
145
+ handler: async (args, ctx) => {
146
+ const state = args.trim().toLowerCase();
147
+ if (state !== "on" && state !== "off") {
148
+ ctx.ui.notify("Usage: /srp-theme on|off", "info");
149
+ return;
150
+ }
151
+
152
+ const enabled = state === "on";
153
+ footerEnabled = enabled;
154
+ tpsMeter.enabled = enabled;
155
+
156
+ if (ctx.mode === "tui") {
157
+ ctx.ui.setFooter(undefined);
158
+ if (enabled) installFooter(ctx);
159
+ else removeFooter(ctx);
160
+ }
161
+
162
+ if (enabled) {
163
+ const theme = ctx.ui?.theme ?? safeFallbackTheme;
164
+ ctx.ui.setStatus("tps", tpsMeter.renderFinal(theme));
165
+ } else {
166
+ tpsMeter.stopTick();
167
+ ctx.ui.setStatus("tps", undefined);
168
+ }
169
+ ctx.ui.notify(`srp-theme: ${enabled ? "Enabled" : "Disabled"} prompt widget and TPS`, "info");
170
+ },
171
+ });
172
+ }
@@ -0,0 +1,441 @@
1
+ import type { ExtensionContext, Theme } from "@earendil-works/pi-coding-agent";
2
+ import { visibleWidth } from "@earendil-works/pi-tui";
3
+
4
+ export class TpsMeter {
5
+ // Constants
6
+ static readonly WINDOW_SIZE = 60;
7
+ static readonly WINDOW_MS = 60_000;
8
+ static readonly STREAM_INTERVAL_MS = 200;
9
+ static readonly SPARK_LEN = 12;
10
+ static readonly ALLTIME_CAP = 500;
11
+ static readonly FAST = 50;
12
+ static readonly MED = 20;
13
+
14
+ // Render glyphs
15
+ static readonly BLOCKS = ["▁", "▂", "▃", "▄", "▅", "▆", "▇", "█"];
16
+ static readonly HBLOCKS = [" ", "▏", "▎", "▍", "▌", "▋", "▊", "▉"];
17
+ static readonly GAUGE_LEN = 11;
18
+ static readonly GAUGE_FLOOR = 40;
19
+ static readonly TRACK = "·";
20
+ static readonly SPIN = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"];
21
+
22
+ // State variables
23
+ private streamStartMs = 0;
24
+ private firstTokenMs = 0;
25
+ private streamChars = 0;
26
+ private streamTokens = 0;
27
+ private tickTimer: ReturnType<typeof setInterval> | null = null;
28
+ private streaming = false;
29
+
30
+ // 60-second rolling window (ring buffer: [tps, timestamp])
31
+ private readonly winBuf = new Float64Array(TpsMeter.WINDOW_SIZE * 2);
32
+ private winLen = 0;
33
+ private winHead = 0;
34
+
35
+ // Session-wide sampling (ring buffer: [tps])
36
+ private readonly atBuf = new Float64Array(TpsMeter.ALLTIME_CAP);
37
+ private atLen = 0;
38
+ private atHead = 0;
39
+ private atSum = 0;
40
+
41
+ // Sparkline history for last 12 messages
42
+ private readonly sparkBuf = new Float64Array(TpsMeter.SPARK_LEN);
43
+ private sparkLen = 0;
44
+ private sparkHead = 0;
45
+ private sparkMax = 1;
46
+ private sparkCache = "";
47
+ private sparkDirty = true;
48
+ private sparkTheme: Theme | null = null;
49
+ private spinI = 0;
50
+ private lastStatusText: string | undefined = undefined;
51
+
52
+ public enabled = true;
53
+
54
+ private applyStatus(ctx: ExtensionContext, text: string | undefined): void {
55
+ if (this.lastStatusText === text) return;
56
+ this.lastStatusText = text;
57
+ if (ctx.ui?.setStatus) {
58
+ ctx.ui.setStatus("tps", text);
59
+ }
60
+ }
61
+
62
+ reset(ctx?: ExtensionContext): void {
63
+ this.streaming = false;
64
+ this.stopTick();
65
+ this.streamStartMs = 0;
66
+ this.firstTokenMs = 0;
67
+ this.streamChars = 0;
68
+ this.streamTokens = 0;
69
+ this.winLen = 0;
70
+ this.winHead = 0;
71
+ this.atLen = 0;
72
+ this.atHead = 0;
73
+ this.atSum = 0;
74
+ this.sparkLen = 0;
75
+ this.sparkHead = 0;
76
+ this.sparkMax = 1;
77
+ this.sparkCache = "";
78
+ this.sparkDirty = true;
79
+ this.sparkTheme = null;
80
+ this.spinI = 0;
81
+ this.lastStatusText = undefined;
82
+ if (ctx) {
83
+ if (this.enabled) {
84
+ const theme = ctx.ui?.theme ?? safeFallbackTheme;
85
+ this.applyStatus(ctx, this.renderFinal(theme));
86
+ } else {
87
+ this.applyStatus(ctx, undefined);
88
+ }
89
+ }
90
+ }
91
+
92
+ private now(): number {
93
+ return Date.now();
94
+ }
95
+
96
+ private tokEst(ch: number): number {
97
+ return (ch >>> 2) + ((ch & 3) > 0 ? 1 : 0);
98
+ }
99
+
100
+ private winPush(tps: number, ms: number): void {
101
+ const b = this.winHead * 2;
102
+ this.winBuf[b] = tps;
103
+ this.winBuf[b + 1] = ms;
104
+ this.winHead = (this.winHead + 1) % TpsMeter.WINDOW_SIZE;
105
+ if (this.winLen < TpsMeter.WINDOW_SIZE) this.winLen++;
106
+ }
107
+
108
+ private atPush(tps: number): void {
109
+ this.atSum += tps;
110
+ if (this.atLen >= TpsMeter.ALLTIME_CAP) this.atSum -= this.atBuf[this.atHead];
111
+ this.atBuf[this.atHead] = tps;
112
+ this.atHead = (this.atHead + 1) % TpsMeter.ALLTIME_CAP;
113
+ if (this.atLen < TpsMeter.ALLTIME_CAP) this.atLen++;
114
+ }
115
+
116
+ private sparkPush(tps: number): void {
117
+ this.sparkBuf[this.sparkHead] = tps;
118
+ this.sparkHead = (this.sparkHead + 1) % TpsMeter.SPARK_LEN;
119
+ if (this.sparkLen < TpsMeter.SPARK_LEN) this.sparkLen++;
120
+ if (tps > this.sparkMax) this.sparkMax = tps;
121
+ if (this.sparkMax > 10) this.sparkMax *= 0.99;
122
+ this.sparkDirty = true;
123
+ }
124
+
125
+ winAvg(): number {
126
+ if (this.winLen === 0) return 0;
127
+ const cutoff = this.now() - TpsMeter.WINDOW_MS;
128
+ let sum = 0;
129
+ let n = 0;
130
+ const oldest = this.winLen < TpsMeter.WINDOW_SIZE ? 0 : this.winHead;
131
+ for (let i = 0; i < this.winLen; i++) {
132
+ const idx = (oldest + i) % TpsMeter.WINDOW_SIZE;
133
+ const b = idx * 2;
134
+ if (this.winBuf[b + 1] < cutoff) continue;
135
+ sum += this.winBuf[b];
136
+ n++;
137
+ }
138
+ return n === 0 ? 0 : sum / n;
139
+ }
140
+
141
+ atMean(): number {
142
+ return this.atLen === 0 ? 0 : this.atSum / this.atLen;
143
+ }
144
+
145
+ atP95(): number {
146
+ if (this.atLen === 0) return 0;
147
+ const tmp = new Float64Array(this.atLen);
148
+ const oldest = this.atLen < TpsMeter.ALLTIME_CAP ? 0 : this.atHead;
149
+ for (let i = 0; i < this.atLen; i++) tmp[i] = this.atBuf[(oldest + i) % TpsMeter.ALLTIME_CAP];
150
+ // Insertion sort
151
+ for (let i = 1; i < tmp.length; i++) {
152
+ const v = tmp[i];
153
+ let j = i - 1;
154
+ while (j >= 0 && tmp[j] > v) {
155
+ tmp[j + 1] = tmp[j];
156
+ j--;
157
+ }
158
+ tmp[j + 1] = v;
159
+ }
160
+ return tmp[Math.ceil(tmp.length * 0.95) - 1] || 0;
161
+ }
162
+
163
+ private fmt(v: number): string {
164
+ if (v < 10) return v.toFixed(1);
165
+ if (v < 100) return v.toFixed(0);
166
+ return `${Math.round(v)}`;
167
+ }
168
+
169
+ private speedColor(tps: number, text: string, theme: Theme): string {
170
+ if (tps >= TpsMeter.FAST) return theme.fg("success", text);
171
+ if (tps >= TpsMeter.MED) return theme.fg("warning", text);
172
+ if (tps === 0) return theme.fg("dim", text);
173
+ return theme.fg("error", text);
174
+ }
175
+
176
+ private spin(): string {
177
+ const s = TpsMeter.SPIN[this.spinI];
178
+ this.spinI = (this.spinI + 1) % TpsMeter.SPIN.length;
179
+ return s;
180
+ }
181
+
182
+ sparkline(theme: Theme, len = TpsMeter.SPARK_LEN): string {
183
+ len = Math.max(1, Math.min(TpsMeter.SPARK_LEN, Math.floor(len)));
184
+
185
+ if (this.sparkLen === 0) {
186
+ return theme.fg("dim", TpsMeter.TRACK.repeat(len));
187
+ }
188
+
189
+ const available = Math.min(this.sparkLen, len);
190
+ const vals = new Float64Array(available);
191
+ for (let i = 0; i < available; i++) {
192
+ const idx =
193
+ (this.sparkHead - available + i + TpsMeter.SPARK_LEN) %
194
+ TpsMeter.SPARK_LEN;
195
+ vals[i] = this.sparkBuf[idx];
196
+ }
197
+
198
+ let mn = Infinity;
199
+ let mx = 0;
200
+ for (let i = 0; i < available; i++) {
201
+ const v = vals[i];
202
+ if (v < mn) mn = v;
203
+ if (v > mx) mx = v;
204
+ }
205
+ const range = mx - mn;
206
+ const pad = len - available;
207
+ let result = pad > 0 ? theme.fg("dim", TpsMeter.TRACK.repeat(pad)) : "";
208
+
209
+ for (let i = 0; i < available; i++) {
210
+ const v = vals[i];
211
+ const norm =
212
+ range < 1e-6
213
+ ? mx > 0
214
+ ? 4
215
+ : 0
216
+ : Math.min(7, Math.max(0, Math.round(((v - mn) / range) * 7)));
217
+ const ch = TpsMeter.BLOCKS[norm];
218
+ result += this.speedColor(v, ch, theme);
219
+ }
220
+ return result;
221
+ }
222
+
223
+ gauge(tps: number, theme: Theme, gaugeLen = TpsMeter.GAUGE_LEN): string {
224
+ gaugeLen = Math.max(1, Math.floor(gaugeLen));
225
+ const scale = Math.max(this.sparkMax, TpsMeter.GAUGE_FLOOR);
226
+ let frac = scale > 0 ? tps / scale : 0;
227
+ if (frac < 0) frac = 0;
228
+ if (frac > 1) frac = 1;
229
+
230
+ const eighths = Math.round(frac * gaugeLen * 8);
231
+ const full = (eighths / 8) | 0;
232
+ const rem = eighths % 8;
233
+
234
+ let fill = "█".repeat(Math.min(gaugeLen, full));
235
+ let used = Math.min(gaugeLen, full);
236
+ if (full < gaugeLen && rem > 0) {
237
+ fill += TpsMeter.HBLOCKS[rem];
238
+ used++;
239
+ }
240
+ const track = TpsMeter.TRACK.repeat(Math.max(0, gaugeLen - used));
241
+
242
+ return (
243
+ theme.fg("dim", "▕") +
244
+ this.speedColor(tps, fill, theme) +
245
+ theme.fg("dim", track + "▏")
246
+ );
247
+ }
248
+
249
+ isStreaming(): boolean {
250
+ return this.streaming;
251
+ }
252
+
253
+ renderLive(theme: Theme, maxBudget?: number): string {
254
+ const ref = this.firstTokenMs > 0 ? this.firstTokenMs : this.streamStartMs;
255
+ const elapsed = (this.now() - ref) / 1000;
256
+ const tps = elapsed > 0.3 ? this.streamTokens / elapsed : 0;
257
+
258
+ const s = theme.fg("accent", this.spin());
259
+ const num = this.speedColor(tps, this.fmt(tps), theme);
260
+ const unit = theme.fg("dim", "tps");
261
+ const numUnit = `${num} ${unit}`;
262
+ const minCompact = `${s} ${numUnit}`;
263
+ const bareNumUnit = numUnit;
264
+
265
+ if (maxBudget == null) {
266
+ const g = this.gauge(tps, theme, TpsMeter.GAUGE_LEN);
267
+ return `${s} ${g} ${numUnit}`;
268
+ }
269
+
270
+ if (maxBudget < visibleWidth(bareNumUnit)) {
271
+ return "";
272
+ }
273
+
274
+ // Attempt full mode with gauge
275
+ const fixedOverhead = visibleWidth(s) + 1 + 2 + 1 + visibleWidth(numUnit);
276
+ const availableGaugeLen = maxBudget - fixedOverhead;
277
+
278
+ if (availableGaugeLen >= 3) {
279
+ const targetGaugeLen = Math.min(TpsMeter.GAUGE_LEN, availableGaugeLen);
280
+ const g = this.gauge(tps, theme, targetGaugeLen);
281
+ const fullStr = `${s} ${g} ${numUnit}`;
282
+ if (visibleWidth(fullStr) <= maxBudget) {
283
+ return fullStr;
284
+ }
285
+ }
286
+
287
+ if (maxBudget >= visibleWidth(minCompact)) {
288
+ return minCompact;
289
+ }
290
+
291
+ return bareNumUnit;
292
+ }
293
+
294
+ renderFinal(theme: Theme, maxBudget?: number): string {
295
+ const avg = this.winAvg();
296
+ const mu = this.atMean();
297
+ const p95 = this.atP95();
298
+
299
+ const a = this.speedColor(avg, this.fmt(avg), theme);
300
+ const label = theme.fg("dim", "tps");
301
+ const numUnit = `${a} ${label}`;
302
+
303
+ if (maxBudget == null) {
304
+ const sp = this.sparkline(theme, TpsMeter.SPARK_LEN);
305
+ const sep = theme.fg("dim", "·");
306
+ const m = `${theme.fg("dim", "μ")} ${this.speedColor(mu, this.fmt(mu), theme)}`;
307
+ const p = `${theme.fg("dim", "p95")} ${this.speedColor(p95, this.fmt(p95), theme)}`;
308
+ return `${sp} ${numUnit} ${sep} ${m} ${sep} ${p}`;
309
+ }
310
+
311
+ if (maxBudget < visibleWidth(numUnit)) {
312
+ return "";
313
+ }
314
+
315
+ // 1. Attempt full mode (sparkline 12 + numUnit + μ + p95)
316
+ const spFull = this.sparkline(theme, TpsMeter.SPARK_LEN);
317
+ const sep = theme.fg("dim", "·");
318
+ const m = `${theme.fg("dim", "μ")} ${this.speedColor(mu, this.fmt(mu), theme)}`;
319
+ const p = `${theme.fg("dim", "p95")} ${this.speedColor(p95, this.fmt(p95), theme)}`;
320
+ const l3 = `${spFull} ${numUnit} ${sep} ${m} ${sep} ${p}`;
321
+ if (visibleWidth(l3) <= maxBudget) {
322
+ return l3;
323
+ }
324
+
325
+ // 2. Dynamic sparkline (length 12 down to 2)
326
+ const fixedOverhead = 1 + visibleWidth(numUnit);
327
+ const availableSparkLen = maxBudget - fixedOverhead;
328
+
329
+ if (availableSparkLen >= 2) {
330
+ const targetSparkLen = Math.min(TpsMeter.SPARK_LEN, availableSparkLen);
331
+ const sp = this.sparkline(theme, targetSparkLen);
332
+ const l2 = `${sp} ${numUnit}`;
333
+ if (visibleWidth(l2) <= maxBudget) {
334
+ return l2;
335
+ }
336
+ }
337
+
338
+ // 3. Bare numUnit fallback
339
+ return numUnit;
340
+ }
341
+
342
+ renderAdaptive(theme: Theme, maxBudget: number): string {
343
+ if (!this.enabled) return "";
344
+ if (this.streaming) {
345
+ return this.renderLive(theme, maxBudget);
346
+ }
347
+ return this.renderFinal(theme, maxBudget);
348
+ }
349
+
350
+ private startTick(ctx: ExtensionContext): void {
351
+ if (this.tickTimer) return;
352
+ this.tickTimer = setInterval(() => {
353
+ if (!this.streaming || !this.enabled) {
354
+ this.stopTick(ctx);
355
+ return;
356
+ }
357
+ const theme = ctx.ui?.theme ?? safeFallbackTheme;
358
+ this.applyStatus(ctx, this.renderLive(theme));
359
+ }, TpsMeter.STREAM_INTERVAL_MS);
360
+ (this.tickTimer as any)?.unref?.();
361
+ }
362
+
363
+ stopTick(ctx?: ExtensionContext): void {
364
+ this.streaming = false;
365
+ if (this.tickTimer) {
366
+ clearInterval(this.tickTimer);
367
+ this.tickTimer = null;
368
+ }
369
+ if (ctx && this.enabled) {
370
+ const theme = ctx.ui?.theme ?? safeFallbackTheme;
371
+ this.applyStatus(ctx, this.renderFinal(theme));
372
+ }
373
+ }
374
+
375
+ onMessageStart(ctx: ExtensionContext): void {
376
+ if (!this.enabled) return;
377
+ this.streamStartMs = this.now();
378
+ this.firstTokenMs = 0;
379
+ this.streamChars = 0;
380
+ this.streamTokens = 0;
381
+ this.streaming = true;
382
+ this.spinI = 0;
383
+ this.startTick(ctx);
384
+ }
385
+
386
+ onMessageUpdate(evt?: { type: string; delta?: unknown }): void {
387
+ if (!this.enabled || !evt) return;
388
+ if (evt.type === "text_delta" || evt.type === "thinking_delta") {
389
+ const d = evt.delta as string;
390
+ if (!d) return;
391
+ if (this.firstTokenMs === 0) this.firstTokenMs = this.now();
392
+ this.streamChars += d.length;
393
+ this.streamTokens = this.tokEst(this.streamChars);
394
+ }
395
+ }
396
+
397
+ onMessageEnd(usageOutput: number | undefined, ctx: ExtensionContext): void {
398
+ if (!this.enabled) return;
399
+ this.streaming = false;
400
+ this.stopTick();
401
+
402
+ const realOut = usageOutput;
403
+ const tokens =
404
+ typeof realOut === "number" && realOut > 0 ? realOut : this.streamTokens;
405
+
406
+ const ref = this.firstTokenMs > 0 ? this.firstTokenMs : this.streamStartMs;
407
+ const elapsed = (this.now() - ref) / 1000;
408
+ if (elapsed >= 0.1 && tokens > 0) {
409
+ const tps = tokens / elapsed;
410
+ this.winPush(tps, this.now());
411
+ this.atPush(tps);
412
+ this.sparkPush(tps);
413
+ }
414
+
415
+ const theme = ctx.ui?.theme ?? safeFallbackTheme;
416
+ const txt = this.renderFinal(theme);
417
+ this.applyStatus(ctx, txt || undefined);
418
+ }
419
+
420
+ onToolStart(ctx: ExtensionContext): void {
421
+ this.stopTick(ctx);
422
+ }
423
+
424
+ onTurnEnd(ctx?: ExtensionContext): void {
425
+ this.stopTick(ctx);
426
+ }
427
+
428
+ onAgentEnd(ctx?: ExtensionContext): void {
429
+ this.stopTick(ctx);
430
+ }
431
+
432
+ onAgentSettled(ctx?: ExtensionContext): void {
433
+ this.stopTick(ctx);
434
+ }
435
+ }
436
+
437
+ export const safeFallbackTheme: Theme = {
438
+ fg: (_color: string, text: string) => text,
439
+ bg: (_color: string, text: string) => text,
440
+ bold: (text: string) => text,
441
+ } as unknown as Theme;
@@ -0,0 +1,35 @@
1
+ # subagent
2
+
3
+ Delegate tasks to independent, full-powered Pi workers. The `subagent` tool returns after launch; completion or questions are delivered back to the parent session asynchronously.
4
+
5
+ ## Install
6
+
7
+ ```sh
8
+ pi install npm:@rol1n/subagent
9
+ ```
10
+
11
+ Also included in `@rol1n/pi-kit`. The parent and workers require the `pi` executable on PATH and their normal Pi authentication.
12
+
13
+ ## Use
14
+
15
+ ```json
16
+ {"action":"spawn","name":"reviewer","cwd":"/existing/project","task":"Review the changed files and report findings."}
17
+ {"action":"send","target":"reviewer","task":"Also inspect the error handling."}
18
+ {"action":"stop","target":"reviewer"}
19
+ ```
20
+
21
+ `model` optionally selects a worker model. `/subagent` shows active workers. Each worker inherits its own normal Pi capabilities; this is a delegation runtime, not a restricted execution environment.
22
+
23
+ ## Backends
24
+
25
+ | Variable | Behavior |
26
+ |---|---|
27
+ | `PI_SUBAGENT_BACKEND=process` | Run silent background processes |
28
+ | `PI_SUBAGENT_MUX=tmux` | Prefer tmux when running inside an available tmux session |
29
+ | `PI_SUBAGENT_MUX=zellij` | Prefer Zellij when running inside an available Zellij session |
30
+
31
+ Without a usable multiplexer, the worker backend falls back to a background process. Native Windows supports PowerShell launch commands and Zellij panes; tmux is for POSIX environments. Session transcripts and task artifacts are stored under `~/.pi/agent/`.
32
+
33
+ ## Other delegation packages
34
+
35
+ Use [multi-harness](https://github.com/RolinShmily/pi-kit/tree/main/multi-harness) for Codex, Claude, or OpenCode peers. That package shares multiplexer helpers (`mux.ts`) while exposing a separate tool.