@op1/threads 0.2.1 → 0.2.2
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/README.md +7 -5
- package/docs/workflows-verification.md +17 -0
- package/package.json +5 -3
- package/tui.js +2093 -0
- package/tui.ts +0 -240
package/tui.js
ADDED
|
@@ -0,0 +1,2093 @@
|
|
|
1
|
+
// @bun
|
|
2
|
+
// tui.ts
|
|
3
|
+
import { Plugin } from "@opencode/plugin/tui";
|
|
4
|
+
import { getComponentCatalogue } from "@opentui/solid/components";
|
|
5
|
+
import { createEffect as createEffect3, createSignal as createSignal3 } from "solid-js";
|
|
6
|
+
import { z as z5 } from "zod";
|
|
7
|
+
|
|
8
|
+
// src/rpc.ts
|
|
9
|
+
import { Rpc } from "@opencode/plugin/rpc";
|
|
10
|
+
import { z } from "zod";
|
|
11
|
+
var Report = z.object({
|
|
12
|
+
verdict: z.enum(["PASS", "PASS WITH NOTES", "FAIL", "INCONCLUSIVE"]),
|
|
13
|
+
summary: z.string().min(1),
|
|
14
|
+
evidence: z.array(z.string())
|
|
15
|
+
}).strict();
|
|
16
|
+
var WorkerView = z.object({
|
|
17
|
+
workerID: z.string(),
|
|
18
|
+
coordinatorID: z.string(),
|
|
19
|
+
key: z.string(),
|
|
20
|
+
title: z.string(),
|
|
21
|
+
directory: z.string(),
|
|
22
|
+
agent: z.string().nullable(),
|
|
23
|
+
model: z.object({
|
|
24
|
+
providerID: z.string(),
|
|
25
|
+
id: z.string(),
|
|
26
|
+
variant: z.string().optional()
|
|
27
|
+
}).nullable(),
|
|
28
|
+
outcome: z.enum(["succeeded", "failed", "interrupted"]).nullable(),
|
|
29
|
+
report: Report.nullable(),
|
|
30
|
+
hidden: z.boolean()
|
|
31
|
+
});
|
|
32
|
+
var ThreadsRpc = Rpc.define({
|
|
33
|
+
id: "threads",
|
|
34
|
+
methods: {
|
|
35
|
+
snapshot: {
|
|
36
|
+
input: z.object({ coordinatorIDs: z.array(z.string()).max(100) }).strict(),
|
|
37
|
+
output: z.object({ workers: z.array(WorkerView) }),
|
|
38
|
+
errors: {}
|
|
39
|
+
},
|
|
40
|
+
restore: {
|
|
41
|
+
input: z.object({ coordinatorIDs: z.array(z.string()).max(100) }).strict(),
|
|
42
|
+
output: z.object({ workers: z.array(WorkerView) }),
|
|
43
|
+
errors: {}
|
|
44
|
+
}
|
|
45
|
+
},
|
|
46
|
+
events: {}
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
// src/activity.ts
|
|
50
|
+
import { z as z2 } from "zod";
|
|
51
|
+
|
|
52
|
+
// src/activity-model.ts
|
|
53
|
+
function cleanRoleTitle(title, managed) {
|
|
54
|
+
if (!managed)
|
|
55
|
+
return title;
|
|
56
|
+
const remainder = title.replace(/^\[(?:Main|Worker)\] /, "");
|
|
57
|
+
return remainder.trim() ? remainder : title;
|
|
58
|
+
}
|
|
59
|
+
function activityTime(time) {
|
|
60
|
+
return time.idle ?? time.updated ?? time.created;
|
|
61
|
+
}
|
|
62
|
+
function activitySubtitle(input) {
|
|
63
|
+
const folder = (path) => path.replace(/[\\/]+$/, "").split(/[\\/]/).pop() || path;
|
|
64
|
+
const location = folder(input.directory);
|
|
65
|
+
const project = input.project?.name?.trim() || (input.project ? folder(input.project.canonical) : location);
|
|
66
|
+
const parts = [project];
|
|
67
|
+
if (input.role)
|
|
68
|
+
parts.push(input.role);
|
|
69
|
+
if (input.project && input.project.canonical !== input.directory && location !== project)
|
|
70
|
+
parts.push(location);
|
|
71
|
+
return parts.join(" \xB7 ");
|
|
72
|
+
}
|
|
73
|
+
function dateGroup(timestamp, now = new Date) {
|
|
74
|
+
const date = new Date(timestamp);
|
|
75
|
+
const day = (value) => Date.UTC(value.getFullYear(), value.getMonth(), value.getDate());
|
|
76
|
+
const age = (day(now) - day(date)) / 86400000;
|
|
77
|
+
if (age === 0)
|
|
78
|
+
return "Today";
|
|
79
|
+
if (age === 1)
|
|
80
|
+
return "Yesterday";
|
|
81
|
+
if (age > 1 && age < 7)
|
|
82
|
+
return date.toLocaleDateString(undefined, { weekday: "long" });
|
|
83
|
+
return date.toLocaleDateString(undefined, {
|
|
84
|
+
year: "numeric",
|
|
85
|
+
month: "short",
|
|
86
|
+
day: "numeric"
|
|
87
|
+
});
|
|
88
|
+
}
|
|
89
|
+
function priority(item) {
|
|
90
|
+
return item.attention ? 3 : item.busy ? 2 : item.unread ? 1 : 0;
|
|
91
|
+
}
|
|
92
|
+
function visible(item) {
|
|
93
|
+
return !item.hidden || item.open || item.active || item.busy || item.attention;
|
|
94
|
+
}
|
|
95
|
+
function compare(a, b) {
|
|
96
|
+
return priority(b) - priority(a) || Number(b.pinned) - Number(a.pinned) || b.updated - a.updated || a.id.localeCompare(b.id);
|
|
97
|
+
}
|
|
98
|
+
function activityGroups(items, now = new Date) {
|
|
99
|
+
const groups = new Map;
|
|
100
|
+
const ordered = items.filter(visible).sort(compare);
|
|
101
|
+
for (const item of ordered) {
|
|
102
|
+
const key = priority(item) ? "Priority" : item.pinned ? "Pinned" : dateGroup(item.updated, now);
|
|
103
|
+
const group = groups.get(key) ?? [];
|
|
104
|
+
group.push(item);
|
|
105
|
+
groups.set(key, group);
|
|
106
|
+
}
|
|
107
|
+
return groups;
|
|
108
|
+
}
|
|
109
|
+
function activityThreads(items, now = new Date) {
|
|
110
|
+
const available = new Map(items.filter(visible).map((item) => [item.id, item]));
|
|
111
|
+
const children = new Map;
|
|
112
|
+
const roots = [];
|
|
113
|
+
for (const item of available.values()) {
|
|
114
|
+
const parent = item.coordinatorID ? available.get(item.coordinatorID) : undefined;
|
|
115
|
+
if (parent && parent.id !== item.id && !parent.coordinatorID) {
|
|
116
|
+
const siblings = children.get(parent.id) ?? [];
|
|
117
|
+
siblings.push(item);
|
|
118
|
+
children.set(parent.id, siblings);
|
|
119
|
+
} else
|
|
120
|
+
roots.push(item);
|
|
121
|
+
}
|
|
122
|
+
const threads = new Map;
|
|
123
|
+
const summaries = roots.map((item) => {
|
|
124
|
+
const workers = (children.get(item.id) ?? []).sort(compare);
|
|
125
|
+
const members = [item, ...workers];
|
|
126
|
+
const status = {
|
|
127
|
+
attention: members.some((member) => member.attention),
|
|
128
|
+
busy: members.some((member) => member.busy),
|
|
129
|
+
unread: members.some((member) => member.unread === "error") ? "error" : members.find((member) => member.unread)?.unread
|
|
130
|
+
};
|
|
131
|
+
threads.set(item.id, { item, children: workers, status });
|
|
132
|
+
return {
|
|
133
|
+
...item,
|
|
134
|
+
...status,
|
|
135
|
+
pinned: members.some((member) => member.pinned),
|
|
136
|
+
updated: Math.max(...members.map((member) => member.updated))
|
|
137
|
+
};
|
|
138
|
+
});
|
|
139
|
+
return new Map([...activityGroups(summaries, now)].map(([name, group]) => [
|
|
140
|
+
name,
|
|
141
|
+
group.flatMap((item) => {
|
|
142
|
+
const thread = threads.get(item.id);
|
|
143
|
+
return thread ? [thread] : [];
|
|
144
|
+
})
|
|
145
|
+
]));
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
// src/activity-rail.ts
|
|
149
|
+
function activityRail(ctx, core, mount) {
|
|
150
|
+
const { BoxRenderable, ScrollBoxRenderable } = core;
|
|
151
|
+
let owned;
|
|
152
|
+
let enabled = true;
|
|
153
|
+
let dirty = true;
|
|
154
|
+
let geometry = "";
|
|
155
|
+
const detach = () => {
|
|
156
|
+
if (!owned)
|
|
157
|
+
return;
|
|
158
|
+
owned.cleanup();
|
|
159
|
+
if (!owned.content.isDestroyed)
|
|
160
|
+
owned.content.destroyRecursively();
|
|
161
|
+
for (const [child, visible2] of owned.restore)
|
|
162
|
+
if (!child.isDestroyed)
|
|
163
|
+
child.visible = visible2;
|
|
164
|
+
owned = undefined;
|
|
165
|
+
};
|
|
166
|
+
const find = () => {
|
|
167
|
+
const matches = [];
|
|
168
|
+
const queue = [
|
|
169
|
+
{ node: ctx.renderer.root, depth: 0 }
|
|
170
|
+
];
|
|
171
|
+
let count = 0;
|
|
172
|
+
while (queue.length && count++ < 128) {
|
|
173
|
+
const entry = queue.shift();
|
|
174
|
+
if (!entry)
|
|
175
|
+
break;
|
|
176
|
+
const children = entry.node.getChildren();
|
|
177
|
+
for (const child of children) {
|
|
178
|
+
if (child instanceof BoxRenderable && child.visible && child.screenX === 0 && child.screenY === 0 && child.width >= 24 && child.width <= 60 && child.height === ctx.renderer.height && child.getChildren().some((node) => node instanceof ScrollBoxRenderable) && children.filter((sibling) => sibling !== child && sibling.screenX === child.width && sibling.screenY === 0 && sibling.height === child.height && sibling.width >= 60).length === 1)
|
|
179
|
+
matches.push(child);
|
|
180
|
+
if (entry.depth < 4)
|
|
181
|
+
queue.push({ node: child, depth: entry.depth + 1 });
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
return queue.length === 0 && matches.length === 1 ? matches[0] : undefined;
|
|
185
|
+
};
|
|
186
|
+
const frame = () => {
|
|
187
|
+
if (owned?.rail.isDestroyed || owned?.content.isDestroyed) {
|
|
188
|
+
detach();
|
|
189
|
+
dirty = true;
|
|
190
|
+
}
|
|
191
|
+
const current = `${ctx.renderer.width}:${ctx.renderer.height}:${owned?.rail.width}:${owned?.rail.height}`;
|
|
192
|
+
if (current !== geometry) {
|
|
193
|
+
geometry = current;
|
|
194
|
+
dirty = true;
|
|
195
|
+
}
|
|
196
|
+
if (!dirty)
|
|
197
|
+
return;
|
|
198
|
+
dirty = false;
|
|
199
|
+
const rail = enabled && ctx.ui.tabs.enabled() ? find() : undefined;
|
|
200
|
+
if (owned?.rail !== rail)
|
|
201
|
+
detach();
|
|
202
|
+
if (!rail)
|
|
203
|
+
return;
|
|
204
|
+
if (!owned) {
|
|
205
|
+
const content = new BoxRenderable(ctx.renderer, {
|
|
206
|
+
id: "op-threads-activity",
|
|
207
|
+
position: "absolute",
|
|
208
|
+
left: 0,
|
|
209
|
+
top: 0,
|
|
210
|
+
width: "100%",
|
|
211
|
+
height: "100%",
|
|
212
|
+
paddingLeft: 1,
|
|
213
|
+
paddingRight: 2,
|
|
214
|
+
flexDirection: "column",
|
|
215
|
+
onMouseDown: (event) => event.stopPropagation(),
|
|
216
|
+
onMouseUp: (event) => event.stopPropagation()
|
|
217
|
+
});
|
|
218
|
+
rail.add(content);
|
|
219
|
+
owned = { rail, content, restore: new Map, cleanup: mount(content) };
|
|
220
|
+
}
|
|
221
|
+
for (const child of rail.getChildren()) {
|
|
222
|
+
if (child === owned.content)
|
|
223
|
+
continue;
|
|
224
|
+
if (!owned.restore.has(child))
|
|
225
|
+
owned.restore.set(child, child.visible);
|
|
226
|
+
child.visible = false;
|
|
227
|
+
}
|
|
228
|
+
for (const child of owned.restore.keys())
|
|
229
|
+
if (child.isDestroyed)
|
|
230
|
+
owned.restore.delete(child);
|
|
231
|
+
};
|
|
232
|
+
const invalidate = () => {
|
|
233
|
+
dirty = true;
|
|
234
|
+
ctx.renderer.requestRender();
|
|
235
|
+
};
|
|
236
|
+
const beforeFrame = async () => frame();
|
|
237
|
+
ctx.renderer.setFrameCallback(beforeFrame);
|
|
238
|
+
ctx.renderer.on("resize", invalidate);
|
|
239
|
+
invalidate();
|
|
240
|
+
return {
|
|
241
|
+
invalidate,
|
|
242
|
+
enabled: () => enabled,
|
|
243
|
+
mounted: () => Boolean(owned && !owned.rail.isDestroyed && !owned.content.isDestroyed),
|
|
244
|
+
toggle(value = !enabled) {
|
|
245
|
+
enabled = value;
|
|
246
|
+
if (!enabled)
|
|
247
|
+
detach();
|
|
248
|
+
invalidate();
|
|
249
|
+
},
|
|
250
|
+
dispose() {
|
|
251
|
+
ctx.renderer.removeFrameCallback(beforeFrame);
|
|
252
|
+
ctx.renderer.off("resize", invalidate);
|
|
253
|
+
detach();
|
|
254
|
+
}
|
|
255
|
+
};
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
// src/activity-theme.ts
|
|
259
|
+
function themeColor(token, fallback) {
|
|
260
|
+
return ("base" in token ? token.base : token.default) ?? fallback;
|
|
261
|
+
}
|
|
262
|
+
function themeMuted(token, fallback) {
|
|
263
|
+
return ("muted" in token ? token.muted : token.subdued) ?? fallback;
|
|
264
|
+
}
|
|
265
|
+
function luminance(color) {
|
|
266
|
+
const linear = (value) => value <= 0.04045 ? value / 12.92 : ((value + 0.055) / 1.055) ** 2.4;
|
|
267
|
+
return 0.2126 * linear(color.r) + 0.7152 * linear(color.g) + 0.0722 * linear(color.b);
|
|
268
|
+
}
|
|
269
|
+
function themeHue(hue, foreground, background) {
|
|
270
|
+
const target = luminance(foreground);
|
|
271
|
+
let selected = foreground;
|
|
272
|
+
let distance = Infinity;
|
|
273
|
+
for (const color of Object.values(hue ?? {})) {
|
|
274
|
+
if (!color)
|
|
275
|
+
continue;
|
|
276
|
+
const delta = Math.abs(luminance(color) - target);
|
|
277
|
+
if (delta >= distance)
|
|
278
|
+
continue;
|
|
279
|
+
selected = color;
|
|
280
|
+
distance = delta;
|
|
281
|
+
}
|
|
282
|
+
const light = luminance(selected);
|
|
283
|
+
const base = luminance(background);
|
|
284
|
+
const contrast = (Math.max(light, base) + 0.05) / (Math.min(light, base) + 0.05);
|
|
285
|
+
return contrast >= 4.5 ? selected : foreground;
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
// src/activity-picker.tsx
|
|
289
|
+
import { insert as _$insert } from "@opentui/solid";
|
|
290
|
+
import { effect as _$effect } from "@opentui/solid";
|
|
291
|
+
import { createComponent as _$createComponent } from "@opentui/solid";
|
|
292
|
+
import { use as _$use } from "@opentui/solid";
|
|
293
|
+
import { createTextNode as _$createTextNode } from "@opentui/solid";
|
|
294
|
+
import { insertNode as _$insertNode } from "@opentui/solid";
|
|
295
|
+
import { setProp as _$setProp } from "@opentui/solid";
|
|
296
|
+
import { createElement as _$createElement } from "@opentui/solid";
|
|
297
|
+
import { createEffect, createMemo, createSignal, For, onCleanup, Show } from "solid-js";
|
|
298
|
+
import fuzzysort from "fuzzysort";
|
|
299
|
+
function ActivityPicker(props) {
|
|
300
|
+
const {
|
|
301
|
+
ctx
|
|
302
|
+
} = props;
|
|
303
|
+
const [query, setQuery] = createSignal("");
|
|
304
|
+
const [selectedID, setSelectedID] = createSignal(props.current);
|
|
305
|
+
const [pinning, setPinning] = createSignal(false);
|
|
306
|
+
const [height, setHeight] = createSignal(ctx.renderer.height);
|
|
307
|
+
let input;
|
|
308
|
+
let scroll;
|
|
309
|
+
let closed = false;
|
|
310
|
+
const foreground = () => themeColor(ctx.theme.text, props.fallbackColor);
|
|
311
|
+
const muted = () => themeMuted(ctx.theme.text, foreground());
|
|
312
|
+
const grouped = createMemo(() => {
|
|
313
|
+
const items = query() ? fuzzysort.go(query(), props.items(), {
|
|
314
|
+
keys: ["title", "category"],
|
|
315
|
+
scoreFn: (result) => result[0].score * 2 + result[1].score
|
|
316
|
+
}).map((result) => result.obj) : props.items();
|
|
317
|
+
const groups = new Map;
|
|
318
|
+
for (const item of items) {
|
|
319
|
+
const group = groups.get(item.category) ?? [];
|
|
320
|
+
group.push(item);
|
|
321
|
+
groups.set(item.category, group);
|
|
322
|
+
}
|
|
323
|
+
return [...groups];
|
|
324
|
+
});
|
|
325
|
+
const choices = createMemo(() => grouped().flatMap(([, items]) => items));
|
|
326
|
+
const selected = () => choices().find((item) => item.id === selectedID()) ?? choices()[0];
|
|
327
|
+
const select = (index) => setSelectedID(choices()[index]?.id);
|
|
328
|
+
const move = (direction) => {
|
|
329
|
+
const items = choices();
|
|
330
|
+
if (!items.length)
|
|
331
|
+
return;
|
|
332
|
+
const index = items.findIndex((item) => item.id === selected()?.id);
|
|
333
|
+
select((index + direction % items.length + items.length) % items.length);
|
|
334
|
+
};
|
|
335
|
+
const open = (id = selected()?.id) => {
|
|
336
|
+
if (!id)
|
|
337
|
+
return;
|
|
338
|
+
ctx.ui.dialog.clear();
|
|
339
|
+
props.open(id);
|
|
340
|
+
};
|
|
341
|
+
const togglePin = async () => {
|
|
342
|
+
const item = selected();
|
|
343
|
+
if (!item || pinning())
|
|
344
|
+
return;
|
|
345
|
+
setSelectedID(item.id);
|
|
346
|
+
setPinning(true);
|
|
347
|
+
try {
|
|
348
|
+
await props.pin(item.id);
|
|
349
|
+
} finally {
|
|
350
|
+
if (!closed)
|
|
351
|
+
setPinning(false);
|
|
352
|
+
}
|
|
353
|
+
};
|
|
354
|
+
ctx.keymap.layer(() => ({
|
|
355
|
+
mode: "global",
|
|
356
|
+
target: () => input,
|
|
357
|
+
priority: 100,
|
|
358
|
+
commands: [{
|
|
359
|
+
bind: "up",
|
|
360
|
+
run: () => move(-1)
|
|
361
|
+
}, {
|
|
362
|
+
bind: "ctrl+p",
|
|
363
|
+
run: () => move(-1)
|
|
364
|
+
}, {
|
|
365
|
+
bind: "down",
|
|
366
|
+
run: () => move(1)
|
|
367
|
+
}, {
|
|
368
|
+
bind: "ctrl+n",
|
|
369
|
+
run: () => move(1)
|
|
370
|
+
}, {
|
|
371
|
+
bind: "pageup",
|
|
372
|
+
run: () => move(-10)
|
|
373
|
+
}, {
|
|
374
|
+
bind: "pagedown",
|
|
375
|
+
run: () => move(10)
|
|
376
|
+
}, {
|
|
377
|
+
bind: "home",
|
|
378
|
+
run: () => {
|
|
379
|
+
select(0);
|
|
380
|
+
}
|
|
381
|
+
}, {
|
|
382
|
+
bind: "end",
|
|
383
|
+
run: () => {
|
|
384
|
+
select(choices().length - 1);
|
|
385
|
+
}
|
|
386
|
+
}, {
|
|
387
|
+
bind: "return",
|
|
388
|
+
run: () => open()
|
|
389
|
+
}, {
|
|
390
|
+
bind: "escape",
|
|
391
|
+
run: () => ctx.ui.dialog.clear()
|
|
392
|
+
}, {
|
|
393
|
+
id: "threads.activity.choose.pin",
|
|
394
|
+
title: "Pin/unpin highlighted Activity conversation",
|
|
395
|
+
bind: "ctrl+f",
|
|
396
|
+
run: togglePin
|
|
397
|
+
}]
|
|
398
|
+
}));
|
|
399
|
+
let scrollTo;
|
|
400
|
+
createEffect(() => {
|
|
401
|
+
grouped();
|
|
402
|
+
scrollTo = selected()?.id;
|
|
403
|
+
});
|
|
404
|
+
const reveal = () => {
|
|
405
|
+
if (!scrollTo || !scroll)
|
|
406
|
+
return;
|
|
407
|
+
scroll.scrollChildIntoView(`activity-picker-row-${scrollTo}`);
|
|
408
|
+
scrollTo = undefined;
|
|
409
|
+
};
|
|
410
|
+
const resize = () => setHeight(ctx.renderer.height);
|
|
411
|
+
ctx.renderer.addPostProcessFn(reveal);
|
|
412
|
+
ctx.renderer.on("resize", resize);
|
|
413
|
+
onCleanup(() => {
|
|
414
|
+
closed = true;
|
|
415
|
+
ctx.renderer.removePostProcessFn(reveal);
|
|
416
|
+
ctx.renderer.off("resize", resize);
|
|
417
|
+
});
|
|
418
|
+
return (() => {
|
|
419
|
+
var _el$ = _$createElement("box"), _el$2 = _$createElement("text"), _el$3 = _$createElement("b"), _el$5 = _$createElement("input"), _el$6 = _$createElement("scrollbox"), _el$9 = _$createElement("text");
|
|
420
|
+
_$insertNode(_el$, _el$2);
|
|
421
|
+
_$insertNode(_el$, _el$5);
|
|
422
|
+
_$insertNode(_el$, _el$6);
|
|
423
|
+
_$insertNode(_el$, _el$9);
|
|
424
|
+
_$setProp(_el$, "id", "activity-picker");
|
|
425
|
+
_$setProp(_el$, "paddingX", 2);
|
|
426
|
+
_$setProp(_el$, "paddingY", 1);
|
|
427
|
+
_$setProp(_el$, "gap", 1);
|
|
428
|
+
_$insertNode(_el$2, _el$3);
|
|
429
|
+
_$insertNode(_el$3, _$createTextNode(`Activity`));
|
|
430
|
+
_$use((node) => {
|
|
431
|
+
input = node;
|
|
432
|
+
}, _el$5);
|
|
433
|
+
_$setProp(_el$5, "id", "activity-picker-search");
|
|
434
|
+
_$setProp(_el$5, "focused", true);
|
|
435
|
+
_$setProp(_el$5, "placeholder", "Search");
|
|
436
|
+
_$setProp(_el$5, "onInput", (value) => {
|
|
437
|
+
setQuery(value);
|
|
438
|
+
setSelectedID(undefined);
|
|
439
|
+
});
|
|
440
|
+
_$use((node) => {
|
|
441
|
+
scroll = node;
|
|
442
|
+
}, _el$6);
|
|
443
|
+
_$setProp(_el$6, "scrollX", false);
|
|
444
|
+
_$setProp(_el$6, "scrollbarOptions", {
|
|
445
|
+
visible: false
|
|
446
|
+
});
|
|
447
|
+
_$insert(_el$6, _$createComponent(For, {
|
|
448
|
+
get each() {
|
|
449
|
+
return grouped();
|
|
450
|
+
},
|
|
451
|
+
children: ([category, items]) => [(() => {
|
|
452
|
+
var _el$0 = _$createElement("text");
|
|
453
|
+
_$setProp(_el$0, "marginTop", 1);
|
|
454
|
+
_$insert(_el$0, category);
|
|
455
|
+
_$effect((_$p) => _$setProp(_el$0, "fg", muted(), _$p));
|
|
456
|
+
return _el$0;
|
|
457
|
+
})(), _$createComponent(For, {
|
|
458
|
+
each: items,
|
|
459
|
+
children: (item) => (() => {
|
|
460
|
+
var _el$1 = _$createElement("box"), _el$10 = _$createElement("text"), _el$11 = _$createElement("text");
|
|
461
|
+
_$insertNode(_el$1, _el$10);
|
|
462
|
+
_$insertNode(_el$1, _el$11);
|
|
463
|
+
_$setProp(_el$1, "height", 1);
|
|
464
|
+
_$setProp(_el$1, "flexShrink", 0);
|
|
465
|
+
_$setProp(_el$1, "flexDirection", "row");
|
|
466
|
+
_$setProp(_el$1, "onMouseUp", (event) => {
|
|
467
|
+
event.stopPropagation();
|
|
468
|
+
if (event.button === 0)
|
|
469
|
+
open(item.id);
|
|
470
|
+
});
|
|
471
|
+
_$setProp(_el$10, "width", "60%");
|
|
472
|
+
_$setProp(_el$10, "wrapMode", "none");
|
|
473
|
+
_$setProp(_el$10, "truncate", true);
|
|
474
|
+
_$insert(_el$10, () => `${selected()?.id === item.id ? ">" : " "} ${item.pinned ? "\u25C6" : "\u25C7"} ${item.title}`);
|
|
475
|
+
_$setProp(_el$11, "flexGrow", 1);
|
|
476
|
+
_$setProp(_el$11, "flexShrink", 1);
|
|
477
|
+
_$setProp(_el$11, "minWidth", 0);
|
|
478
|
+
_$setProp(_el$11, "wrapMode", "none");
|
|
479
|
+
_$setProp(_el$11, "truncate", true);
|
|
480
|
+
_$insert(_el$11, () => `${item.subtitle}${item.closed ? " \xB7 Closed" : ""}`);
|
|
481
|
+
_$effect((_p$) => {
|
|
482
|
+
var _v$6 = `activity-picker-row-${item.id}`, _v$7 = selected()?.id === item.id ? ctx.theme.background.raised?.high ?? themeColor(ctx.theme.background, props.fallbackColor) : undefined, _v$8 = `activity-picker-title-${item.id}`, _v$9 = foreground(), _v$0 = muted();
|
|
483
|
+
_v$6 !== _p$.e && (_p$.e = _$setProp(_el$1, "id", _v$6, _p$.e));
|
|
484
|
+
_v$7 !== _p$.t && (_p$.t = _$setProp(_el$1, "backgroundColor", _v$7, _p$.t));
|
|
485
|
+
_v$8 !== _p$.a && (_p$.a = _$setProp(_el$10, "id", _v$8, _p$.a));
|
|
486
|
+
_v$9 !== _p$.o && (_p$.o = _$setProp(_el$10, "fg", _v$9, _p$.o));
|
|
487
|
+
_v$0 !== _p$.i && (_p$.i = _$setProp(_el$11, "fg", _v$0, _p$.i));
|
|
488
|
+
return _p$;
|
|
489
|
+
}, {
|
|
490
|
+
e: undefined,
|
|
491
|
+
t: undefined,
|
|
492
|
+
a: undefined,
|
|
493
|
+
o: undefined,
|
|
494
|
+
i: undefined
|
|
495
|
+
});
|
|
496
|
+
return _el$1;
|
|
497
|
+
})()
|
|
498
|
+
})]
|
|
499
|
+
}), null);
|
|
500
|
+
_$insert(_el$6, _$createComponent(Show, {
|
|
501
|
+
get when() {
|
|
502
|
+
return !choices().length;
|
|
503
|
+
},
|
|
504
|
+
get children() {
|
|
505
|
+
var _el$7 = _$createElement("text");
|
|
506
|
+
_$insertNode(_el$7, _$createTextNode(`No matching conversations`));
|
|
507
|
+
_$effect((_$p) => _$setProp(_el$7, "fg", muted(), _$p));
|
|
508
|
+
return _el$7;
|
|
509
|
+
}
|
|
510
|
+
}), null);
|
|
511
|
+
_$setProp(_el$9, "id", "activity-picker-hint");
|
|
512
|
+
_$insert(_el$9, () => `${ctx.keymap.shortcuts("threads.activity.choose.pin").join(" / ")} ${selected()?.pinned ? "Unpin" : "Pin"} \xB7 enter Open \xB7 esc Close`);
|
|
513
|
+
_$effect((_p$) => {
|
|
514
|
+
var _v$ = foreground(), _v$2 = foreground(), _v$3 = foreground(), _v$4 = Math.max(1, Math.min(choices().length + grouped().length * 2, Math.floor(height() / 2) - 6)), _v$5 = muted();
|
|
515
|
+
_v$ !== _p$.e && (_p$.e = _$setProp(_el$2, "fg", _v$, _p$.e));
|
|
516
|
+
_v$2 !== _p$.t && (_p$.t = _$setProp(_el$5, "textColor", _v$2, _p$.t));
|
|
517
|
+
_v$3 !== _p$.a && (_p$.a = _$setProp(_el$5, "focusedTextColor", _v$3, _p$.a));
|
|
518
|
+
_v$4 !== _p$.o && (_p$.o = _$setProp(_el$6, "height", _v$4, _p$.o));
|
|
519
|
+
_v$5 !== _p$.i && (_p$.i = _$setProp(_el$9, "fg", _v$5, _p$.i));
|
|
520
|
+
return _p$;
|
|
521
|
+
}, {
|
|
522
|
+
e: undefined,
|
|
523
|
+
t: undefined,
|
|
524
|
+
a: undefined,
|
|
525
|
+
o: undefined,
|
|
526
|
+
i: undefined
|
|
527
|
+
});
|
|
528
|
+
return _el$;
|
|
529
|
+
})();
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
// src/activity.ts
|
|
533
|
+
var Link = z2.object({
|
|
534
|
+
workerID: z2.string(),
|
|
535
|
+
coordinatorID: z2.string(),
|
|
536
|
+
key: z2.string(),
|
|
537
|
+
fingerprint: z2.string(),
|
|
538
|
+
initialMessageID: z2.string(),
|
|
539
|
+
reportMessageID: z2.string()
|
|
540
|
+
}).strict();
|
|
541
|
+
function activity(ctx, core, solid, Spinner) {
|
|
542
|
+
const { BoxRenderable, ScrollBoxRenderable, TextRenderable, TextAttributes } = core;
|
|
543
|
+
const { createEffect: createEffect2, createSignal: createSignal2 } = solid;
|
|
544
|
+
const fallbackColor = core.RGBA.fromHex("#808080");
|
|
545
|
+
const [revision, setRevision] = createSignal2(0);
|
|
546
|
+
const [pins, savePins] = ctx.storage.store("activity-pins", {
|
|
547
|
+
initial: { ids: [] }
|
|
548
|
+
});
|
|
549
|
+
const [sections, saveSections] = ctx.storage.store("activity-sections", {
|
|
550
|
+
initial: { collapsed: [] }
|
|
551
|
+
});
|
|
552
|
+
const [threadState, saveThreadState] = ctx.storage.store("activity-threads", {
|
|
553
|
+
initial: { collapsed: [] }
|
|
554
|
+
});
|
|
555
|
+
const [dismissed, saveDismissed] = ctx.storage.store("activity-dismissed", {
|
|
556
|
+
initial: { ids: [] }
|
|
557
|
+
});
|
|
558
|
+
const closingRows = new Set;
|
|
559
|
+
const sessions = new Map;
|
|
560
|
+
const workers = new Map;
|
|
561
|
+
const deleted = new Set;
|
|
562
|
+
const abort = new AbortController;
|
|
563
|
+
const rpc = ctx.client.rpc(ThreadsRpc);
|
|
564
|
+
let stopped = false;
|
|
565
|
+
let loading = false;
|
|
566
|
+
let lastError;
|
|
567
|
+
let render = () => {};
|
|
568
|
+
const changed = () => {
|
|
569
|
+
if (!stopped)
|
|
570
|
+
setRevision((value) => value + 1);
|
|
571
|
+
};
|
|
572
|
+
const error = (value) => {
|
|
573
|
+
const detail = z2.object({ message: z2.string() }).safeParse(value);
|
|
574
|
+
const message = `Activity: ${detail.success ? detail.data.message : String(value)}`;
|
|
575
|
+
if (!stopped && message !== lastError)
|
|
576
|
+
ctx.ui.toast.show({ message, variant: "error" });
|
|
577
|
+
lastError = message;
|
|
578
|
+
};
|
|
579
|
+
async function pin(id) {
|
|
580
|
+
try {
|
|
581
|
+
await savePins((draft) => {
|
|
582
|
+
draft.ids = draft.ids.includes(id) ? draft.ids.filter((value) => value !== id) : [...draft.ids, id];
|
|
583
|
+
});
|
|
584
|
+
changed();
|
|
585
|
+
} catch (value) {
|
|
586
|
+
error(value);
|
|
587
|
+
}
|
|
588
|
+
}
|
|
589
|
+
async function restore(ids) {
|
|
590
|
+
if (!ids.some((id) => dismissed.ids.includes(id)))
|
|
591
|
+
return;
|
|
592
|
+
await saveDismissed((draft) => {
|
|
593
|
+
draft.ids = draft.ids.filter((id) => !ids.includes(id));
|
|
594
|
+
});
|
|
595
|
+
changed();
|
|
596
|
+
}
|
|
597
|
+
async function focus(id) {
|
|
598
|
+
try {
|
|
599
|
+
await restore([id]);
|
|
600
|
+
if (!stopped)
|
|
601
|
+
ctx.ui.tabs.focus(id);
|
|
602
|
+
} catch (value) {
|
|
603
|
+
error(value);
|
|
604
|
+
}
|
|
605
|
+
}
|
|
606
|
+
async function close(id) {
|
|
607
|
+
if (stopped || closingRows.has(id))
|
|
608
|
+
return;
|
|
609
|
+
closingRows.add(id);
|
|
610
|
+
try {
|
|
611
|
+
if (ctx.ui.tabs.list().some((tab) => tab.sessionID === id) && !ctx.ui.tabs.close(id))
|
|
612
|
+
return;
|
|
613
|
+
await saveDismissed((draft) => {
|
|
614
|
+
if (!draft.ids.includes(id))
|
|
615
|
+
draft.ids.push(id);
|
|
616
|
+
});
|
|
617
|
+
} catch (value) {
|
|
618
|
+
error(value);
|
|
619
|
+
} finally {
|
|
620
|
+
closingRows.delete(id);
|
|
621
|
+
changed();
|
|
622
|
+
}
|
|
623
|
+
}
|
|
624
|
+
async function toggleSection(name) {
|
|
625
|
+
try {
|
|
626
|
+
await saveSections((draft) => {
|
|
627
|
+
draft.collapsed = draft.collapsed.includes(name) ? draft.collapsed.filter((value) => value !== name) : [...draft.collapsed, name];
|
|
628
|
+
});
|
|
629
|
+
changed();
|
|
630
|
+
} catch (value) {
|
|
631
|
+
error(value);
|
|
632
|
+
}
|
|
633
|
+
}
|
|
634
|
+
async function toggleThread(id) {
|
|
635
|
+
try {
|
|
636
|
+
await saveThreadState((draft) => {
|
|
637
|
+
draft.collapsed = draft.collapsed.includes(id) ? draft.collapsed.filter((value) => value !== id) : [...draft.collapsed, id];
|
|
638
|
+
});
|
|
639
|
+
changed();
|
|
640
|
+
} catch (value) {
|
|
641
|
+
error(value);
|
|
642
|
+
}
|
|
643
|
+
}
|
|
644
|
+
function items(includeDismissed = false) {
|
|
645
|
+
revision();
|
|
646
|
+
const tabs = new Map(ctx.ui.tabs.list().map((tab) => [tab.sessionID, tab]));
|
|
647
|
+
const merged = new Map(sessions);
|
|
648
|
+
for (const id of [
|
|
649
|
+
...tabs.keys(),
|
|
650
|
+
...pins.ids,
|
|
651
|
+
...[...workers.values()].map((worker) => worker.coordinatorID)
|
|
652
|
+
]) {
|
|
653
|
+
const session = ctx.data.session.get(id);
|
|
654
|
+
if (session)
|
|
655
|
+
merged.set(id, session);
|
|
656
|
+
}
|
|
657
|
+
const result = [];
|
|
658
|
+
const coordinators = new Set([...workers.values()].map((worker) => worker.coordinatorID));
|
|
659
|
+
for (const saved of merged.values()) {
|
|
660
|
+
const session = ctx.data.session.get(saved.id) ?? saved;
|
|
661
|
+
if (deleted.has(session.id) || session.parentID || session.time.archived)
|
|
662
|
+
continue;
|
|
663
|
+
const tab = tabs.get(session.id);
|
|
664
|
+
const isDismissed = dismissed.ids.includes(session.id);
|
|
665
|
+
if (!includeDismissed && isDismissed && !tab?.active)
|
|
666
|
+
continue;
|
|
667
|
+
const link = Link.safeParse(session.metadata?.opThreads);
|
|
668
|
+
const worker = workers.get(session.id);
|
|
669
|
+
const attention = tab ? tab.attention : Boolean(ctx.data.session.permission.list(session.id)?.length || ctx.data.session.form.list(session.id)?.length);
|
|
670
|
+
const busy = tab ? tab.busy : ctx.data.session.status(session.id) === "running";
|
|
671
|
+
if (link.success && !tab && !busy && !attention && !(includeDismissed && isDismissed))
|
|
672
|
+
continue;
|
|
673
|
+
const role = worker && link.success && link.data.workerID === session.id && link.data.coordinatorID === worker.coordinatorID ? "Worker" : coordinators.has(session.id) ? "Main" : undefined;
|
|
674
|
+
result.push({
|
|
675
|
+
id: session.id,
|
|
676
|
+
title: cleanRoleTitle(tab?.title ?? session.title ?? "Untitled", role !== undefined),
|
|
677
|
+
subtitle: activitySubtitle({
|
|
678
|
+
directory: session.location.directory,
|
|
679
|
+
project: ctx.data.project.get(session.projectID),
|
|
680
|
+
role
|
|
681
|
+
}),
|
|
682
|
+
updated: activityTime(session.time),
|
|
683
|
+
active: tab?.active ?? false,
|
|
684
|
+
attention,
|
|
685
|
+
busy,
|
|
686
|
+
unread: tab?.unread,
|
|
687
|
+
pinned: pins.ids.includes(session.id),
|
|
688
|
+
hidden: includeDismissed && isDismissed ? false : worker?.hidden ?? false,
|
|
689
|
+
open: Boolean(tab),
|
|
690
|
+
coordinatorID: role === "Worker" ? worker?.coordinatorID : undefined
|
|
691
|
+
});
|
|
692
|
+
}
|
|
693
|
+
return activityThreads(result);
|
|
694
|
+
}
|
|
695
|
+
async function resolveSession(id) {
|
|
696
|
+
if (stopped || sessions.has(id) || deleted.has(id))
|
|
697
|
+
return;
|
|
698
|
+
try {
|
|
699
|
+
const result = await ctx.client.session.get({ sessionID: id }, { signal: abort.signal });
|
|
700
|
+
if (!stopped)
|
|
701
|
+
sessions.set(id, result);
|
|
702
|
+
} catch (value) {
|
|
703
|
+
const missing = z2.object({
|
|
704
|
+
_tag: z2.literal("SessionNotFoundError"),
|
|
705
|
+
sessionID: z2.string()
|
|
706
|
+
}).safeParse(value);
|
|
707
|
+
if (!missing.success || missing.data.sessionID !== id)
|
|
708
|
+
throw value;
|
|
709
|
+
deleted.add(id);
|
|
710
|
+
if (pins.ids.includes(id))
|
|
711
|
+
await savePins((draft) => {
|
|
712
|
+
draft.ids = draft.ids.filter((value2) => value2 !== id);
|
|
713
|
+
});
|
|
714
|
+
await restore([id]);
|
|
715
|
+
}
|
|
716
|
+
}
|
|
717
|
+
async function load() {
|
|
718
|
+
if (loading || stopped)
|
|
719
|
+
return;
|
|
720
|
+
loading = true;
|
|
721
|
+
try {
|
|
722
|
+
const page = await ctx.client.session.list({
|
|
723
|
+
parentID: null,
|
|
724
|
+
limit: 100,
|
|
725
|
+
order: "desc"
|
|
726
|
+
}, { signal: abort.signal });
|
|
727
|
+
if (stopped)
|
|
728
|
+
return;
|
|
729
|
+
for (const session of page.data)
|
|
730
|
+
sessions.set(session.id, session);
|
|
731
|
+
for (const id of new Set([...pins.ids, ...dismissed.ids]))
|
|
732
|
+
await resolveSession(id);
|
|
733
|
+
const ids = [
|
|
734
|
+
...new Set([...sessions.values()].flatMap((session) => {
|
|
735
|
+
const link = Link.safeParse(session.metadata?.opThreads);
|
|
736
|
+
return link.success ? [session.id, link.data.coordinatorID] : [session.id];
|
|
737
|
+
}))
|
|
738
|
+
];
|
|
739
|
+
for (let index = 0;index < ids.length && !stopped; index += 100) {
|
|
740
|
+
const result = await rpc.snapshot({ coordinatorIDs: ids.slice(index, index + 100) }, {
|
|
741
|
+
signal: abort.signal,
|
|
742
|
+
location: ctx.location ?? ctx.data.location.default()
|
|
743
|
+
});
|
|
744
|
+
for (const worker of result.workers)
|
|
745
|
+
workers.set(worker.workerID, worker);
|
|
746
|
+
}
|
|
747
|
+
for (const id of new Set([...workers.values()].map((worker) => worker.coordinatorID))) {
|
|
748
|
+
await resolveSession(id);
|
|
749
|
+
}
|
|
750
|
+
lastError = undefined;
|
|
751
|
+
} catch (value) {
|
|
752
|
+
error(value);
|
|
753
|
+
} finally {
|
|
754
|
+
loading = false;
|
|
755
|
+
changed();
|
|
756
|
+
}
|
|
757
|
+
}
|
|
758
|
+
async function actions(item, hasChildren) {
|
|
759
|
+
const action = await ctx.ui.dialog.select({
|
|
760
|
+
title: item.title,
|
|
761
|
+
options: [
|
|
762
|
+
{ title: item.pinned ? "Unpin" : "Pin", value: "pin" },
|
|
763
|
+
{
|
|
764
|
+
title: "Close from Activity (keep history)",
|
|
765
|
+
value: "close"
|
|
766
|
+
},
|
|
767
|
+
...hasChildren ? [{
|
|
768
|
+
title: threadState.collapsed.includes(item.id) ? "Expand workers" : "Collapse workers",
|
|
769
|
+
value: "workers"
|
|
770
|
+
}] : []
|
|
771
|
+
]
|
|
772
|
+
});
|
|
773
|
+
if (stopped)
|
|
774
|
+
return;
|
|
775
|
+
if (action === "pin")
|
|
776
|
+
await pin(item.id);
|
|
777
|
+
if (action === "close")
|
|
778
|
+
await close(item.id);
|
|
779
|
+
if (action === "workers")
|
|
780
|
+
await toggleThread(item.id);
|
|
781
|
+
}
|
|
782
|
+
const rail = activityRail(ctx, core, (content) => {
|
|
783
|
+
const runningIndicator = (id) => {
|
|
784
|
+
if (!Spinner)
|
|
785
|
+
return;
|
|
786
|
+
const node = new Spinner(ctx.renderer, {
|
|
787
|
+
id: `activity-running-${id}`,
|
|
788
|
+
frames: ["\u280B", "\u2819", "\u2839", "\u2838", "\u283C", "\u2834", "\u2826", "\u2827", "\u2807", "\u280F"],
|
|
789
|
+
interval: 80,
|
|
790
|
+
color: themeColor(ctx.theme.text, fallbackColor)
|
|
791
|
+
});
|
|
792
|
+
if ("color" in node)
|
|
793
|
+
return node;
|
|
794
|
+
node.destroy();
|
|
795
|
+
};
|
|
796
|
+
const text = (value) => new TextRenderable(ctx.renderer, {
|
|
797
|
+
content: value,
|
|
798
|
+
fg: themeColor(ctx.theme.text, fallbackColor),
|
|
799
|
+
height: 1,
|
|
800
|
+
flexShrink: 0
|
|
801
|
+
});
|
|
802
|
+
const header = text("Activity");
|
|
803
|
+
header.attributes = TextAttributes.BOLD;
|
|
804
|
+
content.add(header);
|
|
805
|
+
const fresh = text("+ New session");
|
|
806
|
+
fresh.id = "activity-new-session";
|
|
807
|
+
fresh.marginTop = 1;
|
|
808
|
+
fresh.marginBottom = 1;
|
|
809
|
+
fresh.onMouseUp = (event) => {
|
|
810
|
+
event.stopPropagation();
|
|
811
|
+
if (event.button === 0)
|
|
812
|
+
ctx.keymap.dispatch("session.new");
|
|
813
|
+
};
|
|
814
|
+
content.add(fresh);
|
|
815
|
+
const scroll = new ScrollBoxRenderable(ctx.renderer, {
|
|
816
|
+
flexGrow: 1,
|
|
817
|
+
scrollY: true,
|
|
818
|
+
scrollX: false
|
|
819
|
+
});
|
|
820
|
+
content.add(scroll);
|
|
821
|
+
const rows = new Map;
|
|
822
|
+
const headings = new Map;
|
|
823
|
+
const help = text("/activities");
|
|
824
|
+
content.add(help);
|
|
825
|
+
render = () => {
|
|
826
|
+
const fg = themeColor(ctx.theme.text, fallbackColor);
|
|
827
|
+
const muted = themeMuted(ctx.theme.text, fg);
|
|
828
|
+
const background = ctx.theme.background.raised?.base ?? themeColor(ctx.theme.background, fallbackColor);
|
|
829
|
+
const accent = themeHue(ctx.theme.hue?.accent, fg, background);
|
|
830
|
+
const workerColor = themeHue(ctx.theme.hue?.purple, fg, background);
|
|
831
|
+
const runningColor = themeColor(ctx.theme.text.feedback.info, fg);
|
|
832
|
+
const attentionColor = themeColor(ctx.theme.text.feedback.warning, fg);
|
|
833
|
+
header.fg = accent;
|
|
834
|
+
fresh.fg = accent;
|
|
835
|
+
help.fg = muted;
|
|
836
|
+
const desired = [];
|
|
837
|
+
const keep = new Set;
|
|
838
|
+
const groups = items();
|
|
839
|
+
for (const [name, group] of groups) {
|
|
840
|
+
let heading = headings.get(name);
|
|
841
|
+
if (!heading) {
|
|
842
|
+
const box = new BoxRenderable(ctx.renderer, {
|
|
843
|
+
id: `activity-section-${encodeURIComponent(name)}`,
|
|
844
|
+
flexDirection: "column",
|
|
845
|
+
flexShrink: 0,
|
|
846
|
+
shouldFill: false,
|
|
847
|
+
onMouseUp(event) {
|
|
848
|
+
event.stopPropagation();
|
|
849
|
+
if (event.button === 0)
|
|
850
|
+
toggleSection(name);
|
|
851
|
+
}
|
|
852
|
+
});
|
|
853
|
+
const label = text("");
|
|
854
|
+
label.attributes = TextAttributes.BOLD;
|
|
855
|
+
box.add(label);
|
|
856
|
+
heading = { box, label };
|
|
857
|
+
headings.set(name, heading);
|
|
858
|
+
}
|
|
859
|
+
const collapsed = sections.collapsed.includes(name);
|
|
860
|
+
heading.box.border = desired.length ? ["top"] : false;
|
|
861
|
+
heading.box.borderColor = themeColor(ctx.theme.border, fg);
|
|
862
|
+
heading.box.height = desired.length ? 3 : 2;
|
|
863
|
+
heading.label.fg = name === "Priority" ? attentionColor : name === "Pinned" ? workerColor : accent;
|
|
864
|
+
const count = group.reduce((total, thread) => total + 1 + thread.children.length, 0);
|
|
865
|
+
heading.label.content = `${collapsed ? "\u25B8" : "\u25BE"} ${name} (${count})`;
|
|
866
|
+
desired.push(heading.box);
|
|
867
|
+
if (collapsed)
|
|
868
|
+
continue;
|
|
869
|
+
const displayed = group.flatMap((thread) => {
|
|
870
|
+
const collapsed2 = threadState.collapsed.includes(thread.item.id);
|
|
871
|
+
return [
|
|
872
|
+
{
|
|
873
|
+
item: thread.item,
|
|
874
|
+
depth: 0,
|
|
875
|
+
children: thread.children.length,
|
|
876
|
+
collapsed: collapsed2,
|
|
877
|
+
status: collapsed2 ? thread.status : thread.item
|
|
878
|
+
},
|
|
879
|
+
...collapsed2 ? [] : thread.children.map((item) => ({
|
|
880
|
+
item,
|
|
881
|
+
depth: 1,
|
|
882
|
+
children: 0,
|
|
883
|
+
collapsed: false,
|
|
884
|
+
status: item
|
|
885
|
+
}))
|
|
886
|
+
];
|
|
887
|
+
});
|
|
888
|
+
for (const { item, depth, children, collapsed: collapsed2, status } of displayed) {
|
|
889
|
+
keep.add(item.id);
|
|
890
|
+
let row = rows.get(item.id);
|
|
891
|
+
if (!row) {
|
|
892
|
+
const box = new BoxRenderable(ctx.renderer, {
|
|
893
|
+
id: `activity-row-${item.id}`,
|
|
894
|
+
height: 3,
|
|
895
|
+
flexShrink: 0,
|
|
896
|
+
flexDirection: "column"
|
|
897
|
+
});
|
|
898
|
+
const line = new BoxRenderable(ctx.renderer, {
|
|
899
|
+
height: 1,
|
|
900
|
+
flexShrink: 0,
|
|
901
|
+
flexDirection: "row"
|
|
902
|
+
});
|
|
903
|
+
const selected = text("");
|
|
904
|
+
selected.id = `activity-selected-${item.id}`;
|
|
905
|
+
selected.width = 2;
|
|
906
|
+
const status2 = new BoxRenderable(ctx.renderer, {
|
|
907
|
+
width: 2,
|
|
908
|
+
height: 1,
|
|
909
|
+
flexShrink: 0
|
|
910
|
+
});
|
|
911
|
+
const marker = text("");
|
|
912
|
+
marker.id = `activity-marker-${item.id}`;
|
|
913
|
+
status2.add(marker);
|
|
914
|
+
const title = text("");
|
|
915
|
+
title.id = `activity-title-${item.id}`;
|
|
916
|
+
title.flexShrink = 1;
|
|
917
|
+
title.minWidth = 0;
|
|
918
|
+
title.wrapMode = "none";
|
|
919
|
+
title.truncate = true;
|
|
920
|
+
const pinButton = text("");
|
|
921
|
+
pinButton.id = `activity-pin-${item.id}`;
|
|
922
|
+
pinButton.marginLeft = 2;
|
|
923
|
+
pinButton.width = 4;
|
|
924
|
+
const closeButton = text("");
|
|
925
|
+
closeButton.id = `activity-close-${item.id}`;
|
|
926
|
+
closeButton.marginLeft = 1;
|
|
927
|
+
closeButton.width = 4;
|
|
928
|
+
row = {
|
|
929
|
+
box,
|
|
930
|
+
selected,
|
|
931
|
+
status: status2,
|
|
932
|
+
marker,
|
|
933
|
+
spinner: undefined,
|
|
934
|
+
title,
|
|
935
|
+
subtitle: text(""),
|
|
936
|
+
workers: undefined,
|
|
937
|
+
pin: pinButton,
|
|
938
|
+
close: closeButton
|
|
939
|
+
};
|
|
940
|
+
line.add(selected);
|
|
941
|
+
line.add(status2);
|
|
942
|
+
line.add(title);
|
|
943
|
+
line.add(pinButton);
|
|
944
|
+
line.add(closeButton);
|
|
945
|
+
box.add(line);
|
|
946
|
+
row.subtitle.id = `activity-subtitle-${item.id}`;
|
|
947
|
+
row.subtitle.wrapMode = "none";
|
|
948
|
+
row.subtitle.truncate = true;
|
|
949
|
+
box.add(row.subtitle);
|
|
950
|
+
rows.set(item.id, row);
|
|
951
|
+
}
|
|
952
|
+
row.box.marginLeft = depth * 3;
|
|
953
|
+
row.box.height = children ? 5 : 3;
|
|
954
|
+
const running = status.busy && !status.attention;
|
|
955
|
+
if (running && !row.spinner) {
|
|
956
|
+
row.spinner = runningIndicator(item.id);
|
|
957
|
+
if (row.spinner)
|
|
958
|
+
row.status.add(row.spinner);
|
|
959
|
+
} else if (!running && row.spinner) {
|
|
960
|
+
row.spinner.destroy();
|
|
961
|
+
row.spinner = undefined;
|
|
962
|
+
}
|
|
963
|
+
if (row.spinner)
|
|
964
|
+
row.spinner.color = runningColor;
|
|
965
|
+
row.selected.content = item.active ? "> " : " ";
|
|
966
|
+
row.selected.fg = accent;
|
|
967
|
+
row.selected.attributes = item.active ? TextAttributes.BOLD : 0;
|
|
968
|
+
row.marker.content = status.attention ? "?" : running ? "\u280B" : status.unread === "error" ? "!" : status.unread ? "\u2022" : " ";
|
|
969
|
+
row.marker.visible = !row.spinner;
|
|
970
|
+
row.marker.fg = status.attention ? attentionColor : running ? runningColor : status.unread === "error" ? themeColor(ctx.theme.text.feedback.error, fg) : accent;
|
|
971
|
+
row.title.content = item.title;
|
|
972
|
+
row.title.fg = status.attention ? attentionColor : item.active ? accent : fg;
|
|
973
|
+
row.subtitle.fg = muted;
|
|
974
|
+
row.title.attributes = item.active ? TextAttributes.BOLD : 0;
|
|
975
|
+
row.subtitle.content = ` ${item.subtitle}`;
|
|
976
|
+
if (children && !row.workers) {
|
|
977
|
+
row.workers = text("");
|
|
978
|
+
row.workers.id = `activity-workers-${item.id}`;
|
|
979
|
+
row.workers.marginLeft = 3;
|
|
980
|
+
row.workers.marginTop = 1;
|
|
981
|
+
row.workers.attributes = TextAttributes.BOLD;
|
|
982
|
+
row.workers.onMouseUp = (event) => {
|
|
983
|
+
event.stopPropagation();
|
|
984
|
+
if (event.button === 0)
|
|
985
|
+
toggleThread(item.id);
|
|
986
|
+
};
|
|
987
|
+
row.box.add(row.workers);
|
|
988
|
+
} else if (!children && row.workers) {
|
|
989
|
+
row.workers.destroy();
|
|
990
|
+
row.workers = undefined;
|
|
991
|
+
}
|
|
992
|
+
if (row.workers) {
|
|
993
|
+
row.workers.content = `${collapsed2 ? "\u25B8" : "\u25BE"} Workers (${children})`;
|
|
994
|
+
row.workers.fg = workerColor;
|
|
995
|
+
}
|
|
996
|
+
row.pin.content = item.pinned ? "[\u25C6] " : "[\u25C7] ";
|
|
997
|
+
row.pin.fg = item.pinned ? workerColor : muted;
|
|
998
|
+
row.pin.attributes = TextAttributes.BOLD;
|
|
999
|
+
row.pin.onMouseUp = (event) => {
|
|
1000
|
+
event.stopPropagation();
|
|
1001
|
+
if (event.button === 0)
|
|
1002
|
+
pin(item.id);
|
|
1003
|
+
};
|
|
1004
|
+
row.close.content = "[\xD7] ";
|
|
1005
|
+
row.close.fg = muted;
|
|
1006
|
+
row.close.attributes = TextAttributes.BOLD;
|
|
1007
|
+
row.close.onMouseUp = (event) => {
|
|
1008
|
+
event.stopPropagation();
|
|
1009
|
+
if (event.button === 0)
|
|
1010
|
+
close(item.id);
|
|
1011
|
+
};
|
|
1012
|
+
row.box.onMouseUp = (event) => {
|
|
1013
|
+
event.stopPropagation();
|
|
1014
|
+
if (event.button === 0)
|
|
1015
|
+
focus(item.id);
|
|
1016
|
+
if (event.button === 2)
|
|
1017
|
+
actions(item, children > 0);
|
|
1018
|
+
};
|
|
1019
|
+
desired.push(row.box);
|
|
1020
|
+
}
|
|
1021
|
+
}
|
|
1022
|
+
for (const [id, row] of rows)
|
|
1023
|
+
if (!keep.has(id)) {
|
|
1024
|
+
row.box.destroyRecursively();
|
|
1025
|
+
rows.delete(id);
|
|
1026
|
+
}
|
|
1027
|
+
for (const [name, heading] of headings)
|
|
1028
|
+
if (!groups.has(name)) {
|
|
1029
|
+
heading.box.destroyRecursively();
|
|
1030
|
+
headings.delete(name);
|
|
1031
|
+
}
|
|
1032
|
+
for (const [index, node] of desired.entries())
|
|
1033
|
+
if (scroll.getChildren()[index] !== node)
|
|
1034
|
+
scroll.add(node, index);
|
|
1035
|
+
};
|
|
1036
|
+
render();
|
|
1037
|
+
return () => {
|
|
1038
|
+
render = () => {};
|
|
1039
|
+
};
|
|
1040
|
+
});
|
|
1041
|
+
rail.toggle(ctx.options.activity !== false);
|
|
1042
|
+
let refreshTimer;
|
|
1043
|
+
const stopEvents = ctx.data.listen(({ details }) => {
|
|
1044
|
+
if (!details.type.startsWith("session."))
|
|
1045
|
+
return;
|
|
1046
|
+
if (details.type === "session.deleted") {
|
|
1047
|
+
const sessionID = details.data.sessionID;
|
|
1048
|
+
deleted.add(sessionID);
|
|
1049
|
+
sessions.delete(sessionID);
|
|
1050
|
+
workers.delete(sessionID);
|
|
1051
|
+
restore([sessionID]).catch(error);
|
|
1052
|
+
if (pins.ids.includes(sessionID))
|
|
1053
|
+
savePins((draft) => {
|
|
1054
|
+
draft.ids = draft.ids.filter((id) => id !== sessionID);
|
|
1055
|
+
}).catch(error);
|
|
1056
|
+
changed();
|
|
1057
|
+
}
|
|
1058
|
+
if (details.type === "session.created") {
|
|
1059
|
+
deleted.delete(details.data.sessionID);
|
|
1060
|
+
changed();
|
|
1061
|
+
}
|
|
1062
|
+
if (!refreshTimer)
|
|
1063
|
+
refreshTimer = setTimeout(() => {
|
|
1064
|
+
refreshTimer = undefined;
|
|
1065
|
+
load();
|
|
1066
|
+
}, 1000);
|
|
1067
|
+
});
|
|
1068
|
+
const timer = setInterval(() => {
|
|
1069
|
+
rail.invalidate();
|
|
1070
|
+
changed();
|
|
1071
|
+
load();
|
|
1072
|
+
}, 30000);
|
|
1073
|
+
const removeSlot = ctx.ui.slot({
|
|
1074
|
+
append: "app",
|
|
1075
|
+
render() {
|
|
1076
|
+
let selected;
|
|
1077
|
+
createEffect2(() => {
|
|
1078
|
+
const route = ctx.ui.router.current();
|
|
1079
|
+
const id = route.type === "session" ? route.sessionID : undefined;
|
|
1080
|
+
if (id === selected)
|
|
1081
|
+
return;
|
|
1082
|
+
selected = id;
|
|
1083
|
+
if (id && !closingRows.has(id))
|
|
1084
|
+
restore([id]).catch(error);
|
|
1085
|
+
});
|
|
1086
|
+
createEffect2(() => {
|
|
1087
|
+
ctx.themeMode;
|
|
1088
|
+
themeColor(ctx.theme.text, fallbackColor);
|
|
1089
|
+
themeColor(ctx.theme.border, fallbackColor);
|
|
1090
|
+
sections.collapsed.length;
|
|
1091
|
+
threadState.collapsed.length;
|
|
1092
|
+
items();
|
|
1093
|
+
render();
|
|
1094
|
+
rail.invalidate();
|
|
1095
|
+
});
|
|
1096
|
+
ctx.keymap.layer(() => ({
|
|
1097
|
+
mode: "global",
|
|
1098
|
+
commands: [
|
|
1099
|
+
{
|
|
1100
|
+
id: "threads.activity.threads",
|
|
1101
|
+
title: "Expand/collapse managed workers",
|
|
1102
|
+
palette: true,
|
|
1103
|
+
slash: { name: "activity-threads" },
|
|
1104
|
+
async run() {
|
|
1105
|
+
const id = await ctx.ui.dialog.select({
|
|
1106
|
+
title: "Managed worker stacks",
|
|
1107
|
+
options: [...items().values()].flat().filter((thread) => thread.children.length).map((thread) => ({
|
|
1108
|
+
title: `${threadState.collapsed.includes(thread.item.id) ? "Expand" : "Collapse"} ${thread.item.title}`,
|
|
1109
|
+
description: `${thread.children.length} workers`,
|
|
1110
|
+
value: thread.item.id
|
|
1111
|
+
}))
|
|
1112
|
+
});
|
|
1113
|
+
if (id && !stopped)
|
|
1114
|
+
await toggleThread(id);
|
|
1115
|
+
}
|
|
1116
|
+
},
|
|
1117
|
+
{
|
|
1118
|
+
id: "threads.activity.sections",
|
|
1119
|
+
title: "Expand/collapse Activity section",
|
|
1120
|
+
palette: true,
|
|
1121
|
+
slash: { name: "activity-sections" },
|
|
1122
|
+
async run() {
|
|
1123
|
+
const name = await ctx.ui.dialog.select({
|
|
1124
|
+
title: "Activity sections",
|
|
1125
|
+
options: [...items()].map(([name2, group]) => ({
|
|
1126
|
+
title: `${sections.collapsed.includes(name2) ? "Expand" : "Collapse"} ${name2}`,
|
|
1127
|
+
description: `${group.reduce((total, thread) => total + 1 + thread.children.length, 0)} conversations`,
|
|
1128
|
+
value: name2
|
|
1129
|
+
}))
|
|
1130
|
+
});
|
|
1131
|
+
if (name && !stopped)
|
|
1132
|
+
await toggleSection(name);
|
|
1133
|
+
}
|
|
1134
|
+
},
|
|
1135
|
+
{
|
|
1136
|
+
id: "threads.activity.toggle",
|
|
1137
|
+
title: "Toggle Activity sidebar",
|
|
1138
|
+
palette: true,
|
|
1139
|
+
slash: { name: "activity" },
|
|
1140
|
+
run() {
|
|
1141
|
+
rail.toggle();
|
|
1142
|
+
}
|
|
1143
|
+
},
|
|
1144
|
+
{
|
|
1145
|
+
id: "threads.activity.pin",
|
|
1146
|
+
title: "Pin/unpin current Activity conversation",
|
|
1147
|
+
palette: true,
|
|
1148
|
+
slash: { name: "pin" },
|
|
1149
|
+
async run() {
|
|
1150
|
+
const route = ctx.ui.router.current();
|
|
1151
|
+
if (route.type === "session")
|
|
1152
|
+
await pin(route.sessionID);
|
|
1153
|
+
}
|
|
1154
|
+
},
|
|
1155
|
+
{
|
|
1156
|
+
id: "threads.activity.choose",
|
|
1157
|
+
title: "Choose Activity conversation",
|
|
1158
|
+
palette: true,
|
|
1159
|
+
slash: { name: "activities" },
|
|
1160
|
+
run() {
|
|
1161
|
+
const route = ctx.ui.router.current();
|
|
1162
|
+
ctx.ui.dialog.show(() => ActivityPicker({
|
|
1163
|
+
ctx,
|
|
1164
|
+
fallbackColor,
|
|
1165
|
+
current: route.type === "session" ? route.sessionID : undefined,
|
|
1166
|
+
items: () => [...items(true)].flatMap(([category, group]) => group.flatMap((thread) => [thread.item, ...thread.children]).map((item) => ({
|
|
1167
|
+
...item,
|
|
1168
|
+
category,
|
|
1169
|
+
closed: dismissed.ids.includes(item.id)
|
|
1170
|
+
}))),
|
|
1171
|
+
pin,
|
|
1172
|
+
open: focus
|
|
1173
|
+
}));
|
|
1174
|
+
}
|
|
1175
|
+
}
|
|
1176
|
+
]
|
|
1177
|
+
}));
|
|
1178
|
+
return null;
|
|
1179
|
+
}
|
|
1180
|
+
});
|
|
1181
|
+
load();
|
|
1182
|
+
return {
|
|
1183
|
+
mounted: rail.mounted,
|
|
1184
|
+
isDismissed: (id) => closingRows.has(id) || dismissed.ids.includes(id),
|
|
1185
|
+
restore,
|
|
1186
|
+
updateWorkers(values) {
|
|
1187
|
+
for (const worker of values)
|
|
1188
|
+
workers.set(worker.workerID, worker);
|
|
1189
|
+
changed();
|
|
1190
|
+
},
|
|
1191
|
+
dispose() {
|
|
1192
|
+
stopped = true;
|
|
1193
|
+
abort.abort();
|
|
1194
|
+
clearInterval(timer);
|
|
1195
|
+
clearTimeout(refreshTimer);
|
|
1196
|
+
stopEvents();
|
|
1197
|
+
removeSlot();
|
|
1198
|
+
rail.dispose();
|
|
1199
|
+
}
|
|
1200
|
+
};
|
|
1201
|
+
}
|
|
1202
|
+
|
|
1203
|
+
// src/workflow-ui.tsx
|
|
1204
|
+
import { memo as _$memo } from "@opentui/solid";
|
|
1205
|
+
import { insert as _$insert2 } from "@opentui/solid";
|
|
1206
|
+
import { createComponent as _$createComponent2 } from "@opentui/solid";
|
|
1207
|
+
import { createTextNode as _$createTextNode2 } from "@opentui/solid";
|
|
1208
|
+
import { insertNode as _$insertNode2 } from "@opentui/solid";
|
|
1209
|
+
import { setProp as _$setProp2 } from "@opentui/solid";
|
|
1210
|
+
import { createElement as _$createElement2 } from "@opentui/solid";
|
|
1211
|
+
import { createEffect as createEffect2, createSignal as createSignal2, For as For2, Show as Show2 } from "solid-js";
|
|
1212
|
+
|
|
1213
|
+
// src/workflow-rpc.ts
|
|
1214
|
+
import { Rpc as Rpc2 } from "@opencode/plugin/rpc";
|
|
1215
|
+
import { z as z4 } from "zod";
|
|
1216
|
+
|
|
1217
|
+
// src/workflow-types.ts
|
|
1218
|
+
import { z as z3 } from "zod";
|
|
1219
|
+
var WORKFLOW_CONTROL_HEADROOM = 64 * 1024;
|
|
1220
|
+
var Json = z3.json();
|
|
1221
|
+
var WorkflowModel = z3.object({
|
|
1222
|
+
providerID: z3.string(),
|
|
1223
|
+
id: z3.string(),
|
|
1224
|
+
variant: z3.string().optional()
|
|
1225
|
+
});
|
|
1226
|
+
var WorkflowLimits = z3.object({
|
|
1227
|
+
concurrency: z3.number().int().min(1).max(8).default(3),
|
|
1228
|
+
maxAgents: z3.number().int().min(1).max(1000).default(4),
|
|
1229
|
+
agentTimeoutMs: z3.number().int().min(1000).max(604800000).default(1800000),
|
|
1230
|
+
timeoutMs: z3.number().int().min(1000).max(604800000).default(86400000),
|
|
1231
|
+
tokenBudget: z3.number().int().positive().optional()
|
|
1232
|
+
});
|
|
1233
|
+
var WorkflowStart = WorkflowLimits.extend({
|
|
1234
|
+
key: z3.string().min(1).max(120),
|
|
1235
|
+
script: z3.string().min(1).max(200000).optional(),
|
|
1236
|
+
name: z3.string().regex(/^[a-zA-Z0-9][a-zA-Z0-9_-]{0,79}$/).optional(),
|
|
1237
|
+
args: Json.default(null)
|
|
1238
|
+
}).strict().refine((value) => Boolean(value.script) !== Boolean(value.name), {
|
|
1239
|
+
message: "Supply exactly one of script or saved workflow name"
|
|
1240
|
+
});
|
|
1241
|
+
var WorkflowAgentInput = z3.object({
|
|
1242
|
+
key: z3.string().min(1).max(120),
|
|
1243
|
+
prompt: z3.string().min(1).max(1e5),
|
|
1244
|
+
agent: z3.string().min(1),
|
|
1245
|
+
label: z3.string().min(1).max(160).optional(),
|
|
1246
|
+
phase: z3.string().min(1).max(160).optional(),
|
|
1247
|
+
schema: z3.record(z3.string(), Json).optional(),
|
|
1248
|
+
access: z3.enum(["read", "write"]).default("read"),
|
|
1249
|
+
isolation: z3.enum(["shared", "worktree"]).default("shared"),
|
|
1250
|
+
directory: z3.string().min(1).optional(),
|
|
1251
|
+
timeoutMs: z3.number().int().min(1000).max(604800000).optional()
|
|
1252
|
+
}).strict();
|
|
1253
|
+
var WorkflowResult = Report.extend({ result: Json }).strict();
|
|
1254
|
+
var WorkflowUsage = z3.object({
|
|
1255
|
+
tokens: z3.number().nonnegative(),
|
|
1256
|
+
cost: z3.number().nonnegative(),
|
|
1257
|
+
measured: z3.boolean()
|
|
1258
|
+
});
|
|
1259
|
+
var StepBase = z3.object({
|
|
1260
|
+
key: z3.string(),
|
|
1261
|
+
fingerprint: z3.string(),
|
|
1262
|
+
index: z3.number().int().nonnegative(),
|
|
1263
|
+
input: WorkflowAgentInput,
|
|
1264
|
+
workerID: z3.string(),
|
|
1265
|
+
spawnKey: z3.string(),
|
|
1266
|
+
created: z3.number(),
|
|
1267
|
+
phase: z3.string(),
|
|
1268
|
+
directory: z3.string(),
|
|
1269
|
+
model: WorkflowModel,
|
|
1270
|
+
profileFingerprint: z3.string()
|
|
1271
|
+
});
|
|
1272
|
+
var WorkflowStep = z3.discriminatedUnion("status", [
|
|
1273
|
+
StepBase.extend({ status: z3.literal("prepared") }),
|
|
1274
|
+
StepBase.extend({ status: z3.literal("running"), usage: WorkflowUsage.optional() }),
|
|
1275
|
+
StepBase.extend({
|
|
1276
|
+
status: z3.literal("completed"),
|
|
1277
|
+
completed: z3.number(),
|
|
1278
|
+
report: WorkflowResult,
|
|
1279
|
+
usage: WorkflowUsage
|
|
1280
|
+
}),
|
|
1281
|
+
StepBase.extend({
|
|
1282
|
+
status: z3.literal("failed"),
|
|
1283
|
+
error: z3.string(),
|
|
1284
|
+
retryable: z3.boolean(),
|
|
1285
|
+
usage: WorkflowUsage.optional()
|
|
1286
|
+
})
|
|
1287
|
+
]);
|
|
1288
|
+
var WorkflowCheckpoint = z3.object({
|
|
1289
|
+
key: z3.string(),
|
|
1290
|
+
prompt: z3.string(),
|
|
1291
|
+
response: Json.optional()
|
|
1292
|
+
});
|
|
1293
|
+
var WorkflowSettlement = z3.discriminatedUnion("kind", [
|
|
1294
|
+
z3.object({ kind: z3.literal("agent"), key: z3.string(), outcome: z3.enum(["success", "failure"]), error: z3.string().optional() }),
|
|
1295
|
+
z3.object({ kind: z3.literal("checkpoint"), key: z3.string(), response: Json })
|
|
1296
|
+
]);
|
|
1297
|
+
var WorkflowRun = z3.object({
|
|
1298
|
+
version: z3.literal(1),
|
|
1299
|
+
id: z3.string(),
|
|
1300
|
+
key: z3.string(),
|
|
1301
|
+
ownerID: z3.string(),
|
|
1302
|
+
callerAgent: z3.string(),
|
|
1303
|
+
model: WorkflowModel,
|
|
1304
|
+
projectID: z3.string(),
|
|
1305
|
+
directory: z3.string(),
|
|
1306
|
+
name: z3.string(),
|
|
1307
|
+
description: z3.string(),
|
|
1308
|
+
script: z3.string(),
|
|
1309
|
+
args: Json,
|
|
1310
|
+
fingerprint: z3.string(),
|
|
1311
|
+
limits: WorkflowLimits,
|
|
1312
|
+
status: z3.enum(["running", "pausing", "paused", "stopping", "stopped", "waiting", "interrupted", "failed", "completed"]),
|
|
1313
|
+
created: z3.number(),
|
|
1314
|
+
updated: z3.number(),
|
|
1315
|
+
phase: z3.string(),
|
|
1316
|
+
steps: z3.array(WorkflowStep),
|
|
1317
|
+
logs: z3.array(z3.object({ time: z3.number(), text: z3.string() })),
|
|
1318
|
+
checkpoints: z3.array(WorkflowCheckpoint),
|
|
1319
|
+
settlements: z3.array(WorkflowSettlement).optional(),
|
|
1320
|
+
result: Json.optional(),
|
|
1321
|
+
error: z3.string().optional(),
|
|
1322
|
+
deliveryID: z3.string(),
|
|
1323
|
+
delivered: z3.boolean()
|
|
1324
|
+
});
|
|
1325
|
+
var WorkflowSummary = WorkflowRun.omit({ script: true, args: true, result: true, steps: true, logs: true, checkpoints: true, settlements: true, fingerprint: true, callerAgent: true, deliveryID: true, delivered: true }).extend({
|
|
1326
|
+
counts: z3.object({ completed: z3.number(), running: z3.number(), failed: z3.number(), total: z3.number() }),
|
|
1327
|
+
usage: WorkflowUsage
|
|
1328
|
+
});
|
|
1329
|
+
|
|
1330
|
+
// src/workflow-rpc.ts
|
|
1331
|
+
var WorkflowControl = z4.object({
|
|
1332
|
+
runID: z4.string().min(1),
|
|
1333
|
+
action: z4.enum(["pause", "resume", "stop"]),
|
|
1334
|
+
checkpointKey: z4.string().optional(),
|
|
1335
|
+
response: Json.optional()
|
|
1336
|
+
}).strict();
|
|
1337
|
+
var WorkflowsRpc = Rpc2.define({
|
|
1338
|
+
id: "workflows",
|
|
1339
|
+
methods: {
|
|
1340
|
+
snapshot: {
|
|
1341
|
+
input: z4.object({ ownerID: z4.string() }).strict(),
|
|
1342
|
+
output: z4.object({ runs: z4.array(WorkflowSummary) }),
|
|
1343
|
+
errors: {}
|
|
1344
|
+
},
|
|
1345
|
+
inspect: {
|
|
1346
|
+
input: z4.object({ ownerID: z4.string(), runID: z4.string() }).strict(),
|
|
1347
|
+
output: WorkflowRun,
|
|
1348
|
+
errors: {}
|
|
1349
|
+
},
|
|
1350
|
+
control: {
|
|
1351
|
+
input: WorkflowControl.extend({ ownerID: z4.string() }).strict(),
|
|
1352
|
+
output: WorkflowRun,
|
|
1353
|
+
errors: {}
|
|
1354
|
+
},
|
|
1355
|
+
save: {
|
|
1356
|
+
input: z4.object({ ownerID: z4.string(), runID: z4.string(), name: z4.string(), scope: z4.enum(["project", "user"]) }).strict(),
|
|
1357
|
+
output: z4.object({ path: z4.string() }),
|
|
1358
|
+
errors: {}
|
|
1359
|
+
}
|
|
1360
|
+
},
|
|
1361
|
+
events: {
|
|
1362
|
+
updated: { schema: z4.object({ ownerID: z4.string(), runID: z4.string() }) }
|
|
1363
|
+
}
|
|
1364
|
+
});
|
|
1365
|
+
|
|
1366
|
+
// src/workflow-ui.tsx
|
|
1367
|
+
function workflowUI(ctx) {
|
|
1368
|
+
const rpc = ctx.client.rpc(WorkflowsRpc);
|
|
1369
|
+
const [runs, setRuns] = createSignal2([]);
|
|
1370
|
+
const [selected, setSelected] = createSignal2();
|
|
1371
|
+
const [stepIndex, setStepIndex] = createSignal2(0);
|
|
1372
|
+
const [error, setError] = createSignal2("");
|
|
1373
|
+
const abort = new AbortController;
|
|
1374
|
+
let owner = "";
|
|
1375
|
+
let ownerGeneration = 0;
|
|
1376
|
+
let refreshInFlight;
|
|
1377
|
+
const location = () => ctx.location ?? ctx.data.location.default();
|
|
1378
|
+
function ownerID() {
|
|
1379
|
+
const route = ctx.ui.router.current();
|
|
1380
|
+
if (route.type !== "session")
|
|
1381
|
+
return;
|
|
1382
|
+
const session = ctx.data.session.get(route.sessionID);
|
|
1383
|
+
const link = session?.metadata?.opThreads;
|
|
1384
|
+
return link && typeof link === "object" && "coordinatorID" in link && typeof link.coordinatorID === "string" ? link.coordinatorID : route.sessionID;
|
|
1385
|
+
}
|
|
1386
|
+
function synchronizeOwner() {
|
|
1387
|
+
const current = ownerID() ?? "";
|
|
1388
|
+
if (current !== owner) {
|
|
1389
|
+
owner = current;
|
|
1390
|
+
ownerGeneration += 1;
|
|
1391
|
+
setRuns([]);
|
|
1392
|
+
setSelected(undefined);
|
|
1393
|
+
}
|
|
1394
|
+
return current ? {
|
|
1395
|
+
ownerID: current,
|
|
1396
|
+
generation: ownerGeneration
|
|
1397
|
+
} : undefined;
|
|
1398
|
+
}
|
|
1399
|
+
function isCurrent(context) {
|
|
1400
|
+
const current = synchronizeOwner();
|
|
1401
|
+
return !!current && current.ownerID === context.ownerID && current.generation === context.generation && !abort.signal.aborted;
|
|
1402
|
+
}
|
|
1403
|
+
function requestRefresh(context) {
|
|
1404
|
+
let request;
|
|
1405
|
+
request = (async () => {
|
|
1406
|
+
try {
|
|
1407
|
+
const snapshot = await rpc.snapshot({
|
|
1408
|
+
ownerID: context.ownerID
|
|
1409
|
+
}, {
|
|
1410
|
+
location: location(),
|
|
1411
|
+
signal: abort.signal
|
|
1412
|
+
});
|
|
1413
|
+
if (!isCurrent(context))
|
|
1414
|
+
return;
|
|
1415
|
+
setRuns(snapshot.runs);
|
|
1416
|
+
const id = selected()?.id;
|
|
1417
|
+
if (id) {
|
|
1418
|
+
const run = await rpc.inspect({
|
|
1419
|
+
ownerID: context.ownerID,
|
|
1420
|
+
runID: id
|
|
1421
|
+
}, {
|
|
1422
|
+
location: location(),
|
|
1423
|
+
signal: abort.signal
|
|
1424
|
+
});
|
|
1425
|
+
if (!isCurrent(context))
|
|
1426
|
+
return;
|
|
1427
|
+
if (selected()?.id === id)
|
|
1428
|
+
setSelected(run);
|
|
1429
|
+
}
|
|
1430
|
+
setError("");
|
|
1431
|
+
return {
|
|
1432
|
+
context,
|
|
1433
|
+
runs: snapshot.runs
|
|
1434
|
+
};
|
|
1435
|
+
} catch (cause) {
|
|
1436
|
+
if (isCurrent(context))
|
|
1437
|
+
setError(String(cause));
|
|
1438
|
+
} finally {
|
|
1439
|
+
if (refreshInFlight === request)
|
|
1440
|
+
refreshInFlight = undefined;
|
|
1441
|
+
}
|
|
1442
|
+
})();
|
|
1443
|
+
refreshInFlight = request;
|
|
1444
|
+
return request;
|
|
1445
|
+
}
|
|
1446
|
+
function refreshInBackground() {
|
|
1447
|
+
const context = synchronizeOwner();
|
|
1448
|
+
if (!context || refreshInFlight || abort.signal.aborted)
|
|
1449
|
+
return;
|
|
1450
|
+
requestRefresh(context);
|
|
1451
|
+
}
|
|
1452
|
+
async function refreshFresh() {
|
|
1453
|
+
synchronizeOwner();
|
|
1454
|
+
while (refreshInFlight)
|
|
1455
|
+
await refreshInFlight;
|
|
1456
|
+
const context = synchronizeOwner();
|
|
1457
|
+
if (!context || abort.signal.aborted)
|
|
1458
|
+
return;
|
|
1459
|
+
return requestRefresh(context);
|
|
1460
|
+
}
|
|
1461
|
+
async function openRun(id, expected = synchronizeOwner()) {
|
|
1462
|
+
if (!expected || !isCurrent(expected))
|
|
1463
|
+
return;
|
|
1464
|
+
try {
|
|
1465
|
+
const run = await rpc.inspect({
|
|
1466
|
+
ownerID: expected.ownerID,
|
|
1467
|
+
runID: id
|
|
1468
|
+
}, {
|
|
1469
|
+
location: location(),
|
|
1470
|
+
signal: abort.signal
|
|
1471
|
+
});
|
|
1472
|
+
if (!isCurrent(expected))
|
|
1473
|
+
return;
|
|
1474
|
+
setSelected(run);
|
|
1475
|
+
setStepIndex(0);
|
|
1476
|
+
ctx.ui.panel.open("threads.workflows");
|
|
1477
|
+
} catch (cause) {
|
|
1478
|
+
ctx.ui.toast.show({
|
|
1479
|
+
message: String(cause),
|
|
1480
|
+
variant: "error"
|
|
1481
|
+
});
|
|
1482
|
+
}
|
|
1483
|
+
}
|
|
1484
|
+
async function choose() {
|
|
1485
|
+
const fresh = await refreshFresh();
|
|
1486
|
+
if (!fresh || !isCurrent(fresh.context))
|
|
1487
|
+
return;
|
|
1488
|
+
if (!fresh.runs.length) {
|
|
1489
|
+
ctx.ui.toast.show({
|
|
1490
|
+
message: "No workflows yet. Use /workflow-run to describe a task.",
|
|
1491
|
+
variant: "info"
|
|
1492
|
+
});
|
|
1493
|
+
return;
|
|
1494
|
+
}
|
|
1495
|
+
const id = await ctx.ui.dialog.select({
|
|
1496
|
+
title: "Dynamic workflows",
|
|
1497
|
+
options: fresh.runs.map((run) => ({
|
|
1498
|
+
title: `${run.name} \xB7 ${run.status}`,
|
|
1499
|
+
description: `${run.counts.completed}/${run.counts.total} recorded steps \xB7 ${run.phase || "starting"} \xB7 ${run.usage.measured ? "" : "\u2265"}${run.usage.tokens} tokens`,
|
|
1500
|
+
value: run.id
|
|
1501
|
+
}))
|
|
1502
|
+
});
|
|
1503
|
+
if (id && fresh.runs.some((run) => run.id === id) && isCurrent(fresh.context))
|
|
1504
|
+
await openRun(id, fresh.context);
|
|
1505
|
+
}
|
|
1506
|
+
function canControl(action) {
|
|
1507
|
+
const run = selected();
|
|
1508
|
+
const status = run?.status;
|
|
1509
|
+
if (!status || run.ownerID !== ownerID())
|
|
1510
|
+
return false;
|
|
1511
|
+
if (action === "pause")
|
|
1512
|
+
return status === "running";
|
|
1513
|
+
if (action === "resume")
|
|
1514
|
+
return status === "paused" || status === "interrupted" || status === "waiting";
|
|
1515
|
+
return status === "running" || status === "pausing" || status === "paused" || status === "interrupted" || status === "waiting";
|
|
1516
|
+
}
|
|
1517
|
+
async function control(action) {
|
|
1518
|
+
const run = selected();
|
|
1519
|
+
const context = synchronizeOwner();
|
|
1520
|
+
if (!run || !context || run.ownerID !== context.ownerID || !canControl(action))
|
|
1521
|
+
return;
|
|
1522
|
+
try {
|
|
1523
|
+
const checkpoint = run.checkpoints.find((item) => item.response === undefined);
|
|
1524
|
+
let answer;
|
|
1525
|
+
if (action === "resume" && checkpoint) {
|
|
1526
|
+
const text = await ctx.ui.dialog.prompt({
|
|
1527
|
+
title: checkpoint.prompt,
|
|
1528
|
+
placeholder: "JSON response, for example true or a quoted string"
|
|
1529
|
+
});
|
|
1530
|
+
if (text === undefined)
|
|
1531
|
+
return;
|
|
1532
|
+
if (!isCurrent(context) || selected()?.id !== run.id)
|
|
1533
|
+
return;
|
|
1534
|
+
answer = Json.parse(JSON.parse(text));
|
|
1535
|
+
}
|
|
1536
|
+
const result = await rpc.control({
|
|
1537
|
+
ownerID: run.ownerID,
|
|
1538
|
+
runID: run.id,
|
|
1539
|
+
action,
|
|
1540
|
+
...checkpoint && action === "resume" ? {
|
|
1541
|
+
checkpointKey: checkpoint.key,
|
|
1542
|
+
response: answer
|
|
1543
|
+
} : {}
|
|
1544
|
+
}, {
|
|
1545
|
+
location: location(),
|
|
1546
|
+
signal: abort.signal
|
|
1547
|
+
});
|
|
1548
|
+
if (!isCurrent(context) || selected()?.id !== run.id)
|
|
1549
|
+
return;
|
|
1550
|
+
setSelected(result);
|
|
1551
|
+
await refreshFresh();
|
|
1552
|
+
} catch (cause) {
|
|
1553
|
+
ctx.ui.toast.show({
|
|
1554
|
+
message: String(cause),
|
|
1555
|
+
variant: "error"
|
|
1556
|
+
});
|
|
1557
|
+
}
|
|
1558
|
+
}
|
|
1559
|
+
async function save() {
|
|
1560
|
+
const run = selected();
|
|
1561
|
+
if (!run)
|
|
1562
|
+
return;
|
|
1563
|
+
const name = await ctx.ui.dialog.prompt({
|
|
1564
|
+
title: "Save workflow",
|
|
1565
|
+
placeholder: run.name
|
|
1566
|
+
});
|
|
1567
|
+
if (!name)
|
|
1568
|
+
return;
|
|
1569
|
+
const scope = await ctx.ui.dialog.select({
|
|
1570
|
+
title: "Save location",
|
|
1571
|
+
options: [{
|
|
1572
|
+
title: "Project",
|
|
1573
|
+
value: "project"
|
|
1574
|
+
}, {
|
|
1575
|
+
title: "User",
|
|
1576
|
+
value: "user"
|
|
1577
|
+
}]
|
|
1578
|
+
});
|
|
1579
|
+
if (!scope)
|
|
1580
|
+
return;
|
|
1581
|
+
try {
|
|
1582
|
+
const output = await rpc.save({
|
|
1583
|
+
ownerID: run.ownerID,
|
|
1584
|
+
runID: run.id,
|
|
1585
|
+
name,
|
|
1586
|
+
scope
|
|
1587
|
+
}, {
|
|
1588
|
+
location: location(),
|
|
1589
|
+
signal: abort.signal
|
|
1590
|
+
});
|
|
1591
|
+
ctx.ui.toast.show({
|
|
1592
|
+
message: `Saved ${ctx.ui.format.path(output.path)}`,
|
|
1593
|
+
variant: "success"
|
|
1594
|
+
});
|
|
1595
|
+
} catch (cause) {
|
|
1596
|
+
ctx.ui.toast.show({
|
|
1597
|
+
message: String(cause),
|
|
1598
|
+
variant: "error"
|
|
1599
|
+
});
|
|
1600
|
+
}
|
|
1601
|
+
}
|
|
1602
|
+
const removePanel = ctx.ui.slot({
|
|
1603
|
+
append: "session.panel",
|
|
1604
|
+
render: (panel) => {
|
|
1605
|
+
const moveStep = (delta) => {
|
|
1606
|
+
const length = selected()?.steps.length ?? 0;
|
|
1607
|
+
if (length)
|
|
1608
|
+
setStepIndex((index) => (index + delta + length) % length);
|
|
1609
|
+
};
|
|
1610
|
+
const openStep = () => {
|
|
1611
|
+
const step = selected()?.steps[stepIndex()];
|
|
1612
|
+
if (!step || step.status === "prepared")
|
|
1613
|
+
return;
|
|
1614
|
+
panel.close();
|
|
1615
|
+
ctx.ui.router.navigate({
|
|
1616
|
+
type: "session",
|
|
1617
|
+
sessionID: step.workerID
|
|
1618
|
+
});
|
|
1619
|
+
};
|
|
1620
|
+
ctx.keymap.layer(() => ({
|
|
1621
|
+
commands: panel.name === "threads.workflows" ? [{
|
|
1622
|
+
id: "workflows.pause",
|
|
1623
|
+
bind: "p",
|
|
1624
|
+
title: "Pause workflow",
|
|
1625
|
+
enabled: () => canControl("pause"),
|
|
1626
|
+
run: () => control("pause")
|
|
1627
|
+
}, {
|
|
1628
|
+
id: "workflows.resume",
|
|
1629
|
+
bind: "r",
|
|
1630
|
+
title: "Resume workflow",
|
|
1631
|
+
enabled: () => canControl("resume"),
|
|
1632
|
+
run: () => control("resume")
|
|
1633
|
+
}, {
|
|
1634
|
+
id: "workflows.stop",
|
|
1635
|
+
bind: "x",
|
|
1636
|
+
title: "Stop workflow",
|
|
1637
|
+
enabled: () => canControl("stop"),
|
|
1638
|
+
run: () => control("stop")
|
|
1639
|
+
}, {
|
|
1640
|
+
id: "workflows.save",
|
|
1641
|
+
bind: "s",
|
|
1642
|
+
title: "Save workflow",
|
|
1643
|
+
run: save
|
|
1644
|
+
}, {
|
|
1645
|
+
id: "workflows.previous-step",
|
|
1646
|
+
bind: "up",
|
|
1647
|
+
title: "Previous workflow step",
|
|
1648
|
+
run: () => moveStep(-1)
|
|
1649
|
+
}, {
|
|
1650
|
+
id: "workflows.next-step",
|
|
1651
|
+
bind: "down",
|
|
1652
|
+
title: "Next workflow step",
|
|
1653
|
+
run: () => moveStep(1)
|
|
1654
|
+
}, {
|
|
1655
|
+
id: "workflows.open-step",
|
|
1656
|
+
bind: "return",
|
|
1657
|
+
title: "Open workflow worker",
|
|
1658
|
+
run: openStep
|
|
1659
|
+
}, {
|
|
1660
|
+
id: "workflows.fullscreen",
|
|
1661
|
+
bind: "f",
|
|
1662
|
+
title: "Toggle workflow fullscreen",
|
|
1663
|
+
run: panel.toggleFullscreen
|
|
1664
|
+
}, {
|
|
1665
|
+
id: "workflows.close",
|
|
1666
|
+
bind: "escape",
|
|
1667
|
+
run: panel.close
|
|
1668
|
+
}] : []
|
|
1669
|
+
}));
|
|
1670
|
+
return _$createComponent2(Show2, {
|
|
1671
|
+
get when() {
|
|
1672
|
+
return panel.name === "threads.workflows";
|
|
1673
|
+
},
|
|
1674
|
+
get children() {
|
|
1675
|
+
var _el$ = _$createElement2("scrollbox"), _el$2 = _$createElement2("text"), _el$3 = _$createElement2("b");
|
|
1676
|
+
_$insertNode2(_el$, _el$2);
|
|
1677
|
+
_$setProp2(_el$, "flexGrow", 1);
|
|
1678
|
+
_$setProp2(_el$, "padding", 1);
|
|
1679
|
+
_$insertNode2(_el$2, _el$3);
|
|
1680
|
+
_$insertNode2(_el$3, _$createTextNode2(`Dynamic workflows`));
|
|
1681
|
+
_$insert2(_el$, _$createComponent2(Show2, {
|
|
1682
|
+
get when() {
|
|
1683
|
+
return selected();
|
|
1684
|
+
},
|
|
1685
|
+
children: (run) => (() => {
|
|
1686
|
+
var _el$6 = _$createElement2("box"), _el$7 = _$createElement2("text"), _el$8 = _$createElement2("b"), _el$9 = _$createElement2("text"), _el$0 = _$createElement2("text"), _el$1 = _$createElement2("text"), _el$10 = _$createElement2("text");
|
|
1687
|
+
_$insertNode2(_el$6, _el$7);
|
|
1688
|
+
_$insertNode2(_el$6, _el$9);
|
|
1689
|
+
_$insertNode2(_el$6, _el$0);
|
|
1690
|
+
_$insertNode2(_el$6, _el$1);
|
|
1691
|
+
_$insertNode2(_el$6, _el$10);
|
|
1692
|
+
_$setProp2(_el$6, "flexDirection", "column");
|
|
1693
|
+
_$setProp2(_el$6, "gap", 1);
|
|
1694
|
+
_$insertNode2(_el$7, _el$8);
|
|
1695
|
+
_$insert2(_el$8, () => run().name);
|
|
1696
|
+
_$insert2(_el$7, () => ` \xB7 ${run().status}`, null);
|
|
1697
|
+
_$insert2(_el$9, () => run().description);
|
|
1698
|
+
_$insert2(_el$0, () => `Run: ${run().id}`);
|
|
1699
|
+
_$insert2(_el$1, () => `Phase: ${run().phase || "starting"}`);
|
|
1700
|
+
_$insert2(_el$10, () => [...canControl("pause") ? ["p pause"] : [], ...canControl("resume") ? ["r resume"] : [], ...canControl("stop") ? ["x stop"] : [], "s save", "f fullscreen", "Esc close"].join(" \xB7 "));
|
|
1701
|
+
_$insert2(_el$6, _$createComponent2(Show2, {
|
|
1702
|
+
get when() {
|
|
1703
|
+
return run().steps.length > 0;
|
|
1704
|
+
},
|
|
1705
|
+
get children() {
|
|
1706
|
+
var _el$11 = _$createElement2("text");
|
|
1707
|
+
_$insertNode2(_el$11, _$createTextNode2(`\u2191/\u2193 select step \xB7 Enter open worker`));
|
|
1708
|
+
return _el$11;
|
|
1709
|
+
}
|
|
1710
|
+
}), null);
|
|
1711
|
+
_$insert2(_el$6, _$createComponent2(Show2, {
|
|
1712
|
+
get when() {
|
|
1713
|
+
return run().error;
|
|
1714
|
+
},
|
|
1715
|
+
get children() {
|
|
1716
|
+
var _el$13 = _$createElement2("text");
|
|
1717
|
+
_$insert2(_el$13, () => `Error: ${run().error}`);
|
|
1718
|
+
return _el$13;
|
|
1719
|
+
}
|
|
1720
|
+
}), null);
|
|
1721
|
+
_$insert2(_el$6, _$createComponent2(For2, {
|
|
1722
|
+
get each() {
|
|
1723
|
+
return run().steps;
|
|
1724
|
+
},
|
|
1725
|
+
children: (step, index) => (() => {
|
|
1726
|
+
var _el$15 = _$createElement2("box"), _el$16 = _$createElement2("text"), _el$17 = _$createElement2("b"), _el$18 = _$createElement2("text"), _el$19 = _$createElement2("text"), _el$20 = _$createElement2("text");
|
|
1727
|
+
_$insertNode2(_el$15, _el$16);
|
|
1728
|
+
_$insertNode2(_el$15, _el$18);
|
|
1729
|
+
_$insertNode2(_el$15, _el$19);
|
|
1730
|
+
_$insertNode2(_el$15, _el$20);
|
|
1731
|
+
_$setProp2(_el$15, "flexDirection", "column");
|
|
1732
|
+
_$setProp2(_el$15, "border", true);
|
|
1733
|
+
_$setProp2(_el$15, "padding", 1);
|
|
1734
|
+
_$setProp2(_el$15, "onMouseUp", () => {
|
|
1735
|
+
setStepIndex(index());
|
|
1736
|
+
openStep();
|
|
1737
|
+
});
|
|
1738
|
+
_$insertNode2(_el$16, _el$17);
|
|
1739
|
+
_$insert2(_el$16, () => index() === stepIndex() ? "\u203A " : " ", _el$17);
|
|
1740
|
+
_$insert2(_el$17, () => step.input.label ?? step.key);
|
|
1741
|
+
_$insert2(_el$16, () => ` \xB7 ${step.status}`, null);
|
|
1742
|
+
_$insert2(_el$18, () => `${step.phase} \xB7 ${step.input.agent} \xB7 ${step.model.providerID}/${step.model.id}`);
|
|
1743
|
+
_$insert2(_el$19, () => ctx.ui.format.path(step.directory));
|
|
1744
|
+
_$insert2(_el$15, _$createComponent2(Show2, {
|
|
1745
|
+
get when() {
|
|
1746
|
+
return step.status === "completed" && step;
|
|
1747
|
+
},
|
|
1748
|
+
children: (done) => [(() => {
|
|
1749
|
+
var _el$21 = _$createElement2("text");
|
|
1750
|
+
_$insert2(_el$21, () => `${done().report.verdict}: ${done().report.summary}`);
|
|
1751
|
+
return _el$21;
|
|
1752
|
+
})(), (() => {
|
|
1753
|
+
var _el$22 = _$createElement2("text");
|
|
1754
|
+
_$insert2(_el$22, () => `${done().usage.measured ? "" : "unmeasured \xB7 "}${done().usage.tokens} tokens \xB7 $${done().usage.cost.toFixed(4)}`);
|
|
1755
|
+
return _el$22;
|
|
1756
|
+
})(), _$createComponent2(For2, {
|
|
1757
|
+
get each() {
|
|
1758
|
+
return done().report.evidence;
|
|
1759
|
+
},
|
|
1760
|
+
children: (evidence) => (() => {
|
|
1761
|
+
var _el$23 = _$createElement2("text");
|
|
1762
|
+
_$insert2(_el$23, evidence);
|
|
1763
|
+
return _el$23;
|
|
1764
|
+
})()
|
|
1765
|
+
})]
|
|
1766
|
+
}), _el$20);
|
|
1767
|
+
_$insert2(_el$15, _$createComponent2(Show2, {
|
|
1768
|
+
get when() {
|
|
1769
|
+
return step.status === "failed" && step;
|
|
1770
|
+
},
|
|
1771
|
+
children: (failed) => (() => {
|
|
1772
|
+
var _el$24 = _$createElement2("text");
|
|
1773
|
+
_$insert2(_el$24, () => failed().error);
|
|
1774
|
+
return _el$24;
|
|
1775
|
+
})()
|
|
1776
|
+
}), _el$20);
|
|
1777
|
+
_$insert2(_el$20, () => step.status === "prepared" ? "Waiting for worker admission" : "Click or select and press Enter to open worker");
|
|
1778
|
+
return _el$15;
|
|
1779
|
+
})()
|
|
1780
|
+
}), null);
|
|
1781
|
+
_$insert2(_el$6, _$createComponent2(For2, {
|
|
1782
|
+
get each() {
|
|
1783
|
+
return run().checkpoints;
|
|
1784
|
+
},
|
|
1785
|
+
children: (checkpoint) => (() => {
|
|
1786
|
+
var _el$25 = _$createElement2("text");
|
|
1787
|
+
_$insert2(_el$25, () => `${checkpoint.response === undefined ? "Waiting" : "Answered"}: ${checkpoint.prompt}`);
|
|
1788
|
+
return _el$25;
|
|
1789
|
+
})()
|
|
1790
|
+
}), null);
|
|
1791
|
+
_$insert2(_el$6, _$createComponent2(For2, {
|
|
1792
|
+
get each() {
|
|
1793
|
+
return run().logs.slice(-30);
|
|
1794
|
+
},
|
|
1795
|
+
children: (entry) => (() => {
|
|
1796
|
+
var _el$26 = _$createElement2("text");
|
|
1797
|
+
_$insert2(_el$26, () => entry.text);
|
|
1798
|
+
return _el$26;
|
|
1799
|
+
})()
|
|
1800
|
+
}), null);
|
|
1801
|
+
_$insert2(_el$6, _$createComponent2(Show2, {
|
|
1802
|
+
get when() {
|
|
1803
|
+
return run().result !== undefined;
|
|
1804
|
+
},
|
|
1805
|
+
get children() {
|
|
1806
|
+
var _el$14 = _$createElement2("text");
|
|
1807
|
+
_$insert2(_el$14, () => `Result:
|
|
1808
|
+
${JSON.stringify(run().result, null, 2)}`);
|
|
1809
|
+
return _el$14;
|
|
1810
|
+
}
|
|
1811
|
+
}), null);
|
|
1812
|
+
return _el$6;
|
|
1813
|
+
})()
|
|
1814
|
+
}), null);
|
|
1815
|
+
_$insert2(_el$, _$createComponent2(Show2, {
|
|
1816
|
+
get when() {
|
|
1817
|
+
return error();
|
|
1818
|
+
},
|
|
1819
|
+
get children() {
|
|
1820
|
+
var _el$5 = _$createElement2("text");
|
|
1821
|
+
_$insert2(_el$5, error);
|
|
1822
|
+
return _el$5;
|
|
1823
|
+
}
|
|
1824
|
+
}), null);
|
|
1825
|
+
return _el$;
|
|
1826
|
+
}
|
|
1827
|
+
});
|
|
1828
|
+
}
|
|
1829
|
+
});
|
|
1830
|
+
const removeFooter = ctx.ui.slot({
|
|
1831
|
+
append: "session.composer.top",
|
|
1832
|
+
render: () => {
|
|
1833
|
+
createEffect2(() => {
|
|
1834
|
+
ownerID();
|
|
1835
|
+
refreshInBackground();
|
|
1836
|
+
});
|
|
1837
|
+
return _$createComponent2(Show2, {
|
|
1838
|
+
get when() {
|
|
1839
|
+
return runs().find((run) => ["running", "pausing", "waiting"].includes(run.status));
|
|
1840
|
+
},
|
|
1841
|
+
children: (run) => (() => {
|
|
1842
|
+
var _el$27 = _$createElement2("box"), _el$28 = _$createElement2("text");
|
|
1843
|
+
_$insertNode2(_el$27, _el$28);
|
|
1844
|
+
_$setProp2(_el$27, "onMouseUp", () => void openRun(run().id));
|
|
1845
|
+
_$insert2(_el$28, () => `Workflow ${run().name}: ${run().status} \xB7 ${run().counts.completed}/${run().counts.total} recorded steps \xB7 /workflows`);
|
|
1846
|
+
return _el$27;
|
|
1847
|
+
})()
|
|
1848
|
+
});
|
|
1849
|
+
}
|
|
1850
|
+
});
|
|
1851
|
+
const removeCommands = ctx.ui.slot({
|
|
1852
|
+
append: "app",
|
|
1853
|
+
render: () => {
|
|
1854
|
+
ctx.keymap.layer(() => ({
|
|
1855
|
+
mode: "global",
|
|
1856
|
+
commands: [{
|
|
1857
|
+
id: "workflows.open",
|
|
1858
|
+
title: "Open dynamic workflows",
|
|
1859
|
+
palette: true,
|
|
1860
|
+
slash: {
|
|
1861
|
+
name: "workflows"
|
|
1862
|
+
},
|
|
1863
|
+
run: choose
|
|
1864
|
+
}]
|
|
1865
|
+
}));
|
|
1866
|
+
return null;
|
|
1867
|
+
}
|
|
1868
|
+
});
|
|
1869
|
+
const stopEvents = ctx.data.listen(({
|
|
1870
|
+
details
|
|
1871
|
+
}) => {
|
|
1872
|
+
if (details.type.startsWith("session.") || details.type.startsWith("rpc.workflows."))
|
|
1873
|
+
refreshInBackground();
|
|
1874
|
+
});
|
|
1875
|
+
const timer = setInterval(refreshInBackground, 3000);
|
|
1876
|
+
return () => {
|
|
1877
|
+
abort.abort();
|
|
1878
|
+
clearInterval(timer);
|
|
1879
|
+
stopEvents();
|
|
1880
|
+
removePanel();
|
|
1881
|
+
removeFooter();
|
|
1882
|
+
removeCommands();
|
|
1883
|
+
};
|
|
1884
|
+
}
|
|
1885
|
+
|
|
1886
|
+
// tui.ts
|
|
1887
|
+
import {
|
|
1888
|
+
BoxRenderable,
|
|
1889
|
+
ScrollBoxRenderable,
|
|
1890
|
+
TextRenderable,
|
|
1891
|
+
TextAttributes,
|
|
1892
|
+
RGBA
|
|
1893
|
+
} from "@opentui/core";
|
|
1894
|
+
var CoordinatorRef = z5.object({ coordinatorID: z5.string() });
|
|
1895
|
+
var tui_default = Plugin.define({
|
|
1896
|
+
id: "op-threads",
|
|
1897
|
+
setup(ctx) {
|
|
1898
|
+
const stopWorkflows = workflowUI(ctx);
|
|
1899
|
+
const rpc = ctx.client.rpc(ThreadsRpc);
|
|
1900
|
+
const sidebar = activity(ctx, { BoxRenderable, ScrollBoxRenderable, TextRenderable, TextAttributes, RGBA }, { createEffect: createEffect3, createSignal: createSignal3 }, getComponentCatalogue().spinner);
|
|
1901
|
+
const [cleaned, saveCleaned] = ctx.storage.store("role-title-cleanup", {
|
|
1902
|
+
initial: { ids: [] }
|
|
1903
|
+
});
|
|
1904
|
+
const initial = { workerIDs: [] };
|
|
1905
|
+
const [seen, updateSeen] = ctx.storage.memory("seen-workers", { initial });
|
|
1906
|
+
const closing = new Set;
|
|
1907
|
+
const abort = new AbortController;
|
|
1908
|
+
let stopped = false;
|
|
1909
|
+
let running = false;
|
|
1910
|
+
let reopenPending = false;
|
|
1911
|
+
let lastError;
|
|
1912
|
+
let movingFrom;
|
|
1913
|
+
function groupTabs() {
|
|
1914
|
+
if (sidebar.mounted())
|
|
1915
|
+
return;
|
|
1916
|
+
const tabs = ctx.ui.tabs.list().map((tab) => {
|
|
1917
|
+
const projectID = ctx.data.session.get(tab.sessionID)?.projectID;
|
|
1918
|
+
return {
|
|
1919
|
+
sessionID: tab.sessionID,
|
|
1920
|
+
priority: tab.busy || tab.attention,
|
|
1921
|
+
projectID: typeof projectID === "string" && projectID.length > 0 ? projectID : undefined
|
|
1922
|
+
};
|
|
1923
|
+
});
|
|
1924
|
+
const groups = new Map;
|
|
1925
|
+
for (const tab of tabs) {
|
|
1926
|
+
const key = tab.projectID === undefined ? `session:${tab.sessionID}` : `project:${tab.projectID}`;
|
|
1927
|
+
const group = groups.get(key);
|
|
1928
|
+
if (group)
|
|
1929
|
+
group.push(tab);
|
|
1930
|
+
else
|
|
1931
|
+
groups.set(key, [tab]);
|
|
1932
|
+
}
|
|
1933
|
+
const ordered = [...groups.values()].flatMap((group) => group.sort((left, right) => Number(right.priority) - Number(left.priority)).map((tab) => tab.sessionID));
|
|
1934
|
+
const current = tabs.map((tab) => tab.sessionID);
|
|
1935
|
+
const stamp = JSON.stringify(current);
|
|
1936
|
+
if (movingFrom === stamp)
|
|
1937
|
+
return;
|
|
1938
|
+
movingFrom = undefined;
|
|
1939
|
+
for (const [index, sessionID] of ordered.entries()) {
|
|
1940
|
+
if (current[index] === sessionID)
|
|
1941
|
+
continue;
|
|
1942
|
+
if (ctx.ui.tabs.move(sessionID, index))
|
|
1943
|
+
movingFrom = stamp;
|
|
1944
|
+
break;
|
|
1945
|
+
}
|
|
1946
|
+
}
|
|
1947
|
+
async function reconcile(reopen = false) {
|
|
1948
|
+
if (stopped || !ctx.ui.tabs.enabled())
|
|
1949
|
+
return;
|
|
1950
|
+
if (running) {
|
|
1951
|
+
reopenPending ||= reopen;
|
|
1952
|
+
return;
|
|
1953
|
+
}
|
|
1954
|
+
running = true;
|
|
1955
|
+
try {
|
|
1956
|
+
groupTabs();
|
|
1957
|
+
const route = ctx.ui.router.current();
|
|
1958
|
+
const coordinatorIDs = [
|
|
1959
|
+
...new Set([
|
|
1960
|
+
...ctx.ui.tabs.list().flatMap((tab) => {
|
|
1961
|
+
const link = CoordinatorRef.safeParse(ctx.data.session.get(tab.sessionID)?.metadata?.opThreads);
|
|
1962
|
+
return link.success ? [tab.sessionID, link.data.coordinatorID] : [tab.sessionID];
|
|
1963
|
+
}),
|
|
1964
|
+
...route.type === "session" ? [route.sessionID] : []
|
|
1965
|
+
])
|
|
1966
|
+
].slice(0, 100);
|
|
1967
|
+
if (!coordinatorIDs.length)
|
|
1968
|
+
return;
|
|
1969
|
+
const { workers } = await (reopen ? rpc.restore : rpc.snapshot)({ coordinatorIDs }, {
|
|
1970
|
+
location: ctx.location ?? ctx.data.location.default(),
|
|
1971
|
+
signal: abort.signal
|
|
1972
|
+
});
|
|
1973
|
+
if (reopen)
|
|
1974
|
+
await sidebar.restore(workers.map((worker) => worker.workerID));
|
|
1975
|
+
sidebar.updateWorkers(workers);
|
|
1976
|
+
for (const worker of workers) {
|
|
1977
|
+
if (stopped)
|
|
1978
|
+
return;
|
|
1979
|
+
if (!reopen && sidebar.isDismissed(worker.workerID))
|
|
1980
|
+
continue;
|
|
1981
|
+
const tab = ctx.ui.tabs.list().find((tab2) => tab2.sessionID === worker.workerID);
|
|
1982
|
+
if (closing.has(worker.workerID) && tab)
|
|
1983
|
+
continue;
|
|
1984
|
+
const closed = closing.delete(worker.workerID);
|
|
1985
|
+
if (worker.hidden && !tab?.active && !tab?.busy && !tab?.attention && ctx.data.session.status(worker.workerID) !== "running") {
|
|
1986
|
+
if (tab) {
|
|
1987
|
+
if (!ctx.ui.tabs.close(worker.workerID))
|
|
1988
|
+
continue;
|
|
1989
|
+
closing.add(worker.workerID);
|
|
1990
|
+
}
|
|
1991
|
+
if (seen.workerIDs.includes(worker.workerID)) {
|
|
1992
|
+
updateSeen((draft) => {
|
|
1993
|
+
draft.workerIDs = draft.workerIDs.filter((id) => id !== worker.workerID);
|
|
1994
|
+
});
|
|
1995
|
+
}
|
|
1996
|
+
continue;
|
|
1997
|
+
}
|
|
1998
|
+
if (!reopen && !closed && seen.workerIDs.includes(worker.workerID))
|
|
1999
|
+
continue;
|
|
2000
|
+
await ctx.data.session.sync(worker.workerID);
|
|
2001
|
+
if (!stopped && ctx.ui.tabs.open(worker.workerID) && !seen.workerIDs.includes(worker.workerID)) {
|
|
2002
|
+
updateSeen((draft) => {
|
|
2003
|
+
draft.workerIDs.push(worker.workerID);
|
|
2004
|
+
});
|
|
2005
|
+
}
|
|
2006
|
+
}
|
|
2007
|
+
if (!stopped && ctx.ui.tabs.enabled()) {
|
|
2008
|
+
const roles = new Map;
|
|
2009
|
+
for (const worker of workers) {
|
|
2010
|
+
roles.set(worker.coordinatorID, "Main");
|
|
2011
|
+
roles.set(worker.workerID, "Worker");
|
|
2012
|
+
}
|
|
2013
|
+
for (const tab of ctx.ui.tabs.list()) {
|
|
2014
|
+
if (stopped)
|
|
2015
|
+
return;
|
|
2016
|
+
const role = roles.get(tab.sessionID);
|
|
2017
|
+
const session = ctx.data.session.get(tab.sessionID);
|
|
2018
|
+
if (!role || !session || cleaned.ids.includes(tab.sessionID))
|
|
2019
|
+
continue;
|
|
2020
|
+
const fresh = await ctx.client.session.get({ sessionID: tab.sessionID }, { signal: abort.signal });
|
|
2021
|
+
if (stopped)
|
|
2022
|
+
return;
|
|
2023
|
+
const title = cleanRoleTitle(fresh.title ?? "", true);
|
|
2024
|
+
if (title && title !== fresh.title)
|
|
2025
|
+
await ctx.client.session.update({ sessionID: tab.sessionID, title }, { signal: abort.signal });
|
|
2026
|
+
if (stopped)
|
|
2027
|
+
return;
|
|
2028
|
+
await saveCleaned((draft) => {
|
|
2029
|
+
if (!draft.ids.includes(tab.sessionID))
|
|
2030
|
+
draft.ids.push(tab.sessionID);
|
|
2031
|
+
});
|
|
2032
|
+
}
|
|
2033
|
+
groupTabs();
|
|
2034
|
+
}
|
|
2035
|
+
lastError = undefined;
|
|
2036
|
+
} catch (error) {
|
|
2037
|
+
const message = `Managed worker tabs: ${String(error)}`;
|
|
2038
|
+
if (!stopped && message !== lastError)
|
|
2039
|
+
ctx.ui.toast.show({ message, variant: "error" });
|
|
2040
|
+
lastError = message;
|
|
2041
|
+
} finally {
|
|
2042
|
+
running = false;
|
|
2043
|
+
if (reopenPending) {
|
|
2044
|
+
reopenPending = false;
|
|
2045
|
+
reconcile(true);
|
|
2046
|
+
}
|
|
2047
|
+
}
|
|
2048
|
+
}
|
|
2049
|
+
const refresh = () => {
|
|
2050
|
+
reconcile();
|
|
2051
|
+
};
|
|
2052
|
+
const stopEvents = ctx.data.listen(({ details }) => {
|
|
2053
|
+
if (details.type.startsWith("session."))
|
|
2054
|
+
refresh();
|
|
2055
|
+
});
|
|
2056
|
+
const timer = setInterval(refresh, 3000);
|
|
2057
|
+
const removeSlot = ctx.ui.slot({
|
|
2058
|
+
append: "app",
|
|
2059
|
+
render: () => {
|
|
2060
|
+
createEffect3(() => {
|
|
2061
|
+
ctx.ui.tabs.list();
|
|
2062
|
+
refresh();
|
|
2063
|
+
});
|
|
2064
|
+
ctx.keymap.layer(() => ({
|
|
2065
|
+
mode: "global",
|
|
2066
|
+
commands: [
|
|
2067
|
+
{
|
|
2068
|
+
id: "threads.reopen",
|
|
2069
|
+
title: "Reopen managed worker tabs",
|
|
2070
|
+
palette: true,
|
|
2071
|
+
slash: { name: "threads" },
|
|
2072
|
+
run: () => reconcile(true)
|
|
2073
|
+
}
|
|
2074
|
+
]
|
|
2075
|
+
}));
|
|
2076
|
+
return null;
|
|
2077
|
+
}
|
|
2078
|
+
});
|
|
2079
|
+
refresh();
|
|
2080
|
+
return () => {
|
|
2081
|
+
stopWorkflows();
|
|
2082
|
+
stopped = true;
|
|
2083
|
+
abort.abort();
|
|
2084
|
+
sidebar.dispose();
|
|
2085
|
+
clearInterval(timer);
|
|
2086
|
+
stopEvents();
|
|
2087
|
+
removeSlot();
|
|
2088
|
+
};
|
|
2089
|
+
}
|
|
2090
|
+
});
|
|
2091
|
+
export {
|
|
2092
|
+
tui_default as default
|
|
2093
|
+
};
|