@bastani/atomic 0.9.3-alpha.1 → 0.9.3-alpha.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +15 -0
- package/dist/builtin/cursor/CHANGELOG.md +21 -0
- package/dist/builtin/cursor/README.md +2 -1
- package/dist/builtin/cursor/package.json +2 -2
- package/dist/builtin/cursor/src/cursor-models-raw.json +2 -9
- package/dist/builtin/cursor/src/model-mapper.ts +14 -3
- package/dist/builtin/cursor/src/proto/protobuf-codec-base64.ts +22 -0
- package/dist/builtin/cursor/src/proto/protobuf-codec-request.ts +53 -13
- package/dist/builtin/cursor/src/proto/protobuf-codec-wire.ts +24 -7
- package/dist/builtin/cursor/src/proto/protobuf-codec.ts +3 -2
- package/dist/builtin/cursor/src/stream.ts +5 -11
- package/dist/builtin/cursor/src/transport-types.ts +3 -0
- package/dist/builtin/cursor/src/transport.ts +1 -0
- package/dist/builtin/intercom/CHANGELOG.md +6 -0
- package/dist/builtin/intercom/package.json +1 -1
- package/dist/builtin/mcp/CHANGELOG.md +6 -0
- package/dist/builtin/mcp/package.json +1 -1
- package/dist/builtin/subagents/CHANGELOG.md +15 -0
- package/dist/builtin/subagents/package.json +1 -1
- package/dist/builtin/subagents/src/extension/fanout-child.ts +1 -0
- package/dist/builtin/subagents/src/extension/index.ts +6 -3
- package/dist/builtin/subagents/src/extension/schemas.ts +0 -5
- package/dist/builtin/subagents/src/runs/background/async-job-tracker.ts +1 -4
- package/dist/builtin/subagents/src/runs/foreground/subagent-executor-single.ts +15 -1
- package/dist/builtin/subagents/src/runs/foreground/subagent-executor.ts +35 -1
- package/dist/builtin/subagents/src/runs/shared/subagent-prompt-runtime.ts +4 -2
- package/dist/builtin/subagents/src/shared/types-async.ts +1 -0
- package/dist/builtin/subagents/src/slash/prompt-template-bridge.ts +27 -5
- package/dist/builtin/subagents/src/tui/render-layout.ts +27 -4
- package/dist/builtin/subagents/src/tui/render-result-animation.ts +22 -31
- package/dist/builtin/subagents/src/tui/render-result-compact.ts +6 -6
- package/dist/builtin/subagents/src/tui/render-result.ts +20 -19
- package/dist/builtin/subagents/src/tui/render-status-progress.ts +3 -3
- package/dist/builtin/subagents/src/tui/render-widget.ts +46 -7
- package/dist/builtin/subagents/src/tui/render.ts +2 -2
- package/dist/builtin/web-access/CHANGELOG.md +6 -0
- package/dist/builtin/web-access/package.json +1 -1
- package/dist/builtin/workflows/CHANGELOG.md +49 -0
- package/dist/builtin/workflows/README.md +1 -1
- package/dist/builtin/workflows/package.json +1 -1
- package/dist/builtin/workflows/src/authoring.d.ts +1 -1
- package/dist/builtin/workflows/src/durable/backend.ts +343 -0
- package/dist/builtin/workflows/src/durable/child-primitive.ts +79 -0
- package/dist/builtin/workflows/src/durable/dbos-backend.ts +421 -0
- package/dist/builtin/workflows/src/durable/dbos-envelope.ts +171 -0
- package/dist/builtin/workflows/src/durable/factory.ts +96 -0
- package/dist/builtin/workflows/src/durable/file-backend.ts +433 -0
- package/dist/builtin/workflows/src/durable/index.ts +73 -0
- package/dist/builtin/workflows/src/durable/resume-catalog.ts +217 -0
- package/dist/builtin/workflows/src/durable/resume-runtime.ts +299 -0
- package/dist/builtin/workflows/src/durable/scoped-backend.ts +171 -0
- package/dist/builtin/workflows/src/durable/stage-primitive.ts +284 -0
- package/dist/builtin/workflows/src/durable/tool-primitive.ts +180 -0
- package/dist/builtin/workflows/src/durable/types.ts +168 -0
- package/dist/builtin/workflows/src/durable/ui-primitive.ts +96 -0
- package/dist/builtin/workflows/src/engine/options.ts +3 -0
- package/dist/builtin/workflows/src/engine/primitives/parallel.ts +2 -2
- package/dist/builtin/workflows/src/engine/primitives/task.ts +4 -4
- package/dist/builtin/workflows/src/engine/primitives/ui.ts +22 -8
- package/dist/builtin/workflows/src/engine/primitives/workflow.ts +8 -0
- package/dist/builtin/workflows/src/engine/run-durable-finalize.ts +69 -0
- package/dist/builtin/workflows/src/engine/run-durable-stage-session.ts +31 -0
- package/dist/builtin/workflows/src/engine/run.ts +148 -6
- package/dist/builtin/workflows/src/engine/runtime.ts +8 -2
- package/dist/builtin/workflows/src/extension/extension-factory.ts +6 -12
- package/dist/builtin/workflows/src/extension/extension-lifecycle.ts +5 -1
- package/dist/builtin/workflows/src/extension/extension-runtime-state.ts +3 -0
- package/dist/builtin/workflows/src/extension/runtime.ts +48 -9
- package/dist/builtin/workflows/src/extension/workflow-run-control-command.ts +143 -4
- package/dist/builtin/workflows/src/runs/background/quit.ts +61 -0
- package/dist/builtin/workflows/src/runs/background/status.ts +1 -0
- package/dist/builtin/workflows/src/runs/foreground/executor-direct-helpers.ts +5 -5
- package/dist/builtin/workflows/src/runs/foreground/executor-stage-call.ts +74 -33
- package/dist/builtin/workflows/src/runs/foreground/executor-stage-context.ts +20 -1
- package/dist/builtin/workflows/src/runs/foreground/executor-stage-factory.ts +8 -7
- package/dist/builtin/workflows/src/runs/foreground/executor-stage-replay.ts +1 -0
- package/dist/builtin/workflows/src/runs/foreground/executor-stage-types.ts +1 -1
- package/dist/builtin/workflows/src/runs/foreground/executor-types.ts +19 -2
- package/dist/builtin/workflows/src/runs/foreground/stage-runner-context.ts +4 -0
- package/dist/builtin/workflows/src/runs/foreground/stage-runner-controller.ts +10 -10
- package/dist/builtin/workflows/src/runs/foreground/stage-runner-options.ts +5 -1
- package/dist/builtin/workflows/src/runs/foreground/stage-runner-send-user-message.ts +25 -0
- package/dist/builtin/workflows/src/runs/foreground/stage-runner-types.ts +3 -0
- package/dist/builtin/workflows/src/shared/authoring-contract-stage.d.ts +16 -0
- package/dist/builtin/workflows/src/shared/authoring-contract-stage.ts +20 -0
- package/dist/builtin/workflows/src/shared/authoring-contract-ui.d.ts +23 -1
- package/dist/builtin/workflows/src/shared/authoring-contract-ui.ts +30 -1
- package/dist/builtin/workflows/src/shared/store-public-types.ts +6 -2
- package/dist/builtin/workflows/src/shared/store-run-methods.ts +12 -6
- package/dist/builtin/workflows/src/shared/types.ts +55 -0
- package/dist/builtin/workflows/src/tui/graph-view-constants.ts +1 -1
- package/dist/builtin/workflows/src/tui/graph-view-graph-render.ts +41 -0
- package/dist/builtin/workflows/src/tui/graph-view-input.ts +82 -24
- package/dist/builtin/workflows/src/tui/graph-view-render.ts +7 -0
- package/dist/builtin/workflows/src/tui/graph-view-state.ts +22 -2
- package/dist/builtin/workflows/src/tui/graph-view-types.ts +4 -5
- package/dist/builtin/workflows/src/tui/overlay-adapter.ts +9 -11
- package/dist/builtin/workflows/src/tui/stage-chat-view-footer-status.ts +9 -3
- package/dist/builtin/workflows/src/tui/stage-chat-view-input.ts +11 -2
- package/dist/builtin/workflows/src/tui/stage-chat-view-live-events.ts +35 -0
- package/dist/builtin/workflows/src/tui/stage-chat-view-state.ts +51 -17
- package/dist/builtin/workflows/src/tui/stage-chat-view-status.ts +36 -0
- package/dist/builtin/workflows/src/tui/stage-chat-view-types.ts +5 -1
- package/dist/builtin/workflows/src/tui/stage-chat-view.ts +3 -1
- package/dist/builtin/workflows/src/tui/status-list.ts +14 -2
- package/dist/builtin/workflows/src/tui/widget.ts +23 -8
- package/dist/builtin/workflows/src/tui/workflow-attach-pane-types.ts +5 -4
- package/dist/builtin/workflows/src/tui/workflow-attach-pane.ts +8 -8
- package/dist/builtin/workflows/src/tui/workflow-resume-selector.ts +151 -0
- package/dist/core/extensions/loader-virtual-modules.d.ts.map +1 -1
- package/dist/core/extensions/loader-virtual-modules.js +47 -30
- package/dist/core/extensions/loader-virtual-modules.js.map +1 -1
- package/dist/core/messages.d.ts +1 -0
- package/dist/core/messages.d.ts.map +1 -1
- package/dist/core/messages.js +46 -1
- package/dist/core/messages.js.map +1 -1
- package/dist/core/sdk.d.ts.map +1 -1
- package/dist/core/sdk.js +12 -0
- package/dist/core/sdk.js.map +1 -1
- package/dist/core/session-manager-core.d.ts +15 -7
- package/dist/core/session-manager-core.d.ts.map +1 -1
- package/dist/core/session-manager-core.js +20 -9
- package/dist/core/session-manager-core.js.map +1 -1
- package/dist/core/session-manager-entries.d.ts +2 -2
- package/dist/core/session-manager-entries.d.ts.map +1 -1
- package/dist/core/session-manager-entries.js +9 -3
- package/dist/core/session-manager-entries.js.map +1 -1
- package/dist/core/session-manager-history.d.ts.map +1 -1
- package/dist/core/session-manager-history.js +2 -1
- package/dist/core/session-manager-history.js.map +1 -1
- package/dist/core/session-manager-list.d.ts +3 -3
- package/dist/core/session-manager-list.d.ts.map +1 -1
- package/dist/core/session-manager-list.js +27 -8
- package/dist/core/session-manager-list.js.map +1 -1
- package/dist/core/session-manager-storage.d.ts +3 -1
- package/dist/core/session-manager-storage.d.ts.map +1 -1
- package/dist/core/session-manager-storage.js +55 -12
- package/dist/core/session-manager-storage.js.map +1 -1
- package/dist/core/session-manager-tool-dependencies.d.ts +10 -0
- package/dist/core/session-manager-tool-dependencies.d.ts.map +1 -0
- package/dist/core/session-manager-tool-dependencies.js +133 -0
- package/dist/core/session-manager-tool-dependencies.js.map +1 -0
- package/dist/core/session-manager-types.d.ts +22 -0
- package/dist/core/session-manager-types.d.ts.map +1 -1
- package/dist/core/session-manager-types.js.map +1 -1
- package/dist/core/session-manager.d.ts +2 -2
- package/dist/core/session-manager.d.ts.map +1 -1
- package/dist/core/session-manager.js +1 -1
- package/dist/core/session-manager.js.map +1 -1
- package/dist/modes/interactive/components/chat-session-host-runtime.d.ts +1 -0
- package/dist/modes/interactive/components/chat-session-host-runtime.d.ts.map +1 -1
- package/dist/modes/interactive/components/chat-session-host-runtime.js +12 -0
- package/dist/modes/interactive/components/chat-session-host-runtime.js.map +1 -1
- package/dist/modes/interactive/components/chat-session-host-terminal-cleanup.d.ts +4 -0
- package/dist/modes/interactive/components/chat-session-host-terminal-cleanup.d.ts.map +1 -0
- package/dist/modes/interactive/components/chat-session-host-terminal-cleanup.js +131 -0
- package/dist/modes/interactive/components/chat-session-host-terminal-cleanup.js.map +1 -0
- package/dist/modes/interactive/components/chat-session-host.d.ts +2 -0
- package/dist/modes/interactive/components/chat-session-host.d.ts.map +1 -1
- package/dist/modes/interactive/components/chat-session-host.js +7 -1
- package/dist/modes/interactive/components/chat-session-host.js.map +1 -1
- package/dist/modes/interactive/components/chat-transcript.d.ts.map +1 -1
- package/dist/modes/interactive/components/chat-transcript.js +15 -4
- package/dist/modes/interactive/components/chat-transcript.js.map +1 -1
- package/dist/modes/interactive/components/tool-execution.d.ts +3 -0
- package/dist/modes/interactive/components/tool-execution.d.ts.map +1 -1
- package/dist/modes/interactive/components/tool-execution.js +26 -0
- package/dist/modes/interactive/components/tool-execution.js.map +1 -1
- package/docs/compaction.md +2 -0
- package/docs/models.md +1 -1
- package/docs/providers.md +2 -1
- package/docs/session-format.md +6 -0
- package/docs/sessions.md +6 -0
- package/docs/workflows.md +105 -3
- package/package.json +4 -3
|
@@ -10,7 +10,7 @@ import { discoverAgents } from "../agents/agents.ts";
|
|
|
10
10
|
import { cleanupAllArtifactDirs, cleanupOldArtifacts, getArtifactsDir } from "../shared/artifacts.ts";
|
|
11
11
|
import { resolveCurrentSessionId } from "../shared/session-identity.ts";
|
|
12
12
|
import { cleanupOldChainDirs } from "../shared/settings.ts";
|
|
13
|
-
import { renderLiveSubagentResult, renderSubagentResult, stopResultAnimations, stopWidgetAnimation, type SubagentResultRenderState } from "../tui/render.ts";
|
|
13
|
+
import { advanceResultPulseFrame, renderLiveSubagentResult, renderSubagentResult, stopResultAnimations, stopWidgetAnimation, type SubagentResultRenderState } from "../tui/render.ts";
|
|
14
14
|
import { SubagentParams } from "./schemas.ts";
|
|
15
15
|
import { createSubagentExecutor, type SubagentParamsLike } from "../runs/foreground/subagent-executor.ts";
|
|
16
16
|
import { createAsyncJobTracker } from "../runs/background/async-job-tracker.ts";
|
|
@@ -69,7 +69,7 @@ type SubagentToolRenderState = SubagentResultRenderState;
|
|
|
69
69
|
function rebuildSlashResultContainer(
|
|
70
70
|
container: Container,
|
|
71
71
|
result: AgentToolResult<Details>,
|
|
72
|
-
options: { expanded: boolean; now?: number },
|
|
72
|
+
options: { expanded: boolean; now?: number; pulseFrame?: number },
|
|
73
73
|
theme: ExtensionContext["ui"]["theme"],
|
|
74
74
|
): void {
|
|
75
75
|
container.clear();
|
|
@@ -87,12 +87,14 @@ function createSlashResultComponent(
|
|
|
87
87
|
const container = new Container();
|
|
88
88
|
let lastVersion = -1;
|
|
89
89
|
let lastSnapshotNow = 0;
|
|
90
|
+
let pulseFrame = 0;
|
|
90
91
|
container.render = (width: number): string[] => {
|
|
91
92
|
const snapshot = getSlashRenderableSnapshot(details);
|
|
92
93
|
if (snapshot.version !== lastVersion) {
|
|
93
94
|
lastVersion = snapshot.version;
|
|
94
95
|
lastSnapshotNow = Date.now();
|
|
95
|
-
|
|
96
|
+
pulseFrame = advanceResultPulseFrame(pulseFrame);
|
|
97
|
+
rebuildSlashResultContainer(container, snapshot.result, { ...options, now: lastSnapshotNow, pulseFrame }, theme);
|
|
96
98
|
}
|
|
97
99
|
return Container.prototype.render.call(container, width);
|
|
98
100
|
};
|
|
@@ -179,6 +181,7 @@ export default function registerSubagentExtension(pi: ExtensionAPI): void {
|
|
|
179
181
|
baseCwd: "",
|
|
180
182
|
currentSessionId: null,
|
|
181
183
|
asyncJobs: new Map(),
|
|
184
|
+
subagentInProgress: false,
|
|
182
185
|
foregroundRuns: new Map(),
|
|
183
186
|
foregroundControls: new Map(),
|
|
184
187
|
lastForegroundControlId: null,
|
|
@@ -137,11 +137,6 @@ const ChainItem = Type.Object({
|
|
|
137
137
|
}, {
|
|
138
138
|
description: "Chain step: use {agent, task?, ...} for sequential, {parallel: [...]} for static concurrent execution, or {expand, parallel: {...}, collect} for dynamic fanout.",
|
|
139
139
|
additionalProperties: false,
|
|
140
|
-
allOf: [
|
|
141
|
-
{ if: { required: ["expand"] }, then: { required: ["parallel", "collect"], properties: { parallel: { type: "object" } } } },
|
|
142
|
-
{ if: { required: ["collect"] }, then: { required: ["expand", "parallel"], properties: { parallel: { type: "object" } } } },
|
|
143
|
-
{ not: { required: ["expand"], properties: { parallel: { type: "array", items: {} } } } },
|
|
144
|
-
],
|
|
145
140
|
});
|
|
146
141
|
|
|
147
142
|
const ControlOverrides = Type.Object({
|
|
@@ -395,10 +395,7 @@ export function createAsyncJobTracker(pi: Pick<ExtensionAPI, "events">, state: S
|
|
|
395
395
|
state.foregroundControls?.clear();
|
|
396
396
|
state.lastForegroundControlId = null;
|
|
397
397
|
state.resultFileCoalescer.clear();
|
|
398
|
-
if (ctx?.hasUI)
|
|
399
|
-
state.lastUiContext = ctx;
|
|
400
|
-
rerenderWidget(ctx, []);
|
|
401
|
-
}
|
|
398
|
+
if (ctx?.hasUI) state.lastUiContext = ctx;
|
|
402
399
|
};
|
|
403
400
|
|
|
404
401
|
return { ensurePoller, handleStarted, handleComplete, resetJobs, hydrateActiveJobs };
|
|
@@ -25,11 +25,25 @@ import {
|
|
|
25
25
|
wrapForkTask,
|
|
26
26
|
type AgentProgress,
|
|
27
27
|
type ArtifactPaths,
|
|
28
|
+
type SingleResult,
|
|
28
29
|
type SubagentToolResult,
|
|
29
30
|
} from "../../shared/types.ts";
|
|
30
31
|
import type { ExecutionContextData, ResolvedExecutorDeps } from "./subagent-executor-types.ts";
|
|
31
32
|
import { createForegroundControlNotifier, maybeBuildForegroundIntercomReceipt, rememberForegroundRun } from "./subagent-executor-status.ts";
|
|
32
33
|
|
|
34
|
+
function formatFailedSingleRunOutput(result: SingleResult, displayOutput: string): string {
|
|
35
|
+
const error = result.error || "Failed";
|
|
36
|
+
const output = displayOutput.trim();
|
|
37
|
+
const lines = [error];
|
|
38
|
+
if (output && output !== error.trim()) {
|
|
39
|
+
lines.push("", "Output:", output);
|
|
40
|
+
}
|
|
41
|
+
if (result.artifactPaths?.outputPath) {
|
|
42
|
+
lines.push("", `Output artifact: ${result.artifactPaths.outputPath}`);
|
|
43
|
+
}
|
|
44
|
+
return lines.join("\n");
|
|
45
|
+
}
|
|
46
|
+
|
|
33
47
|
export async function runSinglePath(data: ExecutionContextData, deps: ResolvedExecutorDeps): Promise<SubagentToolResult> {
|
|
34
48
|
const {
|
|
35
49
|
params,
|
|
@@ -309,7 +323,7 @@ export async function runSinglePath(data: ExecutionContextData, deps: ResolvedEx
|
|
|
309
323
|
|
|
310
324
|
if (r.exitCode !== 0)
|
|
311
325
|
return {
|
|
312
|
-
content: [{ type: "text", text: r.
|
|
326
|
+
content: [{ type: "text", text: formatFailedSingleRunOutput(r, finalizedOutput.displayOutput) }],
|
|
313
327
|
details,
|
|
314
328
|
isError: true,
|
|
315
329
|
};
|
|
@@ -172,6 +172,23 @@ async function handleInterruptRequest(input: {
|
|
|
172
172
|
};
|
|
173
173
|
}
|
|
174
174
|
|
|
175
|
+
function inferExecutionMode(params: SubagentParamsLike): "single" | "parallel" | "chain" {
|
|
176
|
+
if ((params.chain?.length ?? 0) > 0) return "chain";
|
|
177
|
+
if ((params.tasks?.length ?? 0) > 0) return "parallel";
|
|
178
|
+
return "single";
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
function duplicateSubagentCallResult(params: SubagentParamsLike): SubagentToolResult {
|
|
182
|
+
return {
|
|
183
|
+
content: [{
|
|
184
|
+
type: "text",
|
|
185
|
+
text: "Rejected: a subagent call is already in progress. Issue exactly ONE subagent call per turn.",
|
|
186
|
+
}],
|
|
187
|
+
isError: true,
|
|
188
|
+
details: { mode: inferExecutionMode(params), results: [] },
|
|
189
|
+
};
|
|
190
|
+
}
|
|
191
|
+
|
|
175
192
|
export function createSubagentExecutor(rawDeps: ExecutorDeps): {
|
|
176
193
|
execute: (
|
|
177
194
|
id: string,
|
|
@@ -249,5 +266,22 @@ export function createSubagentExecutor(rawDeps: ExecutorDeps): {
|
|
|
249
266
|
}, prepared.effectiveParams.context);
|
|
250
267
|
};
|
|
251
268
|
|
|
252
|
-
|
|
269
|
+
const executeWithSingleDispatchGuard = async (
|
|
270
|
+
id: string,
|
|
271
|
+
params: SubagentParamsLike,
|
|
272
|
+
signal: AbortSignal,
|
|
273
|
+
onUpdate: ((r: SubagentToolResult) => void) | undefined,
|
|
274
|
+
ctx: ExtensionContext,
|
|
275
|
+
): Promise<SubagentToolResult> => {
|
|
276
|
+
if (params.action) return execute(id, params, signal, onUpdate, ctx);
|
|
277
|
+
if (deps.state.subagentInProgress === true) return duplicateSubagentCallResult(params);
|
|
278
|
+
deps.state.subagentInProgress = true;
|
|
279
|
+
try {
|
|
280
|
+
return await execute(id, params, signal, onUpdate, ctx);
|
|
281
|
+
} finally {
|
|
282
|
+
deps.state.subagentInProgress = false;
|
|
283
|
+
}
|
|
284
|
+
};
|
|
285
|
+
|
|
286
|
+
return { execute: executeWithSingleDispatchGuard };
|
|
253
287
|
}
|
|
@@ -31,6 +31,7 @@ export const CHILD_FANOUT_BOUNDARY_INSTRUCTIONS = [
|
|
|
31
31
|
const PARENT_ONLY_CUSTOM_MESSAGE_TYPES = new Set([
|
|
32
32
|
"subagent-orchestration-instructions",
|
|
33
33
|
"subagent-slash-result",
|
|
34
|
+
"subagent-slash-text-result",
|
|
34
35
|
"subagent-notify",
|
|
35
36
|
"subagent_control_notice",
|
|
36
37
|
"subagent-control",
|
|
@@ -130,14 +131,15 @@ function stripAssistantSubagentToolCallBlocks(message: unknown): unknown | undef
|
|
|
130
131
|
}
|
|
131
132
|
|
|
132
133
|
export function stripParentOnlySubagentMessages(messages: unknown[]): unknown[] {
|
|
134
|
+
const preserveCurrentFanoutToolHistory = process.env[SUBAGENT_FANOUT_CHILD_ENV] === "1";
|
|
133
135
|
let changed = false;
|
|
134
136
|
const filtered: unknown[] = [];
|
|
135
137
|
for (const message of messages) {
|
|
136
|
-
if (isParentOnlySubagentMessage(message) || isSubagentToolResultMessage(message)) {
|
|
138
|
+
if (isParentOnlySubagentMessage(message) || (!preserveCurrentFanoutToolHistory && isSubagentToolResultMessage(message))) {
|
|
137
139
|
changed = true;
|
|
138
140
|
continue;
|
|
139
141
|
}
|
|
140
|
-
const stripped = stripAssistantSubagentToolCallBlocks(message);
|
|
142
|
+
const stripped = preserveCurrentFanoutToolHistory ? message : stripAssistantSubagentToolCallBlocks(message);
|
|
141
143
|
if (stripped === undefined) {
|
|
142
144
|
changed = true;
|
|
143
145
|
continue;
|
|
@@ -230,6 +230,7 @@ export interface SubagentState {
|
|
|
230
230
|
baseCwd: string;
|
|
231
231
|
currentSessionId: string | null;
|
|
232
232
|
asyncJobs: Map<string, AsyncJobState>;
|
|
233
|
+
subagentInProgress?: boolean;
|
|
233
234
|
foregroundRuns?: Map<string, ForegroundResumeRun>;
|
|
234
235
|
foregroundControls: Map<string, {
|
|
235
236
|
runId: string;
|
|
@@ -82,6 +82,7 @@ interface PromptTemplateBridgeResult {
|
|
|
82
82
|
exitCode?: number;
|
|
83
83
|
error?: string;
|
|
84
84
|
model?: string;
|
|
85
|
+
toolCalls?: Array<{ text?: string; expandedText?: string }>;
|
|
85
86
|
}>;
|
|
86
87
|
progress?: Array<{
|
|
87
88
|
index?: number;
|
|
@@ -147,10 +148,13 @@ function parsePromptTemplateRequest(data: unknown): PromptTemplateDelegationRequ
|
|
|
147
148
|
if (!hasSingle && tasks.length === 0) return undefined;
|
|
148
149
|
|
|
149
150
|
const fallbackTask = tasks[0];
|
|
151
|
+
const agent = hasSingle ? value.agent : fallbackTask?.agent;
|
|
152
|
+
const task = hasSingle ? value.task : fallbackTask?.task;
|
|
153
|
+
if (!agent || !task) return undefined;
|
|
150
154
|
return {
|
|
151
155
|
requestId: value.requestId,
|
|
152
|
-
agent
|
|
153
|
-
task
|
|
156
|
+
agent,
|
|
157
|
+
task,
|
|
154
158
|
...(tasks.length > 0 ? { tasks } : {}),
|
|
155
159
|
context: value.context,
|
|
156
160
|
model: value.model,
|
|
@@ -209,13 +213,31 @@ function resolveProgressModel(
|
|
|
209
213
|
return firstWithModel?.model;
|
|
210
214
|
}
|
|
211
215
|
|
|
212
|
-
function
|
|
216
|
+
function toolCallSummaryText(summary: { text?: string; expandedText?: string }): string | undefined {
|
|
217
|
+
const text = typeof summary.expandedText === "string" && summary.expandedText.trim().length > 0
|
|
218
|
+
? summary.expandedText.trim()
|
|
219
|
+
: typeof summary.text === "string"
|
|
220
|
+
? summary.text.trim()
|
|
221
|
+
: "";
|
|
222
|
+
return text || undefined;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
function buildDelegationMessages(
|
|
226
|
+
result: { messages?: unknown[]; finalOutput?: string; toolCalls?: Array<{ text?: string; expandedText?: string }> },
|
|
227
|
+
fallbackText?: string,
|
|
228
|
+
): unknown[] {
|
|
213
229
|
if (Array.isArray(result.messages) && result.messages.length > 0) return result.messages;
|
|
230
|
+
const toolCallSummaries = (result.toolCalls ?? []).flatMap((summary) => {
|
|
231
|
+
const text = toolCallSummaryText(summary);
|
|
232
|
+
return text ? [`- ${text}`] : [];
|
|
233
|
+
});
|
|
234
|
+
const toolCallText = toolCallSummaries.length > 0 ? `Tool calls:\n${toolCallSummaries.join("\n")}` : undefined;
|
|
214
235
|
const text = typeof result.finalOutput === "string" && result.finalOutput.trim().length > 0
|
|
215
236
|
? result.finalOutput.trim()
|
|
216
237
|
: fallbackText;
|
|
217
|
-
|
|
218
|
-
|
|
238
|
+
const contentText = [toolCallText, text].filter((part): part is string => Boolean(part)).join("\n\n");
|
|
239
|
+
if (!contentText) return [];
|
|
240
|
+
return [{ role: "assistant", content: [{ type: "text", text: contentText }] }];
|
|
219
241
|
}
|
|
220
242
|
|
|
221
243
|
function toDelegationUpdate(requestId: string, update: PromptTemplateBridgeResult): PromptTemplateDelegationUpdate | undefined {
|
|
@@ -66,16 +66,22 @@ export function truncLine(text: string, maxWidth: number): string {
|
|
|
66
66
|
return result + activeStyles.join("") + "…";
|
|
67
67
|
}
|
|
68
68
|
|
|
69
|
-
const RUNNING_FRAMES = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"];
|
|
69
|
+
export const RUNNING_FRAMES = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"];
|
|
70
70
|
|
|
71
71
|
/**
|
|
72
72
|
* Spinner cadence (ms per frame). The running glyph is derived from wall-clock
|
|
73
73
|
* time so every active spinner advances smoothly and in lockstep, independent
|
|
74
74
|
* of how often (or how irregularly) progress data updates arrive. The animation
|
|
75
75
|
* timers below only schedule re-renders; the displayed frame always comes from
|
|
76
|
-
* the clock. This fixes the frozen/stuttering spinner from issue #1084
|
|
77
|
-
*
|
|
78
|
-
*
|
|
76
|
+
* the clock. This fixes the frozen/stuttering spinner from issue #1084.
|
|
77
|
+
*
|
|
78
|
+
* IMPORTANT: a wall-clock spinner only stays flicker-free for widgets pinned to
|
|
79
|
+
* the bottom of the buffer (e.g. the below-editor async widget), where every
|
|
80
|
+
* tick stays inside the viewport. Content rendered into chat scrollback (live
|
|
81
|
+
* foreground subagent results) can scroll above the viewport fold; there, even
|
|
82
|
+
* a single-cell spinner diff forces pi-tui into a destructive full-screen +
|
|
83
|
+
* scrollback clear on every tick. Such surfaces must NOT animate on a timer —
|
|
84
|
+
* see pulseGlyph(), which is advanced once per real progress update instead.
|
|
79
85
|
*/
|
|
80
86
|
export const RUNNING_ANIMATION_MS = 80;
|
|
81
87
|
|
|
@@ -106,6 +112,23 @@ export function runningGlyph(seed?: number, now?: number): string {
|
|
|
106
112
|
return RUNNING_FRAMES[Math.abs(animatedSeed) % RUNNING_FRAMES.length]!;
|
|
107
113
|
}
|
|
108
114
|
|
|
115
|
+
export const PULSE_FRAMES = ["·", "•", "●", "•"];
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* Activity "heartbeat" glyph for live foreground subagent results. Unlike
|
|
119
|
+
* runningGlyph(), the frame is NOT derived from wall-clock time: the caller
|
|
120
|
+
* advances `frame` exactly once per real progress update (see
|
|
121
|
+
* renderLiveSubagentResult). With no animation timer, the only line diffs this
|
|
122
|
+
* produces coincide with progress data that genuinely changed, so the pulse can
|
|
123
|
+
* live in chat scrollback (above or below the fold) without ever triggering
|
|
124
|
+
* pi-tui's full-screen/scrollback clear. Returns a steady breathing dot that
|
|
125
|
+
* grows and settles as the subagent reports activity.
|
|
126
|
+
*/
|
|
127
|
+
export function pulseGlyph(frame?: number): string {
|
|
128
|
+
const index = Number.isFinite(frame) ? Math.abs(Math.trunc(frame as number)) : 0;
|
|
129
|
+
return PULSE_FRAMES[index % PULSE_FRAMES.length]!;
|
|
130
|
+
}
|
|
131
|
+
|
|
109
132
|
export function progressRunningSeed(progress: ProgressSeedSource | undefined): number | undefined {
|
|
110
133
|
if (!progress) return undefined;
|
|
111
134
|
return runningSeed(
|
|
@@ -1,56 +1,47 @@
|
|
|
1
|
-
import { RUNNING_ANIMATION_MS } from "./render-layout.ts";
|
|
2
|
-
|
|
3
1
|
type ResultAnimationTimer = ReturnType<typeof setInterval>;
|
|
4
2
|
|
|
5
3
|
export interface SubagentResultRenderState {
|
|
6
4
|
subagentResultAnimationTimer?: ResultAnimationTimer;
|
|
5
|
+
subagentResultAnimationCleanup?: () => void;
|
|
7
6
|
subagentResultSnapshotKey?: string;
|
|
8
7
|
/** Stable semantic/content timestamp used for durations and activity text. */
|
|
9
8
|
subagentResultSnapshotNow?: number;
|
|
10
|
-
/**
|
|
11
|
-
|
|
9
|
+
/** Monotonic pulse frame, advanced once per progress update (no timer). */
|
|
10
|
+
subagentResultPulseFrame?: number;
|
|
12
11
|
}
|
|
13
12
|
|
|
14
13
|
export type ResultAnimationContext = {
|
|
15
14
|
state: SubagentResultRenderState;
|
|
16
15
|
invalidate: () => void;
|
|
17
16
|
};
|
|
18
|
-
type LegacyResultAnimationContext = {
|
|
19
|
-
|
|
20
|
-
|
|
17
|
+
type LegacyResultAnimationContext = {
|
|
18
|
+
state: {
|
|
19
|
+
subagentResultAnimationTimer?: ResultAnimationTimer;
|
|
20
|
+
subagentResultAnimationCleanup?: () => void;
|
|
21
|
+
};
|
|
22
|
+
};
|
|
21
23
|
|
|
24
|
+
/**
|
|
25
|
+
* Legacy safety net for render state objects created by earlier timer-driven
|
|
26
|
+
* foreground result rendering. New code never schedules result timers, but
|
|
27
|
+
* clearing the field prevents a stale interval from surviving across upgrades.
|
|
28
|
+
*/
|
|
22
29
|
export function clearResultAnimationTimer(context: LegacyResultAnimationContext): void {
|
|
23
30
|
const timer = context.state.subagentResultAnimationTimer;
|
|
24
|
-
if (timer)
|
|
25
|
-
clearInterval(timer);
|
|
26
|
-
activeResultAnimationTimers.delete(timer);
|
|
27
|
-
}
|
|
31
|
+
if (timer) clearInterval(timer);
|
|
28
32
|
context.state.subagentResultAnimationTimer = undefined;
|
|
33
|
+
context.state.subagentResultAnimationCleanup = undefined;
|
|
29
34
|
}
|
|
30
35
|
|
|
31
|
-
export function
|
|
32
|
-
|
|
36
|
+
export function advanceResultPulseFrame(frame: number | undefined): number {
|
|
37
|
+
return (frame ?? 0) + 1;
|
|
33
38
|
}
|
|
34
39
|
|
|
35
|
-
export function
|
|
36
|
-
|
|
37
|
-
const timer = setInterval(() => {
|
|
38
|
-
context.state.subagentResultSpinnerFrameNow = Date.now();
|
|
39
|
-
try {
|
|
40
|
-
context.invalidate();
|
|
41
|
-
} catch {
|
|
42
|
-
clearResultAnimationTimer(context);
|
|
43
|
-
}
|
|
44
|
-
}, RUNNING_ANIMATION_MS);
|
|
45
|
-
timer.unref?.();
|
|
46
|
-
context.state.subagentResultAnimationTimer = timer;
|
|
47
|
-
activeResultAnimationTimers.set(timer, context.state);
|
|
40
|
+
export function clearLegacyResultAnimationTimer(context: LegacyResultAnimationContext): void {
|
|
41
|
+
clearResultAnimationTimer(context);
|
|
48
42
|
}
|
|
49
43
|
|
|
50
44
|
export function stopResultAnimations(): void {
|
|
51
|
-
for
|
|
52
|
-
|
|
53
|
-
if (state.subagentResultAnimationTimer === timer) state.subagentResultAnimationTimer = undefined;
|
|
54
|
-
}
|
|
55
|
-
activeResultAnimationTimers.clear();
|
|
45
|
+
// Retained for extension teardown compatibility; result rendering no longer
|
|
46
|
+
// registers global animation timers.
|
|
56
47
|
}
|
|
@@ -2,7 +2,7 @@ import { Container, Text, type Component } from "@earendil-works/pi-tui";
|
|
|
2
2
|
import type { AgentProgress, AsyncJobStep, Details } from "../shared/types.ts";
|
|
3
3
|
import { shortenPath } from "../shared/formatters.ts";
|
|
4
4
|
import { getSingleResultOutput } from "../shared/utils.ts";
|
|
5
|
-
import { getTermWidth,
|
|
5
|
+
import { getTermWidth, pulseGlyph, truncLine, type Theme } from "./render-layout.ts";
|
|
6
6
|
import {
|
|
7
7
|
buildLiveStatusLine,
|
|
8
8
|
compactCurrentActivity,
|
|
@@ -25,7 +25,7 @@ import {
|
|
|
25
25
|
} from "./render-chain-graph.ts";
|
|
26
26
|
import { modelThinkingBadge, widgetStepGlyph, widgetStepStatus } from "./render-event-formatting.ts";
|
|
27
27
|
|
|
28
|
-
export function renderSingleCompact(d: Details, r: Details["results"][number], theme: Theme, now?: number,
|
|
28
|
+
export function renderSingleCompact(d: Details, r: Details["results"][number], theme: Theme, now?: number, pulseFrame?: number): Component {
|
|
29
29
|
const output = r.truncation?.text || getSingleResultOutput(r);
|
|
30
30
|
const progress = r.progress || r.progressSummary;
|
|
31
31
|
const isRunning = r.progress?.status === "running";
|
|
@@ -37,7 +37,7 @@ export function renderSingleCompact(d: Details, r: Details["results"][number], t
|
|
|
37
37
|
const c = new Container();
|
|
38
38
|
const width = getTermWidth() - 4;
|
|
39
39
|
const modelDisplay = modelThinkingBadge(theme, r.model, undefined, r.fastMode);
|
|
40
|
-
c.addChild(new Text(truncLine(`${resultGlyph(r, output, theme, isRunning,
|
|
40
|
+
c.addChild(new Text(truncLine(`${resultGlyph(r, output, theme, isRunning, pulseFrame)} ${theme.fg("toolTitle", theme.bold(r.agent))}${modelDisplay}${contextBadge}${stats ? ` ${theme.fg("dim", "·")} ${stats}` : ""}`, width), 0, 0));
|
|
41
41
|
|
|
42
42
|
if (isRunning && r.progress) {
|
|
43
43
|
const progressSnapshotNow = snapshotNowForProgress(r.progress, now);
|
|
@@ -61,7 +61,7 @@ export function renderSingleCompact(d: Details, r: Details["results"][number], t
|
|
|
61
61
|
return c;
|
|
62
62
|
}
|
|
63
63
|
|
|
64
|
-
export function renderMultiCompact(d: Details, theme: Theme, now?: number,
|
|
64
|
+
export function renderMultiCompact(d: Details, theme: Theme, now?: number, pulseFrame?: number): Component {
|
|
65
65
|
const hasRunning = d.progress?.some((p) => p.status === "running")
|
|
66
66
|
|| d.results.some((r) => r.progress?.status === "running")
|
|
67
67
|
|| workflowGraphHasStatus(d, ["running"]);
|
|
@@ -87,7 +87,7 @@ export function renderMultiCompact(d: Details, theme: Theme, now?: number, spinn
|
|
|
87
87
|
const itemTitle = multiLabel.itemTitle;
|
|
88
88
|
const stats = statJoin(theme, [multiLabel.headerLabel, formatProgressStats(theme, totalSummary, true, now)]);
|
|
89
89
|
const glyph = hasRunning
|
|
90
|
-
? theme.fg("accent",
|
|
90
|
+
? theme.fg("accent", pulseGlyph(pulseFrame))
|
|
91
91
|
: failed
|
|
92
92
|
? theme.fg("error", "✗")
|
|
93
93
|
: paused
|
|
@@ -133,7 +133,7 @@ export function renderMultiCompact(d: Details, theme: Theme, now?: number, spinn
|
|
|
133
133
|
const rPending = rProg && "status" in rProg && rProg.status === "pending";
|
|
134
134
|
const stepNumber = r.progress?.index !== undefined ? r.progress.index + 1 : progressFromArray?.index !== undefined ? progressFromArray.index + 1 : i + 1;
|
|
135
135
|
const stepStats = formatProgressStats(theme, rProg, true, now);
|
|
136
|
-
const glyph = rPending ? theme.fg("dim", "◦") : resultGlyph(r, output, theme, rRunning,
|
|
136
|
+
const glyph = rPending ? theme.fg("dim", "◦") : resultGlyph(r, output, theme, rRunning, pulseFrame);
|
|
137
137
|
const pendingLabel = rPending ? ` ${theme.fg("dim", "· pending")}` : "";
|
|
138
138
|
const stepLabel = resultRowLabel(d, multiLabel, i, stepNumber);
|
|
139
139
|
const line = `${glyph} ${stepLabel}: ${themeBold(theme, agentName)}${stepStats ? ` ${theme.fg("dim", "·")} ${stepStats}` : ""}${pendingLabel}`;
|
|
@@ -5,10 +5,10 @@ import type { AgentProgress, AsyncJobStep, Details } from "../shared/types.ts";
|
|
|
5
5
|
import { formatDuration, formatTokens, formatUsage, shortenPath } from "../shared/formatters.ts";
|
|
6
6
|
import { getSingleResultOutput } from "../shared/utils.ts";
|
|
7
7
|
import { getTermWidth, truncLine, type Theme } from "./render-layout.ts";
|
|
8
|
-
import {
|
|
8
|
+
import { advanceResultPulseFrame, clearResultAnimationTimer, type ResultAnimationContext } from "./render-result-animation.ts";
|
|
9
9
|
import { renderMultiCompact, renderSingleCompact } from "./render-result-compact.ts";
|
|
10
10
|
import { buildChainRenderEntries, buildMultiProgressLabel, resultRowLabel, workflowGraphHasStatus, type ChainRenderEntry } from "./render-chain-graph.ts";
|
|
11
|
-
import {
|
|
11
|
+
import { subagentResultRenderKey } from "./render-stable-output.ts";
|
|
12
12
|
import { modelThinkingBadge, widgetStepStatus } from "./render-event-formatting.ts";
|
|
13
13
|
import {
|
|
14
14
|
buildLiveStatusLine,
|
|
@@ -28,26 +28,27 @@ export function renderLiveSubagentResult(
|
|
|
28
28
|
): Component {
|
|
29
29
|
const nextKey = subagentResultRenderKey(result, options);
|
|
30
30
|
if (context.state.subagentResultSnapshotKey !== nextKey) {
|
|
31
|
-
const frameNow = Date.now();
|
|
32
31
|
context.state.subagentResultSnapshotKey = nextKey;
|
|
33
|
-
context.state.subagentResultSnapshotNow =
|
|
34
|
-
|
|
32
|
+
context.state.subagentResultSnapshotNow = Date.now();
|
|
33
|
+
// Advance the activity pulse exactly once per real progress update.
|
|
34
|
+
// Foreground subagent results render into chat scrollback, which can sit
|
|
35
|
+
// above the viewport fold. Animating on a timer there forces pi-tui into a
|
|
36
|
+
// destructive full-screen/scrollback clear on every tick (the flicker that
|
|
37
|
+
// scaled with widget height). Driving the pulse off genuine updates keeps
|
|
38
|
+
// the only line diffs tied to content that actually changed, so the
|
|
39
|
+
// differential renderer repaints exactly as it would for any progress
|
|
40
|
+
// update — no extra above-fold churn between updates.
|
|
41
|
+
context.state.subagentResultPulseFrame = advanceResultPulseFrame(context.state.subagentResultPulseFrame);
|
|
35
42
|
}
|
|
36
43
|
context.state.subagentResultSnapshotNow ??= Date.now();
|
|
37
|
-
context.state.
|
|
38
|
-
//
|
|
39
|
-
//
|
|
40
|
-
|
|
41
|
-
// of updating elapsed/tool/activity text and causing broad chatbox churn.
|
|
42
|
-
if (options.isPartial && isRunningSubagentResult(result)) {
|
|
43
|
-
ensureResultAnimation(context);
|
|
44
|
-
} else {
|
|
45
|
-
clearResultAnimationTimer(context);
|
|
46
|
-
}
|
|
44
|
+
context.state.subagentResultPulseFrame ??= 0;
|
|
45
|
+
// Never schedule timer-driven re-renders for scrollback content; clear any
|
|
46
|
+
// stale timer a previous version may have installed for this render slot.
|
|
47
|
+
clearResultAnimationTimer(context);
|
|
47
48
|
return renderSubagentResult(result, {
|
|
48
49
|
...options,
|
|
49
50
|
now: context.state.subagentResultSnapshotNow,
|
|
50
|
-
|
|
51
|
+
pulseFrame: context.state.subagentResultPulseFrame,
|
|
51
52
|
}, theme);
|
|
52
53
|
}
|
|
53
54
|
|
|
@@ -56,7 +57,7 @@ export function renderLiveSubagentResult(
|
|
|
56
57
|
*/
|
|
57
58
|
export function renderSubagentResult(
|
|
58
59
|
result: AgentToolResult<Details>,
|
|
59
|
-
options: { expanded: boolean; now?: number;
|
|
60
|
+
options: { expanded: boolean; now?: number; pulseFrame?: number },
|
|
60
61
|
theme: Theme,
|
|
61
62
|
): Component {
|
|
62
63
|
const d = result.details;
|
|
@@ -72,7 +73,7 @@ export function renderSubagentResult(
|
|
|
72
73
|
|
|
73
74
|
if (d.mode === "single" && d.results.length === 1) {
|
|
74
75
|
const r = d.results[0];
|
|
75
|
-
if (!expanded) return renderSingleCompact(d, r, theme, options.now, options.
|
|
76
|
+
if (!expanded) return renderSingleCompact(d, r, theme, options.now, options.pulseFrame);
|
|
76
77
|
const isRunning = r.progress?.status === "running";
|
|
77
78
|
const icon = isRunning
|
|
78
79
|
? theme.fg("warning", "running")
|
|
@@ -166,7 +167,7 @@ export function renderSubagentResult(
|
|
|
166
167
|
return c;
|
|
167
168
|
}
|
|
168
169
|
|
|
169
|
-
if (!expanded) return renderMultiCompact(d, theme, options.now, options.
|
|
170
|
+
if (!expanded) return renderMultiCompact(d, theme, options.now, options.pulseFrame);
|
|
170
171
|
|
|
171
172
|
const hasRunning = d.progress?.some((p) => p.status === "running")
|
|
172
173
|
|| d.results.some((r) => r.progress?.status === "running")
|
|
@@ -2,7 +2,7 @@ import type { AgentProgress, Details } from "../shared/types.ts";
|
|
|
2
2
|
import { formatDuration, formatTokens, formatToolCall } from "../shared/formatters.ts";
|
|
3
3
|
import { getDisplayItems } from "../shared/utils.ts";
|
|
4
4
|
import { formatActivityLabel } from "../shared/status-format.ts";
|
|
5
|
-
import { getTermWidth,
|
|
5
|
+
import { getTermWidth, pulseGlyph, type Theme } from "./render-layout.ts";
|
|
6
6
|
|
|
7
7
|
export function extractOutputTarget(task: string): string | undefined {
|
|
8
8
|
const writeToMatch = task.match(/\[Write to:\s*([^\]\n]+)\]/i);
|
|
@@ -118,8 +118,8 @@ export function resultStatusLine(result: Details["results"][number], output: str
|
|
|
118
118
|
return "Done";
|
|
119
119
|
}
|
|
120
120
|
|
|
121
|
-
export function resultGlyph(result: Details["results"][number], output: string, theme: Theme, running = result.progress?.status === "running",
|
|
122
|
-
if (running) return theme.fg("accent",
|
|
121
|
+
export function resultGlyph(result: Details["results"][number], output: string, theme: Theme, running = result.progress?.status === "running", pulseFrame?: number): string {
|
|
122
|
+
if (running) return theme.fg("accent", pulseGlyph(pulseFrame));
|
|
123
123
|
if (result.detached) return theme.fg("warning", "■");
|
|
124
124
|
if (result.interrupted) return theme.fg("warning", "■");
|
|
125
125
|
if (result.exitCode !== 0) return theme.fg("error", "✗");
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { ExtensionContext } from "@bastani/atomic";
|
|
2
2
|
import { Container, Text, type Component } from "@earendil-works/pi-tui";
|
|
3
|
+
import * as path from "node:path";
|
|
3
4
|
import { MAX_WIDGET_JOBS, WIDGET_KEY, type AsyncJobState } from "../shared/types.ts";
|
|
4
5
|
import { getTermWidth, RUNNING_ANIMATION_MS, runningGlyph, truncLine, type Theme } from "./render-layout.ts";
|
|
5
6
|
import { themeBold } from "./render-status-progress.ts";
|
|
@@ -64,6 +65,7 @@ let latestWidgetJobs: AsyncJobState[] = [];
|
|
|
64
65
|
let latestWidgetFrameNow = 0;
|
|
65
66
|
let widgetTimer: ReturnType<typeof setInterval> | undefined;
|
|
66
67
|
let mountedWidgetCtx: ExtensionContext | undefined;
|
|
68
|
+
let mountedWidgetOwnerKey: string | undefined;
|
|
67
69
|
let widgetMounted = false;
|
|
68
70
|
|
|
69
71
|
function getLatestWidgetJobs(): AsyncJobState[] {
|
|
@@ -87,9 +89,38 @@ function clearLatestWidgetState(): void {
|
|
|
87
89
|
latestWidgetJobs = [];
|
|
88
90
|
latestWidgetFrameNow = 0;
|
|
89
91
|
mountedWidgetCtx = undefined;
|
|
92
|
+
mountedWidgetOwnerKey = undefined;
|
|
90
93
|
widgetMounted = false;
|
|
91
94
|
}
|
|
92
95
|
|
|
96
|
+
function getWidgetOwnerKey(ctx: ExtensionContext): string {
|
|
97
|
+
const resolvedCwd = ctx.cwd ? path.resolve(ctx.cwd) : undefined;
|
|
98
|
+
const cwdOwner = resolvedCwd ?? "cwd:unknown";
|
|
99
|
+
let sessionOwner = "session:unknown";
|
|
100
|
+
try {
|
|
101
|
+
const sessionFile = ctx.sessionManager.getSessionFile?.();
|
|
102
|
+
if (sessionFile) {
|
|
103
|
+
const resolvedSessionFile = resolvedCwd
|
|
104
|
+
? path.resolve(resolvedCwd, sessionFile)
|
|
105
|
+
: path.resolve(sessionFile);
|
|
106
|
+
sessionOwner = `sessionFile:${resolvedSessionFile}`;
|
|
107
|
+
}
|
|
108
|
+
} catch {
|
|
109
|
+
// Fall through to the session id fallback below.
|
|
110
|
+
}
|
|
111
|
+
if (sessionOwner === "session:unknown") {
|
|
112
|
+
try {
|
|
113
|
+
const sessionId = ctx.sessionManager.getSessionId?.();
|
|
114
|
+
if (sessionId) sessionOwner = `sessionId:${sessionId}`;
|
|
115
|
+
} catch {
|
|
116
|
+
// Keep the unknown marker; cwd still scopes ownership.
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
// If no session identifier is available, cwd is the best available owner
|
|
120
|
+
// boundary and may intentionally coalesce concurrent sessions in one folder.
|
|
121
|
+
return `${sessionOwner}|cwd:${cwdOwner}`;
|
|
122
|
+
}
|
|
123
|
+
|
|
93
124
|
function requestWidgetRender(ctx: ExtensionContext): void {
|
|
94
125
|
(ctx as RenderRequestingContext).ui.requestRender?.();
|
|
95
126
|
}
|
|
@@ -226,10 +257,12 @@ export function buildWidgetLines(jobs: AsyncJobState[], theme: Theme, width = ge
|
|
|
226
257
|
* Render the async jobs widget
|
|
227
258
|
*/
|
|
228
259
|
export function renderWidget(ctx: ExtensionContext, jobs: AsyncJobState[]): void {
|
|
260
|
+
const ownerKey = getWidgetOwnerKey(ctx);
|
|
229
261
|
if (jobs.length === 0) {
|
|
230
|
-
if (widgetMounted &&
|
|
231
|
-
//
|
|
232
|
-
//
|
|
262
|
+
if (widgetMounted && mountedWidgetOwnerKey !== ownerKey) {
|
|
263
|
+
// With no visible job frame, stale-owner empty updates and newly-active
|
|
264
|
+
// empty owners are indistinguishable here. Preserve active-widget safety;
|
|
265
|
+
// host session disposal owns cross-owner empty-session teardown.
|
|
233
266
|
return;
|
|
234
267
|
}
|
|
235
268
|
stopWidgetAnimation();
|
|
@@ -242,11 +275,12 @@ export function renderWidget(ctx: ExtensionContext, jobs: AsyncJobState[]): void
|
|
|
242
275
|
latestWidgetCtx = ctx;
|
|
243
276
|
latestWidgetJobs = [...jobs];
|
|
244
277
|
latestWidgetFrameNow = Date.now();
|
|
245
|
-
if (widgetMounted &&
|
|
246
|
-
//
|
|
247
|
-
// old mount before installing the singleton widget on the new context.
|
|
278
|
+
if (widgetMounted && mountedWidgetOwnerKey !== ownerKey) {
|
|
279
|
+
// Session rebinding can leave the previous host UI alive briefly; clear the
|
|
280
|
+
// old mount before installing the singleton widget on the new owner/context.
|
|
248
281
|
unmountWidgetBestEffort(mountedWidgetCtx);
|
|
249
282
|
mountedWidgetCtx = undefined;
|
|
283
|
+
mountedWidgetOwnerKey = undefined;
|
|
250
284
|
widgetMounted = false;
|
|
251
285
|
}
|
|
252
286
|
if (!widgetMounted) {
|
|
@@ -260,10 +294,15 @@ export function renderWidget(ctx: ExtensionContext, jobs: AsyncJobState[]): void
|
|
|
260
294
|
placement: "belowEditor",
|
|
261
295
|
});
|
|
262
296
|
mountedWidgetCtx = ctx;
|
|
297
|
+
mountedWidgetOwnerKey = ownerKey;
|
|
263
298
|
widgetMounted = true;
|
|
264
299
|
} else {
|
|
265
300
|
// The mounted widget reads latestWidgetJobs via getLatestWidgetJobs(), so a
|
|
266
|
-
// visible->visible update only needs to ask the host to render in place.
|
|
301
|
+
// visible->visible update only needs to ask the host to render in place. Keep
|
|
302
|
+
// teardown pointed at the freshest same-owner wrapper because older wrappers
|
|
303
|
+
// can go stale after host session/context rebinding.
|
|
304
|
+
mountedWidgetCtx = ctx;
|
|
305
|
+
mountedWidgetOwnerKey = ownerKey;
|
|
267
306
|
requestWidgetRender(ctx);
|
|
268
307
|
}
|
|
269
308
|
// Keep the just-rendered ctx/jobs as the last-rendered state; only the ticker
|
|
@@ -5,11 +5,11 @@
|
|
|
5
5
|
* rendering responsibility across sibling modules.
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
|
-
export { RUNNING_ANIMATION_MS, currentRunningFrame } from "./render-layout.ts";
|
|
8
|
+
export { PULSE_FRAMES, RUNNING_ANIMATION_MS, RUNNING_FRAMES, currentRunningFrame, pulseGlyph } from "./render-layout.ts";
|
|
9
9
|
export {
|
|
10
|
+
advanceResultPulseFrame,
|
|
10
11
|
clearLegacyResultAnimationTimer,
|
|
11
12
|
clearResultAnimationTimer,
|
|
12
|
-
ensureResultAnimation,
|
|
13
13
|
stopResultAnimations,
|
|
14
14
|
} from "./render-result-animation.ts";
|
|
15
15
|
export type { SubagentResultRenderState } from "./render-result-animation.ts";
|