@bastani/atomic 0.8.21 → 0.8.22-0
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 +40 -9
- package/dist/builtin/intercom/broker/broker.ts +3 -3
- package/dist/builtin/intercom/config.ts +3 -3
- package/dist/builtin/intercom/index.ts +1 -1
- package/dist/builtin/intercom/package.json +1 -1
- package/dist/builtin/intercom/ui/compose.ts +2 -2
- package/dist/builtin/mcp/host-html-template.ts +0 -3
- package/dist/builtin/mcp/package.json +1 -1
- package/dist/builtin/subagents/CHANGELOG.md +13 -4
- package/dist/builtin/subagents/agents/codebase-online-researcher.md +9 -9
- package/dist/builtin/subagents/agents/debugger.md +6 -6
- package/dist/builtin/subagents/package.json +1 -1
- package/dist/builtin/subagents/prompts/parallel-handoff-plan.md +1 -1
- package/dist/builtin/subagents/skills/browser-use/SKILL.md +234 -0
- package/dist/builtin/subagents/skills/browser-use/references/cdp-python.md +76 -0
- package/dist/builtin/subagents/skills/browser-use/references/multi-session.md +92 -0
- package/dist/builtin/subagents/skills/subagent/SKILL.md +4 -4
- package/dist/builtin/subagents/src/agents/skills.ts +19 -1
- package/dist/builtin/subagents/src/extension/index.ts +24 -22
- package/dist/builtin/subagents/src/intercom/intercom-bridge.ts +7 -1
- package/dist/builtin/subagents/src/runs/background/async-execution.ts +23 -7
- package/dist/builtin/subagents/src/runs/background/async-job-tracker.ts +98 -3
- package/dist/builtin/subagents/src/runs/background/async-status.ts +3 -1
- package/dist/builtin/subagents/src/runs/background/run-status.ts +1 -1
- package/dist/builtin/subagents/src/runs/background/stale-run-reconciler.ts +3 -0
- package/dist/builtin/subagents/src/runs/background/subagent-runner.ts +37 -12
- package/dist/builtin/subagents/src/runs/foreground/chain-clarify.ts +15 -15
- package/dist/builtin/subagents/src/runs/foreground/execution.ts +26 -2
- package/dist/builtin/subagents/src/runs/shared/nested-render.ts +1 -1
- package/dist/builtin/subagents/src/runs/shared/parallel-utils.ts +7 -0
- package/dist/builtin/subagents/src/runs/shared/pi-args.ts +28 -1
- package/dist/builtin/subagents/src/shared/fast-mode.ts +80 -0
- package/dist/builtin/subagents/src/shared/formatters.ts +4 -2
- package/dist/builtin/subagents/src/shared/types.ts +4 -2
- package/dist/builtin/subagents/src/shared/utils.ts +3 -61
- package/dist/builtin/subagents/src/tui/render.ts +303 -157
- package/dist/builtin/web-access/package.json +1 -1
- package/dist/builtin/workflows/CHANGELOG.md +95 -35
- package/dist/builtin/workflows/README.md +228 -41
- package/dist/builtin/workflows/builtin/deep-research-codebase.ts +535 -541
- package/dist/builtin/workflows/builtin/goal.ts +39 -25
- package/dist/builtin/workflows/builtin/open-claude-design.ts +66 -69
- package/dist/builtin/workflows/builtin/ralph.ts +21 -21
- package/dist/builtin/workflows/package.json +6 -5
- package/dist/builtin/workflows/skills/research-codebase/SKILL.md +1 -1
- package/dist/builtin/workflows/src/extension/background-ui-adapter.ts +2 -2
- package/dist/builtin/workflows/src/extension/discovery.ts +25 -146
- package/dist/builtin/workflows/src/extension/dispatcher.ts +72 -24
- package/dist/builtin/workflows/src/extension/hil-answer-notifications.ts +363 -0
- package/dist/builtin/workflows/src/extension/index.ts +690 -352
- package/dist/builtin/workflows/src/extension/lifecycle-notifications.ts +99 -62
- package/dist/builtin/workflows/src/extension/render-call.ts +2 -1
- package/dist/builtin/workflows/src/extension/render-result.ts +9 -3
- package/dist/builtin/workflows/src/extension/renderers.ts +5 -3
- package/dist/builtin/workflows/src/extension/runtime.ts +68 -33
- package/dist/builtin/workflows/src/extension/status-writer.ts +1 -1
- package/dist/builtin/workflows/src/extension/wiring.ts +34 -13
- package/dist/builtin/workflows/src/extension/workflow-module-loader.ts +142 -0
- package/dist/builtin/workflows/src/extension/workflow-schema.ts +4 -4
- package/dist/builtin/workflows/src/index.ts +2 -0
- package/dist/builtin/workflows/src/intercom/result-intercom.ts +1 -1
- package/dist/builtin/workflows/src/runs/background/runner.ts +6 -4
- package/dist/builtin/workflows/src/runs/background/status.ts +45 -21
- package/dist/builtin/workflows/src/runs/foreground/executor.ts +624 -52
- package/dist/builtin/workflows/src/runs/foreground/stage-control-registry.ts +1 -1
- package/dist/builtin/workflows/src/runs/foreground/stage-runner.ts +80 -24
- package/dist/builtin/workflows/src/runs/shared/validate-inputs.ts +61 -24
- package/dist/builtin/workflows/src/runs/shared/workflow-runner.ts +32 -10
- package/dist/builtin/workflows/src/sdk-surface.ts +6 -0
- package/dist/builtin/workflows/src/shared/expanded-workflow-graph.ts +178 -0
- package/dist/builtin/workflows/src/shared/persistence-restore.ts +92 -12
- package/dist/builtin/workflows/src/shared/persistence-session-entries.ts +21 -3
- package/dist/builtin/workflows/src/shared/render-inputs-schema.ts +1 -2
- package/dist/builtin/workflows/src/shared/run-visibility.ts +9 -0
- package/dist/builtin/workflows/src/shared/schema-introspection.ts +121 -0
- package/dist/builtin/workflows/src/shared/serializable.ts +132 -0
- package/dist/builtin/workflows/src/shared/stage-ui-broker.ts +91 -9
- package/dist/builtin/workflows/src/shared/store-types.ts +31 -3
- package/dist/builtin/workflows/src/shared/store.ts +58 -14
- package/dist/builtin/workflows/src/shared/types.ts +105 -40
- package/dist/builtin/workflows/src/tui/chat-surface-message.ts +129 -13
- package/dist/builtin/workflows/src/tui/chat-surface.ts +6 -1
- package/dist/builtin/workflows/src/tui/dispatch-confirm.ts +3 -2
- package/dist/builtin/workflows/src/tui/graph-canvas.ts +1 -1
- package/dist/builtin/workflows/src/tui/graph-view.ts +91 -65
- package/dist/builtin/workflows/src/tui/inline-form-card.ts +1 -1
- package/dist/builtin/workflows/src/tui/inline-form-overlay.ts +3 -2
- package/dist/builtin/workflows/src/tui/inputs-overlay.ts +3 -2
- package/dist/builtin/workflows/src/tui/inputs-picker.ts +8 -7
- package/dist/builtin/workflows/src/tui/keybindings-adapter.ts +2 -0
- package/dist/builtin/workflows/src/tui/node-card.ts +34 -8
- package/dist/builtin/workflows/src/tui/overlay-adapter.ts +4 -11
- package/dist/builtin/workflows/src/tui/prompt-card.ts +98 -50
- package/dist/builtin/workflows/src/tui/session-list.ts +7 -2
- package/dist/builtin/workflows/src/tui/session-picker.ts +2 -0
- package/dist/builtin/workflows/src/tui/stage-chat-view.ts +226 -55
- package/dist/builtin/workflows/src/tui/status-helpers.ts +2 -0
- package/dist/builtin/workflows/src/tui/store-widget-installer.ts +37 -158
- package/dist/builtin/workflows/src/tui/toast.ts +2 -2
- package/dist/builtin/workflows/src/tui/widget.ts +53 -12
- package/dist/builtin/workflows/src/tui/workflow-attach-pane.ts +270 -19
- package/dist/builtin/workflows/src/tui/workflow-notice-card.ts +184 -0
- package/dist/builtin/workflows/src/workflows/define-workflow.ts +138 -43
- package/dist/config.d.ts +9 -0
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +45 -0
- package/dist/config.js.map +1 -1
- package/dist/core/agent-session.d.ts +27 -9
- package/dist/core/agent-session.d.ts.map +1 -1
- package/dist/core/agent-session.js +196 -17
- package/dist/core/agent-session.js.map +1 -1
- package/dist/core/atomic-guide-command.d.ts.map +1 -1
- package/dist/core/atomic-guide-command.js +2 -2
- package/dist/core/atomic-guide-command.js.map +1 -1
- package/dist/core/codex-fast-mode.d.ts +36 -0
- package/dist/core/codex-fast-mode.d.ts.map +1 -0
- package/dist/core/codex-fast-mode.js +117 -0
- package/dist/core/codex-fast-mode.js.map +1 -0
- package/dist/core/compaction/branch-summarization.d.ts.map +1 -1
- package/dist/core/compaction/branch-summarization.js +1 -1
- package/dist/core/compaction/branch-summarization.js.map +1 -1
- package/dist/core/compaction/compaction.d.ts.map +1 -1
- package/dist/core/compaction/compaction.js +1 -1
- package/dist/core/compaction/compaction.js.map +1 -1
- package/dist/core/extensions/index.d.ts +4 -1
- package/dist/core/extensions/index.d.ts.map +1 -1
- package/dist/core/extensions/index.js +1 -0
- package/dist/core/extensions/index.js.map +1 -1
- package/dist/core/extensions/loader.d.ts +7 -2
- package/dist/core/extensions/loader.d.ts.map +1 -1
- package/dist/core/extensions/loader.js +23 -8
- package/dist/core/extensions/loader.js.map +1 -1
- package/dist/core/extensions/reactive-widget.d.ts +58 -0
- package/dist/core/extensions/reactive-widget.d.ts.map +1 -0
- package/dist/core/extensions/reactive-widget.js +182 -0
- package/dist/core/extensions/reactive-widget.js.map +1 -0
- package/dist/core/extensions/runner.d.ts.map +1 -1
- package/dist/core/extensions/runner.js +1 -0
- package/dist/core/extensions/runner.js.map +1 -1
- package/dist/core/extensions/types.d.ts +26 -12
- package/dist/core/extensions/types.d.ts.map +1 -1
- package/dist/core/extensions/types.js.map +1 -1
- package/dist/core/messages.d.ts +1 -1
- package/dist/core/messages.d.ts.map +1 -1
- package/dist/core/messages.js +8 -2
- package/dist/core/messages.js.map +1 -1
- package/dist/core/model-registry.d.ts +4 -0
- package/dist/core/model-registry.d.ts.map +1 -1
- package/dist/core/model-registry.js +11 -0
- package/dist/core/model-registry.js.map +1 -1
- package/dist/core/resource-loader.d.ts +9 -1
- package/dist/core/resource-loader.d.ts.map +1 -1
- package/dist/core/resource-loader.js +49 -21
- package/dist/core/resource-loader.js.map +1 -1
- package/dist/core/sdk.d.ts.map +1 -1
- package/dist/core/sdk.js +22 -13
- package/dist/core/sdk.js.map +1 -1
- package/dist/core/session-manager.d.ts +7 -5
- package/dist/core/session-manager.d.ts.map +1 -1
- package/dist/core/session-manager.js +5 -3
- package/dist/core/session-manager.js.map +1 -1
- package/dist/core/settings-manager.d.ts +16 -0
- package/dist/core/settings-manager.d.ts.map +1 -1
- package/dist/core/settings-manager.js +64 -5
- package/dist/core/settings-manager.js.map +1 -1
- package/dist/core/slash-commands.d.ts.map +1 -1
- package/dist/core/slash-commands.js +1 -0
- package/dist/core/slash-commands.js.map +1 -1
- package/dist/core/system-prompt.d.ts.map +1 -1
- package/dist/core/system-prompt.js +7 -4
- package/dist/core/system-prompt.js.map +1 -1
- package/dist/core/tools/ask-user-question/ask-user-question.d.ts.map +1 -1
- package/dist/core/tools/ask-user-question/ask-user-question.js +2 -2
- package/dist/core/tools/ask-user-question/ask-user-question.js.map +1 -1
- package/dist/index.d.ts +4 -3
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +3 -2
- package/dist/index.js.map +1 -1
- package/dist/main.d.ts +3 -0
- package/dist/main.d.ts.map +1 -1
- package/dist/main.js +12 -0
- package/dist/main.js.map +1 -1
- package/dist/modes/interactive/chat-input-actions.d.ts.map +1 -1
- package/dist/modes/interactive/chat-input-actions.js.map +1 -1
- package/dist/modes/interactive/components/diff.d.ts.map +1 -1
- package/dist/modes/interactive/components/diff.js +0 -1
- package/dist/modes/interactive/components/diff.js.map +1 -1
- package/dist/modes/interactive/components/fast-mode-selector.d.ts +27 -0
- package/dist/modes/interactive/components/fast-mode-selector.d.ts.map +1 -0
- package/dist/modes/interactive/components/fast-mode-selector.js +105 -0
- package/dist/modes/interactive/components/fast-mode-selector.js.map +1 -0
- package/dist/modes/interactive/components/footer.d.ts.map +1 -1
- package/dist/modes/interactive/components/footer.js +7 -12
- package/dist/modes/interactive/components/footer.js.map +1 -1
- package/dist/modes/interactive/components/index.d.ts +1 -0
- package/dist/modes/interactive/components/index.d.ts.map +1 -1
- package/dist/modes/interactive/components/index.js +1 -0
- package/dist/modes/interactive/components/index.js.map +1 -1
- package/dist/modes/interactive/interactive-mode.d.ts +4 -0
- package/dist/modes/interactive/interactive-mode.d.ts.map +1 -1
- package/dist/modes/interactive/interactive-mode.js +132 -30
- package/dist/modes/interactive/interactive-mode.js.map +1 -1
- package/dist/modes/print-mode.d.ts.map +1 -1
- package/dist/modes/print-mode.js +53 -6
- package/dist/modes/print-mode.js.map +1 -1
- package/dist/modes/rpc/rpc-mode.d.ts.map +1 -1
- package/dist/modes/rpc/rpc-mode.js +3 -0
- package/dist/modes/rpc/rpc-mode.js.map +1 -1
- package/docs/compaction.md +1 -1
- package/docs/custom-provider.md +2 -2
- package/docs/development.md +2 -2
- package/docs/docs.json +2 -2
- package/docs/extensions.md +18 -13
- package/docs/providers.md +5 -1
- package/docs/quickstart.md +5 -3
- package/docs/rpc.md +5 -5
- package/docs/sdk.md +12 -12
- package/docs/settings.md +18 -0
- package/docs/themes.md +6 -6
- package/docs/tui.md +20 -18
- package/docs/usage.md +2 -0
- package/docs/workflows.md +403 -39
- package/examples/extensions/qna.ts +2 -2
- package/package.json +4 -4
- package/dist/builtin/subagents/skills/playwright-cli/SKILL.md +0 -392
- package/dist/builtin/subagents/skills/playwright-cli/references/element-attributes.md +0 -23
- package/dist/builtin/subagents/skills/playwright-cli/references/playwright-tests.md +0 -39
- package/dist/builtin/subagents/skills/playwright-cli/references/request-mocking.md +0 -87
- package/dist/builtin/subagents/skills/playwright-cli/references/running-code.md +0 -241
- package/dist/builtin/subagents/skills/playwright-cli/references/session-management.md +0 -225
- package/dist/builtin/subagents/skills/playwright-cli/references/spec-driven-testing.md +0 -305
- package/dist/builtin/subagents/skills/playwright-cli/references/storage-state.md +0 -275
- package/dist/builtin/subagents/skills/playwright-cli/references/test-generation.md +0 -134
- package/dist/builtin/subagents/skills/playwright-cli/references/tracing.md +0 -139
- package/dist/builtin/subagents/skills/playwright-cli/references/video-recording.md +0 -143
|
@@ -119,11 +119,11 @@ function runningSeed(...values: Array<number | undefined>): number | undefined {
|
|
|
119
119
|
return seed;
|
|
120
120
|
}
|
|
121
121
|
|
|
122
|
-
function runningGlyph(seed?: number): string {
|
|
122
|
+
function runningGlyph(seed?: number, now?: number): string {
|
|
123
123
|
// Fold the wall-clock frame into the (optional) progress seed so the glyph
|
|
124
|
-
// advances over time.
|
|
125
|
-
//
|
|
126
|
-
const animatedSeed = runningSeed(seed, currentRunningFrame()) ?? 0;
|
|
124
|
+
// advances over time. Callers that render into chat scrollback can pass a
|
|
125
|
+
// captured `now` so host re-renders do not mutate already-emitted lines.
|
|
126
|
+
const animatedSeed = runningSeed(seed, currentRunningFrame(now)) ?? 0;
|
|
127
127
|
return RUNNING_FRAMES[Math.abs(animatedSeed) % RUNNING_FRAMES.length]!;
|
|
128
128
|
}
|
|
129
129
|
|
|
@@ -140,83 +140,59 @@ function progressRunningSeed(progress: ProgressSeedSource | undefined): number |
|
|
|
140
140
|
);
|
|
141
141
|
}
|
|
142
142
|
|
|
143
|
-
type
|
|
143
|
+
type ResultAnimationTimer = ReturnType<typeof setInterval>;
|
|
144
144
|
|
|
145
|
-
interface
|
|
146
|
-
|
|
147
|
-
|
|
145
|
+
export interface SubagentResultRenderState {
|
|
146
|
+
subagentResultAnimationTimer?: ResultAnimationTimer;
|
|
147
|
+
subagentResultSnapshotKey?: string;
|
|
148
|
+
/** Stable semantic/content timestamp used for durations and activity text. */
|
|
149
|
+
subagentResultSnapshotNow?: number;
|
|
150
|
+
/** Timer-driven timestamp used only for spinner glyph frames. */
|
|
151
|
+
subagentResultSpinnerFrameNow?: number;
|
|
148
152
|
}
|
|
149
153
|
|
|
150
|
-
type
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
// Each tick reads the latest `invalidate` from here so a re-sync can refresh the
|
|
156
|
-
// callback if the host ever swaps render contexts for the same renderable.
|
|
157
|
-
const resultAnimationTimers = new Map<ReturnType<typeof setInterval>, ResultAnimationEntry>();
|
|
154
|
+
type ResultAnimationContext = {
|
|
155
|
+
state: SubagentResultRenderState;
|
|
156
|
+
invalidate: () => void;
|
|
157
|
+
};
|
|
158
|
+
type LegacyResultAnimationContext = { state: { subagentResultAnimationTimer?: ResultAnimationTimer } };
|
|
158
159
|
|
|
159
|
-
|
|
160
|
-
return Boolean(
|
|
161
|
-
result.details?.progress?.some((entry) => entry.status === "running")
|
|
162
|
-
|| result.details?.results.some((entry) => entry.progress?.status === "running"),
|
|
163
|
-
);
|
|
164
|
-
}
|
|
160
|
+
const activeResultAnimationTimers = new Map<ResultAnimationTimer, SubagentResultRenderState>();
|
|
165
161
|
|
|
166
|
-
function
|
|
162
|
+
export function clearResultAnimationTimer(context: LegacyResultAnimationContext): void {
|
|
167
163
|
const timer = context.state.subagentResultAnimationTimer;
|
|
168
|
-
if (
|
|
169
|
-
|
|
170
|
-
|
|
164
|
+
if (timer) {
|
|
165
|
+
clearInterval(timer);
|
|
166
|
+
activeResultAnimationTimers.delete(timer);
|
|
167
|
+
}
|
|
171
168
|
context.state.subagentResultAnimationTimer = undefined;
|
|
172
169
|
}
|
|
173
170
|
|
|
174
171
|
export function clearLegacyResultAnimationTimer(context: LegacyResultAnimationContext): void {
|
|
175
|
-
|
|
172
|
+
clearResultAnimationTimer(context);
|
|
176
173
|
}
|
|
177
174
|
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
* re-render while it is active, and tearing the timer down once it settles.
|
|
181
|
-
* The timer only calls `context.invalidate()`; the glyph value itself comes
|
|
182
|
-
* from {@link currentRunningFrame}, so each tick produces a single-glyph diff.
|
|
183
|
-
*/
|
|
184
|
-
export function syncResultAnimation(result: AgentToolResult<Details>, context: ResultAnimationContext): void {
|
|
185
|
-
if (!resultIsRunning(result)) {
|
|
186
|
-
stopResultAnimation(context);
|
|
187
|
-
return;
|
|
188
|
-
}
|
|
189
|
-
const existing = context.state.subagentResultAnimationTimer;
|
|
190
|
-
if (existing) {
|
|
191
|
-
// Keep using the most recent invalidate in case the host handed us a fresh
|
|
192
|
-
// render context object on this re-sync.
|
|
193
|
-
const entry = resultAnimationTimers.get(existing);
|
|
194
|
-
if (entry) entry.invalidate = context.invalidate;
|
|
195
|
-
return;
|
|
196
|
-
}
|
|
175
|
+
export function ensureResultAnimation(context: ResultAnimationContext): void {
|
|
176
|
+
if (context.state.subagentResultAnimationTimer) return;
|
|
197
177
|
const timer = setInterval(() => {
|
|
198
|
-
|
|
199
|
-
if (!entry) return;
|
|
178
|
+
context.state.subagentResultSpinnerFrameNow = Date.now();
|
|
200
179
|
try {
|
|
201
|
-
|
|
180
|
+
context.invalidate();
|
|
202
181
|
} catch {
|
|
203
|
-
|
|
204
|
-
// context after reload/session swap, or any other render glitch). Stop this
|
|
205
|
-
// timer; the next real render re-syncs and restarts it while still running.
|
|
206
|
-
stopResultAnimation(context);
|
|
182
|
+
clearResultAnimationTimer(context);
|
|
207
183
|
}
|
|
208
184
|
}, RUNNING_ANIMATION_MS);
|
|
209
185
|
timer.unref?.();
|
|
210
186
|
context.state.subagentResultAnimationTimer = timer;
|
|
211
|
-
|
|
187
|
+
activeResultAnimationTimers.set(timer, context.state);
|
|
212
188
|
}
|
|
213
189
|
|
|
214
190
|
export function stopResultAnimations(): void {
|
|
215
|
-
for (const [timer,
|
|
191
|
+
for (const [timer, state] of activeResultAnimationTimers) {
|
|
216
192
|
clearInterval(timer);
|
|
217
|
-
|
|
193
|
+
if (state.subagentResultAnimationTimer === timer) state.subagentResultAnimationTimer = undefined;
|
|
218
194
|
}
|
|
219
|
-
|
|
195
|
+
activeResultAnimationTimers.clear();
|
|
220
196
|
}
|
|
221
197
|
|
|
222
198
|
function extractOutputTarget(task: string): string | undefined {
|
|
@@ -247,9 +223,11 @@ function getToolCallLines(
|
|
|
247
223
|
}
|
|
248
224
|
|
|
249
225
|
|
|
250
|
-
function snapshotNowForProgress(progress: Pick<AgentProgress, "currentToolStartedAt" | "durationMs" | "lastActivityAt"
|
|
226
|
+
function snapshotNowForProgress(progress: Pick<AgentProgress, "currentToolStartedAt" | "durationMs" | "lastActivityAt">, now?: number): number | undefined {
|
|
227
|
+
if (now !== undefined) return now;
|
|
228
|
+
if (progress.lastActivityAt !== undefined) return progress.lastActivityAt;
|
|
251
229
|
if (progress.currentToolStartedAt !== undefined && progress.durationMs !== undefined) return progress.currentToolStartedAt + progress.durationMs;
|
|
252
|
-
return
|
|
230
|
+
return undefined;
|
|
253
231
|
}
|
|
254
232
|
|
|
255
233
|
function formatCurrentToolLine(
|
|
@@ -297,12 +275,25 @@ function formatToolUseStat(count: number): string {
|
|
|
297
275
|
return `${count} tool use${count === 1 ? "" : "s"}`;
|
|
298
276
|
}
|
|
299
277
|
|
|
300
|
-
function
|
|
278
|
+
function displayProgressDurationMs(progress: Pick<AgentProgress, "durationMs"> & Partial<Pick<AgentProgress, "lastActivityAt" | "status">>, now?: number): number {
|
|
279
|
+
if (progress.status === "running" && progress.lastActivityAt !== undefined && now !== undefined) {
|
|
280
|
+
return progress.durationMs + Math.max(0, now - progress.lastActivityAt);
|
|
281
|
+
}
|
|
282
|
+
return progress.durationMs;
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
function formatProgressStats(
|
|
286
|
+
theme: Theme,
|
|
287
|
+
progress: (Pick<AgentProgress, "toolCount" | "tokens" | "durationMs"> & Partial<Pick<AgentProgress, "lastActivityAt" | "status">>) | undefined,
|
|
288
|
+
includeDuration = true,
|
|
289
|
+
now?: number,
|
|
290
|
+
): string {
|
|
301
291
|
if (!progress) return "";
|
|
302
292
|
const parts: string[] = [];
|
|
303
293
|
if (progress.toolCount > 0) parts.push(formatToolUseStat(progress.toolCount));
|
|
304
294
|
if (progress.tokens > 0) parts.push(formatTokenStat(progress.tokens));
|
|
305
|
-
|
|
295
|
+
const durationMs = displayProgressDurationMs(progress, now);
|
|
296
|
+
if (includeDuration && durationMs > 0) parts.push(formatDuration(durationMs));
|
|
306
297
|
return statJoin(theme, parts);
|
|
307
298
|
}
|
|
308
299
|
|
|
@@ -318,8 +309,8 @@ function resultStatusLine(result: Details["results"][number], output: string): s
|
|
|
318
309
|
return "Done";
|
|
319
310
|
}
|
|
320
311
|
|
|
321
|
-
function resultGlyph(result: Details["results"][number], output: string, theme: Theme, running = result.progress?.status === "running", seed = progressRunningSeed(result.progress ?? result.progressSummary)): string {
|
|
322
|
-
if (running) return theme.fg("accent", runningGlyph(seed));
|
|
312
|
+
function resultGlyph(result: Details["results"][number], output: string, theme: Theme, running = result.progress?.status === "running", seed = progressRunningSeed(result.progress ?? result.progressSummary), now?: number): string {
|
|
313
|
+
if (running) return theme.fg("accent", runningGlyph(seed, now));
|
|
323
314
|
if (result.detached) return theme.fg("warning", "■");
|
|
324
315
|
if (result.interrupted) return theme.fg("warning", "■");
|
|
325
316
|
if (result.exitCode !== 0) return theme.fg("error", "✗");
|
|
@@ -327,8 +318,8 @@ function resultGlyph(result: Details["results"][number], output: string, theme:
|
|
|
327
318
|
return theme.fg("success", "✓");
|
|
328
319
|
}
|
|
329
320
|
|
|
330
|
-
function compactCurrentActivity(progress: AgentProgress): string {
|
|
331
|
-
const snapshotNow = snapshotNowForProgress(progress);
|
|
321
|
+
function compactCurrentActivity(progress: AgentProgress, now?: number): string {
|
|
322
|
+
const snapshotNow = snapshotNowForProgress(progress, now);
|
|
332
323
|
return formatCurrentToolLine(progress, getTermWidth() - 4, false, snapshotNow) ?? buildLiveStatusLine(progress, snapshotNow) ?? "thinking…";
|
|
333
324
|
}
|
|
334
325
|
|
|
@@ -432,16 +423,16 @@ function widgetJobsRunningSeed(jobs: AsyncJobState[]): number | undefined {
|
|
|
432
423
|
return seed;
|
|
433
424
|
}
|
|
434
425
|
|
|
435
|
-
function widgetStatusGlyph(job: AsyncJobState, theme: Theme): string {
|
|
436
|
-
if (job.status === "running") return theme.fg("accent", runningGlyph(widgetJobRunningSeed(job)));
|
|
426
|
+
function widgetStatusGlyph(job: AsyncJobState, theme: Theme, now?: number): string {
|
|
427
|
+
if (job.status === "running") return theme.fg("accent", runningGlyph(widgetJobRunningSeed(job), now));
|
|
437
428
|
if (job.status === "queued") return theme.fg("muted", "◦");
|
|
438
429
|
if (job.status === "complete") return theme.fg("success", "✓");
|
|
439
430
|
if (job.status === "paused") return theme.fg("warning", "■");
|
|
440
431
|
return theme.fg("error", "✗");
|
|
441
432
|
}
|
|
442
433
|
|
|
443
|
-
function widgetStepGlyph(status: AsyncJobStep["status"], theme: Theme, seed?: number): string {
|
|
444
|
-
if (status === "running") return theme.fg("accent", runningGlyph(seed));
|
|
434
|
+
function widgetStepGlyph(status: AsyncJobStep["status"], theme: Theme, seed?: number, now?: number): string {
|
|
435
|
+
if (status === "running") return theme.fg("accent", runningGlyph(seed, now));
|
|
445
436
|
if (status === "complete" || status === "completed") return theme.fg("success", "✓");
|
|
446
437
|
if (status === "failed") return theme.fg("error", "✗");
|
|
447
438
|
if (status === "paused") return theme.fg("warning", "■");
|
|
@@ -471,7 +462,7 @@ function widgetStepActivity(step: NonNullable<AsyncJobState["steps"]>[number], s
|
|
|
471
462
|
}
|
|
472
463
|
|
|
473
464
|
|
|
474
|
-
function widgetChainDetails(job: AsyncJobState, theme: Theme, expanded = false, width = getTermWidth()): string[] {
|
|
465
|
+
function widgetChainDetails(job: AsyncJobState, theme: Theme, expanded = false, width = getTermWidth(), now?: number): string[] {
|
|
475
466
|
if (!job.steps?.length) return [];
|
|
476
467
|
const total = job.chainStepCount ?? job.steps.length;
|
|
477
468
|
const lines: string[] = [];
|
|
@@ -479,7 +470,7 @@ function widgetChainDetails(job: AsyncJobState, theme: Theme, expanded = false,
|
|
|
479
470
|
const steps = job.steps.slice(span.start, span.start + span.count);
|
|
480
471
|
if (span.isParallel) {
|
|
481
472
|
const status = aggregateStepStatus(steps);
|
|
482
|
-
lines.push(` ${widgetStepGlyph(status, theme, widgetStepsRunningSeed(steps))} Step ${span.stepIndex + 1}/${total}: ${themeBold(theme, "parallel group")} ${theme.fg("dim", "·")} ${theme.fg("dim", formatParallelOutcome(steps, span.count))}`);
|
|
473
|
+
lines.push(` ${widgetStepGlyph(status, theme, widgetStepsRunningSeed(steps), now)} Step ${span.stepIndex + 1}/${total}: ${themeBold(theme, "parallel group")} ${theme.fg("dim", "·")} ${theme.fg("dim", formatParallelOutcome(steps, span.count))}`);
|
|
483
474
|
continue;
|
|
484
475
|
}
|
|
485
476
|
const step = steps[0];
|
|
@@ -487,24 +478,24 @@ function widgetChainDetails(job: AsyncJobState, theme: Theme, expanded = false,
|
|
|
487
478
|
lines.push(` ${theme.fg("dim", `◦ Step ${span.stepIndex + 1}/${total}: pending`)}`);
|
|
488
479
|
continue;
|
|
489
480
|
}
|
|
490
|
-
lines.push(...foregroundStyleWidgetStepLines(job, theme, step, "Step", span.stepIndex + 1, total, expanded, width));
|
|
481
|
+
lines.push(...foregroundStyleWidgetStepLines(job, theme, step, "Step", span.stepIndex + 1, total, expanded, width, now));
|
|
491
482
|
}
|
|
492
483
|
return lines;
|
|
493
484
|
}
|
|
494
485
|
|
|
495
|
-
function widgetParallelAgentDetails(job: AsyncJobState, theme: Theme, expanded = false, width = getTermWidth()): string[] {
|
|
486
|
+
function widgetParallelAgentDetails(job: AsyncJobState, theme: Theme, expanded = false, width = getTermWidth(), now?: number): string[] {
|
|
496
487
|
if (!job.steps?.length) return [];
|
|
497
488
|
if (job.mode !== "parallel" && job.mode !== "chain") return [];
|
|
498
|
-
if (job.mode === "chain" && !job.activeParallelGroup && job.parallelGroups?.length) return widgetChainDetails(job, theme, expanded, width);
|
|
489
|
+
if (job.mode === "chain" && !job.activeParallelGroup && job.parallelGroups?.length) return widgetChainDetails(job, theme, expanded, width, now);
|
|
499
490
|
const total = job.stepsTotal ?? job.steps.length;
|
|
500
491
|
const lines: string[] = [];
|
|
501
492
|
for (const [index, step] of job.steps.entries()) {
|
|
502
493
|
const marker = index === job.steps.length - 1 ? "└" : "├";
|
|
503
494
|
const activity = widgetStepActivity(step, job.updatedAt);
|
|
504
495
|
const itemTitle = job.mode === "parallel" || job.activeParallelGroup ? "Agent" : "Step";
|
|
505
|
-
const modelDisplay = modelThinkingBadge(theme, step.model, step.thinking);
|
|
506
|
-
lines.push(` ${theme.fg("dim", `${marker} ${widgetStepGlyph(step.status, theme, widgetStepRunningSeed(step, index))} ${itemTitle} ${index + 1}/${total}: ${step.agent} · ${widgetStepStatus(step.status, theme)}${modelDisplay}${activity ? ` · ${activity}` : ""}`)}`);
|
|
507
|
-
for (const nestedLine of formatNestedWidgetLines(step.children, theme, width, expanded, job.updatedAt, expanded ? 8 : 1)) lines.push(` ${nestedLine}`);
|
|
496
|
+
const modelDisplay = modelThinkingBadge(theme, step.model, step.thinking, step.fastMode);
|
|
497
|
+
lines.push(` ${theme.fg("dim", `${marker} ${widgetStepGlyph(step.status, theme, widgetStepRunningSeed(step, index), now)} ${itemTitle} ${index + 1}/${total}: ${step.agent} · ${widgetStepStatus(step.status, theme)}${modelDisplay}${activity ? ` · ${activity}` : ""}`)}`);
|
|
498
|
+
for (const nestedLine of formatNestedWidgetLines(step.children, theme, width, expanded, job.updatedAt, expanded ? 8 : 1, now)) lines.push(` ${nestedLine}`);
|
|
508
499
|
}
|
|
509
500
|
return lines;
|
|
510
501
|
}
|
|
@@ -723,8 +714,8 @@ function widgetStepStats(theme: Theme, step: NonNullable<AsyncJobState["steps"]>
|
|
|
723
714
|
]);
|
|
724
715
|
}
|
|
725
716
|
|
|
726
|
-
function modelThinkingBadge(theme: Theme, model?: string, thinking?: string): string {
|
|
727
|
-
const label = formatModelThinking(model, thinking);
|
|
717
|
+
function modelThinkingBadge(theme: Theme, model?: string, thinking?: string, fastMode?: boolean): string {
|
|
718
|
+
const label = formatModelThinking(model, thinking, fastMode);
|
|
728
719
|
return label ? theme.fg("dim", ` (${label})`) : "";
|
|
729
720
|
}
|
|
730
721
|
|
|
@@ -748,8 +739,8 @@ function nestedRunName(run: NestedRunSummary): string {
|
|
|
748
739
|
return run.id;
|
|
749
740
|
}
|
|
750
741
|
|
|
751
|
-
function nestedStatusGlyph(state: NestedRunSummary["state"] | NestedStepSummary["status"], theme: Theme, seed?: number): string {
|
|
752
|
-
if (state === "running") return theme.fg("accent", runningGlyph(seed));
|
|
742
|
+
function nestedStatusGlyph(state: NestedRunSummary["state"] | NestedStepSummary["status"], theme: Theme, seed?: number, now?: number): string {
|
|
743
|
+
if (state === "running") return theme.fg("accent", runningGlyph(seed, now));
|
|
753
744
|
if (state === "complete" || state === "completed") return theme.fg("success", "✓");
|
|
754
745
|
if (state === "failed") return theme.fg("error", "✗");
|
|
755
746
|
if (state === "paused") return theme.fg("warning", "■");
|
|
@@ -778,7 +769,7 @@ function nestedActivity(input: Pick<NestedRunSummary | NestedStepSummary, "activ
|
|
|
778
769
|
return "Done";
|
|
779
770
|
}
|
|
780
771
|
|
|
781
|
-
function formatNestedWidgetLines(children: NestedRunSummary[] | undefined, theme: Theme, width: number, expanded: boolean, snapshotNow?: number, lineBudget = expanded ? 12 : 1): string[] {
|
|
772
|
+
function formatNestedWidgetLines(children: NestedRunSummary[] | undefined, theme: Theme, width: number, expanded: boolean, snapshotNow?: number, lineBudget = expanded ? 12 : 1, now?: number): string[] {
|
|
782
773
|
if (!children?.length || lineBudget <= 0) return [];
|
|
783
774
|
if (!expanded) {
|
|
784
775
|
const aggregate = formatNestedAggregate(children);
|
|
@@ -802,7 +793,7 @@ function formatNestedWidgetLines(children: NestedRunSummary[] | undefined, theme
|
|
|
802
793
|
}
|
|
803
794
|
const activity = nestedActivity(child, child.state, snapshotNow ?? child.lastUpdate);
|
|
804
795
|
const error = child.error ? ` · ${child.error}` : "";
|
|
805
|
-
lines.push(theme.fg("dim", `${prefix}↳ ${nestedStatusGlyph(child.state, theme, nestedRunSeed(child))} ${nestedRunName(child)} · ${child.state} · ${activity}${error}`));
|
|
796
|
+
lines.push(theme.fg("dim", `${prefix}↳ ${nestedStatusGlyph(child.state, theme, nestedRunSeed(child), now)} ${nestedRunName(child)} · ${child.state} · ${activity}${error}`));
|
|
806
797
|
if (depth === maxDepth) {
|
|
807
798
|
const aggregate = formatNestedAggregate([...(child.steps?.flatMap((step) => step.children ?? []) ?? []), ...(child.children ?? [])]);
|
|
808
799
|
if (aggregate && lines.length < lineBudget) lines.push(theme.fg("dim", `${prefix} ↳ ${aggregate}`));
|
|
@@ -810,7 +801,7 @@ function formatNestedWidgetLines(children: NestedRunSummary[] | undefined, theme
|
|
|
810
801
|
}
|
|
811
802
|
for (const step of child.steps ?? []) {
|
|
812
803
|
if (lines.length >= lineBudget) return;
|
|
813
|
-
lines.push(theme.fg("dim", `${prefix} ↳ ${nestedStatusGlyph(step.status, theme)} ${step.agent} · ${step.status} · ${nestedActivity(step, step.status, snapshotNow ?? child.lastUpdate)}`));
|
|
804
|
+
lines.push(theme.fg("dim", `${prefix} ↳ ${nestedStatusGlyph(step.status, theme, undefined, now)} ${step.agent} · ${step.status} · ${nestedActivity(step, step.status, snapshotNow ?? child.lastUpdate)}`));
|
|
814
805
|
append(step.children, depth + 1, `${prefix} `);
|
|
815
806
|
}
|
|
816
807
|
append(child.children, depth + 1, `${prefix} `);
|
|
@@ -829,14 +820,15 @@ function foregroundStyleWidgetStepLines(
|
|
|
829
820
|
total: number,
|
|
830
821
|
expanded: boolean,
|
|
831
822
|
width: number,
|
|
823
|
+
now?: number,
|
|
832
824
|
): string[] {
|
|
833
825
|
const status = widgetStepStatus(step.status, theme);
|
|
834
826
|
const stats = widgetStepStats(theme, step);
|
|
835
|
-
const modelDisplay = modelThinkingBadge(theme, step.model, step.thinking);
|
|
836
|
-
const lines = [` ${widgetStepGlyph(step.status, theme, widgetStepRunningSeed(step, index - 1))} ${itemTitle} ${index}/${total}: ${themeBold(theme, step.agent)} ${theme.fg("dim", "·")} ${status}${modelDisplay}${stats ? ` ${theme.fg("dim", "·")} ${stats}` : ""}`];
|
|
827
|
+
const modelDisplay = modelThinkingBadge(theme, step.model, step.thinking, step.fastMode);
|
|
828
|
+
const lines = [` ${widgetStepGlyph(step.status, theme, widgetStepRunningSeed(step, index - 1), now)} ${itemTitle} ${index}/${total}: ${themeBold(theme, step.agent)} ${theme.fg("dim", "·")} ${status}${modelDisplay}${stats ? ` ${theme.fg("dim", "·")} ${stats}` : ""}`];
|
|
837
829
|
const activity = widgetStepActivityLine(step, width, expanded, job.updatedAt);
|
|
838
830
|
if (activity) lines.push(` ${theme.fg("dim", `⎿ ${activity}`)}`);
|
|
839
|
-
for (const nestedLine of formatNestedWidgetLines(step.children, theme, width, expanded, job.updatedAt)) {
|
|
831
|
+
for (const nestedLine of formatNestedWidgetLines(step.children, theme, width, expanded, job.updatedAt, undefined, now)) {
|
|
840
832
|
lines.push(` ${nestedLine}`);
|
|
841
833
|
}
|
|
842
834
|
if (step.status === "running") {
|
|
@@ -859,40 +851,40 @@ function foregroundStyleWidgetStepLines(
|
|
|
859
851
|
return lines;
|
|
860
852
|
}
|
|
861
853
|
|
|
862
|
-
function foregroundStyleWidgetDetails(job: AsyncJobState, theme: Theme, expanded: boolean, width: number): string[] {
|
|
854
|
+
function foregroundStyleWidgetDetails(job: AsyncJobState, theme: Theme, expanded: boolean, width: number, now?: number): string[] {
|
|
863
855
|
if (!job.steps?.length) return [
|
|
864
856
|
` ${theme.fg("dim", `⎿ ${widgetActivity(job)}`)}`,
|
|
865
|
-
...formatNestedWidgetLines(job.nestedChildren, theme, width, expanded, job.updatedAt).map((line) => ` ${line}`),
|
|
857
|
+
...formatNestedWidgetLines(job.nestedChildren, theme, width, expanded, job.updatedAt, undefined, now).map((line) => ` ${line}`),
|
|
866
858
|
];
|
|
867
|
-
if (job.mode === "chain" && !job.activeParallelGroup && job.parallelGroups?.length) return widgetChainDetails(job, theme, expanded, width);
|
|
859
|
+
if (job.mode === "chain" && !job.activeParallelGroup && job.parallelGroups?.length) return widgetChainDetails(job, theme, expanded, width, now);
|
|
868
860
|
const total = job.stepsTotal ?? job.steps.length;
|
|
869
861
|
const itemTitle = job.mode === "parallel" || job.activeParallelGroup ? "Agent" : "Step";
|
|
870
862
|
const lines: string[] = [];
|
|
871
863
|
for (const [index, step] of job.steps.entries()) {
|
|
872
|
-
lines.push(...foregroundStyleWidgetStepLines(job, theme, step, itemTitle, index + 1, total, expanded, width));
|
|
864
|
+
lines.push(...foregroundStyleWidgetStepLines(job, theme, step, itemTitle, index + 1, total, expanded, width, now));
|
|
873
865
|
}
|
|
874
866
|
const attached = new Set(job.steps.flatMap((step) => step.children?.map((child) => child.id) ?? []));
|
|
875
867
|
const unattached = job.nestedChildren?.filter((child) => !attached.has(child.id)) ?? [];
|
|
876
|
-
for (const nestedLine of formatNestedWidgetLines(unattached, theme, width, expanded, job.updatedAt)) {
|
|
868
|
+
for (const nestedLine of formatNestedWidgetLines(unattached, theme, width, expanded, job.updatedAt, undefined, now)) {
|
|
877
869
|
lines.push(` ${nestedLine}`);
|
|
878
870
|
}
|
|
879
871
|
return lines;
|
|
880
872
|
}
|
|
881
873
|
|
|
882
|
-
function buildSingleWidgetLines(job: AsyncJobState, theme: Theme, width: number, expanded: boolean): string[] {
|
|
874
|
+
function buildSingleWidgetLines(job: AsyncJobState, theme: Theme, width: number, expanded: boolean, now?: number): string[] {
|
|
883
875
|
const stats = widgetStats(job, theme);
|
|
884
876
|
const count = job.mode === "chain" ? job.chainStepCount : job.stepsTotal ?? job.agents?.length ?? job.steps?.length;
|
|
885
877
|
const mode = widgetJobName(job);
|
|
886
878
|
const title = `async subagent ${mode}${count && count > 1 ? ` (${count})` : ""}`;
|
|
887
879
|
return [
|
|
888
880
|
`${theme.fg("toolTitle", themeBold(theme, title))} ${theme.fg("dim", "· background")}`,
|
|
889
|
-
`${widgetStatusGlyph(job, theme)} ${themeBold(theme, mode)}${stats ? ` ${theme.fg("dim", "·")} ${stats}` : ""}`,
|
|
890
|
-
...foregroundStyleWidgetDetails(job, theme, expanded, width),
|
|
881
|
+
`${widgetStatusGlyph(job, theme, now)} ${themeBold(theme, mode)}${stats ? ` ${theme.fg("dim", "·")} ${stats}` : ""}`,
|
|
882
|
+
...foregroundStyleWidgetDetails(job, theme, expanded, width, now),
|
|
891
883
|
].map((line) => truncLine(line, width));
|
|
892
884
|
}
|
|
893
885
|
|
|
894
|
-
function compactSingleWidgetLines(job: AsyncJobState, theme: Theme, width: number): string[] {
|
|
895
|
-
const fullLines = buildSingleWidgetLines(job, theme, width, false);
|
|
886
|
+
function compactSingleWidgetLines(job: AsyncJobState, theme: Theme, width: number, now?: number): string[] {
|
|
887
|
+
const fullLines = buildSingleWidgetLines(job, theme, width, false, now);
|
|
896
888
|
if (fullLines.length <= 10 || !job.steps?.length || (job.mode !== "parallel" && !job.activeParallelGroup)) return fullLines;
|
|
897
889
|
|
|
898
890
|
const total = job.stepsTotal ?? job.steps.length;
|
|
@@ -903,9 +895,9 @@ function compactSingleWidgetLines(job: AsyncJobState, theme: Theme, width: numbe
|
|
|
903
895
|
const activity = widgetStepActivityLine(step, width, false, job.updatedAt);
|
|
904
896
|
const stepStats = widgetStepStats(theme, step);
|
|
905
897
|
const activitySuffix = activity ? ` ${theme.fg("dim", "·")} ${theme.fg("dim", activity)}` : "";
|
|
906
|
-
const modelDisplay = modelThinkingBadge(theme, step.model, step.thinking);
|
|
907
|
-
lines.push(` ${widgetStepGlyph(step.status, theme, widgetStepRunningSeed(step, index))} ${itemTitle} ${index + 1}/${total}: ${themeBold(theme, step.agent)} ${theme.fg("dim", "·")} ${status}${modelDisplay}${activitySuffix}${stepStats ? ` ${theme.fg("dim", "·")} ${stepStats}` : ""}`);
|
|
908
|
-
for (const nestedLine of formatNestedWidgetLines(step.children, theme, width, false, job.updatedAt)) lines.push(` ${nestedLine}`);
|
|
898
|
+
const modelDisplay = modelThinkingBadge(theme, step.model, step.thinking, step.fastMode);
|
|
899
|
+
lines.push(` ${widgetStepGlyph(step.status, theme, widgetStepRunningSeed(step, index), now)} ${itemTitle} ${index + 1}/${total}: ${themeBold(theme, step.agent)} ${theme.fg("dim", "·")} ${status}${modelDisplay}${activitySuffix}${stepStats ? ` ${theme.fg("dim", "·")} ${stepStats}` : ""}`);
|
|
900
|
+
for (const nestedLine of formatNestedWidgetLines(step.children, theme, width, false, job.updatedAt, undefined, now)) lines.push(` ${nestedLine}`);
|
|
909
901
|
}
|
|
910
902
|
if (job.steps.some((step) => step.status === "running")) lines.push(theme.fg("accent", " Press ctrl+o for live detail"));
|
|
911
903
|
return lines.map((line) => truncLine(line, width));
|
|
@@ -926,38 +918,44 @@ function fitWidgetLineBudget(lines: string[], theme: Theme, width: number, expan
|
|
|
926
918
|
}
|
|
927
919
|
|
|
928
920
|
/**
|
|
929
|
-
* Live async-agents widget. Recomputes its lines on every render
|
|
930
|
-
*
|
|
931
|
-
*
|
|
921
|
+
* Live async-agents widget. Recomputes its lines on every render from the latest
|
|
922
|
+
* job snapshot, but reads animation time from the widget ticker/status-update
|
|
923
|
+
* snapshot rather than Date.now(). That keeps arbitrary host re-renders from
|
|
924
|
+
* mutating widget bytes unless the widget itself intentionally advanced.
|
|
932
925
|
*/
|
|
933
926
|
class LiveWidgetComponent implements Component {
|
|
934
927
|
private readonly container = new Container();
|
|
935
928
|
|
|
936
929
|
constructor(
|
|
937
|
-
private readonly
|
|
930
|
+
private readonly getJobs: () => AsyncJobState[],
|
|
938
931
|
private readonly theme: Theme,
|
|
939
932
|
private readonly getExpanded: () => boolean,
|
|
933
|
+
private readonly getNow: () => number,
|
|
940
934
|
) {}
|
|
941
935
|
|
|
942
936
|
render(width: number): string[] {
|
|
937
|
+
const jobs = this.getJobs();
|
|
943
938
|
const expanded = this.getExpanded();
|
|
944
|
-
const
|
|
945
|
-
|
|
946
|
-
: this.jobs.length === 1
|
|
947
|
-
? compactSingleWidgetLines(this.jobs[0]!, this.theme, width)
|
|
948
|
-
: buildWidgetLines(this.jobs, this.theme, width, false);
|
|
939
|
+
const now = this.getNow();
|
|
940
|
+
const lines = this.buildLines(jobs, width, expanded, now);
|
|
949
941
|
this.container.clear();
|
|
950
942
|
for (const line of fitWidgetLineBudget(lines, this.theme, width, expanded)) this.container.addChild(new Text(line, 1, 0));
|
|
951
943
|
return this.container.render(width);
|
|
952
944
|
}
|
|
953
945
|
|
|
946
|
+
private buildLines(jobs: AsyncJobState[], width: number, expanded: boolean, now: number): string[] {
|
|
947
|
+
if (expanded) return buildWidgetLines(jobs, this.theme, width, true, now);
|
|
948
|
+
if (jobs.length === 1) return compactSingleWidgetLines(jobs[0]!, this.theme, width, now);
|
|
949
|
+
return buildWidgetLines(jobs, this.theme, width, false, now);
|
|
950
|
+
}
|
|
951
|
+
|
|
954
952
|
invalidate(): void {
|
|
955
953
|
this.container.invalidate();
|
|
956
954
|
}
|
|
957
955
|
}
|
|
958
956
|
|
|
959
|
-
function buildWidgetComponent(
|
|
960
|
-
return (_tui, theme) => new LiveWidgetComponent(
|
|
957
|
+
function buildWidgetComponent(getJobs: () => AsyncJobState[], getExpanded: () => boolean, getNow: () => number): (_tui: unknown, theme: Theme) => Component {
|
|
958
|
+
return (_tui, theme) => new LiveWidgetComponent(getJobs, theme, getExpanded, getNow);
|
|
961
959
|
}
|
|
962
960
|
|
|
963
961
|
interface RenderRequestingContext {
|
|
@@ -965,10 +963,53 @@ interface RenderRequestingContext {
|
|
|
965
963
|
}
|
|
966
964
|
|
|
967
965
|
// There is only ever one async-agents widget per host process, so the widget
|
|
968
|
-
// ticker
|
|
966
|
+
// ticker and mounted component read their driving context/jobs from module-level
|
|
967
|
+
// singletons instead of remounting the widget for every visible update.
|
|
969
968
|
let latestWidgetCtx: ExtensionContext | undefined;
|
|
970
969
|
let latestWidgetJobs: AsyncJobState[] = [];
|
|
970
|
+
let latestWidgetFrameNow = 0;
|
|
971
971
|
let widgetTimer: ReturnType<typeof setInterval> | undefined;
|
|
972
|
+
let mountedWidgetCtx: ExtensionContext | undefined;
|
|
973
|
+
let widgetMounted = false;
|
|
974
|
+
|
|
975
|
+
function getLatestWidgetJobs(): AsyncJobState[] {
|
|
976
|
+
return latestWidgetJobs;
|
|
977
|
+
}
|
|
978
|
+
|
|
979
|
+
function getLatestWidgetFrameNow(): number {
|
|
980
|
+
return latestWidgetFrameNow;
|
|
981
|
+
}
|
|
982
|
+
|
|
983
|
+
function getLatestWidgetExpanded(): boolean {
|
|
984
|
+
// LiveWidgetComponent re-renders outside a specific renderWidget() call, so
|
|
985
|
+
// read expansion from the latest live singleton context. If that context was
|
|
986
|
+
// cleared or went stale, collapse safely instead of consulting a stale caller.
|
|
987
|
+
if (!latestWidgetCtx?.hasUI) return false;
|
|
988
|
+
return latestWidgetCtx.ui.getToolsExpanded?.() ?? false;
|
|
989
|
+
}
|
|
990
|
+
|
|
991
|
+
function clearLatestWidgetState(): void {
|
|
992
|
+
latestWidgetCtx = undefined;
|
|
993
|
+
latestWidgetJobs = [];
|
|
994
|
+
latestWidgetFrameNow = 0;
|
|
995
|
+
mountedWidgetCtx = undefined;
|
|
996
|
+
widgetMounted = false;
|
|
997
|
+
}
|
|
998
|
+
|
|
999
|
+
function requestWidgetRender(ctx: ExtensionContext): void {
|
|
1000
|
+
(ctx as RenderRequestingContext).ui.requestRender?.();
|
|
1001
|
+
}
|
|
1002
|
+
|
|
1003
|
+
function unmountWidgetBestEffort(ctx: ExtensionContext | undefined): void {
|
|
1004
|
+
if (!ctx?.hasUI) return;
|
|
1005
|
+
try {
|
|
1006
|
+
ctx.ui.setWidget(WIDGET_KEY, undefined);
|
|
1007
|
+
} catch {
|
|
1008
|
+
// Best-effort teardown only: stale host contexts can reject cleanup during
|
|
1009
|
+
// reload/session rebinding, but local state still needs to move on so the
|
|
1010
|
+
// next status update can mount cleanly on the active UI context.
|
|
1011
|
+
}
|
|
1012
|
+
}
|
|
972
1013
|
|
|
973
1014
|
function hasAnimatedWidgetJobs(jobs: AsyncJobState[]): boolean {
|
|
974
1015
|
// Animate while any job — or any of its nested steps — is still running so the
|
|
@@ -979,12 +1020,12 @@ function hasAnimatedWidgetJobs(jobs: AsyncJobState[]): boolean {
|
|
|
979
1020
|
function refreshAnimatedWidget(): void {
|
|
980
1021
|
if (!latestWidgetCtx?.hasUI) return;
|
|
981
1022
|
try {
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
// requestRender that the running interactive host actually provides.
|
|
985
|
-
(latestWidgetCtx as RenderRequestingContext).ui.requestRender?.();
|
|
1023
|
+
latestWidgetFrameNow = Date.now();
|
|
1024
|
+
requestWidgetRender(latestWidgetCtx);
|
|
986
1025
|
} catch {
|
|
987
|
-
//
|
|
1026
|
+
// A stale render context means the cosmetic ticker can no longer update the
|
|
1027
|
+
// mounted widget safely; tear it down best-effort and let the next status
|
|
1028
|
+
// update remount on the active host context.
|
|
988
1029
|
stopWidgetAnimation();
|
|
989
1030
|
}
|
|
990
1031
|
}
|
|
@@ -1009,23 +1050,24 @@ function stopWidgetTicker(): void {
|
|
|
1009
1050
|
}
|
|
1010
1051
|
}
|
|
1011
1052
|
|
|
1012
|
-
// Full teardown: stop the ticker
|
|
1053
|
+
// Full teardown: stop the ticker, clear the mounted widget if possible, and
|
|
1054
|
+
// forget the driving context/jobs entirely.
|
|
1013
1055
|
export function stopWidgetAnimation(): void {
|
|
1014
1056
|
stopWidgetTicker();
|
|
1015
|
-
|
|
1016
|
-
|
|
1057
|
+
if (widgetMounted) unmountWidgetBestEffort(mountedWidgetCtx);
|
|
1058
|
+
clearLatestWidgetState();
|
|
1017
1059
|
}
|
|
1018
1060
|
|
|
1019
|
-
export function buildWidgetLines(jobs: AsyncJobState[], theme: Theme, width = getTermWidth(), expanded = false): string[] {
|
|
1061
|
+
export function buildWidgetLines(jobs: AsyncJobState[], theme: Theme, width = getTermWidth(), expanded = false, now: number = Date.now()): string[] {
|
|
1020
1062
|
if (jobs.length === 0) return [];
|
|
1021
|
-
if (jobs.length === 1) return buildSingleWidgetLines(jobs[0]!, theme, width, expanded);
|
|
1063
|
+
if (jobs.length === 1) return buildSingleWidgetLines(jobs[0]!, theme, width, expanded, now);
|
|
1022
1064
|
const running = jobs.filter((job) => job.status === "running");
|
|
1023
1065
|
const queued = jobs.filter((job) => job.status === "queued");
|
|
1024
1066
|
const finished = jobs.filter((job) => job.status !== "running" && job.status !== "queued");
|
|
1025
1067
|
|
|
1026
1068
|
const lines: string[] = [];
|
|
1027
1069
|
const hasActive = running.length > 0 || queued.length > 0;
|
|
1028
|
-
const headerGlyph = running.length > 0 ? runningGlyph(widgetJobsRunningSeed(running)) : hasActive ? "●" : "○";
|
|
1070
|
+
const headerGlyph = running.length > 0 ? runningGlyph(widgetJobsRunningSeed(running), now) : hasActive ? "●" : "○";
|
|
1029
1071
|
lines.push(truncLine(`${theme.fg(hasActive ? "accent" : "dim", headerGlyph)} ${theme.fg(hasActive ? "accent" : "dim", "Async agents")} ${theme.fg("dim", "· background")}`, width));
|
|
1030
1072
|
|
|
1031
1073
|
const items: string[][] = [];
|
|
@@ -1038,9 +1080,9 @@ export function buildWidgetLines(jobs: AsyncJobState[], theme: Theme, width = ge
|
|
|
1038
1080
|
if (slots <= 0) { hiddenRunning++; continue; }
|
|
1039
1081
|
const stats = widgetStats(job, theme);
|
|
1040
1082
|
items.push([
|
|
1041
|
-
`${widgetStatusGlyph(job, theme)} ${themeBold(theme, widgetJobName(job))}${stats ? ` ${theme.fg("dim", "·")} ${stats}` : ""}`,
|
|
1083
|
+
`${widgetStatusGlyph(job, theme, now)} ${themeBold(theme, widgetJobName(job))}${stats ? ` ${theme.fg("dim", "·")} ${stats}` : ""}`,
|
|
1042
1084
|
` ${theme.fg("dim", `⎿ ${widgetActivity(job)}`)}`,
|
|
1043
|
-
...widgetParallelAgentDetails(job, theme, expanded, width),
|
|
1085
|
+
...widgetParallelAgentDetails(job, theme, expanded, width, now),
|
|
1044
1086
|
]);
|
|
1045
1087
|
slots--;
|
|
1046
1088
|
}
|
|
@@ -1055,9 +1097,9 @@ export function buildWidgetLines(jobs: AsyncJobState[], theme: Theme, width = ge
|
|
|
1055
1097
|
if (slots <= 0) { hiddenFinished++; continue; }
|
|
1056
1098
|
const stats = widgetStats(job, theme);
|
|
1057
1099
|
items.push([
|
|
1058
|
-
`${widgetStatusGlyph(job, theme)} ${themeBold(theme, widgetJobName(job))}${stats ? ` ${theme.fg("dim", "·")} ${stats}` : ""}`,
|
|
1100
|
+
`${widgetStatusGlyph(job, theme, now)} ${themeBold(theme, widgetJobName(job))}${stats ? ` ${theme.fg("dim", "·")} ${stats}` : ""}`,
|
|
1059
1101
|
` ${theme.fg("dim", `⎿ ${widgetActivity(job)}`)}`,
|
|
1060
|
-
...widgetParallelAgentDetails(job, theme, expanded, width),
|
|
1102
|
+
...widgetParallelAgentDetails(job, theme, expanded, width, now),
|
|
1061
1103
|
]);
|
|
1062
1104
|
slots--;
|
|
1063
1105
|
}
|
|
@@ -1091,8 +1133,12 @@ export function buildWidgetLines(jobs: AsyncJobState[], theme: Theme, width = ge
|
|
|
1091
1133
|
*/
|
|
1092
1134
|
export function renderWidget(ctx: ExtensionContext, jobs: AsyncJobState[]): void {
|
|
1093
1135
|
if (jobs.length === 0) {
|
|
1136
|
+
if (widgetMounted && mountedWidgetCtx !== ctx) {
|
|
1137
|
+
// Empty updates from stale contexts must not clear the active context's
|
|
1138
|
+
// widget. The mounted context owns the eventual teardown.
|
|
1139
|
+
return;
|
|
1140
|
+
}
|
|
1094
1141
|
stopWidgetAnimation();
|
|
1095
|
-
if (ctx.hasUI) ctx.ui.setWidget(WIDGET_KEY, undefined);
|
|
1096
1142
|
return;
|
|
1097
1143
|
}
|
|
1098
1144
|
if (!ctx.hasUI) {
|
|
@@ -1101,38 +1147,54 @@ export function renderWidget(ctx: ExtensionContext, jobs: AsyncJobState[]): void
|
|
|
1101
1147
|
}
|
|
1102
1148
|
latestWidgetCtx = ctx;
|
|
1103
1149
|
latestWidgetJobs = [...jobs];
|
|
1104
|
-
|
|
1105
|
-
|
|
1106
|
-
|
|
1107
|
-
|
|
1108
|
-
|
|
1109
|
-
|
|
1110
|
-
|
|
1111
|
-
|
|
1112
|
-
|
|
1150
|
+
latestWidgetFrameNow = Date.now();
|
|
1151
|
+
if (widgetMounted && mountedWidgetCtx !== ctx) {
|
|
1152
|
+
// Context rebinding can leave the previous host UI alive briefly; clear the
|
|
1153
|
+
// old mount before installing the singleton widget on the new context.
|
|
1154
|
+
unmountWidgetBestEffort(mountedWidgetCtx);
|
|
1155
|
+
mountedWidgetCtx = undefined;
|
|
1156
|
+
widgetMounted = false;
|
|
1157
|
+
}
|
|
1158
|
+
if (!widgetMounted) {
|
|
1159
|
+
// belowEditor: the widget animates a running glyph / elapsed labels on a
|
|
1160
|
+
// timer. pi-tui full-clears the screen+scrollback whenever a changed line
|
|
1161
|
+
// sits above the viewport fold, so an aboveEditor widget flickers once the
|
|
1162
|
+
// bottom region grows tall and pushes it above the fold. Rendering below the
|
|
1163
|
+
// editor keeps the live line within the bottom viewport (flicker-free), and
|
|
1164
|
+
// matches the workflow companion widget's placement (#1109).
|
|
1165
|
+
ctx.ui.setWidget(WIDGET_KEY, buildWidgetComponent(getLatestWidgetJobs, getLatestWidgetExpanded, getLatestWidgetFrameNow), {
|
|
1166
|
+
placement: "belowEditor",
|
|
1167
|
+
});
|
|
1168
|
+
mountedWidgetCtx = ctx;
|
|
1169
|
+
widgetMounted = true;
|
|
1170
|
+
} else {
|
|
1171
|
+
// The mounted widget reads latestWidgetJobs via getLatestWidgetJobs(), so a
|
|
1172
|
+
// visible->visible update only needs to ask the host to render in place.
|
|
1173
|
+
requestWidgetRender(ctx);
|
|
1174
|
+
}
|
|
1113
1175
|
// Keep the just-rendered ctx/jobs as the last-rendered state; only the ticker
|
|
1114
1176
|
// is conditional on whether anything is still animating.
|
|
1115
1177
|
if (hasAnimatedWidgetJobs(jobs)) ensureWidgetAnimation();
|
|
1116
1178
|
else stopWidgetTicker();
|
|
1117
1179
|
}
|
|
1118
1180
|
|
|
1119
|
-
function renderSingleCompact(d: Details, r: Details["results"][number], theme: Theme): Component {
|
|
1181
|
+
function renderSingleCompact(d: Details, r: Details["results"][number], theme: Theme, now?: number, spinnerNow?: number): Component {
|
|
1120
1182
|
const output = r.truncation?.text || getSingleResultOutput(r);
|
|
1121
1183
|
const progress = r.progress || r.progressSummary;
|
|
1122
1184
|
const isRunning = r.progress?.status === "running";
|
|
1123
1185
|
const contextBadge = d.context === "fork" ? theme.fg("warning", " [fork]") : "";
|
|
1124
1186
|
const stats = statJoin(theme, [
|
|
1125
1187
|
r.usage?.turns ? `⟳ ${r.usage.turns}` : "",
|
|
1126
|
-
formatProgressStats(theme, progress),
|
|
1188
|
+
formatProgressStats(theme, progress, true, now),
|
|
1127
1189
|
]);
|
|
1128
1190
|
const c = new Container();
|
|
1129
1191
|
const width = getTermWidth() - 4;
|
|
1130
|
-
const modelDisplay = modelThinkingBadge(theme, r.model);
|
|
1131
|
-
c.addChild(new Text(truncLine(`${resultGlyph(r, output, theme, isRunning)} ${theme.fg("toolTitle", theme.bold(r.agent))}${modelDisplay}${contextBadge}${stats ? ` ${theme.fg("dim", "·")} ${stats}` : ""}`, width), 0, 0));
|
|
1192
|
+
const modelDisplay = modelThinkingBadge(theme, r.model, undefined, r.fastMode);
|
|
1193
|
+
c.addChild(new Text(truncLine(`${resultGlyph(r, output, theme, isRunning, progressRunningSeed(r.progress ?? r.progressSummary), spinnerNow ?? now)} ${theme.fg("toolTitle", theme.bold(r.agent))}${modelDisplay}${contextBadge}${stats ? ` ${theme.fg("dim", "·")} ${stats}` : ""}`, width), 0, 0));
|
|
1132
1194
|
|
|
1133
1195
|
if (isRunning && r.progress) {
|
|
1134
|
-
const progressSnapshotNow = snapshotNowForProgress(r.progress);
|
|
1135
|
-
const activity = compactCurrentActivity(r.progress);
|
|
1196
|
+
const progressSnapshotNow = snapshotNowForProgress(r.progress, now);
|
|
1197
|
+
const activity = compactCurrentActivity(r.progress, now);
|
|
1136
1198
|
c.addChild(new Text(truncLine(theme.fg("dim", ` ⎿ ${activity}`), width), 0, 0));
|
|
1137
1199
|
const liveStatus = buildLiveStatusLine(r.progress, progressSnapshotNow);
|
|
1138
1200
|
if (liveStatus && liveStatus !== activity) c.addChild(new Text(truncLine(theme.fg("dim", ` ${liveStatus}`), width), 0, 0));
|
|
@@ -1152,7 +1214,7 @@ function renderSingleCompact(d: Details, r: Details["results"][number], theme: T
|
|
|
1152
1214
|
return c;
|
|
1153
1215
|
}
|
|
1154
1216
|
|
|
1155
|
-
function renderMultiCompact(d: Details, theme: Theme): Component {
|
|
1217
|
+
function renderMultiCompact(d: Details, theme: Theme, now?: number, spinnerNow?: number): Component {
|
|
1156
1218
|
const hasRunning = d.progress?.some((p) => p.status === "running")
|
|
1157
1219
|
|| d.results.some((r) => r.progress?.status === "running");
|
|
1158
1220
|
const failed = d.results.some((r) => r.exitCode !== 0 && r.progress?.status !== "running");
|
|
@@ -1173,9 +1235,9 @@ function renderMultiCompact(d: Details, theme: Theme): Component {
|
|
|
1173
1235
|
}
|
|
1174
1236
|
const multiLabel = buildMultiProgressLabel(d, hasRunning);
|
|
1175
1237
|
const itemTitle = multiLabel.itemTitle;
|
|
1176
|
-
const stats = statJoin(theme, [multiLabel.headerLabel, formatProgressStats(theme, totalSummary)]);
|
|
1238
|
+
const stats = statJoin(theme, [multiLabel.headerLabel, formatProgressStats(theme, totalSummary, true, now)]);
|
|
1177
1239
|
const glyph = hasRunning
|
|
1178
|
-
? theme.fg("accent", runningGlyph(runningSeed(progressRunningSeed(totalSummary), d.currentStepIndex)))
|
|
1240
|
+
? theme.fg("accent", runningGlyph(runningSeed(progressRunningSeed(totalSummary), d.currentStepIndex), spinnerNow ?? now))
|
|
1179
1241
|
: failed
|
|
1180
1242
|
? theme.fg("error", "✗")
|
|
1181
1243
|
: paused
|
|
@@ -1204,14 +1266,14 @@ function renderMultiCompact(d: Details, theme: Theme): Component {
|
|
|
1204
1266
|
const rRunning = rProg && "status" in rProg && rProg.status === "running";
|
|
1205
1267
|
const rPending = rProg && "status" in rProg && rProg.status === "pending";
|
|
1206
1268
|
const stepNumber = r.progress?.index !== undefined ? r.progress.index + 1 : progressFromArray?.index !== undefined ? progressFromArray.index + 1 : i + 1;
|
|
1207
|
-
const stepStats = formatProgressStats(theme, rProg);
|
|
1208
|
-
const glyph = rPending ? theme.fg("dim", "◦") : resultGlyph(r, output, theme, rRunning, progressRunningSeed(rProg));
|
|
1269
|
+
const stepStats = formatProgressStats(theme, rProg, true, now);
|
|
1270
|
+
const glyph = rPending ? theme.fg("dim", "◦") : resultGlyph(r, output, theme, rRunning, progressRunningSeed(rProg), spinnerNow ?? now);
|
|
1209
1271
|
const pendingLabel = rPending ? ` ${theme.fg("dim", "· pending")}` : "";
|
|
1210
1272
|
const stepLabel = resultRowLabel(d, multiLabel, i, stepNumber);
|
|
1211
1273
|
const line = `${glyph} ${stepLabel}: ${themeBold(theme, agentName)}${stepStats ? ` ${theme.fg("dim", "·")} ${stepStats}` : ""}${pendingLabel}`;
|
|
1212
1274
|
c.addChild(new Text(truncLine(` ${line}`, width), 0, 0));
|
|
1213
1275
|
if (rRunning && rProg && "status" in rProg) {
|
|
1214
|
-
const activity = compactCurrentActivity(rProg);
|
|
1276
|
+
const activity = compactCurrentActivity(rProg, now);
|
|
1215
1277
|
c.addChild(new Text(truncLine(theme.fg("dim", ` ⎿ ${activity}`), width), 0, 0));
|
|
1216
1278
|
c.addChild(new Text(truncLine(theme.fg("accent", " Press ctrl+o for live detail"), width), 0, 0));
|
|
1217
1279
|
} else if (!rPending && (r.exitCode !== 0 || r.interrupted || r.detached || hasEmptyTextOutputWithoutOutputTarget(r.task, output))) {
|
|
@@ -1225,12 +1287,96 @@ function renderMultiCompact(d: Details, theme: Theme): Component {
|
|
|
1225
1287
|
return c;
|
|
1226
1288
|
}
|
|
1227
1289
|
|
|
1290
|
+
function progressRenderKey(progress: Partial<AgentProgress> | undefined): string {
|
|
1291
|
+
if (!progress) return "";
|
|
1292
|
+
return [
|
|
1293
|
+
progress.index,
|
|
1294
|
+
progress.agent,
|
|
1295
|
+
progress.status,
|
|
1296
|
+
progress.durationMs,
|
|
1297
|
+
progress.toolCount,
|
|
1298
|
+
progress.tokens,
|
|
1299
|
+
progress.turnCount ?? "",
|
|
1300
|
+
progress.lastActivityAt ?? "",
|
|
1301
|
+
progress.currentTool ?? "",
|
|
1302
|
+
progress.currentToolStartedAt ?? "",
|
|
1303
|
+
progress.currentPath ?? "",
|
|
1304
|
+
].join(":");
|
|
1305
|
+
}
|
|
1306
|
+
|
|
1307
|
+
function isRunningSubagentResult(result: AgentToolResult<Details>): boolean {
|
|
1308
|
+
return result.details?.progress?.some((entry) => entry.status === "running")
|
|
1309
|
+
|| result.details?.results.some((entry) => entry.progress?.status === "running")
|
|
1310
|
+
|| false;
|
|
1311
|
+
}
|
|
1312
|
+
|
|
1313
|
+
function subagentResultRenderKey(
|
|
1314
|
+
result: AgentToolResult<Details>,
|
|
1315
|
+
options: { expanded: boolean; isPartial: boolean },
|
|
1316
|
+
): string {
|
|
1317
|
+
const details = result.details;
|
|
1318
|
+
if (!details) return `${options.isPartial ? "partial" : "final"}:${result.content.length}`;
|
|
1319
|
+
const progressKeys = [
|
|
1320
|
+
...(details.progress ?? []).map(progressRenderKey),
|
|
1321
|
+
...details.results.map((entry) => [
|
|
1322
|
+
entry.agent,
|
|
1323
|
+
entry.exitCode,
|
|
1324
|
+
entry.interrupted === true ? "interrupted" : "",
|
|
1325
|
+
entry.detached === true ? "detached" : "",
|
|
1326
|
+
progressRenderKey(entry.progress),
|
|
1327
|
+
progressRenderKey(entry.progressSummary),
|
|
1328
|
+
entry.finalOutput?.length ?? "",
|
|
1329
|
+
entry.error?.length ?? "",
|
|
1330
|
+
].join(":")),
|
|
1331
|
+
];
|
|
1332
|
+
return [
|
|
1333
|
+
options.isPartial ? "partial" : "final",
|
|
1334
|
+
options.expanded ? "expanded" : "compact",
|
|
1335
|
+
details.mode,
|
|
1336
|
+
details.currentStepIndex ?? "",
|
|
1337
|
+
details.totalSteps ?? "",
|
|
1338
|
+
progressRenderKey(details.progressSummary),
|
|
1339
|
+
progressKeys.join("|"),
|
|
1340
|
+
].join("|");
|
|
1341
|
+
}
|
|
1342
|
+
|
|
1343
|
+
export function renderLiveSubagentResult(
|
|
1344
|
+
result: AgentToolResult<Details>,
|
|
1345
|
+
options: { expanded: boolean; isPartial: boolean },
|
|
1346
|
+
theme: Theme,
|
|
1347
|
+
context: ResultAnimationContext,
|
|
1348
|
+
): Component {
|
|
1349
|
+
const nextKey = subagentResultRenderKey(result, options);
|
|
1350
|
+
if (context.state.subagentResultSnapshotKey !== nextKey) {
|
|
1351
|
+
const frameNow = Date.now();
|
|
1352
|
+
context.state.subagentResultSnapshotKey = nextKey;
|
|
1353
|
+
context.state.subagentResultSnapshotNow = frameNow;
|
|
1354
|
+
context.state.subagentResultSpinnerFrameNow = frameNow;
|
|
1355
|
+
}
|
|
1356
|
+
context.state.subagentResultSnapshotNow ??= Date.now();
|
|
1357
|
+
context.state.subagentResultSpinnerFrameNow ??= context.state.subagentResultSnapshotNow;
|
|
1358
|
+
// Foreground subagent results render inside chat scrollback. Keep semantic
|
|
1359
|
+
// content time stable between tool/progress updates, but let the spinner tick
|
|
1360
|
+
// independently. That limits timer-driven diffs to spinner glyph cells instead
|
|
1361
|
+
// of updating elapsed/tool/activity text and causing broad chatbox churn.
|
|
1362
|
+
if (options.isPartial && isRunningSubagentResult(result)) {
|
|
1363
|
+
ensureResultAnimation(context);
|
|
1364
|
+
} else {
|
|
1365
|
+
clearResultAnimationTimer(context);
|
|
1366
|
+
}
|
|
1367
|
+
return renderSubagentResult(result, {
|
|
1368
|
+
...options,
|
|
1369
|
+
now: context.state.subagentResultSnapshotNow,
|
|
1370
|
+
spinnerNow: context.state.subagentResultSpinnerFrameNow,
|
|
1371
|
+
}, theme);
|
|
1372
|
+
}
|
|
1373
|
+
|
|
1228
1374
|
/**
|
|
1229
1375
|
* Render a subagent result
|
|
1230
1376
|
*/
|
|
1231
1377
|
export function renderSubagentResult(
|
|
1232
1378
|
result: AgentToolResult<Details>,
|
|
1233
|
-
options: { expanded: boolean },
|
|
1379
|
+
options: { expanded: boolean; now?: number; spinnerNow?: number },
|
|
1234
1380
|
theme: Theme,
|
|
1235
1381
|
): Component {
|
|
1236
1382
|
const d = result.details;
|
|
@@ -1246,7 +1392,7 @@ export function renderSubagentResult(
|
|
|
1246
1392
|
|
|
1247
1393
|
if (d.mode === "single" && d.results.length === 1) {
|
|
1248
1394
|
const r = d.results[0];
|
|
1249
|
-
if (!expanded) return renderSingleCompact(d, r, theme);
|
|
1395
|
+
if (!expanded) return renderSingleCompact(d, r, theme, options.now, options.spinnerNow);
|
|
1250
1396
|
const isRunning = r.progress?.status === "running";
|
|
1251
1397
|
const icon = isRunning
|
|
1252
1398
|
? theme.fg("warning", "running")
|
|
@@ -1259,7 +1405,7 @@ export function renderSubagentResult(
|
|
|
1259
1405
|
const output = r.truncation?.text || getSingleResultOutput(r);
|
|
1260
1406
|
|
|
1261
1407
|
const progressInfo = isRunning && r.progress
|
|
1262
|
-
? ` | ${r.progress.toolCount} tools, ${formatTokens(r.progress.tokens)} tok, ${formatDuration(r.progress.
|
|
1408
|
+
? ` | ${r.progress.toolCount} tools, ${formatTokens(r.progress.tokens)} tok, ${formatDuration(displayProgressDurationMs(r.progress, options.now))}`
|
|
1263
1409
|
: r.progressSummary
|
|
1264
1410
|
? ` | ${r.progressSummary.toolCount} tools, ${formatTokens(r.progressSummary.tokens)} tok, ${formatDuration(r.progressSummary.durationMs)}`
|
|
1265
1411
|
: "";
|
|
@@ -1280,7 +1426,7 @@ export function renderSubagentResult(
|
|
|
1280
1426
|
c.addChild(new Spacer(1));
|
|
1281
1427
|
|
|
1282
1428
|
if (isRunning && r.progress) {
|
|
1283
|
-
const progressSnapshotNow = snapshotNowForProgress(r.progress);
|
|
1429
|
+
const progressSnapshotNow = snapshotNowForProgress(r.progress, options.now);
|
|
1284
1430
|
const toolLine = formatCurrentToolLine(r.progress, w, expanded, progressSnapshotNow);
|
|
1285
1431
|
if (toolLine) {
|
|
1286
1432
|
c.addChild(new Text(fit(theme.fg("warning", `> ${toolLine}`)), 0, 0));
|
|
@@ -1340,7 +1486,7 @@ export function renderSubagentResult(
|
|
|
1340
1486
|
return c;
|
|
1341
1487
|
}
|
|
1342
1488
|
|
|
1343
|
-
if (!expanded) return renderMultiCompact(d, theme);
|
|
1489
|
+
if (!expanded) return renderMultiCompact(d, theme, options.now, options.spinnerNow);
|
|
1344
1490
|
|
|
1345
1491
|
const hasRunning = d.progress?.some((p) => p.status === "running")
|
|
1346
1492
|
|| d.results.some((r) => r.progress?.status === "running");
|
|
@@ -1458,8 +1604,8 @@ export function renderSubagentResult(
|
|
|
1458
1604
|
: hasEmptyTextOutputWithoutOutputTarget(r.task, resultOutput)
|
|
1459
1605
|
? theme.fg("warning", "warning")
|
|
1460
1606
|
: theme.fg("success", "done");
|
|
1461
|
-
const stats = rProg ? ` | ${rProg.toolCount} tools, ${formatDuration(rProg.
|
|
1462
|
-
const modelDisplay = modelThinkingBadge(theme, r.model);
|
|
1607
|
+
const stats = rProg ? ` | ${rProg.toolCount} tools, ${formatDuration(displayProgressDurationMs(rProg, options.now))}` : "";
|
|
1608
|
+
const modelDisplay = modelThinkingBadge(theme, r.model, undefined, r.fastMode);
|
|
1463
1609
|
const stepLabel = resultRowLabel(d, multiLabel, i, stepNumber);
|
|
1464
1610
|
const stepHeader = rRunning
|
|
1465
1611
|
? `${statusIcon} ${stepLabel}: ${theme.bold(theme.fg("warning", r.agent))}${modelDisplay}${stats}`
|
|
@@ -1492,7 +1638,7 @@ export function renderSubagentResult(
|
|
|
1492
1638
|
if (rProg.skills?.length) {
|
|
1493
1639
|
c.addChild(new Text(fit(theme.fg("accent", ` skills: ${rProg.skills.join(", ")}`)), 0, 0));
|
|
1494
1640
|
}
|
|
1495
|
-
const progressSnapshotNow = snapshotNowForProgress(rProg);
|
|
1641
|
+
const progressSnapshotNow = snapshotNowForProgress(rProg, options.now);
|
|
1496
1642
|
const toolLine = formatCurrentToolLine(rProg, w, expanded, progressSnapshotNow);
|
|
1497
1643
|
if (toolLine) {
|
|
1498
1644
|
c.addChild(new Text(fit(theme.fg("warning", ` > ${toolLine}`)), 0, 0));
|