opencode-subagents-sidebar-plugin 0.1.2 → 0.1.5

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/dist/tui.js CHANGED
@@ -1,173 +1,485 @@
1
+ import { use as _$use } from "@opentui/solid";
1
2
  import { createComponent as _$createComponent } from "@opentui/solid";
2
3
  import { effect as _$effect } from "@opentui/solid";
4
+ import { memo as _$memo } from "@opentui/solid";
5
+ import { insert as _$insert } from "@opentui/solid";
3
6
  import { createTextNode as _$createTextNode } from "@opentui/solid";
4
7
  import { insertNode as _$insertNode } from "@opentui/solid";
5
- import { insert as _$insert } from "@opentui/solid";
6
- import { memo as _$memo } from "@opentui/solid";
7
8
  import { setProp as _$setProp } from "@opentui/solid";
8
9
  import { createElement as _$createElement } from "@opentui/solid";
9
10
  /** @jsxImportSource @opentui/solid */
10
11
 
11
- import { activityForPart, elapsedLabel, normalizeMeaningful } from "./activity.js";
12
- import { isVisibleTask, taskSessionID } from "./tasks.js";
12
+ import { Portal, useTerminalDimensions } from "@opentui/solid";
13
+ import { elapsedLabel } from "./activity.js";
14
+ import { placePopover } from "./popover.js";
15
+ import { canOpenTaskSession, isTaskPart, isVisibleTask, latestAssistantText, resolveTaskModel, taskAgent, taskDescription, taskModeLabel, taskModelLabel, taskSessionID, taskStatusLabel, taskVariantLabel } from "./tasks.js";
13
16
  import { createEffect, createMemo, createSignal, For, on, onCleanup, onMount, Show } from "solid-js";
14
- function stringValue(value) {
15
- return typeof value === "string" && value.trim() ? value.trim() : undefined;
16
- }
17
- function objectValue(value) {
18
- return value && typeof value === "object" && !Array.isArray(value) ? value : {};
19
- }
20
- function taskAgent(part) {
21
- return stringValue(part.state.input.subagent_type) ?? "subagent";
22
- }
23
- function taskDescription(part) {
24
- return normalizeMeaningful(part.state.input.description) ?? normalizeMeaningful(objectValue(part.state).title) ?? "Working";
17
+ const POPOVER_SIZE = {
18
+ width: 52,
19
+ height: 14
20
+ };
21
+ const POPOVER_Z_INDEX = 100;
22
+ function resolvedModel(api, part, session, sessionID) {
23
+ return resolveTaskModel(part, session, sessionID ? api.state.session.messages(sessionID) : []);
25
24
  }
26
- function taskModel(part) {
27
- const metadata = objectValue(objectValue(part.state).metadata);
28
- const model = objectValue(metadata.model);
29
- const providerID = stringValue(model.providerID);
30
- const modelID = stringValue(model.modelID);
31
- return providerID && modelID ? {
32
- providerID,
33
- modelID
34
- } : undefined;
25
+ function modelName(api, model) {
26
+ const name = model ? api.state.provider.find(provider => provider.id === model.providerID)?.models[model.modelID]?.name : undefined;
27
+ return taskModelLabel(model, name);
35
28
  }
36
- function latestActivity(api, sessionID, fallback) {
37
- if (!sessionID) return fallback;
38
- const messages = api.state.session.messages(sessionID);
39
- for (let messageIndex = messages.length - 1; messageIndex >= 0; messageIndex -= 1) {
40
- const message = messages[messageIndex];
41
- if (message?.role !== "assistant") continue;
42
- const parts = api.state.part(message.id);
43
- for (let partIndex = parts.length - 1; partIndex >= 0; partIndex -= 1) {
44
- const candidate = parts[partIndex];
45
- if (!candidate) continue;
46
- const activity = activityForPart(candidate);
47
- if (activity) return activity;
29
+ function TaskPopover(props) {
30
+ const viewport = useTerminalDimensions();
31
+ const [hydratedChildMessages, setHydratedChildMessages] = createSignal();
32
+ const placement = createMemo(() => placePopover(props.ownership().anchor, viewport(), POPOVER_SIZE));
33
+ const callID = () => props.ownership().callID;
34
+ let portalWrapper;
35
+ let hydrationRequest = 0;
36
+ let requestedHydration;
37
+ const fallback = () => {
38
+ const known = props.fallbackFor(callID());
39
+ if (known) return known;
40
+ const sessionID = taskSessionID(props.fallbackPart);
41
+ return {
42
+ part: props.fallbackPart,
43
+ ...(sessionID ? {
44
+ sessionID
45
+ } : {})
46
+ };
47
+ };
48
+ const part = createMemo(() => {
49
+ props.revision();
50
+ return props.tasksByCallID().get(callID()) ?? fallback().part;
51
+ });
52
+ const childSessionID = () => taskSessionID(part() ?? fallback().part) ?? fallback().sessionID;
53
+ const childSession = () => {
54
+ props.revision();
55
+ const sessionID = childSessionID();
56
+ return (sessionID ? props.api.state.session.get(sessionID) : undefined) ?? fallback().session;
57
+ };
58
+ const model = createMemo(() => {
59
+ props.revision();
60
+ return resolvedModel(props.api, part() ?? fallback().part, childSession(), childSessionID());
61
+ });
62
+ const canOpen = () => canOpenTaskSession(childSessionID(), props.currentSessionID);
63
+ const liveChildMessages = createMemo(() => {
64
+ props.revision();
65
+ const sessionID = childSessionID();
66
+ if (!sessionID) return undefined;
67
+ const messages = props.api.state.session.messages(sessionID);
68
+ const parts = new Map();
69
+ let hasParts = false;
70
+ for (const message of messages) {
71
+ const messageParts = props.api.state.part(message.id);
72
+ parts.set(message.id, messageParts);
73
+ if (messageParts.length) hasParts = true;
48
74
  }
49
- }
50
- return fallback;
51
- }
52
- function modelLabel(api, part) {
53
- let model = taskModel(part);
54
- const childSessionID = taskSessionID(part);
55
- if (!model && childSessionID) {
56
- const messages = api.state.session.messages(childSessionID);
57
- for (let index = messages.length - 1; index >= 0; index -= 1) {
58
- const message = messages[index];
59
- if (message?.role === "assistant") {
60
- model = {
61
- providerID: message.providerID,
62
- modelID: message.modelID
63
- };
64
- break;
75
+ return {
76
+ sessionID,
77
+ messages,
78
+ parts,
79
+ hasParts
80
+ };
81
+ });
82
+ const ownsHydration = (token, sessionID, request) => {
83
+ if (props.api.lifecycle.signal.aborted || request !== hydrationRequest) return false;
84
+ return props.ownership().token === token && childSessionID() === sessionID;
85
+ };
86
+ const hydrateChildMessages = async (token, sessionID, request) => {
87
+ try {
88
+ const result = await props.api.client.session.messages({
89
+ sessionID,
90
+ directory: props.api.state.path.directory
91
+ }, {
92
+ throwOnError: true
93
+ });
94
+ if (!ownsHydration(token, sessionID, request)) return;
95
+ if (liveChildMessages()?.hasParts) return;
96
+ setHydratedChildMessages({
97
+ token,
98
+ sessionID,
99
+ messages: result.data
100
+ });
101
+ } catch {
102
+ if (ownsHydration(token, sessionID, request)) requestedHydration = undefined;
103
+ }
104
+ };
105
+ createEffect(() => {
106
+ const ownership = props.ownership();
107
+ const sessionID = childSessionID();
108
+ const live = liveChildMessages();
109
+ if (!sessionID || live?.hasParts) {
110
+ if (requestedHydration) {
111
+ requestedHydration = undefined;
112
+ hydrationRequest += 1;
65
113
  }
114
+ return;
66
115
  }
67
- }
68
- if (!model) return "starting…";
69
- return api.state.provider.find(provider => provider.id === model.providerID)?.models[model.modelID]?.name ?? model.modelID;
116
+ if (requestedHydration?.token === ownership.token && requestedHydration.sessionID === sessionID) return;
117
+ const request = ++hydrationRequest;
118
+ requestedHydration = {
119
+ token: ownership.token,
120
+ sessionID
121
+ };
122
+ setHydratedChildMessages(undefined);
123
+ void hydrateChildMessages(ownership.token, sessionID, request);
124
+ });
125
+ const latestResponse = createMemo(() => {
126
+ const ownership = props.ownership();
127
+ const sessionID = childSessionID();
128
+ if (!sessionID) return undefined;
129
+ const live = liveChildMessages();
130
+ if (live?.hasParts) return latestAssistantText(live.messages, messageID => live.parts.get(messageID) ?? []);
131
+ const hydrated = hydratedChildMessages();
132
+ if (!hydrated || hydrated.token !== ownership.token || hydrated.sessionID !== sessionID) return undefined;
133
+ const parts = new Map(hydrated.messages.map(message => [message.info.id, message.parts]));
134
+ return latestAssistantText(hydrated.messages.map(message => message.info), messageID => parts.get(messageID) ?? []);
135
+ });
136
+ const responseLabel = () => latestResponse() ?? (childSessionID() ? "Waiting for assistant response" : "Waiting for child session");
137
+ const openSession = () => {
138
+ const sessionID = childSessionID();
139
+ if (!canOpen() || !sessionID) return;
140
+ props.api.route.navigate("session", {
141
+ sessionID
142
+ });
143
+ props.close();
144
+ };
145
+ const applyPortalPlacement = (next = placement()) => {
146
+ const wrapper = portalWrapper;
147
+ if (!wrapper || wrapper.isDestroyed) return;
148
+ wrapper.position = "absolute";
149
+ wrapper.left = next.left;
150
+ wrapper.top = next.top;
151
+ wrapper.width = next.width;
152
+ wrapper.height = next.height;
153
+ wrapper.zIndex = POPOVER_Z_INDEX;
154
+ wrapper.focusable = false;
155
+ };
156
+ createEffect(() => applyPortalPlacement(placement()));
157
+ onCleanup(() => {
158
+ hydrationRequest += 1;
159
+ requestedHydration = undefined;
160
+ portalWrapper = undefined;
161
+ });
162
+ return _$createComponent(Portal, {
163
+ get mount() {
164
+ return props.api.renderer.root;
165
+ },
166
+ ref: element => {
167
+ portalWrapper = element;
168
+ applyPortalPlacement();
169
+ },
170
+ get children() {
171
+ var _el$ = _$createElement("box"),
172
+ _el$2 = _$createElement("text"),
173
+ _el$3 = _$createElement("b"),
174
+ _el$5 = _$createElement("text"),
175
+ _el$6 = _$createTextNode(`Agent: `),
176
+ _el$7 = _$createElement("text"),
177
+ _el$8 = _$createTextNode(`Assigned: `),
178
+ _el$9 = _$createElement("text"),
179
+ _el$0 = _$createTextNode(`Latest: `),
180
+ _el$1 = _$createElement("text"),
181
+ _el$10 = _$createTextNode(`Status: `),
182
+ _el$11 = _$createElement("text"),
183
+ _el$12 = _$createTextNode(`Model: `),
184
+ _el$13 = _$createTextNode(` · `),
185
+ _el$14 = _$createElement("text"),
186
+ _el$15 = _$createTextNode(` · `),
187
+ _el$16 = _$createElement("text"),
188
+ _el$17 = _$createTextNode(`Child: `),
189
+ _el$18 = _$createElement("box"),
190
+ _el$19 = _$createElement("box"),
191
+ _el$20 = _$createElement("text"),
192
+ _el$21 = _$createElement("b"),
193
+ _el$23 = _$createElement("box"),
194
+ _el$24 = _$createElement("text"),
195
+ _el$25 = _$createElement("b");
196
+ _$insertNode(_el$, _el$2);
197
+ _$insertNode(_el$, _el$5);
198
+ _$insertNode(_el$, _el$7);
199
+ _$insertNode(_el$, _el$9);
200
+ _$insertNode(_el$, _el$1);
201
+ _$insertNode(_el$, _el$11);
202
+ _$insertNode(_el$, _el$14);
203
+ _$insertNode(_el$, _el$16);
204
+ _$insertNode(_el$, _el$18);
205
+ _$setProp(_el$, "width", "100%");
206
+ _$setProp(_el$, "height", "100%");
207
+ _$setProp(_el$, "focusable", false);
208
+ _$setProp(_el$, "live", true);
209
+ _$setProp(_el$, "border", true);
210
+ _$setProp(_el$, "padding", 1);
211
+ _$setProp(_el$, "flexDirection", "column");
212
+ _$setProp(_el$, "overflow", "hidden");
213
+ _$insertNode(_el$2, _el$3);
214
+ _$setProp(_el$2, "wrapMode", "none");
215
+ _$setProp(_el$2, "truncate", true);
216
+ _$insertNode(_el$3, _$createTextNode(`Subagent details`));
217
+ _$insertNode(_el$5, _el$6);
218
+ _$setProp(_el$5, "wrapMode", "none");
219
+ _$setProp(_el$5, "truncate", true);
220
+ _$insert(_el$5, () => taskAgent(part() ?? fallback().part), null);
221
+ _$insertNode(_el$7, _el$8);
222
+ _$setProp(_el$7, "wrapMode", "none");
223
+ _$setProp(_el$7, "truncate", true);
224
+ _$insert(_el$7, () => taskDescription(part() ?? fallback().part), null);
225
+ _$insertNode(_el$9, _el$0);
226
+ _$setProp(_el$9, "wrapMode", "none");
227
+ _$setProp(_el$9, "truncate", true);
228
+ _$insert(_el$9, responseLabel, null);
229
+ _$insertNode(_el$1, _el$10);
230
+ _$setProp(_el$1, "wrapMode", "none");
231
+ _$setProp(_el$1, "truncate", true);
232
+ _$insert(_el$1, () => taskStatusLabel(part() ?? fallback().part, childSessionID() ? props.api.state.session.status(childSessionID()) : undefined), null);
233
+ _$insertNode(_el$11, _el$12);
234
+ _$insertNode(_el$11, _el$13);
235
+ _$setProp(_el$11, "wrapMode", "none");
236
+ _$setProp(_el$11, "truncate", true);
237
+ _$insert(_el$11, () => modelName(props.api, model()), _el$13);
238
+ _$insert(_el$11, () => taskVariantLabel(model()) ?? "pending", null);
239
+ _$insertNode(_el$14, _el$15);
240
+ _$setProp(_el$14, "wrapMode", "none");
241
+ _$setProp(_el$14, "truncate", true);
242
+ _$insert(_el$14, () => taskModeLabel(part() ?? fallback().part), _el$15);
243
+ _$insert(_el$14, () => elapsedLabel(props.now() - props.startedAt(part() ?? fallback().part)), null);
244
+ _$insertNode(_el$16, _el$17);
245
+ _$setProp(_el$16, "wrapMode", "none");
246
+ _$setProp(_el$16, "truncate", true);
247
+ _$insert(_el$16, () => childSessionID() ?? "Waiting for child session", null);
248
+ _$insertNode(_el$18, _el$19);
249
+ _$insertNode(_el$18, _el$23);
250
+ _$setProp(_el$18, "flexDirection", "row");
251
+ _$setProp(_el$18, "gap", 2);
252
+ _$insertNode(_el$19, _el$20);
253
+ _$setProp(_el$19, "onMouseUp", openSession);
254
+ _$insertNode(_el$20, _el$21);
255
+ _$insertNode(_el$21, _$createTextNode(`Open Session`));
256
+ _$insertNode(_el$23, _el$24);
257
+ _$insertNode(_el$24, _el$25);
258
+ _$insertNode(_el$25, _$createTextNode(`Close`));
259
+ _$effect(_p$ => {
260
+ var _v$ = props.api.theme.current.borderActive,
261
+ _v$2 = props.api.theme.current.backgroundPanel,
262
+ _v$3 = props.api.theme.current.text,
263
+ _v$4 = props.api.theme.current.text,
264
+ _v$5 = props.api.theme.current.textMuted,
265
+ _v$6 = props.api.theme.current.textMuted,
266
+ _v$7 = props.api.theme.current.textMuted,
267
+ _v$8 = props.api.theme.current.textMuted,
268
+ _v$9 = props.api.theme.current.textMuted,
269
+ _v$0 = props.api.theme.current.textMuted,
270
+ _v$1 = canOpen() ? props.api.theme.current.primary : props.api.theme.current.textMuted,
271
+ _v$10 = props.close,
272
+ _v$11 = props.api.theme.current.primary;
273
+ _v$ !== _p$.e && (_p$.e = _$setProp(_el$, "borderColor", _v$, _p$.e));
274
+ _v$2 !== _p$.t && (_p$.t = _$setProp(_el$, "backgroundColor", _v$2, _p$.t));
275
+ _v$3 !== _p$.a && (_p$.a = _$setProp(_el$2, "fg", _v$3, _p$.a));
276
+ _v$4 !== _p$.o && (_p$.o = _$setProp(_el$5, "fg", _v$4, _p$.o));
277
+ _v$5 !== _p$.i && (_p$.i = _$setProp(_el$7, "fg", _v$5, _p$.i));
278
+ _v$6 !== _p$.n && (_p$.n = _$setProp(_el$9, "fg", _v$6, _p$.n));
279
+ _v$7 !== _p$.s && (_p$.s = _$setProp(_el$1, "fg", _v$7, _p$.s));
280
+ _v$8 !== _p$.h && (_p$.h = _$setProp(_el$11, "fg", _v$8, _p$.h));
281
+ _v$9 !== _p$.r && (_p$.r = _$setProp(_el$14, "fg", _v$9, _p$.r));
282
+ _v$0 !== _p$.d && (_p$.d = _$setProp(_el$16, "fg", _v$0, _p$.d));
283
+ _v$1 !== _p$.l && (_p$.l = _$setProp(_el$20, "fg", _v$1, _p$.l));
284
+ _v$10 !== _p$.u && (_p$.u = _$setProp(_el$23, "onMouseUp", _v$10, _p$.u));
285
+ _v$11 !== _p$.c && (_p$.c = _$setProp(_el$24, "fg", _v$11, _p$.c));
286
+ return _p$;
287
+ }, {
288
+ e: undefined,
289
+ t: undefined,
290
+ a: undefined,
291
+ o: undefined,
292
+ i: undefined,
293
+ n: undefined,
294
+ s: undefined,
295
+ h: undefined,
296
+ r: undefined,
297
+ d: undefined,
298
+ l: undefined,
299
+ u: undefined,
300
+ c: undefined
301
+ });
302
+ return _el$;
303
+ }
304
+ });
70
305
  }
71
306
  function TaskCard(props) {
72
307
  // The call ID is stable, while the actual ToolPart is re-read after each event.
73
308
  // This prevents cards from retaining a stale part object after a state update.
74
309
  const livePart = createMemo(() => props.tasksByCallID().get(props.callID));
75
- const childSessionID = () => taskSessionID(livePart());
76
- const isCurrent = () => childSessionID() === props.currentSessionID;
77
- const model = () => {
310
+ const part = () => livePart() ?? props.fallbackFor(props.callID)?.part;
311
+ const childSession = () => {
78
312
  props.revision();
79
- return modelLabel(props.api, livePart());
313
+ const current = part();
314
+ if (!current) return undefined;
315
+ const sessionID = taskSessionID(current);
316
+ return sessionID ? props.api.state.session.get(sessionID) : undefined;
80
317
  };
81
- const activity = () => {
318
+ const model = () => {
82
319
  props.revision();
83
- return latestActivity(props.api, childSessionID(), taskDescription(livePart()));
320
+ const current = part();
321
+ return current ? resolvedModel(props.api, current, childSession(), taskSessionID(current)) : undefined;
322
+ };
323
+ const elapsed = () => {
324
+ const current = part();
325
+ return current ? elapsedLabel(props.now() - props.startedAt(current)) : "";
326
+ };
327
+ let card;
328
+ let lastAnchor;
329
+ const anchor = () => card && {
330
+ x: card.screenX,
331
+ y: card.screenY,
332
+ width: card.width,
333
+ height: card.height
334
+ };
335
+ const updateAnchor = () => {
336
+ const current = anchor();
337
+ if (!current || lastAnchor && lastAnchor.x === current.x && lastAnchor.y === current.y && lastAnchor.width === current.width && lastAnchor.height === current.height) {
338
+ return;
339
+ }
340
+ lastAnchor = current;
341
+ props.updatePopupAnchor(props.callID, current);
342
+ };
343
+ onCleanup(() => {
344
+ // The popover owns its last reported rect, so losing this card must not
345
+ // reset its placement before the user explicitly closes it.
346
+ card = undefined;
347
+ lastAnchor = undefined;
348
+ });
349
+ const openDetails = () => {
350
+ if (props.api.renderer.getSelection()?.getSelectedText()) return;
351
+ const part = livePart();
352
+ const currentAnchor = anchor();
353
+ if (!part || !currentAnchor) return;
354
+ props.remember(part);
355
+ props.openPopup(props.callID, currentAnchor);
84
356
  };
85
- const elapsed = () => elapsedLabel(props.now() - props.startedAt(livePart()));
86
357
  return (() => {
87
- var _el$ = _$createElement("box"),
88
- _el$2 = _$createElement("box"),
89
- _el$3 = _$createElement("text"),
90
- _el$4 = _$createElement("span"),
91
- _el$5 = _$createElement("span"),
92
- _el$6 = _$createTextNode(` `),
93
- _el$7 = _$createElement("b"),
94
- _el$8 = _$createElement("text"),
95
- _el$9 = _$createElement("box"),
96
- _el$0 = _$createElement("text"),
97
- _el$1 = _$createElement("text");
98
- _$insertNode(_el$, _el$2);
99
- _$insertNode(_el$, _el$9);
100
- _$setProp(_el$, "width", "100%");
101
- _$setProp(_el$, "height", 2);
102
- _$setProp(_el$, "overflow", "hidden");
103
- _$insertNode(_el$2, _el$3);
104
- _$insertNode(_el$2, _el$8);
105
- _$setProp(_el$2, "width", "100%");
106
- _$setProp(_el$2, "height", 1);
107
- _$setProp(_el$2, "flexDirection", "row");
108
- _$setProp(_el$2, "overflow", "hidden");
109
- _$insertNode(_el$3, _el$4);
110
- _$insertNode(_el$3, _el$5);
111
- _$setProp(_el$3, "flexGrow", 1);
112
- _$setProp(_el$3, "flexShrink", 1);
113
- _$setProp(_el$3, "minWidth", 0);
114
- _$setProp(_el$3, "wrapMode", "none");
115
- _$setProp(_el$3, "truncate", true);
116
- _$insert(_el$4, (() => {
117
- var _c$ = _$memo(() => !!isCurrent());
118
- return () => _c$() ? "" : livePart().state.status === "pending" ? "…" : "●";
358
+ var _el$27 = _$createElement("box"),
359
+ _el$28 = _$createElement("box"),
360
+ _el$29 = _$createElement("text"),
361
+ _el$30 = _$createElement("b"),
362
+ _el$31 = _$createElement("box"),
363
+ _el$32 = _$createElement("text"),
364
+ _el$33 = _$createElement("box"),
365
+ _el$34 = _$createElement("text"),
366
+ _el$35 = _$createElement("box"),
367
+ _el$36 = _$createElement("text");
368
+ _$insertNode(_el$27, _el$28);
369
+ _$insertNode(_el$27, _el$33);
370
+ _$use(element => {
371
+ card = element;
372
+ updateAnchor();
373
+ }, _el$27);
374
+ _$setProp(_el$27, "onSizeChange", updateAnchor);
375
+ _$setProp(_el$27, "renderBefore", updateAnchor);
376
+ _$setProp(_el$27, "width", "100%");
377
+ _$setProp(_el$27, "height", 2);
378
+ _$setProp(_el$27, "flexDirection", "column");
379
+ _$setProp(_el$27, "overflow", "hidden");
380
+ _$setProp(_el$27, "onMouseUp", openDetails);
381
+ _$insertNode(_el$28, _el$29);
382
+ _$insertNode(_el$28, _el$31);
383
+ _$setProp(_el$28, "width", "100%");
384
+ _$setProp(_el$28, "height", 1);
385
+ _$setProp(_el$28, "flexDirection", "row");
386
+ _$setProp(_el$28, "overflow", "hidden");
387
+ _$insertNode(_el$29, _el$30);
388
+ _$setProp(_el$29, "flexGrow", 1);
389
+ _$setProp(_el$29, "flexShrink", 1);
390
+ _$setProp(_el$29, "minWidth", 0);
391
+ _$setProp(_el$29, "wrapMode", "none");
392
+ _$setProp(_el$29, "truncate", true);
393
+ _$insert(_el$30, (() => {
394
+ var _c$ = _$memo(() => !!part());
395
+ return () => _c$() ? taskAgent(part()) : "subagent";
119
396
  })());
120
- _$insertNode(_el$5, _el$6);
121
- _$insertNode(_el$5, _el$7);
122
- _$insert(_el$7, () => taskAgent(livePart()));
123
- _$setProp(_el$8, "flexShrink", 0);
124
- _$setProp(_el$8, "maxWidth", 20);
125
- _$setProp(_el$8, "marginLeft", 1);
126
- _$setProp(_el$8, "wrapMode", "none");
127
- _$setProp(_el$8, "truncate", true);
128
- _$insert(_el$8, model);
129
- _$insertNode(_el$9, _el$0);
130
- _$insertNode(_el$9, _el$1);
131
- _$setProp(_el$9, "width", "100%");
132
- _$setProp(_el$9, "height", 1);
133
- _$setProp(_el$9, "flexDirection", "row");
134
- _$setProp(_el$9, "overflow", "hidden");
135
- _$setProp(_el$0, "flexGrow", 1);
136
- _$setProp(_el$0, "flexShrink", 1);
137
- _$setProp(_el$0, "minWidth", 0);
138
- _$setProp(_el$0, "wrapMode", "none");
139
- _$setProp(_el$0, "truncate", true);
140
- _$insert(_el$0, activity);
141
- _$setProp(_el$1, "width", 8);
142
- _$setProp(_el$1, "flexShrink", 0);
143
- _$setProp(_el$1, "marginLeft", 1);
144
- _$setProp(_el$1, "wrapMode", "none");
145
- _$setProp(_el$1, "truncate", true);
146
- _$insert(_el$1, elapsed);
397
+ _$insertNode(_el$31, _el$32);
398
+ _$setProp(_el$31, "flexShrink", 1);
399
+ _$setProp(_el$31, "minWidth", 0);
400
+ _$setProp(_el$31, "maxWidth", 28);
401
+ _$setProp(_el$31, "marginLeft", 1);
402
+ _$setProp(_el$31, "flexDirection", "row");
403
+ _$setProp(_el$32, "flexGrow", 1);
404
+ _$setProp(_el$32, "flexShrink", 1);
405
+ _$setProp(_el$32, "minWidth", 0);
406
+ _$setProp(_el$32, "wrapMode", "none");
407
+ _$setProp(_el$32, "truncate", true);
408
+ _$insert(_el$32, () => modelName(props.api, model()));
409
+ _$insert(_el$31, _$createComponent(Show, {
410
+ get when() {
411
+ return taskVariantLabel(model());
412
+ },
413
+ get fallback() {
414
+ return _$createComponent(Show, {
415
+ get when() {
416
+ return model();
417
+ },
418
+ get children() {
419
+ var _el$37 = _$createElement("text");
420
+ _$insertNode(_el$37, _$createTextNode(`· pending`));
421
+ _$setProp(_el$37, "flexShrink", 0);
422
+ _$setProp(_el$37, "marginLeft", 1);
423
+ _$setProp(_el$37, "wrapMode", "none");
424
+ _$setProp(_el$37, "truncate", true);
425
+ _$effect(_$p => _$setProp(_el$37, "fg", props.api.theme.current.textMuted, _$p));
426
+ return _el$37;
427
+ }
428
+ });
429
+ },
430
+ children: variant => (() => {
431
+ var _el$39 = _$createElement("text"),
432
+ _el$40 = _$createTextNode(`· `);
433
+ _$insertNode(_el$39, _el$40);
434
+ _$setProp(_el$39, "flexShrink", 0);
435
+ _$setProp(_el$39, "marginLeft", 1);
436
+ _$setProp(_el$39, "wrapMode", "none");
437
+ _$setProp(_el$39, "truncate", true);
438
+ _$insert(_el$39, variant, null);
439
+ _$effect(_$p => _$setProp(_el$39, "fg", props.api.theme.current.textMuted, _$p));
440
+ return _el$39;
441
+ })()
442
+ }), null);
443
+ _$insertNode(_el$33, _el$34);
444
+ _$insertNode(_el$33, _el$35);
445
+ _$setProp(_el$33, "width", "100%");
446
+ _$setProp(_el$33, "height", 1);
447
+ _$setProp(_el$33, "flexDirection", "row");
448
+ _$setProp(_el$33, "overflow", "hidden");
449
+ _$setProp(_el$34, "flexGrow", 1);
450
+ _$setProp(_el$34, "flexShrink", 1);
451
+ _$setProp(_el$34, "minWidth", 0);
452
+ _$setProp(_el$34, "wrapMode", "none");
453
+ _$setProp(_el$34, "truncate", true);
454
+ _$insert(_el$34, (() => {
455
+ var _c$2 = _$memo(() => !!part());
456
+ return () => _c$2() ? taskDescription(part()) : "Working";
457
+ })());
458
+ _$insertNode(_el$35, _el$36);
459
+ _$setProp(_el$35, "width", 8);
460
+ _$setProp(_el$35, "flexShrink", 0);
461
+ _$setProp(_el$35, "flexDirection", "row");
462
+ _$setProp(_el$35, "justifyContent", "flex-end");
463
+ _$setProp(_el$36, "wrapMode", "none");
464
+ _$setProp(_el$36, "truncate", true);
465
+ _$insert(_el$36, elapsed);
147
466
  _$effect(_p$ => {
148
- var _v$ = {
149
- fg: livePart().state.status === "pending" ? props.api.theme.current.warning : props.api.theme.current.success
150
- },
151
- _v$2 = {
152
- fg: props.api.theme.current.text
153
- },
154
- _v$3 = props.api.theme.current.textMuted,
155
- _v$4 = props.api.theme.current.textMuted,
156
- _v$5 = props.api.theme.current.textMuted;
157
- _v$ !== _p$.e && (_p$.e = _$setProp(_el$4, "style", _v$, _p$.e));
158
- _v$2 !== _p$.t && (_p$.t = _$setProp(_el$5, "style", _v$2, _p$.t));
159
- _v$3 !== _p$.a && (_p$.a = _$setProp(_el$8, "fg", _v$3, _p$.a));
160
- _v$4 !== _p$.o && (_p$.o = _$setProp(_el$0, "fg", _v$4, _p$.o));
161
- _v$5 !== _p$.i && (_p$.i = _$setProp(_el$1, "fg", _v$5, _p$.i));
467
+ var _v$12 = props.api.theme.current.text,
468
+ _v$13 = props.api.theme.current.textMuted,
469
+ _v$14 = props.api.theme.current.textMuted,
470
+ _v$15 = props.api.theme.current.textMuted;
471
+ _v$12 !== _p$.e && (_p$.e = _$setProp(_el$29, "fg", _v$12, _p$.e));
472
+ _v$13 !== _p$.t && (_p$.t = _$setProp(_el$32, "fg", _v$13, _p$.t));
473
+ _v$14 !== _p$.a && (_p$.a = _$setProp(_el$34, "fg", _v$14, _p$.a));
474
+ _v$15 !== _p$.o && (_p$.o = _$setProp(_el$36, "fg", _v$15, _p$.o));
162
475
  return _p$;
163
476
  }, {
164
477
  e: undefined,
165
478
  t: undefined,
166
479
  a: undefined,
167
- o: undefined,
168
- i: undefined
480
+ o: undefined
169
481
  });
170
- return _el$;
482
+ return _el$27;
171
483
  })();
172
484
  }
173
485
  function RunningSubagents(props) {
@@ -176,6 +488,8 @@ function RunningSubagents(props) {
176
488
  const [revision, setRevision] = createSignal(0);
177
489
  const [now, setNow] = createSignal(Date.now());
178
490
  const firstSeen = new Map();
491
+ const fallbacks = new Map();
492
+ const [openPopup, setOpenPopup] = createSignal();
179
493
  let sessionRequest = 0;
180
494
  const loadMessages = async (sessionID, request) => {
181
495
  if (props.api.state.session.messages(sessionID).length) return;
@@ -219,18 +533,52 @@ function RunningSubagents(props) {
219
533
  setRevision(value => value + 1);
220
534
  void loadMessages(parentSessionID, currentRequest);
221
535
  };
222
- createEffect(on(() => props.sessionID, sessionID => void selectSession(sessionID)));
223
- const running = createMemo(() => {
536
+ createEffect(on(() => props.sessionID, (sessionID, previousSessionID) => {
537
+ if (previousSessionID !== undefined && sessionID !== previousSessionID) setOpenPopup(undefined);
538
+ void selectSession(sessionID);
539
+ }));
540
+ const allTaskParts = createMemo(() => {
224
541
  revision();
225
542
  const sessionID = scopeSessionID();
226
543
  if (!sessionID) return [];
227
544
  const messages = props.api.state.session.messages(sessionID);
228
545
  const parts = messages.length ? messages.flatMap(message => props.api.state.part(message.id)) : hydratedMessages().flatMap(message => message.parts);
229
- return parts.filter(part => isVisibleTask(part, props.api.state.session.status));
546
+ return parts.filter(isTaskPart);
547
+ });
548
+ const visibleTaskParts = createMemo(() => allTaskParts().filter(part => isVisibleTask(part, props.api.state.session.status)));
549
+ // Popups need the direct parent's complete/error parts too, even after their
550
+ // visible card has gone away.
551
+ const tasksByCallID = createMemo(() => new Map(allTaskParts().map(part => [part.callID, part])));
552
+ const taskCallIDs = createMemo(() => visibleTaskParts().map(part => part.callID));
553
+ const activeChildSessions = createMemo(() => new Set(visibleTaskParts().map(taskSessionID).filter(sessionID => Boolean(sessionID))));
554
+ const remember = part => {
555
+ const sessionID = taskSessionID(part);
556
+ const session = sessionID ? props.api.state.session.get(sessionID) : undefined;
557
+ fallbacks.set(part.callID, {
558
+ part,
559
+ ...(sessionID ? {
560
+ sessionID
561
+ } : {}),
562
+ ...(session ? {
563
+ session
564
+ } : {})
565
+ });
566
+ };
567
+ const rememberLiveTasks = () => {
568
+ revision();
569
+ for (const part of allTaskParts()) remember(part);
570
+ };
571
+ createEffect(rememberLiveTasks);
572
+ const popupPart = createMemo(() => {
573
+ revision();
574
+ const callID = openPopup()?.callID;
575
+ return callID ? tasksByCallID().get(callID) ?? fallbacks.get(callID)?.part : undefined;
576
+ });
577
+ const popupSessionID = createMemo(() => {
578
+ const callID = openPopup()?.callID;
579
+ const part = popupPart();
580
+ return (part ? taskSessionID(part) : undefined) ?? (callID ? fallbacks.get(callID)?.sessionID : undefined);
230
581
  });
231
- const tasksByCallID = createMemo(() => new Map(running().map(part => [part.callID, part])));
232
- const taskCallIDs = createMemo(() => running().map(part => part.callID));
233
- const activeChildSessions = createMemo(() => new Set(running().map(taskSessionID).filter(sessionID => Boolean(sessionID))));
234
582
  const startedAt = part => {
235
583
  if (part.state.status !== "pending") return part.state.time.start;
236
584
  const existing = firstSeen.get(part.callID);
@@ -239,52 +587,86 @@ function RunningSubagents(props) {
239
587
  firstSeen.set(part.callID, value);
240
588
  return value;
241
589
  };
590
+ const closePopup = ownership => {
591
+ if (openPopup()?.token !== ownership.token) return;
592
+ setOpenPopup(undefined);
593
+ };
594
+ const showPopup = (callID, anchor) => {
595
+ const ownership = {
596
+ callID,
597
+ token: Symbol(callID),
598
+ anchor
599
+ };
600
+ setOpenPopup(ownership);
601
+ };
602
+ const updatePopupAnchor = (callID, anchor) => {
603
+ const ownership = openPopup();
604
+ if (!ownership || ownership.callID !== callID) return;
605
+ if (ownership.anchor.x === anchor.x && ownership.anchor.y === anchor.y && ownership.anchor.width === anchor.width && ownership.anchor.height === anchor.height) {
606
+ return;
607
+ }
608
+ setOpenPopup({
609
+ ...ownership,
610
+ anchor
611
+ });
612
+ };
613
+ const dismissForHostDialog = () => {
614
+ if (openPopup() && props.api.ui.dialog.open) setOpenPopup(undefined);
615
+ };
616
+ const dismissForHostDialogFrame = async () => {
617
+ dismissForHostDialog();
618
+ };
619
+ createEffect(dismissForHostDialog);
242
620
  const unregister = [];
243
621
  let timer;
244
622
  onMount(() => {
245
623
  const syncCurrentSession = session => {
246
624
  if (session.id === props.sessionID) void selectSession(session.id, session);
247
- if (session.parentID === scopeSessionID() || activeChildSessions().has(session.id)) {
625
+ if (session.parentID === scopeSessionID() || activeChildSessions().has(session.id) || session.id === popupSessionID()) {
248
626
  setRevision(value => value + 1);
249
627
  }
250
628
  };
251
629
  const refreshSession = sessionID => {
252
- if (sessionID === scopeSessionID() || activeChildSessions().has(sessionID)) {
630
+ if (sessionID === scopeSessionID() || activeChildSessions().has(sessionID) || sessionID === popupSessionID()) {
253
631
  setRevision(value => value + 1);
254
632
  }
255
633
  };
256
- unregister.push(props.api.event.on("session.created", event => syncCurrentSession(event.properties.info)), props.api.event.on("session.updated", event => syncCurrentSession(event.properties.info)), props.api.event.on("message.updated", event => refreshSession(event.properties.sessionID)), props.api.event.on("message.part.updated", event => refreshSession(event.properties.sessionID)), props.api.event.on("message.part.removed", event => refreshSession(event.properties.sessionID)));
634
+ unregister.push(() => {
635
+ props.api.renderer.removeFrameCallback(dismissForHostDialogFrame);
636
+ }, props.api.event.on("session.created", event => syncCurrentSession(event.properties.info)), props.api.event.on("session.updated", event => syncCurrentSession(event.properties.info)), props.api.event.on("message.updated", event => refreshSession(event.properties.sessionID)), props.api.event.on("message.part.updated", event => refreshSession(event.properties.sessionID)), props.api.event.on("message.part.removed", event => refreshSession(event.properties.sessionID)));
637
+ props.api.renderer.setFrameCallback(dismissForHostDialogFrame);
257
638
  timer = setInterval(() => setNow(Date.now()), 1_000);
258
639
  });
259
640
  onCleanup(() => {
260
641
  sessionRequest += 1;
261
642
  if (timer) clearInterval(timer);
262
643
  for (const dispose of unregister) dispose();
644
+ setOpenPopup(undefined);
263
645
  });
264
646
  return (() => {
265
- var _el$10 = _$createElement("box"),
266
- _el$11 = _$createElement("text"),
267
- _el$12 = _$createElement("b"),
268
- _el$14 = _$createElement("span");
269
- _$insertNode(_el$10, _el$11);
270
- _$setProp(_el$10, "overflow", "hidden");
271
- _$insertNode(_el$11, _el$12);
272
- _$insertNode(_el$11, _el$14);
273
- _$insertNode(_el$12, _$createTextNode(`Subagents`));
274
- _$insert(_el$14, (() => {
275
- var _c$2 = _$memo(() => !!running().length);
276
- return () => _c$2() ? ` (${running().length} running)` : "";
647
+ var _el$41 = _$createElement("box"),
648
+ _el$42 = _$createElement("text"),
649
+ _el$43 = _$createElement("b"),
650
+ _el$45 = _$createElement("span");
651
+ _$insertNode(_el$41, _el$42);
652
+ _$setProp(_el$41, "overflow", "hidden");
653
+ _$insertNode(_el$42, _el$43);
654
+ _$insertNode(_el$42, _el$45);
655
+ _$insertNode(_el$43, _$createTextNode(`Subagents`));
656
+ _$insert(_el$45, (() => {
657
+ var _c$3 = _$memo(() => !!visibleTaskParts().length);
658
+ return () => _c$3() ? ` (${visibleTaskParts().length} running)` : "";
277
659
  })());
278
- _$insert(_el$10, _$createComponent(Show, {
660
+ _$insert(_el$41, _$createComponent(Show, {
279
661
  get when() {
280
662
  return taskCallIDs().length > 0;
281
663
  },
282
664
  get fallback() {
283
665
  return (() => {
284
- var _el$15 = _$createElement("text");
285
- _$insertNode(_el$15, _$createTextNode(`No subagent running`));
286
- _$effect(_$p => _$setProp(_el$15, "fg", props.api.theme.current.textMuted, _$p));
287
- return _el$15;
666
+ var _el$46 = _$createElement("text");
667
+ _$insertNode(_el$46, _$createTextNode(`No subagent running`));
668
+ _$effect(_$p => _$setProp(_el$46, "fg", props.api.theme.current.textMuted, _$p));
669
+ return _el$46;
288
670
  })();
289
671
  },
290
672
  get children() {
@@ -297,30 +679,54 @@ function RunningSubagents(props) {
297
679
  return props.api;
298
680
  },
299
681
  callID: callID,
300
- get currentSessionID() {
301
- return props.sessionID;
302
- },
303
682
  tasksByCallID: tasksByCallID,
683
+ fallbackFor: id => fallbacks.get(id),
684
+ remember: remember,
304
685
  revision: revision,
305
686
  now: now,
306
- startedAt: startedAt
687
+ startedAt: startedAt,
688
+ openPopup: showPopup,
689
+ updatePopupAnchor: updatePopupAnchor
307
690
  })
308
691
  });
309
692
  }
310
693
  }), null);
694
+ _$insert(_el$41, _$createComponent(Show, {
695
+ get when() {
696
+ return openPopup();
697
+ },
698
+ children: ownership => _$createComponent(TaskPopover, {
699
+ get api() {
700
+ return props.api;
701
+ },
702
+ ownership: ownership,
703
+ get currentSessionID() {
704
+ return props.sessionID;
705
+ },
706
+ get fallbackPart() {
707
+ return fallbacks.get(ownership().callID).part;
708
+ },
709
+ tasksByCallID: tasksByCallID,
710
+ fallbackFor: id => fallbacks.get(id),
711
+ revision: revision,
712
+ now: now,
713
+ startedAt: startedAt,
714
+ close: () => closePopup(ownership())
715
+ })
716
+ }), null);
311
717
  _$effect(_p$ => {
312
- var _v$6 = props.api.theme.current.text,
313
- _v$7 = {
718
+ var _v$16 = props.api.theme.current.text,
719
+ _v$17 = {
314
720
  fg: props.api.theme.current.textMuted
315
721
  };
316
- _v$6 !== _p$.e && (_p$.e = _$setProp(_el$11, "fg", _v$6, _p$.e));
317
- _v$7 !== _p$.t && (_p$.t = _$setProp(_el$14, "style", _v$7, _p$.t));
722
+ _v$16 !== _p$.e && (_p$.e = _$setProp(_el$42, "fg", _v$16, _p$.e));
723
+ _v$17 !== _p$.t && (_p$.t = _$setProp(_el$45, "style", _v$17, _p$.t));
318
724
  return _p$;
319
725
  }, {
320
726
  e: undefined,
321
727
  t: undefined
322
728
  });
323
- return _el$10;
729
+ return _el$41;
324
730
  })();
325
731
  }
326
732
  const tui = async api => {