pi-crew 0.11.0 → 0.11.2
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 +155 -9
- package/README.md +161 -1037
- package/agents/verifier.md +18 -7
- package/dist/index.mjs +744 -90644
- package/docs/README.md +57 -46
- package/docs/architecture.md +87 -33
- package/docs/commands-reference.md +9 -5
- package/docs/troubleshooting.md +3 -2
- package/package.json +1 -3
- package/schema.json +39 -0
- package/skills/real-test-pi-crew/REPORT-TEMPLATE.md +2 -0
- package/skills/real-test-pi-crew/SKILL.md +371 -34
- package/src/agents/agent-config.ts +1 -1
- package/src/agents/discover-agents.ts +1 -1
- package/src/config/config-validation.ts +15 -2
- package/src/config/config.ts +47 -13
- package/src/config/defaults.ts +0 -1
- package/src/config/env-vars.ts +35 -0
- package/src/config/types.ts +19 -5
- package/src/errors.ts +2 -2
- package/src/extension/async-notifier.ts +23 -0
- package/src/extension/crew-vibes/config.ts +0 -21
- package/src/extension/crew-vibes/index.ts +0 -2
- package/src/extension/crew-vibes/render.ts +1 -50
- package/src/extension/help.ts +21 -12
- package/src/extension/knowledge-injection.ts +2 -1
- package/src/extension/management.ts +8 -3
- package/src/extension/notification-sink.ts +17 -0
- package/src/extension/register.ts +7 -2
- package/src/extension/registration/command-utils.ts +28 -2
- package/src/extension/registration/commands/dashboard.ts +11 -1
- package/src/extension/registration/commands/manage.ts +36 -19
- package/src/extension/registration/commands/run.ts +24 -2
- package/src/extension/registration/commands/shared.ts +23 -1
- package/src/extension/registration/commands/status.ts +25 -2
- package/src/extension/registration/context-builder.ts +8 -2
- package/src/extension/registration/health-notify-policy.ts +100 -0
- package/src/extension/registration/lazy-configurers.ts +35 -0
- package/src/extension/registration/lifecycle-handlers.ts +91 -30
- package/src/extension/registration/lifecycle.ts +75 -10
- package/src/extension/registration/observability.ts +98 -35
- package/src/extension/registration/registration-types.ts +7 -5
- package/src/extension/registration/runtime-cleanup.ts +9 -3
- package/src/extension/registration/subagent-helpers.ts +38 -0
- package/src/extension/registration/subagent-tools.ts +16 -6
- package/src/extension/registration/team-tool.ts +10 -3
- package/src/extension/registration/terminal-status-wiring.ts +172 -0
- package/src/extension/registration/viewers.ts +6 -0
- package/src/extension/registration/wire-cross-extension.ts +28 -0
- package/src/extension/run-compare.ts +220 -0
- package/src/extension/run-export.ts +37 -5
- package/src/extension/run-maintenance.ts +155 -5
- package/src/extension/team-tool/dispatch/index.ts +3 -2
- package/src/extension/team-tool/dispatch/manage.ts +5 -2
- package/src/extension/team-tool/goal.ts +4 -1
- package/src/extension/team-tool/handle-settings.ts +33 -4
- package/src/extension/team-tool/health-monitor.ts +21 -7
- package/src/extension/team-tool/lifecycle-actions.ts +49 -1
- package/src/extension/team-tool/plan.ts +10 -0
- package/src/extension/team-tool/routing-hint.ts +63 -0
- package/src/extension/team-tool/status.ts +4 -0
- package/src/extension/team-tool.ts +52 -6
- package/src/extension/webhook-notify.ts +382 -0
- package/src/observability/metric-sink.ts +12 -2
- package/src/prompt/prompt-runtime.ts +82 -31
- package/src/prompt/worker-events-channel.ts +12 -0
- package/src/runtime/README.md +1 -1
- package/src/runtime/async-runner.ts +87 -1
- package/src/runtime/background-runner.ts +313 -234
- package/src/runtime/broker/crew-broker.ts +17 -11
- package/src/runtime/broker/delegate/shadow-lifecycle.ts +92 -0
- package/src/runtime/broker/wait-status-cache.ts +1 -1
- package/src/runtime/child-pi/child-pi-timers.ts +1 -1
- package/src/runtime/child-pi/mock-fixtures.ts +48 -0
- package/src/runtime/crew-agent-records.ts +337 -45
- package/src/runtime/deadletter.ts +43 -1
- package/src/runtime/delegate-spawn.ts +5 -1
- package/src/runtime/dispatch-batch.ts +72 -5
- package/src/runtime/goal-workflow/goal-loop-runner.ts +73 -4
- package/src/runtime/heartbeat/heartbeat-watcher.ts +7 -0
- package/src/runtime/model/model-fallback.ts +21 -1
- package/src/runtime/model/pi-args.ts +8 -10
- package/src/runtime/recovery/crash-recovery.ts +25 -1
- package/src/runtime/run-worker.ts +12 -1
- package/src/runtime/scheduling/global-worker-cap.ts +13 -6
- package/src/runtime/scheduling/run-coalesced-task-group.ts +27 -1
- package/src/runtime/scheduling/scheduler.ts +49 -13
- package/src/runtime/scheduling/semaphore.ts +148 -20
- package/src/runtime/scratchpad/README.md +1 -1
- package/src/runtime/scratchpad/protocol.ts +1 -1
- package/src/runtime/settings-store.ts +1 -1
- package/src/runtime/skill-instructions.ts +22 -0
- package/src/runtime/stale-reconciler.ts +85 -13
- package/src/runtime/task-display.ts +1 -1
- package/src/runtime/task-runner/pre-execution.ts +26 -2
- package/src/runtime/task-runner/prompt-builder.ts +142 -45
- package/src/runtime/task-runner.ts +21 -1
- package/src/runtime/team-runner.ts +38 -1
- package/src/runtime/workspace-lock.ts +4 -1
- package/src/schema/config-schema.ts +18 -0
- package/src/schema/team-tool-schema.ts +17 -0
- package/src/state/atomic-write.ts +53 -0
- package/src/state/contracts.ts +109 -0
- package/src/state/coordination/locks.ts +191 -33
- package/src/state/coordination/mailbox.ts +140 -15
- package/src/state/crew-init.ts +87 -12
- package/src/state/event-log/cursor.ts +37 -1
- package/src/state/event-log/event-log-rotation.ts +72 -7
- package/src/state/stores/active-run-registry.ts +13 -1
- package/src/state/stores/state-store.ts +112 -22
- package/src/state/types.ts +4 -0
- package/src/ui/adaptive-card.ts +65 -0
- package/src/ui/agents-jobs-browser.ts +70 -64
- package/src/ui/card-colors.ts +36 -7
- package/src/ui/dashboard-panes/agents-pane.ts +55 -14
- package/src/ui/dashboard-panes/cancellation-pane.ts +0 -42
- package/src/ui/dashboard-panes/health-pane.ts +7 -5
- package/src/ui/dashboard-panes/mailbox-pane.ts +22 -6
- package/src/ui/dashboard-panes/metrics-pane.ts +15 -7
- package/src/ui/dashboard-panes/pane-theme.ts +21 -0
- package/src/ui/dashboard-panes/plan-pane.ts +63 -30
- package/src/ui/dashboard-panes/progress-pane.ts +3 -2
- package/src/ui/dashboard-panes/schedules-pane.ts +44 -21
- package/src/ui/dashboard-panes/transcript-pane.ts +11 -5
- package/src/ui/dwf-phase-display.ts +3 -20
- package/src/ui/format-helpers.ts +22 -0
- package/src/ui/heartbeat-aggregator.ts +34 -0
- package/src/ui/inline-panel/crew-editor.ts +13 -3
- package/src/ui/inline-panel/index.ts +60 -4
- package/src/ui/keybinding-map.ts +251 -35
- package/src/ui/live-conversation-overlay.ts +180 -47
- package/src/ui/live-run-sidebar.ts +134 -55
- package/src/ui/mascot.ts +32 -16
- package/src/ui/overlays/agent-picker-overlay.ts +81 -26
- package/src/ui/overlays/confirm-overlay.ts +55 -29
- package/src/ui/overlays/help-overlay.ts +108 -53
- package/src/ui/overlays/mailbox-compose-overlay.ts +89 -50
- package/src/ui/overlays/mailbox-detail-overlay.ts +137 -57
- package/src/ui/powerbar-publisher.ts +0 -1
- package/src/ui/rail.ts +333 -0
- package/src/ui/run-dashboard.ts +193 -79
- package/src/ui/run-snapshot-cache.ts +18 -1
- package/src/ui/settings-overlay.ts +81 -39
- package/src/ui/spinner.ts +26 -2
- package/src/ui/terminal-status.ts +7 -1
- package/src/ui/theme-adapter.ts +0 -45
- package/src/ui/theme-discovery.ts +12 -6
- package/src/ui/tool-progress-formatter.ts +128 -9
- package/src/ui/tool-renderers/brief-mode.ts +10 -67
- package/src/ui/tool-renderers/index.ts +374 -523
- package/src/ui/transcript-viewer.ts +30 -12
- package/src/ui/widget/index.ts +32 -52
- package/src/ui/widget/task-list.ts +64 -32
- package/src/ui/widget/widget-formatters.ts +3 -402
- package/src/ui/widget/widget-model.ts +28 -7
- package/src/ui/widget/widget-renderer.ts +201 -128
- package/src/ui/widget/widget-types.ts +0 -2
- package/src/utils/incremental-reader.ts +11 -3
- package/src/utils/paths.ts +94 -12
- package/src/utils/project-markers.ts +40 -0
- package/src/utils/visual.ts +0 -4
- package/src/worktree/worktree-manager.ts +206 -26
- package/workflows/distill.workflow.md +3 -3
- package/workflows/fast-fix.workflow.md +1 -1
- package/workflows/plan-execute.workflow.md +1 -1
- package/workflows/review.workflow.md +1 -1
- package/workflows/strict-fast-fix.workflow.md +1 -1
- package/docs/migration-v0.4-v0.5.md +0 -208
- package/docs/runtime-flow.md +0 -148
- package/src/extension/crew-vibes/figures.ts +0 -22
- package/src/extension/crew-vibes/font-detect.ts +0 -71
- package/src/ui/dynamic-border.ts +0 -35
- package/src/ui/loaders.ts +0 -6
- package/src/ui/overlay-stack.ts +0 -148
|
@@ -1,28 +1,72 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Tool renderer registry —
|
|
2
|
+
* Tool renderer registry — RAIL design language (R3, 2026-09-16).
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
4
|
+
* Visual grammar (a full redesign; the previous rounded-box + `◀ BADGE ▶`
|
|
5
|
+
* frame language is gone):
|
|
6
|
+
*
|
|
7
|
+
* ┏ ┃ ┗ a vertical RAIL down the left edge marks one tool call.
|
|
8
|
+
* `┏` opens the card (call half), `┃` continues it (streaming /
|
|
9
|
+
* expanded rows), `┗` closes it with the outcome summary and the
|
|
10
|
+
* end cap. The rail colour carries status: neutral while idle,
|
|
11
|
+
* accent while streaming, green on success, red on failure.
|
|
12
|
+
*
|
|
13
|
+
* `NAME ▸ SUBJECT` identity canopy: `CREW ▸ implementation`,
|
|
14
|
+
* `AGENT ▸ explorer`, `STATUS ▸ _ui_demo`.
|
|
15
|
+
*
|
|
16
|
+
* `······` dot leaders connect the left and right segments of a
|
|
17
|
+
* line (metrics ↔ elapsed/expand-chord hint), and shrink to a
|
|
18
|
+
* two-space gap when the column is too narrow.
|
|
19
|
+
*
|
|
20
|
+
* `▕████▎░░▏` gauge bar with EIGHTH-BLOCK sub-cell precision, so a
|
|
21
|
+
* narrow column still shows meaningful progress.
|
|
22
|
+
*
|
|
23
|
+
* Cards are width-deferred through AdaptiveCard: the builder receives the
|
|
24
|
+
* real render width, so nothing is "baked" at an assumed terminal size and
|
|
25
|
+
* no line ever wraps into a torn frame.
|
|
7
26
|
*/
|
|
8
27
|
|
|
9
28
|
import { type Container, Text, visibleWidth } from "@earendil-works/pi-tui";
|
|
10
29
|
import type { CrewAgentRecord } from "../../runtime/crew-agent-runtime.ts";
|
|
11
|
-
import {
|
|
30
|
+
import { AdaptiveCard } from "../adaptive-card.ts";
|
|
12
31
|
import type { CrewComponent } from "../component.ts";
|
|
13
|
-
import { formatDuration, formatTokens, truncLine } from "../format-helpers.ts";
|
|
14
|
-
import {
|
|
32
|
+
import { formatCount, formatDuration, formatTokens, truncLine } from "../format-helpers.ts";
|
|
33
|
+
import {
|
|
34
|
+
ACTIVE,
|
|
35
|
+
dedupeAgentLabel,
|
|
36
|
+
gaugeBar,
|
|
37
|
+
PI_EXPAND_CHORD,
|
|
38
|
+
RAIL,
|
|
39
|
+
type RailSlot,
|
|
40
|
+
railLeaders,
|
|
41
|
+
railLine,
|
|
42
|
+
scanGauge,
|
|
43
|
+
shortId,
|
|
44
|
+
statusBadge,
|
|
45
|
+
statusIcon,
|
|
46
|
+
statusSlot,
|
|
47
|
+
truncVisual,
|
|
48
|
+
} from "../rail.ts";
|
|
49
|
+
import { spinnerClockNow, spinnerFrame } from "../spinner.ts";
|
|
15
50
|
import type { CrewTheme } from "../theme-adapter.ts";
|
|
51
|
+
import { parseCompactToolProgress } from "../tool-progress-formatter.ts";
|
|
16
52
|
import { briefToolResult, isBrief } from "./brief-mode.ts";
|
|
17
53
|
|
|
54
|
+
// statusIcon stays part of this module's public API (team-tool.ts imports it
|
|
55
|
+
// from here); the implementation moved to the shared RAIL module.
|
|
56
|
+
export { statusIcon };
|
|
57
|
+
|
|
18
58
|
// ── Types ──────────────────────────────────────────────────────────────
|
|
19
59
|
|
|
20
60
|
export interface ToolRenderContext {
|
|
21
61
|
expanded: boolean;
|
|
22
62
|
lastComponent?: Container;
|
|
63
|
+
/** Pi's ToolRenderContext carries no width — the render width is real. */
|
|
23
64
|
width?: number;
|
|
24
65
|
isPartial?: boolean;
|
|
25
66
|
isError?: boolean;
|
|
67
|
+
/** Pi signals (available since R1 audit): args may still be streaming in. */
|
|
68
|
+
argsComplete?: boolean;
|
|
69
|
+
executionStarted?: boolean;
|
|
26
70
|
}
|
|
27
71
|
|
|
28
72
|
export interface ToolRenderer {
|
|
@@ -30,7 +74,7 @@ export interface ToolRenderer {
|
|
|
30
74
|
renderResult(result: Record<string, unknown>, options: unknown, theme: CrewTheme, ctx: ToolRenderContext): Component;
|
|
31
75
|
}
|
|
32
76
|
|
|
33
|
-
export type Component = Container | Text;
|
|
77
|
+
export type Component = Container | Text | AdaptiveCard;
|
|
34
78
|
|
|
35
79
|
// PR-G3 (UI-3): `Container | Text` structurally satisfies the shared
|
|
36
80
|
// CrewComponent contract — both come from @earendil-works/pi-tui and each
|
|
@@ -39,433 +83,404 @@ export type Component = Container | Text;
|
|
|
39
83
|
// the exported union type or any behavior.
|
|
40
84
|
type _AssertComponentIsCrewComponent = Container extends CrewComponent ? (Text extends CrewComponent ? true : never) : never;
|
|
41
85
|
|
|
42
|
-
// ──
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
}
|
|
86
|
+
// ── RAIL primitives ────────────────────────────────────────────────────
|
|
87
|
+
//
|
|
88
|
+
// Rail glyphs, leaders, gauge, status glyphs and the width helpers now live in
|
|
89
|
+
// the shared `src/ui/rail.ts` (RAIL design system, 2026-09-16) so every
|
|
90
|
+
// pi-crew surface draws from ONE source instead of each re-declaring `┏ ┃ ┗`.
|
|
91
|
+
// The tool-card-specific pieces stay here: the canopy composition (CREW /
|
|
92
|
+
// AGENT / STATUS) and the status→slot choice for the OPEN half of a card.
|
|
50
93
|
|
|
51
|
-
/**
|
|
52
|
-
function
|
|
53
|
-
if (
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
// emoji it splits a surrogate pair (U+FFFD). Use the grapheme/ANSI-aware
|
|
57
|
-
// truncateToWidth with empty ellipsis (the caller appends its own '…').
|
|
58
|
-
return truncateToWidth(str, maxWidth, "");
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
// ── Visual primitives ──────────────────────────────────────────────────
|
|
62
|
-
|
|
63
|
-
/** Status icon with color */
|
|
64
|
-
function statusBadge(status: string, theme: CrewTheme): string {
|
|
65
|
-
switch (status) {
|
|
66
|
-
case "completed":
|
|
67
|
-
return theme.fg("success", "●");
|
|
68
|
-
case "failed":
|
|
69
|
-
case "cancelled":
|
|
70
|
-
return theme.fg("error", "✖");
|
|
71
|
-
case "running":
|
|
72
|
-
return theme.fg("warning", "◉");
|
|
73
|
-
default:
|
|
74
|
-
return theme.fg("dim", "○");
|
|
75
|
-
}
|
|
94
|
+
/** Rail colour for the OPEN half — the call was made; outcome unknown yet. */
|
|
95
|
+
function railOpenSlot(ctx: ToolRenderContext): RailSlot {
|
|
96
|
+
if (ctx.isError) return "error";
|
|
97
|
+
if (ctx.isPartial) return "borderAccent";
|
|
98
|
+
return "border";
|
|
76
99
|
}
|
|
77
100
|
|
|
78
|
-
/**
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
return theme.fg("success", "✓");
|
|
83
|
-
case "failed":
|
|
84
|
-
case "cancelled":
|
|
85
|
-
return theme.fg("error", "✗");
|
|
86
|
-
case "running":
|
|
87
|
-
return theme.fg("warning", "⟳");
|
|
88
|
-
default:
|
|
89
|
-
return theme.fg("dim", "○");
|
|
90
|
-
}
|
|
101
|
+
/** Rail colour for the CLOSE half — the outcome is known here. */
|
|
102
|
+
function railCloseSlot(status: string, ctx: ToolRenderContext): RailSlot {
|
|
103
|
+
if (ctx.isError) return "error";
|
|
104
|
+
return statusSlot(status);
|
|
91
105
|
}
|
|
92
106
|
|
|
93
|
-
|
|
94
|
-
function shortId(id: string | undefined): string {
|
|
95
|
-
return id ? id.slice(-8) : "????????";
|
|
96
|
-
}
|
|
107
|
+
// ── Identity canopy ────────────────────────────────────────────────────
|
|
97
108
|
|
|
98
|
-
/**
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
+
/**
|
|
110
|
+
* `┏ CREW ▸ implementation` / `┏ STATUS ▸ _ui_demo` / `┏ AGENT ▸ explorer`.
|
|
111
|
+
* `action === "run"` promotes the subject (team) and the canopy word becomes
|
|
112
|
+
* the brand; for any other action the action IS the identity.
|
|
113
|
+
*/
|
|
114
|
+
function canopy(args: {
|
|
115
|
+
action?: string;
|
|
116
|
+
team?: string;
|
|
117
|
+
agentName?: string;
|
|
118
|
+
runId?: string;
|
|
119
|
+
theme: CrewTheme;
|
|
120
|
+
ctx: ToolRenderContext;
|
|
121
|
+
budget: number;
|
|
122
|
+
}): string {
|
|
123
|
+
const { action = "", team, agentName, runId, theme, ctx, budget } = args;
|
|
124
|
+
const isRun = !action || action === "run";
|
|
125
|
+
const word = agentName !== undefined ? "AGENT" : isRun ? "CREW" : (action || "…").toUpperCase();
|
|
126
|
+
const subject = agentName !== undefined ? agentName : isRun ? (team ?? "") : (team ?? shortRun(runId));
|
|
127
|
+
// Subject rules: a real subject always prints; while args are still
|
|
128
|
+
// streaming a `▸ …` placeholder signals "not parsed yet"; a COMPLETE call
|
|
129
|
+
// with nothing to name (e.g. `team action=list`) prints the word alone —
|
|
130
|
+
// a dangling chevron there would read as a bug.
|
|
131
|
+
const streamingArgs = ctx.argsComplete === false;
|
|
132
|
+
const tail = subject
|
|
133
|
+
? ` ${theme.fg("dim", ACTIVE)} ${theme.fg("toolTitle", theme.bold(subject))}`
|
|
134
|
+
: streamingArgs
|
|
135
|
+
? ` ${theme.fg("dim", ACTIVE)} ${theme.fg("toolTitle", theme.bold("…"))}`
|
|
136
|
+
: "";
|
|
137
|
+
const label = `${theme.fg("accent", theme.bold(word))}${tail}`;
|
|
138
|
+
return railLine(RAIL.open, railOpenSlot(ctx), label, theme, budget);
|
|
109
139
|
}
|
|
110
140
|
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
const clamped = Math.max(0, Math.min(1, ratio));
|
|
114
|
-
const filled = Math.round(clamped * barWidth);
|
|
115
|
-
const empty = barWidth - filled;
|
|
116
|
-
const bar = theme.fg("success", "█".repeat(filled)) + theme.fg("dim", "░".repeat(empty));
|
|
117
|
-
const pct = theme.fg("text", ` ${Math.round(clamped * 100)}%`.padStart(5));
|
|
118
|
-
return bar + pct;
|
|
141
|
+
function shortRun(runId: string | undefined): string {
|
|
142
|
+
return runId ? shortId(runId) : "";
|
|
119
143
|
}
|
|
120
144
|
|
|
121
|
-
// ──
|
|
145
|
+
// ── Shared line builders ───────────────────────────────────────────────
|
|
122
146
|
|
|
123
|
-
/**
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
/** Create a rounded-corner framed card.
|
|
130
|
-
* With renderShell="self", Pi no longer wraps in Box(1,1).
|
|
131
|
-
* Frame uses full totalWidth.
|
|
132
|
-
* When bgSlot is provided, the interior is filled with a subtle status-tinted
|
|
133
|
-
* background derived from the theme (mixBg at 8% intensity).
|
|
134
|
-
*/
|
|
135
|
-
function buildFrame(
|
|
136
|
-
contentLines: string[],
|
|
137
|
-
totalWidth: number,
|
|
138
|
-
theme: CrewTheme,
|
|
139
|
-
borderSlot: "success" | "error" | "border" | "borderAccent" = "border",
|
|
140
|
-
bgSlot?: "success" | "error" | "border" | "borderAccent",
|
|
141
|
-
): string {
|
|
142
|
-
const frameW = totalWidth - 2; // available after Box(1,1) padding
|
|
143
|
-
const innerW = frameW - 2; // │ chars
|
|
144
|
-
const top = theme.fg(borderSlot, `╭${"─".repeat(innerW)}╮`);
|
|
145
|
-
const bottom = theme.fg(borderSlot, `╰${"─".repeat(innerW)}╯`);
|
|
146
|
-
const v = theme.fg(borderSlot, "│");
|
|
147
|
-
const bg = bgSlot ? deriveCardBackground(theme, bgSlot) : "";
|
|
148
|
-
|
|
149
|
-
const lines: string[] = [top];
|
|
150
|
-
for (const line of contentLines) {
|
|
151
|
-
const padded = bg ? padWithBackground(line, innerW, bg) : padVisual(line, innerW);
|
|
152
|
-
lines.push(v + padded + v);
|
|
153
|
-
}
|
|
154
|
-
lines.push(bottom);
|
|
155
|
-
return lines.join("\n");
|
|
147
|
+
/** Dim preview line (goals, prompts, output snippets) with visual truncation. */
|
|
148
|
+
function previewLine(glyph: string, slot: RailSlot, text: string, theme: CrewTheme, budget: number, shorten = false): string {
|
|
149
|
+
const flat = (shorten ? shortenPath(text) : text).replace(/\n/g, " ");
|
|
150
|
+
const maxLen = budget - 2;
|
|
151
|
+
const body = visibleWidth(flat) > maxLen ? `${truncVisual(flat, maxLen - 1)}…` : flat;
|
|
152
|
+
return railLine(glyph, slot, theme.fg("dim", body), theme, budget);
|
|
156
153
|
}
|
|
157
154
|
|
|
158
|
-
/**
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
totalWidth: number,
|
|
163
|
-
theme: CrewTheme,
|
|
164
|
-
borderSlot: "success" | "error" | "border" | "borderAccent" = "border",
|
|
165
|
-
bgSlot?: "success" | "error" | "border" | "borderAccent",
|
|
166
|
-
): string {
|
|
167
|
-
const frameW = totalWidth - 2;
|
|
168
|
-
const innerW = frameW - 2;
|
|
169
|
-
const top = theme.fg(borderSlot, `╭${"─".repeat(innerW)}╮`);
|
|
170
|
-
const v = theme.fg(borderSlot, "│");
|
|
171
|
-
const bg = bgSlot ? deriveCardBackground(theme, bgSlot) : "";
|
|
172
|
-
const lines: string[] = [top];
|
|
173
|
-
for (const line of contentLines) {
|
|
174
|
-
const padded = bg ? padWithBackground(line, innerW, bg) : padVisual(line, innerW);
|
|
175
|
-
lines.push(v + padded + v);
|
|
176
|
-
}
|
|
177
|
-
return lines.join("\n");
|
|
178
|
-
}
|
|
179
|
-
|
|
180
|
-
/** Build frame BOTTOM: content lines + bottom border, NO top border.
|
|
181
|
-
* Pairs with buildFrameTop() so renderCall + renderResult merge into ONE frame. */
|
|
182
|
-
function buildFrameBottom(
|
|
183
|
-
contentLines: string[],
|
|
184
|
-
totalWidth: number,
|
|
185
|
-
theme: CrewTheme,
|
|
186
|
-
borderSlot: "success" | "error" | "border" | "borderAccent" = "border",
|
|
187
|
-
bgSlot?: "success" | "error" | "border" | "borderAccent",
|
|
188
|
-
): string {
|
|
189
|
-
const frameW = totalWidth - 2;
|
|
190
|
-
const innerW = frameW - 2;
|
|
191
|
-
const v = theme.fg(borderSlot, "│");
|
|
192
|
-
const bottom = theme.fg(borderSlot, `╰${"─".repeat(innerW)}╯`);
|
|
193
|
-
const bg = bgSlot ? deriveCardBackground(theme, bgSlot) : "";
|
|
194
|
-
const lines: string[] = [];
|
|
195
|
-
for (const line of contentLines) {
|
|
196
|
-
const padded = bg ? padWithBackground(line, innerW, bg) : padVisual(line, innerW);
|
|
197
|
-
lines.push(v + padded + v);
|
|
198
|
-
}
|
|
199
|
-
lines.push(bottom);
|
|
200
|
-
return lines.join("\n");
|
|
201
|
-
}
|
|
202
|
-
|
|
203
|
-
/** Derive border color from context so renderCall's top matches renderResult's bottom. */
|
|
204
|
-
function borderFromContext(ctx: ToolRenderContext): "success" | "error" | "borderAccent" {
|
|
205
|
-
if (ctx.isPartial) return "borderAccent";
|
|
206
|
-
if (ctx.isError) return "error";
|
|
207
|
-
return "success";
|
|
155
|
+
/** `n/m · pct%` tally shared by streaming and expanded gauges. */
|
|
156
|
+
function tallyText(completed: number, total: number, theme: CrewTheme): string {
|
|
157
|
+
const pct = total > 0 ? Math.round((completed / total) * 100) : 0;
|
|
158
|
+
return theme.fg("muted", `${completed}/${total} · ${pct}%`);
|
|
208
159
|
}
|
|
209
160
|
|
|
210
161
|
// ── Team Tool Renderer ─────────────────────────────────────────────────
|
|
211
162
|
|
|
212
163
|
export const teamToolRenderer: ToolRenderer = {
|
|
213
164
|
renderCall(args, theme, ctx) {
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
contentLines.push(padVisual(header, innerW));
|
|
227
|
-
|
|
228
|
-
// Goal preview (P5: shorten paths)
|
|
229
|
-
if (goal) {
|
|
230
|
-
const maxLen = innerW - 2;
|
|
231
|
-
const preview = shortenPath(goal.replace(/\n/g, " "));
|
|
232
|
-
const previewText = visibleWidth(preview) > maxLen ? truncVisual(preview, maxLen - 1) + "…" : preview;
|
|
233
|
-
contentLines.push(padVisual(` ${theme.fg("dim", previewText)}`, innerW));
|
|
234
|
-
}
|
|
235
|
-
|
|
236
|
-
return new Text(buildFrameTop(contentLines, w, theme, borderFromContext(ctx), borderFromContext(ctx)), 0, 0);
|
|
165
|
+
return new AdaptiveCard(
|
|
166
|
+
(w) => {
|
|
167
|
+
const budget = w - 2;
|
|
168
|
+
const action = (args.action as string) ?? "";
|
|
169
|
+
const goal = (args.goal as string) ?? "";
|
|
170
|
+
const team = args.team as string | undefined;
|
|
171
|
+
const lines = [canopy({ action, team, runId: (args.runId as string) ?? (args.run as string), theme, ctx, budget })];
|
|
172
|
+
if (goal) lines.push(previewLine(RAIL.body, "border", goal, theme, budget, true));
|
|
173
|
+
return lines.join("\n");
|
|
174
|
+
},
|
|
175
|
+
(text) => theme.fg("error", text),
|
|
176
|
+
);
|
|
237
177
|
},
|
|
238
178
|
|
|
239
179
|
renderResult(result, _options, theme, ctx) {
|
|
240
180
|
try {
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
181
|
+
return new AdaptiveCard(
|
|
182
|
+
(w) => renderTeamResult(result, _options, theme, ctx, w),
|
|
183
|
+
(text) => theme.fg("error", text),
|
|
184
|
+
);
|
|
185
|
+
} catch (e) {
|
|
186
|
+
// FAIL-VISIBLE: a render bug or an unexpected payload shape must
|
|
187
|
+
// never masquerade as a successful card.
|
|
188
|
+
const msg = e instanceof Error ? e.message : String(e);
|
|
189
|
+
return new Text(theme.fg("error", `✖ crew render error: ${truncLine(msg, 56)}`), 0, 0);
|
|
245
190
|
}
|
|
246
191
|
},
|
|
247
192
|
};
|
|
248
193
|
|
|
249
|
-
function renderTeamResult(result: Record<string, unknown>, options: unknown, theme: CrewTheme, ctx: ToolRenderContext): string {
|
|
194
|
+
function renderTeamResult(result: Record<string, unknown>, options: unknown, theme: CrewTheme, ctx: ToolRenderContext, w: number): string {
|
|
250
195
|
const d = (result.details ?? result) as Record<string, unknown>;
|
|
251
196
|
const records = (d.agentRecords ?? d.results) as CrewAgentRecord[] | undefined;
|
|
252
197
|
const action = typeof d.action === "string" ? d.action : "";
|
|
253
198
|
const status = typeof d.status === "string" ? d.status : "";
|
|
254
199
|
const runId = typeof d.runId === "string" ? d.runId : "";
|
|
255
|
-
const
|
|
256
|
-
const
|
|
257
|
-
const
|
|
258
|
-
const contentLines: string[] = [];
|
|
200
|
+
const teamName = typeof d.team === "string" ? d.team : "";
|
|
201
|
+
const budget = w - 2;
|
|
202
|
+
const closeSlot = railCloseSlot(status, ctx);
|
|
259
203
|
|
|
260
|
-
//
|
|
204
|
+
// ── Streaming (partial result, collapsed view) ──
|
|
261
205
|
const isPartial = (options as Record<string, unknown>)?.isPartial === true;
|
|
262
206
|
if (isPartial && !ctx.expanded) {
|
|
263
|
-
const
|
|
264
|
-
const parsed = parseStreamingProgress(content);
|
|
265
|
-
|
|
207
|
+
const parsed = parseStreamingProgress(extractContentText(result?.content));
|
|
266
208
|
if (parsed) {
|
|
267
|
-
const
|
|
268
|
-
const
|
|
269
|
-
const
|
|
270
|
-
const elapsed = formatDuration(parsed.elapsedMs);
|
|
209
|
+
const spinner = theme.fg("accent", spinnerFrame(String(spinnerClockNow())));
|
|
210
|
+
const elapsed = theme.fg("dim", formatDuration(parsed.elapsedMs));
|
|
211
|
+
const lines: string[] = [];
|
|
271
212
|
|
|
272
213
|
if (parsed.completed != null && parsed.total != null && parsed.total > 0) {
|
|
273
|
-
// ── Real task progress ──
|
|
274
214
|
const ratio = parsed.completed / parsed.total;
|
|
275
|
-
const barW = Math.min(
|
|
276
|
-
const
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
215
|
+
const barW = Math.min(Math.max(8, budget - 26), 32);
|
|
216
|
+
const left = `${spinner} ${tallyText(parsed.completed, parsed.total, theme)} ${gaugeBar(ratio, barW, theme)}`;
|
|
217
|
+
lines.push(railLine(RAIL.body, "borderAccent", railLeaders(left, elapsed, budget, theme), theme, budget));
|
|
218
|
+
if (parsed.activeAgent) {
|
|
219
|
+
const dot = theme.fg("accent", spinnerFrame(parsed.activeAgent));
|
|
220
|
+
const label = dedupeAgentLabel(parsed.activeAgent);
|
|
221
|
+
lines.push(railLine(RAIL.body, "border", ` ${dot} ${theme.fg("dim", label)}`, theme, budget));
|
|
222
|
+
}
|
|
283
223
|
} else {
|
|
284
|
-
|
|
285
|
-
const
|
|
286
|
-
|
|
287
|
-
contentLines.push(padVisual(` ${spinner} ${theme.fg("muted", "crew starting")} ${theme.fg("dim", elapsed)}`, innerW));
|
|
288
|
-
contentLines.push(padVisual(` ${scanBar}`, innerW));
|
|
224
|
+
const barW = Math.min(Math.max(8, budget - 24), 32);
|
|
225
|
+
const left = `${spinner} ${theme.fg("muted", "starting")} ${scanGauge(barW, parsed.elapsedMs, theme)}`;
|
|
226
|
+
lines.push(railLine(RAIL.body, "borderAccent", railLeaders(left, elapsed, budget, theme), theme, budget));
|
|
289
227
|
}
|
|
290
|
-
|
|
291
|
-
const preview = truncLine(content.split("\n").filter(Boolean).pop() ?? "", innerW - 4);
|
|
292
|
-
contentLines.push(padVisual(` ${theme.fg("warning", "◉")} ${theme.fg("dim", preview)}`, innerW));
|
|
228
|
+
return lines.join("\n");
|
|
293
229
|
}
|
|
294
|
-
|
|
295
|
-
if (
|
|
296
|
-
return
|
|
230
|
+
const fallback = extractContentText(result?.content).split("\n").filter(Boolean).pop();
|
|
231
|
+
if (fallback) {
|
|
232
|
+
return railLine(RAIL.body, "borderAccent", `${theme.fg("warning", "◉")} ${theme.fg("dim", fallback)}`, theme, budget);
|
|
297
233
|
}
|
|
298
234
|
}
|
|
299
235
|
|
|
300
|
-
// Brief mode:
|
|
301
|
-
// But NOT for action=run — those always get progress bar + metrics
|
|
236
|
+
// ── Brief mode: single end-cap line (never for action=run) ──
|
|
302
237
|
if (isBrief() && !ctx.expanded && action !== "run") {
|
|
303
238
|
const briefText = briefToolResult("team", result as { content?: unknown[] }, theme);
|
|
304
|
-
|
|
305
|
-
return buildFrameBottom(contentLines, w, theme, bColor, bColor);
|
|
239
|
+
return railLine(RAIL.close, closeSlot, `${briefText}`, theme, budget);
|
|
306
240
|
}
|
|
307
241
|
|
|
242
|
+
// ── Closed card ──
|
|
308
243
|
if (!ctx.expanded) {
|
|
309
|
-
// ── Collapsed: compact framed card ──
|
|
310
244
|
if (action === "run" && records?.length) {
|
|
311
|
-
|
|
312
|
-
} else if (action === "run") {
|
|
313
|
-
const m = d.metrics as Metrics | undefined;
|
|
314
|
-
if (m) {
|
|
315
|
-
appendMetricsCard(contentLines, m, status, runId, theme, innerW);
|
|
316
|
-
} else {
|
|
317
|
-
appendSimpleCard(contentLines, status, runId, theme, innerW);
|
|
318
|
-
}
|
|
319
|
-
} else {
|
|
320
|
-
appendSimpleCard(contentLines, status, runId, theme, innerW);
|
|
245
|
+
return railLine(RAIL.close, closeSlot, `${runCap(records, status, teamName, theme, budget)}`, theme, budget);
|
|
321
246
|
}
|
|
322
|
-
|
|
323
|
-
// ── Expanded: detailed card ──
|
|
324
|
-
if (action === "run" && records?.length) {
|
|
325
|
-
appendExpandedRun(contentLines, records, status, runId, theme, w, innerW);
|
|
326
|
-
} else if (action === "run") {
|
|
247
|
+
if (action === "run") {
|
|
327
248
|
const m = d.metrics as Metrics | undefined;
|
|
328
|
-
|
|
329
|
-
} else {
|
|
330
|
-
const text = extractContentText(result?.content);
|
|
331
|
-
contentLines.push(padVisual(` ${text.slice(0, innerW - 2)}`, innerW));
|
|
249
|
+
return railLine(RAIL.close, closeSlot, `${metricsCap(m, status, theme)}`, theme, budget);
|
|
332
250
|
}
|
|
251
|
+
const badge = statusBadge(status, theme);
|
|
252
|
+
const label = [status || "done", runId ? shortId(runId) : ""].filter(Boolean).join(" · ");
|
|
253
|
+
return railLine(RAIL.close, closeSlot, `${badge} ${theme.fg("text", label)}`, theme, budget);
|
|
333
254
|
}
|
|
334
255
|
|
|
335
|
-
|
|
256
|
+
// ── Expanded card ──
|
|
257
|
+
const lines: string[] = [];
|
|
258
|
+
if (action === "run" && records?.length) {
|
|
259
|
+
const completed = records.filter((r) => r.status === "completed").length;
|
|
260
|
+
const ratio = records.length > 0 ? completed / records.length : 0;
|
|
261
|
+
const barW = Math.min(Math.max(8, budget - 26), 40);
|
|
262
|
+
lines.push(
|
|
263
|
+
railLine(RAIL.body, "border", `${gaugeBar(ratio, barW, theme)} ${tallyText(completed, records.length, theme)}`, theme, budget),
|
|
264
|
+
);
|
|
265
|
+
for (const r of records) {
|
|
266
|
+
lines.push(railLine(RAIL.body, "border", agentRow(r, theme, budget), theme, budget));
|
|
267
|
+
const usage = usageRow(r, theme, budget);
|
|
268
|
+
if (usage) lines.push(railLine(RAIL.body, "border", usage, theme, budget));
|
|
269
|
+
}
|
|
270
|
+
const duration = computeTotalDuration(records);
|
|
271
|
+
const tokens = computeTotalTokens(records);
|
|
272
|
+
const cost = computeTotalCost(records);
|
|
273
|
+
const tail = [teamName, formatDuration(duration), `${formatTokens(tokens)} tok`, cost > 0 ? `$${cost.toFixed(3)}` : ""]
|
|
274
|
+
.filter(Boolean)
|
|
275
|
+
.join(" · ");
|
|
276
|
+
lines.push(
|
|
277
|
+
railLine(
|
|
278
|
+
RAIL.close,
|
|
279
|
+
closeSlot,
|
|
280
|
+
railLeaders(theme.fg("muted", tail), theme.fg("dim", PI_EXPAND_CHORD), budget, theme),
|
|
281
|
+
theme,
|
|
282
|
+
budget,
|
|
283
|
+
),
|
|
284
|
+
);
|
|
285
|
+
return lines.join("\n");
|
|
286
|
+
}
|
|
287
|
+
if (action === "run") {
|
|
288
|
+
const m = d.metrics as Metrics | undefined;
|
|
289
|
+
if (m?.taskCount) {
|
|
290
|
+
const ratio = (m.completedCount ?? 0) / m.taskCount;
|
|
291
|
+
const barW = Math.min(Math.max(8, budget - 26), 40);
|
|
292
|
+
lines.push(
|
|
293
|
+
railLine(
|
|
294
|
+
RAIL.body,
|
|
295
|
+
"border",
|
|
296
|
+
`${gaugeBar(ratio, barW, theme)} ${tallyText(m.completedCount ?? 0, m.taskCount, theme)}`,
|
|
297
|
+
theme,
|
|
298
|
+
budget,
|
|
299
|
+
),
|
|
300
|
+
);
|
|
301
|
+
}
|
|
302
|
+
const parts: string[] = [];
|
|
303
|
+
if (m?.durationMs) parts.push(formatDuration(m.durationMs));
|
|
304
|
+
if (m?.totalTokens) parts.push(`${formatTokens(m.totalTokens)} tok`);
|
|
305
|
+
lines.push(railLine(RAIL.close, closeSlot, `${theme.fg("muted", parts.join(" · ") || "done")}`, theme, budget));
|
|
306
|
+
return lines.join("\n");
|
|
307
|
+
}
|
|
308
|
+
// Non-run action, expanded: the raw payload.
|
|
309
|
+
lines.push(previewLine(RAIL.body, "border", extractContentText(result?.content), theme, budget));
|
|
310
|
+
lines.push(railLine(RAIL.close, closeSlot, "", theme, budget));
|
|
311
|
+
return lines.join("\n");
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
/** End-cap for a finished run: `● 5/5 · implementation · 24m4s · 103k tok · $0.204 ···· ctrl+o`. */
|
|
315
|
+
function runCap(records: CrewAgentRecord[], status: string, teamName: string, theme: CrewTheme, budget: number): string {
|
|
316
|
+
const completed = records.filter((r) => r.status === "completed").length;
|
|
317
|
+
const duration = computeTotalDuration(records);
|
|
318
|
+
const tokens = computeTotalTokens(records);
|
|
319
|
+
const cost = computeTotalCost(records);
|
|
320
|
+
const badge = statusBadge(status, theme);
|
|
321
|
+
const parts = [
|
|
322
|
+
`${completed}/${records.length}`,
|
|
323
|
+
teamName,
|
|
324
|
+
formatDuration(duration),
|
|
325
|
+
`${formatTokens(tokens)} tok`,
|
|
326
|
+
cost > 0 ? `$${cost.toFixed(3)}` : "",
|
|
327
|
+
].filter(Boolean);
|
|
328
|
+
return railLeaders(`${badge} ${theme.fg("text", parts.join(" · "))}`, theme.fg("dim", PI_EXPAND_CHORD), budget, theme);
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
function metricsCap(m: Metrics | undefined, status: string, theme: CrewTheme): string {
|
|
332
|
+
const badge = statusBadge(status, theme);
|
|
333
|
+
const parts: string[] = [];
|
|
334
|
+
if (m?.completedCount != null && m.taskCount) parts.push(`${m.completedCount}/${m.taskCount}`);
|
|
335
|
+
if (m?.durationMs) parts.push(formatDuration(m.durationMs));
|
|
336
|
+
if (m?.totalTokens) parts.push(`${formatTokens(m.totalTokens)} tok`);
|
|
337
|
+
if (m?.totalCost) parts.push(`$${m.totalCost.toFixed(3)}`);
|
|
338
|
+
return `${badge} ${theme.fg("text", parts.join(" · ") || status || "done")}`;
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
/** `✓ explorer (gpt-5) · 2m1s` */
|
|
342
|
+
function agentRow(r: CrewAgentRecord, theme: CrewTheme, budget: number): string {
|
|
343
|
+
const icon = statusIcon(r.status, theme);
|
|
344
|
+
const role = theme.fg("toolTitle", theme.bold(r.role || r.agent || "agent"));
|
|
345
|
+
const model = r.model ? theme.fg("dim", ` (${r.model.split("/").at(-1)})`) : "";
|
|
346
|
+
const tools = formatCount(r.toolUses ?? r.progress?.toolCount ?? 0, "tool");
|
|
347
|
+
const dur = r.startedAt ? formatDuration(computeRecordDuration(r)) : "";
|
|
348
|
+
return `${icon} ${role}${model} ${theme.fg("dim", [tools, dur].filter(Boolean).join(" · "))}`;
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
/** ` ↑12k ↓4.1k $0.031` (omitted when the agent reported no usage). */
|
|
352
|
+
function usageRow(r: CrewAgentRecord, theme: CrewTheme, budget: number): string | undefined {
|
|
353
|
+
const usage = r.usage;
|
|
354
|
+
const parts: string[] = [];
|
|
355
|
+
if (usage?.input) parts.push(theme.fg("dim", `↑${formatTokens(usage.input)}`));
|
|
356
|
+
if (usage?.output) parts.push(theme.fg("dim", `↓${formatTokens(usage.output)}`));
|
|
357
|
+
if (usage?.cost) parts.push(theme.fg("dim", `$${usage.cost.toFixed(3)}`));
|
|
358
|
+
return parts.length ? ` ${parts.join(" ")}` : undefined;
|
|
336
359
|
}
|
|
337
360
|
|
|
338
361
|
// ── Agent Tool Renderer ────────────────────────────────────────────────
|
|
339
362
|
|
|
340
363
|
export const agentToolRenderer: ToolRenderer = {
|
|
341
364
|
renderCall(args, theme, ctx) {
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
const maxLen = innerW - 2;
|
|
354
|
-
const preview = prompt.replace(/\n/g, " ");
|
|
355
|
-
const previewText = visibleWidth(preview) > maxLen ? truncVisual(preview, maxLen - 1) + "…" : preview;
|
|
356
|
-
contentLines.push(padVisual(` ${theme.fg("dim", previewText)}`, innerW));
|
|
357
|
-
}
|
|
358
|
-
|
|
359
|
-
return new Text(buildFrameTop(contentLines, w, theme, borderFromContext(ctx), borderFromContext(ctx)), 0, 0);
|
|
365
|
+
return new AdaptiveCard(
|
|
366
|
+
(w) => {
|
|
367
|
+
const budget = w - 2;
|
|
368
|
+
const agentName = (args.agent as string) ?? (args.subagent_type as string) ?? "";
|
|
369
|
+
const prompt = (args.prompt ?? args.task ?? "") as string;
|
|
370
|
+
const lines = [canopy({ agentName, theme, ctx, budget })];
|
|
371
|
+
if (prompt) lines.push(previewLine(RAIL.body, "border", prompt, theme, budget));
|
|
372
|
+
return lines.join("\n");
|
|
373
|
+
},
|
|
374
|
+
(text) => theme.fg("error", text),
|
|
375
|
+
);
|
|
360
376
|
},
|
|
361
377
|
|
|
362
378
|
renderResult(result, _options, theme, ctx) {
|
|
363
379
|
try {
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
380
|
+
return new AdaptiveCard(
|
|
381
|
+
(w) => renderAgentResult(result, _options, theme, ctx, w),
|
|
382
|
+
(text) => theme.fg("error", text),
|
|
383
|
+
);
|
|
384
|
+
} catch (e) {
|
|
385
|
+
// FAIL-VISIBLE — mirror the team renderer's error card.
|
|
386
|
+
const msg = e instanceof Error ? e.message : String(e);
|
|
387
|
+
return new Text(theme.fg("error", `✖ agent render error: ${truncLine(msg, 56)}`), 0, 0);
|
|
368
388
|
}
|
|
369
389
|
},
|
|
370
390
|
};
|
|
371
391
|
|
|
372
|
-
function renderAgentResult(result: Record<string, unknown>, options: unknown, theme: CrewTheme, ctx: ToolRenderContext): string {
|
|
392
|
+
function renderAgentResult(result: Record<string, unknown>, options: unknown, theme: CrewTheme, ctx: ToolRenderContext, w: number): string {
|
|
373
393
|
const d = (result.details ?? result) as Record<string, unknown>;
|
|
374
394
|
const results = d.results as Array<Record<string, unknown>> | undefined;
|
|
375
|
-
const
|
|
376
|
-
const innerW = w - 4;
|
|
395
|
+
const budget = w - 2;
|
|
377
396
|
const status = ((d.status ?? (results?.[0] as Record<string, unknown>)?.status ?? "") as string) || "completed";
|
|
378
|
-
const
|
|
379
|
-
|
|
380
|
-
const contentLines: string[] = [];
|
|
397
|
+
const closeSlot = railCloseSlot(status, ctx);
|
|
381
398
|
|
|
382
|
-
//
|
|
399
|
+
// ── Streaming ──
|
|
383
400
|
const isPartial = (options as Record<string, unknown>)?.isPartial === true;
|
|
384
401
|
if (isPartial && !ctx.expanded) {
|
|
385
|
-
const
|
|
386
|
-
const
|
|
387
|
-
|
|
388
|
-
// Extract progress from onUpdate content (formatCompactToolProgress output)
|
|
402
|
+
const name = (d.agentName as string) ?? (d.agentId as string) ?? "agent";
|
|
403
|
+
const spinner = theme.fg("accent", spinnerFrame(String(d.agentId ?? "")));
|
|
404
|
+
const meta: string[] = [];
|
|
389
405
|
const progressText = extractContentText(result?.content);
|
|
390
406
|
if (progressText) {
|
|
391
|
-
const
|
|
392
|
-
const
|
|
393
|
-
const
|
|
394
|
-
const elapsedSec = elapsedMatch ? parseInt(elapsedMatch[1]!, 10) : 0;
|
|
395
|
-
const tokens = tokenMatch ? parseInt(tokenMatch[1]!, 10) : 0;
|
|
407
|
+
const parsed = parseCompactToolProgress(progressText);
|
|
408
|
+
const elapsedSec = parsed ? Math.round(parsed.elapsedMs / 1000) : 0;
|
|
409
|
+
const tokens = parsed?.tokens ?? 0;
|
|
396
410
|
const tps = elapsedSec > 0 ? Math.round(tokens / elapsedSec) : 0;
|
|
397
|
-
|
|
398
|
-
if (
|
|
399
|
-
if (
|
|
400
|
-
if (toolMatch) parts.push(theme.fg("dim", `· ${toolMatch[1]}`));
|
|
411
|
+
if (elapsedSec > 0) meta.push(formatDuration(elapsedSec * 1000));
|
|
412
|
+
if (tps > 0) meta.push(`${formatTokens(tps)} tok/s`);
|
|
413
|
+
if (parsed?.currentTool) meta.push(parsed.currentTool);
|
|
401
414
|
}
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
return buildFrameBottom(contentLines, w, theme, "borderAccent", "borderAccent");
|
|
415
|
+
const left = `${spinner} ${theme.fg("toolTitle", theme.bold(name))}`;
|
|
416
|
+
return railLine(RAIL.body, "borderAccent", railLeaders(left, theme.fg("dim", meta.join(" · ")), budget, theme), theme, budget);
|
|
405
417
|
}
|
|
406
418
|
|
|
407
|
-
// Brief
|
|
419
|
+
// ── Brief / nameless results ──
|
|
408
420
|
if (!results?.length && !d.agentId) {
|
|
409
421
|
const briefText = briefToolResult("agent", result as { content?: unknown[] }, theme);
|
|
410
|
-
|
|
411
|
-
return buildFrameBottom(contentLines, w, theme, bColor, bColor);
|
|
422
|
+
return railLine(RAIL.close, closeSlot, `${briefText}`, theme, budget);
|
|
412
423
|
}
|
|
413
424
|
|
|
425
|
+
const label =
|
|
426
|
+
(d.agentName as string) ?? (d.agentId as string) ?? ((results?.[0] as Record<string, unknown>)?.agentId as string) ?? "agent";
|
|
427
|
+
|
|
428
|
+
// ── Closed card ──
|
|
414
429
|
if (!ctx.expanded) {
|
|
415
|
-
// Collapsed: compact card
|
|
416
430
|
const badge = statusBadge(status, theme);
|
|
417
|
-
const
|
|
418
|
-
const nameTag = theme.fg("toolTitle", theme.bold(agentId));
|
|
419
|
-
contentLines.push(padVisual(` ${badge} ${nameTag}`, innerW));
|
|
420
|
-
|
|
421
|
-
// Error or output preview
|
|
431
|
+
const head = `${badge} ${theme.fg("toolTitle", theme.bold(label))}`;
|
|
422
432
|
if (d.error) {
|
|
423
|
-
|
|
424
|
-
} else if (results?.length) {
|
|
425
|
-
const output = (results[0] as Record<string, unknown>).output as string | undefined;
|
|
426
|
-
if (output) {
|
|
427
|
-
const preview = truncLine(output.split("\n")[0] ?? "", innerW - 4);
|
|
428
|
-
contentLines.push(padVisual(` ${theme.fg("muted", preview)}`, innerW));
|
|
429
|
-
}
|
|
433
|
+
return railLine(RAIL.close, "error", `${head} ${theme.fg("error", truncLine(String(d.error), budget - 24))}`, theme, budget);
|
|
430
434
|
}
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
435
|
+
const output = results?.length ? ((results[0] as Record<string, unknown>).output as string | undefined) : undefined;
|
|
436
|
+
const preview = output ? output.split("\n").find((l) => l.trim()) : undefined;
|
|
437
|
+
const right = theme.fg("dim", PI_EXPAND_CHORD);
|
|
438
|
+
if (preview) {
|
|
439
|
+
const left = `${head} ${theme.fg("muted", truncLine(preview, Math.max(8, budget - visibleWidth(head) - 8)))}`;
|
|
440
|
+
return railLine(RAIL.close, closeSlot, railLeaders(left, right, budget, theme), theme, budget);
|
|
441
|
+
}
|
|
442
|
+
// Live 2026-09-16: with no output preview the cap printed a bare
|
|
443
|
+
// `┗ ● explorer`, so the expand affordance vanished exactly when the card
|
|
444
|
+
// is hardest to read (an agent run whose result carries no output line).
|
|
445
|
+
// The team card always shows the chord; this one now does too.
|
|
446
|
+
return railLine(RAIL.close, closeSlot, railLeaders(head, right, budget, theme), theme, budget);
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
// ── Expanded card ──
|
|
450
|
+
const lines: string[] = [];
|
|
451
|
+
if (results?.length) {
|
|
452
|
+
for (const item of results) {
|
|
453
|
+
const icon = statusIcon((item.status as string) ?? "", theme);
|
|
454
|
+
lines.push(
|
|
455
|
+
railLine(
|
|
456
|
+
RAIL.body,
|
|
457
|
+
"border",
|
|
458
|
+
`${icon} ${theme.fg("toolTitle", theme.bold((item.agentId as string) ?? "agent"))}`,
|
|
459
|
+
theme,
|
|
460
|
+
budget,
|
|
461
|
+
),
|
|
462
|
+
);
|
|
463
|
+
if (item.error) {
|
|
464
|
+
lines.push(previewLine(RAIL.body, "error", String(item.error), theme, budget));
|
|
465
|
+
} else if (item.output) {
|
|
466
|
+
for (const line of String(item.output).split("\n").slice(0, 5)) {
|
|
467
|
+
lines.push(previewLine(RAIL.body, "border", line, theme, budget));
|
|
451
468
|
}
|
|
452
469
|
}
|
|
453
|
-
} else if (d.agentId) {
|
|
454
|
-
const icon = statusIcon((d.status as string) ?? "", theme);
|
|
455
|
-
contentLines.push(padVisual(` ${icon} ${theme.fg("toolTitle", theme.bold(d.agentId as string))}`, innerW));
|
|
456
|
-
if (d.error) {
|
|
457
|
-
contentLines.push(padVisual(` ${theme.fg("error", truncLine(String(d.error), innerW - 6))}`, innerW));
|
|
458
|
-
}
|
|
459
|
-
} else {
|
|
460
|
-
const text = extractContentText(result?.content);
|
|
461
|
-
if (text) contentLines.push(padVisual(` ${theme.fg("dim", truncLine(text, innerW - 4))}`, innerW));
|
|
462
470
|
}
|
|
471
|
+
} else if (d.agentId) {
|
|
472
|
+
lines.push(
|
|
473
|
+
railLine(RAIL.body, "border", `${statusIcon(status, theme)} ${theme.fg("toolTitle", theme.bold(label))}`, theme, budget),
|
|
474
|
+
);
|
|
475
|
+
if (d.error) lines.push(previewLine(RAIL.body, "error", String(d.error), theme, budget));
|
|
476
|
+
} else {
|
|
477
|
+
lines.push(previewLine(RAIL.body, "border", extractContentText(result?.content), theme, budget));
|
|
463
478
|
}
|
|
464
|
-
|
|
465
|
-
return
|
|
479
|
+
lines.push(railLine(RAIL.close, closeSlot, `${theme.fg("muted", label)} ${theme.fg("dim", PI_EXPAND_CHORD)}`, theme, budget));
|
|
480
|
+
return lines.join("\n");
|
|
466
481
|
}
|
|
467
482
|
|
|
468
|
-
// ──
|
|
483
|
+
// ── Metrics ────────────────────────────────────────────────────────────
|
|
469
484
|
|
|
470
485
|
interface Metrics {
|
|
471
486
|
taskCount?: number;
|
|
@@ -475,134 +490,6 @@ interface Metrics {
|
|
|
475
490
|
durationMs?: number;
|
|
476
491
|
}
|
|
477
492
|
|
|
478
|
-
function appendRunCard(lines: string[], records: CrewAgentRecord[], status: string, runId: string, theme: CrewTheme, innerW: number): void {
|
|
479
|
-
const completed = records.filter((r) => r.status === "completed").length;
|
|
480
|
-
const total = records.length;
|
|
481
|
-
const duration = computeTotalDuration(records);
|
|
482
|
-
const tokens = computeTotalTokens(records);
|
|
483
|
-
const cost = computeTotalCost(records);
|
|
484
|
-
|
|
485
|
-
// Line 1: badge + summary + expand hint (P2)
|
|
486
|
-
const badge = statusBadge(status, theme);
|
|
487
|
-
const title = theme.fg("toolTitle", theme.bold("crew run"));
|
|
488
|
-
const idTag = theme.fg("dim", shortId(runId));
|
|
489
|
-
const hint = theme.fg("dim", "⌘E");
|
|
490
|
-
const left = ` ${badge} ${title} ${idTag}`;
|
|
491
|
-
const right = `${hint}`;
|
|
492
|
-
const gap = innerW - visibleWidth(left) - visibleWidth(right);
|
|
493
|
-
lines.push(padVisual(left + (gap > 1 ? " ".repeat(gap) : " ") + right, innerW));
|
|
494
|
-
|
|
495
|
-
// Line 2: compact metrics + cost (P1)
|
|
496
|
-
const parts: string[] = [];
|
|
497
|
-
parts.push(`${completed}/${total} done`);
|
|
498
|
-
if (duration > 0) parts.push(formatDuration(duration));
|
|
499
|
-
if (tokens > 0) parts.push(`${formatTokens(tokens)} tok`);
|
|
500
|
-
if (cost > 0) parts.push(`$${cost.toFixed(3)}`);
|
|
501
|
-
lines.push(padVisual(` ${theme.fg("dim", parts.join(" · "))}`, innerW));
|
|
502
|
-
}
|
|
503
|
-
|
|
504
|
-
function appendMetricsCard(lines: string[], m: Metrics, status: string, runId: string, theme: CrewTheme, innerW: number): void {
|
|
505
|
-
// Line 1: badge + title + expand hint (P2)
|
|
506
|
-
const badge = statusBadge(status, theme);
|
|
507
|
-
const title = theme.fg("toolTitle", theme.bold("crew run"));
|
|
508
|
-
const idTag = theme.fg("dim", shortId(runId));
|
|
509
|
-
const hint = theme.fg("dim", "⌘E");
|
|
510
|
-
const left = ` ${badge} ${title} ${idTag}`;
|
|
511
|
-
const right = `${hint}`;
|
|
512
|
-
const gap = innerW - visibleWidth(left) - visibleWidth(right);
|
|
513
|
-
lines.push(padVisual(left + (gap > 1 ? " ".repeat(gap) : " ") + right, innerW));
|
|
514
|
-
|
|
515
|
-
// Line 2: compact metrics + cost (P1)
|
|
516
|
-
const parts: string[] = [];
|
|
517
|
-
if (m.completedCount != null && m.taskCount) parts.push(`${m.completedCount}/${m.taskCount} done`);
|
|
518
|
-
if (m.durationMs) parts.push(formatDuration(m.durationMs));
|
|
519
|
-
if (m.totalTokens) parts.push(`${formatTokens(m.totalTokens)} tok`);
|
|
520
|
-
if (m.totalCost) parts.push(`$${m.totalCost.toFixed(3)}`);
|
|
521
|
-
if (parts.length) lines.push(padVisual(` ${theme.fg("dim", parts.join(" · "))}`, innerW));
|
|
522
|
-
}
|
|
523
|
-
|
|
524
|
-
function appendSimpleCard(lines: string[], status: string, runId: string, theme: CrewTheme, innerW: number): void {
|
|
525
|
-
const badge = statusBadge(status, theme);
|
|
526
|
-
const parts: string[] = [status];
|
|
527
|
-
if (runId) parts.push(shortId(runId));
|
|
528
|
-
lines.push(padVisual(` ${badge} ${theme.fg("text", parts.join(" · ") || "done")}`, innerW));
|
|
529
|
-
}
|
|
530
|
-
|
|
531
|
-
function appendExpandedRun(
|
|
532
|
-
lines: string[],
|
|
533
|
-
records: CrewAgentRecord[],
|
|
534
|
-
status: string,
|
|
535
|
-
runId: string,
|
|
536
|
-
theme: CrewTheme,
|
|
537
|
-
w: number,
|
|
538
|
-
innerW: number,
|
|
539
|
-
): void {
|
|
540
|
-
const completed = records.filter((r) => r.status === "completed").length;
|
|
541
|
-
const total = records.length;
|
|
542
|
-
const ratio = total > 0 ? completed / total : 0;
|
|
543
|
-
const duration = computeTotalDuration(records);
|
|
544
|
-
const tokens = computeTotalTokens(records);
|
|
545
|
-
|
|
546
|
-
// Header
|
|
547
|
-
lines.push(padVisual(` ${theme.fg("toolTitle", theme.bold("CREW RUN RESULT"))} ${theme.fg("dim", shortId(runId))}`, innerW));
|
|
548
|
-
lines.push(padVisual(theme.fg("borderMuted", "─".repeat(innerW - 2)), innerW));
|
|
549
|
-
|
|
550
|
-
// Progress bar
|
|
551
|
-
const barW = Math.min(innerW - 22, 40);
|
|
552
|
-
const bar = progressBar(ratio, barW, theme);
|
|
553
|
-
const count = theme.fg("muted", ` ${completed}/${total}`);
|
|
554
|
-
lines.push(padVisual(` ${theme.fg("muted", "Progress")} ${bar}${count}`, innerW));
|
|
555
|
-
|
|
556
|
-
// Metrics
|
|
557
|
-
const metricLine = [theme.fg("muted", formatDuration(duration)), theme.fg("muted", `${formatTokens(tokens)} tok`)].join(
|
|
558
|
-
theme.fg("dim", " · "),
|
|
559
|
-
);
|
|
560
|
-
lines.push(padVisual(` ${metricLine}`, innerW));
|
|
561
|
-
lines.push(padVisual(theme.fg("borderMuted", "─".repeat(innerW - 2)), innerW));
|
|
562
|
-
|
|
563
|
-
// Agent rows
|
|
564
|
-
for (const r of records) {
|
|
565
|
-
const icon = statusIcon(r.status, theme);
|
|
566
|
-
const role = theme.fg("toolTitle", theme.bold(r.role || r.agent || "agent"));
|
|
567
|
-
const model = r.model ? theme.fg("dim", ` (${r.model.split("/").at(-1)})`) : "";
|
|
568
|
-
const dur = r.startedAt ? formatDuration(computeRecordDuration(r)) : "";
|
|
569
|
-
const toolCount = `${r.toolUses ?? r.progress?.toolCount ?? 0} tools`;
|
|
570
|
-
lines.push(padVisual(` ${icon} ${role}${model} ${theme.fg("dim", `${toolCount} · ${dur}`)}`, innerW));
|
|
571
|
-
|
|
572
|
-
// Usage
|
|
573
|
-
const usage = r.usage;
|
|
574
|
-
const usageParts: string[] = [];
|
|
575
|
-
if (usage?.input) usageParts.push(theme.fg("dim", `↑${formatTokens(usage.input)}`));
|
|
576
|
-
if (usage?.output) usageParts.push(theme.fg("dim", `↓${formatTokens(usage.output)}`));
|
|
577
|
-
if (usage?.cost) usageParts.push(theme.fg("dim", `$${usage.cost.toFixed(3)}`));
|
|
578
|
-
if (usageParts.length) lines.push(padVisual(` ${usageParts.join(" ")}`, innerW));
|
|
579
|
-
}
|
|
580
|
-
}
|
|
581
|
-
|
|
582
|
-
function appendExpandedMetrics(
|
|
583
|
-
lines: string[],
|
|
584
|
-
m: Metrics | undefined,
|
|
585
|
-
status: string,
|
|
586
|
-
runId: string,
|
|
587
|
-
theme: CrewTheme,
|
|
588
|
-
w: number,
|
|
589
|
-
innerW: number,
|
|
590
|
-
): void {
|
|
591
|
-
lines.push(padVisual(` ${theme.fg("toolTitle", theme.bold("CREW RUN RESULT"))} ${theme.fg("dim", shortId(runId))}`, innerW));
|
|
592
|
-
lines.push(padVisual(theme.fg("borderMuted", "─".repeat(innerW - 2)), innerW));
|
|
593
|
-
if (m) {
|
|
594
|
-
const ratio = m.taskCount ? (m.completedCount ?? 0) / m.taskCount : 0;
|
|
595
|
-
const barW = Math.min(innerW - 22, 40);
|
|
596
|
-
const bar = progressBar(ratio, barW, theme);
|
|
597
|
-
const count = theme.fg("muted", ` ${m.completedCount ?? 0}/${m.taskCount ?? 0}`);
|
|
598
|
-
lines.push(padVisual(` ${bar}${count}`, innerW));
|
|
599
|
-
const parts: string[] = [];
|
|
600
|
-
if (m.durationMs) parts.push(formatDuration(m.durationMs));
|
|
601
|
-
if (m.totalTokens) parts.push(`${formatTokens(m.totalTokens)} tok`);
|
|
602
|
-
if (parts.length) lines.push(padVisual(` ${theme.fg("dim", parts.join(" · "))}`, innerW));
|
|
603
|
-
}
|
|
604
|
-
}
|
|
605
|
-
|
|
606
493
|
// ── Helpers ────────────────────────────────────────────────────────────
|
|
607
494
|
|
|
608
495
|
function extractContentText(content: unknown): string {
|
|
@@ -615,7 +502,11 @@ function extractContentText(content: unknown): string {
|
|
|
615
502
|
return String((texts[texts.length - 1]! as Record<string, unknown>).text ?? "");
|
|
616
503
|
}
|
|
617
504
|
|
|
618
|
-
/** Parse streaming progress text from team-tool progress binder.
|
|
505
|
+
/** Parse streaming progress text from the team-tool progress binder.
|
|
506
|
+
* The format is a shared contract — `parseCompactToolProgress`
|
|
507
|
+
* (../tool-progress-formatter.ts) owns the tokens; this adapter only maps them
|
|
508
|
+
* onto the card's display model (task counts win over the bare status row).
|
|
509
|
+
*/
|
|
619
510
|
interface StreamingProgress {
|
|
620
511
|
elapsedMs: number;
|
|
621
512
|
completed: number | null;
|
|
@@ -625,71 +516,31 @@ interface StreamingProgress {
|
|
|
625
516
|
}
|
|
626
517
|
|
|
627
518
|
function parseStreamingProgress(text: string): StreamingProgress | null {
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
// Format: "team status=starting elapsed=11s"
|
|
631
|
-
const elapsedMatch = text.match(/elapsed=(\d+)s/);
|
|
632
|
-
const elapsedMs = elapsedMatch ? parseInt(elapsedMatch[1]!, 10) * 1000 : 0;
|
|
633
|
-
|
|
634
|
-
// Format from formatCompactToolProgress: "tasks completed=2 running=1"
|
|
635
|
-
const taskMatch = text.match(/tasks[^\n]*(?:completed|done)=(\d+)[^\n]*(?:running|waiting|queued)=(\d+)/);
|
|
636
|
-
const doneMatch = text.match(/(\d+)\/(\d+)\s+done/);
|
|
637
|
-
|
|
638
|
-
// Active agent: " explorer->explorer turn=5 tokens=1234"
|
|
639
|
-
const agentMatch = text.match(/\s+(\w+)->(\w+)\s+turn=/);
|
|
640
|
-
const activeAgent = agentMatch ? `${agentMatch[1]}/${agentMatch[2]}` : null;
|
|
641
|
-
|
|
642
|
-
// Current tool: " tool: bash (#3)"
|
|
643
|
-
const toolMatch = text.match(/tool:\s+(\S+)/);
|
|
644
|
-
const toolInfo = toolMatch ? ` · ${toolMatch[1]}` : "";
|
|
519
|
+
const parsed = parseCompactToolProgress(text);
|
|
520
|
+
if (!parsed) return null;
|
|
645
521
|
|
|
646
|
-
|
|
522
|
+
const toolInfo = parsed.currentTool ? ` · ${parsed.currentTool}` : "";
|
|
523
|
+
if (parsed.completed != null && parsed.total != null) {
|
|
647
524
|
return {
|
|
648
|
-
elapsedMs,
|
|
649
|
-
completed:
|
|
650
|
-
total:
|
|
525
|
+
elapsedMs: parsed.elapsedMs,
|
|
526
|
+
completed: parsed.completed,
|
|
527
|
+
total: parsed.total,
|
|
651
528
|
status: null,
|
|
652
|
-
activeAgent: (activeAgent ?? "") + toolInfo,
|
|
529
|
+
activeAgent: (parsed.activeAgent ?? "") + toolInfo,
|
|
653
530
|
};
|
|
654
531
|
}
|
|
655
|
-
if (
|
|
656
|
-
const done = parseInt(taskMatch[1]!, 10);
|
|
657
|
-
const running = parseInt(taskMatch[2]!, 10);
|
|
658
|
-
const total = done + running;
|
|
532
|
+
if (parsed.elapsedMs > 0) {
|
|
659
533
|
return {
|
|
660
|
-
elapsedMs,
|
|
661
|
-
completed: done,
|
|
662
|
-
total,
|
|
663
|
-
status: null,
|
|
664
|
-
activeAgent: (activeAgent ?? "") + toolInfo,
|
|
665
|
-
};
|
|
666
|
-
}
|
|
667
|
-
if (elapsedMs > 0) {
|
|
668
|
-
const statusMatch = text.match(/status=(\w+)/);
|
|
669
|
-
return {
|
|
670
|
-
elapsedMs,
|
|
534
|
+
elapsedMs: parsed.elapsedMs,
|
|
671
535
|
completed: null,
|
|
672
536
|
total: null,
|
|
673
|
-
status:
|
|
674
|
-
activeAgent,
|
|
537
|
+
status: parsed.status,
|
|
538
|
+
activeAgent: parsed.activeAgent,
|
|
675
539
|
};
|
|
676
540
|
}
|
|
677
|
-
|
|
678
541
|
return null;
|
|
679
542
|
}
|
|
680
543
|
|
|
681
|
-
/** Animated scanning bar for indeterminate progress. */
|
|
682
|
-
function renderScanBar(barWidth: number, elapsedMs: number, theme: CrewTheme): string {
|
|
683
|
-
const pos = Math.floor((elapsedMs / 400) % (barWidth + 6)) - 3; // bounce range
|
|
684
|
-
const segW = Math.max(3, Math.floor(barWidth * 0.3));
|
|
685
|
-
let bar = "";
|
|
686
|
-
for (let i = 0; i < barWidth; i++) {
|
|
687
|
-
const inSeg = i >= pos && i < pos + segW;
|
|
688
|
-
bar += inSeg ? theme.fg("accent", "█") : theme.fg("dim", "░");
|
|
689
|
-
}
|
|
690
|
-
return bar;
|
|
691
|
-
}
|
|
692
|
-
|
|
693
544
|
function computeTotalDuration(records: CrewAgentRecord[]): number {
|
|
694
545
|
let total = 0;
|
|
695
546
|
for (const r of records) total += computeRecordDuration(r);
|
|
@@ -699,7 +550,7 @@ function computeTotalDuration(records: CrewAgentRecord[]): number {
|
|
|
699
550
|
function computeRecordDuration(r: CrewAgentRecord): number {
|
|
700
551
|
if (!r.startedAt) return 0;
|
|
701
552
|
const start = new Date(r.startedAt).getTime();
|
|
702
|
-
const end = r.completedAt ? new Date(r.completedAt).getTime() :
|
|
553
|
+
const end = r.completedAt ? new Date(r.completedAt).getTime() : spinnerClockNow();
|
|
703
554
|
if (!Number.isFinite(start) || !Number.isFinite(end)) return 0;
|
|
704
555
|
return Math.max(0, end - start);
|
|
705
556
|
}
|
|
@@ -720,14 +571,14 @@ function computeTotalTokens(records: CrewAgentRecord[]): number {
|
|
|
720
571
|
return total;
|
|
721
572
|
}
|
|
722
573
|
|
|
723
|
-
/**
|
|
574
|
+
/** Shorten file path by replacing $HOME with ~ */
|
|
724
575
|
function shortenPath(p: string): string {
|
|
725
576
|
const home = process.env.HOME ?? process.env.USERPROFILE ?? "";
|
|
726
577
|
if (home && p.startsWith(home)) return "~" + p.slice(home.length);
|
|
727
578
|
return p;
|
|
728
579
|
}
|
|
729
580
|
|
|
730
|
-
/**
|
|
581
|
+
/** Create clickable file hyperlink via OSC 8.
|
|
731
582
|
* Removed (M-13 fix, code-review 2026-06-23): this function was dead code (no
|
|
732
583
|
* callers) and interpolated a path into an OSC-8 escape without sanitizing
|
|
733
584
|
* control chars (\x07 BEL / \x1b\\ ST), a potential terminal-injection sink.
|