@selesai/code 0.5.21 → 0.5.23
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 +1 -1
- package/dist/core/agent-session-skill-block.test.d.ts +2 -0
- package/dist/core/agent-session-skill-block.test.d.ts.map +1 -0
- package/dist/core/agent-session-skill-block.test.js +20 -0
- package/dist/core/agent-session-skill-block.test.js.map +1 -0
- package/dist/core/agent-session.d.ts +7 -4
- package/dist/core/agent-session.d.ts.map +1 -1
- package/dist/core/agent-session.js +22 -12
- package/dist/core/agent-session.js.map +1 -1
- package/dist/core/skills.d.ts.map +1 -1
- package/dist/core/skills.js +1 -1
- package/dist/core/skills.js.map +1 -1
- package/dist/data/capabilities.d.ts +48 -0
- package/dist/data/capabilities.d.ts.map +1 -0
- package/dist/data/capabilities.js +282 -0
- package/dist/data/capabilities.js.map +1 -0
- package/dist/data/navigation.d.ts +3 -0
- package/dist/data/navigation.d.ts.map +1 -0
- package/dist/data/navigation.js +90 -0
- package/dist/data/navigation.js.map +1 -0
- package/dist/extensions/inline-skills.test.ts +96 -0
- package/dist/extensions/inline-skills.ts +92 -0
- package/dist/extensions/package.json +1 -0
- package/dist/extensions/pi-powerline-footer/tests/tps.test.ts +39 -0
- package/dist/extensions/pi-powerline-footer/tps.ts +172 -113
- package/dist/extensions/pi-subagents/src/tui/fleet.ts +0 -4
- package/dist/extensions/pi-subagents/test/integration/slash-commands.test.ts +1 -1
- package/dist/extensions/pi-subagents/test/unit/fleet.test.ts +1 -1
- package/dist/extensions/question/batch.ts +89 -25
- package/dist/extensions/question/constants.ts +1 -1
- package/dist/extensions/question/helpers.ts +34 -78
- package/dist/extensions/question/index.ts +327 -564
- package/dist/extensions/question/package.json +2 -1
- package/dist/extensions/question/question-list.ts +21 -14
- package/dist/extensions/question/row-layout.ts +2 -2
- package/dist/extensions/question/schemas.ts +33 -47
- package/dist/extensions/question/selection-mode.ts +5 -7
- package/dist/extensions/question/tests/batch.test.ts +65 -16
- package/dist/extensions/question/tests/helpers.test.ts +31 -156
- package/dist/extensions/question/tests/question-list.test.ts +17 -3
- package/dist/extensions/question/tests/row-layout.test.ts +4 -4
- package/dist/extensions/question/tests/schemas.test.ts +30 -0
- package/dist/extensions/question/tests/wizard.test.ts +99 -0
- package/dist/extensions/question/types.ts +65 -66
- package/dist/extensions/workflow/adapter.ts +20 -77
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/dist/modes/interactive/interactive-mode.d.ts.map +1 -1
- package/dist/modes/interactive/interactive-mode.js +10 -10
- package/dist/modes/interactive/interactive-mode.js.map +1 -1
- package/dist/skills/workflow-creation/SKILL.md +2 -2
- package/docs/skills.md +8 -0
- package/docs/workflows.md +4 -4
- package/package.json +1 -1
|
@@ -1,39 +1,98 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* TPS Tracker — tokens-per-second tracking merged into the powerline footer.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
* Tracks:
|
|
9
|
-
* - Main model output tokens vs. streaming wall time (excludes first-token
|
|
10
|
-
* latency and tool execution gaps).
|
|
11
|
-
* - pi-subagents child output tokens via tool_execution_* events, aggregated
|
|
12
|
-
* into a combined "main + subagents" TPS at agent_end.
|
|
4
|
+
* Live status remains intentionally approximate. Final main-model TPS uses
|
|
5
|
+
* provider output usage plus pi-tps' reliability, stall, and tool-call gates.
|
|
6
|
+
* Subagent UI/aggregation remains separate.
|
|
13
7
|
*/
|
|
14
8
|
|
|
15
9
|
import type { ExtensionAPI } from "@selesai/code";
|
|
16
10
|
|
|
11
|
+
const STALL_THRESHOLD_MS = 500;
|
|
12
|
+
const MAX_PLAUSIBLE_TPS = 10_000;
|
|
13
|
+
const MIN_STREAM_MS = 1;
|
|
14
|
+
const MIN_STREAM_UPDATES = 5;
|
|
15
|
+
const MIN_INTER_CHUNK_MS = 1;
|
|
16
|
+
const MIN_GENERATION_MS = 200;
|
|
17
|
+
const ACTIVE_TIME_THRESHOLD_MS = 200;
|
|
18
|
+
const STALL_REDUCTION_DENOM = 2;
|
|
19
|
+
const STALL_DOMINANCE_RATIO = 0.85;
|
|
20
|
+
|
|
21
|
+
export interface TpsTiming {
|
|
22
|
+
messageStartMs: number;
|
|
23
|
+
lastUpdateMs: number;
|
|
24
|
+
firstTokenMs: number | null;
|
|
25
|
+
updateCount: number;
|
|
26
|
+
firstStreamUpdateMs: number | null;
|
|
27
|
+
lastStreamUpdateMs: number;
|
|
28
|
+
stallMs: number;
|
|
29
|
+
generationMs: number;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export interface ReliableTps {
|
|
33
|
+
tps: number;
|
|
34
|
+
effectiveMs: number;
|
|
35
|
+
isPrimary: boolean;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/** Port of pi-tps' primary/fallback reliability and volume gates. */
|
|
39
|
+
export function calculateReliableTps(outputTokens: number, timing: TpsTiming): ReliableTps | null {
|
|
40
|
+
if (outputTokens <= 0 || timing.firstTokenMs === null) return null;
|
|
41
|
+
|
|
42
|
+
const streamMs = timing.updateCount > 0 && timing.firstStreamUpdateMs !== null
|
|
43
|
+
? timing.lastStreamUpdateMs - timing.firstStreamUpdateMs
|
|
44
|
+
: null;
|
|
45
|
+
const avgInterChunkGap = streamMs !== null && timing.updateCount > 1
|
|
46
|
+
? streamMs / (timing.updateCount - 1)
|
|
47
|
+
: 0;
|
|
48
|
+
|
|
49
|
+
let tps: number | null = null;
|
|
50
|
+
let effectiveMs = 0;
|
|
51
|
+
let isPrimary = false;
|
|
52
|
+
if (
|
|
53
|
+
streamMs !== null &&
|
|
54
|
+
streamMs >= MIN_STREAM_MS &&
|
|
55
|
+
timing.updateCount >= MIN_STREAM_UPDATES &&
|
|
56
|
+
avgInterChunkGap >= MIN_INTER_CHUNK_MS &&
|
|
57
|
+
timing.stallMs < streamMs &&
|
|
58
|
+
streamMs - timing.stallMs >= MIN_GENERATION_MS &&
|
|
59
|
+
timing.stallMs < streamMs - timing.stallMs
|
|
60
|
+
) {
|
|
61
|
+
effectiveMs = streamMs - timing.stallMs;
|
|
62
|
+
tps = Math.round((outputTokens / (effectiveMs / 1000)) * 10) / 10;
|
|
63
|
+
isPrimary = true;
|
|
64
|
+
} else if (timing.updateCount >= 2 && timing.generationMs >= MIN_GENERATION_MS) {
|
|
65
|
+
const stallsDominate =
|
|
66
|
+
timing.generationMs - timing.stallMs < ACTIVE_TIME_THRESHOLD_MS ||
|
|
67
|
+
timing.stallMs > timing.generationMs * STALL_DOMINANCE_RATIO;
|
|
68
|
+
effectiveMs = stallsDominate
|
|
69
|
+
? Math.max(timing.generationMs - timing.stallMs / STALL_REDUCTION_DENOM, MIN_GENERATION_MS)
|
|
70
|
+
: Math.max(timing.generationMs - timing.stallMs, MIN_GENERATION_MS);
|
|
71
|
+
tps = Math.round((outputTokens / (effectiveMs / 1000)) * 10) / 10;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
if (tps === null || tps > MAX_PLAUSIBLE_TPS) return null;
|
|
75
|
+
return { tps, effectiveMs, isPrimary };
|
|
76
|
+
}
|
|
77
|
+
|
|
17
78
|
export function setupTpsTracker(pi: ExtensionAPI): void {
|
|
18
|
-
/** Timestamp when the current assistant message event started. Used as a fallback. */
|
|
19
79
|
let messageStart: number | null = null;
|
|
20
|
-
/** Timestamp of the first streamed output delta for the current assistant message. */
|
|
21
80
|
let streamStart: number | null = null;
|
|
22
|
-
/** Estimated streamed output tokens for live display before providers report final usage. */
|
|
23
81
|
let estimatedStreamedTokens = 0;
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
82
|
+
let mainTiming: TpsTiming | null = null;
|
|
83
|
+
const mainMeasurements: Array<{
|
|
84
|
+
outputTokens: number;
|
|
85
|
+
timing: TpsTiming;
|
|
86
|
+
modelKey: string | null;
|
|
87
|
+
isToolCall: boolean;
|
|
88
|
+
}> = [];
|
|
89
|
+
let pendingToolMeasurement: (typeof mainMeasurements)[number] | null = null;
|
|
90
|
+
const tpsCaps = new Map<string, number>();
|
|
91
|
+
|
|
29
92
|
const subagentStarts = new Map<string, number>();
|
|
30
|
-
/** Latest cumulative foreground subagent tokens seen during tool_execution_update. */
|
|
31
93
|
const subagentLiveTokens = new Map<string, number>();
|
|
32
|
-
/** Cumulative subagent output tokens in this agent run. */
|
|
33
94
|
let totalSubagentTokens = 0;
|
|
34
|
-
/** Cumulative subagent wall time (ms) in this agent run. */
|
|
35
95
|
let totalSubagentMs = 0;
|
|
36
|
-
/** Completed subagent call summaries in this agent run. */
|
|
37
96
|
const subagentSummaries: Array<{ tokens: number; ms: number; tps: number }> = [];
|
|
38
97
|
|
|
39
98
|
function asRecord(value: unknown): Record<string, unknown> | null {
|
|
@@ -61,25 +120,8 @@ export function setupTpsTracker(pi: ExtensionAPI): void {
|
|
|
61
120
|
return numberValue(usage?.output ?? usage?.outputTokens ?? usage?.completionTokens ?? usage?.completion_tokens);
|
|
62
121
|
}
|
|
63
122
|
|
|
64
|
-
function generatedTokensFromMessage(message: { usage?: unknown; content?: unknown }): number {
|
|
65
|
-
const usageTokens = generatedTokensFromUsage(asRecord(message.usage));
|
|
66
|
-
if (usageTokens > 0) return usageTokens;
|
|
67
|
-
|
|
68
|
-
const content = Array.isArray(message.content) ? message.content : [];
|
|
69
|
-
const chars = content.reduce((sum, block) => {
|
|
70
|
-
const item = asRecord(block);
|
|
71
|
-
if (!item) return sum;
|
|
72
|
-
if (item.type === "text") return sum + String(item.text ?? "").length;
|
|
73
|
-
if (item.type === "thinking") return sum + String(item.thinking ?? "").length;
|
|
74
|
-
if (item.type === "toolCall") return sum + String(item.name ?? "").length + JSON.stringify(item.arguments ?? {}).length;
|
|
75
|
-
return sum;
|
|
76
|
-
}, 0);
|
|
77
|
-
return Math.ceil(chars / 4);
|
|
78
|
-
}
|
|
79
|
-
|
|
80
123
|
function outputTokensFromResult(value: unknown): number {
|
|
81
|
-
|
|
82
|
-
return results.reduce((sum, result) => sum + generatedTokensFromUsage(asRecord(result.usage)), 0);
|
|
124
|
+
return getResults(value).reduce((sum, result) => sum + generatedTokensFromUsage(asRecord(result.usage)), 0);
|
|
83
125
|
}
|
|
84
126
|
|
|
85
127
|
function liveTokensFromResult(value: unknown): number {
|
|
@@ -117,8 +159,9 @@ export function setupTpsTracker(pi: ExtensionAPI): void {
|
|
|
117
159
|
}
|
|
118
160
|
|
|
119
161
|
pi.on("agent_start", async (_event, ctx) => {
|
|
120
|
-
|
|
121
|
-
|
|
162
|
+
mainMeasurements.length = 0;
|
|
163
|
+
pendingToolMeasurement = null;
|
|
164
|
+
mainTiming = null;
|
|
122
165
|
totalSubagentTokens = 0;
|
|
123
166
|
totalSubagentMs = 0;
|
|
124
167
|
subagentSummaries.length = 0;
|
|
@@ -127,77 +170,99 @@ export function setupTpsTracker(pi: ExtensionAPI): void {
|
|
|
127
170
|
estimatedStreamedTokens = 0;
|
|
128
171
|
subagentStarts.clear();
|
|
129
172
|
subagentLiveTokens.clear();
|
|
130
|
-
|
|
131
|
-
ctx.ui.setStatus("tps", theme.fg("success", "generating..."));
|
|
173
|
+
ctx.ui.setStatus("tps", ctx.ui.theme.fg("success", "generating..."));
|
|
132
174
|
});
|
|
133
175
|
|
|
134
176
|
pi.on("message_start", async (event) => {
|
|
135
177
|
if (event.message.role !== "assistant") return;
|
|
136
|
-
|
|
178
|
+
const now = performance.now();
|
|
179
|
+
messageStart = now;
|
|
137
180
|
streamStart = null;
|
|
138
181
|
estimatedStreamedTokens = 0;
|
|
182
|
+
mainTiming = {
|
|
183
|
+
messageStartMs: now,
|
|
184
|
+
lastUpdateMs: now,
|
|
185
|
+
firstTokenMs: null,
|
|
186
|
+
updateCount: 0,
|
|
187
|
+
firstStreamUpdateMs: null,
|
|
188
|
+
lastStreamUpdateMs: 0,
|
|
189
|
+
stallMs: 0,
|
|
190
|
+
generationMs: 0,
|
|
191
|
+
};
|
|
192
|
+
pendingToolMeasurement = null;
|
|
139
193
|
});
|
|
140
194
|
|
|
141
195
|
pi.on("message_update", async (event, ctx) => {
|
|
142
196
|
if (event.message.role !== "assistant") return;
|
|
143
197
|
|
|
198
|
+
const now = performance.now();
|
|
199
|
+
if (mainTiming) {
|
|
200
|
+
if (mainTiming.firstTokenMs === null) {
|
|
201
|
+
mainTiming.firstTokenMs = now;
|
|
202
|
+
mainTiming.lastUpdateMs = now;
|
|
203
|
+
} else {
|
|
204
|
+
mainTiming.updateCount++;
|
|
205
|
+
mainTiming.firstStreamUpdateMs ??= now;
|
|
206
|
+
mainTiming.lastStreamUpdateMs = now;
|
|
207
|
+
const gap = now - mainTiming.lastUpdateMs;
|
|
208
|
+
if (gap >= STALL_THRESHOLD_MS) mainTiming.stallMs += gap;
|
|
209
|
+
mainTiming.lastUpdateMs = now;
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
|
|
144
213
|
const streamEvent = event.assistantMessageEvent;
|
|
145
214
|
const isOutputDelta =
|
|
146
215
|
streamEvent.type === "text_delta" ||
|
|
147
216
|
streamEvent.type === "thinking_delta" ||
|
|
148
217
|
streamEvent.type === "toolcall_delta";
|
|
149
|
-
|
|
150
218
|
if (!isOutputDelta) return;
|
|
151
219
|
|
|
152
|
-
const now = Date.now();
|
|
153
220
|
streamStart ??= now;
|
|
154
221
|
estimatedStreamedTokens += Math.max(0, streamEvent.delta.length / 4);
|
|
155
|
-
|
|
156
222
|
const elapsed = (now - streamStart) / 1000;
|
|
157
223
|
const officialTokens = generatedTokensFromUsage(asRecord(event.message.usage));
|
|
158
224
|
const currentTokens = officialTokens > 0 ? officialTokens : estimatedStreamedTokens;
|
|
159
|
-
|
|
160
225
|
if (elapsed > 0 && currentTokens > 0) {
|
|
161
|
-
|
|
162
|
-
const tokenLabel = officialTokens > 0
|
|
163
|
-
? `${officialTokens} tok`
|
|
164
|
-
: `~${Math.round(estimatedStreamedTokens)} tok`;
|
|
165
|
-
const theme = ctx.ui.theme;
|
|
166
|
-
ctx.ui.setStatus(
|
|
167
|
-
"tps",
|
|
168
|
-
`${theme.fg("accent", `${tps} tok/s`)}`,
|
|
169
|
-
);
|
|
226
|
+
ctx.ui.setStatus("tps", ctx.ui.theme.fg("accent", `${Math.round(currentTokens / elapsed)} tok/s`));
|
|
170
227
|
}
|
|
171
228
|
});
|
|
172
229
|
|
|
173
230
|
pi.on("message_end", async (event) => {
|
|
174
231
|
if (event.message.role !== "assistant") return;
|
|
175
232
|
|
|
176
|
-
const
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
233
|
+
const now = performance.now();
|
|
234
|
+
if (mainTiming) {
|
|
235
|
+
mainTiming.generationMs = now - mainTiming.messageStartMs;
|
|
236
|
+
const message = asRecord(event.message);
|
|
237
|
+
const provider = typeof message?.provider === "string" ? message.provider : null;
|
|
238
|
+
const model = typeof message?.model === "string" ? message.model : null;
|
|
239
|
+
const outputTokens = generatedTokensFromUsage(asRecord(message?.usage));
|
|
240
|
+
if (outputTokens > 0) {
|
|
241
|
+
const measurement = {
|
|
242
|
+
outputTokens,
|
|
243
|
+
timing: mainTiming,
|
|
244
|
+
modelKey: provider && model ? `${provider}:${model}` : null,
|
|
245
|
+
isToolCall: false,
|
|
246
|
+
};
|
|
247
|
+
mainMeasurements.push(measurement);
|
|
248
|
+
pendingToolMeasurement = measurement;
|
|
249
|
+
}
|
|
183
250
|
}
|
|
184
251
|
|
|
185
|
-
|
|
186
|
-
totalStreamMs += Math.max(0, Date.now() - timingStart);
|
|
187
|
-
|
|
252
|
+
mainTiming = null;
|
|
188
253
|
messageStart = null;
|
|
189
254
|
streamStart = null;
|
|
190
255
|
estimatedStreamedTokens = 0;
|
|
191
256
|
});
|
|
192
257
|
|
|
193
258
|
pi.on("tool_execution_start", async (event) => {
|
|
259
|
+
if (pendingToolMeasurement) pendingToolMeasurement.isToolCall = true;
|
|
194
260
|
if (event.toolName !== "subagent") return;
|
|
195
261
|
subagentStarts.set(event.toolCallId, Date.now());
|
|
196
262
|
});
|
|
197
263
|
|
|
198
264
|
pi.on("tool_execution_update", async (event, ctx) => {
|
|
199
265
|
if (event.toolName !== "subagent") return;
|
|
200
|
-
|
|
201
266
|
const start = subagentStarts.get(event.toolCallId);
|
|
202
267
|
if (!start) return;
|
|
203
268
|
|
|
@@ -209,17 +274,13 @@ export function setupTpsTracker(pi: ExtensionAPI): void {
|
|
|
209
274
|
|
|
210
275
|
const tps = Math.round(tokens / elapsed);
|
|
211
276
|
const mode = subagentModeLabel(event.partialResult);
|
|
212
|
-
const agentSummary = items
|
|
213
|
-
.
|
|
214
|
-
|
|
215
|
-
.join(" ");
|
|
216
|
-
const theme = ctx.ui.theme;
|
|
217
|
-
ctx.ui.setStatus("tps", `${theme.fg("accent", `main+${mode} ${tps} t/s`)} ${theme.fg("success", `(${agentSummary || `${tps} t/s`})`)}`);
|
|
277
|
+
const agentSummary = items.filter((item) => item.tokens > 0 || item.status === "running")
|
|
278
|
+
.map((item) => `${item.name} ${Math.round(item.tokens / elapsed)} t/s`).join(" ");
|
|
279
|
+
ctx.ui.setStatus("tps", `${ctx.ui.theme.fg("accent", `main+${mode} ${tps} t/s`)} ${ctx.ui.theme.fg("success", `(${agentSummary || `${tps} t/s`})`)}`);
|
|
218
280
|
});
|
|
219
281
|
|
|
220
282
|
pi.on("tool_execution_end", async (event, ctx) => {
|
|
221
283
|
if (event.toolName !== "subagent") return;
|
|
222
|
-
|
|
223
284
|
const start = subagentStarts.get(event.toolCallId);
|
|
224
285
|
subagentStarts.delete(event.toolCallId);
|
|
225
286
|
const liveTokens = subagentLiveTokens.get(event.toolCallId) ?? liveTokensFromResult(event.result);
|
|
@@ -228,59 +289,57 @@ export function setupTpsTracker(pi: ExtensionAPI): void {
|
|
|
228
289
|
|
|
229
290
|
const outputTokens = outputTokensFromResult(event.result);
|
|
230
291
|
const displayTokens = outputTokens || liveTokens;
|
|
231
|
-
const mode = subagentModeLabel(event.result);
|
|
232
292
|
const elapsedMs = Math.max(0, Date.now() - start);
|
|
233
293
|
if (displayTokens <= 0 || elapsedMs <= 0) return;
|
|
234
294
|
const elapsedSeconds = elapsedMs / 1000;
|
|
235
295
|
const items = subagentProgressItems(event.result);
|
|
236
|
-
const agentSummary = items
|
|
237
|
-
.
|
|
238
|
-
|
|
239
|
-
|
|
296
|
+
const agentSummary = items.filter((item) => item.tokens > 0)
|
|
297
|
+
.map((item) => `${item.name} ${Math.round(item.tokens / elapsedSeconds)} t/s`).join(" ");
|
|
298
|
+
const tps = Math.round(displayTokens / elapsedSeconds);
|
|
299
|
+
const mode = subagentModeLabel(event.result);
|
|
240
300
|
|
|
241
|
-
totalOutputTokens += displayTokens;
|
|
242
|
-
totalStreamMs += elapsedMs;
|
|
243
301
|
totalSubagentTokens += displayTokens;
|
|
244
302
|
totalSubagentMs += elapsedMs;
|
|
245
|
-
|
|
246
|
-
const tps = Math.round(displayTokens / elapsedSeconds);
|
|
247
303
|
subagentSummaries.push({ tokens: displayTokens, ms: elapsedMs, tps });
|
|
248
|
-
|
|
249
|
-
ctx.ui.
|
|
250
|
-
ctx.ui.setStatus(
|
|
251
|
-
"tps",
|
|
252
|
-
`${theme.fg("accent", `main+${mode} ${tps} t/s`)} ${theme.fg("success", `(${agentSummary || `${tps} t/s`})`)}`,
|
|
253
|
-
);
|
|
304
|
+
ctx.ui.notify(`${ctx.ui.theme.fg("success", "✓")} ${ctx.ui.theme.fg("accent", `${tps} t/s`)}`, "info");
|
|
305
|
+
ctx.ui.setStatus("tps", `${ctx.ui.theme.fg("accent", `main+${mode} ${tps} t/s`)} ${ctx.ui.theme.fg("success", `(${agentSummary || `${tps} t/s`})`)}`);
|
|
254
306
|
});
|
|
255
307
|
|
|
256
308
|
pi.on("agent_end", async (_event, ctx) => {
|
|
257
|
-
|
|
258
|
-
|
|
309
|
+
let measuredMainTokens = 0;
|
|
310
|
+
let measuredMainMs = 0;
|
|
311
|
+
for (const measurement of mainMeasurements) {
|
|
312
|
+
if (!measurement.modelKey) continue;
|
|
313
|
+
const reliable = calculateReliableTps(measurement.outputTokens, measurement.timing);
|
|
314
|
+
if (!reliable) continue;
|
|
315
|
+
|
|
316
|
+
let tps = reliable.tps;
|
|
317
|
+
if (measurement.isToolCall) {
|
|
318
|
+
const cap = tpsCaps.get(measurement.modelKey);
|
|
319
|
+
if (cap === undefined) continue;
|
|
320
|
+
tps = Math.min(tps, cap);
|
|
321
|
+
} else if (reliable.isPrimary) {
|
|
322
|
+
const cap = tpsCaps.get(measurement.modelKey);
|
|
323
|
+
if (cap === undefined || tps > cap) tpsCaps.set(measurement.modelKey, tps);
|
|
324
|
+
}
|
|
325
|
+
measuredMainTokens += tps * (reliable.effectiveMs / 1000);
|
|
326
|
+
measuredMainMs += reliable.effectiveMs;
|
|
327
|
+
}
|
|
259
328
|
|
|
260
|
-
const
|
|
261
|
-
const
|
|
262
|
-
const
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
const mainTokens = totalOutputTokens - totalSubagentTokens;
|
|
266
|
-
const mainMs = totalStreamMs - totalSubagentMs;
|
|
267
|
-
const mainTps = mainTokens > 0 && mainMs > 0 ? Math.round(mainTokens / (mainMs / 1000)) : 0;
|
|
329
|
+
const totalTokens = measuredMainTokens + totalSubagentTokens;
|
|
330
|
+
const totalMs = measuredMainMs + totalSubagentMs;
|
|
331
|
+
const tps = totalTokens > 0 && totalMs > 0 ? Math.round(totalTokens / (totalMs / 1000)) : 0;
|
|
332
|
+
const mainTps = measuredMainTokens > 0 && measuredMainMs > 0
|
|
333
|
+
? Math.round(measuredMainTokens / (measuredMainMs / 1000)) : 0;
|
|
268
334
|
const subagentTps = totalSubagentTokens > 0 && totalSubagentMs > 0
|
|
269
|
-
? Math.round(totalSubagentTokens / (totalSubagentMs / 1000))
|
|
270
|
-
|
|
335
|
+
? Math.round(totalSubagentTokens / (totalSubagentMs / 1000)) : 0;
|
|
336
|
+
const theme = ctx.ui.theme;
|
|
271
337
|
const mainDetail = mainTps > 0 ? theme.fg("success", ` | main ${mainTps} t/s`) : "";
|
|
272
338
|
const subagentDetail = subagentTps > 0
|
|
273
|
-
? theme.fg("success", ` | subagents ${subagentTps} t/s (${subagentSummaries.length} call${subagentSummaries.length === 1 ? "" : "s"})`)
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
ctx.ui.notify(`${icon} total ${tpsLabel}${mainDetail}${subagentDetail}`, "info");
|
|
339
|
+
? theme.fg("success", ` | subagents ${subagentTps} t/s (${subagentSummaries.length} call${subagentSummaries.length === 1 ? "" : "s"})`) : "";
|
|
340
|
+
ctx.ui.notify(`${theme.fg("success", "✓")} total ${tps > 0 ? theme.fg("accent", `${tps} t/s`) : theme.fg("success", "N/A")}${mainDetail}${subagentDetail}`, "info");
|
|
277
341
|
const finalMainStatus = mainTps > 0 ? `main ${mainTps} t/s` : "main N/A";
|
|
278
|
-
const finalSubagentStatus =
|
|
279
|
-
|
|
280
|
-
: "";
|
|
281
|
-
ctx.ui.setStatus(
|
|
282
|
-
"tps",
|
|
283
|
-
`${theme.fg("accent", `done ${tps} t/s`)} ${theme.fg("success", `(${[finalMainStatus, finalSubagentStatus].filter(Boolean).join(" ")})`)}`,
|
|
284
|
-
);
|
|
342
|
+
const finalSubagentStatus = subagentTps > 0 ? `subagents ${subagentTps} t/s` : "";
|
|
343
|
+
ctx.ui.setStatus("tps", `${theme.fg("accent", `done ${tps} t/s`)} ${theme.fg("success", `(${[finalMainStatus, finalSubagentStatus].filter(Boolean).join(" ")})`)}`);
|
|
285
344
|
});
|
|
286
345
|
}
|
|
@@ -653,10 +653,6 @@ export async function openSubagentFleet(ctx: ExtensionContext, state: SubagentSt
|
|
|
653
653
|
try {
|
|
654
654
|
await ctx.ui.custom<undefined>(
|
|
655
655
|
(tui, theme, _keybindings, done) => new SubagentFleetComponent(tui, theme, state, done, options),
|
|
656
|
-
{
|
|
657
|
-
overlay: true,
|
|
658
|
-
overlayOptions: { anchor: "center", width: "95%", minWidth: 60, maxHeight: "85%", margin: 1 },
|
|
659
|
-
},
|
|
660
656
|
);
|
|
661
657
|
} finally {
|
|
662
658
|
state.fleetInspectorOpen = wasOpen;
|
|
@@ -2042,7 +2042,7 @@ describe("subagents-doctor slash command", { skip: !available ? "slash-commands.
|
|
|
2042
2042
|
);
|
|
2043
2043
|
rendered = component.render(100).join("\n");
|
|
2044
2044
|
component.dispose();
|
|
2045
|
-
assert.
|
|
2045
|
+
assert.equal(options, undefined);
|
|
2046
2046
|
return undefined;
|
|
2047
2047
|
};
|
|
2048
2048
|
const pi = {
|
|
@@ -539,7 +539,7 @@ describe("native subagent fleet", () => {
|
|
|
539
539
|
}
|
|
540
540
|
});
|
|
541
541
|
|
|
542
|
-
it("refreshes the roster while the
|
|
542
|
+
it("refreshes the roster while the inspector remains open", async () => {
|
|
543
543
|
const root = fs.mkdtempSync(path.join(os.tmpdir(), "pi-fleet-refresh-"));
|
|
544
544
|
try {
|
|
545
545
|
const state = stateForTest();
|
|
@@ -1,39 +1,103 @@
|
|
|
1
|
-
import {
|
|
2
|
-
|
|
3
|
-
|
|
1
|
+
import type {
|
|
2
|
+
DraftQuestionState,
|
|
3
|
+
PreparedQuestion,
|
|
4
|
+
QuestionAnswer,
|
|
5
|
+
QuestionOption,
|
|
6
|
+
QuestionResponse,
|
|
7
|
+
RawQuestion,
|
|
8
|
+
} from "./types.ts";
|
|
4
9
|
|
|
5
|
-
|
|
10
|
+
function clean(value: unknown): string | undefined {
|
|
11
|
+
if (typeof value !== "string") return undefined;
|
|
12
|
+
const trimmed = value.trim();
|
|
13
|
+
return trimmed || undefined;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function prepareOptions(value: unknown, questionNumber: number): { options: QuestionOption[] } | { error: string } {
|
|
17
|
+
if (!Array.isArray(value) || value.length === 0) {
|
|
18
|
+
return { error: `Question ${questionNumber}: choice questions require at least one option.` };
|
|
19
|
+
}
|
|
20
|
+
const values = new Set<string>();
|
|
21
|
+
const options: QuestionOption[] = [];
|
|
22
|
+
for (let index = 0; index < value.length; index++) {
|
|
23
|
+
const raw = value[index] as { value?: unknown; label?: unknown; description?: unknown };
|
|
24
|
+
const optionValue = clean(raw?.value);
|
|
25
|
+
const label = clean(raw?.label);
|
|
26
|
+
if (!optionValue || !label) {
|
|
27
|
+
return { error: `Question ${questionNumber}, option ${index + 1}: value and label must be non-empty strings.` };
|
|
28
|
+
}
|
|
29
|
+
if (values.has(optionValue)) {
|
|
30
|
+
return { error: `Question ${questionNumber}: option values must be unique; duplicate value: ${optionValue}` };
|
|
31
|
+
}
|
|
32
|
+
values.add(optionValue);
|
|
33
|
+
options.push({ value: optionValue, label, description: clean(raw.description) });
|
|
34
|
+
}
|
|
35
|
+
return { options };
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export function prepareQuestions(value: unknown): { questions: PreparedQuestion[] } | { error: string } {
|
|
6
39
|
if (!Array.isArray(value) || value.length === 0) return { error: "questions must contain at least one question." };
|
|
7
40
|
|
|
8
41
|
const ids = new Set<string>();
|
|
9
|
-
const questions:
|
|
42
|
+
const questions: PreparedQuestion[] = [];
|
|
10
43
|
for (let index = 0; index < value.length; index++) {
|
|
11
|
-
const raw = value[index] as
|
|
12
|
-
const
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
const optionsError = getOptionsFormatError(raw.options);
|
|
16
|
-
if (optionsError) return { error: `Question ${index + 1}: ${optionsError}` };
|
|
17
|
-
const options = normalizeOptions(raw.options);
|
|
18
|
-
const allowFreeform = raw.allowFreeform ?? options.length === 0;
|
|
19
|
-
if (options.length === 0 && !allowFreeform) {
|
|
20
|
-
return { error: `Question ${index + 1}: options are empty and allowFreeform is false.` };
|
|
21
|
-
}
|
|
44
|
+
const raw = value[index] as RawQuestion;
|
|
45
|
+
const number = index + 1;
|
|
46
|
+
const question = clean(raw?.question);
|
|
47
|
+
if (!question) return { error: `Question ${number} must include a non-empty question.` };
|
|
22
48
|
|
|
23
|
-
const id = raw.id
|
|
49
|
+
const id = clean(raw.id) ?? `q${number}`;
|
|
24
50
|
if (ids.has(id)) return { error: `Question ids must be unique; duplicate id: ${id}` };
|
|
25
51
|
ids.add(id);
|
|
26
|
-
|
|
52
|
+
|
|
53
|
+
const common = {
|
|
27
54
|
id,
|
|
28
|
-
label: raw.label
|
|
55
|
+
label: clean(raw.label) ?? `Q${number}`,
|
|
29
56
|
question,
|
|
30
|
-
context: raw.context
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
57
|
+
context: clean(raw.context),
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
if (raw.type === "text") {
|
|
61
|
+
questions.push({ ...common, type: "text", options: [], allowOther: false });
|
|
62
|
+
continue;
|
|
63
|
+
}
|
|
64
|
+
if (raw.type !== "select" && raw.type !== "multiselect") {
|
|
65
|
+
return { error: `Question ${number}: type must be select, multiselect, or text.` };
|
|
66
|
+
}
|
|
67
|
+
const preparedOptions = prepareOptions(raw.options, number);
|
|
68
|
+
if ("error" in preparedOptions) return preparedOptions;
|
|
69
|
+
questions.push({
|
|
70
|
+
...common,
|
|
71
|
+
type: raw.type,
|
|
72
|
+
options: preparedOptions.options,
|
|
73
|
+
allowOther: raw.allowOther ?? false,
|
|
36
74
|
});
|
|
37
75
|
}
|
|
38
76
|
return { questions };
|
|
39
77
|
}
|
|
78
|
+
|
|
79
|
+
export function saveDraftQuestion(
|
|
80
|
+
states: Map<string, DraftQuestionState>,
|
|
81
|
+
id: string,
|
|
82
|
+
response: QuestionResponse | null,
|
|
83
|
+
markBlankSkipped: boolean,
|
|
84
|
+
): void {
|
|
85
|
+
if (response) {
|
|
86
|
+
states.set(id, { status: "answered", response });
|
|
87
|
+
return;
|
|
88
|
+
}
|
|
89
|
+
if (markBlankSkipped) states.set(id, { status: "skipped" });
|
|
90
|
+
else states.delete(id);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
export function buildQuestionAnswers(
|
|
94
|
+
questions: PreparedQuestion[],
|
|
95
|
+
states: ReadonlyMap<string, DraftQuestionState>,
|
|
96
|
+
): QuestionAnswer[] {
|
|
97
|
+
return questions.map((question) => {
|
|
98
|
+
const state = states.get(question.id);
|
|
99
|
+
if (!state) return { id: question.id, status: "unanswered" };
|
|
100
|
+
if (state.status !== "answered" || !state.response) return { id: question.id, status: "skipped" };
|
|
101
|
+
return { id: question.id, status: "answered", response: state.response };
|
|
102
|
+
});
|
|
103
|
+
}
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
import { Key } from "@earendil-works/pi-tui";
|
|
4
4
|
import type { ResolvedShortcut } from "./types.ts";
|
|
5
5
|
|
|
6
|
-
export const QUESTION_VERSION = "
|
|
6
|
+
export const QUESTION_VERSION = "2";
|
|
7
7
|
|
|
8
8
|
export const SPLIT_PANE_MIN_WIDTH = 84;
|
|
9
9
|
export const SPLIT_PANE_LEFT_MIN_WIDTH = 32;
|