pi-crew 0.10.1 → 0.10.3
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 +347 -0
- package/NOTICE.md +21 -0
- package/README.md +44 -2
- package/agents/executor.md +1 -1
- package/agents/test-engineer.md +1 -1
- package/dist/index.mjs +2555 -932
- package/package.json +2 -1
- package/schema.json +503 -94
- package/skills/real-test-pi-crew/REPORT-TEMPLATE.md +6 -2
- package/skills/real-test-pi-crew/SKILL.md +278 -79
- package/src/config/config-merge.ts +11 -1
- package/src/config/config-validation.ts +47 -2
- package/src/config/config.ts +28 -6
- package/src/config/defaults.ts +43 -11
- package/src/config/env-vars.ts +27 -2
- package/src/config/role-tools.ts +4 -2
- package/src/config/types.ts +55 -1
- package/src/extension/crew-cleanup.ts +13 -0
- package/src/extension/crew-vibes/footer.ts +19 -0
- package/src/extension/crew-vibes/index.ts +11 -1
- package/src/extension/register.ts +8 -0
- package/src/extension/registration/command-registration.ts +1 -0
- package/src/extension/registration/commands/run.ts +15 -1
- package/src/extension/registration/commands/shared.ts +8 -0
- package/src/extension/registration/foreground-run-controller.ts +10 -2
- package/src/extension/registration/lifecycle-handlers.ts +92 -11
- package/src/extension/registration/runtime-cleanup.ts +23 -5
- package/src/extension/registration/team-tool.ts +58 -6
- package/src/extension/registration/ui.ts +5 -4
- package/src/extension/team-tool/doctor.ts +364 -7
- package/src/extension/team-tool/handle-settings.ts +19 -0
- package/src/extension/team-tool/inspect.ts +10 -2
- package/src/extension/team-tool/run-deadline.ts +20 -3
- package/src/extension/team-tool/run.ts +26 -2
- package/src/extension/team-tool/status.ts +7 -0
- package/src/extension/team-tool.ts +35 -2
- package/src/hooks/registry.ts +59 -56
- package/src/prompt/inbox-poll.ts +90 -0
- package/src/prompt/message-tool.ts +166 -0
- package/src/prompt/prompt-runtime.ts +201 -18
- package/src/prompt/surface-worker.ts +720 -0
- package/src/prompt/worker-events-channel.ts +49 -3
- package/src/runtime/async-runner.ts +29 -1
- package/src/runtime/background-runner.ts +13 -7
- package/src/runtime/broker/broker-issuer.ts +27 -2
- package/src/runtime/broker/crew-broker-tokens.ts +56 -4
- package/src/runtime/broker/crew-broker.ts +261 -41
- package/src/runtime/child-pi/child-pi-constants.ts +8 -0
- package/src/runtime/child-pi/child-pi-spawn.ts +23 -9
- package/src/runtime/child-pi/child-pi-streams.ts +30 -2
- package/src/runtime/child-pi/child-pi.ts +353 -5
- package/src/runtime/crew-agent-records.ts +13 -1
- package/src/runtime/detached-run-results.ts +90 -0
- package/src/runtime/dispatch-batch.ts +12 -1
- package/src/runtime/event-log-tail-source.ts +374 -0
- package/src/runtime/finalize-run.ts +4 -0
- package/src/runtime/goal-workflow/adaptive-plan.ts +30 -3
- package/src/runtime/goal-workflow/dynamic-workflow-runner.ts +5 -1
- package/src/runtime/live-session/live-agent-manager.ts +34 -1
- package/src/runtime/live-session/live-control-realtime.ts +10 -0
- package/src/runtime/live-session/live-session-runtime.ts +47 -27
- package/src/runtime/manifest-cache.ts +128 -17
- package/src/runtime/merge-gate.ts +25 -9
- package/src/runtime/model/model-fallback.ts +7 -3
- package/src/runtime/model/pi-args.ts +54 -65
- package/src/runtime/output/sidechain-output.ts +61 -6
- package/src/runtime/process/proc-stat.ts +46 -0
- package/src/runtime/process/zombie-scanner.ts +32 -19
- package/src/runtime/process-status.ts +16 -1
- package/src/runtime/recovery/crash-recovery.ts +16 -0
- package/src/runtime/run-tracker.ts +77 -10
- package/src/runtime/spawn-policy.ts +27 -41
- package/src/runtime/surface/degrade.ts +776 -0
- package/src/runtime/surface/herdr-provider.ts +546 -0
- package/src/runtime/surface/launch-script.ts +172 -0
- package/src/runtime/surface/resolve-surface.ts +274 -0
- package/src/runtime/surface/surface-provider.ts +129 -0
- package/src/runtime/surface/surface-spawn.ts +475 -0
- package/src/runtime/surface/tmux-provider.ts +400 -0
- package/src/runtime/task-runner/child-executor.ts +47 -0
- package/src/runtime/task-runner/post-execution.ts +57 -2
- package/src/runtime/task-runner/prompt-builder.ts +1 -0
- package/src/runtime/task-runner/retrieval-orchestrator.ts +191 -56
- package/src/runtime/task-runner/state-helpers.ts +54 -30
- package/src/runtime/task-runner.ts +4 -2
- package/src/runtime/team-runner.ts +104 -2
- package/src/schema/config-schema.ts +25 -1
- package/src/state/atomic-write.ts +219 -40
- package/src/state/coordination/locks.ts +7 -5
- package/src/state/coordination/mailbox.ts +56 -10
- package/src/state/event-log/cursor.ts +413 -23
- package/src/state/event-log/event-log.ts +120 -113
- package/src/state/event-log/sequence-cache.ts +21 -3
- package/src/state/stores/plan-store.ts +1 -1
- package/src/state/stores/state-store.ts +171 -9
- package/src/state/types.ts +53 -0
- package/src/ui/dock-footer.ts +49 -0
- package/src/ui/inline-panel/agent-pane.ts +378 -0
- package/src/ui/inline-panel/agent-transcript.ts +338 -0
- package/src/ui/inline-panel/agent-view-overlay.ts +225 -0
- package/src/ui/inline-panel/crew-editor.ts +192 -0
- package/src/ui/inline-panel/index.ts +290 -0
- package/src/ui/inline-panel/panel-rows.ts +37 -0
- package/src/ui/inline-panel/panel-selection.ts +157 -0
- package/src/ui/inline-panel/panel-store.ts +111 -0
- package/src/ui/inline-panel/view-session-store.ts +36 -0
- package/src/ui/pi-ui-compat.ts +9 -0
- package/src/ui/render-diff.ts +16 -8
- package/src/ui/run-dashboard.ts +87 -42
- package/src/ui/run-event-bus.ts +10 -1
- package/src/ui/run-snapshot-cache.ts +83 -35
- package/src/ui/transcript-cache.ts +101 -13
- package/src/ui/transcript-viewer.ts +92 -24
- package/src/ui/widget/index.ts +203 -25
- package/src/ui/widget/task-list.ts +198 -0
- package/src/ui/widget/widget-formatters.ts +240 -4
- package/src/ui/widget/widget-renderer.ts +234 -37
- package/src/ui/widget/widget-types.ts +11 -0
- package/src/utils/child-process-shield.ts +106 -0
- package/src/utils/redaction.ts +7 -0
- package/src/utils/safe-abort.ts +45 -0
- package/src/utils/visual.ts +43 -0
- package/src/workflows/discover-workflows.ts +1 -0
- package/src/workflows/workflow-config.ts +7 -0
- package/src/worktree/worktree-manager.ts +65 -4
- package/workflows/default.workflow.md +36 -26
- package/workflows/strict-fast-fix.workflow.md +26 -0
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
import type { CrewAgentRecord } from "../../runtime/crew-agent-runtime.ts";
|
|
8
8
|
import type { LiveAgentHandle } from "../../runtime/live-session/live-agent-manager.ts";
|
|
9
9
|
import { getTaskUsage } from "../../runtime/usage-tracker.ts";
|
|
10
|
-
import { visibleWidth } from "../../utils/visual.ts";
|
|
10
|
+
import { truncateToWidth, visibleWidth } from "../../utils/visual.ts";
|
|
11
11
|
import { computeLiveDurationMs } from "../live-duration.ts";
|
|
12
12
|
|
|
13
13
|
// ── No-color mode (UI-10) ─────────────────────────────────────────────
|
|
@@ -75,6 +75,7 @@ const TOKENS_METRIC_WIDTH = 10; // "1.2k tok", "12.3M tok"
|
|
|
75
75
|
const TPS_METRIC_WIDTH = 9; // "411 tok/s"
|
|
76
76
|
const CTX_METRIC_WIDTH = 7; // "100% ctx"
|
|
77
77
|
const DURATION_METRIC_WIDTH = 6; // "120.0s"
|
|
78
|
+
const COST_METRIC_WIDTH = 9; // "$0.001234"
|
|
78
79
|
|
|
79
80
|
function alignMetric(value: string, width: number): string {
|
|
80
81
|
const pad = Math.max(0, width - visibleWidth(value));
|
|
@@ -82,6 +83,11 @@ function alignMetric(value: string, width: number): string {
|
|
|
82
83
|
}
|
|
83
84
|
|
|
84
85
|
export function formatTokensCompact(count: number): string {
|
|
86
|
+
// Display-layer guard: state records have at least once carried the
|
|
87
|
+
// literal "***" in a numeric field (redaction false-positive, fixed at
|
|
88
|
+
// the source). A string here would print `*** tok` verbatim, so non-
|
|
89
|
+
// numeric/undefined input renders as an empty metric instead.
|
|
90
|
+
if (typeof count !== "number" || !Number.isFinite(count)) return "";
|
|
85
91
|
if (count >= 1_000_000) return `${(count / 1_000_000).toFixed(1)}M tok`;
|
|
86
92
|
if (count >= 1_000) return `${(count / 1_000).toFixed(1)}k tok`;
|
|
87
93
|
return `${count} tok`;
|
|
@@ -99,6 +105,13 @@ export function elapsed(iso: string | undefined, now = Date.now()): string | und
|
|
|
99
105
|
return `${Math.floor(ms / 3_600_000)}h`;
|
|
100
106
|
}
|
|
101
107
|
|
|
108
|
+
/** pi-subtask's always-on elapsed tail: `0s` from the start (its fork rows
|
|
109
|
+
* flatten sub-second ages to 0 — not the legacy widget's "now"). */
|
|
110
|
+
export function dockElapsed(iso: string | undefined, now = Date.now()): string {
|
|
111
|
+
const value = elapsed(iso, now);
|
|
112
|
+
return value === undefined ? "" : value === "now" ? "0s" : value;
|
|
113
|
+
}
|
|
114
|
+
|
|
102
115
|
// ── Agent activity description ────────────────────────────────────────
|
|
103
116
|
|
|
104
117
|
const TOOL_LABELS: Record<string, string> = {
|
|
@@ -176,6 +189,218 @@ export function agentActivity(agent: CrewAgentRecord, liveHandle?: LiveAgentHand
|
|
|
176
189
|
return "done";
|
|
177
190
|
}
|
|
178
191
|
|
|
192
|
+
// ── Per-agent cost ────────────────────────────────────────────────────
|
|
193
|
+
|
|
194
|
+
/**
|
|
195
|
+
* Compact per-agent spend for widget rows, or "" when there is nothing to
|
|
196
|
+
* show. `formatCost`'s 6-decimal sub-cent output (`$0.001000`) wastes a third
|
|
197
|
+
* of a one-line row, so the widget uses a short form: cent precision in
|
|
198
|
+
* dollars, milli-precision below, and a `< $0.001` floor.
|
|
199
|
+
*/
|
|
200
|
+
export function formatCostCompact(cost: number): string {
|
|
201
|
+
if (cost >= 1) return `$${cost.toFixed(2)}`;
|
|
202
|
+
if (cost >= 0.01) return `$${cost.toFixed(3).replace(/0+$/, "").replace(/\.$/, "")}`;
|
|
203
|
+
if (cost >= 0.001) return `$${cost.toFixed(4).replace(/0+$/, "").replace(/\.$/, "")}`;
|
|
204
|
+
return "< $0.001";
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
/**
|
|
208
|
+
* Formatted per-agent spend, or "" when there is nothing to show. The value
|
|
209
|
+
* already lives on the durable task record and the dashboard agents pane has
|
|
210
|
+
* shown it since Round 17; the widget omitted it only by oversight.
|
|
211
|
+
*/
|
|
212
|
+
export function agentCost(agent: CrewAgentRecord): string {
|
|
213
|
+
const cost = agent.usage?.cost;
|
|
214
|
+
if (typeof cost !== "number" || !Number.isFinite(cost) || cost <= 0) return "";
|
|
215
|
+
return formatCostCompact(cost);
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
// ── pi-subtask dock formatters ─────────────────────────────────────────
|
|
219
|
+
|
|
220
|
+
/**
|
|
221
|
+
* pi-subtask's FIXED per-status glyphs (`statusIcon` in source/pi-subtask:
|
|
222
|
+
* starting ○, running ✻, done ✓, failed ✗, stopped ■). The compact dock
|
|
223
|
+
* deliberately does NOT spin the running marker like the legacy widget —
|
|
224
|
+
* the row set is stable across ticks, which is what makes the keyboard
|
|
225
|
+
* cursor feel anchored.
|
|
226
|
+
*/
|
|
227
|
+
export function dockStatusIcon(status: string): string {
|
|
228
|
+
switch (status) {
|
|
229
|
+
case "running":
|
|
230
|
+
return "✻";
|
|
231
|
+
case "queued":
|
|
232
|
+
case "waiting":
|
|
233
|
+
return "○";
|
|
234
|
+
case "completed":
|
|
235
|
+
return "✓";
|
|
236
|
+
case "failed":
|
|
237
|
+
return "✗";
|
|
238
|
+
case "needs_attention":
|
|
239
|
+
return "⚠";
|
|
240
|
+
case "cancelled":
|
|
241
|
+
case "stopped":
|
|
242
|
+
return "■";
|
|
243
|
+
default:
|
|
244
|
+
return "?";
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
/** pi-subtask uses the status word as a finished row's activity. */
|
|
249
|
+
export function dockStatusLabel(status: string): string {
|
|
250
|
+
switch (status) {
|
|
251
|
+
case "completed":
|
|
252
|
+
return "done";
|
|
253
|
+
case "failed":
|
|
254
|
+
return "failed";
|
|
255
|
+
case "cancelled":
|
|
256
|
+
case "stopped":
|
|
257
|
+
return "stopped";
|
|
258
|
+
case "needs_attention":
|
|
259
|
+
return "needs attention";
|
|
260
|
+
case "queued":
|
|
261
|
+
return "queued";
|
|
262
|
+
case "waiting":
|
|
263
|
+
return "waiting";
|
|
264
|
+
default:
|
|
265
|
+
return status;
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
/** pi-subtask's formatTokens: raw under 1k, `Nk` under 1M, `N.M` above. */
|
|
270
|
+
export function tokenCountShort(count: number): string {
|
|
271
|
+
if (count < 1_000) return `${count}`;
|
|
272
|
+
if (count < 1_000_000) return `${Math.round(count / 1_000)}k`;
|
|
273
|
+
return `${(count / 1_000_000).toFixed(1)}M`;
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
export interface DockUsageOptions {
|
|
277
|
+
/** While the agent's pane is open: add tok/s + context % like pi-subtask. */
|
|
278
|
+
viewed?: boolean;
|
|
279
|
+
/** Model context window for the `P% / N` gauge; omitted when unknown. */
|
|
280
|
+
contextWindow?: number;
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
/**
|
|
284
|
+
* pi-subtask's footer-style usage (formatUsage): `↑in ↓out Rcache CH% $cost`,
|
|
285
|
+
* plus `N tok/s` and `P% / window` while viewed. Live agents have split
|
|
286
|
+
* input/output/cacheWrite via the usage tracker; non-live ones show the
|
|
287
|
+
* durable token total + cost.
|
|
288
|
+
*/
|
|
289
|
+
export function dockUsageText(agent: CrewAgentRecord, liveHandle?: LiveAgentHandle, options: DockUsageOptions = {}): string {
|
|
290
|
+
const parts: string[] = [];
|
|
291
|
+
if (liveHandle) {
|
|
292
|
+
const usage = getTaskUsage(liveHandle.taskId);
|
|
293
|
+
const input = usage.input ?? 0;
|
|
294
|
+
const output = usage.output ?? 0;
|
|
295
|
+
const cacheWrite = usage.cacheWrite ?? 0;
|
|
296
|
+
if (input > 0) parts.push(`↑${tokenCountShort(input)}`);
|
|
297
|
+
if (output > 0) parts.push(`↓${tokenCountShort(output)}`);
|
|
298
|
+
if (cacheWrite > 0) parts.push(`R${tokenCountShort(cacheWrite)}`);
|
|
299
|
+
const promptTokens = input + cacheWrite;
|
|
300
|
+
if (cacheWrite > 0 && promptTokens > 0) {
|
|
301
|
+
parts.push(`CH${((cacheWrite / promptTokens) * 100).toFixed(1)}%`);
|
|
302
|
+
}
|
|
303
|
+
const cost = agent.usage?.cost;
|
|
304
|
+
if (typeof cost === "number" && Number.isFinite(cost) && cost > 0) parts.push(`$${cost.toFixed(4)}`);
|
|
305
|
+
if (options.viewed) {
|
|
306
|
+
const act = liveHandle.activity;
|
|
307
|
+
const ms = computeLiveDurationMs(act);
|
|
308
|
+
const totalTokens = input + output + cacheWrite;
|
|
309
|
+
if (totalTokens > 0 && ms > 1000) {
|
|
310
|
+
const tps = Math.round(totalTokens / (ms / 1000));
|
|
311
|
+
if (tps > 0) parts.push(`${tps} tok/s`);
|
|
312
|
+
}
|
|
313
|
+
try {
|
|
314
|
+
const ctxPct = liveHandle.session.getSessionStats?.()?.contextUsage?.percent;
|
|
315
|
+
if (ctxPct != null) {
|
|
316
|
+
const window =
|
|
317
|
+
options.contextWindow && options.contextWindow >= 1_000_000
|
|
318
|
+
? `${(options.contextWindow / 1_000_000).toFixed(1)}M`
|
|
319
|
+
: options.contextWindow
|
|
320
|
+
? tokenCountShort(options.contextWindow)
|
|
321
|
+
: "";
|
|
322
|
+
parts.push(`${Math.round(ctxPct)}%${window ? ` / ${window}` : ""}`);
|
|
323
|
+
}
|
|
324
|
+
} catch {
|
|
325
|
+
/* ignore */
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
return parts.join(" ");
|
|
329
|
+
}
|
|
330
|
+
const tokens = agent.progress?.tokens;
|
|
331
|
+
const tokenCount = typeof tokens === "number" && Number.isFinite(tokens) && tokens > 0 ? tokens : 0;
|
|
332
|
+
if (tokenCount > 0) parts.push(`${tokenCountShort(tokenCount)} tok`);
|
|
333
|
+
const cost = agent.usage?.cost;
|
|
334
|
+
if (typeof cost === "number" && Number.isFinite(cost) && cost > 0) parts.push(`$${cost.toFixed(4)}`);
|
|
335
|
+
return parts.join(" ");
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
// ── Adaptive single-line row ───────────────────────────────────────────
|
|
339
|
+
|
|
340
|
+
export interface BudgetedRowParts {
|
|
341
|
+
/** Marker + glyph prefix; never trimmed. */
|
|
342
|
+
lead: string;
|
|
343
|
+
/** Agent label; grows into whatever the activity leaves over. */
|
|
344
|
+
name: string;
|
|
345
|
+
/** Current activity; shrinks first but keeps a readable floor. */
|
|
346
|
+
activity: string;
|
|
347
|
+
/** Metrics tail; never trimmed, so numbers stay comparable across ticks. */
|
|
348
|
+
suffix: string;
|
|
349
|
+
separator?: string;
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
/** Smallest activity/name slice still worth showing before we drop the field. */
|
|
353
|
+
const MIN_FIELD_WIDTH = 12;
|
|
354
|
+
|
|
355
|
+
/**
|
|
356
|
+
* Assemble one row that fills `width` without wrapping.
|
|
357
|
+
*
|
|
358
|
+
* `lead` and `suffix` are fixed costs; the remaining budget is split between
|
|
359
|
+
* `name` and `activity`. The activity absorbs the trimming first (it changes
|
|
360
|
+
* every tick anyway) but keeps a MIN_FIELD_WIDTH floor, and the name expands
|
|
361
|
+
* into the rest up to its natural length — so a 200-column terminal shows the
|
|
362
|
+
* full description instead of the same clip an 80-column one gets.
|
|
363
|
+
*
|
|
364
|
+
* The closing truncate is a hard guard, not an optimisation: pi's renderer
|
|
365
|
+
* throws on a line wider than the terminal.
|
|
366
|
+
*/
|
|
367
|
+
export function budgetedRow(parts: BudgetedRowParts, width: number): string {
|
|
368
|
+
const sep = parts.separator ?? " · ";
|
|
369
|
+
const { lead, suffix } = parts;
|
|
370
|
+
const name = parts.name.replace(/\s+/g, " ").trim();
|
|
371
|
+
const activity = parts.activity.replace(/\s+/g, " ").trim();
|
|
372
|
+
if (!name && !activity) return truncateToWidth(lead + suffix, width);
|
|
373
|
+
if (!activity) return fitNameOnly(lead, name, suffix, width);
|
|
374
|
+
if (!name) return fitNameOnly(lead, activity, suffix, width);
|
|
375
|
+
|
|
376
|
+
const budget = width - visibleWidth(lead) - visibleWidth(suffix) - visibleWidth(sep);
|
|
377
|
+
// Too narrow to hold name + activity above their floors: keep the name
|
|
378
|
+
// only, and never let the metrics tail eat the clipping.
|
|
379
|
+
if (budget < MIN_FIELD_WIDTH * 2) return fitNameOnly(lead, name, suffix, width);
|
|
380
|
+
|
|
381
|
+
const nameNatural = visibleWidth(name);
|
|
382
|
+
const activityNatural = visibleWidth(activity);
|
|
383
|
+
const activityRoom = Math.min(activityNatural, Math.max(MIN_FIELD_WIDTH, budget - nameNatural));
|
|
384
|
+
const nameRoom = Math.max(MIN_FIELD_WIDTH, budget - activityRoom);
|
|
385
|
+
const assembled = lead + truncateToWidth(name, nameRoom) + sep + truncateToWidth(activity, activityRoom) + suffix;
|
|
386
|
+
// The room arithmetic keeps this ≤ width by construction (nameRoom +
|
|
387
|
+
// activityRoom ≤ budget in every branch); this guard is the last line of
|
|
388
|
+
// defense because pi's renderer throws on over-width lines.
|
|
389
|
+
if (visibleWidth(assembled) <= width) return assembled;
|
|
390
|
+
return fitNameOnly(lead, name, suffix, width);
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
/**
|
|
394
|
+
* Narrow-terminal fallback: fixed lead + suffix, the name absorbs every
|
|
395
|
+
* remaining column. The metrics tail is sacred — it must stay comparable
|
|
396
|
+
* across ticks — so clipping always happens in the middle fields.
|
|
397
|
+
*/
|
|
398
|
+
function fitNameOnly(lead: string, name: string, suffix: string, width: number): string {
|
|
399
|
+
const fixed = visibleWidth(lead) + visibleWidth(suffix);
|
|
400
|
+
if (fixed >= width) return truncateToWidth(lead + suffix, width);
|
|
401
|
+
return lead + truncateToWidth(name, width - fixed) + suffix;
|
|
402
|
+
}
|
|
403
|
+
|
|
179
404
|
// ── Agent stats line ──────────────────────────────────────────────────
|
|
180
405
|
|
|
181
406
|
export function agentStats(agent: CrewAgentRecord, liveHandle?: LiveAgentHandle): string {
|
|
@@ -186,6 +411,10 @@ export function agentStats(agent: CrewAgentRecord, liveHandle?: LiveAgentHandle)
|
|
|
186
411
|
const usage = getTaskUsage(liveHandle.taskId);
|
|
187
412
|
const total = (usage.input ?? 0) + (usage.output ?? 0) + (usage.cacheWrite ?? 0);
|
|
188
413
|
if (total > 0) parts.push(alignMetric(formatTokensCompact(total), TOKENS_METRIC_WIDTH));
|
|
414
|
+
// The live usage tracker carries tokens only (LifetimeUsage has no cost
|
|
415
|
+
// field), so cost always comes off the durable task record.
|
|
416
|
+
const liveCost = agentCost(agent);
|
|
417
|
+
if (liveCost) parts.push(alignMetric(liveCost, COST_METRIC_WIDTH));
|
|
189
418
|
try {
|
|
190
419
|
const stats = liveHandle.session.getSessionStats?.();
|
|
191
420
|
const ctxPct = stats?.contextUsage?.percent;
|
|
@@ -200,11 +429,18 @@ export function agentStats(agent: CrewAgentRecord, liveHandle?: LiveAgentHandle)
|
|
|
200
429
|
}
|
|
201
430
|
parts.push(alignMetric(`${(ms / 1000).toFixed(1)}s`, DURATION_METRIC_WIDTH));
|
|
202
431
|
} else {
|
|
432
|
+
// Type-narrowed: state has carried the literal "***" (redaction
|
|
433
|
+
// false-positive) in progress.tokens, which is truthy but not a count.
|
|
434
|
+
// Only real numbers produce metrics; formatTokensCompact guards too.
|
|
435
|
+
const tokens = agent.progress?.tokens;
|
|
436
|
+
const tokenCount = typeof tokens === "number" ? tokens : undefined;
|
|
203
437
|
if (agent.toolUses) parts.push(alignMetric(`${agent.toolUses} tools`, TOOLS_METRIC_WIDTH));
|
|
204
|
-
if (
|
|
438
|
+
if (tokenCount && tokenCount > 0) parts.push(alignMetric(formatTokensCompact(tokenCount), TOKENS_METRIC_WIDTH));
|
|
439
|
+
const cost = agentCost(agent);
|
|
440
|
+
if (cost) parts.push(alignMetric(cost, COST_METRIC_WIDTH));
|
|
205
441
|
const ageMs = agent.startedAt ? Math.max(0, Date.now() - new Date(agent.startedAt).getTime()) : 0;
|
|
206
|
-
if (
|
|
207
|
-
const tps = Math.round(
|
|
442
|
+
if (tokenCount && tokenCount > 0 && ageMs > 1000) {
|
|
443
|
+
const tps = Math.round(tokenCount / (ageMs / 1000));
|
|
208
444
|
if (tps > 0) parts.push(alignMetric(`${formatTokensCompact(tps)}/s`, TPS_METRIC_WIDTH));
|
|
209
445
|
}
|
|
210
446
|
const age = elapsed(agent.completedAt ?? agent.startedAt);
|
|
@@ -4,19 +4,30 @@
|
|
|
4
4
|
* Extracted from crew-widget.ts.
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
|
+
import type { CrewAgentRecord } from "../../runtime/crew-agent-runtime.ts";
|
|
7
8
|
import { listLiveAgents } from "../../runtime/live-session/live-agent-manager.ts";
|
|
8
9
|
import { isPlanApprovalStatePending } from "../../runtime/plan-approval.ts";
|
|
9
10
|
import { isFinishedRunStatus } from "../../runtime/process-status.ts";
|
|
11
|
+
import type { TeamRunManifest } from "../../state/types.ts";
|
|
10
12
|
import { truncate } from "../../utils/visual.ts";
|
|
11
13
|
import { Box, Text } from "../layout-primitives.ts";
|
|
12
14
|
import { spinnerFrame } from "../spinner.ts";
|
|
13
15
|
import { colorizeStatusGlyphs, iconForStatus } from "../status-colors.ts";
|
|
14
16
|
import type { CrewTheme } from "../theme-adapter.ts";
|
|
15
|
-
import {
|
|
17
|
+
import {
|
|
18
|
+
agentActivity,
|
|
19
|
+
agentStats,
|
|
20
|
+
budgetedRow,
|
|
21
|
+
dockElapsed,
|
|
22
|
+
dockStatusIcon,
|
|
23
|
+
dockStatusLabel,
|
|
24
|
+
dockUsageText,
|
|
25
|
+
notificationBadge,
|
|
26
|
+
} from "./widget-formatters.ts";
|
|
16
27
|
import { activeWidgetRuns, shortRunLabel } from "./widget-model.ts";
|
|
17
28
|
import type { WidgetRun } from "./widget-types.ts";
|
|
18
29
|
|
|
19
|
-
const MAX_AGENTS_DISPLAY = 3;
|
|
30
|
+
export const MAX_AGENTS_DISPLAY = 3;
|
|
20
31
|
const FINISHED_LINGER_MAX_AGE = 1;
|
|
21
32
|
/** Default terminal width when caller doesn't pass one explicitly. Keep <= 116
|
|
22
33
|
* (the same default used elsewhere in pi-crew tool renderers) so we never paint
|
|
@@ -43,8 +54,192 @@ export function widgetHeader(runs: WidgetRun[], runningGlyph: string, maxLines =
|
|
|
43
54
|
return `${runningGlyph} Crew agents${notificationBadge(notificationCount)} · ${parts.join(" · ")} · /team-dashboard`;
|
|
44
55
|
}
|
|
45
56
|
|
|
57
|
+
// ── Agent ordering (shared with the inline panel) ──────────────────────
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* L-4: prioritize RUNNING > QUEUED > WAITING so the most relevant live workers
|
|
61
|
+
* are always shown first. Finished rows fill only the leftover budget and never
|
|
62
|
+
* steal a slot from an active agent.
|
|
63
|
+
*/
|
|
64
|
+
const ACTIVE_PRIORITY: Record<string, number> = { running: 0, queued: 1, waiting: 2 };
|
|
65
|
+
|
|
66
|
+
function isActiveStatus(status: string): boolean {
|
|
67
|
+
return status === "running" || status === "queued" || status === "waiting";
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* The agent order the widget paints, split into its two sections.
|
|
72
|
+
*
|
|
73
|
+
* Exported because the inline panel navigates the same list: if the panel
|
|
74
|
+
* derived its own order, the cursor index would drift from the rendered rows.
|
|
75
|
+
* One function, one order.
|
|
76
|
+
*/
|
|
77
|
+
export function orderWidgetAgents(entry: WidgetRun, now = Date.now()): { active: CrewAgentRecord[]; finished: CrewAgentRecord[] } {
|
|
78
|
+
const runDone = isFinishedRunStatus(entry.run.status);
|
|
79
|
+
const active = entry.agents.filter((agent) => isActiveStatus(agent.status));
|
|
80
|
+
const finished = entry.agents.filter((agent) => {
|
|
81
|
+
if (isActiveStatus(agent.status)) return false;
|
|
82
|
+
if (!agent.completedAt) return false;
|
|
83
|
+
// Mid-run, finished agents are the run's HISTORY: they must stay in
|
|
84
|
+
// the dock until the RUN itself is done, not age out after a minute
|
|
85
|
+
// while later phases are still working. The linger windows below only
|
|
86
|
+
// apply once the run reached a terminal status (and the run-level
|
|
87
|
+
// visibility grace then decides how much longer the dock shows at all).
|
|
88
|
+
if (!runDone) return true;
|
|
89
|
+
const maxAgeMs = (ERROR_STATUSES.has(agent.status) ? ERROR_LINGER_MAX_AGE : FINISHED_LINGER_MAX_AGE) * 60_000;
|
|
90
|
+
const age = now - new Date(agent.completedAt).getTime();
|
|
91
|
+
return Number.isFinite(age) && age < maxAgeMs;
|
|
92
|
+
});
|
|
93
|
+
return {
|
|
94
|
+
active: [...active].sort((a, b) => (ACTIVE_PRIORITY[a.status] ?? 9) - (ACTIVE_PRIORITY[b.status] ?? 9)),
|
|
95
|
+
finished,
|
|
96
|
+
};
|
|
97
|
+
}
|
|
98
|
+
|
|
46
99
|
// ── Line builder ──────────────────────────────────────────────────────
|
|
47
100
|
|
|
101
|
+
/**
|
|
102
|
+
* Row layout for the per-agent lines.
|
|
103
|
+
*
|
|
104
|
+
* - `detailed` — the historical two-line tree (name row + `⊶ activity` row).
|
|
105
|
+
* - `compact` — one width-budgeted line per agent, so a wide terminal shows the
|
|
106
|
+
* full description instead of the same clip a narrow one gets.
|
|
107
|
+
*/
|
|
108
|
+
export type WidgetRowStyle = "compact" | "detailed";
|
|
109
|
+
|
|
110
|
+
export interface WidgetRenderOptions {
|
|
111
|
+
rowStyle?: WidgetRowStyle;
|
|
112
|
+
/** Task id under the inline panel cursor, if any. */
|
|
113
|
+
selectedTaskId?: string;
|
|
114
|
+
/** Task id whose transcript pane is open, if any. */
|
|
115
|
+
viewedTaskId?: string;
|
|
116
|
+
/**
|
|
117
|
+
* True while the inline panel holds the cursor. Every agent is then listed
|
|
118
|
+
* (no MAX_AGENTS_DISPLAY cap) so keyboard navigation can reach all of them;
|
|
119
|
+
* the idle widget stays capped to keep the prompt area small.
|
|
120
|
+
*/
|
|
121
|
+
focused?: boolean;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/** Short display form of a model id: `zai/glm-5.3` → `glm-5.3`. */
|
|
125
|
+
function shortModelLabel(agent: CrewAgentRecord, run: TeamRunManifest): string | undefined {
|
|
126
|
+
const model = agent.model ?? run.modelContext?.parentModel ?? run.modelContext?.override;
|
|
127
|
+
if (typeof model !== "string" || !model) return undefined;
|
|
128
|
+
return model.split("/").at(-1) ?? model;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/** One flat dock row (pi-subtask style) for an agent — active or finished. */
|
|
132
|
+
function compactAgentRow(
|
|
133
|
+
run: TeamRunManifest,
|
|
134
|
+
agent: CrewAgentRecord,
|
|
135
|
+
finished: boolean,
|
|
136
|
+
runs: readonly WidgetRun[],
|
|
137
|
+
options: WidgetRenderOptions,
|
|
138
|
+
width: number,
|
|
139
|
+
liveHandle: ReturnType<typeof listLiveAgents>[number] | undefined,
|
|
140
|
+
): string {
|
|
141
|
+
const marker = options.selectedTaskId === agent.taskId ? "❯" : " ";
|
|
142
|
+
const dockGlyph = options.viewedTaskId === agent.taskId ? "⏺" : dockStatusIcon(agent.status);
|
|
143
|
+
const name = liveHandle?.agent ?? agent.agent;
|
|
144
|
+
const label = liveHandle?.description ?? agent.role ?? "";
|
|
145
|
+
// Task-first: the agent exists to run its task, so the row names the task
|
|
146
|
+
// right after the agent. With multiple runs, prefix each row with its run
|
|
147
|
+
// label so the flat dock still says which run an agent belongs to.
|
|
148
|
+
const runTag = runs.length > 1 ? `${shortRunLabel(run)} · ` : "";
|
|
149
|
+
const taskTag = agent.taskId ? ` · ${agent.taskId}` : "";
|
|
150
|
+
const roleTag = label && label !== agent.taskId && label !== name ? ` · ${label}` : "";
|
|
151
|
+
const nameText = runTag + name + taskTag + roleTag;
|
|
152
|
+
// pi-subtask activity: the worker's latest line while running, otherwise
|
|
153
|
+
// the status word.
|
|
154
|
+
const liveLine = liveHandle?.activity?.responseText
|
|
155
|
+
?.split("\n")
|
|
156
|
+
.find((line) => line.trim())
|
|
157
|
+
?.trim();
|
|
158
|
+
const activity =
|
|
159
|
+
!finished && liveHandle?.status === "running" && liveLine
|
|
160
|
+
? liveLine.length > 60
|
|
161
|
+
? `${liveLine.slice(0, 60)}…`
|
|
162
|
+
: liveLine
|
|
163
|
+
: finished
|
|
164
|
+
? dockStatusLabel(agent.status)
|
|
165
|
+
: agentActivity(agent, liveHandle);
|
|
166
|
+
const usage = dockUsageText(agent, liveHandle, { viewed: options.viewedTaskId === agent.taskId });
|
|
167
|
+
const ageText = dockElapsed(agent.completedAt ?? agent.startedAt);
|
|
168
|
+
const model = shortModelLabel(agent, run);
|
|
169
|
+
// Stats tail: `· glm-5.3 · ↑1.2k ↓350 · 41s` — the model the worker is
|
|
170
|
+
// actually on first, then usage, then elapsed.
|
|
171
|
+
const suffix = `${model ? ` · ${model}` : ""}${usage ? ` · ${usage}` : ""}${ageText ? ` · ${ageText}` : ""}`;
|
|
172
|
+
return budgetedRow({ lead: `${marker} ${dockGlyph} `, name: nameText, activity, suffix }, width);
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
/**
|
|
176
|
+
* The compact dock: hint line, the `main` conversation row, then a
|
|
177
|
+
* MAX_AGENTS_DISPLAY-row SCROLL WINDOW over the flat agent list (exactly the
|
|
178
|
+
* order the inline panel navigates — `panelRowsFromRuns` parity). The window
|
|
179
|
+
* follows the panel selection bottom-pinned: moving the cursor past the
|
|
180
|
+
* window scrolls one row at a time and the ❯ marker is always painted; idle
|
|
181
|
+
* (no selection) shows the top of the list, which the shared ordering puts at
|
|
182
|
+
* the highest-priority live agents. Hidden rows surface as `… ↑N earlier` /
|
|
183
|
+
* `… +N more` indicators.
|
|
184
|
+
*/
|
|
185
|
+
function compactDockLines(
|
|
186
|
+
runs: WidgetRun[],
|
|
187
|
+
options: WidgetRenderOptions,
|
|
188
|
+
width: number,
|
|
189
|
+
maxLines: number,
|
|
190
|
+
notificationCount: number,
|
|
191
|
+
runningGlyph: string,
|
|
192
|
+
): string[] {
|
|
193
|
+
const now = Date.now();
|
|
194
|
+
const flat: Array<{
|
|
195
|
+
run: TeamRunManifest;
|
|
196
|
+
agent: CrewAgentRecord;
|
|
197
|
+
finished: boolean;
|
|
198
|
+
liveHandle: ReturnType<typeof listLiveAgents>[number] | undefined;
|
|
199
|
+
}> = [];
|
|
200
|
+
for (const entry of runs) {
|
|
201
|
+
const { active, finished } = orderWidgetAgents(entry, now);
|
|
202
|
+
const liveForRun = listLiveAgents().filter((a) => a.runId === entry.run.runId);
|
|
203
|
+
for (const agent of active) {
|
|
204
|
+
flat.push({ run: entry.run, agent, finished: false, liveHandle: liveForRun.find((h) => h.taskId === agent.taskId) });
|
|
205
|
+
}
|
|
206
|
+
for (const agent of finished) {
|
|
207
|
+
flat.push({ run: entry.run, agent, finished: true, liveHandle: liveForRun.find((h) => h.taskId === agent.taskId) });
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
// No agents at all: fall back to the legacy header so the space under the
|
|
212
|
+
// editor is never just blank.
|
|
213
|
+
if (flat.length === 0) return [widgetHeader(runs, runningGlyph, maxLines, notificationCount)];
|
|
214
|
+
|
|
215
|
+
const lines: string[] = [];
|
|
216
|
+
let hint: string;
|
|
217
|
+
if (options.viewedTaskId) {
|
|
218
|
+
const viewedName = flat.find((row) => row.agent.taskId === options.viewedTaskId)?.agent.agent ?? "agent";
|
|
219
|
+
hint = `viewing @${viewedName} — typing goes to the agent · ↓ switch · esc back`;
|
|
220
|
+
} else if (options.focused) {
|
|
221
|
+
hint = "enter to view · x to stop/cancel · esc back";
|
|
222
|
+
} else {
|
|
223
|
+
hint = `agents (${flat.length}) — ↓ to select`;
|
|
224
|
+
}
|
|
225
|
+
lines.push(truncate(hint, width));
|
|
226
|
+
// Filled ● = you're on the main conversation; hollow ◯ = an agent view is
|
|
227
|
+
// open (same convention as pi-subtask's main row).
|
|
228
|
+
const mainMarker = options.focused && !options.selectedTaskId ? "❯" : " ";
|
|
229
|
+
const mainIcon = options.viewedTaskId ? "◯" : "●";
|
|
230
|
+
lines.push(truncate(`${mainMarker} ${mainIcon} main`, width));
|
|
231
|
+
|
|
232
|
+
const selectedIndex = options.selectedTaskId ? flat.findIndex((row) => row.agent.taskId === options.selectedTaskId) : -1;
|
|
233
|
+
const windowStart = selectedIndex >= 0 ? Math.max(0, selectedIndex - MAX_AGENTS_DISPLAY + 1) : 0;
|
|
234
|
+
const windowEnd = Math.min(flat.length, windowStart + MAX_AGENTS_DISPLAY);
|
|
235
|
+
if (windowStart > 0) lines.push(truncate(` … ↑${windowStart} earlier (↑ to scroll)`, width));
|
|
236
|
+
for (const row of flat.slice(windowStart, windowEnd)) {
|
|
237
|
+
lines.push(compactAgentRow(row.run, row.agent, row.finished, runs, options, width, row.liveHandle));
|
|
238
|
+
}
|
|
239
|
+
if (windowEnd < flat.length) lines.push(truncate(` … +${flat.length - windowEnd} more (↓ to scroll)`, width));
|
|
240
|
+
return lines;
|
|
241
|
+
}
|
|
242
|
+
|
|
48
243
|
export function buildWidgetLines(
|
|
49
244
|
cwd: string,
|
|
50
245
|
frame = 0,
|
|
@@ -52,7 +247,10 @@ export function buildWidgetLines(
|
|
|
52
247
|
providedRuns?: WidgetRun[],
|
|
53
248
|
notificationCount = 0,
|
|
54
249
|
width = DEFAULT_WIDGET_WIDTH,
|
|
250
|
+
options: WidgetRenderOptions = {},
|
|
55
251
|
): string[] {
|
|
252
|
+
const rowStyle: WidgetRowStyle = options.rowStyle ?? "detailed";
|
|
253
|
+
const focused = options.focused === true;
|
|
56
254
|
// Match the legacy `buildCrewWidgetLines` API: when no runs are supplied,
|
|
57
255
|
// auto-fetch via activeWidgetRuns(cwd). Otherwise widgets calling with
|
|
58
256
|
// only `(cwd, frame)` would render an empty line set (regression vs. the
|
|
@@ -61,18 +259,20 @@ export function buildWidgetLines(
|
|
|
61
259
|
if (!runs.length) return [];
|
|
62
260
|
|
|
63
261
|
const runningGlyph = spinnerFrame("widget-header");
|
|
262
|
+
|
|
263
|
+
// Compact = pi-subtask's dock: NO "Crew agents" header, NO tree — hint
|
|
264
|
+
// line, `main` row, then a 3-row scroll window over the flat agent list.
|
|
265
|
+
if (rowStyle === "compact") {
|
|
266
|
+
const lines = compactDockLines(runs, options, width, maxLines, notificationCount, runningGlyph);
|
|
267
|
+
return focused ? lines : lines.slice(0, maxLines);
|
|
268
|
+
}
|
|
269
|
+
|
|
64
270
|
const lines: string[] = [widgetHeader(runs, runningGlyph, maxLines, notificationCount)];
|
|
65
271
|
|
|
66
|
-
for (const
|
|
67
|
-
const
|
|
272
|
+
for (const entry of runs) {
|
|
273
|
+
const { run, agents } = entry;
|
|
68
274
|
const now = Date.now();
|
|
69
|
-
const finishedAgents =
|
|
70
|
-
if (item.status === "running" || item.status === "queued" || item.status === "waiting") return false;
|
|
71
|
-
if (!item.completedAt) return false;
|
|
72
|
-
const maxAgeMs = (ERROR_STATUSES.has(item.status) ? ERROR_LINGER_MAX_AGE : FINISHED_LINGER_MAX_AGE) * 60_000;
|
|
73
|
-
const age = now - new Date(item.completedAt).getTime();
|
|
74
|
-
return Number.isFinite(age) && age < maxAgeMs;
|
|
75
|
-
});
|
|
275
|
+
const { active: activeAgents, finished: finishedAgents } = orderWidgetAgents(entry, now);
|
|
76
276
|
const completed = agents.filter((a) => a.status === "completed").length;
|
|
77
277
|
// WP-3 (H4): while a run is parked awaiting plan approval, the spinner
|
|
78
278
|
// glyph is replaced by a `⚠ plan:<last-8 runId>` badge. Plain-unicode ⚠
|
|
@@ -90,11 +290,6 @@ export function buildWidgetLines(
|
|
|
90
290
|
// activity line) and is GUARANTEED stable across ticks:
|
|
91
291
|
// - agents count — from `agents` array, always populated, never empty.
|
|
92
292
|
// - run elapsed — from `run.createdAt`, always set on manifest.
|
|
93
|
-
// Both come from sources with no race window — `agents` is read from
|
|
94
|
-
// snapshot.agents OR agentsFor(run) (both always return same length
|
|
95
|
-
// for a healthy run), and `run.createdAt` is immutable. The format
|
|
96
|
-
// shape `"X/Y agents · Ns"` is therefore truly invariant: same number
|
|
97
|
-
// of `·`-separated fields, same field meanings, every render tick.
|
|
98
293
|
//
|
|
99
294
|
// Bug 022 (timer-fix + label): for TERMINAL runs (failed/cancelled/
|
|
100
295
|
// completed) the elapsed counter previously kept ticking up forever
|
|
@@ -112,56 +307,58 @@ export function buildWidgetLines(
|
|
|
112
307
|
|
|
113
308
|
const liveForRun = listLiveAgents().filter((a) => a.runId === run.runId);
|
|
114
309
|
|
|
115
|
-
//
|
|
116
|
-
//
|
|
117
|
-
|
|
118
|
-
const ACTIVE_PRIORITY: Record<string, number> = {
|
|
119
|
-
running: 0,
|
|
120
|
-
queued: 1,
|
|
121
|
-
waiting: 2,
|
|
122
|
-
};
|
|
123
|
-
const prioritizedActive = [...activeAgents].sort((a, b) => (ACTIVE_PRIORITY[a.status] ?? 9) - (ACTIVE_PRIORITY[b.status] ?? 9));
|
|
310
|
+
// Focused: list every agent so the keyboard cursor can reach it. Idle: keep
|
|
311
|
+
// the historical cap so the prompt area stays small.
|
|
312
|
+
const activeCap = focused ? activeAgents.length : MAX_AGENTS_DISPLAY;
|
|
124
313
|
// Finished rows only appear in slots not used by active agents (max 2). When
|
|
125
314
|
// there are >= MAX_AGENTS_DISPLAY live workers, finished rows are suppressed
|
|
126
315
|
// entirely so they cannot push a live agent's activity line off-screen.
|
|
127
|
-
const finishedSlots = Math.max(0, Math.min(2, MAX_AGENTS_DISPLAY - activeAgents.length));
|
|
316
|
+
const finishedSlots = focused ? finishedAgents.length : Math.max(0, Math.min(2, MAX_AGENTS_DISPLAY - activeAgents.length));
|
|
317
|
+
|
|
318
|
+
/** Cursor marker column, mirroring the inline panel's selection. */
|
|
319
|
+
const markerFor = (taskId: string): string => (options.selectedTaskId === taskId ? "❯" : " ");
|
|
128
320
|
|
|
129
|
-
const visibleAgents =
|
|
321
|
+
const visibleAgents = activeAgents.slice(0, activeCap);
|
|
130
322
|
for (const [index, agent] of visibleAgents.entries()) {
|
|
131
|
-
const last = index === visibleAgents.length - 1 && activeAgents.length <=
|
|
323
|
+
const last = index === visibleAgents.length - 1 && activeAgents.length <= activeCap && finishedSlots === 0;
|
|
132
324
|
const branch = last ? "└─" : "├─";
|
|
133
|
-
const agentGlyph = iconForStatus(agent.status, { runningGlyph });
|
|
134
325
|
const liveHandle = liveForRun.find((h) => h.taskId === agent.taskId);
|
|
326
|
+
const legacyGlyph = options.viewedTaskId === agent.taskId ? "◉" : iconForStatus(agent.status, { runningGlyph });
|
|
135
327
|
const stats = agentStats(agent, liveHandle);
|
|
136
328
|
const name = liveHandle?.agent ?? agent.agent;
|
|
329
|
+
const activity = agentActivity(agent, liveHandle);
|
|
137
330
|
const desc = truncate(liveHandle?.description ?? agent.role ?? "", TASK_DESC_MAX);
|
|
138
|
-
const _activeMain = truncate(`│ ${branch} ${
|
|
331
|
+
const _activeMain = truncate(`│ ${branch} ${legacyGlyph} ${name}${desc ? ` · ${desc}` : ` · ${agent.role}`}`, width);
|
|
139
332
|
lines.push(_activeMain);
|
|
140
|
-
const _activity = truncate(`│ ⊶ ${
|
|
333
|
+
const _activity = truncate(`│ ⊶ ${activity}${stats ? ` · ${stats}` : ""}`, width);
|
|
141
334
|
lines.push(_activity);
|
|
142
335
|
}
|
|
143
336
|
|
|
144
|
-
if (activeAgents.length >
|
|
145
|
-
lines.push(truncate(`│ └─ … +${activeAgents.length -
|
|
337
|
+
if (activeAgents.length > activeCap) {
|
|
338
|
+
lines.push(truncate(`│ └─ … +${activeAgents.length - activeCap} more agents`, width));
|
|
146
339
|
}
|
|
147
340
|
|
|
148
341
|
for (const [index, agent] of finishedAgents.slice(0, finishedSlots).entries()) {
|
|
149
342
|
const liveHandle = liveForRun.find((h) => h.taskId === agent.taskId);
|
|
150
343
|
const name = liveHandle?.agent ?? agent.agent;
|
|
151
|
-
const
|
|
344
|
+
const legacyIcon =
|
|
152
345
|
agent.status === "completed" ? "✓" : agent.status === "failed" ? "✗" : agent.status === "needs_attention" ? "⚠" : "▪";
|
|
153
346
|
const stats = agentStats(agent, liveHandle);
|
|
154
347
|
const desc = truncate(liveHandle?.description ?? agent.role ?? "", TASK_DESC_MAX);
|
|
155
348
|
const isLastFinished = index === Math.min(finishedAgents.length, finishedSlots) - 1;
|
|
156
349
|
const branch = isLastFinished ? "└─" : "├─";
|
|
157
|
-
const _finished = truncate(`│ ${branch} ${
|
|
350
|
+
const _finished = truncate(`│ ${branch} ${legacyIcon} ${name} · ${desc}${stats ? ` · ${stats}` : ""}`, width);
|
|
158
351
|
lines.push(_finished);
|
|
159
352
|
}
|
|
160
353
|
|
|
161
|
-
|
|
354
|
+
// Focused (inline panel cursor active): the keyboard can reach every
|
|
355
|
+
// agent, so the renderer must list them ALL — clipping here would put
|
|
356
|
+
// the cursor marker (❯) on rows that are never painted. The idle
|
|
357
|
+
// widget keeps its historical cap to hold the prompt area small.
|
|
358
|
+
if (lines.length >= maxLines && !focused) break;
|
|
162
359
|
}
|
|
163
360
|
|
|
164
|
-
return lines.slice(0, maxLines);
|
|
361
|
+
return focused ? lines : lines.slice(0, maxLines);
|
|
165
362
|
}
|
|
166
363
|
|
|
167
364
|
// ── Colorization ──────────────────────────────────────────────────────
|