pi-crew 0.9.37 → 0.9.38
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/CHANGELOG.md +22 -0
- package/dist/build-meta.json +28 -56
- package/dist/index.mjs +58 -291
- package/dist/index.mjs.map +4 -4
- package/package.json +1 -1
- package/src/config/config.ts +2 -2
- package/src/extension/crew-vibes/figures.ts +13 -6
- package/src/extension/crew-vibes/index.ts +36 -13
- package/src/extension/register.ts +0 -6
- package/src/extension/registration/subagent-tools.ts +1 -18
- package/src/extension/team-tool/context.ts +1 -0
- package/src/extension/team-tool/run.ts +1 -0
- package/src/runtime/progress-tracker.ts +0 -140
- package/src/ui/tool-renderers/index.ts +20 -3
- package/src/ui/widget/widget-formatters.ts +10 -0
package/package.json
CHANGED
package/src/config/config.ts
CHANGED
|
@@ -694,7 +694,7 @@ function parseIsolationPolicy(value: unknown): CrewRuntimeConfig["isolationPolic
|
|
|
694
694
|
|
|
695
695
|
function parseRuntimeConfig(value: unknown): CrewRuntimeConfig | undefined {
|
|
696
696
|
const obj = asRecord(value);
|
|
697
|
-
if (!obj) return
|
|
697
|
+
if (!obj) return { inheritContext: true } as CrewRuntimeConfig;
|
|
698
698
|
const runtime: CrewRuntimeConfig = {
|
|
699
699
|
mode: parseWithSchema(
|
|
700
700
|
Type.Union([Type.Literal("auto"), Type.Literal("scaffold"), Type.Literal("child-process"), Type.Literal("live-session")]),
|
|
@@ -704,7 +704,7 @@ function parseRuntimeConfig(value: unknown): CrewRuntimeConfig | undefined {
|
|
|
704
704
|
allowChildProcessFallback: parseWithSchema(Type.Boolean(), obj.allowChildProcessFallback),
|
|
705
705
|
maxTurns: parsePositiveInteger(obj.maxTurns, LIMIT_CEILINGS.runtimeMaxTurns),
|
|
706
706
|
graceTurns: parsePositiveInteger(obj.graceTurns, LIMIT_CEILINGS.runtimeGraceTurns),
|
|
707
|
-
inheritContext: parseWithSchema(Type.Boolean(), obj.inheritContext),
|
|
707
|
+
inheritContext: parseWithSchema(Type.Boolean(), obj.inheritContext) ?? true,
|
|
708
708
|
promptMode: parseWithSchema(Type.Union([Type.Literal("replace"), Type.Literal("append")]), obj.promptMode),
|
|
709
709
|
groupJoin: parseWithSchema(Type.Union([Type.Literal("off"), Type.Literal("group"), Type.Literal("smart")]), obj.groupJoin),
|
|
710
710
|
groupJoinAckTimeoutMs: parsePositiveInteger(obj.groupJoinAckTimeoutMs, 86_400_000),
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { SpeedConfig } from "./config.ts";
|
|
2
|
-
import { isWebTerminal } from "./font-detect.ts";
|
|
2
|
+
import { hasCrewFontFile, isWebTerminal } from "./font-detect.ts";
|
|
3
3
|
|
|
4
4
|
/**
|
|
5
5
|
* Crew figures replacing the original cat glyphs from pi-speeed / pi-chonk.
|
|
@@ -57,12 +57,19 @@ const PUA_CREW_FRAMES: readonly string[] = [
|
|
|
57
57
|
/**
|
|
58
58
|
* Return the indicator frames for the given style.
|
|
59
59
|
*
|
|
60
|
-
*
|
|
61
|
-
*
|
|
60
|
+
* Default is `"braille"` — standard Unicode braille spinner (⠋⠙⠹⠸⠼⠴⠦⠧⠇⠏)
|
|
61
|
+
* that renders correctly on every terminal, every platform, with zero
|
|
62
|
+
* font dependency. PUA runner-glyphs require crew-vibes.ttf installed AND
|
|
63
|
+
* a terminal that does font fallback for Private Use Area codepoints —
|
|
64
|
+
* Windows Terminal does NOT do this reliably, showing garbage chars even
|
|
65
|
+
* with the font installed.
|
|
66
|
+
*
|
|
67
|
+
* To opt into the PUA runner look, pass `style: "pua"` explicitly.
|
|
62
68
|
*/
|
|
63
|
-
export function crewFrames(style: "braille" | "pua" = "
|
|
64
|
-
//
|
|
65
|
-
|
|
69
|
+
export function crewFrames(style: "braille" | "pua" = "braille"): readonly string[] {
|
|
70
|
+
// Even when explicitly requested, fall back to braille if the font is
|
|
71
|
+
// missing or we're on a web terminal (which can't render PUA at all).
|
|
72
|
+
if (style === "pua" && (isWebTerminal() || !hasCrewFontFile())) return BRAILLE_FRAMES;
|
|
66
73
|
return style === "pua" ? PUA_CREW_FRAMES : BRAILLE_FRAMES;
|
|
67
74
|
}
|
|
68
75
|
|
|
@@ -1,5 +1,24 @@
|
|
|
1
1
|
import type { ExtensionAPI, ExtensionCommandContext, ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
2
2
|
import { requestRender, setFooter } from "../../ui/pi-ui-compat.ts";
|
|
3
|
+
import { logInternalError } from "../../utils/internal-error.ts";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Wrap a pi UI call in try/catch. If the call throws (e.g. terminal doesn't
|
|
7
|
+
* support PUA glyphs, theme helper returns undefined, or pi's API signature
|
|
8
|
+
* changed), log it and silently fall back to pi's default behavior — which
|
|
9
|
+
* is simply "do nothing", letting pi show its own built-in indicator.
|
|
10
|
+
*
|
|
11
|
+
* Crew-vibes must NEVER break the user's session. A broken spinner is not
|
|
12
|
+
* worth a crashed pi.
|
|
13
|
+
*/
|
|
14
|
+
function safeUiCall(scope: string, fn: () => void): void {
|
|
15
|
+
try {
|
|
16
|
+
fn();
|
|
17
|
+
} catch (error) {
|
|
18
|
+
logInternalError(`crew-vibes.${scope}`, error, undefined, "warn");
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
|
|
3
22
|
import { type CrewVibesConfig, loadConfig, PROVIDER_STATUS_ID, saveConfig } from "./config.ts";
|
|
4
23
|
import { intervalForSpeed } from "./figures.ts";
|
|
5
24
|
import { type CrewVibesFooterSource, createCrewVibesFooter } from "./footer.ts";
|
|
@@ -72,44 +91,48 @@ export function registerCrewVibes(pi: ExtensionAPI): void {
|
|
|
72
91
|
function installFooter(ctx: ExtensionContext): void {
|
|
73
92
|
if (!ctx?.hasUI) return;
|
|
74
93
|
if (!metersActive()) {
|
|
75
|
-
setFooter(ctx, undefined);
|
|
94
|
+
safeUiCall("clear-footer", () => setFooter(ctx, undefined));
|
|
76
95
|
return;
|
|
77
96
|
}
|
|
78
|
-
|
|
79
|
-
|
|
97
|
+
safeUiCall("install-footer", () => {
|
|
98
|
+
setFooter(ctx, (tui, theme, footerData) => createCrewVibesFooter({ tui, theme, footerData, ctx, source: footerSource }));
|
|
99
|
+
requestRender(ctx);
|
|
100
|
+
});
|
|
80
101
|
}
|
|
81
102
|
|
|
82
103
|
/** Trigger a footer repaint; the footer recomputes capacity/quota on render. */
|
|
83
104
|
function refreshFooter(ctx: ExtensionContext): void {
|
|
84
|
-
if (ctx?.hasUI) requestRender(ctx);
|
|
105
|
+
if (ctx?.hasUI) safeUiCall("refresh-footer", () => requestRender(ctx));
|
|
85
106
|
}
|
|
86
107
|
|
|
87
108
|
function publishSpeedFooter(ctx: ExtensionContext, speed = footerAnimator.value()): void {
|
|
88
109
|
if (!config.enabled || !config.speed.enabled || !config.speed.footer) {
|
|
89
|
-
setSpeedStatus(ctx, config, undefined);
|
|
110
|
+
safeUiCall("clear-speed-status", () => setSpeedStatus(ctx, config, undefined));
|
|
90
111
|
return;
|
|
91
112
|
}
|
|
92
|
-
setSpeedStatus(ctx, config, renderSpeedFooter(themeOf(ctx), config.speed, speed));
|
|
113
|
+
safeUiCall("publish-speed-status", () => setSpeedStatus(ctx, config, renderSpeedFooter(themeOf(ctx), config.speed, speed)));
|
|
93
114
|
}
|
|
94
115
|
|
|
95
116
|
function applyIndicator(ctx: ExtensionContext, speed: number | null, force = false): void {
|
|
96
117
|
if (!ctx.hasUI || !ctx.ui.setWorkingIndicator) return;
|
|
97
118
|
if (!config.enabled || !config.speed.enabled || !config.speed.indicator) {
|
|
98
|
-
ctx.ui.setWorkingIndicator();
|
|
119
|
+
safeUiCall("reset-indicator", () => ctx.ui.setWorkingIndicator?.());
|
|
99
120
|
return;
|
|
100
121
|
}
|
|
101
122
|
const next = intervalForSpeed(config.speed, speed);
|
|
102
123
|
if (!force && Math.abs(next - currentIntervalMs) < 10) return;
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
124
|
+
safeUiCall("set-indicator", () =>
|
|
125
|
+
ctx.ui.setWorkingIndicator?.({
|
|
126
|
+
frames: crewIndicatorFrames(themeOf(ctx)),
|
|
127
|
+
intervalMs: next,
|
|
128
|
+
}),
|
|
129
|
+
);
|
|
107
130
|
currentIntervalMs = next;
|
|
108
131
|
}
|
|
109
132
|
|
|
110
133
|
function renderWorking(ctx: ExtensionContext, speed: number | null): void {
|
|
111
134
|
if (!config.enabled || !config.speed.enabled || !ctx.hasUI) return;
|
|
112
|
-
ctx.ui.setWorkingMessage(renderWorkingMessage(themeOf(ctx), config.speed, speed));
|
|
135
|
+
safeUiCall("set-working-message", () => ctx.ui.setWorkingMessage?.(renderWorkingMessage(themeOf(ctx), config.speed, speed)));
|
|
113
136
|
}
|
|
114
137
|
|
|
115
138
|
function stopLiveTimer(): void {
|
|
@@ -315,7 +338,7 @@ export function registerCrewVibes(pi: ExtensionAPI): void {
|
|
|
315
338
|
stopLiveTimer();
|
|
316
339
|
if (ctx && config.enabled && ctx.hasUI) {
|
|
317
340
|
applyIndicator(ctx, speedTracker.lastTokS);
|
|
318
|
-
ctx.ui.setWorkingMessage();
|
|
341
|
+
safeUiCall("clear-working-message", () => ctx.ui.setWorkingMessage?.());
|
|
319
342
|
}
|
|
320
343
|
});
|
|
321
344
|
|
|
@@ -130,7 +130,6 @@ function purgeStaleActiveRunIndexSyncIfLoaded(): void {
|
|
|
130
130
|
|
|
131
131
|
import { pruneFinishedRuns, pruneUserLevelRuns } from "../extension/run-maintenance.ts";
|
|
132
132
|
import { initI18n } from "../i18n.ts";
|
|
133
|
-
import { globalProgressTracker } from "../runtime/progress-tracker.ts";
|
|
134
133
|
// H3-L2 split: DeliveryCoordinator + OverflowRecoveryTracker moved to registration/lifecycle.ts.
|
|
135
134
|
import { tryRegisterSessionCleanup } from "../runtime/session-resources.ts";
|
|
136
135
|
import { createSessionSnapshot } from "../runtime/session-snapshot.ts";
|
|
@@ -1475,11 +1474,6 @@ export function registerPiTeams(pi: ExtensionAPI): void {
|
|
|
1475
1474
|
const record = event as Record<string, unknown>;
|
|
1476
1475
|
const eventType = typeof record.type === "string" ? record.type : undefined;
|
|
1477
1476
|
if (eventType) lifecycleState.overflowTracker?.feedEvent(taskId, runId, eventType);
|
|
1478
|
-
// Phase 2: forward worker events to progress tracker for real-time
|
|
1479
|
-
// widget display (tool calls, assistant text) on child-process runtime.
|
|
1480
|
-
if (record && typeof record === "object") {
|
|
1481
|
-
globalProgressTracker.handleWorkerEvent(taskId, runId, record);
|
|
1482
|
-
}
|
|
1483
1477
|
},
|
|
1484
1478
|
});
|
|
1485
1479
|
registerSubagentTools(pi, subagentManager, {
|
|
@@ -23,7 +23,6 @@ import { loadConfig } from "../../config/config.ts";
|
|
|
23
23
|
import { t } from "../../i18n.ts";
|
|
24
24
|
import type { BatchBarrier } from "../../runtime/batch-barrier.ts";
|
|
25
25
|
import { readCrewAgents } from "../../runtime/crew-agent-records.ts";
|
|
26
|
-
import { globalProgressTracker } from "../../runtime/progress-tracker.ts";
|
|
27
26
|
import { checkSubagentSpawnPermission, currentCrewRole } from "../../runtime/role-permission.ts";
|
|
28
27
|
import { loadRunManifestById } from "../../state/state-store.ts";
|
|
29
28
|
import {
|
|
@@ -445,23 +444,7 @@ function startAgentToolProgress(cwd: string, agentRecordId: string, onUpdate: On
|
|
|
445
444
|
agents,
|
|
446
445
|
error: record.error,
|
|
447
446
|
});
|
|
448
|
-
|
|
449
|
-
// from globalProgressTracker. For foreground agents, find the task ID from
|
|
450
|
-
// the manifest and look up streaming progress.
|
|
451
|
-
let progressLine = text;
|
|
452
|
-
if (tasks && tasks.length > 0) {
|
|
453
|
-
const wp = globalProgressTracker.getWorkerProgress(tasks[0].id);
|
|
454
|
-
if (wp) {
|
|
455
|
-
const toolLine = wp.currentTool ? `\n ⚡ tool: ${wp.currentTool}` : "";
|
|
456
|
-
const tokenLine =
|
|
457
|
-
wp.tokens.input + wp.tokens.output > 0
|
|
458
|
-
? `\n 📊 tokens: ${(wp.tokens.input / 1000).toFixed(1)}K in, ${(wp.tokens.output / 1000).toFixed(1)}K out`
|
|
459
|
-
: "";
|
|
460
|
-
const textLine = wp.partialText ? `\n 💬 ${wp.partialText.slice(-150).replace(/\n/g, " ")}` : "";
|
|
461
|
-
progressLine = `${text}${toolLine}${tokenLine}${textLine}`;
|
|
462
|
-
}
|
|
463
|
-
}
|
|
464
|
-
onUpdate({ content: [{ type: "text", text: progressLine }] });
|
|
447
|
+
onUpdate({ content: [{ type: "text", text }] });
|
|
465
448
|
} catch (error) {
|
|
466
449
|
logInternalError("subagent-tools.progress", error, `agentId=${agentRecordId}`);
|
|
467
450
|
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
2
|
+
|
|
2
3
|
import type { PiTeamsConfig } from "../../config/config.ts";
|
|
3
4
|
import type { MetricRegistry } from "../../observability/metric-registry.ts";
|
|
4
5
|
import type { RunSnapshotCache } from "../../ui/run-snapshot-cache.ts";
|
|
@@ -832,6 +832,7 @@ export async function handleRun(params: TeamToolParamsValue, ctx: TeamContext):
|
|
|
832
832
|
runtime,
|
|
833
833
|
runtimeConfig: executedConfig.runtime,
|
|
834
834
|
parentContext: buildParentContext(ctx),
|
|
835
|
+
|
|
835
836
|
parentModel: ctx.model,
|
|
836
837
|
modelRegistry: ctx.modelRegistry,
|
|
837
838
|
modelOverride: params.model,
|
|
@@ -9,8 +9,6 @@ export interface AgentProgress {
|
|
|
9
9
|
turns: number;
|
|
10
10
|
tokens: { input: number; output: number };
|
|
11
11
|
status: "idle" | "running" | "completed" | "error";
|
|
12
|
-
/** Most recent partial assistant text (for live streaming display). */
|
|
13
|
-
partialText?: string;
|
|
14
12
|
}
|
|
15
13
|
|
|
16
14
|
export class ProgressTracker {
|
|
@@ -120,145 +118,7 @@ export class ProgressTracker {
|
|
|
120
118
|
getProgress(agentId: string): AgentProgress | undefined {
|
|
121
119
|
return this.sessions.get(agentId)?.progress;
|
|
122
120
|
}
|
|
123
|
-
|
|
124
|
-
// ── Child-process worker event bridge ──────────────────────────────────
|
|
125
|
-
//
|
|
126
|
-
// For child-process runtime, events arrive as raw JSON from the child pi
|
|
127
|
-
// process's stdout (via onJsonEvent). These methods bridge those events into
|
|
128
|
-
// the same crewEventBus stream that live-session uses, so the widget shows
|
|
129
|
-
// real-time tool calls and assistant text for BOTH runtimes.
|
|
130
|
-
|
|
131
|
-
private workerProgress = new Map<string, AgentProgress>();
|
|
132
|
-
|
|
133
|
-
/** Throttle: don't emit more than 1 progress event per 500ms per worker. */
|
|
134
|
-
private lastEmitTs = new Map<string, number>();
|
|
135
|
-
private static readonly EMIT_THROTTLE_MS = 500;
|
|
136
|
-
|
|
137
|
-
/**
|
|
138
|
-
* Handle a raw child-process JSON event (from onJsonEvent callback).
|
|
139
|
-
* Processes tool_execution_start/end, agent_start/end, and assistant text.
|
|
140
|
-
*/
|
|
141
|
-
handleWorkerEvent(taskId: string, runId: string, event: Record<string, unknown>): void {
|
|
142
|
-
let progress = this.workerProgress.get(taskId);
|
|
143
|
-
if (!progress) {
|
|
144
|
-
progress = {
|
|
145
|
-
toolCalls: 0,
|
|
146
|
-
currentTool: null,
|
|
147
|
-
toolStartTime: null,
|
|
148
|
-
errors: [],
|
|
149
|
-
turns: 0,
|
|
150
|
-
tokens: { input: 0, output: 0 },
|
|
151
|
-
status: "running",
|
|
152
|
-
};
|
|
153
|
-
this.workerProgress.set(taskId, progress);
|
|
154
|
-
}
|
|
155
|
-
|
|
156
|
-
const eventType = typeof event.type === "string" ? event.type : undefined;
|
|
157
|
-
|
|
158
|
-
switch (eventType) {
|
|
159
|
-
case "tool_execution_start": {
|
|
160
|
-
progress.toolCalls++;
|
|
161
|
-
progress.currentTool = typeof event.toolName === "string" ? event.toolName : "unknown";
|
|
162
|
-
progress.toolStartTime = Date.now();
|
|
163
|
-
this.emitThrottled(taskId, runId, progress);
|
|
164
|
-
break;
|
|
165
|
-
}
|
|
166
|
-
case "tool_execution_end": {
|
|
167
|
-
progress.currentTool = null;
|
|
168
|
-
progress.toolStartTime = null;
|
|
169
|
-
if (event.isError) {
|
|
170
|
-
progress.errors.push(String(event.result ?? "Unknown error"));
|
|
171
|
-
crewEventBus.emit({
|
|
172
|
-
type: "agent:error",
|
|
173
|
-
runId,
|
|
174
|
-
agentId: taskId,
|
|
175
|
-
payload: String(event.result ?? "Unknown error"),
|
|
176
|
-
timestamp: Date.now(),
|
|
177
|
-
});
|
|
178
|
-
}
|
|
179
|
-
this.emitThrottled(taskId, runId, progress);
|
|
180
|
-
break;
|
|
181
|
-
}
|
|
182
|
-
case "turn_start":
|
|
183
|
-
case "turn_end":
|
|
184
|
-
progress.turns++;
|
|
185
|
-
break;
|
|
186
|
-
case "agent_start":
|
|
187
|
-
progress.status = "running";
|
|
188
|
-
break;
|
|
189
|
-
case "agent_end":
|
|
190
|
-
case "agent_settled":
|
|
191
|
-
progress.status = "completed";
|
|
192
|
-
this.emitThrottled(taskId, runId, progress);
|
|
193
|
-
break;
|
|
194
|
-
case "message":
|
|
195
|
-
case "message_end": {
|
|
196
|
-
// Extract assistant text for streaming display.
|
|
197
|
-
const message = event.message as { role?: string; content?: unknown } | undefined;
|
|
198
|
-
if (message?.role === "assistant") {
|
|
199
|
-
const text = extractWorkerText(message.content);
|
|
200
|
-
if (text) {
|
|
201
|
-
progress.partialText = text.slice(-2000); // keep last 2K chars
|
|
202
|
-
this.emitThrottled(taskId, runId, progress);
|
|
203
|
-
}
|
|
204
|
-
}
|
|
205
|
-
// Track usage from message_end events.
|
|
206
|
-
if (eventType === "message_end" && event.usage && typeof event.usage === "object") {
|
|
207
|
-
const usage = event.usage as { input?: number; output?: number };
|
|
208
|
-
if (typeof usage.input === "number") progress.tokens.input += usage.input;
|
|
209
|
-
if (typeof usage.output === "number") progress.tokens.output += usage.output;
|
|
210
|
-
}
|
|
211
|
-
break;
|
|
212
|
-
}
|
|
213
|
-
}
|
|
214
|
-
}
|
|
215
|
-
|
|
216
|
-
/** Get the accumulated progress for a child-process worker task. */
|
|
217
|
-
getWorkerProgress(taskId: string): AgentProgress | undefined {
|
|
218
|
-
return this.workerProgress.get(taskId);
|
|
219
|
-
}
|
|
220
|
-
|
|
221
|
-
/** Remove a worker from tracking after completion. */
|
|
222
|
-
untrackWorker(taskId: string): void {
|
|
223
|
-
this.workerProgress.delete(taskId);
|
|
224
|
-
this.lastEmitTs.delete(taskId);
|
|
225
|
-
}
|
|
226
|
-
|
|
227
|
-
/**
|
|
228
|
-
* Emit a progress event to crewEventBus, throttled to avoid flooding
|
|
229
|
-
* the widget with re-renders (max 1 event per EMIT_THROTTLE_MS per worker).
|
|
230
|
-
*/
|
|
231
|
-
private emitThrottled(taskId: string, runId: string, progress: AgentProgress): void {
|
|
232
|
-
const now = Date.now();
|
|
233
|
-
const last = this.lastEmitTs.get(taskId) ?? 0;
|
|
234
|
-
if (now - last < ProgressTracker.EMIT_THROTTLE_MS) return;
|
|
235
|
-
this.lastEmitTs.set(taskId, now);
|
|
236
|
-
crewEventBus.emit({
|
|
237
|
-
type: "agent:progress",
|
|
238
|
-
runId,
|
|
239
|
-
agentId: taskId,
|
|
240
|
-
payload: { ...progress },
|
|
241
|
-
timestamp: now,
|
|
242
|
-
});
|
|
243
|
-
}
|
|
244
121
|
}
|
|
245
122
|
|
|
246
123
|
// Export singleton instance
|
|
247
124
|
export const globalProgressTracker = new ProgressTracker();
|
|
248
|
-
|
|
249
|
-
/**
|
|
250
|
-
* Extract text content from a worker message's content field.
|
|
251
|
-
* Handles string content and array-of-parts (like extractTextContent in context.ts).
|
|
252
|
-
*/
|
|
253
|
-
function extractWorkerText(content: unknown): string {
|
|
254
|
-
if (typeof content === "string") return content;
|
|
255
|
-
if (!Array.isArray(content)) return "";
|
|
256
|
-
return content
|
|
257
|
-
.map((part) =>
|
|
258
|
-
part && typeof part === "object" && !Array.isArray(part) && typeof (part as { text?: unknown }).text === "string"
|
|
259
|
-
? (part as { text: string }).text
|
|
260
|
-
: "",
|
|
261
|
-
)
|
|
262
|
-
.filter(Boolean)
|
|
263
|
-
.join("\n");
|
|
264
|
-
}
|
|
@@ -9,6 +9,7 @@
|
|
|
9
9
|
import { type Container, Text, visibleWidth } from "@earendil-works/pi-tui";
|
|
10
10
|
import type { CrewAgentRecord } from "../../runtime/crew-agent-runtime.ts";
|
|
11
11
|
import { truncateToWidth } from "../../utils/visual.ts";
|
|
12
|
+
import { spinnerFrame } from "../spinner.ts";
|
|
12
13
|
import type { CrewTheme } from "../theme-adapter.ts";
|
|
13
14
|
import { formatDuration, formatTokens, truncLine } from "../tool-render.ts";
|
|
14
15
|
import { briefToolResult, isBrief } from "./brief-mode.ts";
|
|
@@ -373,9 +374,25 @@ function renderAgentResult(result: Record<string, unknown>, options: unknown, th
|
|
|
373
374
|
// isPartial = agent still running
|
|
374
375
|
const isPartial = (options as Record<string, unknown>)?.isPartial === true;
|
|
375
376
|
if (isPartial && !ctx.expanded) {
|
|
376
|
-
const spinner = theme.fg("
|
|
377
|
-
const
|
|
378
|
-
|
|
377
|
+
const spinner = theme.fg("accent", spinnerFrame(d.agentId as string));
|
|
378
|
+
const parts: string[] = [theme.fg("muted", "working")];
|
|
379
|
+
|
|
380
|
+
// Extract progress from onUpdate content (formatCompactToolProgress output)
|
|
381
|
+
const progressText = extractContentText(result?.content);
|
|
382
|
+
if (progressText) {
|
|
383
|
+
const elapsedMatch = progressText.match(/elapsed=(\d+)s/);
|
|
384
|
+
const tokenMatch = progressText.match(/tokens=(\d+)/);
|
|
385
|
+
const toolMatch = progressText.match(/tool:\s+(\S+)/);
|
|
386
|
+
const elapsedSec = elapsedMatch ? parseInt(elapsedMatch[1]!, 10) : 0;
|
|
387
|
+
const tokens = tokenMatch ? parseInt(tokenMatch[1]!, 10) : 0;
|
|
388
|
+
const tps = elapsedSec > 0 ? Math.round(tokens / elapsedSec) : 0;
|
|
389
|
+
|
|
390
|
+
if (elapsedSec > 0) parts.push(theme.fg("dim", formatDuration(elapsedSec * 1000)));
|
|
391
|
+
if (tps > 0) parts.push(theme.fg("dim", `${formatTokens(tps)} tok/s`));
|
|
392
|
+
if (toolMatch) parts.push(theme.fg("dim", `· ${toolMatch[1]}`));
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
contentLines.push(padVisual(` ${spinner} ${parts.join(" ")}`, innerW));
|
|
379
396
|
return buildFrameBottom(contentLines, w, theme, "borderAccent", "borderAccent");
|
|
380
397
|
}
|
|
381
398
|
|
|
@@ -18,6 +18,7 @@ import { computeLiveDurationMs } from "../live-duration.ts";
|
|
|
18
18
|
// verbatim (no truncation/crash) — these are rare one-off states, not jitter.
|
|
19
19
|
const TOOLS_METRIC_WIDTH = 8; // "127 tools"
|
|
20
20
|
const TOKENS_METRIC_WIDTH = 10; // "1.2k tok", "12.3M tok"
|
|
21
|
+
const TPS_METRIC_WIDTH = 9; // "411 tok/s"
|
|
21
22
|
const CTX_METRIC_WIDTH = 7; // "100% ctx"
|
|
22
23
|
const DURATION_METRIC_WIDTH = 6; // "120.0s"
|
|
23
24
|
|
|
@@ -139,10 +140,19 @@ export function agentStats(agent: CrewAgentRecord, liveHandle?: LiveAgentHandle)
|
|
|
139
140
|
/* ignore */
|
|
140
141
|
}
|
|
141
142
|
const ms = computeLiveDurationMs(act);
|
|
143
|
+
if (total > 0 && ms > 1000) {
|
|
144
|
+
const tps = Math.round(total / (ms / 1000));
|
|
145
|
+
if (tps > 0) parts.push(alignMetric(`${formatTokensCompact(tps)}/s`, TPS_METRIC_WIDTH));
|
|
146
|
+
}
|
|
142
147
|
parts.push(alignMetric(`${(ms / 1000).toFixed(1)}s`, DURATION_METRIC_WIDTH));
|
|
143
148
|
} else {
|
|
144
149
|
if (agent.toolUses) parts.push(alignMetric(`${agent.toolUses} tools`, TOOLS_METRIC_WIDTH));
|
|
145
150
|
if (agent.progress?.tokens) parts.push(alignMetric(formatTokensCompact(agent.progress.tokens), TOKENS_METRIC_WIDTH));
|
|
151
|
+
const ageMs = agent.startedAt ? Math.max(0, Date.now() - new Date(agent.startedAt).getTime()) : 0;
|
|
152
|
+
if (agent.progress?.tokens && ageMs > 1000) {
|
|
153
|
+
const tps = Math.round(agent.progress.tokens / (ageMs / 1000));
|
|
154
|
+
if (tps > 0) parts.push(alignMetric(`${formatTokensCompact(tps)}/s`, TPS_METRIC_WIDTH));
|
|
155
|
+
}
|
|
146
156
|
const age = elapsed(agent.completedAt ?? agent.startedAt);
|
|
147
157
|
if (age) parts.push(alignMetric(age, DURATION_METRIC_WIDTH));
|
|
148
158
|
}
|