opencode-courier 0.13.0 → 0.13.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/README.md CHANGED
@@ -18,7 +18,7 @@ A narrated tour of the terminal UI: the tree panel, the asks inbox, failing chil
18
18
  ## Quickstart
19
19
 
20
20
  ```bash
21
- npm install -g @opencode/cli@2.2.0 # OpenCode V2, the version courier is tested on
21
+ npm install -g @opencode/cli@2.2.2 # OpenCode V2, the version courier is tested on
22
22
  opencode plugin add opencode-courier
23
23
  opencode
24
24
  ```
@@ -62,10 +62,10 @@ split a task and how to check and merge the parts. Every detail: [the reference]
62
62
  ## Install
63
63
 
64
64
  Requires OpenCode V2: the `opencode` command from `@opencode/cli`. Each release is tested against
65
- one OpenCode version; this one is tested on **OpenCode 2.2.0** (check with `opencode --version`).
65
+ one OpenCode version; this one is tested on **OpenCode 2.2.2** (check with `opencode --version`).
66
66
 
67
67
  ```bash
68
- npm install -g @opencode/cli@2.2.0
68
+ npm install -g @opencode/cli@2.2.2
69
69
  opencode plugin add opencode-courier
70
70
  ```
71
71
 
@@ -78,13 +78,13 @@ name, without a version: OpenCode offers updates only for an entry without an ex
78
78
  <!-- compat-table:start rows=3: generated by script/compat-table.ts from the release tags -->
79
79
  | opencode-courier | OpenCode V2 (`opencode` and `@opencode/plugin`) |
80
80
  |---|---|
81
+ | 0.13.2 | 2.2.2 |
82
+ | 0.13.1 | 2.2.1 |
81
83
  | 0.13.0 | 2.2.0 |
82
- | 0.11.1, 0.12.0 | 2.1.4 |
83
- | 0.11.0 | 2.1.3 |
84
84
  <!-- compat-table:end -->
85
85
 
86
86
  Every release: [the full table](docs/reference.md#the-opencode-version). The server part loads on
87
- any V2 host from 2.0.4 on. The terminal part is tested on OpenCode 2.2.0 only.
87
+ any V2 host from 2.0.4 on. The terminal part is tested on OpenCode 2.2.2 only.
88
88
 
89
89
  ## Options
90
90
 
@@ -128,7 +128,7 @@ Parameters and behaviour: [the reference](docs/reference.md#the-tools).
128
128
  ## In the terminal
129
129
 
130
130
  - **Footer:** `courier 1 waiting on you · 2 running · group pair 1/2`.
131
- - **`/courier tree`:** a panel with the tree. `enter` opens a session, `s` stops it, `q` closes.
131
+ - **`/courier tree`:** a panel with the tree, the session it was opened in as its first row. `enter` opens a session, `s` stops it, `q` closes.
132
132
  - **`/courier stop <session>`:** stops a session and everything under it.
133
133
  - **`ctrl+x k`** or **`/asks`:** every ask waiting on you in the tree, answered in a dialog.
134
134
  - **`/courier memory`:** the project's tree records. `p` pins one, `x` forgets one.
@@ -24,11 +24,11 @@ export declare function readNativeChildren(options: unknown): {
24
24
  };
25
25
  /** A session's times, as `session.get` gives them. */
26
26
  type Times = Awaited<ReturnType<Context["session"]["get"]>>["time"];
27
- /** A session's times as `busy` reads them. */
28
- export type BusyTimes = Pick<Times, "updated" | "idle">;
27
+ /** A session's times as `busy` reads them; `busy`, when its latest execution started, is stamped since OpenCode 2.2.2. */
28
+ export type BusyTimes = Pick<Times, "updated" | "idle" | "busy">;
29
29
  /**
30
- * Whether a session may run by its times: it has never finished a turn, or `updated` was stamped after the last one ended, as
31
- * a step does, and as much that is no activity does (a rename, an archive, a waiting inbox item), so only its transcript tells.
30
+ * Whether a session may run by its times: it has never finished a turn, or its latest execution started after the last one ended
31
+ * (`busy`; without it `updated`, which a rename, an archive or a waiting inbox item also stamp); only its transcript then tells.
32
32
  */
33
33
  export declare const busy: (time: BusyTimes) => boolean;
34
34
  /** A stored message as `turnEnd` reads it: an assistant message's `error` says how its step ended. */
@@ -24,10 +24,10 @@ export function readNativeChildren(options) {
24
24
  return { nativeChildren: false, problems: [`nativeChildren must be true or false, not ${JSON.stringify(value)}; using false`] };
25
25
  }
26
26
  /**
27
- * Whether a session may run by its times: it has never finished a turn, or `updated` was stamped after the last one ended, as
28
- * a step does, and as much that is no activity does (a rename, an archive, a waiting inbox item), so only its transcript tells.
27
+ * Whether a session may run by its times: it has never finished a turn, or its latest execution started after the last one ended
28
+ * (`busy`; without it `updated`, which a rename, an archive or a waiting inbox item also stamp); only its transcript then tells.
29
29
  */
30
- export const busy = (time) => time.idle === undefined || millis(time.updated) > millis(time.idle);
30
+ export const busy = (time) => time.idle === undefined || millis(time.busy ?? time.updated) > millis(time.idle);
31
31
  /** The messages the person's switch of agent or model, move or shell command appends, at rest or mid-turn alike. */
32
32
  const ASIDES = new Set(["agent-switched", "model-switched", "location-switched", "shell"]);
33
33
  /**
@@ -13,8 +13,8 @@ export declare const waitOf: (pending: Pending) => Wait;
13
13
  /** A form a session shows, as the panel says what it waits on. */
14
14
  export declare const formWait: (form: ShownForm) => Wait;
15
15
  /**
16
- * The tree panel's rows: the sessions under a session as courier_tree lists them, at most `maxTotal`, the live ones
17
- * first, then the newest, each as `row` reads it; `truncated` counts the older ones left out, and `stalled` says the root is.
16
+ * The tree panel's rows: the sessions under a session as courier_tree lists them, at most `maxTotal`, live ones first,
17
+ * then the newest, each as `row` reads it; `top` the session itself, `truncated` the ones left out, `stalled` its stall.
18
18
  */
19
19
  export declare function panel(ports: CourierPorts, rootID: string): Promise<Tree>;
20
20
  export {};
@@ -2,7 +2,7 @@ import { startedAs } from "./domain.js";
2
2
  import { lastStatus, stoppedSincePrompt } from "./report.js";
3
3
  import { standingIn } from "./status.js";
4
4
  import { childStore } from "./store.js";
5
- import { readTree } from "./subtree.js";
5
+ import { readTop, readTree } from "./subtree.js";
6
6
  import { stalledAmong } from "./wake.js";
7
7
  /** The last tool call in a session's messages, and when it was made. */
8
8
  export function lastTool(messages) {
@@ -91,14 +91,17 @@ async function row(ports, windows, { entry, depth, info, running, endedAt, pendi
91
91
  };
92
92
  }
93
93
  /**
94
- * The tree panel's rows: the sessions under a session as courier_tree lists them, at most `maxTotal`, the live ones
95
- * first, then the newest, each as `row` reads it; `truncated` counts the older ones left out, and `stalled` says the root is.
94
+ * The tree panel's rows: the sessions under a session as courier_tree lists them, at most `maxTotal`, live ones first,
95
+ * then the newest, each as `row` reads it; `top` the session itself, `truncated` the ones left out, `stalled` its stall.
96
96
  */
97
97
  export async function panel(ports, rootID) {
98
- const { kept, leftOut } = await readTree(ports, rootID);
98
+ const [{ kept, leftOut }, root] = await Promise.all([readTree(ports, rootID), readTop(ports, rootID)]);
99
99
  const stalled = await stalledAmong(ports, [rootID, ...kept.map((node) => node.entry.sessionID)]);
100
100
  const windows = await ports.models().catch(() => []);
101
- const rows = await Promise.all(kept.map((node) => row(ports, windows, node, stalled.get(node.entry.sessionID))));
101
+ const [rows, top] = await Promise.all([
102
+ Promise.all(kept.map((node) => row(ports, windows, node, stalled.get(node.entry.sessionID)))),
103
+ root && row(ports, windows, root, stalled.get(rootID)),
104
+ ]);
102
105
  const own = stalled.get(rootID);
103
- return { rows, ...(leftOut ? { truncated: leftOut } : {}), ...(own !== undefined ? { stalled: own } : {}) };
106
+ return { rows, ...(top ? { top } : {}), ...(leftOut ? { truncated: leftOut } : {}), ...(own !== undefined ? { stalled: own } : {}) };
104
107
  }
@@ -96,6 +96,48 @@ export declare function registerTreeRpc(rpc: Pick<Plugin.Context["rpc"], "regist
96
96
  readonly resumes?: number | undefined;
97
97
  }[];
98
98
  readonly stalled?: number | undefined;
99
+ readonly top?: {
100
+ readonly sessionID: string;
101
+ readonly title: string;
102
+ readonly depth: number;
103
+ readonly isolated: boolean;
104
+ readonly parentID: string;
105
+ readonly created: number;
106
+ readonly state: "gone" | "stalled" | "running" | "idle" | "waiting";
107
+ readonly waits: readonly ({
108
+ readonly type: "permission";
109
+ readonly action: string;
110
+ readonly resources: readonly string[];
111
+ } | {
112
+ readonly type: "question";
113
+ readonly question: string;
114
+ } | {
115
+ readonly type: "form";
116
+ readonly title: string;
117
+ })[];
118
+ readonly stopped?: boolean | undefined;
119
+ readonly stalled?: number | undefined;
120
+ readonly group?: {
121
+ readonly name: string;
122
+ readonly standing: string;
123
+ } | undefined;
124
+ readonly cost?: number | undefined;
125
+ readonly context?: number | undefined;
126
+ readonly outcome?: "failed" | "interrupted" | "succeeded" | undefined;
127
+ readonly report?: {
128
+ readonly progressed: boolean;
129
+ readonly state: "ended" | "reported" | "owed";
130
+ readonly status?: string | undefined;
131
+ } | undefined;
132
+ readonly idle?: number | undefined;
133
+ readonly native?: boolean | undefined;
134
+ readonly model?: string | undefined;
135
+ readonly tool?: {
136
+ readonly name: string;
137
+ readonly at: number;
138
+ } | undefined;
139
+ readonly resumes?: number | undefined;
140
+ } | undefined;
99
141
  readonly truncated?: number | undefined;
100
142
  }, {
101
143
  readonly rows: readonly {
@@ -141,6 +183,48 @@ export declare function registerTreeRpc(rpc: Pick<Plugin.Context["rpc"], "regist
141
183
  readonly resumes?: number | undefined;
142
184
  }[];
143
185
  readonly stalled?: number | undefined;
186
+ readonly top?: {
187
+ readonly sessionID: string;
188
+ readonly title: string;
189
+ readonly depth: number;
190
+ readonly isolated: boolean;
191
+ readonly parentID: string;
192
+ readonly created: number;
193
+ readonly state: "gone" | "stalled" | "running" | "idle" | "waiting";
194
+ readonly waits: readonly ({
195
+ readonly type: "permission";
196
+ readonly action: string;
197
+ readonly resources: readonly string[];
198
+ } | {
199
+ readonly type: "question";
200
+ readonly question: string;
201
+ } | {
202
+ readonly type: "form";
203
+ readonly title: string;
204
+ })[];
205
+ readonly stopped?: boolean | undefined;
206
+ readonly stalled?: number | undefined;
207
+ readonly group?: {
208
+ readonly name: string;
209
+ readonly standing: string;
210
+ } | undefined;
211
+ readonly cost?: number | undefined;
212
+ readonly context?: number | undefined;
213
+ readonly outcome?: "failed" | "interrupted" | "succeeded" | undefined;
214
+ readonly report?: {
215
+ readonly progressed: boolean;
216
+ readonly state: "ended" | "reported" | "owed";
217
+ readonly status?: string | undefined;
218
+ } | undefined;
219
+ readonly idle?: number | undefined;
220
+ readonly native?: boolean | undefined;
221
+ readonly model?: string | undefined;
222
+ readonly tool?: {
223
+ readonly name: string;
224
+ readonly at: number;
225
+ } | undefined;
226
+ readonly resumes?: number | undefined;
227
+ } | undefined;
144
228
  readonly truncated?: number | undefined;
145
229
  }> & import("effect/Schema").Struct<{
146
230
  readonly rows: import("effect/Schema").$Array<import("effect/Schema").Struct<{
@@ -185,6 +269,48 @@ export declare function registerTreeRpc(rpc: Pick<Plugin.Context["rpc"], "regist
185
269
  readonly title: import("effect/Schema").String;
186
270
  }>]>>;
187
271
  }>>;
272
+ readonly top: import("effect/Schema").optionalKey<import("effect/Schema").Struct<{
273
+ readonly sessionID: import("effect/Schema").String;
274
+ readonly parentID: import("effect/Schema").String;
275
+ readonly title: import("effect/Schema").String;
276
+ readonly depth: import("effect/Schema").Int;
277
+ readonly state: import("effect/Schema").Literals<readonly ["running", "waiting", "idle", "stalled", "gone"]>;
278
+ readonly stalled: import("effect/Schema").optionalKey<import("effect/Schema").Number>;
279
+ readonly outcome: import("effect/Schema").optionalKey<import("effect/Schema").Literals<readonly ["succeeded", "failed", "interrupted"]>>;
280
+ readonly resumes: import("effect/Schema").optionalKey<import("effect/Schema").Number>;
281
+ readonly stopped: import("effect/Schema").optionalKey<import("effect/Schema").Boolean>;
282
+ readonly report: import("effect/Schema").optionalKey<import("effect/Schema").Struct<{
283
+ readonly state: import("effect/Schema").Literals<readonly ["owed", "reported", "ended"]>;
284
+ readonly status: import("effect/Schema").optionalKey<import("effect/Schema").String>;
285
+ readonly progressed: import("effect/Schema").Boolean;
286
+ }>>;
287
+ readonly group: import("effect/Schema").optionalKey<import("effect/Schema").Struct<{
288
+ readonly name: import("effect/Schema").String;
289
+ readonly standing: import("effect/Schema").String;
290
+ }>>;
291
+ readonly isolated: import("effect/Schema").Boolean;
292
+ readonly native: import("effect/Schema").optionalKey<import("effect/Schema").Boolean>;
293
+ readonly model: import("effect/Schema").optionalKey<import("effect/Schema").String>;
294
+ readonly created: import("effect/Schema").Number;
295
+ readonly idle: import("effect/Schema").optionalKey<import("effect/Schema").Number>;
296
+ readonly cost: import("effect/Schema").optionalKey<import("effect/Schema").Number>;
297
+ readonly context: import("effect/Schema").optionalKey<import("effect/Schema").Number>;
298
+ readonly tool: import("effect/Schema").optionalKey<import("effect/Schema").Struct<{
299
+ readonly name: import("effect/Schema").String;
300
+ readonly at: import("effect/Schema").Number;
301
+ }>>;
302
+ readonly waits: import("effect/Schema").$Array<import("effect/Schema").Union<readonly [import("effect/Schema").Struct<{
303
+ readonly type: import("effect/Schema").Literal<"permission">;
304
+ readonly action: import("effect/Schema").String;
305
+ readonly resources: import("effect/Schema").$Array<import("effect/Schema").String>;
306
+ }>, import("effect/Schema").Struct<{
307
+ readonly type: import("effect/Schema").Literal<"question">;
308
+ readonly question: import("effect/Schema").String;
309
+ }>, import("effect/Schema").Struct<{
310
+ readonly type: import("effect/Schema").Literal<"form">;
311
+ readonly title: import("effect/Schema").String;
312
+ }>]>>;
313
+ }>>;
188
314
  readonly truncated: import("effect/Schema").optionalKey<import("effect/Schema").Int>;
189
315
  readonly stalled: import("effect/Schema").optionalKey<import("effect/Schema").Number>;
190
316
  }>;
@@ -149,6 +149,66 @@ declare const Tree: Schema.Struct<{
149
149
  readonly title: Schema.String;
150
150
  }>]>>;
151
151
  }>>;
152
+ /** The panel's own session, at depth 0; absent when OpenCode no longer knows it, and from an older server. */
153
+ readonly top: Schema.optionalKey<Schema.Struct<{
154
+ readonly sessionID: Schema.String;
155
+ readonly parentID: Schema.String;
156
+ readonly title: Schema.String;
157
+ /** A child of the panel's session is at depth 1. */
158
+ readonly depth: Schema.Int;
159
+ /**
160
+ * `waiting` on a request to the person or a form it shows them; `stalled` when a courier message waits in its inbox and
161
+ * waking it again did not start a turn; `gone` when OpenCode no longer knows it.
162
+ */
163
+ readonly state: Schema.Literals<readonly ["running", "waiting", "idle", "stalled", "gone"]>;
164
+ /** Since when its oldest message that did not wake it has waited, when it is stalled. */
165
+ readonly stalled: Schema.optionalKey<Schema.Number>;
166
+ /** How its last turn ended. */
167
+ readonly outcome: Schema.optionalKey<Schema.Literals<readonly ["succeeded", "failed", "interrupted"]>>;
168
+ /** When the provider limit that failed its last turn lifts; left out once that has passed, or when it runs again or was stopped. */
169
+ readonly resumes: Schema.optionalKey<Schema.Number>;
170
+ /** Set when courier_stop stopped it since its last prompt. */
171
+ readonly stopped: Schema.optionalKey<Schema.Boolean>;
172
+ /** Its report since its last prompt: still owed, arrived (held with its group or delivered), or its turn ended without one. */
173
+ readonly report: Schema.optionalKey<Schema.Struct<{
174
+ readonly state: Schema.Literals<readonly ["owed", "reported", "ended"]>;
175
+ readonly status: Schema.optionalKey<Schema.String>;
176
+ readonly progressed: Schema.Boolean;
177
+ }>>;
178
+ /** Its join group, and its standing there: `held`, `out`, how it left, or `released`. */
179
+ readonly group: Schema.optionalKey<Schema.Struct<{
180
+ readonly name: Schema.String;
181
+ readonly standing: Schema.String;
182
+ }>>;
183
+ readonly isolated: Schema.Boolean;
184
+ /** Set when it is OpenCode's subagent of its parent (a native child), which OpenCode's session lists leave out. */
185
+ readonly native: Schema.optionalKey<Schema.Boolean>;
186
+ /** The model it was started on, `provider/model` with `#variant` when one is set, when that is not its parent's. */
187
+ readonly model: Schema.optionalKey<Schema.String>;
188
+ readonly created: Schema.Number;
189
+ /** When its last turn ended, if it is not running. */
190
+ readonly idle: Schema.optionalKey<Schema.Number>;
191
+ /** What it has cost so far, in US dollars. */
192
+ readonly cost: Schema.optionalKey<Schema.Number>;
193
+ /** How full its context is, as a fraction of the model's window; left out when the model or its limit cannot be read. */
194
+ readonly context: Schema.optionalKey<Schema.Number>;
195
+ /** Its last tool call, and when it was made. */
196
+ readonly tool: Schema.optionalKey<Schema.Struct<{
197
+ readonly name: Schema.String;
198
+ readonly at: Schema.Number;
199
+ }>>;
200
+ readonly waits: Schema.$Array<Schema.Union<readonly [Schema.Struct<{
201
+ readonly type: Schema.Literal<"permission">;
202
+ readonly action: Schema.String;
203
+ readonly resources: Schema.$Array<Schema.String>;
204
+ }>, Schema.Struct<{
205
+ readonly type: Schema.Literal<"question">;
206
+ readonly question: Schema.String;
207
+ }>, Schema.Struct<{
208
+ readonly type: Schema.Literal<"form">;
209
+ readonly title: Schema.String;
210
+ }>]>>;
211
+ }>>;
152
212
  /** How many older sessions the listing left out, when it had more than `maxTotal`: the live ones come first, then the newest. */
153
213
  readonly truncated: Schema.optionalKey<Schema.Int>;
154
214
  /** Since when a courier message has waited in the panel's own session, when that session is stalled. */
@@ -275,6 +335,48 @@ export declare const treeRpc: {
275
335
  readonly resumes?: number | undefined;
276
336
  }[];
277
337
  readonly stalled?: number | undefined;
338
+ readonly top?: {
339
+ readonly sessionID: string;
340
+ readonly title: string;
341
+ readonly depth: number;
342
+ readonly isolated: boolean;
343
+ readonly parentID: string;
344
+ readonly created: number;
345
+ readonly state: "gone" | "stalled" | "running" | "idle" | "waiting";
346
+ readonly waits: readonly ({
347
+ readonly type: "permission";
348
+ readonly action: string;
349
+ readonly resources: readonly string[];
350
+ } | {
351
+ readonly type: "question";
352
+ readonly question: string;
353
+ } | {
354
+ readonly type: "form";
355
+ readonly title: string;
356
+ })[];
357
+ readonly stopped?: boolean | undefined;
358
+ readonly stalled?: number | undefined;
359
+ readonly group?: {
360
+ readonly name: string;
361
+ readonly standing: string;
362
+ } | undefined;
363
+ readonly cost?: number | undefined;
364
+ readonly context?: number | undefined;
365
+ readonly outcome?: "failed" | "interrupted" | "succeeded" | undefined;
366
+ readonly report?: {
367
+ readonly progressed: boolean;
368
+ readonly state: "ended" | "reported" | "owed";
369
+ readonly status?: string | undefined;
370
+ } | undefined;
371
+ readonly idle?: number | undefined;
372
+ readonly native?: boolean | undefined;
373
+ readonly model?: string | undefined;
374
+ readonly tool?: {
375
+ readonly name: string;
376
+ readonly at: number;
377
+ } | undefined;
378
+ readonly resumes?: number | undefined;
379
+ } | undefined;
278
380
  readonly truncated?: number | undefined;
279
381
  }, {
280
382
  readonly rows: readonly {
@@ -320,6 +422,48 @@ export declare const treeRpc: {
320
422
  readonly resumes?: number | undefined;
321
423
  }[];
322
424
  readonly stalled?: number | undefined;
425
+ readonly top?: {
426
+ readonly sessionID: string;
427
+ readonly title: string;
428
+ readonly depth: number;
429
+ readonly isolated: boolean;
430
+ readonly parentID: string;
431
+ readonly created: number;
432
+ readonly state: "gone" | "stalled" | "running" | "idle" | "waiting";
433
+ readonly waits: readonly ({
434
+ readonly type: "permission";
435
+ readonly action: string;
436
+ readonly resources: readonly string[];
437
+ } | {
438
+ readonly type: "question";
439
+ readonly question: string;
440
+ } | {
441
+ readonly type: "form";
442
+ readonly title: string;
443
+ })[];
444
+ readonly stopped?: boolean | undefined;
445
+ readonly stalled?: number | undefined;
446
+ readonly group?: {
447
+ readonly name: string;
448
+ readonly standing: string;
449
+ } | undefined;
450
+ readonly cost?: number | undefined;
451
+ readonly context?: number | undefined;
452
+ readonly outcome?: "failed" | "interrupted" | "succeeded" | undefined;
453
+ readonly report?: {
454
+ readonly progressed: boolean;
455
+ readonly state: "ended" | "reported" | "owed";
456
+ readonly status?: string | undefined;
457
+ } | undefined;
458
+ readonly idle?: number | undefined;
459
+ readonly native?: boolean | undefined;
460
+ readonly model?: string | undefined;
461
+ readonly tool?: {
462
+ readonly name: string;
463
+ readonly at: number;
464
+ } | undefined;
465
+ readonly resumes?: number | undefined;
466
+ } | undefined;
323
467
  readonly truncated?: number | undefined;
324
468
  }> & Schema.Struct<{
325
469
  readonly rows: Schema.$Array<Schema.Struct<{
@@ -381,6 +525,66 @@ export declare const treeRpc: {
381
525
  readonly title: Schema.String;
382
526
  }>]>>;
383
527
  }>>;
528
+ /** The panel's own session, at depth 0; absent when OpenCode no longer knows it, and from an older server. */
529
+ readonly top: Schema.optionalKey<Schema.Struct<{
530
+ readonly sessionID: Schema.String;
531
+ readonly parentID: Schema.String;
532
+ readonly title: Schema.String;
533
+ /** A child of the panel's session is at depth 1. */
534
+ readonly depth: Schema.Int;
535
+ /**
536
+ * `waiting` on a request to the person or a form it shows them; `stalled` when a courier message waits in its inbox and
537
+ * waking it again did not start a turn; `gone` when OpenCode no longer knows it.
538
+ */
539
+ readonly state: Schema.Literals<readonly ["running", "waiting", "idle", "stalled", "gone"]>;
540
+ /** Since when its oldest message that did not wake it has waited, when it is stalled. */
541
+ readonly stalled: Schema.optionalKey<Schema.Number>;
542
+ /** How its last turn ended. */
543
+ readonly outcome: Schema.optionalKey<Schema.Literals<readonly ["succeeded", "failed", "interrupted"]>>;
544
+ /** When the provider limit that failed its last turn lifts; left out once that has passed, or when it runs again or was stopped. */
545
+ readonly resumes: Schema.optionalKey<Schema.Number>;
546
+ /** Set when courier_stop stopped it since its last prompt. */
547
+ readonly stopped: Schema.optionalKey<Schema.Boolean>;
548
+ /** Its report since its last prompt: still owed, arrived (held with its group or delivered), or its turn ended without one. */
549
+ readonly report: Schema.optionalKey<Schema.Struct<{
550
+ readonly state: Schema.Literals<readonly ["owed", "reported", "ended"]>;
551
+ readonly status: Schema.optionalKey<Schema.String>;
552
+ readonly progressed: Schema.Boolean;
553
+ }>>;
554
+ /** Its join group, and its standing there: `held`, `out`, how it left, or `released`. */
555
+ readonly group: Schema.optionalKey<Schema.Struct<{
556
+ readonly name: Schema.String;
557
+ readonly standing: Schema.String;
558
+ }>>;
559
+ readonly isolated: Schema.Boolean;
560
+ /** Set when it is OpenCode's subagent of its parent (a native child), which OpenCode's session lists leave out. */
561
+ readonly native: Schema.optionalKey<Schema.Boolean>;
562
+ /** The model it was started on, `provider/model` with `#variant` when one is set, when that is not its parent's. */
563
+ readonly model: Schema.optionalKey<Schema.String>;
564
+ readonly created: Schema.Number;
565
+ /** When its last turn ended, if it is not running. */
566
+ readonly idle: Schema.optionalKey<Schema.Number>;
567
+ /** What it has cost so far, in US dollars. */
568
+ readonly cost: Schema.optionalKey<Schema.Number>;
569
+ /** How full its context is, as a fraction of the model's window; left out when the model or its limit cannot be read. */
570
+ readonly context: Schema.optionalKey<Schema.Number>;
571
+ /** Its last tool call, and when it was made. */
572
+ readonly tool: Schema.optionalKey<Schema.Struct<{
573
+ readonly name: Schema.String;
574
+ readonly at: Schema.Number;
575
+ }>>;
576
+ readonly waits: Schema.$Array<Schema.Union<readonly [Schema.Struct<{
577
+ readonly type: Schema.Literal<"permission">;
578
+ readonly action: Schema.String;
579
+ readonly resources: Schema.$Array<Schema.String>;
580
+ }>, Schema.Struct<{
581
+ readonly type: Schema.Literal<"question">;
582
+ readonly question: Schema.String;
583
+ }>, Schema.Struct<{
584
+ readonly type: Schema.Literal<"form">;
585
+ readonly title: Schema.String;
586
+ }>]>>;
587
+ }>>;
384
588
  /** How many older sessions the listing left out, when it had more than `maxTotal`: the live ones come first, then the newest. */
385
589
  readonly truncated: Schema.optionalKey<Schema.Int>;
386
590
  /** Since when a courier message has waited in the panel's own session, when that session is stalled. */
package/dist/tree/rpc.js CHANGED
@@ -68,6 +68,8 @@ const Row = Schema.Struct({
68
68
  /** The sessions under a session, as courier_tree lists them, in the order a tree is drawn. */
69
69
  const Tree = Schema.Struct({
70
70
  rows: Schema.Array(Row),
71
+ /** The panel's own session, at depth 0; absent when OpenCode no longer knows it, and from an older server. */
72
+ top: Schema.optionalKey(Row),
71
73
  /** How many older sessions the listing left out, when it had more than `maxTotal`: the live ones come first, then the newest. */
72
74
  truncated: Schema.optionalKey(Count),
73
75
  /** Since when a courier message has waited in the panel's own session, when that session is stalled. */
@@ -27,7 +27,7 @@ function resolve(session, sessionID, time, now, load, cached) {
27
27
  return Promise.resolve(rest(millis(time.idle)));
28
28
  const known = answers.get(session) ?? new Map();
29
29
  answers.set(session, known);
30
- const stamp = `${millis(time.updated)} ${millis(time.idle)}`;
30
+ const stamp = [time.updated, time.idle, time.busy].map(millis).join(" ");
31
31
  const found = known.get(sessionID);
32
32
  if (cached && found?.stamp === stamp && now < found.until)
33
33
  return found.turn;
@@ -37,6 +37,13 @@ export interface Listed extends Looked {
37
37
  export type ReadPorts = Pick<CourierPorts, "storage" | "session" | "pending" | "forms" | "now" | "limits">;
38
38
  /** Every session under a session, without those past retention, each looked up, in the order a tree is drawn. */
39
39
  export declare function lookUp(ports: ReadPorts, rootID: string): Promise<Looked[]>;
40
+ /** One session looked up, as `lookUp` does each; `info` when the caller already has it. */
41
+ export declare function look(ports: ReadPorts, node: Below, known?: Info): Promise<Looked>;
42
+ /**
43
+ * The session a subtree is read from, looked up and listed as its own sessions are, at depth 0: its roster entry when it
44
+ * was spawned, else one made of OpenCode's info of it; none when OpenCode does not know it.
45
+ */
46
+ export declare function readTop(ports: ReadPorts, rootID: string): Promise<Listed | undefined>;
40
47
  /**
41
48
  * The listing of at most `max` of the sessions under a session: the live ones first, then the newest, with what is
42
49
  * above them, in the order a tree is drawn (`cut`); `leftOut` counts the rest.
@@ -37,17 +37,41 @@ function drawn(byParent, parentID) {
37
37
  /** Every session under a session, without those past retention, each looked up, in the order a tree is drawn. */
38
38
  export async function lookUp(ports, rootID) {
39
39
  const { nodes } = await subtree(ports.storage, rootID, { now: ports.now() });
40
- return Promise.all(nodes.map(async (node) => {
41
- const [info, pending, forms] = await Promise.all([
42
- ports.session.get({ sessionID: node.entry.sessionID }).catch(() => undefined),
43
- ports.pending(node.entry.sessionID).catch(() => []),
44
- ports.forms(node.entry.sessionID).catch(() => []),
45
- ]);
46
- const turn = info === undefined ? undefined : await turnOf(ports.session, node.entry.sessionID, info.time);
47
- const running = turn?.running ?? false;
48
- // A form outlives its session's turn only when its withdrawal was missed, as while no instance followed the events.
49
- return { ...node, info, running, ...(turn?.endedAt !== undefined ? { endedAt: turn.endedAt } : {}), pending, forms: running ? forms : [] };
50
- }));
40
+ return Promise.all(nodes.map((node) => look(ports, node)));
41
+ }
42
+ /** One session looked up, as `lookUp` does each; `info` when the caller already has it. */
43
+ export async function look(ports, node, known) {
44
+ const [info, pending, forms] = await Promise.all([
45
+ known ?? ports.session.get({ sessionID: node.entry.sessionID }).catch(() => undefined),
46
+ ports.pending(node.entry.sessionID).catch(() => []),
47
+ ports.forms(node.entry.sessionID).catch(() => []),
48
+ ]);
49
+ const turn = info === undefined ? undefined : await turnOf(ports.session, node.entry.sessionID, info.time);
50
+ const running = turn?.running ?? false;
51
+ // A form outlives its session's turn only when its withdrawal was missed, as while no instance followed the events.
52
+ return { ...node, info, running, ...(turn?.endedAt !== undefined ? { endedAt: turn.endedAt } : {}), pending, forms: running ? forms : [] };
53
+ }
54
+ /**
55
+ * The session a subtree is read from, looked up and listed as its own sessions are, at depth 0: its roster entry when it
56
+ * was spawned, else one made of OpenCode's info of it; none when OpenCode does not know it.
57
+ */
58
+ export async function readTop(ports, rootID) {
59
+ const info = await ports.session.get({ sessionID: rootID }).catch(() => undefined);
60
+ if (!info)
61
+ return undefined;
62
+ const children = childStore(ports.storage);
63
+ const spawned = await children.indexedEntry(rootID).catch(() => undefined);
64
+ const entry = spawned ?? {
65
+ sessionID: rootID,
66
+ parentID: info.parentID ?? "",
67
+ title: info.title ?? "",
68
+ directory: info.location.directory,
69
+ isolated: false,
70
+ createdAt: info.time.created,
71
+ };
72
+ const looked = await look(ports, { entry, depth: 0 }, info);
73
+ const report = await reportOf(children, rootID).catch(() => undefined);
74
+ return { ...looked, report, live: isLive(looked.running, looked.pending.length + looked.forms.length, report) };
51
75
  }
52
76
  /**
53
77
  * The listing of at most `max` of the sessions under a session: the live ones first, then the newest, with what is
@@ -130,6 +130,14 @@ export declare const activityLine: (row: Row, now: number) => string;
130
130
  export declare function stalledLine(since: number, now: number): string;
131
131
  /** The lines a row shows under its title: an orchestrator's bar, a paused or stalled session's wait, or else a live leaf's activity. */
132
132
  export declare function detailLines(row: Row, rows: ReadonlyArray<Row>, now: number): string[];
133
+ /**
134
+ * Each row's tree guides, by session, for rows in the order a tree is drawn, below depth `base`: before its mark ("│ ├ "),
135
+ * and before its detail lines, which carry a line down to its first child ("│ │ ").
136
+ */
137
+ export declare function guides(rows: ReadonlyArray<Row>, base: number): Map<string, {
138
+ row: string;
139
+ detail: string;
140
+ }>;
133
141
  /** How often the footer and the panel ask again with no event: the scheduler flags a stalled session without one. */
134
142
  export declare const REREAD_MS = 15000;
135
143
  /**
package/dist/tree/wake.js CHANGED
@@ -1,3 +1,4 @@
1
+ import { millis } from "../platform/json.js";
1
2
  import { busy, endsIdle } from "./domain.js";
2
3
  import { wakeStore } from "./store.js";
3
4
  /** How long a courier message may wait in an idle session's inbox before its wake counts as missed. */
@@ -86,12 +87,13 @@ export async function wakeMissed(ports, claimed) {
86
87
  await Promise.all(due.map((wake) => wakeAgain(ports, store, wake, now).finally(() => claimed.delete(claimOf(wake)))));
87
88
  }
88
89
  const claimOf = (wake) => `wake ${wake.sessionID} ${wake.inboxID}`;
90
+ /** Since when a message has waited in an idle session: it was enqueued, seen waiting as the turn ended, or sent again, whichever is last. */
91
+ const waitingSince = (wake) => Math.max(wake.idleAt ?? wake.at, wake.rewoken ?? 0);
89
92
  /** Whether a message has waited in an idle session past `WAKE_MS`, and since a wake again, if one was sent; a stalled one is done. */
90
93
  export function missed(wake, now) {
91
94
  if (wake.running || wake.stalled !== undefined)
92
95
  return false;
93
- const since = Math.max(wake.idleAt ?? wake.at, wake.rewoken ?? 0);
94
- return now - since >= WAKE_MS;
96
+ return now - waitingSince(wake) >= WAKE_MS;
95
97
  }
96
98
  async function wakeAgain(ports, store, wake, now) {
97
99
  try {
@@ -100,7 +102,7 @@ async function wakeAgain(ports, store, wake, now) {
100
102
  if (!current || !missed(current, now))
101
103
  return;
102
104
  // A shutdown during a step reads as an end, and the turn the next boot resumes may start unseen: that turn takes the message.
103
- if (await runsNow(ports, wake.sessionID)) {
105
+ if (await runsNow(ports, current)) {
104
106
  const { idleAt: _, ...running } = current;
105
107
  return await store.put({ ...running, running: true });
106
108
  }
@@ -127,10 +129,18 @@ async function wakeAgain(ports, store, wake, now) {
127
129
  ports.log(`courier wake: could not wake ${wake.sessionID} again for ${wake.inboxID}: ${String(error)}`);
128
130
  }
129
131
  }
130
- /** Whether a session shows a turn running, by its times and its transcript read now; one that cannot be read does not, and is woken again. */
131
- async function runsNow(ports, sessionID) {
132
+ /**
133
+ * Whether a session shows a turn running that takes the message: by `time.busy` (2.2.2), an execution started since the message was
134
+ * last left waiting and not ended; without it, by its times and its transcript read now. One that cannot be read does not, and is woken again.
135
+ */
136
+ async function runsNow(ports, wake) {
137
+ const { sessionID } = wake;
132
138
  const info = await ports.session.get({ sessionID }).catch(() => undefined);
133
- if (!info || !busy(info.time))
139
+ if (!info)
140
+ return false;
141
+ if (info.time.busy !== undefined)
142
+ return busy(info.time) && millis(info.time.busy) >= waitingSince(wake);
143
+ if (!busy(info.time))
134
144
  return false;
135
145
  const messages = await ports.session.context({ sessionID }).catch(() => []);
136
146
  return messages.length > 0 && !endsIdle(messages);
package/dist/tui.js CHANGED
@@ -1468,6 +1468,7 @@ var Row = Schema3.Struct({
1468
1468
  });
1469
1469
  var Tree = Schema3.Struct({
1470
1470
  rows: Schema3.Array(Row),
1471
+ top: Schema3.optionalKey(Row),
1471
1472
  truncated: Schema3.optionalKey(Count3),
1472
1473
  stalled: Schema3.optionalKey(Schema3.Number)
1473
1474
  });
@@ -1841,6 +1842,31 @@ function detailLines(row, rows, now) {
1841
1842
  return [bar(started)];
1842
1843
  return row.state === "gone" ? [] : [activityLine(row, now)];
1843
1844
  }
1845
+ function guides(rows, base) {
1846
+ const byID = new Map(rows.map((row) => [row.sessionID, row]));
1847
+ const later = new Set;
1848
+ const parents = new Set;
1849
+ rows.forEach((row, index) => {
1850
+ if (rows.slice(index + 1).some((next) => next.parentID === row.parentID))
1851
+ later.add(row.sessionID);
1852
+ parents.add(row.parentID);
1853
+ });
1854
+ const above = (row) => {
1855
+ const parent = row.depth > base + 1 ? byID.get(row.parentID) : undefined;
1856
+ return parent ? [...above(parent), parent] : [];
1857
+ };
1858
+ const line = (row) => later.has(row.sessionID) ? "\u2502 " : " ";
1859
+ const branch = (row) => later.has(row.sessionID) ? "\u251C " : "\u2514 ";
1860
+ return new Map(rows.map((row) => {
1861
+ const through = above(row).map(line).join("");
1862
+ const below = row.depth > base;
1863
+ const down = parents.has(row.sessionID) ? "\u2502 " : " ";
1864
+ return [row.sessionID, {
1865
+ row: through + (below ? branch(row) : ""),
1866
+ detail: through + (below ? line(row) : "") + down
1867
+ }];
1868
+ }));
1869
+ }
1844
1870
  function courierOf(ctx) {
1845
1871
  const courier = ctx.client.rpc(treeRpc);
1846
1872
  const location = (sessionID) => ctx.data.session.get(sessionID)?.location ?? ctx.location ?? ctx.data.location.default();
@@ -2084,6 +2110,11 @@ function TreePanel(props) {
2084
2110
  tree: view.tree
2085
2111
  };
2086
2112
  const rows = () => latest()?.tree?.rows ?? [];
2113
+ const top = () => latest()?.tree?.top;
2114
+ const listed = () => {
2115
+ const own = top();
2116
+ return own ? [own, ...rows()] : rows();
2117
+ };
2087
2118
  createEffect(() => {
2088
2119
  const tree = latest()?.tree;
2089
2120
  if (tree)
@@ -2115,7 +2146,7 @@ function TreePanel(props) {
2115
2146
  const [selectedID, setSelectedID] = createSignal();
2116
2147
  let placed = false;
2117
2148
  createEffect(() => {
2118
- const found = placed ? undefined : rows().find((row) => row.sessionID === input.sessionID);
2149
+ const found = placed ? undefined : listed().find((row) => row.sessionID === input.sessionID);
2119
2150
  if (!found)
2120
2151
  return;
2121
2152
  placed = true;
@@ -2124,24 +2155,24 @@ function TreePanel(props) {
2124
2155
  let at = 0;
2125
2156
  const selected = createMemo(() => {
2126
2157
  const id = selectedID();
2127
- const found = rows().findIndex((row) => row.sessionID === id);
2128
- const index = found >= 0 ? found : Math.min(at, rows().length - 1);
2158
+ const found = listed().findIndex((row) => row.sessionID === id);
2159
+ const index = found >= 0 ? found : Math.min(at, listed().length - 1);
2129
2160
  at = Math.max(index, 0);
2130
2161
  return index;
2131
2162
  });
2132
2163
  createEffect(() => {
2133
- const row = rows()[selected()];
2164
+ const row = listed()[selected()];
2134
2165
  if (row && row.sessionID !== selectedID())
2135
2166
  setSelectedID(row.sessionID);
2136
2167
  });
2137
- const current = () => rows()[selected()];
2168
+ const current = () => listed()[selected()];
2138
2169
  let scroll;
2139
2170
  createEffect(() => {
2140
2171
  if (selected() >= 0)
2141
2172
  scroll?.scrollChildIntoView(`courier-row-${selected()}`);
2142
2173
  });
2143
2174
  const move = (by) => {
2144
- const next = rows()[Math.max(0, Math.min(rows().length - 1, selected() + by))];
2175
+ const next = listed()[Math.max(0, Math.min(listed().length - 1, selected() + by))];
2145
2176
  if (next)
2146
2177
  setSelectedID(next.sessionID);
2147
2178
  };
@@ -2157,7 +2188,7 @@ function TreePanel(props) {
2157
2188
  const [hovered, setHovered] = createSignal();
2158
2189
  createEffect(() => {
2159
2190
  const id = hovered();
2160
- if (id !== undefined && !rows().some((row) => row.sessionID === id))
2191
+ if (id !== undefined && !listed().some((row) => row.sessionID === id))
2161
2192
  setHovered(undefined);
2162
2193
  });
2163
2194
  let pressed;
@@ -2165,9 +2196,19 @@ function TreePanel(props) {
2165
2196
  setSelectedID(row.sessionID);
2166
2197
  openRow(row);
2167
2198
  };
2199
+ const ownRow = (row, key) => {
2200
+ if (row.sessionID !== view.root)
2201
+ return false;
2202
+ ctx.ui.toast.show({
2203
+ title: "courier",
2204
+ message: `${key} acts on a session under "${oneLine3(row.title)}", the panel's own.`,
2205
+ variant: "warning"
2206
+ });
2207
+ return true;
2208
+ };
2168
2209
  const stopSelected = () => {
2169
2210
  const row = current();
2170
- if (row)
2211
+ if (row && !ownRow(row, "s"))
2171
2212
  confirmStop(ctx, courier, location, view.root, {
2172
2213
  sessionID: row.sessionID,
2173
2214
  title: row.title,
@@ -2176,7 +2217,7 @@ function TreePanel(props) {
2176
2217
  };
2177
2218
  const forgetSelected = () => {
2178
2219
  const row = current();
2179
- if (!row)
2220
+ if (!row || ownRow(row, "x"))
2180
2221
  return;
2181
2222
  if (row.state !== "gone")
2182
2223
  return ctx.ui.toast.show({
@@ -2261,7 +2302,9 @@ function TreePanel(props) {
2261
2302
  const glyphShown = () => height() >= GLYPH_ROWS && size().width >= GLYPH_COLUMNS;
2262
2303
  const artShown = () => empty() && height() >= ART_ROWS;
2263
2304
  const [listHeight, setListHeight] = createSignal(0);
2264
- const treeLines = () => rows().reduce((lines, row) => lines + 1 + detailLines(row, rows(), now()).length, 0);
2305
+ const lines = (row) => detailLines(row, row.depth ? rows() : [], now());
2306
+ const guided = createMemo(() => guides(listed(), top() ? 0 : 1));
2307
+ const treeLines = () => listed().reduce((count, row) => count + 1 + lines(row).length, 0);
2265
2308
  const belowShown = () => rows().length > 0 && listHeight() - treeLines() >= BELOW_ROWS;
2266
2309
  const headerGlyph = () => glyphShown() && !artShown() && !belowShown();
2267
2310
  const mascot = createMemo(() => mascotState(rows(), {
@@ -2317,9 +2360,9 @@ function TreePanel(props) {
2317
2360
  return theme.text.muted;
2318
2361
  };
2319
2362
  return (() => {
2320
- var _el$6 = _$createElement("box"), _el$7 = _$createElement("box"), _el$8 = _$createElement("text"), _el$20 = _$createElement("scrollbox"), _el$22 = _$createElement("text");
2363
+ var _el$6 = _$createElement("box"), _el$7 = _$createElement("box"), _el$8 = _$createElement("text"), _el$17 = _$createElement("scrollbox"), _el$22 = _$createElement("text");
2321
2364
  _$insertNode(_el$6, _el$7);
2322
- _$insertNode(_el$6, _el$20);
2365
+ _$insertNode(_el$6, _el$17);
2323
2366
  _$insertNode(_el$6, _el$22);
2324
2367
  _$setProp(_el$6, "flexDirection", "column");
2325
2368
  _$setProp(_el$6, "flexGrow", 1);
@@ -2394,7 +2437,7 @@ function TreePanel(props) {
2394
2437
  _$effect((_$p) => _$setProp(_el$12, "fg", theme.text.muted, _$p));
2395
2438
  return _el$11;
2396
2439
  }
2397
- }), _el$20);
2440
+ }), _el$17);
2398
2441
  _$insert(_el$6, _$createComponent(Show, {
2399
2442
  get when() {
2400
2443
  return latest()?.error;
@@ -2406,10 +2449,10 @@ function TreePanel(props) {
2406
2449
  _$effect((_$p) => _$setProp(_el$13, "fg", theme.text.feedback.error.base, _$p));
2407
2450
  return _el$13;
2408
2451
  }
2409
- }), _el$20);
2452
+ }), _el$17);
2410
2453
  _$insert(_el$6, _$createComponent(Show, {
2411
2454
  get when() {
2412
- return latest()?.tree?.stalled;
2455
+ return _$memo(() => !!!top())() && latest()?.tree?.stalled;
2413
2456
  },
2414
2457
  get children() {
2415
2458
  var _el$15 = _$createElement("text"), _el$16 = _$createTextNode(`This session is stalled: `);
@@ -2418,49 +2461,18 @@ function TreePanel(props) {
2418
2461
  _$effect((_$p) => _$setProp(_el$15, "fg", theme.text.feedback.error.base, _$p));
2419
2462
  return _el$15;
2420
2463
  }
2421
- }), _el$20);
2422
- _$insert(_el$6, _$createComponent(Show, {
2423
- get when() {
2424
- return _$memo(() => !!latest()?.tree)() && !rows().length;
2425
- },
2426
- get children() {
2427
- return [(() => {
2428
- var _el$17 = _$createElement("text");
2429
- _$insertNode(_el$17, _$createTextNode(`No session started with courier_spawn under this one.`));
2430
- _$effect((_$p) => _$setProp(_el$17, "fg", theme.text.muted, _$p));
2431
- return _el$17;
2432
- })(), _$createComponent(Show, {
2433
- get when() {
2434
- return artShown();
2435
- },
2436
- get children() {
2437
- var _el$19 = _$createElement("box");
2438
- _$setProp(_el$19, "flexDirection", "column");
2439
- _$setProp(_el$19, "flexShrink", 0);
2440
- _$setProp(_el$19, "paddingTop", 1);
2441
- _$insert(_el$19, _$createComponent(Index, {
2442
- get each() {
2443
- return art2();
2444
- },
2445
- children: (line) => drawn(line)
2446
- }));
2447
- return _el$19;
2448
- }
2449
- })];
2450
- }
2451
- }), _el$20);
2452
- _$use((value) => scroll = value, _el$20);
2453
- _$setProp(_el$20, "flexGrow", 1);
2454
- _$setProp(_el$20, "onSizeChange", function() {
2464
+ }), _el$17);
2465
+ _$use((value) => scroll = value, _el$17);
2466
+ _$setProp(_el$17, "flexGrow", 1);
2467
+ _$setProp(_el$17, "onSizeChange", function() {
2455
2468
  setListHeight(this.height);
2456
2469
  });
2457
- _$insert(_el$20, _$createComponent(For, {
2470
+ _$insert(_el$17, _$createComponent(For, {
2458
2471
  get each() {
2459
- return rows();
2472
+ return listed();
2460
2473
  },
2461
2474
  children: (row, index) => {
2462
2475
  const chosen = () => index() === selected();
2463
- const indent = " ".repeat(row.depth - 1);
2464
2476
  return (() => {
2465
2477
  var _el$24 = _$createElement("box"), _el$25 = _$createElement("box"), _el$26 = _$createElement("text"), _el$27 = _$createElement("text"), _el$28 = _$createTextNode(` `), _el$29 = _$createElement("text"), _el$30 = _$createElement("text");
2466
2478
  _$insertNode(_el$24, _el$25);
@@ -2486,8 +2498,20 @@ function TreePanel(props) {
2486
2498
  _$setProp(_el$25, "flexShrink", 0);
2487
2499
  _$setProp(_el$26, "wrapMode", "none");
2488
2500
  _$setProp(_el$26, "flexShrink", 0);
2489
- _$insert(_el$26, indent, null);
2490
- _$insert(_el$26, () => chosen() ? "\u25B8 " : " ", null);
2501
+ _$insert(_el$26, () => chosen() ? "\u25B8 " : " ");
2502
+ _$insert(_el$25, _$createComponent(Show, {
2503
+ get when() {
2504
+ return guided().get(row.sessionID)?.row;
2505
+ },
2506
+ children: (guide) => (() => {
2507
+ var _el$32 = _$createElement("text");
2508
+ _$setProp(_el$32, "wrapMode", "none");
2509
+ _$setProp(_el$32, "flexShrink", 0);
2510
+ _$insert(_el$32, guide);
2511
+ _$effect((_$p) => _$setProp(_el$32, "fg", theme.text.muted, _$p));
2512
+ return _el$32;
2513
+ })()
2514
+ }), _el$27);
2491
2515
  _$insertNode(_el$27, _el$28);
2492
2516
  _$setProp(_el$27, "wrapMode", "none");
2493
2517
  _$setProp(_el$27, "flexShrink", 0);
@@ -2509,32 +2533,32 @@ function TreePanel(props) {
2509
2533
  return at();
2510
2534
  },
2511
2535
  get children() {
2512
- var _el$32 = _$createElement("text");
2513
- _$insertNode(_el$32, _$createTextNode(` \xB7 `));
2514
- _$setProp(_el$32, "wrapMode", "none");
2515
- _$setProp(_el$32, "flexShrink", 0);
2516
- _$effect((_$p) => _$setProp(_el$32, "fg", theme.text.muted, _$p));
2517
- return _el$32;
2536
+ var _el$33 = _$createElement("text");
2537
+ _$insertNode(_el$33, _$createTextNode(` \xB7 `));
2538
+ _$setProp(_el$33, "wrapMode", "none");
2539
+ _$setProp(_el$33, "flexShrink", 0);
2540
+ _$effect((_$p) => _$setProp(_el$33, "fg", theme.text.muted, _$p));
2541
+ return _el$33;
2518
2542
  }
2519
2543
  }), (() => {
2520
- var _el$34 = _$createElement("text");
2521
- _$setProp(_el$34, "wrapMode", "none");
2522
- _$setProp(_el$34, "flexShrink", 0);
2523
- _$insert(_el$34, () => part.text);
2524
- _$effect((_$p) => _$setProp(_el$34, "fg", partColor(row, part.kind), _$p));
2525
- return _el$34;
2544
+ var _el$35 = _$createElement("text");
2545
+ _$setProp(_el$35, "wrapMode", "none");
2546
+ _$setProp(_el$35, "flexShrink", 0);
2547
+ _$insert(_el$35, () => part.text);
2548
+ _$effect((_$p) => _$setProp(_el$35, "fg", partColor(row, part.kind), _$p));
2549
+ return _el$35;
2526
2550
  })()]
2527
2551
  }), null);
2528
2552
  _$insert(_el$24, _$createComponent(For, {
2529
2553
  get each() {
2530
- return detailLines(row, rows(), now());
2554
+ return lines(row);
2531
2555
  },
2532
2556
  children: (line) => (() => {
2533
- var _el$35 = _$createElement("text"), _el$36 = _$createTextNode(` `);
2534
- _$insertNode(_el$35, _el$36);
2535
- _$setProp(_el$35, "wrapMode", "none");
2536
- _$insert(_el$35, indent, _el$36);
2537
- _$insert(_el$35, _$createComponent(Show, {
2557
+ var _el$36 = _$createElement("text"), _el$37 = _$createTextNode(` `);
2558
+ _$insertNode(_el$36, _el$37);
2559
+ _$setProp(_el$36, "wrapMode", "none");
2560
+ _$insert(_el$36, () => guided().get(row.sessionID)?.detail ?? " ", null);
2561
+ _$insert(_el$36, _$createComponent(Show, {
2538
2562
  get when() {
2539
2563
  return _$memo(() => row.context !== undefined)() && Math.round(row.context * 100) > CONTEXT_WARN * 100;
2540
2564
  },
@@ -2545,18 +2569,18 @@ function TreePanel(props) {
2545
2569
  return line.split(" \xB7 ");
2546
2570
  },
2547
2571
  children: (part, at) => [_$memo(() => at() ? " \xB7 " : ""), _$memo(() => _$memo(() => part === `ctx ${Math.round(row.context * 100)}%`)() ? (() => {
2548
- var _el$37 = _$createElement("span");
2549
- _$insert(_el$37, part);
2550
- _$effect((_$p) => _$setProp(_el$37, "style", {
2572
+ var _el$38 = _$createElement("span");
2573
+ _$insert(_el$38, part);
2574
+ _$effect((_$p) => _$setProp(_el$38, "style", {
2551
2575
  fg: theme.text.feedback.warning.base
2552
2576
  }, _$p));
2553
- return _el$37;
2577
+ return _el$38;
2554
2578
  })() : part)]
2555
2579
  });
2556
2580
  }
2557
2581
  }), null);
2558
- _$effect((_$p) => _$setProp(_el$35, "fg", theme.text.muted, _$p));
2559
- return _el$35;
2582
+ _$effect((_$p) => _$setProp(_el$36, "fg", theme.text.muted, _$p));
2583
+ return _el$36;
2560
2584
  })()
2561
2585
  }), null);
2562
2586
  _$effect((_p$) => {
@@ -2580,7 +2604,38 @@ function TreePanel(props) {
2580
2604
  })();
2581
2605
  }
2582
2606
  }), null);
2583
- _$insert(_el$20, _$createComponent(Show, {
2607
+ _$insert(_el$17, _$createComponent(Show, {
2608
+ get when() {
2609
+ return _$memo(() => !!latest()?.tree)() && !rows().length;
2610
+ },
2611
+ get children() {
2612
+ return [(() => {
2613
+ var _el$18 = _$createElement("text"), _el$19 = _$createTextNode(`No session started with courier_spawn under this one.`);
2614
+ _$insertNode(_el$18, _el$19);
2615
+ _$insert(_el$18, () => top() ? " " : "", _el$19);
2616
+ _$effect((_$p) => _$setProp(_el$18, "fg", theme.text.muted, _$p));
2617
+ return _el$18;
2618
+ })(), _$createComponent(Show, {
2619
+ get when() {
2620
+ return artShown();
2621
+ },
2622
+ get children() {
2623
+ var _el$20 = _$createElement("box");
2624
+ _$setProp(_el$20, "flexDirection", "column");
2625
+ _$setProp(_el$20, "flexShrink", 0);
2626
+ _$setProp(_el$20, "paddingTop", 1);
2627
+ _$insert(_el$20, _$createComponent(Index, {
2628
+ get each() {
2629
+ return art2();
2630
+ },
2631
+ children: (line) => drawn(line)
2632
+ }));
2633
+ return _el$20;
2634
+ }
2635
+ })];
2636
+ }
2637
+ }), null);
2638
+ _$insert(_el$17, _$createComponent(Show, {
2584
2639
  get when() {
2585
2640
  return belowShown();
2586
2641
  },
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "opencode-courier",
3
- "version": "0.13.0",
3
+ "version": "0.13.2",
4
4
  "description": "Multi-session orchestration for OpenCode V2: spawn child sessions, be woken by them. No polling.",
5
5
  "license": "MIT",
6
6
  "author": "Ivo Pogace <71701666+ivopogace@users.noreply.github.com>",
@@ -56,18 +56,18 @@
56
56
  "test:e2e": "e2e/run.sh"
57
57
  },
58
58
  "peerDependencies": {
59
- "@opencode/plugin": "2.2.0",
60
- "effect": "4.0.1"
59
+ "@opencode/plugin": "2.2.2",
60
+ "effect": "4.0.6"
61
61
  },
62
62
  "devDependencies": {
63
- "@opencode/plugin": "2.2.0",
64
- "@opencode/theme": "2.2.0",
63
+ "@opencode/plugin": "2.2.2",
64
+ "@opencode/theme": "2.2.2",
65
65
  "@opentui/core": "0.7.0",
66
66
  "@opentui/solid": "0.7.0",
67
67
  "@types/bun": "1.4.2",
68
68
  "@types/node": "26.6.4",
69
- "effect": "4.0.1",
70
- "solid-js": "1.9.15",
69
+ "effect": "4.0.6",
70
+ "solid-js": "1.9.17",
71
71
  "typescript": "5.9.3"
72
72
  }
73
73
  }