@opencode-cockpit/subagents 0.7.0
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/LICENSE +21 -0
- package/README.md +107 -0
- package/dist/agent/plugin.js +134 -0
- package/dist/cli/preview.js +99 -0
- package/dist/core/adapt/v1.js +341 -0
- package/dist/core/adapt/v2.js +379 -0
- package/dist/core/model/changes.js +17 -0
- package/dist/core/model/model.js +301 -0
- package/dist/core/sample.js +261 -0
- package/dist/core/view/markdown.js +211 -0
- package/dist/core/view/report.js +58 -0
- package/dist/core/view/rows.js +146 -0
- package/dist/core/view/screen.js +767 -0
- package/dist/core/view/sidebar.js +151 -0
- package/dist/server.js +2 -0
- package/dist/tui/index.js +991 -0
- package/dist/tui/render.js +103 -0
- package/dist/tui/source.js +228 -0
- package/dist/tui/view/overlay.js +87 -0
- package/dist/tui/view/sidebar.js +78 -0
- package/package.json +64 -0
- package/server.js +6 -0
- package/tui.js +6 -0
- package/types/agent/plugin.d.ts +32 -0
- package/types/cli/preview.d.ts +10 -0
- package/types/core/adapt/v1.d.ts +33 -0
- package/types/core/adapt/v2.d.ts +22 -0
- package/types/core/model/changes.d.ts +98 -0
- package/types/core/model/model.d.ts +99 -0
- package/types/core/sample.d.ts +8 -0
- package/types/core/view/markdown.d.ts +22 -0
- package/types/core/view/report.d.ts +15 -0
- package/types/core/view/rows.d.ts +43 -0
- package/types/core/view/screen.d.ts +101 -0
- package/types/core/view/sidebar.d.ts +30 -0
- package/types/server.d.ts +2 -0
- package/types/tui/index.d.ts +26 -0
- package/types/tui/render.d.ts +27 -0
- package/types/tui/source.d.ts +45 -0
- package/types/tui/view/overlay.d.ts +30 -0
- package/types/tui/view/sidebar.d.ts +22 -0
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The only place that knows what a tone looks like: every colour from the user's OpenCode theme,
|
|
3
|
+
* through the host (so OpenCode 2's tokens arrive under the same names — client/host `themeFromV2`).
|
|
4
|
+
*
|
|
5
|
+
* Nothing of OpenTUI is imported at runtime: it is the host's, not installed beside a published
|
|
6
|
+
* plugin (docs/opencode/v2.md). `StyledText` and the colour class are borrowed from objects the host
|
|
7
|
+
* made, as Shell's and Review's pools do.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
const BOLD = 1 << 0;
|
|
11
|
+
const ITALIC = 1 << 2;
|
|
12
|
+
export function toneColour(theme, tone) {
|
|
13
|
+
switch (tone) {
|
|
14
|
+
case "muted":
|
|
15
|
+
return theme.textMuted;
|
|
16
|
+
case "accent":
|
|
17
|
+
return theme.accent;
|
|
18
|
+
case "info":
|
|
19
|
+
return theme.info;
|
|
20
|
+
case "tool":
|
|
21
|
+
return theme.primary;
|
|
22
|
+
case "success":
|
|
23
|
+
return theme.success;
|
|
24
|
+
case "error":
|
|
25
|
+
return theme.error;
|
|
26
|
+
case "warning":
|
|
27
|
+
return theme.warning;
|
|
28
|
+
case "border":
|
|
29
|
+
return theme.border;
|
|
30
|
+
default:
|
|
31
|
+
return theme.text;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
export function fillColour(theme, fill) {
|
|
35
|
+
if (fill === "band") return theme.backgroundPanel;
|
|
36
|
+
if (fill === "block" || fill === "card") return theme.backgroundElement;
|
|
37
|
+
if (fill === "selected") return theme.backgroundPanel;
|
|
38
|
+
return undefined;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/** Bold is bold; faint is the muted colour in italics — the terminal's DIM draws too dark to read. */
|
|
42
|
+
export const attributesOf = run => (run.bold ? BOLD : 0) | (run.faint ? ITALIC : 0);
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* A surface colour certain to paint. A theme may leave its backgrounds transparent (OpenCode's
|
|
46
|
+
* "system" theme), and a full-window surface painted with one lets the conversation show through —
|
|
47
|
+
* the 0.6 fix, carried over from Shell and Review.
|
|
48
|
+
*/
|
|
49
|
+
export function solidSurface(theme) {
|
|
50
|
+
const found = [theme.background, theme.backgroundPanel, theme.backgroundElement].find(c => c !== undefined && c.a > 0);
|
|
51
|
+
if (found) return found;
|
|
52
|
+
const text = theme.text;
|
|
53
|
+
const scale = Math.max(text.r, text.g, text.b) > 1 ? 255 : 1;
|
|
54
|
+
const light = (0.2126 * text.r + 0.7152 * text.g + 0.0722 * text.b) / scale > 0.5;
|
|
55
|
+
const Colour = text.constructor;
|
|
56
|
+
return Colour.fromHex?.(light ? "#0b0b0e" : "#fafafa") ?? text;
|
|
57
|
+
}
|
|
58
|
+
/** Rows onto a pool of lines — Shell's pool: only lines that changed are touched. */
|
|
59
|
+
export function createRowPool(lines) {
|
|
60
|
+
let StyledText;
|
|
61
|
+
let painted = [];
|
|
62
|
+
let drawnWith;
|
|
63
|
+
const styled = chunks => {
|
|
64
|
+
if (!StyledText && lines[0]) {
|
|
65
|
+
const found = lines[0].content?.constructor;
|
|
66
|
+
if (found && found !== Object) StyledText = found;
|
|
67
|
+
}
|
|
68
|
+
return StyledText ? new StyledText(chunks) : chunks.map(part => part.text).join("");
|
|
69
|
+
};
|
|
70
|
+
const chunk = (theme, run) => ({
|
|
71
|
+
__isChunk: true,
|
|
72
|
+
text: run.text,
|
|
73
|
+
fg: toneColour(theme, run.tone),
|
|
74
|
+
bg: fillColour(theme, run.fill),
|
|
75
|
+
attributes: attributesOf(run)
|
|
76
|
+
});
|
|
77
|
+
return {
|
|
78
|
+
draw(rows, theme) {
|
|
79
|
+
/** A new theme changes every colour, so nothing on screen can be trusted to still be right. */
|
|
80
|
+
const all = theme !== drawnWith;
|
|
81
|
+
drawnWith = theme;
|
|
82
|
+
rows.forEach((row, index) => {
|
|
83
|
+
const line = lines[index];
|
|
84
|
+
if (!line) return;
|
|
85
|
+
const key = JSON.stringify(row);
|
|
86
|
+
if (all || painted[index] !== key) {
|
|
87
|
+
line.content = styled(row.map(run => chunk(theme, run)));
|
|
88
|
+
painted[index] = key;
|
|
89
|
+
}
|
|
90
|
+
line.visible = true;
|
|
91
|
+
});
|
|
92
|
+
for (let index = rows.length; index < lines.length; index++) {
|
|
93
|
+
const line = lines[index];
|
|
94
|
+
if (line) line.visible = false;
|
|
95
|
+
painted[index] = undefined;
|
|
96
|
+
}
|
|
97
|
+
},
|
|
98
|
+
clear() {
|
|
99
|
+
for (const line of lines) line.visible = false;
|
|
100
|
+
painted = [];
|
|
101
|
+
}
|
|
102
|
+
};
|
|
103
|
+
}
|
|
@@ -0,0 +1,228 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Where subagents come from: the host's live events, and the stored runs of subagents that existed
|
|
3
|
+
* before this plugin did. The only file that reaches past `Host` into a specific OpenCode — through
|
|
4
|
+
* `api.v1` / `api.v2`, as Status's snapshot does — and every shape it reads was measured on 1.18.32
|
|
5
|
+
* and 2.0.15 (docs/opencode/agents.md). Everything it learns becomes `Change`s for the model.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { createV1Translator } from "../core/adapt/v1.js";
|
|
9
|
+
import { createV2Translator } from "../core/adapt/v2.js";
|
|
10
|
+
/** OpenCode 1 event types that concern sessions, messages and what they wait on. */
|
|
11
|
+
const V1_EVENTS = ["session.created", "session.updated", "session.status", "session.idle", "session.error", "message.updated", "message.part.updated", "message.part.delta", "permission.asked", "permission.replied", "question.asked", "question.replied", "question.rejected"];
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* The host's objects, reached by the shapes measured on real runs (docs/opencode/agents.md) rather than
|
|
15
|
+
* by types: `api.v1` / `api.v2` are typed only as far as the host needs them.
|
|
16
|
+
*/
|
|
17
|
+
// biome-ignore lint/suspicious/noExplicitAny: see above — every access is to a measured field
|
|
18
|
+
|
|
19
|
+
export function createSource(api, log, emit) {
|
|
20
|
+
/** Each unexpected shape once, not once per event. */
|
|
21
|
+
const told = new Set();
|
|
22
|
+
const unknown = (what, detail) => {
|
|
23
|
+
const key = `${what} ${JSON.stringify(detail)}`;
|
|
24
|
+
if (told.has(key)) return;
|
|
25
|
+
told.add(key);
|
|
26
|
+
log.warn("unrecognised event", {
|
|
27
|
+
what,
|
|
28
|
+
...detail
|
|
29
|
+
});
|
|
30
|
+
};
|
|
31
|
+
const offs = [];
|
|
32
|
+
const guard = (where, fn) => {
|
|
33
|
+
try {
|
|
34
|
+
fn();
|
|
35
|
+
} catch (error) {
|
|
36
|
+
log.error(`${where} failed`, {
|
|
37
|
+
error
|
|
38
|
+
});
|
|
39
|
+
}
|
|
40
|
+
};
|
|
41
|
+
if (api.v1) {
|
|
42
|
+
const v1 = api.v1;
|
|
43
|
+
const translate = createV1Translator(unknown);
|
|
44
|
+
for (const type of V1_EVENTS) {
|
|
45
|
+
const off = v1.event.on(type, event => guard("event", () => emit(translate.event(event))));
|
|
46
|
+
if (typeof off === "function") offs.push(off);
|
|
47
|
+
}
|
|
48
|
+
return {
|
|
49
|
+
async load(root) {
|
|
50
|
+
const seen = new Set();
|
|
51
|
+
const visit = async (parent, depth) => {
|
|
52
|
+
if (depth > 4 || seen.has(parent)) return;
|
|
53
|
+
seen.add(parent);
|
|
54
|
+
const result = await v1.client.session.children({
|
|
55
|
+
sessionID: parent
|
|
56
|
+
}).catch(error => {
|
|
57
|
+
log.warn("children failed", {
|
|
58
|
+
parent,
|
|
59
|
+
error
|
|
60
|
+
});
|
|
61
|
+
return undefined;
|
|
62
|
+
});
|
|
63
|
+
const children = result?.data ?? result ?? [];
|
|
64
|
+
for (const child of Array.isArray(children) ? children : []) {
|
|
65
|
+
const id = child.id;
|
|
66
|
+
const messages = v1.state.session.messages(id) ?? [];
|
|
67
|
+
const history = messages.map(info => ({
|
|
68
|
+
info,
|
|
69
|
+
parts: v1.state.part(info.id) ?? []
|
|
70
|
+
}));
|
|
71
|
+
const status = v1.state.session.status(id);
|
|
72
|
+
/**
|
|
73
|
+
* Busy only when the host says so. A finished subagent is not in the host's status store at
|
|
74
|
+
* all — reading "no status" as "unknown" left every old subagent running forever.
|
|
75
|
+
*/
|
|
76
|
+
const busy = status?.type === "busy" || status?.type === "retry";
|
|
77
|
+
emit([...translate.session(child), ...translate.history(history), busy ? {
|
|
78
|
+
type: "status",
|
|
79
|
+
id,
|
|
80
|
+
status: "busy",
|
|
81
|
+
at: Date.now()
|
|
82
|
+
} : {
|
|
83
|
+
type: "status",
|
|
84
|
+
id,
|
|
85
|
+
status: "idle",
|
|
86
|
+
at: Number(child.time?.updated) || Date.now()
|
|
87
|
+
}]);
|
|
88
|
+
await visit(id, depth + 1);
|
|
89
|
+
}
|
|
90
|
+
};
|
|
91
|
+
await visit(root, 0);
|
|
92
|
+
},
|
|
93
|
+
async send(id, text, _busy, agent) {
|
|
94
|
+
await v1.client.session.promptAsync({
|
|
95
|
+
sessionID: id,
|
|
96
|
+
agent,
|
|
97
|
+
parts: [{
|
|
98
|
+
type: "text",
|
|
99
|
+
text
|
|
100
|
+
}]
|
|
101
|
+
});
|
|
102
|
+
},
|
|
103
|
+
async stop(id) {
|
|
104
|
+
await v1.client.session.abort({
|
|
105
|
+
sessionID: id
|
|
106
|
+
});
|
|
107
|
+
},
|
|
108
|
+
async background(parentID) {
|
|
109
|
+
const result = await v1.client.experimental.session.background({
|
|
110
|
+
sessionID: parentID
|
|
111
|
+
});
|
|
112
|
+
return (result?.data ?? result) === true;
|
|
113
|
+
},
|
|
114
|
+
async quiet(sessionID, text, agent) {
|
|
115
|
+
await v1.client.session.promptAsync({
|
|
116
|
+
sessionID,
|
|
117
|
+
noReply: true,
|
|
118
|
+
...(agent ? {
|
|
119
|
+
agent
|
|
120
|
+
} : {}),
|
|
121
|
+
parts: [{
|
|
122
|
+
type: "text",
|
|
123
|
+
text
|
|
124
|
+
}]
|
|
125
|
+
});
|
|
126
|
+
},
|
|
127
|
+
async note(sessionID, text, _busy, agent) {
|
|
128
|
+
await v1.client.session.promptAsync({
|
|
129
|
+
sessionID,
|
|
130
|
+
...(agent ? {
|
|
131
|
+
agent
|
|
132
|
+
} : {}),
|
|
133
|
+
parts: [{
|
|
134
|
+
type: "text",
|
|
135
|
+
text
|
|
136
|
+
}]
|
|
137
|
+
});
|
|
138
|
+
},
|
|
139
|
+
check(id) {
|
|
140
|
+
const status = v1.state.session.status(id);
|
|
141
|
+
return status ? translate.event({
|
|
142
|
+
type: "session.status",
|
|
143
|
+
properties: {
|
|
144
|
+
sessionID: id,
|
|
145
|
+
status
|
|
146
|
+
}
|
|
147
|
+
}) : [];
|
|
148
|
+
},
|
|
149
|
+
dispose: () => {
|
|
150
|
+
for (const off of offs.splice(0)) off();
|
|
151
|
+
}
|
|
152
|
+
};
|
|
153
|
+
}
|
|
154
|
+
const v2 = api.v2;
|
|
155
|
+
const translate = createV2Translator(unknown);
|
|
156
|
+
const off = v2.data.listen(event => guard("event", () => emit(translate.event(event))));
|
|
157
|
+
if (typeof off === "function") offs.push(off);
|
|
158
|
+
return {
|
|
159
|
+
async load(root) {
|
|
160
|
+
const sessions = v2.data.session.list() ?? [];
|
|
161
|
+
const byId = new Map(sessions.map(s => [s.id, s]));
|
|
162
|
+
const family = new Set(v2.data.session.family(root) ?? []);
|
|
163
|
+
for (const id of family) {
|
|
164
|
+
const info = byId.get(id);
|
|
165
|
+
if (!info?.parentID) continue;
|
|
166
|
+
let messages = v2.data.session.message.list(id) ?? [];
|
|
167
|
+
if (messages.length === 0) {
|
|
168
|
+
await Promise.resolve(v2.data.session.message.sync(id)).catch(error => log.warn("history sync failed", {
|
|
169
|
+
id,
|
|
170
|
+
error
|
|
171
|
+
}));
|
|
172
|
+
messages = v2.data.session.message.list(id) ?? [];
|
|
173
|
+
}
|
|
174
|
+
const status = translate.status(id, v2.data.session.status(id));
|
|
175
|
+
emit([...translate.session(info), ...translate.history(id, messages), /** Not known to the host is not running: the same lesson as OpenCode 1's store. */
|
|
176
|
+
...(status.length > 0 ? status : [{
|
|
177
|
+
type: "status",
|
|
178
|
+
id,
|
|
179
|
+
status: "idle",
|
|
180
|
+
at: Number(info.time?.updated) || Date.now()
|
|
181
|
+
}])]);
|
|
182
|
+
}
|
|
183
|
+
},
|
|
184
|
+
async send(id, text, busy) {
|
|
185
|
+
await v2.client.session.prompt({
|
|
186
|
+
sessionID: id,
|
|
187
|
+
text,
|
|
188
|
+
...(busy ? {
|
|
189
|
+
delivery: "steer"
|
|
190
|
+
} : {})
|
|
191
|
+
});
|
|
192
|
+
},
|
|
193
|
+
async stop(id) {
|
|
194
|
+
await v2.client.session.interrupt({
|
|
195
|
+
sessionID: id
|
|
196
|
+
});
|
|
197
|
+
},
|
|
198
|
+
async background(parentID) {
|
|
199
|
+
await v2.client.session.background({
|
|
200
|
+
sessionID: parentID
|
|
201
|
+
});
|
|
202
|
+
return true;
|
|
203
|
+
},
|
|
204
|
+
async quiet(sessionID, text) {
|
|
205
|
+
await v2.client.session.synthetic({
|
|
206
|
+
sessionID,
|
|
207
|
+
text,
|
|
208
|
+
resume: false,
|
|
209
|
+
description: "Subagent exchange"
|
|
210
|
+
});
|
|
211
|
+
},
|
|
212
|
+
async note(sessionID, text, busy) {
|
|
213
|
+
await v2.client.session.prompt({
|
|
214
|
+
sessionID,
|
|
215
|
+
text,
|
|
216
|
+
...(busy ? {
|
|
217
|
+
delivery: "steer"
|
|
218
|
+
} : {})
|
|
219
|
+
});
|
|
220
|
+
},
|
|
221
|
+
check(id) {
|
|
222
|
+
return translate.status(id, v2.data.session.status(id));
|
|
223
|
+
},
|
|
224
|
+
dispose: () => {
|
|
225
|
+
for (const each of offs.splice(0)) each();
|
|
226
|
+
}
|
|
227
|
+
};
|
|
228
|
+
}
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import { effect as _$effect } from "@opentui/solid";
|
|
2
|
+
import { insertNode as _$insertNode } from "@opentui/solid";
|
|
3
|
+
import { insert as _$insert } from "@opentui/solid";
|
|
4
|
+
import { setProp as _$setProp } from "@opentui/solid";
|
|
5
|
+
import { use as _$use } from "@opentui/solid";
|
|
6
|
+
import { createElement as _$createElement } from "@opentui/solid";
|
|
7
|
+
/** @jsxImportSource @opentui/solid */
|
|
8
|
+
// biome-ignore-all lint/a11y/noStaticElementInteractions: these are terminal boxes, not DOM elements
|
|
9
|
+
|
|
10
|
+
/** Enough lines for any terminal: a slot's tree is read once, so the pool cannot grow later. */
|
|
11
|
+
export const MAX_LINES = 300;
|
|
12
|
+
/**
|
|
13
|
+
* The pane's surface — Review's shape: a full-window, transparent backdrop that catches clicks off
|
|
14
|
+
* the pane, and the pane inside it, right-aligned, half the window or all of it.
|
|
15
|
+
*
|
|
16
|
+
* **Nothing here is reactive.** The host reads a slot's children once (docs/opencode/gotchas.md), so
|
|
17
|
+
* the component hands its boxes up and the plugin assigns to them. Anchored bottom-right, because an
|
|
18
|
+
* absolute box is placed against the `app_bottom` container at the foot of the window. Only props a
|
|
19
|
+
* box actually uses: an unused one (`titleColor`) blanked Review's pane on OpenCode 2.
|
|
20
|
+
*/
|
|
21
|
+
export function Overlay(props) {
|
|
22
|
+
let backdrop;
|
|
23
|
+
let panel;
|
|
24
|
+
const lines = [];
|
|
25
|
+
/** Refs arrive child-first; act once every piece is here. */
|
|
26
|
+
const ready = () => {
|
|
27
|
+
if (backdrop && panel && lines.length === MAX_LINES) props.onReady({
|
|
28
|
+
backdrop,
|
|
29
|
+
panel,
|
|
30
|
+
lines
|
|
31
|
+
});
|
|
32
|
+
};
|
|
33
|
+
return (() => {
|
|
34
|
+
var _el$ = _$createElement("box"),
|
|
35
|
+
_el$2 = _$createElement("box");
|
|
36
|
+
_$insertNode(_el$, _el$2);
|
|
37
|
+
_$use(element => {
|
|
38
|
+
backdrop = element;
|
|
39
|
+
ready();
|
|
40
|
+
}, _el$);
|
|
41
|
+
_$setProp(_el$, "visible", false);
|
|
42
|
+
_$setProp(_el$, "position", "absolute");
|
|
43
|
+
_$setProp(_el$, "right", 0);
|
|
44
|
+
_$setProp(_el$, "bottom", 0);
|
|
45
|
+
_$setProp(_el$, "width", 20);
|
|
46
|
+
_$setProp(_el$, "height", 0);
|
|
47
|
+
_$setProp(_el$, "zIndex", 1000);
|
|
48
|
+
_$setProp(_el$, "flexDirection", "row");
|
|
49
|
+
_$setProp(_el$, "justifyContent", "flex-end");
|
|
50
|
+
_$setProp(_el$, "backgroundColor", "transparent");
|
|
51
|
+
_$setProp(_el$, "onMouseDown", () => props.onDismiss());
|
|
52
|
+
_$use(element => {
|
|
53
|
+
panel = element;
|
|
54
|
+
ready();
|
|
55
|
+
}, _el$2);
|
|
56
|
+
_$setProp(_el$2, "width", 20);
|
|
57
|
+
_$setProp(_el$2, "height", 0);
|
|
58
|
+
_$setProp(_el$2, "flexShrink", 0);
|
|
59
|
+
_$setProp(_el$2, "flexDirection", "column");
|
|
60
|
+
_$setProp(_el$2, "onMouseDown", event => event.stopPropagation());
|
|
61
|
+
_$setProp(_el$2, "onMouseUp", event => {
|
|
62
|
+
event.stopPropagation();
|
|
63
|
+
props.onClick(event.y);
|
|
64
|
+
});
|
|
65
|
+
_$setProp(_el$2, "onMouse", event => {
|
|
66
|
+
const scroll = event.scroll;
|
|
67
|
+
if (!scroll) return;
|
|
68
|
+
event.stopPropagation();
|
|
69
|
+
props.onScroll(scroll.direction === "up" ? -3 : 3);
|
|
70
|
+
});
|
|
71
|
+
_$insert(_el$2, () => Array.from({
|
|
72
|
+
length: MAX_LINES
|
|
73
|
+
}, () => (() => {
|
|
74
|
+
var _el$3 = _$createElement("text");
|
|
75
|
+
_$use(element => {
|
|
76
|
+
lines.push(element);
|
|
77
|
+
ready();
|
|
78
|
+
}, _el$3);
|
|
79
|
+
_$setProp(_el$3, "wrapMode", "none");
|
|
80
|
+
_$setProp(_el$3, "flexShrink", 0);
|
|
81
|
+
_$setProp(_el$3, "visible", false);
|
|
82
|
+
return _el$3;
|
|
83
|
+
})()));
|
|
84
|
+
_$effect(_$p => _$setProp(_el$2, "backgroundColor", props.api.theme.current.background, _$p));
|
|
85
|
+
return _el$;
|
|
86
|
+
})();
|
|
87
|
+
}
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import { insertNode as _$insertNode } from "@opentui/solid";
|
|
2
|
+
import { insert as _$insert } from "@opentui/solid";
|
|
3
|
+
import { createComponent as _$createComponent } from "@opentui/solid";
|
|
4
|
+
import { use as _$use } from "@opentui/solid";
|
|
5
|
+
import { setProp as _$setProp } from "@opentui/solid";
|
|
6
|
+
import { createElement as _$createElement } from "@opentui/solid";
|
|
7
|
+
/** @jsxImportSource @opentui/solid */
|
|
8
|
+
// biome-ignore-all lint/a11y/noStaticElementInteractions: these are terminal boxes, not DOM elements
|
|
9
|
+
|
|
10
|
+
import { For } from "solid-js";
|
|
11
|
+
import { fillColour, toneColour } from "../render.js";
|
|
12
|
+
/**
|
|
13
|
+
* The Subagents block: the rows `core/view/sidebar.ts` produced, one `<text>` per row.
|
|
14
|
+
*
|
|
15
|
+
* Shell's and Status's sidebar pattern, proven live on both OpenCodes: a signal read by `<For>`
|
|
16
|
+
* redraws, where anything decided once in a slot's tree never would (docs/opencode/gotchas.md).
|
|
17
|
+
* Every line of a subagent opens it; mouse-up, not mouse-down, because the host acts on the release
|
|
18
|
+
* that follows (as Shell's sidebar found).
|
|
19
|
+
*/
|
|
20
|
+
export function SidebarBlock(props) {
|
|
21
|
+
const theme = () => props.api.theme.current;
|
|
22
|
+
return (() => {
|
|
23
|
+
var _el$ = _$createElement("box");
|
|
24
|
+
_$use(box => props.onReady?.(box), _el$);
|
|
25
|
+
_$setProp(_el$, "flexDirection", "column");
|
|
26
|
+
_$insert(_el$, _$createComponent(For, {
|
|
27
|
+
get each() {
|
|
28
|
+
return props.lines();
|
|
29
|
+
},
|
|
30
|
+
children: line => (() => {
|
|
31
|
+
var _el$2 = _$createElement("box"),
|
|
32
|
+
_el$3 = _$createElement("text");
|
|
33
|
+
_$insertNode(_el$2, _el$3);
|
|
34
|
+
_$setProp(_el$2, "flexDirection", "row");
|
|
35
|
+
_$setProp(_el$2, "onMouseUp", () => {
|
|
36
|
+
if (line.id) props.onOpen(line.id);
|
|
37
|
+
});
|
|
38
|
+
_$setProp(_el$3, "wrapMode", "none");
|
|
39
|
+
_$setProp(_el$3, "flexShrink", 0);
|
|
40
|
+
_$insert(_el$3, _$createComponent(For, {
|
|
41
|
+
get each() {
|
|
42
|
+
return line.row;
|
|
43
|
+
},
|
|
44
|
+
children: run => {
|
|
45
|
+
const style = {
|
|
46
|
+
fg: toneColour(theme(), run.tone),
|
|
47
|
+
...(run.fill && run.fill !== "none" ? {
|
|
48
|
+
bg: fillColour(theme(), run.fill)
|
|
49
|
+
} : {})
|
|
50
|
+
};
|
|
51
|
+
return run.bold ? (() => {
|
|
52
|
+
var _el$4 = _$createElement("span"),
|
|
53
|
+
_el$5 = _$createElement("b");
|
|
54
|
+
_$insertNode(_el$4, _el$5);
|
|
55
|
+
_$setProp(_el$4, "style", style);
|
|
56
|
+
_$insert(_el$5, () => run.text);
|
|
57
|
+
return _el$4;
|
|
58
|
+
})() : run.faint ? (() => {
|
|
59
|
+
var _el$6 = _$createElement("span"),
|
|
60
|
+
_el$7 = _$createElement("i");
|
|
61
|
+
_$insertNode(_el$6, _el$7);
|
|
62
|
+
_$setProp(_el$6, "style", style);
|
|
63
|
+
_$insert(_el$7, () => run.text);
|
|
64
|
+
return _el$6;
|
|
65
|
+
})() : (() => {
|
|
66
|
+
var _el$8 = _$createElement("span");
|
|
67
|
+
_$setProp(_el$8, "style", style);
|
|
68
|
+
_$insert(_el$8, () => run.text);
|
|
69
|
+
return _el$8;
|
|
70
|
+
})();
|
|
71
|
+
}
|
|
72
|
+
}));
|
|
73
|
+
return _el$2;
|
|
74
|
+
})()
|
|
75
|
+
}));
|
|
76
|
+
return _el$;
|
|
77
|
+
})();
|
|
78
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@opencode-cockpit/subagents",
|
|
3
|
+
"version": "0.7.0",
|
|
4
|
+
"description": "See what your subagents are doing: in the sidebar, full screen, and message them",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"license": "MIT",
|
|
7
|
+
"author": "Codestz",
|
|
8
|
+
"repository": {
|
|
9
|
+
"type": "git",
|
|
10
|
+
"url": "git+https://github.com/Codestz/opencode-cockpit.git",
|
|
11
|
+
"directory": "packages/subagents"
|
|
12
|
+
},
|
|
13
|
+
"homepage": "https://github.com/Codestz/opencode-cockpit#readme",
|
|
14
|
+
"bugs": {
|
|
15
|
+
"url": "https://github.com/Codestz/opencode-cockpit/issues"
|
|
16
|
+
},
|
|
17
|
+
"keywords": [
|
|
18
|
+
"opencode",
|
|
19
|
+
"opencode-plugin",
|
|
20
|
+
"subagents",
|
|
21
|
+
"agents",
|
|
22
|
+
"tui",
|
|
23
|
+
"terminal"
|
|
24
|
+
],
|
|
25
|
+
"exports": {
|
|
26
|
+
"./server": {
|
|
27
|
+
"types": "./types/server.d.ts",
|
|
28
|
+
"default": "./dist/server.js"
|
|
29
|
+
},
|
|
30
|
+
"./tui": {
|
|
31
|
+
"types": "./types/tui/index.d.ts",
|
|
32
|
+
"default": "./dist/tui/index.js"
|
|
33
|
+
}
|
|
34
|
+
},
|
|
35
|
+
"engines": {
|
|
36
|
+
"opencode": ">=1.18.0",
|
|
37
|
+
"bun": ">=1.3.5"
|
|
38
|
+
},
|
|
39
|
+
"files": [
|
|
40
|
+
"server.js",
|
|
41
|
+
"tui.js",
|
|
42
|
+
"dist",
|
|
43
|
+
"types",
|
|
44
|
+
"README.md",
|
|
45
|
+
"LICENSE"
|
|
46
|
+
],
|
|
47
|
+
"publishConfig": {
|
|
48
|
+
"access": "public"
|
|
49
|
+
},
|
|
50
|
+
"dependencies": {
|
|
51
|
+
"@opencode-cockpit/client": "0.7.0",
|
|
52
|
+
"@opencode-ai/plugin": "1.18.31"
|
|
53
|
+
},
|
|
54
|
+
"devDependencies": {
|
|
55
|
+
"@opentui/core": "0.4.5",
|
|
56
|
+
"@opentui/keymap": "0.4.5",
|
|
57
|
+
"@opentui/solid": "0.4.5",
|
|
58
|
+
"solid-js": "1.9.12"
|
|
59
|
+
},
|
|
60
|
+
"bin": {
|
|
61
|
+
"opencode-subagents": "./dist/cli/preview.js",
|
|
62
|
+
"subagents": "./dist/cli/preview.js"
|
|
63
|
+
}
|
|
64
|
+
}
|
package/server.js
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* OpenCode 2 finds a plugin configured by *path* by the files at its root — `<package>/tui`,
|
|
3
|
+
* `<package>/server` — rather than through `exports` (docs/opencode/v2.md). A package installed by
|
|
4
|
+
* name resolves through `exports` as before; this file is only the door for the path case.
|
|
5
|
+
*/
|
|
6
|
+
export { default } from "./dist/server.js"
|
package/tui.js
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* OpenCode 2 finds a plugin configured by *path* by the files at its root — `<package>/tui`,
|
|
3
|
+
* `<package>/server` — rather than through `exports` (docs/opencode/v2.md). A package installed by
|
|
4
|
+
* name resolves through `exports` as before; this file is only the door for the path case.
|
|
5
|
+
*/
|
|
6
|
+
export { default } from "./dist/tui/index.js"
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { type ServerStart } from "@opencode-cockpit/client/server";
|
|
2
|
+
import type { Change } from "../core/model/changes.ts";
|
|
3
|
+
/**
|
|
4
|
+
* What the agent is told about subagents, once per request.
|
|
5
|
+
*
|
|
6
|
+
* Measured on both OpenCodes (docs/opencode/agents.md): a subagent launched in the background lets the
|
|
7
|
+
* conversation carry on, and the main agent is told when it finishes. OpenCode 2 offers it on its
|
|
8
|
+
* `subagent` tool; OpenCode 1 only with `OPENCODE_EXPERIMENTAL_BACKGROUND_SUBAGENTS=true`, which a
|
|
9
|
+
* plugin cannot set — so the guidance says "if your tool offers it", and never promises it.
|
|
10
|
+
*
|
|
11
|
+
* Continuing a subagent was measured too: given its id, the same subagent picks up with everything it
|
|
12
|
+
* already read, and answers in seconds. Main agents rarely do it on their own — the id scrolls away —
|
|
13
|
+
* so the guidance asks for it, and `subagents_list` hands the ids back.
|
|
14
|
+
*/
|
|
15
|
+
export declare const GUIDANCE = "## Subagents (opencode-cockpit)\nWhen you delegate independent work to a subagent, launch it with background: true if your task or subagent tool offers that option, so this conversation continues while it works; you are notified when it finishes. Work on something else meanwhile, or tell the user what you launched.\nWhen the user asks for a fix or follow-up on work a subagent already did, continue that same subagent (task_id or sessionID) rather than launching a new one \u2014 it keeps its context. subagents_list gives each subagent's id, task and last answer.\nThe user can watch each subagent and message it directly; when they do, a note in this conversation tells you what they asked and what it answered.";
|
|
16
|
+
export declare const SUBAGENTS_PACKAGE = "@opencode-cockpit/subagents";
|
|
17
|
+
export interface SubagentsServerOptions {
|
|
18
|
+
source?: string;
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* What the agent side keeps of each run: enough to list it — not what calls printed, nor thinking, so
|
|
22
|
+
* a long session does not hold every file its subagents read.
|
|
23
|
+
*/
|
|
24
|
+
export declare function slim(changes: Change[]): Change[];
|
|
25
|
+
/** The agent side as a factory, so the `opencode-cockpit` bundle can include it. */
|
|
26
|
+
export declare function createSubagentsServer({ source, }?: SubagentsServerOptions): ServerStart;
|
|
27
|
+
declare const _default: {
|
|
28
|
+
id: string;
|
|
29
|
+
server: (input: import("@opencode-ai/plugin").PluginInput, options?: unknown) => Promise<import("@opencode-ai/plugin").Hooks>;
|
|
30
|
+
setup: (ctx: import("@opencode-cockpit/client/server").V2ServerContext) => Promise<(() => Promise<void>) | undefined>;
|
|
31
|
+
};
|
|
32
|
+
export default _default;
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
#!/usr/bin/env bun
|
|
2
|
+
/**
|
|
3
|
+
* The see-it loop: the sidebar block and the full screen, drawn in this terminal from a recorded run,
|
|
4
|
+
* with no OpenCode running. The same rows OpenCode draws — only the colours come from a fixed
|
|
5
|
+
* palette (OpenCode's default theme) instead of the user's.
|
|
6
|
+
*
|
|
7
|
+
* bunx @opencode-cockpit/subagents preview a sample run, mid-flight
|
|
8
|
+
* bunx @opencode-cockpit/subagents preview --width 34 the sidebar at another width
|
|
9
|
+
*/
|
|
10
|
+
export {};
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* OpenCode 1's events and state, as changes.
|
|
3
|
+
*
|
|
4
|
+
* OpenCode 1 describes a run as parts of messages: a part is created (`message.part.updated`), may
|
|
5
|
+
* stream (`message.part.delta`, which names the part but not its kind — so kinds are remembered), and
|
|
6
|
+
* is updated as it completes. A user message's text is what the session was told; an assistant's
|
|
7
|
+
* `reasoning` and `text` are its thinking and its answer; a `tool` part is a call. Shapes measured on
|
|
8
|
+
* 1.18.32 (test/fixtures/v1.jsonl).
|
|
9
|
+
*
|
|
10
|
+
* Anything it does not recognise it returns nothing for, and says so through `unknown` — a new event
|
|
11
|
+
* shape is logged, never guessed at.
|
|
12
|
+
*/
|
|
13
|
+
import type { Change } from "../model/changes.ts";
|
|
14
|
+
type Json = Record<string, unknown>;
|
|
15
|
+
export interface V1Translator {
|
|
16
|
+
/** One host event as changes. */
|
|
17
|
+
event(event: unknown, at?: number): Change[];
|
|
18
|
+
/** A session's stored messages (each `{ info, parts }`), for one that existed before we did. */
|
|
19
|
+
history(messages: readonly {
|
|
20
|
+
info: unknown;
|
|
21
|
+
parts: readonly unknown[];
|
|
22
|
+
}[], at?: number): Change[];
|
|
23
|
+
/** A session as `session.children` or `state.session.get` returns it. */
|
|
24
|
+
session(info: unknown, at?: number): Change[];
|
|
25
|
+
}
|
|
26
|
+
export declare function createV1Translator(unknown?: (what: string, detail?: Json) => void): V1Translator;
|
|
27
|
+
/** `input + output + reasoning + cache`, the host's own total. */
|
|
28
|
+
export declare function tokenTotal(tokens: Json): number;
|
|
29
|
+
/** OpenCode 1 titles a subagent session "Task title (@explore subagent)"; the agent is shown apart. */
|
|
30
|
+
export declare function stripAgentSuffix(title: string): string;
|
|
31
|
+
/** A call's result in a few words, when the host gives the number. */
|
|
32
|
+
export declare function summaryOf(tool: string | undefined, metadata: Json): string | undefined;
|
|
33
|
+
export {};
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* OpenCode 2's events and state, as changes.
|
|
3
|
+
*
|
|
4
|
+
* OpenCode 2 names what happened: `session.reasoning.delta`, `session.tool.called`,
|
|
5
|
+
* `session.execution.succeeded`. Its interface context hands them over as
|
|
6
|
+
* `{ name, details: { data } }` (`ctx.data.listen`). Thinking and text are keyed by the assistant
|
|
7
|
+
* message and their ordinal in it; a tool call by its id; what the session was told arrives as an
|
|
8
|
+
* inbox item. Shapes measured on 2.0.15 (test/fixtures/v2.jsonl).
|
|
9
|
+
*/
|
|
10
|
+
import type { Change } from "../model/changes.ts";
|
|
11
|
+
type Json = Record<string, unknown>;
|
|
12
|
+
export interface V2Translator {
|
|
13
|
+
event(event: unknown, at?: number): Change[];
|
|
14
|
+
/** One session's loaded messages (`ctx.data.session.message.list(id)`). */
|
|
15
|
+
history(id: string, messages: readonly unknown[], at?: number): Change[];
|
|
16
|
+
/** A session as `ctx.data.session.list()` returns it. */
|
|
17
|
+
session(info: unknown, at?: number): Change[];
|
|
18
|
+
/** A status as `ctx.data.session.status(id)` returns it: "busy", "idle"… */
|
|
19
|
+
status(id: string, status: unknown, at?: number): Change[];
|
|
20
|
+
}
|
|
21
|
+
export declare function createV2Translator(unknown?: (what: string, detail?: Json) => void): V2Translator;
|
|
22
|
+
export {};
|