opencode-courier 0.5.0 → 0.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,9 +1,9 @@
1
1
  import { Schema } from "effect";
2
2
  /** The sessions started under a session, as the prompt footer's status entry shows them. */
3
3
  declare const Summary: Schema.Struct<{
4
- /** Running, and waiting on no request. */
4
+ /** Running, and waiting on no request or form. */
5
5
  readonly running: Schema.Int;
6
- /** Waiting on a permission request or a question, which go to the person. */
6
+ /** Waiting on a permission request, a question or a form it shows, which go to the person. */
7
7
  readonly waiting: Schema.Int;
8
8
  /** Not running: done, or waiting for a message. */
9
9
  readonly idle: Schema.Int;
@@ -12,11 +12,9 @@ declare const Summary: Schema.Struct<{
12
12
  readonly reported: Schema.Int;
13
13
  readonly members: Schema.Int;
14
14
  }>>;
15
- /** Set when the subtree had more sessions than `maxTotal`, which were not counted. */
16
- readonly truncated: Schema.optionalKey<Schema.Int>;
17
15
  }>;
18
16
  export type Summary = typeof Summary.Type;
19
- /** A request a session waits on until the person answers it: a permission request, or its first question. */
17
+ /** A request a session waits on until the person answers it: a permission request, its first question, or a form it shows. */
20
18
  declare const Wait: Schema.Union<readonly [Schema.Struct<{
21
19
  readonly type: Schema.Literal<"permission">;
22
20
  readonly action: Schema.String;
@@ -24,6 +22,9 @@ declare const Wait: Schema.Union<readonly [Schema.Struct<{
24
22
  }>, Schema.Struct<{
25
23
  readonly type: Schema.Literal<"question">;
26
24
  readonly question: Schema.String;
25
+ }>, Schema.Struct<{
26
+ readonly type: Schema.Literal<"form">;
27
+ readonly title: Schema.String;
27
28
  }>]>;
28
29
  /** One session of the tree panel: what courier_tree shows of it, and what it is doing, in epoch milliseconds. */
29
30
  declare const Row: Schema.Struct<{
@@ -32,10 +33,12 @@ declare const Row: Schema.Struct<{
32
33
  readonly title: Schema.String;
33
34
  /** A child of the panel's session is at depth 1. */
34
35
  readonly depth: Schema.Int;
35
- /** `waiting` on a request to the person; `gone` when OpenCode no longer knows it. */
36
+ /** `waiting` on a request to the person or a form it shows them; `gone` when OpenCode no longer knows it. */
36
37
  readonly state: Schema.Literals<readonly ["running", "waiting", "idle", "gone"]>;
37
38
  /** How its last turn ended. */
38
39
  readonly outcome: Schema.optionalKey<Schema.Literals<readonly ["succeeded", "failed", "interrupted"]>>;
40
+ /** Set when courier_stop stopped it since its last prompt. */
41
+ readonly stopped: Schema.optionalKey<Schema.Boolean>;
39
42
  /** Its report since its last prompt: still owed, arrived (held with its group or delivered), or its turn ended without one. */
40
43
  readonly report: Schema.optionalKey<Schema.Struct<{
41
44
  readonly state: Schema.Literals<readonly ["owed", "reported", "ended"]>;
@@ -65,9 +68,12 @@ declare const Row: Schema.Struct<{
65
68
  }>, Schema.Struct<{
66
69
  readonly type: Schema.Literal<"question">;
67
70
  readonly question: Schema.String;
71
+ }>, Schema.Struct<{
72
+ readonly type: Schema.Literal<"form">;
73
+ readonly title: Schema.String;
68
74
  }>]>>;
69
75
  }>;
70
- /** The sessions under a session, as courier_tree reads them, in the order a tree is drawn. */
76
+ /** The sessions under a session, as courier_tree lists them, in the order a tree is drawn. */
71
77
  declare const Tree: Schema.Struct<{
72
78
  readonly rows: Schema.$Array<Schema.Struct<{
73
79
  readonly sessionID: Schema.String;
@@ -75,10 +81,12 @@ declare const Tree: Schema.Struct<{
75
81
  readonly title: Schema.String;
76
82
  /** A child of the panel's session is at depth 1. */
77
83
  readonly depth: Schema.Int;
78
- /** `waiting` on a request to the person; `gone` when OpenCode no longer knows it. */
84
+ /** `waiting` on a request to the person or a form it shows them; `gone` when OpenCode no longer knows it. */
79
85
  readonly state: Schema.Literals<readonly ["running", "waiting", "idle", "gone"]>;
80
86
  /** How its last turn ended. */
81
87
  readonly outcome: Schema.optionalKey<Schema.Literals<readonly ["succeeded", "failed", "interrupted"]>>;
88
+ /** Set when courier_stop stopped it since its last prompt. */
89
+ readonly stopped: Schema.optionalKey<Schema.Boolean>;
82
90
  /** Its report since its last prompt: still owed, arrived (held with its group or delivered), or its turn ended without one. */
83
91
  readonly report: Schema.optionalKey<Schema.Struct<{
84
92
  readonly state: Schema.Literals<readonly ["owed", "reported", "ended"]>;
@@ -108,9 +116,12 @@ declare const Tree: Schema.Struct<{
108
116
  }>, Schema.Struct<{
109
117
  readonly type: Schema.Literal<"question">;
110
118
  readonly question: Schema.String;
119
+ }>, Schema.Struct<{
120
+ readonly type: Schema.Literal<"form">;
121
+ readonly title: Schema.String;
111
122
  }>]>>;
112
123
  }>>;
113
- /** Set when the subtree had more sessions than `maxTotal`, which were left out. */
124
+ /** How many older sessions the listing left out, when it had more than `maxTotal`: the live ones come first, then the newest. */
114
125
  readonly truncated: Schema.optionalKey<Schema.Int>;
115
126
  }>;
116
127
  export type Row = typeof Row.Type;
@@ -127,6 +138,14 @@ declare const Stopped: Schema.Struct<{
127
138
  readonly cancelled: Schema.Int;
128
139
  }>;
129
140
  export type Stopped = typeof Stopped.Type;
141
+ /** What courier_cleanup did: forgot it, removed its worktree, found that already gone, or kept the worktree, saying why. */
142
+ declare const Forgotten: Schema.Union<readonly [Schema.Struct<{
143
+ readonly outcome: Schema.Literals<readonly ["forgotten", "removed", "gone"]>;
144
+ }>, Schema.Struct<{
145
+ readonly outcome: Schema.Literal<"kept">;
146
+ readonly reason: Schema.String;
147
+ }>]>;
148
+ export type Forgotten = typeof Forgotten.Type;
130
149
  export declare const treeRpc: {
131
150
  readonly id: "courier";
132
151
  readonly methods: {
@@ -140,28 +159,26 @@ export declare const treeRpc: {
140
159
  }>;
141
160
  readonly output: import("effect/StandardSchema").StandardSchemaV1<{
142
161
  readonly running: number;
162
+ readonly waiting: number;
143
163
  readonly idle: number;
144
164
  readonly groups: readonly {
145
165
  readonly name: string;
146
166
  readonly reported: number;
147
167
  readonly members: number;
148
168
  }[];
149
- readonly waiting: number;
150
- readonly truncated?: number | undefined;
151
169
  }, {
152
170
  readonly running: number;
171
+ readonly waiting: number;
153
172
  readonly idle: number;
154
173
  readonly groups: readonly {
155
174
  readonly name: string;
156
175
  readonly reported: number;
157
176
  readonly members: number;
158
177
  }[];
159
- readonly waiting: number;
160
- readonly truncated?: number | undefined;
161
178
  }> & Schema.Struct<{
162
- /** Running, and waiting on no request. */
179
+ /** Running, and waiting on no request or form. */
163
180
  readonly running: Schema.Int;
164
- /** Waiting on a permission request or a question, which go to the person. */
181
+ /** Waiting on a permission request, a question or a form it shows, which go to the person. */
165
182
  readonly waiting: Schema.Int;
166
183
  /** Not running: done, or waiting for a message. */
167
184
  readonly idle: Schema.Int;
@@ -170,8 +187,6 @@ export declare const treeRpc: {
170
187
  readonly reported: Schema.Int;
171
188
  readonly members: Schema.Int;
172
189
  }>>;
173
- /** Set when the subtree had more sessions than `maxTotal`, which were not counted. */
174
- readonly truncated: Schema.optionalKey<Schema.Int>;
175
190
  }>;
176
191
  };
177
192
  readonly tree: {
@@ -198,11 +213,15 @@ export declare const treeRpc: {
198
213
  } | {
199
214
  readonly type: "question";
200
215
  readonly question: string;
216
+ } | {
217
+ readonly type: "form";
218
+ readonly title: string;
201
219
  })[];
202
220
  readonly group?: {
203
221
  readonly name: string;
204
222
  readonly standing: string;
205
223
  } | undefined;
224
+ readonly stopped?: boolean | undefined;
206
225
  readonly idle?: number | undefined;
207
226
  readonly report?: {
208
227
  readonly progressed: boolean;
@@ -233,11 +252,15 @@ export declare const treeRpc: {
233
252
  } | {
234
253
  readonly type: "question";
235
254
  readonly question: string;
255
+ } | {
256
+ readonly type: "form";
257
+ readonly title: string;
236
258
  })[];
237
259
  readonly group?: {
238
260
  readonly name: string;
239
261
  readonly standing: string;
240
262
  } | undefined;
263
+ readonly stopped?: boolean | undefined;
241
264
  readonly idle?: number | undefined;
242
265
  readonly report?: {
243
266
  readonly progressed: boolean;
@@ -259,10 +282,12 @@ export declare const treeRpc: {
259
282
  readonly title: Schema.String;
260
283
  /** A child of the panel's session is at depth 1. */
261
284
  readonly depth: Schema.Int;
262
- /** `waiting` on a request to the person; `gone` when OpenCode no longer knows it. */
285
+ /** `waiting` on a request to the person or a form it shows them; `gone` when OpenCode no longer knows it. */
263
286
  readonly state: Schema.Literals<readonly ["running", "waiting", "idle", "gone"]>;
264
287
  /** How its last turn ended. */
265
288
  readonly outcome: Schema.optionalKey<Schema.Literals<readonly ["succeeded", "failed", "interrupted"]>>;
289
+ /** Set when courier_stop stopped it since its last prompt. */
290
+ readonly stopped: Schema.optionalKey<Schema.Boolean>;
266
291
  /** Its report since its last prompt: still owed, arrived (held with its group or delivered), or its turn ended without one. */
267
292
  readonly report: Schema.optionalKey<Schema.Struct<{
268
293
  readonly state: Schema.Literals<readonly ["owed", "reported", "ended"]>;
@@ -292,9 +317,12 @@ export declare const treeRpc: {
292
317
  }>, Schema.Struct<{
293
318
  readonly type: Schema.Literal<"question">;
294
319
  readonly question: Schema.String;
320
+ }>, Schema.Struct<{
321
+ readonly type: Schema.Literal<"form">;
322
+ readonly title: Schema.String;
295
323
  }>]>>;
296
324
  }>>;
297
- /** Set when the subtree had more sessions than `maxTotal`, which were left out. */
325
+ /** How many older sessions the listing left out, when it had more than `maxTotal`: the live ones come first, then the newest. */
298
326
  readonly truncated: Schema.optionalKey<Schema.Int>;
299
327
  }>;
300
328
  };
@@ -337,6 +365,38 @@ export declare const treeRpc: {
337
365
  readonly refused: import("effect/StandardSchema").StandardSchemaV1<{}, {}> & Schema.Struct<{}>;
338
366
  };
339
367
  };
368
+ /** Forgets `sessionID` as courier_cleanup called from its parent, without `force`; `by` must be above it. */
369
+ readonly cleanup: {
370
+ readonly input: import("effect/StandardSchema").StandardSchemaV1<{
371
+ readonly sessionID: string;
372
+ readonly by: string;
373
+ }, {
374
+ readonly sessionID: string;
375
+ readonly by: string;
376
+ }> & Schema.Struct<{
377
+ readonly sessionID: Schema.String;
378
+ readonly by: Schema.String;
379
+ }>;
380
+ readonly output: import("effect/StandardSchema").StandardSchemaV1<{
381
+ readonly outcome: "removed" | "gone" | "forgotten";
382
+ } | {
383
+ readonly outcome: "kept";
384
+ readonly reason: string;
385
+ }, {
386
+ readonly outcome: "removed" | "gone" | "forgotten";
387
+ } | {
388
+ readonly outcome: "kept";
389
+ readonly reason: string;
390
+ }> & Schema.Union<readonly [Schema.Struct<{
391
+ readonly outcome: Schema.Literals<readonly ["forgotten", "removed", "gone"]>;
392
+ }>, Schema.Struct<{
393
+ readonly outcome: Schema.Literal<"kept">;
394
+ readonly reason: Schema.String;
395
+ }>]>;
396
+ readonly errors: {
397
+ readonly refused: import("effect/StandardSchema").StandardSchemaV1<{}, {}> & Schema.Struct<{}>;
398
+ };
399
+ };
340
400
  };
341
401
  readonly events: {};
342
402
  };
package/dist/tree/rpc.js CHANGED
@@ -9,20 +9,19 @@ const Count = Schema.Int.check(Schema.isGreaterThanOrEqualTo(0));
9
9
  const Group = Schema.Struct({ name: Schema.String, reported: Count, members: Count });
10
10
  /** The sessions started under a session, as the prompt footer's status entry shows them. */
11
11
  const Summary = Schema.Struct({
12
- /** Running, and waiting on no request. */
12
+ /** Running, and waiting on no request or form. */
13
13
  running: Count,
14
- /** Waiting on a permission request or a question, which go to the person. */
14
+ /** Waiting on a permission request, a question or a form it shows, which go to the person. */
15
15
  waiting: Count,
16
16
  /** Not running: done, or waiting for a message. */
17
17
  idle: Count,
18
18
  groups: Schema.Array(Group),
19
- /** Set when the subtree had more sessions than `maxTotal`, which were not counted. */
20
- truncated: Schema.optionalKey(Count),
21
19
  });
22
- /** A request a session waits on until the person answers it: a permission request, or its first question. */
20
+ /** A request a session waits on until the person answers it: a permission request, its first question, or a form it shows. */
23
21
  const Wait = Schema.Union([
24
22
  Schema.Struct({ type: Schema.Literal("permission"), action: Schema.String, resources: Schema.Array(Schema.String) }),
25
23
  Schema.Struct({ type: Schema.Literal("question"), question: Schema.String }),
24
+ Schema.Struct({ type: Schema.Literal("form"), title: Schema.String }),
26
25
  ]);
27
26
  /** One session of the tree panel: what courier_tree shows of it, and what it is doing, in epoch milliseconds. */
28
27
  const Row = Schema.Struct({
@@ -31,10 +30,12 @@ const Row = Schema.Struct({
31
30
  title: Schema.String,
32
31
  /** A child of the panel's session is at depth 1. */
33
32
  depth: Count,
34
- /** `waiting` on a request to the person; `gone` when OpenCode no longer knows it. */
33
+ /** `waiting` on a request to the person or a form it shows them; `gone` when OpenCode no longer knows it. */
35
34
  state: Schema.Literals(["running", "waiting", "idle", "gone"]),
36
35
  /** How its last turn ended. */
37
36
  outcome: Schema.optionalKey(Schema.Literals(["succeeded", "failed", "interrupted"])),
37
+ /** Set when courier_stop stopped it since its last prompt. */
38
+ stopped: Schema.optionalKey(Schema.Boolean),
38
39
  /** Its report since its last prompt: still owed, arrived (held with its group or delivered), or its turn ended without one. */
39
40
  report: Schema.optionalKey(Schema.Struct({ state: Schema.Literals(["owed", "reported", "ended"]), status: Schema.optionalKey(Schema.String), progressed: Schema.Boolean })),
40
41
  /** Its join group, and its standing there: `held`, `out`, how it left, or `released`. */
@@ -49,10 +50,10 @@ const Row = Schema.Struct({
49
50
  tool: Schema.optionalKey(Schema.Struct({ name: Schema.String, at: Schema.Number })),
50
51
  waits: Schema.Array(Wait),
51
52
  });
52
- /** The sessions under a session, as courier_tree reads them, in the order a tree is drawn. */
53
+ /** The sessions under a session, as courier_tree lists them, in the order a tree is drawn. */
53
54
  const Tree = Schema.Struct({
54
55
  rows: Schema.Array(Row),
55
- /** Set when the subtree had more sessions than `maxTotal`, which were left out. */
56
+ /** How many older sessions the listing left out, when it had more than `maxTotal`: the live ones come first, then the newest. */
56
57
  truncated: Schema.optionalKey(Count),
57
58
  });
58
59
  /** What a stop did to each session, deepest first, as courier_stop reports it. */
@@ -61,6 +62,11 @@ const Stopped = Schema.Struct({
61
62
  /** How many scheduled messages were cancelled. */
62
63
  cancelled: Count,
63
64
  });
65
+ /** What courier_cleanup did: forgot it, removed its worktree, found that already gone, or kept the worktree, saying why. */
66
+ const Forgotten = Schema.Union([
67
+ Schema.Struct({ outcome: Schema.Literals(["forgotten", "removed", "gone"]) }),
68
+ Schema.Struct({ outcome: Schema.Literal("kept"), reason: Schema.String }),
69
+ ]);
64
70
  export const treeRpc = Rpc.define({
65
71
  id: "courier",
66
72
  methods: {
@@ -78,6 +84,12 @@ export const treeRpc = Rpc.define({
78
84
  output: Schema.toStandardSchemaV1(Stopped),
79
85
  errors: { refused: Schema.toStandardSchemaV1(Schema.Struct({})) },
80
86
  },
87
+ /** Forgets `sessionID` as courier_cleanup called from its parent, without `force`; `by` must be above it. */
88
+ cleanup: {
89
+ input: Schema.toStandardSchemaV1(Schema.Struct({ sessionID: Schema.String, by: Schema.String })),
90
+ output: Schema.toStandardSchemaV1(Forgotten),
91
+ errors: { refused: Schema.toStandardSchemaV1(Schema.Struct({})) },
92
+ },
81
93
  },
82
94
  events: {},
83
95
  });
@@ -1,20 +1,50 @@
1
- import type { CourierPorts, RosterEntry, RosterStorage } from "./ports.js";
1
+ import type { Plugin } from "@opencode/plugin";
2
+ import type { CourierPorts, Pending, RosterEntry, RosterStorage, ShownForm } from "./ports.js";
3
+ import { reportOf } from "./report.js";
4
+ import { type Exists } from "./roster.js";
2
5
  /** A session below the one a subtree is read from, with how many levels below it is: a child is at depth 1. */
3
6
  export interface Below {
4
7
  readonly entry: RosterEntry;
5
8
  readonly depth: number;
6
9
  }
10
+ /** What a read drops: the entries past retention at `now`, as `courier_children` drops them; `exists` tells a worktree still there. */
11
+ export interface Retention {
12
+ readonly now: number;
13
+ readonly exists?: Exists;
14
+ }
7
15
  /**
8
- * The sessions started from a session, directly or through others, read one prefix scan per parent, at most `max`
9
- * (`truncated` if more), in the order a tree is drawn.
16
+ * The sessions started from a session, directly or through others, every one of them, read one prefix scan per
17
+ * parent, in the order a tree is drawn; with `retention`, without the expired entries and what is under them.
10
18
  */
11
- export declare function subtree(storage: RosterStorage, rootID: string, max?: number): Promise<{
19
+ export declare function subtree(storage: RosterStorage, rootID: string, retention?: Retention): Promise<{
12
20
  nodes: Below[];
13
- truncated: boolean;
21
+ }>;
22
+ type Info = Awaited<ReturnType<Plugin.Context["session"]["get"]>>;
23
+ /** A session of a tree as a read looks it up: OpenCode's info of it, if it still knows it, and what it waits on. */
24
+ export interface Looked extends Below {
25
+ readonly info: Info | undefined;
26
+ readonly pending: ReadonlyArray<Pending>;
27
+ readonly forms: ReadonlyArray<ShownForm>;
28
+ }
29
+ /** A session as a listing looks it up: `Looked` plus its report state, and whether it is live (`isLive`). */
30
+ export interface Listed extends Looked {
31
+ readonly report: Awaited<ReturnType<typeof reportOf>>;
32
+ readonly live: boolean;
33
+ }
34
+ export type ReadPorts = Pick<CourierPorts, "storage" | "session" | "pending" | "forms" | "now" | "limits">;
35
+ /** Every session under a session, without those past retention, each looked up, in the order a tree is drawn. */
36
+ export declare function lookUp(ports: ReadPorts, rootID: string): Promise<Looked[]>;
37
+ /**
38
+ * The listing of at most `max` of the sessions under a session: the live ones first, then the newest, with what is
39
+ * above them, in the order a tree is drawn (`cut`); `leftOut` counts the rest.
40
+ */
41
+ export declare function readTree(ports: ReadPorts, rootID: string, max?: number): Promise<{
42
+ kept: Listed[];
43
+ leftOut: number;
14
44
  }>;
15
45
  /**
16
- * The subtree under a session, at most `maxTotal` sessions: what courier_children shows of each, plus its depth,
17
- * its parent and its report state.
46
+ * The subtree under a session as courier_tree lists it (`readTree`): each session as courier_children shows it, plus
47
+ * its depth, its parent and its report state; `truncated` counts the older sessions left out.
18
48
  */
19
49
  export declare function tree(ports: CourierPorts, rootID: string): Promise<{
20
50
  truncated?: number | undefined;
@@ -41,7 +71,7 @@ export declare function tree(ports: CourierPorts, rootID: string): Promise<{
41
71
  updated?: number | undefined;
42
72
  idle?: number | undefined;
43
73
  lastText?: string | undefined;
44
- pending?: readonly import("./ports.js").Pending[] | undefined;
74
+ pending?: readonly Pending[] | undefined;
45
75
  } | {
46
76
  report?: {
47
77
  status?: "done" | "partial" | "blocked" | "failed" | undefined;
@@ -61,7 +91,7 @@ export declare function tree(ports: CourierPorts, rootID: string): Promise<{
61
91
  updated?: number | undefined;
62
92
  idle?: number | undefined;
63
93
  lastText?: string | undefined;
64
- pending?: readonly import("./ports.js").Pending[] | undefined;
94
+ pending?: readonly Pending[] | undefined;
65
95
  } | {
66
96
  report?: {
67
97
  status?: "done" | "partial" | "blocked" | "failed" | undefined;
@@ -99,3 +129,4 @@ export declare function tree(ports: CourierPorts, rootID: string): Promise<{
99
129
  title: string;
100
130
  })[];
101
131
  }>;
132
+ export {};
@@ -1,24 +1,23 @@
1
+ import { busy, cut, isLive } from "./domain.js";
1
2
  import { lastStatus, reportOf } from "./report.js";
3
+ import { isExpired } from "./roster.js";
2
4
  import { described } from "./status.js";
3
5
  import { childStore } from "./store.js";
4
6
  /**
5
- * The sessions started from a session, directly or through others, read one prefix scan per parent, at most `max`
6
- * (`truncated` if more), in the order a tree is drawn.
7
+ * The sessions started from a session, directly or through others, every one of them, read one prefix scan per
8
+ * parent, in the order a tree is drawn; with `retention`, without the expired entries and what is under them.
7
9
  */
8
- export async function subtree(storage, rootID, max = Number.POSITIVE_INFINITY) {
10
+ export async function subtree(storage, rootID, retention) {
9
11
  const children = childStore(storage);
10
12
  const byParent = new Map();
11
13
  const seen = new Set([rootID]);
12
- let truncated = false;
13
14
  const expand = async (level, depth) => {
14
- if (!level.length || truncated)
15
+ if (!level.length)
15
16
  return;
16
17
  const found = await Promise.all(level.map((parentID) => children.children(parentID)));
17
18
  const next = [];
18
19
  for (const [index, entries] of found.entries()) {
19
- const fresh = entries.filter((entry) => !seen.has(entry.sessionID));
20
- const kept = fresh.slice(0, Math.max(0, max - (seen.size - 1)));
21
- truncated ||= kept.length < fresh.length;
20
+ const kept = entries.filter((entry) => !seen.has(entry.sessionID) && !(retention && isExpired(entry, retention.now, retention.exists)));
22
21
  for (const entry of kept)
23
22
  seen.add(entry.sessionID);
24
23
  if (kept.length)
@@ -28,25 +27,47 @@ export async function subtree(storage, rootID, max = Number.POSITIVE_INFINITY) {
28
27
  return expand(next, depth + 1);
29
28
  };
30
29
  await expand([rootID], 1);
31
- return { nodes: drawn(byParent, rootID), truncated };
30
+ return { nodes: drawn(byParent, rootID) };
32
31
  }
33
32
  /** The nodes in the order a tree is drawn: each followed by what it started. */
34
33
  function drawn(byParent, parentID) {
35
34
  return (byParent.get(parentID) ?? []).flatMap((node) => [node, ...drawn(byParent, node.entry.sessionID)]);
36
35
  }
36
+ /** Every session under a session, without those past retention, each looked up, in the order a tree is drawn. */
37
+ export async function lookUp(ports, rootID) {
38
+ const { nodes } = await subtree(ports.storage, rootID, { now: ports.now() });
39
+ return Promise.all(nodes.map(async (node) => {
40
+ const [info, pending, forms] = await Promise.all([
41
+ ports.session.get({ sessionID: node.entry.sessionID }).catch(() => undefined),
42
+ ports.pending(node.entry.sessionID).catch(() => []),
43
+ ports.forms(node.entry.sessionID).catch(() => []),
44
+ ]);
45
+ // A form outlives its session's turn only when its withdrawal was missed, as while no instance followed the events.
46
+ return { ...node, info, pending, forms: info && busy(info.time) ? forms : [] };
47
+ }));
48
+ }
37
49
  /**
38
- * The subtree under a session, at most `maxTotal` sessions: what courier_children shows of each, plus its depth,
39
- * its parent and its report state.
50
+ * The listing of at most `max` of the sessions under a session: the live ones first, then the newest, with what is
51
+ * above them, in the order a tree is drawn (`cut`); `leftOut` counts the rest.
52
+ */
53
+ export async function readTree(ports, rootID, max = ports.limits.maxTotal) {
54
+ const children = childStore(ports.storage);
55
+ const all = await Promise.all((await lookUp(ports, rootID)).map(async (node) => {
56
+ const report = await reportOf(children, node.entry.sessionID).catch(() => undefined);
57
+ return { ...node, report, live: isLive(node.info?.time, node.pending.length + node.forms.length, report) };
58
+ }));
59
+ const listing = cut(all.map((node) => ({ sessionID: node.entry.sessionID, parentID: node.entry.parentID, createdAt: node.entry.createdAt, live: node.live, node })), max);
60
+ return { kept: listing.kept.map(({ node }) => node), leftOut: listing.leftOut };
61
+ }
62
+ /**
63
+ * The subtree under a session as courier_tree lists it (`readTree`): each session as courier_children shows it, plus
64
+ * its depth, its parent and its report state; `truncated` counts the older sessions left out.
40
65
  */
41
66
  export async function tree(ports, rootID) {
42
- const { nodes, truncated } = await subtree(ports.storage, rootID, ports.limits.maxTotal);
67
+ const { kept, leftOut } = await readTree(ports, rootID);
43
68
  const children = childStore(ports.storage);
44
- const listed = await Promise.all(nodes.map(async ({ entry, depth }) => {
45
- const [info, report, last] = await Promise.all([
46
- described(ports, entry),
47
- reportOf(children, entry.sessionID).catch(() => undefined),
48
- lastStatus(children, entry.sessionID).catch(() => undefined),
49
- ]);
69
+ const listed = await Promise.all(kept.map(async ({ entry, depth, report }) => {
70
+ const [info, last] = await Promise.all([described(ports, entry), lastStatus(children, entry.sessionID).catch(() => undefined)]);
50
71
  return {
51
72
  ...info,
52
73
  parentID: entry.parentID,
@@ -56,5 +77,5 @@ export async function tree(ports, rootID) {
56
77
  : {}),
57
78
  };
58
79
  }));
59
- return { sessionID: rootID, sessions: listed, ...(truncated ? { truncated: ports.limits.maxTotal } : {}) };
80
+ return { sessionID: rootID, sessions: listed, ...(leftOut ? { truncated: leftOut } : {}) };
60
81
  }
@@ -1,7 +1,7 @@
1
1
  import type { CourierPorts } from "./ports.js";
2
2
  import type { Summary } from "./rpc.js";
3
3
  /**
4
- * The sessions started under a session, at most `maxTotal` as courier_tree reads them, counted by what they
5
- * are doing, and its open join groups, those of its own children, with how far each has reported.
4
+ * Every session started under a session, counted by what it is doing, and its open join groups, those of its own
5
+ * children, with how far each has reported.
6
6
  */
7
7
  export declare function summary(ports: CourierPorts, sessionID: string): Promise<Summary>;
@@ -1,35 +1,27 @@
1
1
  import { busy } from "./domain.js";
2
2
  import { groupStandings } from "./group.js";
3
3
  import { groupStore } from "./store.js";
4
- import { subtree } from "./subtree.js";
5
- /** What a session is doing: waiting on a request it made, running, or idle; undefined when OpenCode no longer knows it. */
6
- async function activity(ports, sessionID) {
7
- const [info, pending] = await Promise.all([
8
- ports.session.get({ sessionID }).catch(() => undefined),
9
- ports.pending(sessionID).catch(() => []),
10
- ]);
4
+ import { lookUp } from "./subtree.js";
5
+ /** What a session is doing: waiting on a request it made or a form it shows, running, or idle; undefined when OpenCode no longer knows it. */
6
+ function activity({ info, pending, forms }) {
11
7
  if (!info)
12
8
  return undefined;
13
- if (pending.length)
9
+ if (pending.length || forms.length)
14
10
  return "waiting";
15
11
  return busy(info.time) ? "running" : "idle";
16
12
  }
17
13
  /**
18
- * The sessions started under a session, at most `maxTotal` as courier_tree reads them, counted by what they
19
- * are doing, and its open join groups, those of its own children, with how far each has reported.
14
+ * Every session started under a session, counted by what it is doing, and its open join groups, those of its own
15
+ * children, with how far each has reported.
20
16
  */
21
17
  export async function summary(ports, sessionID) {
22
- const counted = async () => {
23
- const { nodes, truncated } = await subtree(ports.storage, sessionID, ports.limits.maxTotal);
24
- return { activities: await Promise.all(nodes.map(({ entry }) => activity(ports, entry.sessionID))), truncated };
25
- };
26
- const [{ activities, truncated }, standings] = await Promise.all([counted(), groupStandings(groupStore(ports.storage), sessionID)]);
18
+ const [looked, standings] = await Promise.all([lookUp(ports, sessionID), groupStandings(groupStore(ports.storage), sessionID)]);
19
+ const activities = looked.map(activity);
27
20
  const count = (kind) => activities.filter((item) => item === kind).length;
28
21
  return {
29
22
  running: count("running"),
30
23
  waiting: count("waiting"),
31
24
  idle: count("idle"),
32
25
  groups: standings.map(({ name, reported, members }) => ({ name, reported, members })),
33
- ...(truncated ? { truncated: ports.limits.maxTotal } : {}),
34
26
  };
35
27
  }
package/dist/tree/text.js CHANGED
@@ -160,7 +160,10 @@ export function cleanupText(result) {
160
160
  export function treeText(tree) {
161
161
  if (!tree.sessions.length)
162
162
  return "No sessions started with courier_spawn.";
163
- const note = tree.truncated === undefined ? "" : `\nOnly the first ${tree.truncated} sessions are listed (maxTotal); courier_tree on one of them lists what is under it.`;
163
+ if (tree.truncated === undefined)
164
+ return JSON.stringify(tree, null, 2);
165
+ const older = tree.truncated === 1 ? "1 older session is" : `${tree.truncated} older sessions are`;
166
+ const note = `\n${older} not listed: at most maxTotal are, the live ones first, then the newest; courier_tree on a listed session lists what is under it.`;
164
167
  return JSON.stringify(tree, null, 2) + note;
165
168
  }
166
169
  /** What courier_stop did to each session, and to the messages and worktrees. */
@@ -128,7 +128,8 @@ export function addTreeTools(tools, ports) {
128
128
  options: { codemode: false },
129
129
  description: "List the whole tree of sessions under this one (or under sessionID): every session started with courier_spawn " +
130
130
  "from it, directly or through others, each with its depth, what courier_children shows, and whether it still " +
131
- "owes a report, sent progress, or ended without one. For a one-off look, not for waiting; long trees are cut at maxTotal sessions.",
131
+ "owes a report, sent progress, or ended without one. For a one-off look, not for waiting. A long tree lists at most " +
132
+ "maxTotal sessions, the live ones first, then the newest, and says how many older ones are left out.",
132
133
  input: TreeInput,
133
134
  execute: async (input, context) => {
134
135
  const result = await tree(ports.courier, input.sessionID || context.sessionID).catch(rethrow("courier_tree"));
@@ -1,6 +1,8 @@
1
1
  import type { Plugin } from "@opencode/plugin/tui";
2
- import { type Row, type Stopped, type Summary, type Wait } from "./rpc.js";
2
+ import { type Forgotten, type Row, type Stopped, type Summary, type Wait } from "./rpc.js";
3
3
  type Context = Plugin.Context;
4
+ /** The events after which a summary may read differently: a turn started or ended, a request or form, a session made or removed. */
5
+ export declare const CHANGES: Set<string>;
4
6
  /** How long after the first of a burst of changes the entry asks the server again. */
5
7
  export declare const SETTLE_MS = 250;
6
8
  /** The panel's settle: each of its asks reads every row's messages, so a busy tree is asked at most once a second. */
@@ -9,8 +11,11 @@ export declare const PANEL_SETTLE_MS = 1000;
9
11
  export declare const PANEL = "courier-tree";
10
12
  /** How often the panel redraws its running times and how long ago each last tool call was. */
11
13
  export declare const TICK_MS = 1000;
12
- /** The status entry's text, such as "courier 2 running · 1 waiting on you · group pair 1/2"; empty when there is nothing to show. */
13
- export declare function summaryLine(summary: Summary): string;
14
+ /**
15
+ * The status entry's text, such as "courier 1 waiting on you · 2 running · group pair 1/2"; empty when there is nothing to show.
16
+ * Within `maxWidth` it keeps whole parts, most important first, and counts the groups it drops: "· +2 groups".
17
+ */
18
+ export declare function summaryLine(summary: Summary, maxWidth?: number): string;
14
19
  /** A span of time, coarsest first: "45s", "3m 05s", "2h 07m". */
15
20
  export declare function duration(ms: number): string;
16
21
  /** How many of a session's children have reported, out of those it started, among the rows read. */
@@ -23,7 +28,7 @@ export declare function bar({ reported, started }: {
23
28
  reported: number;
24
29
  started: number;
25
30
  }, width?: number): string;
26
- /** What a paused session waits on, such as "asks permission: shell echo hi". */
31
+ /** What a paused session waits on, such as "asks permission: shell echo hi" or "shows a form: Web Search". */
27
32
  export declare function waitLine(wait: Wait): string;
28
33
  /** What a row's session is doing, its report and its group standing: "idle · reported done · group pair held". */
29
34
  export declare function statusLine(row: Row): string;
@@ -38,6 +43,8 @@ export declare function detailLines(row: Row, rows: ReadonlyArray<Row>, now: num
38
43
  export declare function addTreeStatus(ctx: Context, onWaiting?: (sessionID: string) => void): () => void;
39
44
  /** The toast after a stop: how many sessions were interrupted, and how many scheduled messages were cancelled. */
40
45
  export declare function stoppedLine({ stopped, cancelled }: Stopped): string;
46
+ /** The toast after a forget, as courier_cleanup reports it: forgotten, its worktree removed or already gone, or kept and why. */
47
+ export declare function forgottenLine(title: string, result: Forgotten): string;
41
48
  /** The usage the `/courier` command shows for an input it does not know. */
42
49
  export declare const USAGE = "/courier tree opens the session tree; /courier stop [session] stops a session and those under it";
43
50
  /**