@op1/threads 0.1.3 → 0.1.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +7 -1
- package/index.ts +1 -1
- package/package.json +4 -1
- package/skills/managed-sessions/SKILL.md +14 -4
- package/src/threads.ts +1 -1
- package/tui.ts +49 -6
package/README.md
CHANGED
|
@@ -35,7 +35,11 @@ threads_spawn({
|
|
|
35
35
|
})
|
|
36
36
|
```
|
|
37
37
|
|
|
38
|
-
The worker opens in a native tab without taking focus. Select that tab to read its conversation. Use `threads_list` to inspect reports and `/threads` to reopen closed worker tabs.
|
|
38
|
+
The worker opens in a native tab without taking focus. Select that tab to read its conversation. Use `threads_list` to inspect reports and `/threads` to reopen closed worker tabs.
|
|
39
|
+
|
|
40
|
+
Both the parent and managed workers can use native `subagent` for bounded tasks and role-specific reviews. The parent can mix direct subagent calls with managed threads. Each worker can work directly or delegate within its brief and inherited permissions, then review the results and submit its own combined report.
|
|
41
|
+
|
|
42
|
+
The parent writes the `task` brief. The plugin appends an explicit reminder that native delegation is optional and that workers cannot call `threads_spawn`. Include the worker's delegation budget in the brief. Use `No children` only when the task requires it, since that also rules out native subagents.
|
|
39
43
|
|
|
40
44
|
## Tools
|
|
41
45
|
|
|
@@ -78,6 +82,8 @@ Plugin option `maxWorkers` defaults to 4 and accepts integers from 1 through 32.
|
|
|
78
82
|
|
|
79
83
|
The terminal synchronizes workers before opening native tabs without changing focus. A TUI memory index survives plugin reloads and respects manually closed tabs. `/threads` explicitly reopens workers for open coordinator tabs. A new TUI recovers workers from durable storage. Closing the TUI does not interrupt workers.
|
|
80
84
|
|
|
85
|
+
Managed tabs use saved title prefixes: `[Main]` for the coordinator and `[Worker]` for each managed worker. Labels appear when the TUI discovers the relationship, including existing threads. The rest of the title stays editable; renaming a managed conversation reapplies its role prefix. Unrelated sessions keep their titles. OpenCode's native tab API has no separate badge field, so the prefixes also appear in session history.
|
|
86
|
+
|
|
81
87
|
Workers with `PASS` or `PASS WITH NOTES` reports hide automatically once idle. This also applies to reports saved before upgrading. Unreported workers and `FAIL` or `INCONCLUSIVE` reports stay visible. The selected tab, running workers, and tabs needing input stay open until they are inactive.
|
|
82
88
|
|
|
83
89
|
The coordinator can call `threads_hide` when a worker is no longer needed. Hiding preserves the conversation and report, survives restarts, and does not free an admission slot. `/threads` restores hidden workers and keeps them visible for inspection. A valid `threads_send` follow-up also restores its worker. Visibility overrides belong to the original report message ID, so recreating a deleted worker cannot inherit its hidden state.
|
package/index.ts
CHANGED
|
@@ -38,7 +38,7 @@ export default Plugin.define({
|
|
|
38
38
|
editor.add({
|
|
39
39
|
name: "spawn",
|
|
40
40
|
description:
|
|
41
|
-
"Delegate a task to a top-level worker in an existing absolute directory. Reuse key only for identical requests.
|
|
41
|
+
"Delegate a task to a top-level worker in an existing absolute directory. Both you and the worker may use native subagent when useful. Include any delegation limits in task. Workers cannot call threads_spawn. Reuse key only for identical requests.",
|
|
42
42
|
input: Spawn,
|
|
43
43
|
output: WorkerView,
|
|
44
44
|
options: {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@op1/threads",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.5",
|
|
4
4
|
"description": "Visible top-level worker sessions for OpenCode V2, with native tabs and durable reports.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./index.ts",
|
|
@@ -32,6 +32,9 @@
|
|
|
32
32
|
"@opencode/schema": "2.0.3",
|
|
33
33
|
"zod": "4.1.8"
|
|
34
34
|
},
|
|
35
|
+
"peerDependencies": {
|
|
36
|
+
"solid-js": ">=1.9.0"
|
|
37
|
+
},
|
|
35
38
|
"devDependencies": {
|
|
36
39
|
"@types/bun": "1.4.0",
|
|
37
40
|
"solid-js": "1.9.9",
|
|
@@ -5,22 +5,30 @@ description: Spawn and monitor visible top-level OpenCode workers with op-thread
|
|
|
5
5
|
|
|
6
6
|
# Managed sessions
|
|
7
7
|
|
|
8
|
-
Choose the delegation mode
|
|
8
|
+
Choose the delegation mode for each task. The parent can mix both modes in the same run.
|
|
9
9
|
|
|
10
10
|
- Use native `subagent` for a bounded task or review.
|
|
11
11
|
- Use `threads_spawn` for an independent workstream with its own visible session and assigned directory.
|
|
12
12
|
|
|
13
|
-
|
|
13
|
+
Both the parent and managed workers may use native `subagent` when useful. Delegation is optional. Managed workers cannot create further managed workers. Count both modes and workers' native subagents against the active protocol's delegation budget. Keep every writer in its own assigned worktree. These tools do not create worktrees.
|
|
14
14
|
|
|
15
15
|
## Start work
|
|
16
16
|
|
|
17
17
|
1. Confirm the `threads_spawn` tool is available. If it is unavailable, use native `subagent` or report that the plugin needs activation. Do not substitute a hidden `opencode run` process.
|
|
18
18
|
2. Assign an existing absolute directory and a stable task key.
|
|
19
|
-
3. Call `threads_spawn` with `key`, `title`, `directory`, and `task`. Include the goal, scope, relevant context, constraints, acceptance criteria, verification commands, and expected report in `task`.
|
|
19
|
+
3. Call `threads_spawn` with `key`, `title`, `directory`, and `task`. Include the goal, scope, relevant context, constraints, delegation allowance, acceptance criteria, verification commands, and expected report in `task`.
|
|
20
20
|
4. Save the returned worker session ID with the work unit.
|
|
21
21
|
|
|
22
22
|
An identical spawn key retries the original admission. Different work requires a new key. A worker inherits the coordinator's agent, model, and permission constraints. It has a separate conversation, so include all context it needs in the task brief.
|
|
23
23
|
|
|
24
|
+
## Write the delegation allowance
|
|
25
|
+
|
|
26
|
+
The parent writes `task`; the plugin appends worker instructions. Give workers the option to delegate without requiring a sub-coordinator role. Include this allowance and the worker's share of the remaining delegation budget:
|
|
27
|
+
|
|
28
|
+
> You may work directly or use native `subagent` for bounded tasks and reviews when useful. Pass your scope and constraints to subagents, review their results, and resolve outstanding work before calling `threads_report` yourself.
|
|
29
|
+
|
|
30
|
+
Use a no-delegation restriction only for a task-specific reason or an explicit user constraint, and state the reason. Being a managed worker or having bounded scope does not by itself make the worker a leaf. To limit managed-thread nesting, say `Do not call threads_spawn` rather than `No children`.
|
|
31
|
+
|
|
24
32
|
## Coordinate
|
|
25
33
|
|
|
26
34
|
- Use `threads_list` for a progress snapshot when making a scheduling or delivery decision.
|
|
@@ -32,10 +40,12 @@ An identical spawn key retries the original admission. Different work requires a
|
|
|
32
40
|
|
|
33
41
|
## Finish
|
|
34
42
|
|
|
35
|
-
The worker
|
|
43
|
+
The worker reviews its subagents' results and resolves outstanding work before calling `threads_report` with a combined verdict, concise summary, and concrete evidence, then stops. Native subagents return results to the worker. Valid verdicts are `PASS`, `PASS WITH NOTES`, `FAIL`, and `INCONCLUSIVE`. Report partial work and blockers honestly.
|
|
36
44
|
|
|
37
45
|
A worker report is a claim to review. Run the protocol's independent verification before marking the work unit complete. An idle session or a stopped loading indicator does not establish task success.
|
|
38
46
|
|
|
39
47
|
## Visibility
|
|
40
48
|
|
|
49
|
+
Managed tab titles start with `[Main]` for the coordinator or `[Worker]` for the worker. The TUI applies these prefixes to saved titles while preserving the rest of the name.
|
|
50
|
+
|
|
41
51
|
The TUI plugin opens managed workers as ordinary tabs without changing focus. Workers with `PASS` or `PASS WITH NOTES` reports hide automatically once inactive. Failed, inconclusive, and unreported workers stay visible until the coordinator hides them. Running, selected, and attention-needed tabs stay open. Use `/threads` to restore hidden tabs for inspection. Sending a valid follow-up also restores that worker. Reports are delivered silently to the coordinator and remain available through `threads_list`.
|
package/src/threads.ts
CHANGED
|
@@ -249,7 +249,7 @@ export function threads(
|
|
|
249
249
|
sessionID: link.workerID,
|
|
250
250
|
id: link.initialMessageID,
|
|
251
251
|
delivery: "queue",
|
|
252
|
-
text: `${input.task}\n\nYou are a managed worker assigned to ${input.directory}. Work only within the assigned scope. Do not call threads_spawn.
|
|
252
|
+
text: `${input.task}\n\nYou are a managed worker assigned to ${input.directory}. Work only within the assigned scope. You may use native subagent for bounded tasks or reviews when useful, within the brief's delegation limits and inherited permissions. Delegation is optional. Pass relevant context, scope, and constraints to each subagent. Do not call threads_spawn. Review your subagents' results and resolve any outstanding work before reporting. Only you call threads_report with the combined verdict, summary, and evidence; subagents return results to you. Runtime completion alone does not establish task success.`,
|
|
253
253
|
});
|
|
254
254
|
return view(await ctx.session.get({ sessionID: workerID }));
|
|
255
255
|
});
|
package/tui.ts
CHANGED
|
@@ -1,16 +1,22 @@
|
|
|
1
1
|
import { Plugin } from "@opencode/plugin/tui";
|
|
2
|
+
import { createEffect } from "solid-js";
|
|
3
|
+
import { z } from "zod";
|
|
2
4
|
import { ThreadsRpc } from "./src/rpc";
|
|
3
5
|
|
|
6
|
+
const CoordinatorRef = z.object({ coordinatorID: z.string() });
|
|
7
|
+
|
|
4
8
|
export default Plugin.define({
|
|
5
9
|
id: "op-threads",
|
|
6
10
|
setup(ctx) {
|
|
7
11
|
const rpc = ctx.client.rpc(ThreadsRpc);
|
|
8
12
|
const initial: { workerIDs: string[] } = { workerIDs: [] };
|
|
9
13
|
const [seen, updateSeen] = ctx.storage.memory("seen-workers", { initial });
|
|
14
|
+
const closing = new Set<string>();
|
|
10
15
|
let stopped = false;
|
|
11
16
|
let running = false;
|
|
12
17
|
let reopenPending = false;
|
|
13
18
|
let lastError: string | undefined;
|
|
19
|
+
let movingFrom: string | undefined;
|
|
14
20
|
function groupTabs() {
|
|
15
21
|
const tabs = ctx.ui.tabs.list().map((tab) => {
|
|
16
22
|
const projectID = ctx.data.session.get(tab.sessionID)?.projectID;
|
|
@@ -38,9 +44,14 @@ export default Plugin.define({
|
|
|
38
44
|
.sort((left, right) => Number(right.priority) - Number(left.priority))
|
|
39
45
|
.map((tab) => tab.sessionID),
|
|
40
46
|
);
|
|
47
|
+
const current = tabs.map((tab) => tab.sessionID);
|
|
48
|
+
const stamp = JSON.stringify(current);
|
|
49
|
+
if (movingFrom === stamp) return;
|
|
50
|
+
movingFrom = undefined;
|
|
41
51
|
for (const [index, sessionID] of ordered.entries()) {
|
|
42
|
-
if (
|
|
43
|
-
if (
|
|
52
|
+
if (current[index] === sessionID) continue;
|
|
53
|
+
if (ctx.ui.tabs.move(sessionID, index)) movingFrom = stamp;
|
|
54
|
+
break;
|
|
44
55
|
}
|
|
45
56
|
}
|
|
46
57
|
async function reconcile(reopen = false) {
|
|
@@ -55,7 +66,14 @@ export default Plugin.define({
|
|
|
55
66
|
const route = ctx.ui.router.current();
|
|
56
67
|
const coordinatorIDs = [
|
|
57
68
|
...new Set([
|
|
58
|
-
...ctx.ui.tabs.list().
|
|
69
|
+
...ctx.ui.tabs.list().flatMap((tab) => {
|
|
70
|
+
const link = CoordinatorRef.safeParse(
|
|
71
|
+
ctx.data.session.get(tab.sessionID)?.metadata?.opThreads,
|
|
72
|
+
);
|
|
73
|
+
return link.success
|
|
74
|
+
? [tab.sessionID, link.data.coordinatorID]
|
|
75
|
+
: [tab.sessionID];
|
|
76
|
+
}),
|
|
59
77
|
...(route.type === "session" ? [route.sessionID] : []),
|
|
60
78
|
]),
|
|
61
79
|
].slice(0, 100);
|
|
@@ -67,6 +85,8 @@ export default Plugin.define({
|
|
|
67
85
|
for (const worker of workers) {
|
|
68
86
|
if (stopped) return;
|
|
69
87
|
const tab = ctx.ui.tabs.list().find((tab) => tab.sessionID === worker.workerID);
|
|
88
|
+
if (closing.has(worker.workerID) && tab) continue;
|
|
89
|
+
const closed = closing.delete(worker.workerID);
|
|
70
90
|
if (
|
|
71
91
|
worker.hidden &&
|
|
72
92
|
!tab?.active &&
|
|
@@ -74,7 +94,10 @@ export default Plugin.define({
|
|
|
74
94
|
!tab?.attention &&
|
|
75
95
|
ctx.data.session.status(worker.workerID) !== "running"
|
|
76
96
|
) {
|
|
77
|
-
if (tab
|
|
97
|
+
if (tab) {
|
|
98
|
+
if (!ctx.ui.tabs.close(worker.workerID)) continue;
|
|
99
|
+
closing.add(worker.workerID);
|
|
100
|
+
}
|
|
78
101
|
if (seen.workerIDs.includes(worker.workerID)) {
|
|
79
102
|
updateSeen((draft) => {
|
|
80
103
|
draft.workerIDs = draft.workerIDs.filter(
|
|
@@ -84,7 +107,7 @@ export default Plugin.define({
|
|
|
84
107
|
}
|
|
85
108
|
continue;
|
|
86
109
|
}
|
|
87
|
-
if (!reopen && seen.workerIDs.includes(worker.workerID)) continue;
|
|
110
|
+
if (!reopen && !closed && seen.workerIDs.includes(worker.workerID)) continue;
|
|
88
111
|
await ctx.data.session.sync(worker.workerID);
|
|
89
112
|
if (
|
|
90
113
|
!stopped &&
|
|
@@ -96,7 +119,23 @@ export default Plugin.define({
|
|
|
96
119
|
});
|
|
97
120
|
}
|
|
98
121
|
}
|
|
99
|
-
if (!stopped && ctx.ui.tabs.enabled())
|
|
122
|
+
if (!stopped && ctx.ui.tabs.enabled()) {
|
|
123
|
+
const roles = new Map<string, "Main" | "Worker">();
|
|
124
|
+
for (const worker of workers) {
|
|
125
|
+
roles.set(worker.coordinatorID, "Main");
|
|
126
|
+
roles.set(worker.workerID, "Worker");
|
|
127
|
+
}
|
|
128
|
+
for (const tab of ctx.ui.tabs.list()) {
|
|
129
|
+
if (stopped) return;
|
|
130
|
+
const role = roles.get(tab.sessionID);
|
|
131
|
+
const session = ctx.data.session.get(tab.sessionID);
|
|
132
|
+
if (!role || !session?.title) continue;
|
|
133
|
+
const title = `[${role}] ${session.title.replace(/^\[(?:Main|Worker)\] /, "")}`;
|
|
134
|
+
if (title === session.title) continue;
|
|
135
|
+
await ctx.client.session.rename({ sessionID: tab.sessionID, title });
|
|
136
|
+
}
|
|
137
|
+
groupTabs();
|
|
138
|
+
}
|
|
100
139
|
} finally {
|
|
101
140
|
running = false;
|
|
102
141
|
if (reopenPending) {
|
|
@@ -127,6 +166,10 @@ export default Plugin.define({
|
|
|
127
166
|
const removeSlot = ctx.ui.slot({
|
|
128
167
|
append: "app",
|
|
129
168
|
render: () => {
|
|
169
|
+
createEffect(() => {
|
|
170
|
+
ctx.ui.tabs.list();
|
|
171
|
+
refresh();
|
|
172
|
+
});
|
|
130
173
|
ctx.keymap.layer(() => ({
|
|
131
174
|
mode: "global",
|
|
132
175
|
commands: [
|