@op1/threads 0.2.0 → 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 +9 -3
- package/docs/workflows-verification.md +28 -0
- package/package.json +6 -3
- package/tsconfig.json +15 -0
- package/tui.js +2093 -0
- package/tui.ts +0 -240
package/tui.ts
DELETED
|
@@ -1,240 +0,0 @@
|
|
|
1
|
-
import { Plugin } from "@opencode/plugin/tui";
|
|
2
|
-
import { getComponentCatalogue } from "@opentui/solid/components";
|
|
3
|
-
import { createEffect, createSignal } from "solid-js";
|
|
4
|
-
import { z } from "zod";
|
|
5
|
-
import { ThreadsRpc } from "./src/rpc";
|
|
6
|
-
import { activity } from "./src/activity";
|
|
7
|
-
import { workflowUI } from "./src/workflow-ui";
|
|
8
|
-
import { cleanRoleTitle } from "./src/activity-model";
|
|
9
|
-
import {
|
|
10
|
-
BoxRenderable,
|
|
11
|
-
ScrollBoxRenderable,
|
|
12
|
-
TextRenderable,
|
|
13
|
-
TextAttributes,
|
|
14
|
-
RGBA,
|
|
15
|
-
} from "@opentui/core";
|
|
16
|
-
|
|
17
|
-
const CoordinatorRef = z.object({ coordinatorID: z.string() });
|
|
18
|
-
|
|
19
|
-
export default Plugin.define({
|
|
20
|
-
id: "op-threads",
|
|
21
|
-
setup(ctx) {
|
|
22
|
-
const stopWorkflows = workflowUI(ctx);
|
|
23
|
-
const rpc = ctx.client.rpc(ThreadsRpc);
|
|
24
|
-
const sidebar = activity(
|
|
25
|
-
ctx,
|
|
26
|
-
{ BoxRenderable, ScrollBoxRenderable, TextRenderable, TextAttributes, RGBA },
|
|
27
|
-
{ createEffect, createSignal },
|
|
28
|
-
getComponentCatalogue().spinner,
|
|
29
|
-
);
|
|
30
|
-
const [cleaned, saveCleaned] = ctx.storage.store("role-title-cleanup", {
|
|
31
|
-
initial: { ids: [] as string[] },
|
|
32
|
-
});
|
|
33
|
-
const initial: { workerIDs: string[] } = { workerIDs: [] };
|
|
34
|
-
const [seen, updateSeen] = ctx.storage.memory("seen-workers", { initial });
|
|
35
|
-
const closing = new Set<string>();
|
|
36
|
-
const abort = new AbortController();
|
|
37
|
-
let stopped = false;
|
|
38
|
-
let running = false;
|
|
39
|
-
let reopenPending = false;
|
|
40
|
-
let lastError: string | undefined;
|
|
41
|
-
let movingFrom: string | undefined;
|
|
42
|
-
function groupTabs() {
|
|
43
|
-
if (sidebar.mounted()) return;
|
|
44
|
-
const tabs = ctx.ui.tabs.list().map((tab) => {
|
|
45
|
-
const projectID = ctx.data.session.get(tab.sessionID)?.projectID;
|
|
46
|
-
return {
|
|
47
|
-
sessionID: tab.sessionID,
|
|
48
|
-
priority: tab.busy || tab.attention,
|
|
49
|
-
projectID:
|
|
50
|
-
typeof projectID === "string" && projectID.length > 0
|
|
51
|
-
? projectID
|
|
52
|
-
: undefined,
|
|
53
|
-
};
|
|
54
|
-
});
|
|
55
|
-
const groups = new Map<string, typeof tabs>();
|
|
56
|
-
for (const tab of tabs) {
|
|
57
|
-
const key =
|
|
58
|
-
tab.projectID === undefined
|
|
59
|
-
? `session:${tab.sessionID}`
|
|
60
|
-
: `project:${tab.projectID}`;
|
|
61
|
-
const group = groups.get(key);
|
|
62
|
-
if (group) group.push(tab);
|
|
63
|
-
else groups.set(key, [tab]);
|
|
64
|
-
}
|
|
65
|
-
const ordered = [...groups.values()].flatMap((group) =>
|
|
66
|
-
group
|
|
67
|
-
.sort((left, right) => Number(right.priority) - Number(left.priority))
|
|
68
|
-
.map((tab) => tab.sessionID),
|
|
69
|
-
);
|
|
70
|
-
const current = tabs.map((tab) => tab.sessionID);
|
|
71
|
-
const stamp = JSON.stringify(current);
|
|
72
|
-
if (movingFrom === stamp) return;
|
|
73
|
-
movingFrom = undefined;
|
|
74
|
-
for (const [index, sessionID] of ordered.entries()) {
|
|
75
|
-
if (current[index] === sessionID) continue;
|
|
76
|
-
if (ctx.ui.tabs.move(sessionID, index)) movingFrom = stamp;
|
|
77
|
-
break;
|
|
78
|
-
}
|
|
79
|
-
}
|
|
80
|
-
async function reconcile(reopen = false) {
|
|
81
|
-
if (stopped || !ctx.ui.tabs.enabled()) return;
|
|
82
|
-
if (running) {
|
|
83
|
-
reopenPending ||= reopen;
|
|
84
|
-
return;
|
|
85
|
-
}
|
|
86
|
-
running = true;
|
|
87
|
-
try {
|
|
88
|
-
groupTabs();
|
|
89
|
-
const route = ctx.ui.router.current();
|
|
90
|
-
const coordinatorIDs = [
|
|
91
|
-
...new Set([
|
|
92
|
-
...ctx.ui.tabs.list().flatMap((tab) => {
|
|
93
|
-
const link = CoordinatorRef.safeParse(
|
|
94
|
-
ctx.data.session.get(tab.sessionID)?.metadata?.opThreads,
|
|
95
|
-
);
|
|
96
|
-
return link.success
|
|
97
|
-
? [tab.sessionID, link.data.coordinatorID]
|
|
98
|
-
: [tab.sessionID];
|
|
99
|
-
}),
|
|
100
|
-
...(route.type === "session" ? [route.sessionID] : []),
|
|
101
|
-
]),
|
|
102
|
-
].slice(0, 100);
|
|
103
|
-
if (!coordinatorIDs.length) return;
|
|
104
|
-
const { workers } = await (reopen ? rpc.restore : rpc.snapshot)(
|
|
105
|
-
{ coordinatorIDs },
|
|
106
|
-
{
|
|
107
|
-
location: ctx.location ?? ctx.data.location.default(),
|
|
108
|
-
signal: abort.signal,
|
|
109
|
-
},
|
|
110
|
-
);
|
|
111
|
-
if (reopen)
|
|
112
|
-
await sidebar.restore(workers.map((worker) => worker.workerID));
|
|
113
|
-
sidebar.updateWorkers(workers);
|
|
114
|
-
for (const worker of workers) {
|
|
115
|
-
if (stopped) return;
|
|
116
|
-
if (!reopen && sidebar.isDismissed(worker.workerID)) continue;
|
|
117
|
-
const tab = ctx.ui.tabs
|
|
118
|
-
.list()
|
|
119
|
-
.find((tab) => tab.sessionID === worker.workerID);
|
|
120
|
-
if (closing.has(worker.workerID) && tab) continue;
|
|
121
|
-
const closed = closing.delete(worker.workerID);
|
|
122
|
-
if (
|
|
123
|
-
worker.hidden &&
|
|
124
|
-
!tab?.active &&
|
|
125
|
-
!tab?.busy &&
|
|
126
|
-
!tab?.attention &&
|
|
127
|
-
ctx.data.session.status(worker.workerID) !== "running"
|
|
128
|
-
) {
|
|
129
|
-
if (tab) {
|
|
130
|
-
if (!ctx.ui.tabs.close(worker.workerID)) continue;
|
|
131
|
-
closing.add(worker.workerID);
|
|
132
|
-
}
|
|
133
|
-
if (seen.workerIDs.includes(worker.workerID)) {
|
|
134
|
-
updateSeen((draft) => {
|
|
135
|
-
draft.workerIDs = draft.workerIDs.filter(
|
|
136
|
-
(id) => id !== worker.workerID,
|
|
137
|
-
);
|
|
138
|
-
});
|
|
139
|
-
}
|
|
140
|
-
continue;
|
|
141
|
-
}
|
|
142
|
-
if (!reopen && !closed && seen.workerIDs.includes(worker.workerID))
|
|
143
|
-
continue;
|
|
144
|
-
await ctx.data.session.sync(worker.workerID);
|
|
145
|
-
if (
|
|
146
|
-
!stopped &&
|
|
147
|
-
ctx.ui.tabs.open(worker.workerID) &&
|
|
148
|
-
!seen.workerIDs.includes(worker.workerID)
|
|
149
|
-
) {
|
|
150
|
-
updateSeen((draft) => {
|
|
151
|
-
draft.workerIDs.push(worker.workerID);
|
|
152
|
-
});
|
|
153
|
-
}
|
|
154
|
-
}
|
|
155
|
-
if (!stopped && ctx.ui.tabs.enabled()) {
|
|
156
|
-
const roles = new Map<string, "Main" | "Worker">();
|
|
157
|
-
for (const worker of workers) {
|
|
158
|
-
roles.set(worker.coordinatorID, "Main");
|
|
159
|
-
roles.set(worker.workerID, "Worker");
|
|
160
|
-
}
|
|
161
|
-
for (const tab of ctx.ui.tabs.list()) {
|
|
162
|
-
if (stopped) return;
|
|
163
|
-
const role = roles.get(tab.sessionID);
|
|
164
|
-
const session = ctx.data.session.get(tab.sessionID);
|
|
165
|
-
if (!role || !session || cleaned.ids.includes(tab.sessionID))
|
|
166
|
-
continue;
|
|
167
|
-
const fresh = await ctx.client.session.get(
|
|
168
|
-
{ sessionID: tab.sessionID },
|
|
169
|
-
{ signal: abort.signal },
|
|
170
|
-
);
|
|
171
|
-
if (stopped) return;
|
|
172
|
-
const title = cleanRoleTitle(fresh.title ?? "", true);
|
|
173
|
-
if (title && title !== fresh.title)
|
|
174
|
-
await ctx.client.session.update(
|
|
175
|
-
{ sessionID: tab.sessionID, title },
|
|
176
|
-
{ signal: abort.signal },
|
|
177
|
-
);
|
|
178
|
-
if (stopped) return;
|
|
179
|
-
await saveCleaned((draft) => {
|
|
180
|
-
if (!draft.ids.includes(tab.sessionID))
|
|
181
|
-
draft.ids.push(tab.sessionID);
|
|
182
|
-
});
|
|
183
|
-
}
|
|
184
|
-
groupTabs();
|
|
185
|
-
}
|
|
186
|
-
lastError = undefined;
|
|
187
|
-
} catch (error) {
|
|
188
|
-
const message = `Managed worker tabs: ${String(error)}`;
|
|
189
|
-
if (!stopped && message !== lastError)
|
|
190
|
-
ctx.ui.toast.show({ message, variant: "error" });
|
|
191
|
-
lastError = message;
|
|
192
|
-
} finally {
|
|
193
|
-
running = false;
|
|
194
|
-
if (reopenPending) {
|
|
195
|
-
reopenPending = false;
|
|
196
|
-
void reconcile(true);
|
|
197
|
-
}
|
|
198
|
-
}
|
|
199
|
-
}
|
|
200
|
-
const refresh = () => {
|
|
201
|
-
void reconcile();
|
|
202
|
-
};
|
|
203
|
-
const stopEvents = ctx.data.listen(({ details }) => {
|
|
204
|
-
if (details.type.startsWith("session.")) refresh();
|
|
205
|
-
});
|
|
206
|
-
const timer = setInterval(refresh, 3000);
|
|
207
|
-
const removeSlot = ctx.ui.slot({
|
|
208
|
-
append: "app",
|
|
209
|
-
render: () => {
|
|
210
|
-
createEffect(() => {
|
|
211
|
-
ctx.ui.tabs.list();
|
|
212
|
-
refresh();
|
|
213
|
-
});
|
|
214
|
-
ctx.keymap.layer(() => ({
|
|
215
|
-
mode: "global",
|
|
216
|
-
commands: [
|
|
217
|
-
{
|
|
218
|
-
id: "threads.reopen",
|
|
219
|
-
title: "Reopen managed worker tabs",
|
|
220
|
-
palette: true,
|
|
221
|
-
slash: { name: "threads" },
|
|
222
|
-
run: () => reconcile(true),
|
|
223
|
-
},
|
|
224
|
-
],
|
|
225
|
-
}));
|
|
226
|
-
return null;
|
|
227
|
-
},
|
|
228
|
-
});
|
|
229
|
-
refresh();
|
|
230
|
-
return () => {
|
|
231
|
-
stopWorkflows();
|
|
232
|
-
stopped = true;
|
|
233
|
-
abort.abort();
|
|
234
|
-
sidebar.dispose();
|
|
235
|
-
clearInterval(timer);
|
|
236
|
-
stopEvents();
|
|
237
|
-
removeSlot();
|
|
238
|
-
};
|
|
239
|
-
},
|
|
240
|
-
});
|