opencode-courier 0.4.1 → 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,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,11 +43,13 @@ 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
  /**
44
51
  * Adds the tree panel to the `session.panel` slot, opened by `/courier tree` or the palette, which follows the person
45
- * between sessions while open, and `/courier stop <session>`, which stops a session under the open one once confirmed.
52
+ * between sessions until closed, and `/courier stop <session>`, which stops a session under the open one once confirmed.
46
53
  */
47
54
  export declare function addTreePanel(ctx: Context): () => void;
48
55
  /**