@aparte/plugin-subagents 0.0.0-stage → 1.7.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/LICENSE +21 -0
- package/README.md +89 -2
- package/dist/aparte-subagent-view.d.ts +150 -0
- package/dist/aparte-subagent-view.d.ts.map +1 -0
- package/dist/connect-65jiVmf_.js +1469 -0
- package/dist/connect-65jiVmf_.js.map +1 -0
- package/dist/connect.d.ts +44 -0
- package/dist/connect.d.ts.map +1 -0
- package/dist/custom-elements.json +2057 -0
- package/dist/drawing.d.ts +52 -0
- package/dist/drawing.d.ts.map +1 -0
- package/dist/index.d.ts +23 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +11 -0
- package/dist/index.js.map +1 -0
- package/dist/index.node.d.ts +18 -0
- package/dist/index.node.d.ts.map +1 -0
- package/dist/index.node.js +10 -0
- package/dist/index.node.js.map +1 -0
- package/dist/lanes.d.ts +44 -0
- package/dist/lanes.d.ts.map +1 -0
- package/dist/line.d.ts +25 -0
- package/dist/line.d.ts.map +1 -0
- package/dist/open.d.ts +34 -0
- package/dist/open.d.ts.map +1 -0
- package/dist/segment.d.ts +20 -0
- package/dist/segment.d.ts.map +1 -0
- package/dist/setup.d.ts +23 -0
- package/dist/setup.d.ts.map +1 -0
- package/dist/status.d.ts +24 -0
- package/dist/status.d.ts.map +1 -0
- package/dist/steps.d.ts +22 -0
- package/dist/steps.d.ts.map +1 -0
- package/dist/styles.d.ts +5 -0
- package/dist/styles.d.ts.map +1 -0
- package/dist/transcript.d.ts +28 -0
- package/dist/transcript.d.ts.map +1 -0
- package/dist/types.d.ts +131 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/words.d.ts +88 -0
- package/dist/words.d.ts.map +1 -0
- package/package.json +92 -3
|
@@ -0,0 +1,1469 @@
|
|
|
1
|
+
import { createSegmentList, createStreamAdapter, runWithConfig, getSegmentRenderer, installDefaultRenderersOnce, pluralize, deferHydrationOf, onRenderersChange, subscribeConfigChange, resolveConfig, contextConfig, formatDuration, segmentTiming, aparteGlobalConfig, registerSegmentRenderer, unregisterSegmentRenderer } from "@aparte/core";
|
|
2
|
+
const SUBAGENT_SEGMENT_TYPE = "subagent";
|
|
3
|
+
const STATUSES = /* @__PURE__ */ new Set(["running", "waiting", "done", "failed", "stopped"]);
|
|
4
|
+
function subagentOf(segment) {
|
|
5
|
+
const value = segment.subagent;
|
|
6
|
+
if (!value || typeof value !== "object") return void 0;
|
|
7
|
+
const sub = value;
|
|
8
|
+
if (typeof sub.id !== "string") return void 0;
|
|
9
|
+
if (segment.type !== "tool_call" && segment.type !== SUBAGENT_SEGMENT_TYPE) return void 0;
|
|
10
|
+
return {
|
|
11
|
+
...sub,
|
|
12
|
+
id: sub.id,
|
|
13
|
+
status: STATUSES.has(sub.status) ? sub.status : "stopped",
|
|
14
|
+
segments: Array.isArray(sub.segments) ? sub.segments : []
|
|
15
|
+
};
|
|
16
|
+
}
|
|
17
|
+
function anchorSettled(segment) {
|
|
18
|
+
if (segment.type === "tool_call") {
|
|
19
|
+
const status = segment.status;
|
|
20
|
+
return status !== "pending" && status !== "awaiting-approval";
|
|
21
|
+
}
|
|
22
|
+
return segment.isStreaming !== true;
|
|
23
|
+
}
|
|
24
|
+
function subagentStatus(segment) {
|
|
25
|
+
const sub = subagentOf(segment);
|
|
26
|
+
if (!sub) return "stopped";
|
|
27
|
+
if ((sub.status === "running" || sub.status === "waiting") && anchorSettled(segment)) return "stopped";
|
|
28
|
+
return sub.status;
|
|
29
|
+
}
|
|
30
|
+
function statusOfCall(status) {
|
|
31
|
+
switch (status ?? "pending") {
|
|
32
|
+
case "pending":
|
|
33
|
+
return "running";
|
|
34
|
+
case "awaiting-approval":
|
|
35
|
+
return "waiting";
|
|
36
|
+
case "resolved":
|
|
37
|
+
return "done";
|
|
38
|
+
case "failed":
|
|
39
|
+
return "failed";
|
|
40
|
+
default:
|
|
41
|
+
return "stopped";
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
function callStatusOf(status) {
|
|
45
|
+
switch (status) {
|
|
46
|
+
case "running":
|
|
47
|
+
return "pending";
|
|
48
|
+
case "waiting":
|
|
49
|
+
return "awaiting-approval";
|
|
50
|
+
case "done":
|
|
51
|
+
return "resolved";
|
|
52
|
+
case "failed":
|
|
53
|
+
return "failed";
|
|
54
|
+
default:
|
|
55
|
+
return "aborted";
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
const open = /* @__PURE__ */ new Map();
|
|
59
|
+
const keyOf$1 = (messageId, agentId) => `${messageId}\0${agentId}`;
|
|
60
|
+
function stepStarted(messageId, agentId, name) {
|
|
61
|
+
const key = keyOf$1(messageId, agentId);
|
|
62
|
+
open.set(key, [...open.get(key) ?? [], name]);
|
|
63
|
+
}
|
|
64
|
+
function stepEnded(messageId, agentId, name) {
|
|
65
|
+
const key = keyOf$1(messageId, agentId);
|
|
66
|
+
const steps = open.get(key);
|
|
67
|
+
if (!steps) return;
|
|
68
|
+
const at = steps.lastIndexOf(name);
|
|
69
|
+
if (at < 0) return;
|
|
70
|
+
const rest = steps.filter((_, i) => i !== at);
|
|
71
|
+
if (rest.length > 0) open.set(key, rest);
|
|
72
|
+
else open.delete(key);
|
|
73
|
+
}
|
|
74
|
+
function stepsEnded(messageId, agentId) {
|
|
75
|
+
open.delete(keyOf$1(messageId, agentId));
|
|
76
|
+
}
|
|
77
|
+
function currentStep(messageId, agentId) {
|
|
78
|
+
if (messageId !== void 0) return open.get(keyOf$1(messageId, agentId))?.at(-1);
|
|
79
|
+
const suffix = `\0${agentId}`;
|
|
80
|
+
for (const [key, steps] of open) if (key.endsWith(suffix)) return steps.at(-1);
|
|
81
|
+
return void 0;
|
|
82
|
+
}
|
|
83
|
+
const MAX_RUNS = 64;
|
|
84
|
+
const MAX_LANES = 512;
|
|
85
|
+
const MAX_EARLY = 2e3;
|
|
86
|
+
const COALESCED = /* @__PURE__ */ new Set(["text-delta", "thinking-delta", "sources", "text-flush", "turn-start"]);
|
|
87
|
+
const isOpen = (status) => (status ?? "pending") === "pending" || status === "awaiting-approval";
|
|
88
|
+
const lastWrites = /* @__PURE__ */ new Map();
|
|
89
|
+
const writeKey = (messageId, agentId) => `${messageId}\0${agentId}`;
|
|
90
|
+
function lastLaneWrite(messageId, agentId) {
|
|
91
|
+
return lastWrites.get(writeKey(messageId, agentId));
|
|
92
|
+
}
|
|
93
|
+
const laneWatchers = /* @__PURE__ */ new Set();
|
|
94
|
+
function watchLaneWrites(fn) {
|
|
95
|
+
laneWatchers.add(fn);
|
|
96
|
+
return () => {
|
|
97
|
+
laneWatchers.delete(fn);
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
const claims = /* @__PURE__ */ new WeakMap();
|
|
101
|
+
function claim(target, messageId, owner) {
|
|
102
|
+
let byMessage = claims.get(target);
|
|
103
|
+
if (!byMessage) claims.set(target, byMessage = /* @__PURE__ */ new Map());
|
|
104
|
+
const holder2 = byMessage.get(messageId);
|
|
105
|
+
if (holder2) return holder2 === owner;
|
|
106
|
+
byMessage.set(messageId, owner);
|
|
107
|
+
return true;
|
|
108
|
+
}
|
|
109
|
+
function release(target, messageId, owner) {
|
|
110
|
+
const byMessage = claims.get(target);
|
|
111
|
+
if (byMessage?.get(messageId) === owner) byMessage.delete(messageId);
|
|
112
|
+
}
|
|
113
|
+
function nextFrame(fn) {
|
|
114
|
+
if (typeof requestAnimationFrame === "function") {
|
|
115
|
+
const handle2 = requestAnimationFrame(fn);
|
|
116
|
+
return () => cancelAnimationFrame(handle2);
|
|
117
|
+
}
|
|
118
|
+
const handle = setTimeout(fn, 16);
|
|
119
|
+
return () => clearTimeout(handle);
|
|
120
|
+
}
|
|
121
|
+
function stopFrame(run) {
|
|
122
|
+
const cancel = run.cancelFrame;
|
|
123
|
+
run.cancelFrame = null;
|
|
124
|
+
cancel?.();
|
|
125
|
+
}
|
|
126
|
+
const hostKey = (host, toolCallId) => `${host?.id ?? ""}\0${toolCallId}`;
|
|
127
|
+
const ownSegmentId = (agentId) => `subagent-${agentId}`;
|
|
128
|
+
function freeOwnId(segments, agentId) {
|
|
129
|
+
const base = ownSegmentId(agentId);
|
|
130
|
+
let id = base;
|
|
131
|
+
for (let n = 2; segments?.some((s) => s.id === id); n += 1) id = `${base}-${n}`;
|
|
132
|
+
return id;
|
|
133
|
+
}
|
|
134
|
+
function createLanes() {
|
|
135
|
+
const owner = {};
|
|
136
|
+
const runs = /* @__PURE__ */ new Map();
|
|
137
|
+
const over = /* @__PURE__ */ new Set();
|
|
138
|
+
const replySegments = (run) => {
|
|
139
|
+
const t = run.target;
|
|
140
|
+
try {
|
|
141
|
+
const message = t.getMessage?.(run.messageId) ?? t.getMessages?.()?.find((m) => m.id === run.messageId);
|
|
142
|
+
return message ? message.segments ?? [] : void 0;
|
|
143
|
+
} catch {
|
|
144
|
+
return void 0;
|
|
145
|
+
}
|
|
146
|
+
};
|
|
147
|
+
const hostSegments = (run, host) => host ? host.list.segments : replySegments(run);
|
|
148
|
+
const anchorOf = (run, lane) => hostSegments(run, lane.host)?.find((s) => s.id === lane.anchorId);
|
|
149
|
+
const isWaiting = (run, lane) => {
|
|
150
|
+
if (lane.status === "waiting" || lane.asking.size > 0) return true;
|
|
151
|
+
for (const other of run.lanes.values()) {
|
|
152
|
+
if (other.host === lane && (other.status === "running" || other.status === "waiting") && isWaiting(run, other)) return true;
|
|
153
|
+
}
|
|
154
|
+
return false;
|
|
155
|
+
};
|
|
156
|
+
const snapshot = (run, lane) => ({
|
|
157
|
+
id: lane.id,
|
|
158
|
+
...lane.name !== void 0 ? { name: lane.name } : {},
|
|
159
|
+
...lane.description !== void 0 ? { description: lane.description } : {},
|
|
160
|
+
status: lane.status === "running" && isWaiting(run, lane) ? "waiting" : lane.status,
|
|
161
|
+
...lane.result !== void 0 ? { result: lane.result } : {},
|
|
162
|
+
...lane.error !== void 0 ? { error: lane.error } : {},
|
|
163
|
+
...lane.parentId !== void 0 ? { parentId: lane.parentId } : {},
|
|
164
|
+
segments: lane.list.segments
|
|
165
|
+
});
|
|
166
|
+
const write = (run, host, segmentId, updates) => {
|
|
167
|
+
if (host) {
|
|
168
|
+
host.list.updateSegment?.(segmentId, updates);
|
|
169
|
+
return;
|
|
170
|
+
}
|
|
171
|
+
const segments = replySegments(run);
|
|
172
|
+
if (segments && !segments.some((s) => s.id === segmentId)) return;
|
|
173
|
+
run.target.updateSegment?.(segmentId, updates);
|
|
174
|
+
};
|
|
175
|
+
const writeAnchor = (run, lane) => {
|
|
176
|
+
const updates = { subagent: snapshot(run, lane) };
|
|
177
|
+
if (lane.anchorKind === "own") {
|
|
178
|
+
updates["isStreaming"] = lane.streaming;
|
|
179
|
+
} else {
|
|
180
|
+
const call = anchorOf(run, lane);
|
|
181
|
+
if (call && isOpen(call.status)) {
|
|
182
|
+
const waiting = isWaiting(run, lane);
|
|
183
|
+
if (waiting && call.status !== "awaiting-approval") {
|
|
184
|
+
updates["status"] = "awaiting-approval";
|
|
185
|
+
lane.holding = true;
|
|
186
|
+
} else if (!waiting && lane.holding && call.status === "awaiting-approval") {
|
|
187
|
+
updates["status"] = "pending";
|
|
188
|
+
lane.holding = false;
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
write(run, lane.host, lane.anchorId, updates);
|
|
193
|
+
if (run.closed) return;
|
|
194
|
+
const anchor = anchorOf(run, lane) ?? {
|
|
195
|
+
id: lane.anchorId,
|
|
196
|
+
type: SUBAGENT_SEGMENT_TYPE,
|
|
197
|
+
subagent: updates["subagent"],
|
|
198
|
+
isStreaming: lane.streaming
|
|
199
|
+
};
|
|
200
|
+
const note = {
|
|
201
|
+
target: run.target,
|
|
202
|
+
...run.targetId !== void 0 ? { targetId: run.targetId } : {},
|
|
203
|
+
messageId: run.messageId,
|
|
204
|
+
agentId: lane.id,
|
|
205
|
+
anchor
|
|
206
|
+
};
|
|
207
|
+
lastWrites.set(writeKey(run.messageId, lane.id), note);
|
|
208
|
+
for (const fn of [...laneWatchers]) {
|
|
209
|
+
try {
|
|
210
|
+
fn(note);
|
|
211
|
+
} catch (err) {
|
|
212
|
+
console.error("[aparte] a sub-agent view threw", err);
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
};
|
|
216
|
+
const flush = (run) => {
|
|
217
|
+
if (run.closed) return;
|
|
218
|
+
stopFrame(run);
|
|
219
|
+
for (let guard = 0; run.dirty.size > 0 && guard < 1e4; guard += 1) {
|
|
220
|
+
let deepest;
|
|
221
|
+
for (const lane of run.dirty) if (!deepest || lane.depth > deepest.depth) deepest = lane;
|
|
222
|
+
run.dirty.delete(deepest);
|
|
223
|
+
if (deepest.ready) writeAnchor(run, deepest);
|
|
224
|
+
}
|
|
225
|
+
stopFrame(run);
|
|
226
|
+
};
|
|
227
|
+
const schedule = (run) => {
|
|
228
|
+
if (run.cancelFrame || run.closed) return;
|
|
229
|
+
run.cancelFrame = nextFrame(() => {
|
|
230
|
+
run.cancelFrame = null;
|
|
231
|
+
flush(run);
|
|
232
|
+
});
|
|
233
|
+
};
|
|
234
|
+
const dispatchLifecycleEvent = (run, name, detail) => {
|
|
235
|
+
if (typeof CustomEvent !== "function") return;
|
|
236
|
+
const id = run.targetId ?? (run.target.id || void 0);
|
|
237
|
+
try {
|
|
238
|
+
run.target.dispatchEvent(new CustomEvent(name, { bubbles: true, composed: true, detail: { targetId: id, ...detail } }));
|
|
239
|
+
} catch {
|
|
240
|
+
}
|
|
241
|
+
};
|
|
242
|
+
const dispatchEnd = (run, lane, status) => dispatchLifecycleEvent(run, "aparte-subagent-end", { messageId: run.messageId, agentId: lane.id, status });
|
|
243
|
+
const settleLane = (lane, calls) => {
|
|
244
|
+
for (const segment of lane.list.segments) {
|
|
245
|
+
if (segment.type === "tool_call") {
|
|
246
|
+
if (calls && isOpen(segment.status)) {
|
|
247
|
+
lane.list.updateSegment?.(segment.id, { status: "aborted" });
|
|
248
|
+
}
|
|
249
|
+
} else if (segment.isStreaming === true) {
|
|
250
|
+
lane.list.updateSegment?.(segment.id, { isStreaming: false });
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
};
|
|
254
|
+
const close = (run) => {
|
|
255
|
+
if (run.closed) return;
|
|
256
|
+
for (const lane of run.lanes.values()) {
|
|
257
|
+
const working = lane.status === "running" || lane.status === "waiting";
|
|
258
|
+
stepsEnded(run.messageId, lane.id);
|
|
259
|
+
settleLane(lane, true);
|
|
260
|
+
lane.streaming = false;
|
|
261
|
+
run.dirty.add(lane);
|
|
262
|
+
if (working) dispatchEnd(run, lane, "stopped");
|
|
263
|
+
}
|
|
264
|
+
flush(run);
|
|
265
|
+
forget(run);
|
|
266
|
+
};
|
|
267
|
+
const forget = (run) => {
|
|
268
|
+
for (const lane of run.lanes.values()) {
|
|
269
|
+
stepsEnded(run.messageId, lane.id);
|
|
270
|
+
lastWrites.delete(writeKey(run.messageId, lane.id));
|
|
271
|
+
}
|
|
272
|
+
run.closed = true;
|
|
273
|
+
stopFrame(run);
|
|
274
|
+
run.unlisten();
|
|
275
|
+
release(run.target, run.messageId, owner);
|
|
276
|
+
if (runs.get(run.messageId) === run) runs.delete(run.messageId);
|
|
277
|
+
over.add(run.messageId);
|
|
278
|
+
if (over.size > MAX_RUNS) over.delete(over.values().next().value);
|
|
279
|
+
};
|
|
280
|
+
const markDirty = (run, lane, now) => {
|
|
281
|
+
run.dirty.add(lane);
|
|
282
|
+
if (now) flush(run);
|
|
283
|
+
else schedule(run);
|
|
284
|
+
};
|
|
285
|
+
const settleDueRows = (run) => {
|
|
286
|
+
if (run.rowsDue.length === 0) return;
|
|
287
|
+
const due = run.rowsDue;
|
|
288
|
+
run.rowsDue = [];
|
|
289
|
+
for (const lane of due) {
|
|
290
|
+
lane.ready = true;
|
|
291
|
+
run.dirty.add(lane);
|
|
292
|
+
}
|
|
293
|
+
flush(run);
|
|
294
|
+
};
|
|
295
|
+
const callStarted = (run, host, toolCallId) => {
|
|
296
|
+
const key = hostKey(host, toolCallId);
|
|
297
|
+
const held = run.awaitingRow.get(key);
|
|
298
|
+
if (!held) return;
|
|
299
|
+
run.awaitingRow.delete(key);
|
|
300
|
+
if (host) {
|
|
301
|
+
for (const lane of held) {
|
|
302
|
+
lane.ready = true;
|
|
303
|
+
run.dirty.add(lane);
|
|
304
|
+
}
|
|
305
|
+
flush(run);
|
|
306
|
+
return;
|
|
307
|
+
}
|
|
308
|
+
run.rowsDue.push(...held);
|
|
309
|
+
queueMicrotask(() => settleDueRows(run));
|
|
310
|
+
};
|
|
311
|
+
const listen = (run) => {
|
|
312
|
+
const target = run.target;
|
|
313
|
+
if (typeof target.addEventListener !== "function" || typeof target.removeEventListener !== "function") return () => {
|
|
314
|
+
};
|
|
315
|
+
const mine = (e) => e.detail?.messageId === run.messageId;
|
|
316
|
+
const onEnd = (e) => {
|
|
317
|
+
if (mine(e)) close(run);
|
|
318
|
+
};
|
|
319
|
+
const onPause = (e) => {
|
|
320
|
+
if (!mine(e)) return;
|
|
321
|
+
for (const lane of run.lanes.values()) run.dirty.add(lane);
|
|
322
|
+
flush(run);
|
|
323
|
+
};
|
|
324
|
+
const ends = ["aparte-message-done", "aparte-message-aborted", "aparte-message-error"];
|
|
325
|
+
for (const name of ends) target.addEventListener(name, onEnd, true);
|
|
326
|
+
target.addEventListener("aparte-message-paused", onPause, true);
|
|
327
|
+
return () => {
|
|
328
|
+
for (const name of ends) target.removeEventListener(name, onEnd, true);
|
|
329
|
+
target.removeEventListener("aparte-message-paused", onPause, true);
|
|
330
|
+
};
|
|
331
|
+
};
|
|
332
|
+
const openRun = (ctx) => {
|
|
333
|
+
if (!claim(ctx.target, ctx.messageId, owner)) return void 0;
|
|
334
|
+
if (runs.size >= MAX_RUNS) {
|
|
335
|
+
const oldest = runs.values().next().value;
|
|
336
|
+
if (oldest) forget(oldest);
|
|
337
|
+
}
|
|
338
|
+
const run = {
|
|
339
|
+
messageId: ctx.messageId,
|
|
340
|
+
target: ctx.target,
|
|
341
|
+
...ctx.targetId !== void 0 ? { targetId: ctx.targetId } : {},
|
|
342
|
+
lanes: /* @__PURE__ */ new Map(),
|
|
343
|
+
callAgents: /* @__PURE__ */ new Map(),
|
|
344
|
+
early: /* @__PURE__ */ new Map(),
|
|
345
|
+
earlyCount: 0,
|
|
346
|
+
awaitingRow: /* @__PURE__ */ new Map(),
|
|
347
|
+
rowsDue: [],
|
|
348
|
+
dirty: /* @__PURE__ */ new Set(),
|
|
349
|
+
cancelFrame: null,
|
|
350
|
+
closed: false,
|
|
351
|
+
unlisten: () => {
|
|
352
|
+
}
|
|
353
|
+
};
|
|
354
|
+
run.unlisten = listen(run);
|
|
355
|
+
runs.set(ctx.messageId, run);
|
|
356
|
+
return run;
|
|
357
|
+
};
|
|
358
|
+
const savedAnchor = (segments, agentId, toolCallId) => {
|
|
359
|
+
if (!segments) return void 0;
|
|
360
|
+
return segments.find((s) => s.id === ownSegmentId(agentId) && subagentOf(s)?.id === agentId) ?? (toolCallId ? segments.find((s) => s.id === `tool-${toolCallId}` && subagentOf(s)?.id === agentId) : void 0);
|
|
361
|
+
};
|
|
362
|
+
const retire = (run, lane) => {
|
|
363
|
+
run.lanes.delete(lane.id);
|
|
364
|
+
stepsEnded(run.messageId, lane.id);
|
|
365
|
+
if (lane.status === "running" || lane.status === "waiting") {
|
|
366
|
+
lane.status = "stopped";
|
|
367
|
+
settleLane(lane, true);
|
|
368
|
+
lane.streaming = false;
|
|
369
|
+
dispatchEnd(run, lane, "stopped");
|
|
370
|
+
}
|
|
371
|
+
markDirty(run, lane, true);
|
|
372
|
+
};
|
|
373
|
+
const startAgent = (run, e, ctx) => {
|
|
374
|
+
const known = run.lanes.get(e.agentId);
|
|
375
|
+
const reused = known !== void 0 && e.toolCallId !== void 0 && e.toolCallId !== known.toolCallId;
|
|
376
|
+
if (known && reused) retire(run, known);
|
|
377
|
+
if (known && !reused) {
|
|
378
|
+
known.status = "running";
|
|
379
|
+
delete known.result;
|
|
380
|
+
delete known.error;
|
|
381
|
+
if (e.name !== void 0) known.name = e.name;
|
|
382
|
+
if (e.description !== void 0) known.description = e.description;
|
|
383
|
+
known.streaming = true;
|
|
384
|
+
announce(run, known, e.toolCallId);
|
|
385
|
+
markDirty(run, known, true);
|
|
386
|
+
return;
|
|
387
|
+
}
|
|
388
|
+
if (run.lanes.size >= MAX_LANES) return;
|
|
389
|
+
const host = e.parentAgentId ? run.lanes.get(e.parentAgentId) ?? null : null;
|
|
390
|
+
const segments = hostSegments(run, host);
|
|
391
|
+
const saved = reused ? void 0 : savedAnchor(segments, e.agentId, e.toolCallId);
|
|
392
|
+
const callKey = e.toolCallId ? hostKey(host, e.toolCallId) : "";
|
|
393
|
+
const callId = e.toolCallId ? `tool-${e.toolCallId}` : "";
|
|
394
|
+
const callTaken = callKey !== "" && (run.callAgents.has(callKey) || !!segments?.some((s) => s.id === callId && subagentOf(s) && subagentOf(s).id !== e.agentId));
|
|
395
|
+
const onCall = saved ? saved.type === "tool_call" : callKey !== "" && !callTaken;
|
|
396
|
+
if (onCall) run.callAgents.set(callKey, e.agentId);
|
|
397
|
+
const previous = saved ? subagentOf(saved) : void 0;
|
|
398
|
+
const list = createSegmentList({ messageId: run.messageId, ...previous ? { segments: previous.segments } : {}, config: ctx.config });
|
|
399
|
+
const lane = {
|
|
400
|
+
id: e.agentId,
|
|
401
|
+
...e.name !== void 0 ? { name: e.name } : previous?.name !== void 0 ? { name: previous.name } : {},
|
|
402
|
+
...e.description !== void 0 ? { description: e.description } : previous?.description !== void 0 ? { description: previous.description } : {},
|
|
403
|
+
...e.parentAgentId !== void 0 ? { parentId: e.parentAgentId } : {},
|
|
404
|
+
...e.toolCallId !== void 0 ? { toolCallId: e.toolCallId } : {},
|
|
405
|
+
status: "running",
|
|
406
|
+
list,
|
|
407
|
+
// Not observed: every observer heard these events from the reply's adapter already.
|
|
408
|
+
emit: createStreamAdapter({ target: list, config: ctx.config, messageId: run.messageId, observe: false }),
|
|
409
|
+
host,
|
|
410
|
+
anchorId: onCall ? callId : saved ? saved.id : freeOwnId(segments, e.agentId),
|
|
411
|
+
anchorKind: onCall ? "call" : "own",
|
|
412
|
+
ready: true,
|
|
413
|
+
streaming: true,
|
|
414
|
+
asking: /* @__PURE__ */ new Set(),
|
|
415
|
+
holding: false,
|
|
416
|
+
depth: host ? host.depth + 1 : 0
|
|
417
|
+
};
|
|
418
|
+
run.lanes.set(lane.id, lane);
|
|
419
|
+
list.onChange(() => {
|
|
420
|
+
if (!run.closed) {
|
|
421
|
+
run.dirty.add(lane);
|
|
422
|
+
schedule(run);
|
|
423
|
+
}
|
|
424
|
+
});
|
|
425
|
+
if (lane.anchorKind === "call" && !saved && segments && !segments.some((s) => s.id === lane.anchorId)) {
|
|
426
|
+
lane.ready = false;
|
|
427
|
+
const key = callKey;
|
|
428
|
+
run.awaitingRow.set(key, [...run.awaitingRow.get(key) ?? [], lane]);
|
|
429
|
+
} else if (lane.anchorKind === "own" && !saved && !run.closed) {
|
|
430
|
+
const segment = {
|
|
431
|
+
id: lane.anchorId,
|
|
432
|
+
type: SUBAGENT_SEGMENT_TYPE,
|
|
433
|
+
subagent: snapshot(run, lane),
|
|
434
|
+
isStreaming: true
|
|
435
|
+
};
|
|
436
|
+
if (host) host.list.addSegment?.(segment);
|
|
437
|
+
else run.target.addSegment?.(segment);
|
|
438
|
+
}
|
|
439
|
+
announce(run, lane, e.toolCallId);
|
|
440
|
+
const early = run.early.get(lane.id);
|
|
441
|
+
if (early) {
|
|
442
|
+
run.early.delete(lane.id);
|
|
443
|
+
run.earlyCount -= early.length;
|
|
444
|
+
for (const event of early) feed(run, lane, event);
|
|
445
|
+
}
|
|
446
|
+
markDirty(run, lane, true);
|
|
447
|
+
};
|
|
448
|
+
const announce = (run, lane, toolCallId) => dispatchLifecycleEvent(run, "aparte-subagent-start", {
|
|
449
|
+
messageId: run.messageId,
|
|
450
|
+
agentId: lane.id,
|
|
451
|
+
...lane.name !== void 0 ? { name: lane.name } : {},
|
|
452
|
+
...lane.description !== void 0 ? { description: lane.description } : {},
|
|
453
|
+
...toolCallId !== void 0 ? { toolCallId } : {},
|
|
454
|
+
...lane.parentId !== void 0 ? { parentId: lane.parentId } : {}
|
|
455
|
+
});
|
|
456
|
+
const endAgent = (run, e) => {
|
|
457
|
+
const lane = run.lanes.get(e.agentId);
|
|
458
|
+
if (!lane) return;
|
|
459
|
+
lane.status = e.status;
|
|
460
|
+
if (e.result !== void 0) lane.result = e.result;
|
|
461
|
+
if (e.error !== void 0) lane.error = e.error;
|
|
462
|
+
if (e.status !== "waiting") {
|
|
463
|
+
stepsEnded(run.messageId, lane.id);
|
|
464
|
+
settleLane(lane, false);
|
|
465
|
+
lane.streaming = false;
|
|
466
|
+
}
|
|
467
|
+
dispatchEnd(run, lane, e.status);
|
|
468
|
+
markDirty(run, lane, true);
|
|
469
|
+
};
|
|
470
|
+
const feed = (run, lane, e) => {
|
|
471
|
+
const { agentId: _owner, ...event } = e;
|
|
472
|
+
if ("toolCallId" in event && typeof event.toolCallId === "string") {
|
|
473
|
+
if (event.type === "tool-awaiting-approval") lane.asking.add(event.toolCallId);
|
|
474
|
+
else if (event.type !== "tool-start") lane.asking.delete(event.toolCallId);
|
|
475
|
+
}
|
|
476
|
+
if (event.type === "step-start") stepStarted(run.messageId, lane.id, event.name);
|
|
477
|
+
else if (event.type === "step-end") stepEnded(run.messageId, lane.id, event.name);
|
|
478
|
+
const before = lane.list.segments.length;
|
|
479
|
+
lane.emit(event);
|
|
480
|
+
if (event.type === "tool-start") callStarted(run, lane, event.toolCallId);
|
|
481
|
+
const now = !COALESCED.has(event.type) || lane.list.segments.length !== before;
|
|
482
|
+
markDirty(run, lane, now);
|
|
483
|
+
};
|
|
484
|
+
const observe = (event, ctx) => {
|
|
485
|
+
const agentId = event.agentId;
|
|
486
|
+
const attributed = typeof agentId === "string" || event.type === "agent-start" || event.type === "agent-end";
|
|
487
|
+
let run = runs.get(ctx.messageId);
|
|
488
|
+
if (run && run.target !== ctx.target) return;
|
|
489
|
+
if (!run) {
|
|
490
|
+
if (!attributed || over.has(ctx.messageId)) return;
|
|
491
|
+
run = openRun(ctx);
|
|
492
|
+
if (!run) return;
|
|
493
|
+
}
|
|
494
|
+
settleDueRows(run);
|
|
495
|
+
if (!attributed) {
|
|
496
|
+
switch (event.type) {
|
|
497
|
+
case "tool-start":
|
|
498
|
+
callStarted(run, null, event.toolCallId);
|
|
499
|
+
break;
|
|
500
|
+
case "run-suspended":
|
|
501
|
+
for (const lane2 of run.lanes.values()) run.dirty.add(lane2);
|
|
502
|
+
flush(run);
|
|
503
|
+
break;
|
|
504
|
+
case "run-done":
|
|
505
|
+
case "run-aborted":
|
|
506
|
+
close(run);
|
|
507
|
+
break;
|
|
508
|
+
}
|
|
509
|
+
return;
|
|
510
|
+
}
|
|
511
|
+
if (event.type === "agent-start") {
|
|
512
|
+
startAgent(run, event, ctx);
|
|
513
|
+
return;
|
|
514
|
+
}
|
|
515
|
+
if (event.type === "agent-end") {
|
|
516
|
+
endAgent(run, event);
|
|
517
|
+
return;
|
|
518
|
+
}
|
|
519
|
+
if (event.type === "run-start" || event.type === "run-done" || event.type === "run-aborted" || event.type === "run-suspended") return;
|
|
520
|
+
const lane = run.lanes.get(agentId);
|
|
521
|
+
if (lane) {
|
|
522
|
+
feed(run, lane, event);
|
|
523
|
+
return;
|
|
524
|
+
}
|
|
525
|
+
if (run.earlyCount >= MAX_EARLY) return;
|
|
526
|
+
run.early.set(agentId, [...run.early.get(agentId) ?? [], event]);
|
|
527
|
+
run.earlyCount += 1;
|
|
528
|
+
};
|
|
529
|
+
return {
|
|
530
|
+
observe,
|
|
531
|
+
dispose: () => {
|
|
532
|
+
for (const run of [...runs.values()]) forget(run);
|
|
533
|
+
}
|
|
534
|
+
};
|
|
535
|
+
}
|
|
536
|
+
const shown = /* @__PURE__ */ new Map();
|
|
537
|
+
const keyOf = (messageId, agentId) => `${messageId}\0${agentId}`;
|
|
538
|
+
function isShown(messageId, agentId) {
|
|
539
|
+
if (!messageId || shown.size === 0) return false;
|
|
540
|
+
const key = keyOf(messageId, agentId);
|
|
541
|
+
for (const value of shown.values()) if (value === key) return true;
|
|
542
|
+
return false;
|
|
543
|
+
}
|
|
544
|
+
function setShown(owner, at) {
|
|
545
|
+
if (at) shown.set(owner, keyOf(at.messageId, at.agentId));
|
|
546
|
+
else shown.delete(owner);
|
|
547
|
+
}
|
|
548
|
+
const openButtonOf = (row) => row.querySelector(":scope > :is(.aparte-tool-line, .aparte-tool-open-slot) > .aparte-tool-open");
|
|
549
|
+
function markRow(row, on) {
|
|
550
|
+
const button = openButtonOf(row);
|
|
551
|
+
if (!button) return;
|
|
552
|
+
row.toggleAttribute("data-current", on);
|
|
553
|
+
if (on) button.setAttribute("aria-current", "true");
|
|
554
|
+
else button.removeAttribute("aria-current");
|
|
555
|
+
}
|
|
556
|
+
function unmarkAll(root) {
|
|
557
|
+
for (const row of root.querySelectorAll(".aparte-segment-tool-call[data-current]")) markRow(row, false);
|
|
558
|
+
}
|
|
559
|
+
function targetIdOf(row) {
|
|
560
|
+
const chat = row.closest("aparte-chat");
|
|
561
|
+
if (chat) return chat.id || void 0;
|
|
562
|
+
const view = row.closest("aparte-subagent-view:not([data-inline])");
|
|
563
|
+
if (!view) return void 0;
|
|
564
|
+
const id = view.getAttribute("for") || (typeof view.chat?.id === "string" ? view.chat.id : "");
|
|
565
|
+
return id || void 0;
|
|
566
|
+
}
|
|
567
|
+
function dispatchOpen(row, segment, sub, standIn) {
|
|
568
|
+
const messageId = segment.messageId || row.closest("[message-id]")?.getAttribute("message-id") || "";
|
|
569
|
+
if (!messageId) return;
|
|
570
|
+
const targetId = targetIdOf(row);
|
|
571
|
+
const name = sub.name?.trim();
|
|
572
|
+
const toolCallId = standIn ? void 0 : segment.toolCall?.id;
|
|
573
|
+
const detail = {
|
|
574
|
+
...targetId ? { targetId } : {},
|
|
575
|
+
messageId,
|
|
576
|
+
agentId: sub.id,
|
|
577
|
+
...name ? { name } : {},
|
|
578
|
+
...toolCallId ? { toolCallId } : {}
|
|
579
|
+
};
|
|
580
|
+
row.dispatchEvent(new CustomEvent("aparte-subagent-open", { bubbles: true, composed: true, detail }));
|
|
581
|
+
}
|
|
582
|
+
function innerRendererFor(segment, config) {
|
|
583
|
+
if (segment.type === "activity") return config.getActivityRenderer(segment.activityType) ?? null;
|
|
584
|
+
if (segment.type !== "tool_call") return null;
|
|
585
|
+
const name = segment.toolCall?.name;
|
|
586
|
+
return name && !config.isToolHidden(name) ? config.getToolRenderer(name) ?? null : null;
|
|
587
|
+
}
|
|
588
|
+
function rendererFor(type, config) {
|
|
589
|
+
const renderer = getSegmentRenderer(type, config);
|
|
590
|
+
if (renderer) return renderer;
|
|
591
|
+
installDefaultRenderersOnce(config);
|
|
592
|
+
return getSegmentRenderer(type, config);
|
|
593
|
+
}
|
|
594
|
+
function toElement(out, segment) {
|
|
595
|
+
let el;
|
|
596
|
+
if (typeof out === "string") {
|
|
597
|
+
const tpl = document.createElement("template");
|
|
598
|
+
tpl.innerHTML = out.trim();
|
|
599
|
+
el = tpl.content.firstElementChild;
|
|
600
|
+
} else {
|
|
601
|
+
el = out;
|
|
602
|
+
}
|
|
603
|
+
if (el && !el.hasAttribute("data-segment-id")) el.setAttribute("data-segment-id", segment.id);
|
|
604
|
+
return el;
|
|
605
|
+
}
|
|
606
|
+
function fallbackOf(segment) {
|
|
607
|
+
const text = segment.fallback;
|
|
608
|
+
if (typeof text !== "string" || !text.trim()) return null;
|
|
609
|
+
const el = document.createElement("div");
|
|
610
|
+
el.className = "aparte-segment aparte-segment-fallback";
|
|
611
|
+
el.setAttribute("data-segment-id", segment.id);
|
|
612
|
+
el.textContent = text;
|
|
613
|
+
return el;
|
|
614
|
+
}
|
|
615
|
+
function drawnAs(segment, live) {
|
|
616
|
+
if (!live || segment.type !== "text" && segment.type !== "thinking") return segment;
|
|
617
|
+
if (segment.isStreaming !== void 0) return segment;
|
|
618
|
+
return { ...segment, isStreaming: true };
|
|
619
|
+
}
|
|
620
|
+
class LaneDrawing {
|
|
621
|
+
constructor(container) {
|
|
622
|
+
this.container = container;
|
|
623
|
+
}
|
|
624
|
+
rows = /* @__PURE__ */ new Map();
|
|
625
|
+
/** How many rows are drawn. */
|
|
626
|
+
get size() {
|
|
627
|
+
return this.rows.size;
|
|
628
|
+
}
|
|
629
|
+
/**
|
|
630
|
+
* Draw `segments`, in order: a new segment gets a row, a changed one is patched, one that
|
|
631
|
+
* left loses its row. `live`: the sub-agent still works, so what it adds is arriving.
|
|
632
|
+
*/
|
|
633
|
+
sync(segments, config, live) {
|
|
634
|
+
const seen = /* @__PURE__ */ new Set();
|
|
635
|
+
let previous = null;
|
|
636
|
+
for (const segment of segments) {
|
|
637
|
+
if (!segment || typeof segment.id !== "string" || seen.has(segment.id)) continue;
|
|
638
|
+
seen.add(segment.id);
|
|
639
|
+
const renderer = rendererFor(segment.type, config);
|
|
640
|
+
let row = this.rows.get(segment.id);
|
|
641
|
+
if (row && (row.renderer !== renderer || row.inner !== innerRendererFor(segment, config))) {
|
|
642
|
+
this.release(segment.id, config);
|
|
643
|
+
row.el.remove();
|
|
644
|
+
row = void 0;
|
|
645
|
+
}
|
|
646
|
+
if (!row) row = this.draw(segment, renderer, config, live);
|
|
647
|
+
else if (row.segment !== segment) this.patch(row, segment, config);
|
|
648
|
+
if (!row) continue;
|
|
649
|
+
const expected = previous ? previous.nextSibling : this.container.firstChild;
|
|
650
|
+
if (row.el !== expected) this.container.insertBefore(row.el, expected);
|
|
651
|
+
previous = row.el;
|
|
652
|
+
}
|
|
653
|
+
for (const [id, row] of [...this.rows]) {
|
|
654
|
+
if (seen.has(id)) continue;
|
|
655
|
+
this.release(id, config);
|
|
656
|
+
row.el.remove();
|
|
657
|
+
}
|
|
658
|
+
}
|
|
659
|
+
/** Every row re-reads what it takes from the config (a language switch). */
|
|
660
|
+
relabel(config) {
|
|
661
|
+
for (const row of this.rows.values()) {
|
|
662
|
+
const relabel = row.renderer?.relabel;
|
|
663
|
+
if (!relabel) continue;
|
|
664
|
+
runWithConfig(config, () => relabel.call(row.renderer, row.el, row.segment));
|
|
665
|
+
}
|
|
666
|
+
}
|
|
667
|
+
/** Every row goes: each renderer cleans up what it drew. */
|
|
668
|
+
clear(config) {
|
|
669
|
+
for (const [id, row] of [...this.rows]) {
|
|
670
|
+
this.release(id, config);
|
|
671
|
+
row.el.remove();
|
|
672
|
+
}
|
|
673
|
+
}
|
|
674
|
+
draw(segment, renderer, config, live) {
|
|
675
|
+
const el = renderer ? toElement(runWithConfig(config, () => renderer.render(segment)), segment) : fallbackOf(segment);
|
|
676
|
+
if (!el) return void 0;
|
|
677
|
+
const row = { el, renderer, inner: innerRendererFor(segment, config), segment };
|
|
678
|
+
this.rows.set(segment.id, row);
|
|
679
|
+
if (renderer?.setup) {
|
|
680
|
+
const as = drawnAs(segment, live);
|
|
681
|
+
runWithConfig(config, () => renderer.setup(el, as));
|
|
682
|
+
}
|
|
683
|
+
return row;
|
|
684
|
+
}
|
|
685
|
+
patch(row, segment, config) {
|
|
686
|
+
const renderer = row.renderer;
|
|
687
|
+
row.segment = segment;
|
|
688
|
+
if (!renderer) {
|
|
689
|
+
const next2 = fallbackOf(segment);
|
|
690
|
+
if (next2) {
|
|
691
|
+
row.el.replaceWith(next2);
|
|
692
|
+
row.el = next2;
|
|
693
|
+
}
|
|
694
|
+
return;
|
|
695
|
+
}
|
|
696
|
+
if (renderer.update) {
|
|
697
|
+
const before = row.el;
|
|
698
|
+
const next2 = before.nextSibling;
|
|
699
|
+
runWithConfig(config, () => renderer.update(before, segment));
|
|
700
|
+
if (before.parentNode !== this.container) {
|
|
701
|
+
const now = this.childOf(segment.id) ?? this.inPlaceOf(next2, segment.id);
|
|
702
|
+
if (now) row.el = now;
|
|
703
|
+
}
|
|
704
|
+
return;
|
|
705
|
+
}
|
|
706
|
+
const next = toElement(runWithConfig(config, () => renderer.render(segment)), segment);
|
|
707
|
+
if (!next) return;
|
|
708
|
+
this.cleanup(row, config);
|
|
709
|
+
row.el.replaceWith(next);
|
|
710
|
+
row.el = next;
|
|
711
|
+
if (renderer.setup) runWithConfig(config, () => renderer.setup(next, segment));
|
|
712
|
+
}
|
|
713
|
+
/** The row for `id` among the container's own children — never a decoy in a row's prose. */
|
|
714
|
+
childOf(id) {
|
|
715
|
+
for (const child of this.container.children) {
|
|
716
|
+
if (child.getAttribute("data-segment-id") === id) return child;
|
|
717
|
+
}
|
|
718
|
+
return null;
|
|
719
|
+
}
|
|
720
|
+
/**
|
|
721
|
+
* The element standing where a row stood (before `next`) when it is no row's: what a
|
|
722
|
+
* renderer put in place of its own without the id (`outerHTML`, `replaceWith`), stamped so
|
|
723
|
+
* the loop finds it. A renderer that took its element away left another row there, or
|
|
724
|
+
* nothing: the row keeps its element, and the loop puts it back.
|
|
725
|
+
*/
|
|
726
|
+
inPlaceOf(next, id) {
|
|
727
|
+
const there = next ? next.previousSibling : this.container.lastChild;
|
|
728
|
+
if (there?.nodeType !== Node.ELEMENT_NODE || there.hasAttribute("data-segment-id")) return null;
|
|
729
|
+
there.setAttribute("data-segment-id", id);
|
|
730
|
+
return there;
|
|
731
|
+
}
|
|
732
|
+
release(id, config) {
|
|
733
|
+
const row = this.rows.get(id);
|
|
734
|
+
if (!row) return;
|
|
735
|
+
this.rows.delete(id);
|
|
736
|
+
this.cleanup(row, config);
|
|
737
|
+
}
|
|
738
|
+
cleanup(row, config) {
|
|
739
|
+
const cleanup = row.renderer?.cleanup;
|
|
740
|
+
if (!cleanup) return;
|
|
741
|
+
try {
|
|
742
|
+
runWithConfig(config, () => cleanup.call(row.renderer, row.el, row.segment));
|
|
743
|
+
} catch (err) {
|
|
744
|
+
console.error("[aparte] a segment renderer's cleanup threw", err);
|
|
745
|
+
}
|
|
746
|
+
}
|
|
747
|
+
}
|
|
748
|
+
const styles = "/*\n * @aparte/plugin-subagents — a sub-agent's conversation.\n *\n * Only what is the plugin's own. The rows inside are core's (text, reasoning, tool rows,\n * a nested sub-agent's line) and wear core's sheets; under a line, the transcript sits in\n * core's tool detail, which already draws the rule and the indent a disclosure has. So\n * this sheet draws the task line, the spacing of the rows, and — for the standalone view\n * only — the same quiet rule down the leading edge, since nothing of core's is around it.\n *\n * Logical properties throughout, so a right-to-left locale puts the rule on its own\n * leading edge. No colour of its own: core's tokens. Nothing moves, so there is no motion\n * to reduce.\n */\naparte-subagent-view {\n display: block;\n min-inline-size: 0;\n}\n\n/* Standalone (a canvas, a pane, a dialog): outside any message, so the conversation's\n type is set here, as core's segment container sets it in a reply. */\naparte-subagent-view:not([data-inline]) {\n font-size: var(--aparte-content-font-size);\n line-height: var(--aparte-content-line-height);\n color: var(--aparte-content-color);\n font-family: var(--aparte-font-family);\n}\n\n/* Nothing to show (an unknown sub-agent, or one not started yet): nothing drawn. */\naparte-subagent-view[data-empty] {\n display: none;\n}\n\n/* A closable view (a pane of its own): the sub-agent's name, and Close at the far end. The\n name takes the focus when the pane opens, so it shows a ring only to the keyboard. */\n.aparte-subagent-head {\n display: flex;\n align-items: center;\n gap: var(--aparte-space-3);\n margin-block-end: var(--aparte-space-3);\n}\n\n.aparte-subagent-head[hidden] {\n display: none;\n}\n\n.aparte-subagent-name {\n flex: 1 1 auto;\n min-inline-size: 0;\n margin: 0;\n overflow: hidden;\n font-size: var(--aparte-font-size-md);\n font-weight: var(--aparte-font-weight-semibold);\n text-overflow: ellipsis;\n white-space: nowrap;\n}\n\n.aparte-subagent-name:focus {\n outline: none;\n}\n\n.aparte-subagent-name:focus-visible {\n outline: var(--aparte-focus-outline-width) solid var(--aparte-border-focus);\n}\n\n.aparte-subagent-close {\n flex: none;\n}\n\n.aparte-subagent-task {\n margin: 0 0 var(--aparte-space-3);\n color: var(--aparte-text-muted);\n font-size: var(--aparte-font-size-sm);\n}\n\n.aparte-subagent-transcript {\n display: flex;\n flex-direction: column;\n gap: var(--aparte-space-4);\n min-inline-size: 0;\n}\n\n.aparte-subagent-transcript:empty {\n display: none;\n}\n\naparte-subagent-view:not([data-inline]) > .aparte-subagent-transcript {\n padding-inline-start: var(--aparte-subagent-indent, var(--aparte-space-6));\n border-inline-start: var(--aparte-border-width) solid var(--aparte-border);\n}\n";
|
|
749
|
+
const STYLE_ID = "aparte-subagent-styles";
|
|
750
|
+
const MARK = ".aparte-subagent-transcript";
|
|
751
|
+
let holder = null;
|
|
752
|
+
function onPage() {
|
|
753
|
+
if (holder?.isConnected) return true;
|
|
754
|
+
holder = document.getElementById(STYLE_ID) ?? [...document.querySelectorAll("style")].find((sheet) => sheet.textContent?.includes(MARK)) ?? null;
|
|
755
|
+
return holder !== null;
|
|
756
|
+
}
|
|
757
|
+
function rendererStyles() {
|
|
758
|
+
return typeof document !== "undefined" && document.getElementById(STYLE_ID) ? "" : styles;
|
|
759
|
+
}
|
|
760
|
+
function ensureStyles() {
|
|
761
|
+
if (typeof document === "undefined" || onPage()) return;
|
|
762
|
+
const style = document.createElement("style");
|
|
763
|
+
style.id = STYLE_ID;
|
|
764
|
+
style.textContent = styles;
|
|
765
|
+
document.head.appendChild(style);
|
|
766
|
+
holder = style;
|
|
767
|
+
}
|
|
768
|
+
const WORDS = {
|
|
769
|
+
/** How many tool calls a sub-agent made, on its line: "3 tools". */
|
|
770
|
+
subagentsTools: {
|
|
771
|
+
en: "one:{n} tool|other:{n} tools",
|
|
772
|
+
fr: "one:{n} outil|other:{n} outils",
|
|
773
|
+
de: "one:{n} Tool|other:{n} Tools",
|
|
774
|
+
es: "one:{n} herramienta|other:{n} herramientas",
|
|
775
|
+
it: "one:{n} strumento|other:{n} strumenti",
|
|
776
|
+
ja: "other:{n} 個のツール",
|
|
777
|
+
ko: "other:도구 {n}개",
|
|
778
|
+
pt: "one:{n} ferramenta|other:{n} ferramentas",
|
|
779
|
+
ru: "one:{n} инструмент|few:{n} инструмента|many:{n} инструментов|other:{n} инструмента",
|
|
780
|
+
zh: "other:{n} 个工具"
|
|
781
|
+
},
|
|
782
|
+
/** What a sub-agent's second line says before its first call. */
|
|
783
|
+
subagentsStarting: {
|
|
784
|
+
en: "Starting…",
|
|
785
|
+
fr: "Démarrage…",
|
|
786
|
+
de: "Wird gestartet…",
|
|
787
|
+
es: "Iniciando…",
|
|
788
|
+
it: "Avvio…",
|
|
789
|
+
ja: "開始しています…",
|
|
790
|
+
ko: "시작하는 중…",
|
|
791
|
+
pt: "Iniciando…",
|
|
792
|
+
ru: "Запуск…",
|
|
793
|
+
zh: "正在启动…"
|
|
794
|
+
},
|
|
795
|
+
/** The name of a sub-agent that was given none, when no call names its line either. */
|
|
796
|
+
subagentsUnnamed: {
|
|
797
|
+
en: "Sub-agent",
|
|
798
|
+
fr: "Sous-agent",
|
|
799
|
+
de: "Subagent",
|
|
800
|
+
es: "Subagente",
|
|
801
|
+
it: "Sottoagente",
|
|
802
|
+
ja: "サブエージェント",
|
|
803
|
+
ko: "하위 에이전트",
|
|
804
|
+
pt: "Subagente",
|
|
805
|
+
ru: "Субагент",
|
|
806
|
+
zh: "子智能体"
|
|
807
|
+
},
|
|
808
|
+
/** The Open button on a sub-agent's line (`setupSubagents({ open: 'event' })`): its name and tooltip. */
|
|
809
|
+
subagentsOpen: {
|
|
810
|
+
en: "Open {name}",
|
|
811
|
+
fr: "Ouvrir {name}",
|
|
812
|
+
de: "{name} öffnen",
|
|
813
|
+
es: "Abrir {name}",
|
|
814
|
+
it: "Apri {name}",
|
|
815
|
+
ja: "{name} を開く",
|
|
816
|
+
ko: "{name} 열기",
|
|
817
|
+
pt: "Abrir {name}",
|
|
818
|
+
ru: "Открыть: {name}",
|
|
819
|
+
zh: "打开 {name}"
|
|
820
|
+
},
|
|
821
|
+
/** The Close button of a `<aparte-subagent-view closable>`. */
|
|
822
|
+
subagentsClose: {
|
|
823
|
+
en: "Close",
|
|
824
|
+
fr: "Fermer",
|
|
825
|
+
de: "Schließen",
|
|
826
|
+
es: "Cerrar",
|
|
827
|
+
it: "Chiudi",
|
|
828
|
+
ja: "閉じる",
|
|
829
|
+
ko: "닫기",
|
|
830
|
+
pt: "Fechar",
|
|
831
|
+
ru: "Закрыть",
|
|
832
|
+
zh: "关闭"
|
|
833
|
+
}
|
|
834
|
+
};
|
|
835
|
+
function languageOf(config) {
|
|
836
|
+
const tag = config.getLocale().tag;
|
|
837
|
+
return typeof tag === "string" && tag ? tag.split("-")[0].toLowerCase() : "en";
|
|
838
|
+
}
|
|
839
|
+
function word(config, key) {
|
|
840
|
+
const fromApp = config.getLocale()[key];
|
|
841
|
+
const table = WORDS[key];
|
|
842
|
+
return typeof fromApp === "string" && fromApp || table[languageOf(config)] || table.en;
|
|
843
|
+
}
|
|
844
|
+
function toolCount(config, n) {
|
|
845
|
+
const tag = config.getLocale().tag;
|
|
846
|
+
return pluralize(word(config, "subagentsTools"), n, typeof tag === "string" && tag ? tag : void 0);
|
|
847
|
+
}
|
|
848
|
+
const CONVERSATION_EVENTS = [
|
|
849
|
+
"aparte-subagent-start",
|
|
850
|
+
"aparte-subagent-end",
|
|
851
|
+
"aparte-segment-update",
|
|
852
|
+
"aparte-message-done",
|
|
853
|
+
"aparte-message-aborted",
|
|
854
|
+
"aparte-message-error",
|
|
855
|
+
"aparte-message-paused",
|
|
856
|
+
"aparte-path-changed",
|
|
857
|
+
"aparte-reset-done",
|
|
858
|
+
"aparte-conversation-loaded"
|
|
859
|
+
];
|
|
860
|
+
const MAX_DEPTH = 16;
|
|
861
|
+
const adopted = /* @__PURE__ */ new WeakMap();
|
|
862
|
+
function laneOf(anchor) {
|
|
863
|
+
const sub = subagentOf(anchor);
|
|
864
|
+
if (!sub) return { segments: [], live: false };
|
|
865
|
+
const status = subagentStatus(anchor);
|
|
866
|
+
if (status === "running" || status === "waiting") return { segments: sub.segments, live: true };
|
|
867
|
+
let settled = adopted.get(sub.segments);
|
|
868
|
+
if (!settled) {
|
|
869
|
+
settled = createSegmentList({ messageId: anchor.messageId ?? "", segments: sub.segments }).segments;
|
|
870
|
+
adopted.set(sub.segments, settled);
|
|
871
|
+
}
|
|
872
|
+
return { segments: settled, live: false };
|
|
873
|
+
}
|
|
874
|
+
function findSubagent(segments, agentId, depth = 0) {
|
|
875
|
+
for (const segment of segments) {
|
|
876
|
+
const sub = subagentOf(segment);
|
|
877
|
+
if (!sub) continue;
|
|
878
|
+
if (sub.id === agentId) return segment;
|
|
879
|
+
if (depth >= MAX_DEPTH) continue;
|
|
880
|
+
const found = findSubagent(laneOf(segment).segments, agentId, depth + 1);
|
|
881
|
+
if (found) return found;
|
|
882
|
+
}
|
|
883
|
+
return void 0;
|
|
884
|
+
}
|
|
885
|
+
const fed = /* @__PURE__ */ new WeakMap();
|
|
886
|
+
const ElementBase = typeof HTMLElement === "undefined" ? class {
|
|
887
|
+
} : HTMLElement;
|
|
888
|
+
class AparteSubagentView extends ElementBase {
|
|
889
|
+
static get observedAttributes() {
|
|
890
|
+
return ["for", "message-id", "agent-id", "closable"];
|
|
891
|
+
}
|
|
892
|
+
_chat = null;
|
|
893
|
+
_task = null;
|
|
894
|
+
/** A closable view's head: the sub-agent's name and Close. */
|
|
895
|
+
_head = null;
|
|
896
|
+
_name = null;
|
|
897
|
+
_close = null;
|
|
898
|
+
_drawing = null;
|
|
899
|
+
/** The config the rows were drawn with, and the segment they were drawn from. */
|
|
900
|
+
_config = null;
|
|
901
|
+
_anchor = null;
|
|
902
|
+
_listening = null;
|
|
903
|
+
_stopConfig = null;
|
|
904
|
+
_stopWatching = null;
|
|
905
|
+
_stopRenderers = null;
|
|
906
|
+
_queued = false;
|
|
907
|
+
_releaseQueued = false;
|
|
908
|
+
_warned = "";
|
|
909
|
+
_onConversation = () => this._schedule();
|
|
910
|
+
_onKeydown = (event) => {
|
|
911
|
+
if (event.key !== "Escape" || event.defaultPrevented || !this._closable()) return;
|
|
912
|
+
event.preventDefault();
|
|
913
|
+
this._emitClose();
|
|
914
|
+
};
|
|
915
|
+
/** A renderer registered or taken off on the config the rows were drawn with: the rows it changes are drawn again. */
|
|
916
|
+
_onRenderers = (config) => {
|
|
917
|
+
if (config === this._config) this.refresh();
|
|
918
|
+
};
|
|
919
|
+
_onWrite = (write) => {
|
|
920
|
+
if (fed.has(this) || !this.isConnected) return;
|
|
921
|
+
if (write.messageId !== this.getAttribute("message-id") || write.agentId !== this.getAttribute("agent-id")) return;
|
|
922
|
+
if (!this._matches(write)) return;
|
|
923
|
+
this._draw(write.anchor, this._sourceConfig());
|
|
924
|
+
};
|
|
925
|
+
// ── What the app sets ──────────────────────────────────────────────────
|
|
926
|
+
/** The chat, when it has no `id` for the `for` attribute: an element, or anything with `getMessages()`. */
|
|
927
|
+
get chat() {
|
|
928
|
+
return this._chat;
|
|
929
|
+
}
|
|
930
|
+
set chat(value) {
|
|
931
|
+
this._chat = value;
|
|
932
|
+
if (!this.isConnected) return;
|
|
933
|
+
this._listen();
|
|
934
|
+
this.refresh();
|
|
935
|
+
}
|
|
936
|
+
/** Put the focus on the sub-agent's name, in a `closable` view: where a pane that just opened starts. */
|
|
937
|
+
focusTitle() {
|
|
938
|
+
if (this._name && !this._head?.hidden) this._name.focus();
|
|
939
|
+
}
|
|
940
|
+
/** Read the sub-agent again and draw it: what the element does on every change it hears. */
|
|
941
|
+
refresh() {
|
|
942
|
+
const feed = fed.get(this);
|
|
943
|
+
if (!feed && !this.isConnected) return;
|
|
944
|
+
const anchor = feed ? feed.anchor : this._find();
|
|
945
|
+
this._draw(anchor === null ? this._anchor : anchor, feed ? feed.config : this._sourceConfig());
|
|
946
|
+
}
|
|
947
|
+
// ── Lifecycle ──────────────────────────────────────────────────────────
|
|
948
|
+
connectedCallback() {
|
|
949
|
+
ensureStyles();
|
|
950
|
+
this._build();
|
|
951
|
+
this.addEventListener("keydown", this._onKeydown);
|
|
952
|
+
this._stopRenderers ??= onRenderersChange(this._onRenderers);
|
|
953
|
+
if (!fed.has(this)) {
|
|
954
|
+
this._listen();
|
|
955
|
+
this._stopWatching ??= watchLaneWrites(this._onWrite);
|
|
956
|
+
} else {
|
|
957
|
+
this._stopConfig?.();
|
|
958
|
+
this._stopConfig = subscribeConfigChange(this, () => this._onConfigChange());
|
|
959
|
+
}
|
|
960
|
+
this.refresh();
|
|
961
|
+
}
|
|
962
|
+
disconnectedCallback() {
|
|
963
|
+
this.removeEventListener("keydown", this._onKeydown);
|
|
964
|
+
this._unlisten();
|
|
965
|
+
this._stopWatching?.();
|
|
966
|
+
this._stopWatching = null;
|
|
967
|
+
this._stopConfig?.();
|
|
968
|
+
this._stopConfig = null;
|
|
969
|
+
this._stopRenderers?.();
|
|
970
|
+
this._stopRenderers = null;
|
|
971
|
+
if (this._releaseQueued) return;
|
|
972
|
+
this._releaseQueued = true;
|
|
973
|
+
queueMicrotask(() => {
|
|
974
|
+
this._releaseQueued = false;
|
|
975
|
+
if (this.isConnected) return;
|
|
976
|
+
this._release();
|
|
977
|
+
});
|
|
978
|
+
}
|
|
979
|
+
attributeChangedCallback(_name, previous, value) {
|
|
980
|
+
if (previous === value || !this.isConnected || fed.has(this)) return;
|
|
981
|
+
if (_name === "for") this._listen();
|
|
982
|
+
this.refresh();
|
|
983
|
+
if (_name === "closable" && this._config) this._relabelHead(this._config);
|
|
984
|
+
}
|
|
985
|
+
// ── Reading the conversation ───────────────────────────────────────────
|
|
986
|
+
_source() {
|
|
987
|
+
if (this._chat) return this._chat;
|
|
988
|
+
const id = this.getAttribute("for");
|
|
989
|
+
return id ? this.ownerDocument.getElementById(id) : null;
|
|
990
|
+
}
|
|
991
|
+
_sourceConfig() {
|
|
992
|
+
const source = this._source();
|
|
993
|
+
return resolveConfig(typeof Element !== "undefined" && source instanceof Element ? source : this);
|
|
994
|
+
}
|
|
995
|
+
/** A write from this view's chat: the run's target is the chat, inside it, or named by its id. */
|
|
996
|
+
_matches(write) {
|
|
997
|
+
const source = this._source();
|
|
998
|
+
if (!source) return false;
|
|
999
|
+
if (source === write.target) return true;
|
|
1000
|
+
if (typeof Node !== "undefined" && source instanceof Node && write.target instanceof Node && (source.contains(write.target) || write.target.contains(source))) return true;
|
|
1001
|
+
return write.targetId !== void 0 && source.id === write.targetId;
|
|
1002
|
+
}
|
|
1003
|
+
/**
|
|
1004
|
+
* The segment that stands for the sub-agent in the chat's reply; `undefined` when there is
|
|
1005
|
+
* none, `null` when the chat cannot be read at all.
|
|
1006
|
+
*/
|
|
1007
|
+
_find() {
|
|
1008
|
+
const messageId = this.getAttribute("message-id");
|
|
1009
|
+
const agentId = this.getAttribute("agent-id");
|
|
1010
|
+
const source = this._source();
|
|
1011
|
+
if (!messageId || !agentId || !source) return void 0;
|
|
1012
|
+
const own = source;
|
|
1013
|
+
const reader = typeof own.getMessage === "function" || typeof own.getMessages === "function" ? own : own.querySelector?.("aparte-chat-viewport") ?? null;
|
|
1014
|
+
if (!reader || typeof reader.getMessage !== "function" && typeof reader.getMessages !== "function") return null;
|
|
1015
|
+
let message;
|
|
1016
|
+
try {
|
|
1017
|
+
message = reader.getMessage?.(messageId) ?? reader.getMessages?.()?.find((m) => m.id === messageId);
|
|
1018
|
+
} catch {
|
|
1019
|
+
return null;
|
|
1020
|
+
}
|
|
1021
|
+
const live = lastLaneWrite(messageId, agentId);
|
|
1022
|
+
const anchor = (message ? findSubagent(message.segments ?? [], agentId) : void 0) ?? (live && this._matches(live) ? live.anchor : void 0);
|
|
1023
|
+
const key = `${messageId}\0${agentId}`;
|
|
1024
|
+
if (message && !anchor && !message.isStreaming && message.status !== "pending" && message.status !== "streaming" && this._warned !== key) {
|
|
1025
|
+
this._warned = key;
|
|
1026
|
+
console.warn(`[aparte] <aparte-subagent-view>: the reply "${messageId}" holds no sub-agent "${agentId}".`);
|
|
1027
|
+
}
|
|
1028
|
+
return anchor;
|
|
1029
|
+
}
|
|
1030
|
+
_listen() {
|
|
1031
|
+
const source = this._source();
|
|
1032
|
+
const target = source && typeof source.addEventListener === "function" ? source : null;
|
|
1033
|
+
if (target !== this._listening) {
|
|
1034
|
+
this._unlisten();
|
|
1035
|
+
if (target) for (const name of CONVERSATION_EVENTS) target.addEventListener(name, this._onConversation);
|
|
1036
|
+
this._listening = target;
|
|
1037
|
+
}
|
|
1038
|
+
this._stopConfig?.();
|
|
1039
|
+
const holder2 = typeof Element !== "undefined" && source instanceof Element ? source : this;
|
|
1040
|
+
this._stopConfig = subscribeConfigChange(holder2, () => this._onConfigChange());
|
|
1041
|
+
}
|
|
1042
|
+
_unlisten() {
|
|
1043
|
+
if (this._listening) for (const name of CONVERSATION_EVENTS) this._listening.removeEventListener(name, this._onConversation);
|
|
1044
|
+
this._listening = null;
|
|
1045
|
+
}
|
|
1046
|
+
/** Folds a burst of events (a whole run's end) into one read. */
|
|
1047
|
+
_schedule() {
|
|
1048
|
+
if (this._queued) return;
|
|
1049
|
+
this._queued = true;
|
|
1050
|
+
queueMicrotask(() => {
|
|
1051
|
+
this._queued = false;
|
|
1052
|
+
this.refresh();
|
|
1053
|
+
});
|
|
1054
|
+
}
|
|
1055
|
+
_onConfigChange() {
|
|
1056
|
+
this.refresh();
|
|
1057
|
+
if (this._config) {
|
|
1058
|
+
this._drawing?.relabel(this._config);
|
|
1059
|
+
this._relabelHead(this._config);
|
|
1060
|
+
}
|
|
1061
|
+
}
|
|
1062
|
+
/** A pane of its own, never a line's detail. */
|
|
1063
|
+
_closable() {
|
|
1064
|
+
return this.hasAttribute("closable") && !fed.has(this);
|
|
1065
|
+
}
|
|
1066
|
+
_emitClose() {
|
|
1067
|
+
this.dispatchEvent(new CustomEvent("aparte-subagent-view-close", { bubbles: true, composed: true }));
|
|
1068
|
+
}
|
|
1069
|
+
/** The head's words, in the chat's language: the name, and Close. */
|
|
1070
|
+
_relabelHead(config) {
|
|
1071
|
+
if (!this._head) return;
|
|
1072
|
+
const closable = this._closable();
|
|
1073
|
+
this._head.hidden = !closable;
|
|
1074
|
+
if (!closable) {
|
|
1075
|
+
this._name.textContent = "";
|
|
1076
|
+
return;
|
|
1077
|
+
}
|
|
1078
|
+
const name = (this._anchor ? subagentOf(this._anchor)?.name?.trim() : "") || word(config, "subagentsUnnamed");
|
|
1079
|
+
if (this._name.textContent !== name) this._name.textContent = name;
|
|
1080
|
+
const close = word(config, "subagentsClose");
|
|
1081
|
+
if (this._close.getAttribute("aria-label") !== close) {
|
|
1082
|
+
this._close.setAttribute("aria-label", close);
|
|
1083
|
+
this._close.title = close;
|
|
1084
|
+
this._close.innerHTML = config.getIcon("close");
|
|
1085
|
+
}
|
|
1086
|
+
}
|
|
1087
|
+
// ── Drawing ────────────────────────────────────────────────────────────
|
|
1088
|
+
_build() {
|
|
1089
|
+
if (this._drawing) {
|
|
1090
|
+
if (this._task.parentNode !== this) this.prepend(this._task);
|
|
1091
|
+
if (this._head.parentNode !== this) this.prepend(this._head);
|
|
1092
|
+
if (this._drawing.container.parentNode !== this) this.append(this._drawing.container);
|
|
1093
|
+
return;
|
|
1094
|
+
}
|
|
1095
|
+
const head = this.ownerDocument.createElement("div");
|
|
1096
|
+
head.className = "aparte-subagent-head";
|
|
1097
|
+
head.hidden = true;
|
|
1098
|
+
const name = this.ownerDocument.createElement("h2");
|
|
1099
|
+
name.className = "aparte-subagent-name";
|
|
1100
|
+
name.tabIndex = -1;
|
|
1101
|
+
const close = this.ownerDocument.createElement("button");
|
|
1102
|
+
close.type = "button";
|
|
1103
|
+
close.className = "aparte-btn aparte-btn--icon aparte-btn--sm aparte-subagent-close";
|
|
1104
|
+
close.addEventListener("click", () => this._emitClose());
|
|
1105
|
+
head.append(name, close);
|
|
1106
|
+
this._head = head;
|
|
1107
|
+
this._name = name;
|
|
1108
|
+
this._close = close;
|
|
1109
|
+
const task = this.ownerDocument.createElement("p");
|
|
1110
|
+
task.className = "aparte-subagent-task";
|
|
1111
|
+
task.hidden = true;
|
|
1112
|
+
const list = this.ownerDocument.createElement("div");
|
|
1113
|
+
list.className = "aparte-subagent-transcript";
|
|
1114
|
+
this.append(head, task, list);
|
|
1115
|
+
this._task = task;
|
|
1116
|
+
this._drawing = new LaneDrawing(list);
|
|
1117
|
+
}
|
|
1118
|
+
/** Draw `anchor`'s conversation with `config`. */
|
|
1119
|
+
_draw(anchor, config) {
|
|
1120
|
+
this._build();
|
|
1121
|
+
const drawing = this._drawing;
|
|
1122
|
+
if (this._config && this._config !== config) drawing.clear(this._config);
|
|
1123
|
+
this._config = config;
|
|
1124
|
+
this._anchor = anchor ?? null;
|
|
1125
|
+
this.toggleAttribute("data-inline", fed.has(this));
|
|
1126
|
+
this.toggleAttribute("data-empty", !anchor);
|
|
1127
|
+
const task = (anchor ? subagentOf(anchor)?.description?.trim() : "") ?? "";
|
|
1128
|
+
if (this._task.textContent !== task) this._task.textContent = task;
|
|
1129
|
+
this._task.hidden = !task;
|
|
1130
|
+
this._relabelHead(config);
|
|
1131
|
+
const lane = anchor ? laneOf(anchor) : { segments: [], live: false };
|
|
1132
|
+
runWithConfig(config, () => drawing.sync(lane.segments, config, lane.live));
|
|
1133
|
+
}
|
|
1134
|
+
/** Out of the page for good: every row's renderer cleans up, and the drawing goes. */
|
|
1135
|
+
_release() {
|
|
1136
|
+
if (this._config) this._drawing?.clear(this._config);
|
|
1137
|
+
const feed = fed.get(this);
|
|
1138
|
+
if (feed) {
|
|
1139
|
+
fed.delete(this);
|
|
1140
|
+
feed.onRelease?.();
|
|
1141
|
+
}
|
|
1142
|
+
}
|
|
1143
|
+
}
|
|
1144
|
+
function feedView(view, anchor, config, onRelease) {
|
|
1145
|
+
const release2 = onRelease ?? fed.get(view)?.onRelease;
|
|
1146
|
+
fed.set(view, { anchor, config, ...release2 ? { onRelease: release2 } : {} });
|
|
1147
|
+
view._draw(anchor, config);
|
|
1148
|
+
}
|
|
1149
|
+
if (typeof customElements !== "undefined" && !customElements.get("aparte-subagent-view")) {
|
|
1150
|
+
deferHydrationOf(AparteSubagentView);
|
|
1151
|
+
customElements.define("aparte-subagent-view", AparteSubagentView);
|
|
1152
|
+
}
|
|
1153
|
+
const views = /* @__PURE__ */ new WeakMap();
|
|
1154
|
+
const MAX_VIEWS = 500;
|
|
1155
|
+
const subagentTranscript = (segment) => {
|
|
1156
|
+
if (typeof document === "undefined") return "";
|
|
1157
|
+
const sub = subagentOf(segment);
|
|
1158
|
+
if (!sub) return "";
|
|
1159
|
+
if (!sub.description?.trim() && sub.segments.length === 0) return "";
|
|
1160
|
+
const config = contextConfig();
|
|
1161
|
+
let byKey = views.get(config);
|
|
1162
|
+
if (!byKey) views.set(config, byKey = /* @__PURE__ */ new Map());
|
|
1163
|
+
const key = `${segment.messageId ?? ""}\0${segment.id}\0${sub.id}`;
|
|
1164
|
+
let view = byKey.get(key);
|
|
1165
|
+
if (!view) {
|
|
1166
|
+
view = document.createElement("aparte-subagent-view");
|
|
1167
|
+
const held = view;
|
|
1168
|
+
const map = byKey;
|
|
1169
|
+
feedView(view, segment, config, () => {
|
|
1170
|
+
if (map.get(key) === held) map.delete(key);
|
|
1171
|
+
});
|
|
1172
|
+
map.set(key, view);
|
|
1173
|
+
if (map.size > MAX_VIEWS) {
|
|
1174
|
+
for (const [k, v] of map) {
|
|
1175
|
+
if (map.size <= MAX_VIEWS) break;
|
|
1176
|
+
if (!v.isConnected) map.delete(k);
|
|
1177
|
+
}
|
|
1178
|
+
}
|
|
1179
|
+
return view;
|
|
1180
|
+
}
|
|
1181
|
+
feedView(view, segment, config);
|
|
1182
|
+
return view;
|
|
1183
|
+
};
|
|
1184
|
+
const standIns = /* @__PURE__ */ new WeakSet();
|
|
1185
|
+
function markStandIn(segment) {
|
|
1186
|
+
standIns.add(segment);
|
|
1187
|
+
return segment;
|
|
1188
|
+
}
|
|
1189
|
+
const isOpenCall = (segment) => segment.type === "tool_call" && ["pending", "awaiting-approval"].includes(segment.status ?? "pending");
|
|
1190
|
+
const textOf = (read) => {
|
|
1191
|
+
try {
|
|
1192
|
+
const value = read();
|
|
1193
|
+
return typeof value === "string" ? value.trim() : "";
|
|
1194
|
+
} catch {
|
|
1195
|
+
return "";
|
|
1196
|
+
}
|
|
1197
|
+
};
|
|
1198
|
+
function callLine(call, config) {
|
|
1199
|
+
const name = call.toolCall?.name ?? "";
|
|
1200
|
+
const nested = subagentOf(call);
|
|
1201
|
+
const label = config.getToolDisplay(name)?.label;
|
|
1202
|
+
const shown2 = (label !== void 0 ? textOf(() => typeof label === "function" ? label(call) : label) : "") || nested?.name?.trim() || name;
|
|
1203
|
+
const summary = config.getToolSummary(name);
|
|
1204
|
+
const target = (summary ? textOf(() => summary(call)) : "") || nested?.description?.trim() || "";
|
|
1205
|
+
return [shown2, target].filter(Boolean).join(" ");
|
|
1206
|
+
}
|
|
1207
|
+
function activityOf(sub, config, step) {
|
|
1208
|
+
let latest;
|
|
1209
|
+
for (let i = sub.segments.length - 1; i >= 0; i -= 1) {
|
|
1210
|
+
const segment = sub.segments[i];
|
|
1211
|
+
if (segment.type !== "tool_call") continue;
|
|
1212
|
+
if (isOpenCall(segment)) return callLine(segment, config);
|
|
1213
|
+
latest ??= segment;
|
|
1214
|
+
}
|
|
1215
|
+
if (step) return step;
|
|
1216
|
+
return latest ? callLine(latest, config) : "";
|
|
1217
|
+
}
|
|
1218
|
+
function durationOf(anchor, sub) {
|
|
1219
|
+
const start = segmentTiming(anchor)?.startedAt;
|
|
1220
|
+
if (start === void 0) return void 0;
|
|
1221
|
+
let end;
|
|
1222
|
+
for (const segment of sub.segments) {
|
|
1223
|
+
const timing = segmentTiming(segment);
|
|
1224
|
+
const at = timing?.endedAt ?? timing?.startedAt;
|
|
1225
|
+
if (at !== void 0 && (end === void 0 || at > end)) end = at;
|
|
1226
|
+
}
|
|
1227
|
+
end ??= segmentTiming(anchor)?.endedAt;
|
|
1228
|
+
return end === void 0 || end < start ? void 0 : end - start;
|
|
1229
|
+
}
|
|
1230
|
+
function stateWord(config, status) {
|
|
1231
|
+
switch (status) {
|
|
1232
|
+
case "running":
|
|
1233
|
+
return config.t("toolRunning");
|
|
1234
|
+
case "waiting":
|
|
1235
|
+
return config.t("approvalWaiting");
|
|
1236
|
+
case "done":
|
|
1237
|
+
return config.t("toolCompleted");
|
|
1238
|
+
case "failed":
|
|
1239
|
+
return config.t("toolFailed");
|
|
1240
|
+
default:
|
|
1241
|
+
return config.t("toolStopped");
|
|
1242
|
+
}
|
|
1243
|
+
}
|
|
1244
|
+
function createLineProvider(options = {}) {
|
|
1245
|
+
return (segment) => {
|
|
1246
|
+
const sub = subagentOf(segment);
|
|
1247
|
+
if (!sub) return void 0;
|
|
1248
|
+
const config = contextConfig();
|
|
1249
|
+
const standIn = standIns.has(segment);
|
|
1250
|
+
const status = subagentStatus(segment);
|
|
1251
|
+
const live = status === "running" || status === "waiting";
|
|
1252
|
+
const line = {};
|
|
1253
|
+
const name = sub.name?.trim();
|
|
1254
|
+
if (name) line.label = name;
|
|
1255
|
+
else if (standIn) line.label = word(config, "subagentsUnnamed");
|
|
1256
|
+
const task = sub.description?.trim();
|
|
1257
|
+
if (task) line.target = task;
|
|
1258
|
+
const summary = () => {
|
|
1259
|
+
if (options.counts === false) return "";
|
|
1260
|
+
const parts = [];
|
|
1261
|
+
const tools = sub.segments.filter((s) => s.type === "tool_call").length;
|
|
1262
|
+
if (tools > 0) parts.push(toolCount(config, tools));
|
|
1263
|
+
const ms = durationOf(segment, sub);
|
|
1264
|
+
if (ms !== void 0) {
|
|
1265
|
+
const tag = config.getLocale().tag;
|
|
1266
|
+
parts.push(formatDuration(ms, typeof tag === "string" && tag ? tag : void 0));
|
|
1267
|
+
}
|
|
1268
|
+
return parts.join(" · ");
|
|
1269
|
+
};
|
|
1270
|
+
const note = [];
|
|
1271
|
+
if (!standIn && statusOfCall(segment.status) !== status) note.push(stateWord(config, status));
|
|
1272
|
+
if (options.activity !== false) {
|
|
1273
|
+
line.activity = live ? activityOf(sub, config, currentStep(segment.messageId, sub.id)) || word(config, "subagentsStarting") : summary() || stateWord(config, status);
|
|
1274
|
+
} else if (!live) {
|
|
1275
|
+
const done = summary();
|
|
1276
|
+
if (done) note.push(done);
|
|
1277
|
+
}
|
|
1278
|
+
if (note.length > 0) line.note = note.join(" · ");
|
|
1279
|
+
const details = options.details;
|
|
1280
|
+
line.details = typeof details === "function" ? details : details === "transcript" ? subagentTranscript : false;
|
|
1281
|
+
if (options.open === "event") {
|
|
1282
|
+
const shownName = name || word(config, "subagentsUnnamed");
|
|
1283
|
+
line.open = {
|
|
1284
|
+
label: word(config, "subagentsOpen").replace("{name}", () => shownName),
|
|
1285
|
+
current: isShown(segment.messageId, sub.id),
|
|
1286
|
+
onOpen: (row) => dispatchOpen(row, segment, sub, standIn)
|
|
1287
|
+
};
|
|
1288
|
+
}
|
|
1289
|
+
return line;
|
|
1290
|
+
};
|
|
1291
|
+
}
|
|
1292
|
+
const UNNAMED_AGENT = "subagent";
|
|
1293
|
+
function standInCall(segment) {
|
|
1294
|
+
const sub = subagentOf(segment);
|
|
1295
|
+
const status = subagentStatus(segment);
|
|
1296
|
+
const call = {
|
|
1297
|
+
id: segment.id,
|
|
1298
|
+
type: "tool_call",
|
|
1299
|
+
...segment.messageId !== void 0 ? { messageId: segment.messageId } : {},
|
|
1300
|
+
...segment.index !== void 0 ? { index: segment.index } : {},
|
|
1301
|
+
// Its timing is the segment's: the line's duration reads it.
|
|
1302
|
+
...segment.meta !== void 0 ? { meta: segment.meta } : {},
|
|
1303
|
+
...segment.collapsed !== void 0 ? { collapsed: segment.collapsed } : {},
|
|
1304
|
+
toolCall: { id: sub?.id ?? segment.id, name: sub?.name?.trim() || UNNAMED_AGENT, input: {} },
|
|
1305
|
+
status: callStatusOf(status),
|
|
1306
|
+
// A failure reads as a failed call's does.
|
|
1307
|
+
...status === "failed" && sub?.error ? { result: sub.error } : {},
|
|
1308
|
+
subagent: segment.subagent
|
|
1309
|
+
};
|
|
1310
|
+
return markStandIn(call);
|
|
1311
|
+
}
|
|
1312
|
+
const toolRow = () => getSegmentRenderer("tool_call", contextConfig());
|
|
1313
|
+
function replace(element, out, segment) {
|
|
1314
|
+
const tpl = document.createElement("template");
|
|
1315
|
+
tpl.innerHTML = typeof out === "string" ? out.trim() : "";
|
|
1316
|
+
const next = typeof out === "string" ? tpl.content.firstElementChild : out;
|
|
1317
|
+
if (!next) return null;
|
|
1318
|
+
if (!next.hasAttribute("data-segment-id")) next.setAttribute("data-segment-id", segment.id);
|
|
1319
|
+
element.replaceWith(next);
|
|
1320
|
+
return next;
|
|
1321
|
+
}
|
|
1322
|
+
const subagentRenderer = {
|
|
1323
|
+
type: SUBAGENT_SEGMENT_TYPE,
|
|
1324
|
+
render: (segment) => toolRow()?.render(standInCall(segment)) ?? "",
|
|
1325
|
+
setup: (element, segment) => toolRow()?.setup?.(element, standInCall(segment)),
|
|
1326
|
+
update: (element, segment) => {
|
|
1327
|
+
const row = toolRow();
|
|
1328
|
+
if (!row) return;
|
|
1329
|
+
const call = standInCall(segment);
|
|
1330
|
+
if (row.update) {
|
|
1331
|
+
row.update(element, call);
|
|
1332
|
+
return;
|
|
1333
|
+
}
|
|
1334
|
+
row.cleanup?.(element, call);
|
|
1335
|
+
const next = replace(element, row.render(call), segment);
|
|
1336
|
+
if (next) row.setup?.(next, call);
|
|
1337
|
+
},
|
|
1338
|
+
relabel: (element, segment) => toolRow()?.relabel?.(element, standInCall(segment)),
|
|
1339
|
+
cleanup: (element, segment) => toolRow()?.cleanup?.(element, standInCall(segment)),
|
|
1340
|
+
// The plugin's sheet (the transcript's, the view's): core puts it on the page with the
|
|
1341
|
+
// renderers' own, once, unless a view already did.
|
|
1342
|
+
getStyles: rendererStyles
|
|
1343
|
+
};
|
|
1344
|
+
const installed = /* @__PURE__ */ new WeakMap();
|
|
1345
|
+
function setupSubagents(options = {}, config = aparteGlobalConfig) {
|
|
1346
|
+
installed.get(config)?.();
|
|
1347
|
+
const lanes = createLanes();
|
|
1348
|
+
const provider = createLineProvider({ ...options });
|
|
1349
|
+
const stopObserving = config.observeRunEvents(lanes.observe);
|
|
1350
|
+
config.setToolLineProvider(provider);
|
|
1351
|
+
registerSegmentRenderer(subagentRenderer, config);
|
|
1352
|
+
let disposed = false;
|
|
1353
|
+
const dispose = () => {
|
|
1354
|
+
if (disposed) return;
|
|
1355
|
+
disposed = true;
|
|
1356
|
+
stopObserving();
|
|
1357
|
+
lanes.dispose();
|
|
1358
|
+
if (config.getToolLineProvider() === provider) config.setToolLineProvider(null);
|
|
1359
|
+
if (getSegmentRenderer(SUBAGENT_SEGMENT_TYPE, config) === subagentRenderer) unregisterSegmentRenderer(SUBAGENT_SEGMENT_TYPE, config);
|
|
1360
|
+
if (installed.get(config) === dispose) installed.delete(config);
|
|
1361
|
+
};
|
|
1362
|
+
installed.set(config, dispose);
|
|
1363
|
+
return dispose;
|
|
1364
|
+
}
|
|
1365
|
+
function connectSubagentView(options) {
|
|
1366
|
+
const { chat, view, split } = options;
|
|
1367
|
+
const viewPane = options.pane ?? "end";
|
|
1368
|
+
const chatPane = viewPane === "end" ? "start" : "end";
|
|
1369
|
+
const owner = {};
|
|
1370
|
+
if (!view.chat && !view.getAttribute("for")) view.chat = chat;
|
|
1371
|
+
const wasClosable = view.hasAttribute("closable");
|
|
1372
|
+
view.setAttribute("closable", "");
|
|
1373
|
+
let opener = null;
|
|
1374
|
+
let opened = null;
|
|
1375
|
+
let isOpen2 = false;
|
|
1376
|
+
const roots = () => chat.contains(view) ? [chat] : [chat, view];
|
|
1377
|
+
const mark = (row) => {
|
|
1378
|
+
for (const root of roots()) unmarkAll(root);
|
|
1379
|
+
if (row) markRow(row, true);
|
|
1380
|
+
};
|
|
1381
|
+
const fold = () => {
|
|
1382
|
+
if (split) {
|
|
1383
|
+
split.pane = chatPane;
|
|
1384
|
+
split.single = true;
|
|
1385
|
+
}
|
|
1386
|
+
isOpen2 = false;
|
|
1387
|
+
options.onClose?.();
|
|
1388
|
+
};
|
|
1389
|
+
const onOpen = (event) => {
|
|
1390
|
+
const detail = event.detail;
|
|
1391
|
+
if (!detail?.messageId || !detail.agentId) return;
|
|
1392
|
+
const row = event.target instanceof HTMLElement ? event.target : null;
|
|
1393
|
+
opener = row;
|
|
1394
|
+
opened = detail;
|
|
1395
|
+
setShown(owner, { messageId: detail.messageId, agentId: detail.agentId });
|
|
1396
|
+
mark(row);
|
|
1397
|
+
view.setAttribute("message-id", detail.messageId);
|
|
1398
|
+
view.setAttribute("agent-id", detail.agentId);
|
|
1399
|
+
if (split) {
|
|
1400
|
+
split.single = false;
|
|
1401
|
+
split.pane = viewPane;
|
|
1402
|
+
}
|
|
1403
|
+
isOpen2 = true;
|
|
1404
|
+
options.onOpen?.(detail);
|
|
1405
|
+
view.focusTitle();
|
|
1406
|
+
};
|
|
1407
|
+
const close = (giveFocusBack) => {
|
|
1408
|
+
const active = view.ownerDocument.activeElement;
|
|
1409
|
+
const adrift = !active || active === view.ownerDocument.body || view.contains(active);
|
|
1410
|
+
setShown(owner, null);
|
|
1411
|
+
mark(null);
|
|
1412
|
+
fold();
|
|
1413
|
+
if (giveFocusBack) {
|
|
1414
|
+
const line = opener?.isConnected ? opener : lineOf(opened);
|
|
1415
|
+
const button = line ? openButtonOf(line) : null;
|
|
1416
|
+
if (button) button.focus();
|
|
1417
|
+
else if (adrift) focusComposer(chat);
|
|
1418
|
+
}
|
|
1419
|
+
opener = null;
|
|
1420
|
+
opened = null;
|
|
1421
|
+
};
|
|
1422
|
+
const lineOf = (detail) => {
|
|
1423
|
+
if (!detail) return null;
|
|
1424
|
+
const id = detail.toolCallId ?? detail.agentId;
|
|
1425
|
+
for (const root of roots()) {
|
|
1426
|
+
for (const row of root.querySelectorAll(".aparte-segment-tool-call[data-tool-call-id]")) {
|
|
1427
|
+
if (row.getAttribute("data-tool-call-id") === id && openButtonOf(row)) return row;
|
|
1428
|
+
}
|
|
1429
|
+
}
|
|
1430
|
+
return null;
|
|
1431
|
+
};
|
|
1432
|
+
const onClose = () => close(true);
|
|
1433
|
+
const onLoaded = () => {
|
|
1434
|
+
if (isOpen2 || opened) close(false);
|
|
1435
|
+
view.removeAttribute("message-id");
|
|
1436
|
+
view.removeAttribute("agent-id");
|
|
1437
|
+
};
|
|
1438
|
+
chat.addEventListener("aparte-subagent-open", onOpen);
|
|
1439
|
+
const viewListens = !chat.contains(view);
|
|
1440
|
+
if (viewListens) view.addEventListener("aparte-subagent-open", onOpen);
|
|
1441
|
+
view.addEventListener("aparte-subagent-view-close", onClose);
|
|
1442
|
+
for (const name of ["aparte-conversation-loaded", "aparte-reset-done"]) chat.addEventListener(name, onLoaded);
|
|
1443
|
+
if (split && !view.getAttribute("agent-id")) {
|
|
1444
|
+
split.pane = chatPane;
|
|
1445
|
+
split.single = true;
|
|
1446
|
+
}
|
|
1447
|
+
return () => {
|
|
1448
|
+
chat.removeEventListener("aparte-subagent-open", onOpen);
|
|
1449
|
+
if (viewListens) view.removeEventListener("aparte-subagent-open", onOpen);
|
|
1450
|
+
view.removeEventListener("aparte-subagent-view-close", onClose);
|
|
1451
|
+
for (const name of ["aparte-conversation-loaded", "aparte-reset-done"]) chat.removeEventListener(name, onLoaded);
|
|
1452
|
+
setShown(owner, null);
|
|
1453
|
+
mark(null);
|
|
1454
|
+
if (!wasClosable) view.removeAttribute("closable");
|
|
1455
|
+
};
|
|
1456
|
+
}
|
|
1457
|
+
function focusComposer(chat) {
|
|
1458
|
+
chat.querySelector("aparte-composer-input")?.focusEnd?.();
|
|
1459
|
+
}
|
|
1460
|
+
export {
|
|
1461
|
+
AparteSubagentView as A,
|
|
1462
|
+
SUBAGENT_SEGMENT_TYPE as S,
|
|
1463
|
+
subagentOf as a,
|
|
1464
|
+
subagentStatus as b,
|
|
1465
|
+
connectSubagentView as c,
|
|
1466
|
+
subagentTranscript as d,
|
|
1467
|
+
setupSubagents as s
|
|
1468
|
+
};
|
|
1469
|
+
//# sourceMappingURL=connect-65jiVmf_.js.map
|