@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.
Files changed (41) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +107 -0
  3. package/dist/agent/plugin.js +134 -0
  4. package/dist/cli/preview.js +99 -0
  5. package/dist/core/adapt/v1.js +341 -0
  6. package/dist/core/adapt/v2.js +379 -0
  7. package/dist/core/model/changes.js +17 -0
  8. package/dist/core/model/model.js +301 -0
  9. package/dist/core/sample.js +261 -0
  10. package/dist/core/view/markdown.js +211 -0
  11. package/dist/core/view/report.js +58 -0
  12. package/dist/core/view/rows.js +146 -0
  13. package/dist/core/view/screen.js +767 -0
  14. package/dist/core/view/sidebar.js +151 -0
  15. package/dist/server.js +2 -0
  16. package/dist/tui/index.js +991 -0
  17. package/dist/tui/render.js +103 -0
  18. package/dist/tui/source.js +228 -0
  19. package/dist/tui/view/overlay.js +87 -0
  20. package/dist/tui/view/sidebar.js +78 -0
  21. package/package.json +64 -0
  22. package/server.js +6 -0
  23. package/tui.js +6 -0
  24. package/types/agent/plugin.d.ts +32 -0
  25. package/types/cli/preview.d.ts +10 -0
  26. package/types/core/adapt/v1.d.ts +33 -0
  27. package/types/core/adapt/v2.d.ts +22 -0
  28. package/types/core/model/changes.d.ts +98 -0
  29. package/types/core/model/model.d.ts +99 -0
  30. package/types/core/sample.d.ts +8 -0
  31. package/types/core/view/markdown.d.ts +22 -0
  32. package/types/core/view/report.d.ts +15 -0
  33. package/types/core/view/rows.d.ts +43 -0
  34. package/types/core/view/screen.d.ts +101 -0
  35. package/types/core/view/sidebar.d.ts +30 -0
  36. package/types/server.d.ts +2 -0
  37. package/types/tui/index.d.ts +26 -0
  38. package/types/tui/render.d.ts +27 -0
  39. package/types/tui/source.d.ts +45 -0
  40. package/types/tui/view/overlay.d.ts +30 -0
  41. package/types/tui/view/sidebar.d.ts +22 -0
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Codestz
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,107 @@
1
+ # @opencode-cockpit/subagents
2
+
3
+ **See what your subagents are doing, while they do it — and reuse them.** When the main agent hands
4
+ work to a subagent, you get one line in the chat and nothing else. This puts every subagent in the
5
+ sidebar with what it is doing right now, opens its whole run in a pane — thinking, every tool call as
6
+ OpenCode draws its own, the answer — lets you message it, stop it or move it to the background, and
7
+ has the main agent continue the subagent that did the work instead of starting from nothing.
8
+
9
+ Part of [opencode-cockpit](https://github.com/Codestz/opencode-cockpit). Install it on its own, or
10
+ through the bundle. Works on OpenCode 1.18+ and 2.0.15+.
11
+
12
+ ```sh
13
+ opencode plugin @opencode-cockpit/subagents@0.7.0 --global --force # OpenCode 1
14
+ opencode plugin add @opencode-cockpit/subagents@0.7.0 # OpenCode 2
15
+ ```
16
+
17
+ ## What it does
18
+
19
+ **In the sidebar**, a Subagents block: each subagent in this conversation, its type and task, and
20
+ under it what it is doing now — `grep "session" src/auth/**`, `thinking`, `waiting for permission`,
21
+ `done · 2 rounds`, `cancelled` — with how many calls and how long on the right. A subagent that
22
+ launched its own has them indented under it.
23
+
24
+ **Click one** — or `ctrl+x w`, or `/subagents` — and its run opens in a pane on the right, half the
25
+ window or all of it: its model and who launched it, the task, then the run. A shell command or a file
26
+ change is a box with its output (ten lines, sixty open, all with `a`); reads and searches are one quiet
27
+ line each; thinking folds; the answer is drawn as markdown.
28
+
29
+ | Key | |
30
+ | --- | --- |
31
+ | `j` `k` | Move the cursor through the run's items |
32
+ | `enter` · a click | Open or fold the item under it |
33
+ | `e` | Open, or fold, every call |
34
+ | `a` | A call's whole output — open shows its first 60 lines, whole up to 2,000 |
35
+ | `t` | Show or hide thinking — shown by default, and remembered |
36
+ | `m` | Write it a message, at the foot of the pane (pasting works) |
37
+ | `x` | Stop it (press twice) — or, once it has finished, remove it from the list |
38
+ | `X` | Remove every finished subagent from the list |
39
+ | `b` | Move it to the background, so the main agent carries on (OpenCode's own `ctrl+b`) |
40
+ | `i` | Details: model, what it is denied, calls by tool, tokens, cost |
41
+ | `w` | Half the window, or all of it (remembered) |
42
+ | `[` `]` | Another subagent of this conversation |
43
+ | `d` `u` · `g` `G` | Page down · up · to the start · follow the run |
44
+ | `esc` `q` | Back to the conversation |
45
+
46
+ **Follow-ups keep their context.** The main agent is asked to continue the subagent that did the work
47
+ (`task_id` on OpenCode 1, `sessionID` on 2) rather than launch a new one, and has a `subagents_list`
48
+ tool: each subagent's id, task, state and last answer — saying when one was cancelled, or ended on a
49
+ progress note rather than an answer. Each round shows in the pane under a "Round N" rule.
50
+
51
+ **Message it.** A subagent that is still working picks your message up in its current run and answers
52
+ it in its report, so the main agent sees it too. A finished one wakes up and answers you, and Cockpit
53
+ adds the exchange to the main conversation without starting a turn there, so the main agent knows it
54
+ next time. A message it finished without reading comes back to the field.
55
+
56
+ **Stop and remove.** `x` twice stops a working subagent — and first tells the main agent you stopped
57
+ it on purpose, so it reports the stop instead of launching the subagent again. `x` on a finished one
58
+ removes it from the list, `X` removes every finished one; the sessions stay in OpenCode.
59
+
60
+ **Background subagents.** The main agent is asked to launch independent subagents with
61
+ `background: true` when its tool offers it, so the conversation keeps going. OpenCode 2 offers it
62
+ always; **OpenCode 1 only when started with `OPENCODE_EXPERIMENTAL_BACKGROUND_SUBAGENTS=true`** — a
63
+ plugin cannot set that for you. `b` moves one already running in the foreground, the same way.
64
+
65
+ ```sh
66
+ export OPENCODE_EXPERIMENTAL_BACKGROUND_SUBAGENTS=true # in ~/.zshrc, then start OpenCode
67
+ ```
68
+
69
+ ## Settings
70
+
71
+ In the bundle's entry (`"subagents": { … }`) or this package's own:
72
+
73
+ | Setting | Default | |
74
+ | --- | --- | --- |
75
+ | `sidebarRows` | `6` | Subagents shown before the rest fold into a count — working ones first |
76
+ | `hideFinishedAfter` | unset | Minutes a finished subagent stays in the sidebar; unset keeps it for the conversation |
77
+ | `sidebarOrder` | `150` | Where the block sits in the sidebar; lower draws first |
78
+ | `keybinds` | `{ "cockpit.subagents.open": "<leader>w" }` | The key that opens the latest one |
79
+ | `guidance` | `true` | Tell the main agent about background subagents and follow-ups (agent side) |
80
+
81
+ ## See it without OpenCode
82
+
83
+ ```sh
84
+ bunx @opencode-cockpit/subagents preview
85
+ ```
86
+
87
+ Draws the sidebar block and the pane from a sample run, in your terminal.
88
+
89
+ ## Troubleshooting
90
+
91
+ ```sh
92
+ npx opencode-cockpit@latest doctor
93
+ ```
94
+
95
+ checks OpenCode, its config, Cockpit's logs and the daemon, and prints the fix for anything wrong —
96
+ on OpenCode 1 and 2, and when Cockpit will not load at all ([what it checks](https://codestz.github.io/opencode-cockpit/help/doctor/)).
97
+
98
+ Everything Cockpit does inside OpenCode goes to one file — which OpenCode loaded which bay, and every
99
+ error with its stack:
100
+
101
+ ```sh
102
+ tail -50 ~/.cache/opencode-cockpit/cockpit.log
103
+ ```
104
+
105
+ `COCKPIT_DEBUG=1 opencode` adds the detail. [Troubleshooting](https://codestz.github.io/opencode-cockpit/help/troubleshooting/) covers
106
+ the failures people hit and what to attach to an issue; [OpenCode 1 and 2](https://codestz.github.io/opencode-cockpit/start/opencode-versions/)
107
+ covers what differs between the two.
@@ -0,0 +1,134 @@
1
+ import { tool } from "@opencode-ai/plugin";
2
+ import { claimFeature, duplicateFeatureMessage } from "@opencode-cockpit/client";
3
+ import { dualServer } from "@opencode-cockpit/client/server";
4
+ import { createV1Translator } from "../core/adapt/v1.js";
5
+ import { createV2Translator } from "../core/adapt/v2.js";
6
+ import { applyAll, emptyModel, subagentsOf } from "../core/model/model.js";
7
+ import { subagentReport } from "../core/view/report.js";
8
+
9
+ /**
10
+ * What the agent is told about subagents, once per request.
11
+ *
12
+ * Measured on both OpenCodes (docs/opencode/agents.md): a subagent launched in the background lets the
13
+ * conversation carry on, and the main agent is told when it finishes. OpenCode 2 offers it on its
14
+ * `subagent` tool; OpenCode 1 only with `OPENCODE_EXPERIMENTAL_BACKGROUND_SUBAGENTS=true`, which a
15
+ * plugin cannot set — so the guidance says "if your tool offers it", and never promises it.
16
+ *
17
+ * Continuing a subagent was measured too: given its id, the same subagent picks up with everything it
18
+ * already read, and answers in seconds. Main agents rarely do it on their own — the id scrolls away —
19
+ * so the guidance asks for it, and `subagents_list` hands the ids back.
20
+ */
21
+ export const GUIDANCE = `## Subagents (opencode-cockpit)
22
+ When 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.
23
+ When 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 — it keeps its context. subagents_list gives each subagent's id, task and last answer.
24
+ The 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.`;
25
+ const LIST = `List the subagents of this conversation: each one's id, agent, task, state and last answer.
26
+
27
+ Use it before following up on work a subagent did, to continue that same subagent by its id instead of starting a new one — it keeps everything it already read and tried.`;
28
+ export const SUBAGENTS_PACKAGE = "@opencode-cockpit/subagents";
29
+ /**
30
+ * What the agent side keeps of each run: enough to list it — not what calls printed, nor thinking, so
31
+ * a long session does not hold every file its subagents read.
32
+ */
33
+ export function slim(changes) {
34
+ const out = [];
35
+ for (const change of changes) {
36
+ if (change.type === "thinking") continue;
37
+ if (change.type === "tool") {
38
+ const {
39
+ output: _output,
40
+ input: _input,
41
+ ...rest
42
+ } = change;
43
+ out.push(rest);
44
+ } else out.push(change);
45
+ }
46
+ return out;
47
+ }
48
+
49
+ /**
50
+ * The conversation's subagents: what the events said, and on OpenCode 1 any from before we started —
51
+ * with their history, read once. Listed bare, those read "working now · 0 calls" to the main agent,
52
+ * which then waits on work long finished, or does it again.
53
+ */
54
+ async function nodesFor(host, model, history, sessionID) {
55
+ const listed = (await host.session.children?.(sessionID).catch(() => [])) ?? [];
56
+ const missing = listed.filter(child => (model.sessions.get(child.id)?.entries.length ?? 0) === 0);
57
+ for (const child of missing) {
58
+ const at = child.time?.updated ?? Date.now();
59
+ const messages = (await host.session.messages?.(child.id).catch(() => [])) ?? [];
60
+ applyAll(model, [{
61
+ type: "session",
62
+ id: child.id,
63
+ parentID: sessionID,
64
+ ...(child.title ? {
65
+ title: child.title
66
+ } : {}),
67
+ at
68
+ }, ...slim(history(messages)), {
69
+ type: "status",
70
+ id: child.id,
71
+ status: "idle",
72
+ at
73
+ }]);
74
+ }
75
+ return subagentsOf(model, sessionID);
76
+ }
77
+
78
+ /** The agent side as a factory, so the `opencode-cockpit` bundle can include it. */
79
+ export function createSubagentsServer({
80
+ source = SUBAGENTS_PACKAGE
81
+ } = {}) {
82
+ return async (host, rawOptions) => {
83
+ const claim = claimFeature(host.scope, "subagents", source);
84
+ if (!claim.active) {
85
+ host.log.warn(duplicateFeatureMessage("Subagents", claim.owner, source));
86
+ return {};
87
+ }
88
+ const log = host.log.child("subagents");
89
+ const options = rawOptions ?? {};
90
+ const model = emptyModel();
91
+ const v1 = host.version === 1 ? createV1Translator() : undefined;
92
+ const translate = v1 ?? createV2Translator();
93
+ const history = messages => v1?.history(messages) ?? [];
94
+ const list = tool({
95
+ description: LIST,
96
+ args: {},
97
+ async execute(_args, context) {
98
+ const nodes = await nodesFor(host, model, history, context.sessionID);
99
+ log.debug("list", {
100
+ sessionID: context.sessionID,
101
+ count: nodes.length
102
+ });
103
+ return subagentReport({
104
+ nodes,
105
+ now: Date.now(),
106
+ version: host.version
107
+ });
108
+ }
109
+ });
110
+ return {
111
+ ...(options.guidance === false ? {} : {
112
+ system: async () => [GUIDANCE]
113
+ }),
114
+ tools: {
115
+ subagents_list: list
116
+ },
117
+ event: event => {
118
+ try {
119
+ const changes = slim(translate.event(event));
120
+ if (changes.length > 0) applyAll(model, changes);
121
+ } catch (error) {
122
+ log.warn("event failed", {
123
+ error
124
+ });
125
+ }
126
+ },
127
+ sessionDeleted: async sessionID => {
128
+ model.sessions.delete(sessionID);
129
+ },
130
+ dispose: () => claim.release()
131
+ };
132
+ };
133
+ }
134
+ export default dualServer("opencode-cockpit.subagents", createSubagentsServer());
@@ -0,0 +1,99 @@
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
+ import { applyAll, emptyModel, subagentsOf } from "../core/model/model.js";
11
+ import { SAMPLE_NOW, SAMPLE_ROOT, sample } from "../core/sample.js";
12
+ import { screenRows } from "../core/view/screen.js";
13
+ import { sidebarLines } from "../core/view/sidebar.js";
14
+
15
+ /** OpenCode's default theme, measured (docs/opencode/v2.md). */
16
+ const HEX = {
17
+ text: "#eeeeee",
18
+ muted: "#808080",
19
+ accent: "#9d7cd8",
20
+ info: "#56b6c2",
21
+ tool: "#fab283",
22
+ success: "#7fd88f",
23
+ error: "#e06c75",
24
+ warning: "#f5a742",
25
+ border: "#484848"
26
+ };
27
+ const FILL = {
28
+ band: "#141414",
29
+ block: "#1e1e1e",
30
+ card: "#1e1e1e",
31
+ selected: "#141414"
32
+ };
33
+ const rgb = hex => [1, 3, 5].map(i => Number.parseInt(hex.slice(i, i + 2), 16)).join(";");
34
+ const color = process.stdout.isTTY && !process.env.NO_COLOR;
35
+ function paint(row) {
36
+ if (!color) return row.map(run => run.text).join("");
37
+ return row.map(run => {
38
+ const codes = [`38;2;${rgb(HEX[run.tone ?? "text"])}`];
39
+ if (run.fill && run.fill !== "none") codes.push(`48;2;${rgb(FILL[run.fill])}`);
40
+ if (run.bold) codes.push("1");
41
+ if (run.faint) codes.push("2");
42
+ return `\x1b[${codes.join(";")}m${run.text}\x1b[0m`;
43
+ }).join("");
44
+ }
45
+ const args = process.argv.slice(2);
46
+ if (args.includes("--help") || args.includes("-h")) {
47
+ process.stdout.write("Usage: subagents preview [--width <sidebar columns>]\n");
48
+ process.exit(0);
49
+ }
50
+ const at = args.indexOf("--width");
51
+ const sidebarWidth = at >= 0 ? Number(args[at + 1]) || 36 : 36;
52
+ const columns = Math.max(60, Math.min(process.stdout.columns || 100, 140));
53
+ const model = applyAll(emptyModel(), sample());
54
+ const nodes = subagentsOf(model, SAMPLE_ROOT);
55
+ const out = ["", "Sidebar", ""];
56
+ for (const line of sidebarLines({
57
+ nodes,
58
+ width: sidebarWidth,
59
+ now: SAMPLE_NOW,
60
+ frame: 2
61
+ })) out.push(paint(line.row));
62
+ const first = nodes[0]?.session;
63
+ if (first) {
64
+ const base = {
65
+ session: first,
66
+ nodes,
67
+ launcher: "build",
68
+ width: columns,
69
+ height: 30,
70
+ now: SAMPLE_NOW,
71
+ frame: 2,
72
+ open: new Set(),
73
+ closed: new Set(),
74
+ thinking: false,
75
+ details: false
76
+ };
77
+ out.push("", "The pane — the first subagent", "");
78
+ const folded = screenRows(base);
79
+ for (const row of folded.rows) out.push(paint(row));
80
+ /** The same, with the cursor on the first call and that call open. */
81
+ const call = folded.keys.find(key => key.startsWith("tool:"));
82
+ if (call) {
83
+ out.push("", "A call, selected and open", "");
84
+ const opened = screenRows({
85
+ ...base,
86
+ selected: call,
87
+ open: new Set([call]),
88
+ top: 0
89
+ });
90
+ for (const row of opened.rows) out.push(paint(row));
91
+ }
92
+ out.push("", "Details", "");
93
+ for (const row of screenRows({
94
+ ...base,
95
+ details: true,
96
+ height: 20
97
+ }).rows) out.push(paint(row));
98
+ }
99
+ process.stdout.write(`${out.join("\n")}\n\n`);