viberoom 0.3.0 → 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.
Files changed (64) hide show
  1. package/NOTICE +12 -0
  2. package/README.md +119 -96
  3. package/dist/hub.js +18 -0
  4. package/dist/launcher.js +22 -0
  5. package/dist/main.js +4 -3
  6. package/dist/persona.js +2 -2
  7. package/dist/recipes.js +37 -14
  8. package/dist/room.js +3 -2
  9. package/dist/server.js +16 -3
  10. package/package.json +8 -5
  11. package/scripts/vendor-acp.mjs +85 -0
  12. package/ui/app.css +7 -0
  13. package/ui/app.js +116 -26
  14. package/ui/index.html +2 -0
  15. package/vendor/acp/claude-agent-acp/LICENSE +191 -0
  16. package/vendor/acp/claude-agent-acp/dist/acp-agent.js +7694 -0
  17. package/vendor/acp/claude-agent-acp/dist/acp-subagents.js +13 -0
  18. package/vendor/acp/claude-agent-acp/dist/air-extension.js +63 -0
  19. package/vendor/acp/claude-agent-acp/dist/async-tasks.js +613 -0
  20. package/vendor/acp/claude-agent-acp/dist/clear-context-coordinator.js +80 -0
  21. package/vendor/acp/claude-agent-acp/dist/elicitation.js +304 -0
  22. package/vendor/acp/claude-agent-acp/dist/exit-plan.js +154 -0
  23. package/vendor/acp/claude-agent-acp/dist/file-change-audit.js +350 -0
  24. package/vendor/acp/claude-agent-acp/dist/fork-session.js +41 -0
  25. package/vendor/acp/claude-agent-acp/dist/goal-extension.js +50 -0
  26. package/vendor/acp/claude-agent-acp/dist/index.js +98 -0
  27. package/vendor/acp/claude-agent-acp/dist/lib.js +5 -0
  28. package/vendor/acp/claude-agent-acp/dist/native-subagents.js +422 -0
  29. package/vendor/acp/claude-agent-acp/dist/permissions/effects.js +166 -0
  30. package/vendor/acp/claude-agent-acp/dist/permissions/modes.js +41 -0
  31. package/vendor/acp/claude-agent-acp/dist/permissions/normalization.js +100 -0
  32. package/vendor/acp/claude-agent-acp/dist/permissions/options/filesystem.js +124 -0
  33. package/vendor/acp/claude-agent-acp/dist/permissions/options/shared.js +64 -0
  34. package/vendor/acp/claude-agent-acp/dist/permissions/options/shell.js +100 -0
  35. package/vendor/acp/claude-agent-acp/dist/permissions/options/tools.js +135 -0
  36. package/vendor/acp/claude-agent-acp/dist/permissions/options.js +60 -0
  37. package/vendor/acp/claude-agent-acp/dist/permissions/presentation.js +83 -0
  38. package/vendor/acp/claude-agent-acp/dist/permissions/response.js +19 -0
  39. package/vendor/acp/claude-agent-acp/dist/session-config-ids.js +4 -0
  40. package/vendor/acp/claude-agent-acp/dist/session-failure-extension.js +324 -0
  41. package/vendor/acp/claude-agent-acp/dist/session-mode.js +234 -0
  42. package/vendor/acp/claude-agent-acp/dist/session-titles.js +199 -0
  43. package/vendor/acp/claude-agent-acp/dist/settings.js +185 -0
  44. package/vendor/acp/claude-agent-acp/dist/tool-result-meta.js +19 -0
  45. package/vendor/acp/claude-agent-acp/dist/tools.js +1235 -0
  46. package/vendor/acp/claude-agent-acp/dist/utils.js +81 -0
  47. package/vendor/acp/claude-agent-acp/package.json +7 -0
  48. package/vendor/acp/claude-agent-sdk/LICENSE.md +1 -0
  49. package/vendor/acp/claude-agent-sdk/agentSdkTypes.d.ts +1 -0
  50. package/vendor/acp/claude-agent-sdk/bridge.d.ts +378 -0
  51. package/vendor/acp/claude-agent-sdk/bridge.mjs +221 -0
  52. package/vendor/acp/claude-agent-sdk/browser-sdk.d.ts +107 -0
  53. package/vendor/acp/claude-agent-sdk/browser-sdk.js +185 -0
  54. package/vendor/acp/claude-agent-sdk/extractFromBunfs.d.ts +1 -0
  55. package/vendor/acp/claude-agent-sdk/extractFromBunfs.js +156 -0
  56. package/vendor/acp/claude-agent-sdk/manifest.json +65 -0
  57. package/vendor/acp/claude-agent-sdk/manifest.zst.json +73 -0
  58. package/vendor/acp/claude-agent-sdk/package.json +7 -0
  59. package/vendor/acp/claude-agent-sdk/sdk-tools.d.ts +4129 -0
  60. package/vendor/acp/claude-agent-sdk/sdk.d.ts +8687 -0
  61. package/vendor/acp/claude-agent-sdk/sdk.mjs +204 -0
  62. package/vendor/acp/codex-acp/LICENSE +190 -0
  63. package/vendor/acp/codex-acp/dist/index.js +34238 -0
  64. 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
+ }