viberoom 0.3.1 → 0.4.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/NOTICE +12 -0
- package/README.md +119 -96
- package/dist/hub.js +18 -0
- package/dist/launcher.js +22 -0
- package/dist/main.js +4 -3
- package/dist/persona.js +1 -1
- package/dist/recipes.js +37 -14
- package/dist/room.js +3 -2
- package/dist/server.js +5 -0
- package/package.json +8 -5
- package/scripts/vendor-acp.mjs +85 -0
- package/ui/app.css +7 -0
- package/ui/app.js +47 -2
- package/ui/index.html +2 -0
- package/vendor/acp/claude-agent-acp/LICENSE +191 -0
- package/vendor/acp/claude-agent-acp/dist/acp-agent.js +7694 -0
- package/vendor/acp/claude-agent-acp/dist/acp-subagents.js +13 -0
- package/vendor/acp/claude-agent-acp/dist/air-extension.js +63 -0
- package/vendor/acp/claude-agent-acp/dist/async-tasks.js +613 -0
- package/vendor/acp/claude-agent-acp/dist/clear-context-coordinator.js +80 -0
- package/vendor/acp/claude-agent-acp/dist/elicitation.js +304 -0
- package/vendor/acp/claude-agent-acp/dist/exit-plan.js +154 -0
- package/vendor/acp/claude-agent-acp/dist/file-change-audit.js +350 -0
- package/vendor/acp/claude-agent-acp/dist/fork-session.js +41 -0
- package/vendor/acp/claude-agent-acp/dist/goal-extension.js +50 -0
- package/vendor/acp/claude-agent-acp/dist/index.js +98 -0
- package/vendor/acp/claude-agent-acp/dist/lib.js +5 -0
- package/vendor/acp/claude-agent-acp/dist/native-subagents.js +422 -0
- package/vendor/acp/claude-agent-acp/dist/permissions/effects.js +166 -0
- package/vendor/acp/claude-agent-acp/dist/permissions/modes.js +41 -0
- package/vendor/acp/claude-agent-acp/dist/permissions/normalization.js +100 -0
- package/vendor/acp/claude-agent-acp/dist/permissions/options/filesystem.js +124 -0
- package/vendor/acp/claude-agent-acp/dist/permissions/options/shared.js +64 -0
- package/vendor/acp/claude-agent-acp/dist/permissions/options/shell.js +100 -0
- package/vendor/acp/claude-agent-acp/dist/permissions/options/tools.js +135 -0
- package/vendor/acp/claude-agent-acp/dist/permissions/options.js +60 -0
- package/vendor/acp/claude-agent-acp/dist/permissions/presentation.js +83 -0
- package/vendor/acp/claude-agent-acp/dist/permissions/response.js +19 -0
- package/vendor/acp/claude-agent-acp/dist/session-config-ids.js +4 -0
- package/vendor/acp/claude-agent-acp/dist/session-failure-extension.js +324 -0
- package/vendor/acp/claude-agent-acp/dist/session-mode.js +234 -0
- package/vendor/acp/claude-agent-acp/dist/session-titles.js +199 -0
- package/vendor/acp/claude-agent-acp/dist/settings.js +185 -0
- package/vendor/acp/claude-agent-acp/dist/tool-result-meta.js +19 -0
- package/vendor/acp/claude-agent-acp/dist/tools.js +1235 -0
- package/vendor/acp/claude-agent-acp/dist/utils.js +81 -0
- package/vendor/acp/claude-agent-acp/package.json +7 -0
- package/vendor/acp/claude-agent-sdk/LICENSE.md +1 -0
- package/vendor/acp/claude-agent-sdk/agentSdkTypes.d.ts +1 -0
- package/vendor/acp/claude-agent-sdk/bridge.d.ts +378 -0
- package/vendor/acp/claude-agent-sdk/bridge.mjs +221 -0
- package/vendor/acp/claude-agent-sdk/browser-sdk.d.ts +107 -0
- package/vendor/acp/claude-agent-sdk/browser-sdk.js +185 -0
- package/vendor/acp/claude-agent-sdk/extractFromBunfs.d.ts +1 -0
- package/vendor/acp/claude-agent-sdk/extractFromBunfs.js +156 -0
- package/vendor/acp/claude-agent-sdk/manifest.json +65 -0
- package/vendor/acp/claude-agent-sdk/manifest.zst.json +73 -0
- package/vendor/acp/claude-agent-sdk/package.json +7 -0
- package/vendor/acp/claude-agent-sdk/sdk-tools.d.ts +4129 -0
- package/vendor/acp/claude-agent-sdk/sdk.d.ts +8687 -0
- package/vendor/acp/claude-agent-sdk/sdk.mjs +204 -0
- package/vendor/acp/codex-acp/LICENSE +190 -0
- package/vendor/acp/codex-acp/dist/index.js +34238 -0
- package/vendor/acp/codex-acp/package.json +7 -0
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { AIR_NATIVE_SUBAGENT_SESSIONS_CAPABILITY, clientSupportsAirCapability, } from "./air-extension.js";
|
|
2
|
+
export { AIR_NATIVE_SUBAGENT_SESSIONS_CAPABILITY } from "./air-extension.js";
|
|
3
|
+
export function clientSupportsSubagents(capabilities) {
|
|
4
|
+
const subagents = capabilities?.subagents;
|
|
5
|
+
if (typeof subagents === "object" && subagents !== null && !Array.isArray(subagents)) {
|
|
6
|
+
return true;
|
|
7
|
+
}
|
|
8
|
+
return clientSupportsAirCapability(capabilities, AIR_NATIVE_SUBAGENT_SESSIONS_CAPABILITY);
|
|
9
|
+
}
|
|
10
|
+
/** The only cast needed until the TypeScript SDK publishes PR #1992. */
|
|
11
|
+
export function asSdkSessionNotification(notification) {
|
|
12
|
+
return notification;
|
|
13
|
+
}
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
export const AIR_NATIVE_SUBAGENT_SESSIONS_CAPABILITY = "nativeSubagentSessions";
|
|
2
|
+
export const AIR_ASYNC_TASKS_CAPABILITY = "asyncTasks";
|
|
3
|
+
export const AIR_SESSION_FAILURE_CAPABILITY = "sessionFailure";
|
|
4
|
+
const JETBRAINS_META_KEY = "jetbrains";
|
|
5
|
+
const AIR_META_KEY = "air";
|
|
6
|
+
const AIR_EXTENSION_VERSION_KEY = "version";
|
|
7
|
+
const AIR_EXTENSION_CAPABILITIES_KEY = "capabilities";
|
|
8
|
+
const AIR_EXTENSION_VERSION = 1;
|
|
9
|
+
/** The capability list this side advertises, as its own `_meta` object. */
|
|
10
|
+
export function airCapabilityMeta(...capabilities) {
|
|
11
|
+
return withAirMeta(undefined, AIR_EXTENSION_CAPABILITIES_KEY, capabilities);
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* Merges one AIR extension payload into an existing `_meta`.
|
|
15
|
+
*
|
|
16
|
+
* Every other namespace is preserved: an update can carry both agent-native
|
|
17
|
+
* `claudeCode` metadata and an AIR payload, and two AIR payloads can share the
|
|
18
|
+
* same `air` object. Pass `undefined` to build a fresh `_meta`.
|
|
19
|
+
*/
|
|
20
|
+
export function withAirMeta(meta, capability, payload) {
|
|
21
|
+
const jetbrains = asRecord(meta?.[JETBRAINS_META_KEY]);
|
|
22
|
+
const air = asRecord(jetbrains[AIR_META_KEY]);
|
|
23
|
+
return {
|
|
24
|
+
...meta,
|
|
25
|
+
[JETBRAINS_META_KEY]: {
|
|
26
|
+
...jetbrains,
|
|
27
|
+
[AIR_META_KEY]: {
|
|
28
|
+
...air,
|
|
29
|
+
[AIR_EXTENSION_VERSION_KEY]: AIR_EXTENSION_VERSION,
|
|
30
|
+
[capability]: payload,
|
|
31
|
+
},
|
|
32
|
+
},
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
/** The `air` object inside a `_meta`, or undefined when the peer sent no AIR extension. */
|
|
36
|
+
export function airExtensionMeta(meta) {
|
|
37
|
+
const air = asRecord(asRecord(meta)[JETBRAINS_META_KEY])[AIR_META_KEY];
|
|
38
|
+
return air && typeof air === "object" && !Array.isArray(air)
|
|
39
|
+
? air
|
|
40
|
+
: undefined;
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* Whether the peer advertised `capability`.
|
|
44
|
+
*
|
|
45
|
+
* Takes `unknown` because every caller is reading wire data: an ACP
|
|
46
|
+
* `ClientCapabilities`, or a bag whose `_meta` was never validated.
|
|
47
|
+
*/
|
|
48
|
+
export function clientSupportsAirCapability(capabilities, capability) {
|
|
49
|
+
const air = airExtensionMeta(asRecord(capabilities)._meta);
|
|
50
|
+
const version = air?.[AIR_EXTENSION_VERSION_KEY];
|
|
51
|
+
const advertised = air?.[AIR_EXTENSION_CAPABILITIES_KEY];
|
|
52
|
+
return (typeof version === "number" &&
|
|
53
|
+
Number.isFinite(version) &&
|
|
54
|
+
Number.isInteger(version) &&
|
|
55
|
+
version >= AIR_EXTENSION_VERSION &&
|
|
56
|
+
Array.isArray(advertised) &&
|
|
57
|
+
advertised.includes(capability));
|
|
58
|
+
}
|
|
59
|
+
function asRecord(value) {
|
|
60
|
+
return value && typeof value === "object" && !Array.isArray(value)
|
|
61
|
+
? value
|
|
62
|
+
: {};
|
|
63
|
+
}
|
|
@@ -0,0 +1,613 @@
|
|
|
1
|
+
import { AIR_ASYNC_TASKS_CAPABILITY, clientSupportsAirCapability } from "./air-extension.js";
|
|
2
|
+
export function clientSupportsAsyncTasks(capabilities) {
|
|
3
|
+
return clientSupportsAirCapability(capabilities, AIR_ASYNC_TASKS_CAPABILITY);
|
|
4
|
+
}
|
|
5
|
+
/** Publishes Claude's non-agent background work as a separate AIR task lifecycle. */
|
|
6
|
+
export class AsyncTaskRuntime {
|
|
7
|
+
enabled;
|
|
8
|
+
sessionId;
|
|
9
|
+
publish;
|
|
10
|
+
/**
|
|
11
|
+
* One registry owns both active tasks and terminal tombstones. Keeping an
|
|
12
|
+
* unannounced terminal record is intentional: the Bash result proving that
|
|
13
|
+
* a command was backgrounded can arrive after its terminal SDK edge.
|
|
14
|
+
*/
|
|
15
|
+
tasks = new Map();
|
|
16
|
+
constructor(enabled, sessionId, publish) {
|
|
17
|
+
this.enabled = enabled;
|
|
18
|
+
this.sessionId = sessionId;
|
|
19
|
+
this.publish = publish;
|
|
20
|
+
}
|
|
21
|
+
async taskStarted(message) {
|
|
22
|
+
if (!this.enabled)
|
|
23
|
+
return;
|
|
24
|
+
const taskId = taskIdOf(message);
|
|
25
|
+
if (!taskId)
|
|
26
|
+
return;
|
|
27
|
+
const task = this.task(taskId);
|
|
28
|
+
if (isSubagentTask(message)) {
|
|
29
|
+
task.ignored = true;
|
|
30
|
+
return;
|
|
31
|
+
}
|
|
32
|
+
const previousOutputFilePath = task.outputFilePath;
|
|
33
|
+
const previousToolCallId = task.toolCallId;
|
|
34
|
+
const wasAnnounced = task.announced;
|
|
35
|
+
this.mergeStarted(task, message);
|
|
36
|
+
if (isBackgroundTask(field(message, "isBackgrounded", "is_backgrounded"), field(message, "taskType", "task_type"))) {
|
|
37
|
+
await this.announce(task);
|
|
38
|
+
}
|
|
39
|
+
if (wasAnnounced &&
|
|
40
|
+
(task.outputFilePath !== previousOutputFilePath || task.toolCallId !== previousToolCallId)) {
|
|
41
|
+
await this.publishMetadata(task);
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
async taskBackgrounded(message) {
|
|
45
|
+
if (!this.enabled)
|
|
46
|
+
return;
|
|
47
|
+
const taskId = taskIdOf(message);
|
|
48
|
+
if (!taskId)
|
|
49
|
+
return;
|
|
50
|
+
const task = this.task(taskId);
|
|
51
|
+
if (isSubagentTask(message)) {
|
|
52
|
+
task.ignored = true;
|
|
53
|
+
return;
|
|
54
|
+
}
|
|
55
|
+
const previousOutputFilePath = task.outputFilePath;
|
|
56
|
+
const previousToolCallId = task.toolCallId;
|
|
57
|
+
const wasAnnounced = task.announced;
|
|
58
|
+
this.mergeStarted(task, message);
|
|
59
|
+
await this.announce(task);
|
|
60
|
+
if (wasAnnounced &&
|
|
61
|
+
(task.outputFilePath !== previousOutputFilePath || task.toolCallId !== previousToolCallId)) {
|
|
62
|
+
await this.publishMetadata(task);
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
async taskUpdated(taskOrId, legacyPatch) {
|
|
66
|
+
if (!this.enabled)
|
|
67
|
+
return;
|
|
68
|
+
const taskId = typeof taskOrId === "string" ? nonBlankString(taskOrId) : taskIdOf(taskOrId);
|
|
69
|
+
const patch = typeof taskOrId === "string" ? legacyPatch : taskOrId.patch;
|
|
70
|
+
if (!taskId || !patch)
|
|
71
|
+
return;
|
|
72
|
+
const task = this.task(taskId);
|
|
73
|
+
const previousOutputFilePath = task.outputFilePath;
|
|
74
|
+
this.mergePatch(task, patch);
|
|
75
|
+
if (field(patch, "isBackgrounded", "is_backgrounded") === true) {
|
|
76
|
+
await this.announce(task);
|
|
77
|
+
}
|
|
78
|
+
const state = taskState(patch.status);
|
|
79
|
+
if (!state) {
|
|
80
|
+
if (task.announced && task.outputFilePath !== previousOutputFilePath) {
|
|
81
|
+
await this.publishMetadata(task);
|
|
82
|
+
}
|
|
83
|
+
return;
|
|
84
|
+
}
|
|
85
|
+
if (state === "running" || state === "paused") {
|
|
86
|
+
if (isTerminal(task.state)) {
|
|
87
|
+
if (task.announced && task.outputFilePath !== previousOutputFilePath) {
|
|
88
|
+
await this.publishMetadata(task);
|
|
89
|
+
}
|
|
90
|
+
return;
|
|
91
|
+
}
|
|
92
|
+
if (task.state !== state) {
|
|
93
|
+
task.state = state;
|
|
94
|
+
if (task.announced)
|
|
95
|
+
await this.publishState(task, state, nonBlankString(patch.error));
|
|
96
|
+
}
|
|
97
|
+
else if (task.announced && task.outputFilePath !== previousOutputFilePath) {
|
|
98
|
+
await this.publishMetadata(task);
|
|
99
|
+
}
|
|
100
|
+
return;
|
|
101
|
+
}
|
|
102
|
+
await this.finish(task, state, nonBlankString(patch.error), "event");
|
|
103
|
+
}
|
|
104
|
+
async taskProgress(message) {
|
|
105
|
+
if (!this.enabled)
|
|
106
|
+
return;
|
|
107
|
+
const taskId = taskIdOf(message);
|
|
108
|
+
if (!taskId)
|
|
109
|
+
return;
|
|
110
|
+
const task = this.tasks.get(taskId);
|
|
111
|
+
if (!task || !task.announced || task.ignored)
|
|
112
|
+
return;
|
|
113
|
+
const previousOutputFilePath = task.outputFilePath;
|
|
114
|
+
this.mergeOutputFilePath(task, message);
|
|
115
|
+
const outputChanged = task.outputFilePath !== previousOutputFilePath;
|
|
116
|
+
if (isTerminal(task.state)) {
|
|
117
|
+
if (outputChanged)
|
|
118
|
+
await this.publishMetadata(task);
|
|
119
|
+
return;
|
|
120
|
+
}
|
|
121
|
+
const usage = taskUsage(message.usage);
|
|
122
|
+
const description = nonBlankString(message.description);
|
|
123
|
+
const summary = nonBlankString(message.summary);
|
|
124
|
+
const lastToolName = nonBlankString(field(message, "lastToolName", "last_tool_name"));
|
|
125
|
+
await this.publishProgress(task, {
|
|
126
|
+
...(description ? { description } : {}),
|
|
127
|
+
...(summary ? { summary } : {}),
|
|
128
|
+
...(lastToolName ? { lastToolName } : {}),
|
|
129
|
+
...(usage ? { usage } : {}),
|
|
130
|
+
...(outputChanged && task.outputFilePath ? { outputFilePath: task.outputFilePath } : {}),
|
|
131
|
+
});
|
|
132
|
+
}
|
|
133
|
+
async taskNotification(taskOrId, legacyStatus, legacySummary, legacyOutputFilePath) {
|
|
134
|
+
if (!this.enabled)
|
|
135
|
+
return;
|
|
136
|
+
const message = typeof taskOrId === "string"
|
|
137
|
+
? {
|
|
138
|
+
taskId: taskOrId,
|
|
139
|
+
status: legacyStatus,
|
|
140
|
+
summary: legacySummary,
|
|
141
|
+
outputFilePath: legacyOutputFilePath,
|
|
142
|
+
}
|
|
143
|
+
: taskOrId;
|
|
144
|
+
const taskId = taskIdOf(message);
|
|
145
|
+
const state = taskState(message.status);
|
|
146
|
+
if (!taskId || !state || state === "running" || state === "paused")
|
|
147
|
+
return;
|
|
148
|
+
const task = this.task(taskId);
|
|
149
|
+
const wasTerminal = isTerminal(task.state);
|
|
150
|
+
const correctsLevelState = wasTerminal && task.terminalSource === "level";
|
|
151
|
+
const previousOutputFilePath = task.outputFilePath;
|
|
152
|
+
this.mergeOutputFilePath(task, message);
|
|
153
|
+
await this.finish(task, state, nonBlankString(message.summary), "event");
|
|
154
|
+
if (wasTerminal &&
|
|
155
|
+
!correctsLevelState &&
|
|
156
|
+
task.announced &&
|
|
157
|
+
task.outputFilePath !== previousOutputFilePath) {
|
|
158
|
+
await this.publishMetadata(task);
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
/**
|
|
162
|
+
* Reconciles the SDK's replace-semantics background task level. It does not
|
|
163
|
+
* create unknown tasks because this level normally precedes task_started and
|
|
164
|
+
* carries no tool-call attribution. It can, however, promote a known
|
|
165
|
+
* foreground task and terminate an announced task whose edge was lost.
|
|
166
|
+
*/
|
|
167
|
+
async backgroundTasksChanged(tasksOrMessage) {
|
|
168
|
+
if (!this.enabled)
|
|
169
|
+
return;
|
|
170
|
+
const value = Array.isArray(tasksOrMessage)
|
|
171
|
+
? tasksOrMessage
|
|
172
|
+
: isRecord(tasksOrMessage)
|
|
173
|
+
? tasksOrMessage.tasks
|
|
174
|
+
: undefined;
|
|
175
|
+
if (!Array.isArray(value))
|
|
176
|
+
return;
|
|
177
|
+
const live = new Set();
|
|
178
|
+
for (const item of value) {
|
|
179
|
+
if (!isRecord(item))
|
|
180
|
+
continue;
|
|
181
|
+
const taskId = taskIdOf(item);
|
|
182
|
+
if (!taskId)
|
|
183
|
+
continue;
|
|
184
|
+
live.add(taskId);
|
|
185
|
+
const task = this.task(taskId);
|
|
186
|
+
if (task.ignored || isTerminal(task.state))
|
|
187
|
+
continue;
|
|
188
|
+
if (field(item, "taskType", "task_type") === "local_agent") {
|
|
189
|
+
task.ignored = true;
|
|
190
|
+
continue;
|
|
191
|
+
}
|
|
192
|
+
if (!task.startedObserved) {
|
|
193
|
+
// The replace-level proves liveness but carries neither transcript
|
|
194
|
+
// policy nor full identity. Recover it for the Async Tasks panel only;
|
|
195
|
+
// creating a transcript card here would be irreversible if a late
|
|
196
|
+
// task_started says skip_transcript=true.
|
|
197
|
+
task.panelOnlyRecovery = true;
|
|
198
|
+
task.showInTranscript = false;
|
|
199
|
+
}
|
|
200
|
+
this.mergeLevel(task, item);
|
|
201
|
+
await this.announce(task);
|
|
202
|
+
}
|
|
203
|
+
for (const task of this.tasks.values()) {
|
|
204
|
+
if (task.announced && !task.ignored && !isTerminal(task.state) && !live.has(task.id)) {
|
|
205
|
+
// This replace-semantics level is itself the authoritative liveness
|
|
206
|
+
// boundary. Close immediately so a lost terminal edge cannot leave a
|
|
207
|
+
// permanent running card; a following task_notification may correct
|
|
208
|
+
// the best-effort stopped state to completed/failed.
|
|
209
|
+
await this.finish(task, "stopped", undefined, "level");
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
async finishAll(state) {
|
|
214
|
+
const errors = [];
|
|
215
|
+
for (const task of this.tasks.values()) {
|
|
216
|
+
if (task.announced && !task.ignored && !isTerminal(task.state)) {
|
|
217
|
+
try {
|
|
218
|
+
await this.finish(task, state, undefined, "shutdown");
|
|
219
|
+
}
|
|
220
|
+
catch (error) {
|
|
221
|
+
errors.push(error);
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
if (errors.length === 1)
|
|
226
|
+
throw errors[0];
|
|
227
|
+
if (errors.length > 1)
|
|
228
|
+
throw new AggregateError(errors, "Failed to finish async tasks");
|
|
229
|
+
}
|
|
230
|
+
canStop(taskId) {
|
|
231
|
+
const task = this.tasks.get(taskId);
|
|
232
|
+
return task?.announced === true && !task.ignored && !task.stopping && !isTerminal(task.state);
|
|
233
|
+
}
|
|
234
|
+
claimStop(taskId) {
|
|
235
|
+
if (!this.canStop(taskId))
|
|
236
|
+
return false;
|
|
237
|
+
this.tasks.get(taskId).stopping = true;
|
|
238
|
+
return true;
|
|
239
|
+
}
|
|
240
|
+
releaseStop(taskId) {
|
|
241
|
+
const task = this.tasks.get(taskId);
|
|
242
|
+
if (task && !isTerminal(task.state))
|
|
243
|
+
task.stopping = false;
|
|
244
|
+
}
|
|
245
|
+
async taskStopped(taskId) {
|
|
246
|
+
const task = this.tasks.get(taskId);
|
|
247
|
+
if (!task || !task.announced || task.ignored || task.stopAnnounced)
|
|
248
|
+
return;
|
|
249
|
+
task.stopAnnounced = true;
|
|
250
|
+
// No terminal summary: a stopped task leaves the Async Tasks panel at once,
|
|
251
|
+
// so anything said there is said to nobody.
|
|
252
|
+
await this.finish(task, "stopped", undefined, "event");
|
|
253
|
+
// The transcript is where the acknowledgement has to land, and it is the
|
|
254
|
+
// only one the user gets -- the SDK injects nothing into the model's
|
|
255
|
+
// context for a stopped shell task.
|
|
256
|
+
//
|
|
257
|
+
// Terminal state deliberately does not gate this. The SDK's own
|
|
258
|
+
// `task_notification` routinely wins the race against the `stopTask`
|
|
259
|
+
// response that brought us here, so by now the task is usually already
|
|
260
|
+
// terminal: gating on that would silence the very stop the user asked for.
|
|
261
|
+
// `stopAnnounced` is what keeps this to one line per task.
|
|
262
|
+
//
|
|
263
|
+
// `showInTranscript` does not gate it either: that flag decides whether the
|
|
264
|
+
// task owns a transcript *card* (a background Bash task is already drawn as
|
|
265
|
+
// its tool call), not whether the agent may answer a direct user action.
|
|
266
|
+
await this.publish({
|
|
267
|
+
sessionId: this.sessionId,
|
|
268
|
+
update: {
|
|
269
|
+
sessionUpdate: "agent_message_chunk",
|
|
270
|
+
content: { type: "text", text: `**Task stopped by user:** ${task.name}.` },
|
|
271
|
+
},
|
|
272
|
+
});
|
|
273
|
+
}
|
|
274
|
+
clear() {
|
|
275
|
+
this.tasks.clear();
|
|
276
|
+
}
|
|
277
|
+
task(taskId) {
|
|
278
|
+
let task = this.tasks.get(taskId);
|
|
279
|
+
if (task)
|
|
280
|
+
return task;
|
|
281
|
+
task = {
|
|
282
|
+
id: taskId,
|
|
283
|
+
name: "Background task",
|
|
284
|
+
taskType: "task",
|
|
285
|
+
description: "Background task",
|
|
286
|
+
showInTranscript: true,
|
|
287
|
+
announced: false,
|
|
288
|
+
ignored: false,
|
|
289
|
+
startedObserved: false,
|
|
290
|
+
panelOnlyRecovery: false,
|
|
291
|
+
stopping: false,
|
|
292
|
+
stopAnnounced: false,
|
|
293
|
+
state: "running",
|
|
294
|
+
};
|
|
295
|
+
this.tasks.set(taskId, task);
|
|
296
|
+
return task;
|
|
297
|
+
}
|
|
298
|
+
mergeStarted(task, message) {
|
|
299
|
+
task.startedObserved = true;
|
|
300
|
+
const rawTaskType = field(message, "taskType", "task_type");
|
|
301
|
+
if (rawTaskType !== undefined)
|
|
302
|
+
task.taskType = friendlyTaskType(rawTaskType);
|
|
303
|
+
const description = nonBlankString(message.description);
|
|
304
|
+
if (description)
|
|
305
|
+
task.description = description;
|
|
306
|
+
else if (task.description === "Background task")
|
|
307
|
+
task.description = defaultDescription(task.taskType);
|
|
308
|
+
task.name = nonBlankString(field(message, "workflowName", "workflow_name")) ?? task.description;
|
|
309
|
+
const skipTranscript = field(message, "skipTranscript", "skip_transcript");
|
|
310
|
+
if (task.panelOnlyRecovery) {
|
|
311
|
+
// A level-only recovery was already announced without a transcript
|
|
312
|
+
// entry. Keep that decision monotonic: a late task_started can enrich
|
|
313
|
+
// the panel, but cannot retroactively create or mutate transcript UI.
|
|
314
|
+
task.showInTranscript = false;
|
|
315
|
+
}
|
|
316
|
+
else if (skipTranscript !== undefined) {
|
|
317
|
+
task.showInTranscript = skipTranscript !== true;
|
|
318
|
+
}
|
|
319
|
+
this.mergeOutputFilePath(task, message);
|
|
320
|
+
const toolCallId = nonBlankString(field(message, "toolCallId", "tool_use_id"));
|
|
321
|
+
if (toolCallId)
|
|
322
|
+
task.toolCallId = toolCallId;
|
|
323
|
+
}
|
|
324
|
+
mergePatch(task, patch) {
|
|
325
|
+
const description = nonBlankString(patch.description);
|
|
326
|
+
if (description) {
|
|
327
|
+
task.description = description;
|
|
328
|
+
task.name = description;
|
|
329
|
+
}
|
|
330
|
+
this.mergeOutputFilePath(task, patch);
|
|
331
|
+
}
|
|
332
|
+
mergeLevel(task, level) {
|
|
333
|
+
const rawTaskType = field(level, "taskType", "task_type");
|
|
334
|
+
if (rawTaskType !== undefined)
|
|
335
|
+
task.taskType = friendlyTaskType(rawTaskType);
|
|
336
|
+
const description = nonBlankString(level.description);
|
|
337
|
+
if (description) {
|
|
338
|
+
task.description = description;
|
|
339
|
+
task.name = description;
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
mergeOutputFilePath(task, value) {
|
|
343
|
+
const outputFilePath = nonBlankString(field(value, "outputFilePath", "output_file"));
|
|
344
|
+
if (outputFilePath)
|
|
345
|
+
task.outputFilePath = outputFilePath;
|
|
346
|
+
}
|
|
347
|
+
async announce(task) {
|
|
348
|
+
if (task.announced || task.ignored)
|
|
349
|
+
return;
|
|
350
|
+
await this.publish({
|
|
351
|
+
sessionId: this.sessionId,
|
|
352
|
+
update: {
|
|
353
|
+
sessionUpdate: "async_task_spawned",
|
|
354
|
+
asyncTaskId: task.id,
|
|
355
|
+
name: task.name,
|
|
356
|
+
taskType: task.taskType,
|
|
357
|
+
description: task.description,
|
|
358
|
+
showInTranscript: task.showInTranscript,
|
|
359
|
+
canStop: true,
|
|
360
|
+
...(task.outputFilePath ? { outputFilePath: task.outputFilePath } : {}),
|
|
361
|
+
...(task.toolCallId ? { toolCallId: task.toolCallId } : {}),
|
|
362
|
+
},
|
|
363
|
+
});
|
|
364
|
+
task.announced = true;
|
|
365
|
+
if (isTerminal(task.state)) {
|
|
366
|
+
await this.publishState(task, task.state, task.terminalSummary);
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
async finish(task, state, summary, source) {
|
|
370
|
+
if (task.ignored)
|
|
371
|
+
return;
|
|
372
|
+
if (isTerminal(task.state)) {
|
|
373
|
+
// A level event may precede the authoritative terminal edge. Correct its
|
|
374
|
+
// best-effort stopped state if that edge later arrives.
|
|
375
|
+
if (task.terminalSource !== "level" || source !== "event")
|
|
376
|
+
return;
|
|
377
|
+
}
|
|
378
|
+
const previous = {
|
|
379
|
+
state: task.state,
|
|
380
|
+
terminalSummary: task.terminalSummary,
|
|
381
|
+
terminalSource: task.terminalSource,
|
|
382
|
+
};
|
|
383
|
+
task.state = state;
|
|
384
|
+
task.terminalSummary = summary;
|
|
385
|
+
task.terminalSource = source;
|
|
386
|
+
try {
|
|
387
|
+
if (task.announced)
|
|
388
|
+
await this.publishState(task, state, summary);
|
|
389
|
+
}
|
|
390
|
+
catch (error) {
|
|
391
|
+
task.state = previous.state;
|
|
392
|
+
task.terminalSummary = previous.terminalSummary;
|
|
393
|
+
task.terminalSource = previous.terminalSource;
|
|
394
|
+
throw error;
|
|
395
|
+
}
|
|
396
|
+
}
|
|
397
|
+
async publishMetadata(task) {
|
|
398
|
+
if (task.outputFilePath || task.toolCallId) {
|
|
399
|
+
if (isTerminal(task.state)) {
|
|
400
|
+
await this.publishState(task, task.state, task.terminalSummary);
|
|
401
|
+
}
|
|
402
|
+
else {
|
|
403
|
+
await this.publishProgress(task, {
|
|
404
|
+
...(task.outputFilePath ? { outputFilePath: task.outputFilePath } : {}),
|
|
405
|
+
...(task.toolCallId ? { toolCallId: task.toolCallId } : {}),
|
|
406
|
+
});
|
|
407
|
+
}
|
|
408
|
+
}
|
|
409
|
+
}
|
|
410
|
+
async publishProgress(task, update) {
|
|
411
|
+
await this.publish({
|
|
412
|
+
sessionId: this.sessionId,
|
|
413
|
+
update: {
|
|
414
|
+
sessionUpdate: "async_task_progress",
|
|
415
|
+
asyncTaskId: task.id,
|
|
416
|
+
...update,
|
|
417
|
+
},
|
|
418
|
+
});
|
|
419
|
+
}
|
|
420
|
+
async publishState(task, state, summary) {
|
|
421
|
+
await this.publish({
|
|
422
|
+
sessionId: this.sessionId,
|
|
423
|
+
update: {
|
|
424
|
+
sessionUpdate: "async_task_state_update",
|
|
425
|
+
asyncTaskId: task.id,
|
|
426
|
+
state,
|
|
427
|
+
...(summary ? { summary } : {}),
|
|
428
|
+
...(task.outputFilePath ? { outputFilePath: task.outputFilePath } : {}),
|
|
429
|
+
...(task.toolCallId ? { toolCallId: task.toolCallId } : {}),
|
|
430
|
+
},
|
|
431
|
+
});
|
|
432
|
+
}
|
|
433
|
+
}
|
|
434
|
+
/** Recovers background Bash lifecycle data exposed only on its tool result. */
|
|
435
|
+
export function backgroundBashTaskFromToolResult(content, toolUseResult, toolUses) {
|
|
436
|
+
if (!Array.isArray(content))
|
|
437
|
+
return undefined;
|
|
438
|
+
const toolResults = content.flatMap(toolResultBlock);
|
|
439
|
+
const bashResults = toolResults.filter((result) => toolUses[result.toolUseId]?.name === "Bash");
|
|
440
|
+
if (bashResults.length === 0)
|
|
441
|
+
return undefined;
|
|
442
|
+
const backgroundResults = structuredBackgroundResults(toolUseResult, toolUses);
|
|
443
|
+
const distinctTaskIds = new Set([
|
|
444
|
+
...backgroundResults.map((result) => result.taskId),
|
|
445
|
+
...bashResults
|
|
446
|
+
.map((result) => backgroundTaskIdFromText(result.content))
|
|
447
|
+
.filter((taskId) => taskId !== undefined),
|
|
448
|
+
]);
|
|
449
|
+
if (distinctTaskIds.size !== 1)
|
|
450
|
+
return undefined;
|
|
451
|
+
const taskId = [...distinctTaskIds][0];
|
|
452
|
+
if (!taskId)
|
|
453
|
+
return undefined;
|
|
454
|
+
const hintedToolUseIds = new Set(backgroundResults
|
|
455
|
+
.filter((result) => result.taskId === taskId && result.toolUseId)
|
|
456
|
+
.map((result) => result.toolUseId));
|
|
457
|
+
let matches;
|
|
458
|
+
if (hintedToolUseIds.size > 0) {
|
|
459
|
+
matches = bashResults.filter((result) => hintedToolUseIds.has(result.toolUseId));
|
|
460
|
+
}
|
|
461
|
+
else {
|
|
462
|
+
const pathMatches = bashResults.filter((result) => asyncTaskOutputFilePath(result.content, taskId) !== undefined);
|
|
463
|
+
matches = pathMatches.length > 0 ? pathMatches : bashResults.length === 1 ? bashResults : [];
|
|
464
|
+
}
|
|
465
|
+
if (matches.length !== 1)
|
|
466
|
+
return undefined;
|
|
467
|
+
const match = matches[0];
|
|
468
|
+
const toolUse = toolUses[match.toolUseId];
|
|
469
|
+
const input = isRecord(toolUse?.input) ? toolUse.input : undefined;
|
|
470
|
+
return {
|
|
471
|
+
taskId,
|
|
472
|
+
taskType: "local_bash",
|
|
473
|
+
description: nonBlankString(input?.command),
|
|
474
|
+
isBackgrounded: true,
|
|
475
|
+
outputFilePath: asyncTaskOutputFilePath(match.content, taskId),
|
|
476
|
+
toolCallId: match.toolUseId,
|
|
477
|
+
};
|
|
478
|
+
}
|
|
479
|
+
function backgroundTaskIdFromText(content) {
|
|
480
|
+
const text = textContent(content);
|
|
481
|
+
if (!text)
|
|
482
|
+
return undefined;
|
|
483
|
+
const marker = "Command running in background with ID: ";
|
|
484
|
+
const start = text.indexOf(marker);
|
|
485
|
+
if (start < 0)
|
|
486
|
+
return undefined;
|
|
487
|
+
const valueStart = start + marker.length;
|
|
488
|
+
const valueEnd = text.indexOf(".", valueStart);
|
|
489
|
+
return valueEnd < 0 ? undefined : nonBlankString(text.slice(valueStart, valueEnd));
|
|
490
|
+
}
|
|
491
|
+
function structuredBackgroundResults(value, toolUses, hintedToolUseId, seen = new Set()) {
|
|
492
|
+
if (typeof value !== "object" || value === null || seen.has(value))
|
|
493
|
+
return [];
|
|
494
|
+
seen.add(value);
|
|
495
|
+
if (Array.isArray(value)) {
|
|
496
|
+
return value.flatMap((item) => structuredBackgroundResults(item, toolUses, hintedToolUseId, seen));
|
|
497
|
+
}
|
|
498
|
+
const directTaskId = nonBlankString(field(value, "backgroundTaskId", "background_task_id"));
|
|
499
|
+
const directToolUseId = nonBlankString(field(value, "toolUseId", "tool_use_id")) ?? hintedToolUseId;
|
|
500
|
+
const results = directTaskId
|
|
501
|
+
? [{ taskId: directTaskId, toolUseId: directToolUseId }]
|
|
502
|
+
: [];
|
|
503
|
+
for (const [key, child] of Object.entries(value)) {
|
|
504
|
+
if (key === "backgroundTaskId" || key === "background_task_id")
|
|
505
|
+
continue;
|
|
506
|
+
results.push(...structuredBackgroundResults(child, toolUses, toolUses[key] ? key : directToolUseId, seen));
|
|
507
|
+
}
|
|
508
|
+
return results;
|
|
509
|
+
}
|
|
510
|
+
function toolResultBlock(value) {
|
|
511
|
+
if (!isRecord(value) || value.type !== "tool_result")
|
|
512
|
+
return [];
|
|
513
|
+
const toolUseId = nonBlankString(field(value, "toolUseId", "tool_use_id"));
|
|
514
|
+
return toolUseId ? [{ toolUseId, content: value.content }] : [];
|
|
515
|
+
}
|
|
516
|
+
function asyncTaskOutputFilePath(content, taskId) {
|
|
517
|
+
const text = textContent(content);
|
|
518
|
+
if (!text)
|
|
519
|
+
return undefined;
|
|
520
|
+
const marker = "Output is being written to: ";
|
|
521
|
+
const start = text.indexOf(marker);
|
|
522
|
+
if (start < 0)
|
|
523
|
+
return undefined;
|
|
524
|
+
const valueStart = start + marker.length;
|
|
525
|
+
const valueEnd = text.indexOf(". You will be notified", valueStart);
|
|
526
|
+
if (valueEnd < 0)
|
|
527
|
+
return undefined;
|
|
528
|
+
const path = text.slice(valueStart, valueEnd).trim();
|
|
529
|
+
const expectedPosixSuffix = `/tasks/${taskId}.output`;
|
|
530
|
+
const expectedWindowsSuffix = `\\tasks\\${taskId}.output`;
|
|
531
|
+
return path.endsWith(expectedPosixSuffix) || path.endsWith(expectedWindowsSuffix)
|
|
532
|
+
? path
|
|
533
|
+
: undefined;
|
|
534
|
+
}
|
|
535
|
+
function textContent(value) {
|
|
536
|
+
if (typeof value === "string")
|
|
537
|
+
return value;
|
|
538
|
+
if (Array.isArray(value)) {
|
|
539
|
+
const parts = value.map(textContent).filter((part) => part !== undefined);
|
|
540
|
+
return parts.length > 0 ? parts.join("") : undefined;
|
|
541
|
+
}
|
|
542
|
+
if (!isRecord(value))
|
|
543
|
+
return undefined;
|
|
544
|
+
if (typeof value.text === "string")
|
|
545
|
+
return value.text;
|
|
546
|
+
return value.content === value ? undefined : textContent(value.content);
|
|
547
|
+
}
|
|
548
|
+
function friendlyTaskType(value) {
|
|
549
|
+
if (value === "local_bash")
|
|
550
|
+
return "shell";
|
|
551
|
+
if (value === "local_workflow")
|
|
552
|
+
return "workflow";
|
|
553
|
+
if (value === "local_monitor" || value === "mcp")
|
|
554
|
+
return "monitor";
|
|
555
|
+
return nonBlankString(value) ?? "task";
|
|
556
|
+
}
|
|
557
|
+
function defaultDescription(taskType) {
|
|
558
|
+
return taskType === "task" ? "Background task" : taskType[0].toUpperCase() + taskType.slice(1);
|
|
559
|
+
}
|
|
560
|
+
function isBackgroundTask(isBackgrounded, taskType) {
|
|
561
|
+
if (isBackgrounded === true)
|
|
562
|
+
return true;
|
|
563
|
+
if (isBackgrounded === false)
|
|
564
|
+
return false;
|
|
565
|
+
return taskType !== "local_bash" && taskType !== "local_agent";
|
|
566
|
+
}
|
|
567
|
+
function isSubagentTask(message) {
|
|
568
|
+
return (field(message, "subagentType", "subagent_type") !== undefined ||
|
|
569
|
+
field(message, "taskType", "task_type") === "local_agent");
|
|
570
|
+
}
|
|
571
|
+
function taskState(value) {
|
|
572
|
+
if (value === "pending" || value === "running")
|
|
573
|
+
return "running";
|
|
574
|
+
if (value === "paused")
|
|
575
|
+
return "paused";
|
|
576
|
+
if (value === "completed")
|
|
577
|
+
return "completed";
|
|
578
|
+
if (value === "failed")
|
|
579
|
+
return "failed";
|
|
580
|
+
if (value === "killed" || value === "cancelled" || value === "stopped")
|
|
581
|
+
return "stopped";
|
|
582
|
+
return undefined;
|
|
583
|
+
}
|
|
584
|
+
function isTerminal(state) {
|
|
585
|
+
return state === "completed" || state === "failed" || state === "stopped";
|
|
586
|
+
}
|
|
587
|
+
function taskUsage(value) {
|
|
588
|
+
if (!isRecord(value))
|
|
589
|
+
return undefined;
|
|
590
|
+
const totalTokens = numberField(value, "totalTokens", "total_tokens");
|
|
591
|
+
const toolUses = numberField(value, "toolUses", "tool_uses");
|
|
592
|
+
const durationMs = numberField(value, "durationMs", "duration_ms");
|
|
593
|
+
if (totalTokens === undefined || toolUses === undefined || durationMs === undefined)
|
|
594
|
+
return undefined;
|
|
595
|
+
return { totalTokens, toolUses, durationMs };
|
|
596
|
+
}
|
|
597
|
+
function taskIdOf(value) {
|
|
598
|
+
return nonBlankString(field(value, "taskId", "task_id"));
|
|
599
|
+
}
|
|
600
|
+
function field(value, camelCase, snakeCase) {
|
|
601
|
+
const record = value;
|
|
602
|
+
return record[camelCase] !== undefined ? record[camelCase] : record[snakeCase];
|
|
603
|
+
}
|
|
604
|
+
function numberField(value, camelCase, snakeCase) {
|
|
605
|
+
const result = field(value, camelCase, snakeCase);
|
|
606
|
+
return typeof result === "number" ? result : undefined;
|
|
607
|
+
}
|
|
608
|
+
function nonBlankString(value) {
|
|
609
|
+
return typeof value === "string" && value.trim().length > 0 ? value.trim() : undefined;
|
|
610
|
+
}
|
|
611
|
+
function isRecord(value) {
|
|
612
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
613
|
+
}
|