@opencode-cockpit/subagents 0.7.0 → 0.7.1

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 CHANGED
@@ -10,8 +10,8 @@ Part of [opencode-cockpit](https://github.com/Codestz/opencode-cockpit). Install
10
10
  through the bundle. Works on OpenCode 1.18+ and 2.0.15+.
11
11
 
12
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
13
+ opencode plugin @opencode-cockpit/subagents@0.7.1 --global --force # OpenCode 1
14
+ opencode plugin add @opencode-cockpit/subagents@0.7.1 # OpenCode 2
15
15
  ```
16
16
 
17
17
  ## What it does
@@ -360,12 +360,14 @@ export function createV2Translator(unknown = () => {}) {
360
360
  counted = true;
361
361
  }
362
362
  }
363
+ /** Stamped with the run's own last moment: "now" made a reopened run look like it ended now. */
364
+ const latest = out.reduce((most, change) => Math.max(most, change.at), 0) || at;
363
365
  if (counted) out.push({
364
366
  type: "usage",
365
367
  id,
366
368
  tokens,
367
369
  cost,
368
- at
370
+ at: latest
369
371
  });
370
372
  return out;
371
373
  },
@@ -47,8 +47,8 @@ function applyStatus(s, change) {
47
47
  if (change.error) s.error = change.error;
48
48
  settle(s, change.at);
49
49
  } else if (s.status !== "failed") {
50
- /** Idle before it ever worked is a session that has not started, not one that finished. */
51
- s.status = s.status === "starting" && s.entries.length === 0 ? "starting" : "done";
50
+ /** Idle before it ever worked is a session that has not started, not one that finished — live. */
51
+ s.status = s.status === "starting" && s.entries.length === 0 && !change.settled ? "starting" : "done";
52
52
  s.ended = change.at;
53
53
  settle(s, change.at);
54
54
  }
package/dist/tui/index.js CHANGED
@@ -621,6 +621,21 @@ export function createSubagentsTui({
621
621
  const session = opened();
622
622
  if (!session) return;
623
623
  if (!busy(session) && session.status !== "waiting") return remove([session.id]);
624
+ /**
625
+ * Stopping is only for a run OpenCode says is going. One Cockpit had wrong — shown running after a
626
+ * reopen — was "stopped", and the main agent was told about work that had long ended.
627
+ */
628
+ const said = feed.check(session.id);
629
+ if (said.some(change => change.type === "status" && change.status !== "busy" && change.status !== "waiting")) {
630
+ applyAll(model, said);
631
+ log.info("stop skipped: not running", {
632
+ id: session.id
633
+ });
634
+ return set({
635
+ notice: `${session.agent} had already finished — nothing to stop. x again removes it.`,
636
+ stopping: undefined
637
+ });
638
+ }
624
639
  if (surface.stopping !== session.id) {
625
640
  surface.stopping = session.id;
626
641
  surface.notice = `Press x again to stop ${session.agent}.`;
@@ -16,14 +16,30 @@ const V1_EVENTS = ["session.created", "session.updated", "session.status", "sess
16
16
  */
17
17
  // biome-ignore lint/suspicious/noExplicitAny: see above — every access is to a measured field
18
18
 
19
+ /** The last moment a stored run did anything: its latest change, or `fallback` when it has none. */
20
+ export function endOf(changes, fallback) {
21
+ let last = 0;
22
+ for (const change of changes) {
23
+ last = Math.max(last, change.at);
24
+ if (change.type === "tool" && change.ended !== undefined) last = Math.max(last, change.ended);
25
+ }
26
+ return last || fallback;
27
+ }
19
28
  export function createSource(api, log, emit) {
20
- /** Each unexpected shape once, not once per event. */
29
+ /**
30
+ * Each unexpected shape once, not once per event. An event we do not use is not news — OpenCode 2
31
+ * hands every plugin every event in the app (`provider.updated`, `session.viewed`…), and as
32
+ * warnings they filled doctor's report. A part of a kind we did not expect inside an event we do
33
+ * use still is: that is a shape that changed under us.
34
+ */
21
35
  const told = new Set();
22
36
  const unknown = (what, detail) => {
23
37
  const key = `${what} ${JSON.stringify(detail)}`;
24
38
  if (told.has(key)) return;
25
39
  told.add(key);
26
- log.warn("unrecognised event", {
40
+ if (what === "event") log.debug("unused event", {
41
+ ...detail
42
+ });else log.warn("unrecognised event", {
27
43
  what,
28
44
  ...detail
29
45
  });
@@ -64,17 +80,39 @@ export function createSource(api, log, emit) {
64
80
  for (const child of Array.isArray(children) ? children : []) {
65
81
  const id = child.id;
66
82
  const messages = v1.state.session.messages(id) ?? [];
67
- const history = messages.map(info => ({
83
+ let history = messages.map(info => ({
68
84
  info,
69
85
  parts: v1.state.part(info.id) ?? []
70
86
  }));
87
+ /**
88
+ * The interface's store holds the messages of sessions it has loaded, which in a long
89
+ * conversation is not every subagent. Read from the server when it has none.
90
+ */
91
+ if (history.length === 0) {
92
+ const fetched = await v1.client.session.messages({
93
+ sessionID: id
94
+ }).catch(error => {
95
+ log.warn("history fetch failed", {
96
+ id,
97
+ error
98
+ });
99
+ return undefined;
100
+ });
101
+ const list = fetched?.data ?? fetched ?? [];
102
+ if (Array.isArray(list)) history = list;
103
+ log.debug("history fetched", {
104
+ id,
105
+ messages: history.length
106
+ });
107
+ }
71
108
  const status = v1.state.session.status(id);
72
109
  /**
73
110
  * Busy only when the host says so. A finished subagent is not in the host's status store at
74
111
  * all — reading "no status" as "unknown" left every old subagent running forever.
75
112
  */
76
113
  const busy = status?.type === "busy" || status?.type === "retry";
77
- emit([...translate.session(child), ...translate.history(history), busy ? {
114
+ const past = translate.history(history);
115
+ emit([...translate.session(child), ...past, busy ? {
78
116
  type: "status",
79
117
  id,
80
118
  status: "busy",
@@ -83,7 +121,8 @@ export function createSource(api, log, emit) {
83
121
  type: "status",
84
122
  id,
85
123
  status: "idle",
86
- at: Number(child.time?.updated) || Date.now()
124
+ settled: true,
125
+ at: endOf(past, Number(child.time?.updated) || Date.now())
87
126
  }]);
88
127
  await visit(id, depth + 1);
89
128
  }
@@ -137,14 +176,19 @@ export function createSource(api, log, emit) {
137
176
  });
138
177
  },
139
178
  check(id) {
179
+ /**
180
+ * OpenCode 1 keeps a status only while a session works: a finished one has none (measured on a
181
+ * reopened conversation). Read as "unknown", a subagent stuck at running was never corrected.
182
+ */
140
183
  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
- }) : [];
184
+ if (status?.type === "busy" || status?.type === "retry") return [];
185
+ return [{
186
+ type: "status",
187
+ id,
188
+ status: "idle",
189
+ settled: true,
190
+ at: Date.now()
191
+ }];
148
192
  },
149
193
  dispose: () => {
150
194
  for (const off of offs.splice(0)) off();
@@ -171,13 +215,20 @@ export function createSource(api, log, emit) {
171
215
  }));
172
216
  messages = v2.data.session.message.list(id) ?? [];
173
217
  }
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 : [{
218
+ const past = translate.history(id, messages);
219
+ /** When its work ended, not when the store was last touched — reopening touches it. */
220
+ const ended = endOf(past, Number(info.time?.updated) || Date.now());
221
+ const status = translate.status(id, v2.data.session.status(id), ended);
222
+ emit([...translate.session(info), ...past, /** Not known to the host is not running: the same lesson as OpenCode 1's store. */
223
+ ...(status.length > 0 ? status.map(change => ({
224
+ ...change,
225
+ settled: true
226
+ })) : [{
177
227
  type: "status",
178
228
  id,
179
229
  status: "idle",
180
- at: Number(info.time?.updated) || Date.now()
230
+ settled: true,
231
+ at: ended
181
232
  }])]);
182
233
  }
183
234
  },
@@ -219,7 +270,10 @@ export function createSource(api, log, emit) {
219
270
  });
220
271
  },
221
272
  check(id) {
222
- return translate.status(id, v2.data.session.status(id));
273
+ return translate.status(id, v2.data.session.status(id)).map(change => ({
274
+ ...change,
275
+ settled: true
276
+ }));
223
277
  },
224
278
  dispose: () => {
225
279
  for (const each of offs.splice(0)) each();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@opencode-cockpit/subagents",
3
- "version": "0.7.0",
3
+ "version": "0.7.1",
4
4
  "description": "See what your subagents are doing: in the sidebar, full screen, and message them",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -48,7 +48,7 @@
48
48
  "access": "public"
49
49
  },
50
50
  "dependencies": {
51
- "@opencode-cockpit/client": "0.7.0",
51
+ "@opencode-cockpit/client": "0.7.1",
52
52
  "@opencode-ai/plugin": "1.18.31"
53
53
  },
54
54
  "devDependencies": {
@@ -37,6 +37,12 @@ export type Change =
37
37
  id: string;
38
38
  status: "busy" | "idle" | "failed" | "waiting";
39
39
  error?: string;
40
+ /**
41
+ * Said about a stored run — loaded, or checked after it went quiet — rather than live. Idle then
42
+ * means finished even with nothing recorded: a subagent whose history did not load is not one
43
+ * about to start, and read that way it showed as running for good.
44
+ */
45
+ settled?: boolean;
40
46
  at: number;
41
47
  }
42
48
  /** Something said *to* it: the first is the task it was given, the rest are messages. */
@@ -42,4 +42,6 @@ export interface Source {
42
42
  check: (id: string) => Change[];
43
43
  dispose: () => void;
44
44
  }
45
+ /** The last moment a stored run did anything: its latest change, or `fallback` when it has none. */
46
+ export declare function endOf(changes: readonly Change[], fallback: number): number;
45
47
  export declare function createSource(api: Host, log: Log, emit: (changes: Change[]) => void): Source;