opencode-courier 0.13.1 → 0.13.3

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.1 # 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.1** (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.1
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, 0.13.3 | 2.2.2 |
81
82
  | 0.13.1 | 2.2.1 |
82
83
  | 0.13.0 | 2.2.0 |
83
- | 0.11.1, 0.12.0 | 2.1.4 |
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.1 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
  /**
@@ -15,6 +15,8 @@ export type Cell = {
15
15
  export declare const MASCOT_EVERY = 5;
16
16
  /** He blinks on every this-many-th of his frames where his eyes are open. */
17
17
  export declare const BLINK_EVERY = 11;
18
+ /** Running, he is still and awake for this many of his frames, about 8 s, then digs for one cycle of six. */
19
+ export declare const DIG_EVERY = 18;
18
20
  /**
19
21
  * The 8×4 glyph's frames by state, pixel rows top to bottom; a state with several cycles through them, one per mascot frame.
20
22
  * No muted pixel touches the mound, here or in the 16×16, as `text.muted` is close to `background.raised.max`.
@@ -2,6 +2,8 @@
2
2
  export const MASCOT_EVERY = 5;
3
3
  /** He blinks on every this-many-th of his frames where his eyes are open. */
4
4
  export const BLINK_EVERY = 11;
5
+ /** Running, he is still and awake for this many of his frames, about 8 s, then digs for one cycle of six. */
6
+ export const DIG_EVERY = 18;
5
7
  /**
6
8
  * The 8×4 glyph's frames by state, pixel rows top to bottom; a state with several cycles through them, one per mascot frame.
7
9
  * No muted pixel touches the mound, here or in the 16×16, as `text.muted` is close to `background.raised.max`.
@@ -13,8 +15,8 @@ export const GLYPH = {
13
15
  [".g....g.", "...bb...", "ggbbbbgg", "gggggggg"],
14
16
  ],
15
17
  running: [
16
- ["..g..g..", "...bb...", ".gbbbbg.", "gggggggg"],
17
- [".g....g.", "........", ".gbbbbg.", "gggggggg"],
18
+ ...Array.from({ length: DIG_EVERY }, () => ["..bbbb..", ".bkbbkb.", "gbbaabbg", "gggggggg"]),
19
+ ...["m.bbbb.m", ".mbbbbm.", "..bbbb..", "m.bbbb.m", ".mbbbbm.", "..bbbb.."].map((top) => [top, ".bkbbkb.", "gbbaabbg", "gggggggg"]),
18
20
  ],
19
21
  waiting: [
20
22
  [".bbbbbbw", ".bkbbkbw", ".bbaabbm", "ggggggg."],
@@ -32,7 +34,7 @@ export const GLYPH = {
32
34
  gone: [["........", ".mmmmmm.", ".mkmmkm.", "mmmmmmmm"]],
33
35
  };
34
36
  /** The states in which his eyes are open, so he blinks. */
35
- const OPEN_EYES = new Set(["waiting", "report"]);
37
+ const OPEN_EYES = new Set(["waiting", "report", "running"]);
36
38
  /** The 8×4 glyph's pixels for `state` at mascot frame `frame`, its first frame with none: animations off. */
37
39
  export function glyph(state, frame) {
38
40
  const frames = GLYPH[state];
@@ -114,9 +116,27 @@ export function paint(pixels, spots) {
114
116
  }
115
117
  /** A column of `length` pixels of `pixel` from row `row`. */
116
118
  const post = (row, column, length, pixel) => Array.from({ length }, (_, at) => [row + at, column, pixel]);
117
- /** The head's top row at `rise`, where running's snout pokes up. */
119
+ /** The head's top row at `rise`. */
118
120
  const headTop = (rise) => Math.round(10 - 8 * rise);
119
- const running = (rise, shift, dirt) => paint(digby(rise, { shift }), [[headTop(rise), 7, "aa"], ...dirt]);
121
+ /** Up and awake at `rise`, his eyes `shift` columns over (0 straight ahead) when `open`. */
122
+ const awake = (rise, shift) => (open) => {
123
+ const row = headTop(rise) + 3;
124
+ return paint(digby(rise, { eyes: "open" }), [[row, 6, "b"], [row, 9, "b"], ...(open ? [[row, 6 + shift, "k"], [row, 9 + shift, "k"]] : [])]);
125
+ };
126
+ /** Where a clod flies on each side, frame by frame: out of the hole, up, over and down, a row clear of the mound's rim. */
127
+ const CLOD_LEFT = [[8, 3], [7, 2], [6, 1], [5, 0], [7, 0], [9, 0]];
128
+ const CLOD_RIGHT = [[8, 12], [7, 13], [6, 14], [5, 15], [7, 15], [9, 15]];
129
+ const CLODS = [[CLOD_LEFT, 0], [CLOD_LEFT, 3], [CLOD_RIGHT, 1], [CLOD_RIGHT, 4]];
130
+ /** Digging at frame `at`: up with his eyes open, the mound still, four clods on their arcs, one always in the air. */
131
+ const digging = (at) => (open) => paint(digby(0.6, { eyes: open ? "open" : "none" }), CLODS.map(([path, phase]) => {
132
+ const [row, column] = path[(at + phase) % path.length];
133
+ return [row, column, "m"];
134
+ }));
135
+ /** Running: still and awake, a glance left then right over twelve frames, then one dig cycle; the mound never moves. */
136
+ const running = [
137
+ ...Array.from({ length: DIG_EVERY }, (_, at) => [0, 0, 0, 0, 0, -1, -1, 0, 0, 0, 1, 1][at % 12]).map((shift) => awake(0.6, shift)),
138
+ ...Array.from({ length: CLOD_LEFT.length }, (_, at) => digging(at)),
139
+ ];
120
140
  /** A child's head peeking out of a side mound at columns `column`–`column + 2`, `up` or half sunk. */
121
141
  const peek = (column, up) => [
122
142
  ...(up ? [[8, column, ".b."], [9, column, "bkb"]] : [[9, column, ".b."]]),
@@ -143,10 +163,7 @@ export const ART = {
143
163
  () => spawn(true, [[3, 1, "m"], [5, 3, "m"], [2, 14, "m"], [5, 12, "m"]]),
144
164
  () => spawn(false, [[1, 0, "m"], [4, 2, "m"], [3, 15, "m"], [6, 13, "m"]]),
145
165
  ],
146
- running: [
147
- () => running(0.22, 0, [[5, 2, "m"], [7, 1, "m"], [4, 13, "m"], [6, 14, "m"]]),
148
- () => running(0.12, 1, [[3, 1, "m"], [6, 3, "m"], [5, 12, "m"], [3, 15, "m"]]),
149
- ],
166
+ running,
150
167
  waiting: [waiting(true), waiting(false)],
151
168
  stalled: [stalled("e.e.e"), stalled("e"), stalled("e.e")],
152
169
  report: [report(1, [".s.", "sss", ".s."]), report(1.25, ["s.s", ".s.", "s.s"]), report(1.1, [".s.", "sss", ".s."])],
@@ -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
@@ -1284,6 +1284,7 @@ import { batch, createEffect, createMemo, createResource, createRoot, createSign
1284
1284
  // src/tree/mascot.ts
1285
1285
  var MASCOT_EVERY = 5;
1286
1286
  var BLINK_EVERY = 11;
1287
+ var DIG_EVERY = 18;
1287
1288
  var GLYPH = {
1288
1289
  idle: [["........", "........", "..gggg..", "gggggggg"]],
1289
1290
  spawn: [
@@ -1291,8 +1292,8 @@ var GLYPH = {
1291
1292
  [".g....g.", "...bb...", "ggbbbbgg", "gggggggg"]
1292
1293
  ],
1293
1294
  running: [
1294
- ["..g..g..", "...bb...", ".gbbbbg.", "gggggggg"],
1295
- [".g....g.", "........", ".gbbbbg.", "gggggggg"]
1295
+ ...Array.from({ length: DIG_EVERY }, () => ["..bbbb..", ".bkbbkb.", "gbbaabbg", "gggggggg"]),
1296
+ ...["m.bbbb.m", ".mbbbbm.", "..bbbb..", "m.bbbb.m", ".mbbbbm.", "..bbbb.."].map((top) => [top, ".bkbbkb.", "gbbaabbg", "gggggggg"])
1296
1297
  ],
1297
1298
  waiting: [
1298
1299
  [".bbbbbbw", ".bkbbkbw", ".bbaabbm", "ggggggg."],
@@ -1309,7 +1310,7 @@ var GLYPH = {
1309
1310
  ],
1310
1311
  gone: [["........", ".mmmmmm.", ".mkmmkm.", "mmmmmmmm"]]
1311
1312
  };
1312
- var OPEN_EYES = new Set(["waiting", "report"]);
1313
+ var OPEN_EYES = new Set(["waiting", "report", "running"]);
1313
1314
  function glyph(state, frame) {
1314
1315
  const frames = GLYPH[state];
1315
1316
  const pixels = frames[(frame ?? 0) % frames.length];
@@ -1381,7 +1382,21 @@ function paint(pixels, spots) {
1381
1382
  }
1382
1383
  var post = (row, column, length, pixel) => Array.from({ length }, (_, at) => [row + at, column, pixel]);
1383
1384
  var headTop = (rise) => Math.round(10 - 8 * rise);
1384
- var running = (rise, shift, dirt) => paint(digby(rise, { shift }), [[headTop(rise), 7, "aa"], ...dirt]);
1385
+ var awake = (rise, shift) => (open) => {
1386
+ const row = headTop(rise) + 3;
1387
+ return paint(digby(rise, { eyes: "open" }), [[row, 6, "b"], [row, 9, "b"], ...open ? [[row, 6 + shift, "k"], [row, 9 + shift, "k"]] : []]);
1388
+ };
1389
+ var CLOD_LEFT = [[8, 3], [7, 2], [6, 1], [5, 0], [7, 0], [9, 0]];
1390
+ var CLOD_RIGHT = [[8, 12], [7, 13], [6, 14], [5, 15], [7, 15], [9, 15]];
1391
+ var CLODS = [[CLOD_LEFT, 0], [CLOD_LEFT, 3], [CLOD_RIGHT, 1], [CLOD_RIGHT, 4]];
1392
+ var digging = (at) => (open) => paint(digby(0.6, { eyes: open ? "open" : "none" }), CLODS.map(([path, phase]) => {
1393
+ const [row, column] = path[(at + phase) % path.length];
1394
+ return [row, column, "m"];
1395
+ }));
1396
+ var running = [
1397
+ ...Array.from({ length: DIG_EVERY }, (_, at) => [0, 0, 0, 0, 0, -1, -1, 0, 0, 0, 1, 1][at % 12]).map((shift) => awake(0.6, shift)),
1398
+ ...Array.from({ length: CLOD_LEFT.length }, (_, at) => digging(at))
1399
+ ];
1385
1400
  var peek = (column, up) => [
1386
1401
  ...up ? [[8, column, ".b."], [9, column, "bkb"]] : [[9, column, ".b."]],
1387
1402
  [10, column, "gbg"],
@@ -1403,10 +1418,7 @@ var ART = {
1403
1418
  () => spawn(true, [[3, 1, "m"], [5, 3, "m"], [2, 14, "m"], [5, 12, "m"]]),
1404
1419
  () => spawn(false, [[1, 0, "m"], [4, 2, "m"], [3, 15, "m"], [6, 13, "m"]])
1405
1420
  ],
1406
- running: [
1407
- () => running(0.22, 0, [[5, 2, "m"], [7, 1, "m"], [4, 13, "m"], [6, 14, "m"]]),
1408
- () => running(0.12, 1, [[3, 1, "m"], [6, 3, "m"], [5, 12, "m"], [3, 15, "m"]])
1409
- ],
1421
+ running,
1410
1422
  waiting: [waiting(true), waiting(false)],
1411
1423
  stalled: [stalled("e.e.e"), stalled("e"), stalled("e.e")],
1412
1424
  report: [report(1, [".s.", "sss", ".s."]), report(1.25, ["s.s", ".s.", "s.s"]), report(1.1, [".s.", "sss", ".s."])],
@@ -1468,6 +1480,7 @@ var Row = Schema3.Struct({
1468
1480
  });
1469
1481
  var Tree = Schema3.Struct({
1470
1482
  rows: Schema3.Array(Row),
1483
+ top: Schema3.optionalKey(Row),
1471
1484
  truncated: Schema3.optionalKey(Count3),
1472
1485
  stalled: Schema3.optionalKey(Schema3.Number)
1473
1486
  });
@@ -1841,6 +1854,31 @@ function detailLines(row, rows, now) {
1841
1854
  return [bar(started)];
1842
1855
  return row.state === "gone" ? [] : [activityLine(row, now)];
1843
1856
  }
1857
+ function guides(rows, base) {
1858
+ const byID = new Map(rows.map((row) => [row.sessionID, row]));
1859
+ const later = new Set;
1860
+ const parents = new Set;
1861
+ rows.forEach((row, index) => {
1862
+ if (rows.slice(index + 1).some((next) => next.parentID === row.parentID))
1863
+ later.add(row.sessionID);
1864
+ parents.add(row.parentID);
1865
+ });
1866
+ const above = (row) => {
1867
+ const parent = row.depth > base + 1 ? byID.get(row.parentID) : undefined;
1868
+ return parent ? [...above(parent), parent] : [];
1869
+ };
1870
+ const line = (row) => later.has(row.sessionID) ? "\u2502 " : " ";
1871
+ const branch = (row) => later.has(row.sessionID) ? "\u251C " : "\u2514 ";
1872
+ return new Map(rows.map((row) => {
1873
+ const through = above(row).map(line).join("");
1874
+ const below = row.depth > base;
1875
+ const down = parents.has(row.sessionID) ? "\u2502 " : " ";
1876
+ return [row.sessionID, {
1877
+ row: through + (below ? branch(row) : ""),
1878
+ detail: through + (below ? line(row) : "") + down
1879
+ }];
1880
+ }));
1881
+ }
1844
1882
  function courierOf(ctx) {
1845
1883
  const courier = ctx.client.rpc(treeRpc);
1846
1884
  const location = (sessionID) => ctx.data.session.get(sessionID)?.location ?? ctx.location ?? ctx.data.location.default();
@@ -2084,6 +2122,11 @@ function TreePanel(props) {
2084
2122
  tree: view.tree
2085
2123
  };
2086
2124
  const rows = () => latest()?.tree?.rows ?? [];
2125
+ const top = () => latest()?.tree?.top;
2126
+ const listed = () => {
2127
+ const own = top();
2128
+ return own ? [own, ...rows()] : rows();
2129
+ };
2087
2130
  createEffect(() => {
2088
2131
  const tree = latest()?.tree;
2089
2132
  if (tree)
@@ -2115,7 +2158,7 @@ function TreePanel(props) {
2115
2158
  const [selectedID, setSelectedID] = createSignal();
2116
2159
  let placed = false;
2117
2160
  createEffect(() => {
2118
- const found = placed ? undefined : rows().find((row) => row.sessionID === input.sessionID);
2161
+ const found = placed ? undefined : listed().find((row) => row.sessionID === input.sessionID);
2119
2162
  if (!found)
2120
2163
  return;
2121
2164
  placed = true;
@@ -2124,24 +2167,24 @@ function TreePanel(props) {
2124
2167
  let at = 0;
2125
2168
  const selected = createMemo(() => {
2126
2169
  const id = selectedID();
2127
- const found = rows().findIndex((row) => row.sessionID === id);
2128
- const index = found >= 0 ? found : Math.min(at, rows().length - 1);
2170
+ const found = listed().findIndex((row) => row.sessionID === id);
2171
+ const index = found >= 0 ? found : Math.min(at, listed().length - 1);
2129
2172
  at = Math.max(index, 0);
2130
2173
  return index;
2131
2174
  });
2132
2175
  createEffect(() => {
2133
- const row = rows()[selected()];
2176
+ const row = listed()[selected()];
2134
2177
  if (row && row.sessionID !== selectedID())
2135
2178
  setSelectedID(row.sessionID);
2136
2179
  });
2137
- const current = () => rows()[selected()];
2180
+ const current = () => listed()[selected()];
2138
2181
  let scroll;
2139
2182
  createEffect(() => {
2140
2183
  if (selected() >= 0)
2141
2184
  scroll?.scrollChildIntoView(`courier-row-${selected()}`);
2142
2185
  });
2143
2186
  const move = (by) => {
2144
- const next = rows()[Math.max(0, Math.min(rows().length - 1, selected() + by))];
2187
+ const next = listed()[Math.max(0, Math.min(listed().length - 1, selected() + by))];
2145
2188
  if (next)
2146
2189
  setSelectedID(next.sessionID);
2147
2190
  };
@@ -2157,7 +2200,7 @@ function TreePanel(props) {
2157
2200
  const [hovered, setHovered] = createSignal();
2158
2201
  createEffect(() => {
2159
2202
  const id = hovered();
2160
- if (id !== undefined && !rows().some((row) => row.sessionID === id))
2203
+ if (id !== undefined && !listed().some((row) => row.sessionID === id))
2161
2204
  setHovered(undefined);
2162
2205
  });
2163
2206
  let pressed;
@@ -2165,9 +2208,19 @@ function TreePanel(props) {
2165
2208
  setSelectedID(row.sessionID);
2166
2209
  openRow(row);
2167
2210
  };
2211
+ const ownRow = (row, key) => {
2212
+ if (row.sessionID !== view.root)
2213
+ return false;
2214
+ ctx.ui.toast.show({
2215
+ title: "courier",
2216
+ message: `${key} acts on a session under "${oneLine3(row.title)}", the panel's own.`,
2217
+ variant: "warning"
2218
+ });
2219
+ return true;
2220
+ };
2168
2221
  const stopSelected = () => {
2169
2222
  const row = current();
2170
- if (row)
2223
+ if (row && !ownRow(row, "s"))
2171
2224
  confirmStop(ctx, courier, location, view.root, {
2172
2225
  sessionID: row.sessionID,
2173
2226
  title: row.title,
@@ -2176,7 +2229,7 @@ function TreePanel(props) {
2176
2229
  };
2177
2230
  const forgetSelected = () => {
2178
2231
  const row = current();
2179
- if (!row)
2232
+ if (!row || ownRow(row, "x"))
2180
2233
  return;
2181
2234
  if (row.state !== "gone")
2182
2235
  return ctx.ui.toast.show({
@@ -2261,7 +2314,9 @@ function TreePanel(props) {
2261
2314
  const glyphShown = () => height() >= GLYPH_ROWS && size().width >= GLYPH_COLUMNS;
2262
2315
  const artShown = () => empty() && height() >= ART_ROWS;
2263
2316
  const [listHeight, setListHeight] = createSignal(0);
2264
- const treeLines = () => rows().reduce((lines, row) => lines + 1 + detailLines(row, rows(), now()).length, 0);
2317
+ const lines = (row) => detailLines(row, row.depth ? rows() : [], now());
2318
+ const guided = createMemo(() => guides(listed(), top() ? 0 : 1));
2319
+ const treeLines = () => listed().reduce((count, row) => count + 1 + lines(row).length, 0);
2265
2320
  const belowShown = () => rows().length > 0 && listHeight() - treeLines() >= BELOW_ROWS;
2266
2321
  const headerGlyph = () => glyphShown() && !artShown() && !belowShown();
2267
2322
  const mascot = createMemo(() => mascotState(rows(), {
@@ -2317,9 +2372,9 @@ function TreePanel(props) {
2317
2372
  return theme.text.muted;
2318
2373
  };
2319
2374
  return (() => {
2320
- var _el$6 = _$createElement("box"), _el$7 = _$createElement("box"), _el$8 = _$createElement("text"), _el$20 = _$createElement("scrollbox"), _el$22 = _$createElement("text");
2375
+ var _el$6 = _$createElement("box"), _el$7 = _$createElement("box"), _el$8 = _$createElement("text"), _el$17 = _$createElement("scrollbox"), _el$22 = _$createElement("text");
2321
2376
  _$insertNode(_el$6, _el$7);
2322
- _$insertNode(_el$6, _el$20);
2377
+ _$insertNode(_el$6, _el$17);
2323
2378
  _$insertNode(_el$6, _el$22);
2324
2379
  _$setProp(_el$6, "flexDirection", "column");
2325
2380
  _$setProp(_el$6, "flexGrow", 1);
@@ -2394,7 +2449,7 @@ function TreePanel(props) {
2394
2449
  _$effect((_$p) => _$setProp(_el$12, "fg", theme.text.muted, _$p));
2395
2450
  return _el$11;
2396
2451
  }
2397
- }), _el$20);
2452
+ }), _el$17);
2398
2453
  _$insert(_el$6, _$createComponent(Show, {
2399
2454
  get when() {
2400
2455
  return latest()?.error;
@@ -2406,10 +2461,10 @@ function TreePanel(props) {
2406
2461
  _$effect((_$p) => _$setProp(_el$13, "fg", theme.text.feedback.error.base, _$p));
2407
2462
  return _el$13;
2408
2463
  }
2409
- }), _el$20);
2464
+ }), _el$17);
2410
2465
  _$insert(_el$6, _$createComponent(Show, {
2411
2466
  get when() {
2412
- return latest()?.tree?.stalled;
2467
+ return _$memo(() => !!!top())() && latest()?.tree?.stalled;
2413
2468
  },
2414
2469
  get children() {
2415
2470
  var _el$15 = _$createElement("text"), _el$16 = _$createTextNode(`This session is stalled: `);
@@ -2418,49 +2473,18 @@ function TreePanel(props) {
2418
2473
  _$effect((_$p) => _$setProp(_el$15, "fg", theme.text.feedback.error.base, _$p));
2419
2474
  return _el$15;
2420
2475
  }
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() {
2476
+ }), _el$17);
2477
+ _$use((value) => scroll = value, _el$17);
2478
+ _$setProp(_el$17, "flexGrow", 1);
2479
+ _$setProp(_el$17, "onSizeChange", function() {
2455
2480
  setListHeight(this.height);
2456
2481
  });
2457
- _$insert(_el$20, _$createComponent(For, {
2482
+ _$insert(_el$17, _$createComponent(For, {
2458
2483
  get each() {
2459
- return rows();
2484
+ return listed();
2460
2485
  },
2461
2486
  children: (row, index) => {
2462
2487
  const chosen = () => index() === selected();
2463
- const indent = " ".repeat(row.depth - 1);
2464
2488
  return (() => {
2465
2489
  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
2490
  _$insertNode(_el$24, _el$25);
@@ -2486,8 +2510,20 @@ function TreePanel(props) {
2486
2510
  _$setProp(_el$25, "flexShrink", 0);
2487
2511
  _$setProp(_el$26, "wrapMode", "none");
2488
2512
  _$setProp(_el$26, "flexShrink", 0);
2489
- _$insert(_el$26, indent, null);
2490
- _$insert(_el$26, () => chosen() ? "\u25B8 " : " ", null);
2513
+ _$insert(_el$26, () => chosen() ? "\u25B8 " : " ");
2514
+ _$insert(_el$25, _$createComponent(Show, {
2515
+ get when() {
2516
+ return guided().get(row.sessionID)?.row;
2517
+ },
2518
+ children: (guide) => (() => {
2519
+ var _el$32 = _$createElement("text");
2520
+ _$setProp(_el$32, "wrapMode", "none");
2521
+ _$setProp(_el$32, "flexShrink", 0);
2522
+ _$insert(_el$32, guide);
2523
+ _$effect((_$p) => _$setProp(_el$32, "fg", theme.text.muted, _$p));
2524
+ return _el$32;
2525
+ })()
2526
+ }), _el$27);
2491
2527
  _$insertNode(_el$27, _el$28);
2492
2528
  _$setProp(_el$27, "wrapMode", "none");
2493
2529
  _$setProp(_el$27, "flexShrink", 0);
@@ -2509,32 +2545,32 @@ function TreePanel(props) {
2509
2545
  return at();
2510
2546
  },
2511
2547
  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;
2548
+ var _el$33 = _$createElement("text");
2549
+ _$insertNode(_el$33, _$createTextNode(` \xB7 `));
2550
+ _$setProp(_el$33, "wrapMode", "none");
2551
+ _$setProp(_el$33, "flexShrink", 0);
2552
+ _$effect((_$p) => _$setProp(_el$33, "fg", theme.text.muted, _$p));
2553
+ return _el$33;
2518
2554
  }
2519
2555
  }), (() => {
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;
2556
+ var _el$35 = _$createElement("text");
2557
+ _$setProp(_el$35, "wrapMode", "none");
2558
+ _$setProp(_el$35, "flexShrink", 0);
2559
+ _$insert(_el$35, () => part.text);
2560
+ _$effect((_$p) => _$setProp(_el$35, "fg", partColor(row, part.kind), _$p));
2561
+ return _el$35;
2526
2562
  })()]
2527
2563
  }), null);
2528
2564
  _$insert(_el$24, _$createComponent(For, {
2529
2565
  get each() {
2530
- return detailLines(row, rows(), now());
2566
+ return lines(row);
2531
2567
  },
2532
2568
  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, {
2569
+ var _el$36 = _$createElement("text"), _el$37 = _$createTextNode(` `);
2570
+ _$insertNode(_el$36, _el$37);
2571
+ _$setProp(_el$36, "wrapMode", "none");
2572
+ _$insert(_el$36, () => guided().get(row.sessionID)?.detail ?? " ", null);
2573
+ _$insert(_el$36, _$createComponent(Show, {
2538
2574
  get when() {
2539
2575
  return _$memo(() => row.context !== undefined)() && Math.round(row.context * 100) > CONTEXT_WARN * 100;
2540
2576
  },
@@ -2545,18 +2581,18 @@ function TreePanel(props) {
2545
2581
  return line.split(" \xB7 ");
2546
2582
  },
2547
2583
  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", {
2584
+ var _el$38 = _$createElement("span");
2585
+ _$insert(_el$38, part);
2586
+ _$effect((_$p) => _$setProp(_el$38, "style", {
2551
2587
  fg: theme.text.feedback.warning.base
2552
2588
  }, _$p));
2553
- return _el$37;
2589
+ return _el$38;
2554
2590
  })() : part)]
2555
2591
  });
2556
2592
  }
2557
2593
  }), null);
2558
- _$effect((_$p) => _$setProp(_el$35, "fg", theme.text.muted, _$p));
2559
- return _el$35;
2594
+ _$effect((_$p) => _$setProp(_el$36, "fg", theme.text.muted, _$p));
2595
+ return _el$36;
2560
2596
  })()
2561
2597
  }), null);
2562
2598
  _$effect((_p$) => {
@@ -2580,7 +2616,38 @@ function TreePanel(props) {
2580
2616
  })();
2581
2617
  }
2582
2618
  }), null);
2583
- _$insert(_el$20, _$createComponent(Show, {
2619
+ _$insert(_el$17, _$createComponent(Show, {
2620
+ get when() {
2621
+ return _$memo(() => !!latest()?.tree)() && !rows().length;
2622
+ },
2623
+ get children() {
2624
+ return [(() => {
2625
+ var _el$18 = _$createElement("text"), _el$19 = _$createTextNode(`No session started with courier_spawn under this one.`);
2626
+ _$insertNode(_el$18, _el$19);
2627
+ _$insert(_el$18, () => top() ? " " : "", _el$19);
2628
+ _$effect((_$p) => _$setProp(_el$18, "fg", theme.text.muted, _$p));
2629
+ return _el$18;
2630
+ })(), _$createComponent(Show, {
2631
+ get when() {
2632
+ return artShown();
2633
+ },
2634
+ get children() {
2635
+ var _el$20 = _$createElement("box");
2636
+ _$setProp(_el$20, "flexDirection", "column");
2637
+ _$setProp(_el$20, "flexShrink", 0);
2638
+ _$setProp(_el$20, "paddingTop", 1);
2639
+ _$insert(_el$20, _$createComponent(Index, {
2640
+ get each() {
2641
+ return art2();
2642
+ },
2643
+ children: (line) => drawn(line)
2644
+ }));
2645
+ return _el$20;
2646
+ }
2647
+ })];
2648
+ }
2649
+ }), null);
2650
+ _$insert(_el$17, _$createComponent(Show, {
2584
2651
  get when() {
2585
2652
  return belowShown();
2586
2653
  },
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "opencode-courier",
3
- "version": "0.13.1",
3
+ "version": "0.13.3",
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,17 +56,17 @@
56
56
  "test:e2e": "e2e/run.sh"
57
57
  },
58
58
  "peerDependencies": {
59
- "@opencode/plugin": "2.2.1",
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.1",
64
- "@opencode/theme": "2.2.1",
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",
69
+ "effect": "4.0.6",
70
70
  "solid-js": "1.9.17",
71
71
  "typescript": "5.9.3"
72
72
  }