@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/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
- });