opencode-courier 0.10.1 → 0.10.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -15,6 +15,7 @@ export declare function status(ports: CourierPorts, input: StatusInput): Promise
15
15
  outcome: "failed" | "interrupted" | "succeeded" | undefined;
16
16
  updated: number;
17
17
  idle: number | undefined;
18
+ archived: number | undefined;
18
19
  lastText: string | undefined;
19
20
  pending: readonly import("./ports.js").Pending[] | undefined;
20
21
  }>>;
@@ -38,6 +39,7 @@ export declare function listChildren(ports: CourierPorts, parentID: string): Pro
38
39
  outcome?: "failed" | "interrupted" | "succeeded" | undefined;
39
40
  updated?: number | undefined;
40
41
  idle?: number | undefined;
42
+ archived?: number | undefined;
41
43
  lastText?: string | undefined;
42
44
  pending?: readonly import("./ports.js").Pending[] | undefined;
43
45
  } | {
@@ -51,6 +53,7 @@ export declare function listChildren(ports: CourierPorts, parentID: string): Pro
51
53
  outcome?: "failed" | "interrupted" | "succeeded" | undefined;
52
54
  updated?: number | undefined;
53
55
  idle?: number | undefined;
56
+ archived?: number | undefined;
54
57
  lastText?: string | undefined;
55
58
  pending?: readonly import("./ports.js").Pending[] | undefined;
56
59
  } | {
@@ -88,6 +91,7 @@ export declare function described(ports: CourierPorts, entry: RosterEntry): Prom
88
91
  outcome?: "failed" | "interrupted" | "succeeded" | undefined;
89
92
  updated?: number | undefined;
90
93
  idle?: number | undefined;
94
+ archived?: number | undefined;
91
95
  lastText?: string | undefined;
92
96
  pending?: readonly import("./ports.js").Pending[] | undefined;
93
97
  } | {
@@ -101,6 +105,7 @@ export declare function described(ports: CourierPorts, entry: RosterEntry): Prom
101
105
  outcome?: "failed" | "interrupted" | "succeeded" | undefined;
102
106
  updated?: number | undefined;
103
107
  idle?: number | undefined;
108
+ archived?: number | undefined;
104
109
  lastText?: string | undefined;
105
110
  pending?: readonly import("./ports.js").Pending[] | undefined;
106
111
  } | {
@@ -17,6 +17,7 @@ export async function status(ports, input) {
17
17
  outcome: info.outcome,
18
18
  updated: info.time.updated,
19
19
  idle: info.time.idle,
20
+ archived: info.time.archived,
20
21
  lastText,
21
22
  pending: pending.length ? pending : undefined,
22
23
  });
@@ -20,9 +20,12 @@ export declare function subtree(storage: RosterStorage, rootID: string, retentio
20
20
  nodes: Below[];
21
21
  }>;
22
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. */
23
+ /** A session of a tree as a read looks it up: OpenCode's info of it, if it still knows it, whether it runs, and what it waits on. */
24
24
  export interface Looked extends Below {
25
25
  readonly info: Info | undefined;
26
+ readonly running: boolean;
27
+ /** When its last turn ended, if it does not run and that is known. */
28
+ readonly endedAt?: number;
26
29
  readonly pending: ReadonlyArray<Pending>;
27
30
  readonly forms: ReadonlyArray<ShownForm>;
28
31
  }
@@ -70,6 +73,7 @@ export declare function tree(ports: CourierPorts, rootID: string): Promise<{
70
73
  outcome?: "failed" | "interrupted" | "succeeded" | undefined;
71
74
  updated?: number | undefined;
72
75
  idle?: number | undefined;
76
+ archived?: number | undefined;
73
77
  lastText?: string | undefined;
74
78
  pending?: readonly Pending[] | undefined;
75
79
  } | {
@@ -90,6 +94,7 @@ export declare function tree(ports: CourierPorts, rootID: string): Promise<{
90
94
  outcome?: "failed" | "interrupted" | "succeeded" | undefined;
91
95
  updated?: number | undefined;
92
96
  idle?: number | undefined;
97
+ archived?: number | undefined;
93
98
  lastText?: string | undefined;
94
99
  pending?: readonly Pending[] | undefined;
95
100
  } | {
@@ -1,6 +1,7 @@
1
- import { busy, cut, isLive } from "./domain.js";
1
+ import { cut, isLive } from "./domain.js";
2
2
  import { lastStatus, reportOf } from "./report.js";
3
3
  import { isExpired } from "./roster.js";
4
+ import { turnOf } from "./running.js";
4
5
  import { described } from "./status.js";
5
6
  import { childStore } from "./store.js";
6
7
  /**
@@ -42,8 +43,10 @@ export async function lookUp(ports, rootID) {
42
43
  ports.pending(node.entry.sessionID).catch(() => []),
43
44
  ports.forms(node.entry.sessionID).catch(() => []),
44
45
  ]);
46
+ const turn = info === undefined ? undefined : await turnOf(ports.session, node.entry.sessionID, info.time);
47
+ const running = turn?.running ?? false;
45
48
  // 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 : [] };
49
+ return { ...node, info, running, ...(turn?.endedAt !== undefined ? { endedAt: turn.endedAt } : {}), pending, forms: running ? forms : [] };
47
50
  }));
48
51
  }
49
52
  /**
@@ -54,7 +57,7 @@ export async function readTree(ports, rootID, max = ports.limits.maxTotal) {
54
57
  const children = childStore(ports.storage);
55
58
  const all = await Promise.all((await lookUp(ports, rootID)).map(async (node) => {
56
59
  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) };
60
+ return { ...node, report, live: isLive(node.info ? node.running : undefined, node.pending.length + node.forms.length, report) };
58
61
  }));
59
62
  const listing = cut(all.map((node) => ({ sessionID: node.entry.sessionID, parentID: node.entry.parentID, createdAt: node.entry.createdAt, live: node.live, node })), max);
60
63
  return { kept: listing.kept.map(({ node }) => node), leftOut: listing.leftOut };
@@ -1,20 +1,19 @@
1
- import { busy } from "./domain.js";
2
1
  import { groupStandings } from "./group.js";
3
2
  import { groupStore } from "./store.js";
4
3
  import { lookUp } from "./subtree.js";
5
4
  import { stalledAmong } from "./wake.js";
6
5
  /**
7
6
  * What a session is doing: waiting on a request it made or a form it shows, stalled, running, or idle; undefined when
8
- * OpenCode no longer knows it. A message waiting in its inbox makes a stalled session look busy.
7
+ * OpenCode no longer knows it. A message waiting in its inbox is no turn (`runs`); a stalled one reads stalled.
9
8
  */
10
- function activity({ info, pending, forms, entry }, stalled) {
9
+ function activity({ info, running, pending, forms, entry }, stalled) {
11
10
  if (!info)
12
11
  return undefined;
13
12
  if (pending.length || forms.length)
14
13
  return "waiting";
15
14
  if (stalled.has(entry.sessionID))
16
15
  return "stalled";
17
- return busy(info.time) ? "running" : "idle";
16
+ return running ? "running" : "idle";
18
17
  }
19
18
  /**
20
19
  * Every session started under a session, counted by what it is doing (the session itself too, when it is stalled), and its
@@ -4,7 +4,10 @@ import { type Accessor } from "solid-js";
4
4
  import { type MascotState, type Token } from "./mascot.js";
5
5
  import { type Forgotten, type Row, type Stopped, type Summary, type Wait } from "./rpc.js";
6
6
  type Context = Plugin.Context;
7
- /** The events after which a summary may read differently: a turn started or ended, a request or form, a session made or removed. */
7
+ /**
8
+ * The events after which a summary may read differently: a turn started or ended, a request or form, a session made, removed,
9
+ * archived or unarchived.
10
+ */
8
11
  export declare const CHANGES: Set<string>;
9
12
  /** How long after the first of a burst of changes the entry asks the server again. */
10
13
  export declare const SETTLE_MS = 250;
@@ -39,6 +39,16 @@ export interface Execution {
39
39
  * Returns whether it is watched, which one sent with `resume: false` is not.
40
40
  */
41
41
  export declare function watchWake(ports: WatchPorts, event: Enqueued): Promise<boolean>;
42
+ /**
43
+ * Watches a message `courier_send` has just had enqueued, without waiting for its enqueued event, so the limits see a reported child woken
44
+ * for it at once; the event leaves this row be. Dropped again if the message is already stored, delivered before it was written.
45
+ */
46
+ export declare function noteSent(ports: Pick<WatchPorts, "storage" | "session" | "now">, sent: {
47
+ readonly sessionID: string;
48
+ readonly inboxID: string;
49
+ readonly text: string;
50
+ readonly description?: string;
51
+ }): Promise<void>;
42
52
  /** Stops watching a message once it is delivered or cancelled. */
43
53
  export declare function settleWake(ports: Pick<WatchPorts, "storage">, event: InboxSettled): Promise<void>;
44
54
  /** A turn started in a session: the messages waiting there are its to take. */
@@ -54,16 +64,24 @@ export declare function wakeEnded(ports: WatchPorts, event: Execution & {
54
64
  }): Promise<void>;
55
65
  /** Forgets the messages watched in a session's inbox: it was deleted, or stopped, which nothing is to wake again. */
56
66
  export declare function forgetWakes(ports: Pick<WatchPorts, "storage">, sessionID: string): Promise<void>;
57
- /** The ports the scheduler's tick wakes sessions through: what every hub member's `later` ports have. */
67
+ /** The ports the scheduler's tick wakes sessions through: what every hub-v2 member's `later` ports have, whose session is its `ctx.session`. */
58
68
  export type WakePorts = GroupPorts & {
69
+ readonly session: Pick<WatchPorts["session"], "get" | "context">;
59
70
  readonly now: () => number;
60
71
  };
61
72
  /**
62
- * Sends once more, under its inbox id, each courier message an idle session has left waiting for `WAKE_MS`, and flags
63
- * stalled one still waiting `WAKE_MS` later; run by the scheduler's owner, `claimed` keeping a tick alongside off it.
73
+ * Sends once more, under its inbox id, each courier message an idle session has left waiting for `WAKE_MS`, flags stalled one still
74
+ * waiting `WAKE_MS` later, and marks running one a turn it shows will take; run by the scheduler's owner, `claimed` keeping a tick alongside off it.
64
75
  */
65
76
  export declare function wakeMissed(ports: WakePorts, claimed: Set<string>): Promise<void>;
66
77
  /** 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. */
67
78
  export declare function missed(wake: Wake, now: number): boolean;
79
+ /**
80
+ * Whether a courier message sent with a wake waits undelivered in the session's inbox while it is idle, waiting or, unless
81
+ * `stalled` is false, stalled; one a running turn is to take is not counted, as `runs` says that turn runs.
82
+ */
83
+ export declare function waitsInInbox(ports: Pick<WatchPorts, "storage">, sessionID: string, { stalled }?: {
84
+ stalled?: boolean | undefined;
85
+ }): Promise<boolean>;
68
86
  /** The sessions among `sessionIDs` flagged stalled, each with when its oldest stalled message was enqueued. */
69
87
  export declare function stalledAmong(ports: Pick<WatchPorts, "storage">, sessionIDs: ReadonlyArray<string>): Promise<Map<string, number>>;
package/dist/tree/wake.js CHANGED
@@ -1,8 +1,7 @@
1
+ import { busy, endsIdle } from "./domain.js";
1
2
  import { wakeStore } from "./store.js";
2
3
  /** How long a courier message may wait in an idle session's inbox before its wake counts as missed. */
3
4
  export const WAKE_MS = 30_000;
4
- /** Whether a session's stored messages end with the marker of an ended turn; one with none yet is taken to run. */
5
- const endsIdle = (messages) => messages.at(-1)?.type === "idle";
6
5
  /**
7
6
  * Watches a courier message enqueued for a session until it is delivered, noting whether the session runs.
8
7
  * Returns whether it is watched, which one sent with `resume: false` is not.
@@ -16,19 +15,30 @@ export async function watchWake(ports, event) {
16
15
  if (payload.metadata.resume === false)
17
16
  return false;
18
17
  const at = event.created ?? ports.now();
19
- // Read after the enqueue: a turn it started has stored messages since, and an unknown session is taken to run.
20
- const idle = await ports.session.context({ sessionID }).then(endsIdle, () => false);
21
- await wakeStore(ports.storage).put({
22
- sessionID,
23
- inboxID,
24
- at,
25
- text: payload.text,
26
- ...(typeof payload.description === "string" ? { description: payload.description } : {}),
27
- running: !idle,
28
- ...(idle ? { idleAt: at } : {}),
29
- });
18
+ // A row `noteSent` wrote already is kept: it may have moved on since, and is no later than this one.
19
+ if ((await wakeStore(ports.storage).of(sessionID)).some((wake) => wake.inboxID === inboxID))
20
+ return true;
21
+ await watch(ports, { sessionID, inboxID, at, text: payload.text, ...(typeof payload.description === "string" ? { description: payload.description } : {}) });
30
22
  return true;
31
23
  }
24
+ /**
25
+ * Watches a message `courier_send` has just had enqueued, without waiting for its enqueued event, so the limits see a reported child woken
26
+ * for it at once; the event leaves this row be. Dropped again if the message is already stored, delivered before it was written.
27
+ */
28
+ export async function noteSent(ports, sent) {
29
+ const store = wakeStore(ports.storage);
30
+ if ((await store.of(sent.sessionID)).some((wake) => wake.inboxID === sent.inboxID))
31
+ return;
32
+ await watch(ports, { ...sent, at: ports.now() });
33
+ const stored = await ports.session.context({ sessionID: sent.sessionID }).then((messages) => messages.some((message) => message.id === sent.inboxID), () => false);
34
+ if (stored)
35
+ await store.remove(sent.sessionID, sent.inboxID);
36
+ }
37
+ async function watch(ports, wake) {
38
+ // Read after the enqueue: a turn it started has stored messages since, and an unknown session is taken to run.
39
+ const idle = await ports.session.context({ sessionID: wake.sessionID }).then(endsIdle, () => false);
40
+ await wakeStore(ports.storage).put({ ...wake, running: !idle, ...(idle ? { idleAt: wake.at } : {}) });
41
+ }
32
42
  /** Stops watching a message once it is delivered or cancelled. */
33
43
  export async function settleWake(ports, event) {
34
44
  await wakeStore(ports.storage).remove(event.data.sessionID, event.data.inboxID);
@@ -44,15 +54,18 @@ export async function wakeStarted(ports, event) {
44
54
  * subscription; one still waiting is watched as waiting in an idle session from now.
45
55
  */
46
56
  export async function wakeEnded(ports, event) {
47
- if (event.data.reason === "shutdown")
48
- return;
49
57
  const { sessionID } = event.data;
50
58
  const store = wakeStore(ports.storage);
51
59
  const waiting = await store.of(sessionID);
52
60
  if (!waiting.length)
53
61
  return;
54
62
  const at = event.created ?? ports.now();
55
- const stored = new Set((await ports.session.context({ sessionID }).catch(() => [])).map((message) => message.id));
63
+ const messages = await ports.session.context({ sessionID }).catch(() => []);
64
+ // A plain decline reports a shutdown after the aborted step that ends its turn; a real one between steps ends none. One during a
65
+ // step reads as ended too, until the turn the next boot resumes is seen to start or `wakeAgain` finds it running.
66
+ if (event.data.reason === "shutdown" && !endsIdle(messages))
67
+ return;
68
+ const stored = new Set(messages.map((message) => message.id));
56
69
  await Promise.all(waiting.map((wake) => (stored.has(wake.inboxID) ? store.remove(sessionID, wake.inboxID) : store.put({ ...wake, running: false, idleAt: at }))));
57
70
  }
58
71
  /** Forgets the messages watched in a session's inbox: it was deleted, or stopped, which nothing is to wake again. */
@@ -61,8 +74,8 @@ export async function forgetWakes(ports, sessionID) {
61
74
  await Promise.all((await store.of(sessionID)).map((wake) => store.remove(sessionID, wake.inboxID)));
62
75
  }
63
76
  /**
64
- * Sends once more, under its inbox id, each courier message an idle session has left waiting for `WAKE_MS`, and flags
65
- * stalled one still waiting `WAKE_MS` later; run by the scheduler's owner, `claimed` keeping a tick alongside off it.
77
+ * Sends once more, under its inbox id, each courier message an idle session has left waiting for `WAKE_MS`, flags stalled one still
78
+ * waiting `WAKE_MS` later, and marks running one a turn it shows will take; run by the scheduler's owner, `claimed` keeping a tick alongside off it.
66
79
  */
67
80
  export async function wakeMissed(ports, claimed) {
68
81
  const store = wakeStore(ports.storage);
@@ -86,6 +99,11 @@ async function wakeAgain(ports, store, wake, now) {
86
99
  const current = (await store.of(wake.sessionID)).find((one) => one.inboxID === wake.inboxID);
87
100
  if (!current || !missed(current, now))
88
101
  return;
102
+ // 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)) {
104
+ const { idleAt: _, ...running } = current;
105
+ return await store.put({ ...running, running: true });
106
+ }
89
107
  if (current.rewoken !== undefined) {
90
108
  ports.log(`courier wake: ${wake.sessionID} has not taken message ${wake.inboxID} since it was woken again; flagged stalled`);
91
109
  return await store.put({ ...current, stalled: now });
@@ -109,6 +127,21 @@ async function wakeAgain(ports, store, wake, now) {
109
127
  ports.log(`courier wake: could not wake ${wake.sessionID} again for ${wake.inboxID}: ${String(error)}`);
110
128
  }
111
129
  }
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
+ const info = await ports.session.get({ sessionID }).catch(() => undefined);
133
+ if (!info || !busy(info.time))
134
+ return false;
135
+ const messages = await ports.session.context({ sessionID }).catch(() => []);
136
+ return messages.length > 0 && !endsIdle(messages);
137
+ }
138
+ /**
139
+ * Whether a courier message sent with a wake waits undelivered in the session's inbox while it is idle, waiting or, unless
140
+ * `stalled` is false, stalled; one a running turn is to take is not counted, as `runs` says that turn runs.
141
+ */
142
+ export async function waitsInInbox(ports, sessionID, { stalled = true } = {}) {
143
+ return (await wakeStore(ports.storage).of(sessionID)).some((wake) => !wake.running && (stalled || wake.stalled === undefined));
144
+ }
112
145
  /** The sessions among `sessionIDs` flagged stalled, each with when its oldest stalled message was enqueued. */
113
146
  export async function stalledAmong(ports, sessionIDs) {
114
147
  const wanted = new Set(sessionIDs);
package/dist/tui.js CHANGED
@@ -312,7 +312,7 @@ function addMemoryView(ctx) {
312
312
  } : chosen(value)), requested]);
313
313
  }
314
314
  const showList = (read, current) => {
315
- const open2 = openSession();
315
+ const open = openSession();
316
316
  const now = Date.now();
317
317
  return show({
318
318
  title: `Memory (${read.records.length})`,
@@ -321,7 +321,7 @@ function addMemoryView(ctx) {
321
321
  current
322
322
  } : {},
323
323
  options: read.records.map((record) => ({
324
- title: recordTitle(record, open2),
324
+ title: recordTitle(record, open),
325
325
  value: record,
326
326
  category: recordCategory(record, now),
327
327
  description: recordDescription(record),
@@ -865,12 +865,12 @@ function addAsksInbox(ctx, follows) {
865
865
  } = ask;
866
866
  const answers = [];
867
867
  for (const [index, prompt] of question.questions.entries()) {
868
- const count2 = question.questions.length > 1 ? ` (${index + 1}/${question.questions.length})` : "";
868
+ const count = question.questions.length > 1 ? ` (${index + 1}/${question.questions.length})` : "";
869
869
  const choices = prompt.options.map((option) => ({
870
870
  ...option,
871
871
  value: option.label
872
872
  }));
873
- const picked = await choose(`${prompt.header}: ${prompt.question}${count2}`, choices, {
873
+ const picked = await choose(`${prompt.header}: ${prompt.question}${count}`, choices, {
874
874
  multiple: !!prompt.multiple,
875
875
  own: true
876
876
  });
@@ -1025,26 +1025,26 @@ function addAsksInbox(ctx, follows) {
1025
1025
  const {
1026
1026
  form
1027
1027
  } = ask;
1028
- const answer2 = {};
1028
+ const answer = {};
1029
1029
  for (const field of form.fields) {
1030
- if (!active(field, answer2))
1030
+ if (!active(field, answer))
1031
1031
  continue;
1032
1032
  if ("hidden" in field && field.hidden) {
1033
1033
  if ("default" in field && field.default !== undefined)
1034
- answer2[field.key] = field.default;
1034
+ answer[field.key] = field.default;
1035
1035
  continue;
1036
1036
  }
1037
1037
  const value = await answerField(form, field);
1038
1038
  if (value === undefined)
1039
1039
  return false;
1040
1040
  if (value !== SKIP)
1041
- answer2[field.key] = value;
1041
+ answer[field.key] = value;
1042
1042
  }
1043
1043
  try {
1044
1044
  await ctx.client.session.form.reply({
1045
1045
  sessionID: form.sessionID,
1046
1046
  formID: form.id,
1047
- answer: answer2
1047
+ answer
1048
1048
  });
1049
1049
  } catch (error) {
1050
1050
  const tag = error?._tag;
@@ -1116,8 +1116,8 @@ function addAsksInbox(ctx, follows) {
1116
1116
  try {
1117
1117
  return await Promise.race([choosing.then((chosen) => ({
1118
1118
  chosen
1119
- })), newer.then((read2) => ({
1120
- read: read2
1119
+ })), newer.then((read) => ({
1120
+ read
1121
1121
  }))]);
1122
1122
  } finally {
1123
1123
  listing = undefined;
@@ -1502,7 +1502,7 @@ var treeRpc = Rpc3.define({
1502
1502
  });
1503
1503
 
1504
1504
  // src/tree/tui.tsx
1505
- var CHANGES = new Set(["session.created", "session.deleted", "session.status", "session.idle", "session.execution.started", "session.execution.succeeded", "session.execution.failed", "session.execution.interrupted", "session.inbox.delivered", "session.synthetic", "permission.asked", "permission.replied", "form.created", "form.replied", "form.cancelled"]);
1505
+ var CHANGES = new Set(["session.created", "session.deleted", "session.status", "session.idle", "session.execution.started", "session.execution.succeeded", "session.execution.failed", "session.execution.interrupted", "session.inbox.delivered", "session.synthetic", "session.archived", "session.unarchived", "permission.asked", "permission.replied", "form.created", "form.replied", "form.cancelled"]);
1506
1506
  var PANEL_CHANGES = new Set([...CHANGES, "session.tool.called", "session.tool.success", "session.tool.failed", "session.usage.updated", "session.renamed"]);
1507
1507
  var SETTLE_MS2 = 250;
1508
1508
  var PANEL_SETTLE_MS = 1000;
@@ -1534,14 +1534,14 @@ function createMotion(enabled) {
1534
1534
  const now = untrack(moving);
1535
1535
  for (const one of now)
1536
1536
  one.step(FRAME_MS * every);
1537
- setFrame((count2) => count2 + every);
1537
+ setFrame((count) => count + every);
1538
1538
  const left = now.filter((one) => !one.done());
1539
1539
  if (left.length !== now.length)
1540
1540
  setMoving(left);
1541
1541
  }), FRAME_MS * every);
1542
1542
  onCleanup(() => clearInterval(timer));
1543
1543
  });
1544
- const tween = (from, to, duration2, ease) => {
1544
+ const tween = (from, to, duration, ease) => {
1545
1545
  if (!enabled || from === to)
1546
1546
  return {
1547
1547
  value: () => to,
@@ -1551,11 +1551,11 @@ function createMotion(enabled) {
1551
1551
  value: from
1552
1552
  };
1553
1553
  const timeline = new Timeline({
1554
- duration: duration2,
1554
+ duration,
1555
1555
  autoplay: false
1556
1556
  });
1557
1557
  timeline.add(target, {
1558
- duration: duration2,
1558
+ duration,
1559
1559
  ease,
1560
1560
  value: to
1561
1561
  });
@@ -1568,7 +1568,7 @@ function createMotion(enabled) {
1568
1568
  },
1569
1569
  done: () => {
1570
1570
  frame();
1571
- return elapsed >= duration2;
1571
+ return elapsed >= duration;
1572
1572
  },
1573
1573
  step: (ms) => {
1574
1574
  elapsed += ms;
@@ -1620,14 +1620,14 @@ function createBarFill(motion, counts) {
1620
1620
  const [easing, setEasing] = createSignal();
1621
1621
  let last;
1622
1622
  createEffect(() => {
1623
- const cells2 = target();
1623
+ const cells = target();
1624
1624
  const shown = counts().started > 0;
1625
- if (last && cells2 !== last.cells) {
1625
+ if (last && cells !== last.cells) {
1626
1626
  const from = untrack(() => easing()?.value()) ?? last.cells;
1627
- setEasing(last.shown ? motion.tween(from, cells2, BAR_MS, "outQuad") : undefined);
1627
+ setEasing(last.shown ? motion.tween(from, cells, BAR_MS, "outQuad") : undefined);
1628
1628
  }
1629
1629
  last = {
1630
- cells: cells2,
1630
+ cells,
1631
1631
  shown
1632
1632
  };
1633
1633
  });
@@ -1667,8 +1667,8 @@ function mascotState(rows, {
1667
1667
  return rows.length && rows.every((row) => row.state === "gone") ? "gone" : "idle";
1668
1668
  }
1669
1669
  function countsLine(rows) {
1670
- const count2 = (state) => rows.filter((row) => row.state === state).length;
1671
- return [count2("waiting") ? `${count2("waiting")} waiting on you` : "", count2("stalled") ? `${count2("stalled")} stalled` : "", count2("running") ? `${count2("running")} running` : "", count2("idle") ? `${count2("idle")} idle` : ""].filter(Boolean).join(" \xB7 ");
1670
+ const count = (state) => rows.filter((row) => row.state === state).length;
1671
+ return [count("waiting") ? `${count("waiting")} waiting on you` : "", count("stalled") ? `${count("stalled")} stalled` : "", count("running") ? `${count("running")} running` : "", count("idle") ? `${count("idle")} idle` : ""].filter(Boolean).join(" \xB7 ");
1672
1672
  }
1673
1673
  var GLYPH_ROWS = 12;
1674
1674
  var GLYPH_COLUMNS = 54;
@@ -1694,12 +1694,12 @@ function summaryLine(summary, maxWidth = Infinity) {
1694
1694
  }
1695
1695
  function duration2(ms) {
1696
1696
  const seconds = Math.max(0, Math.floor(ms / 1000));
1697
- const pad2 = (value) => String(value).padStart(2, "0");
1697
+ const pad = (value) => String(value).padStart(2, "0");
1698
1698
  if (seconds < 60)
1699
1699
  return `${seconds}s`;
1700
1700
  if (seconds < 3600)
1701
- return `${Math.floor(seconds / 60)}m ${pad2(seconds % 60)}s`;
1702
- return `${Math.floor(seconds / 3600)}h ${pad2(Math.floor(seconds / 60) % 60)}m`;
1701
+ return `${Math.floor(seconds / 60)}m ${pad(seconds % 60)}s`;
1702
+ return `${Math.floor(seconds / 3600)}h ${pad(Math.floor(seconds / 60) % 60)}m`;
1703
1703
  }
1704
1704
  function parts(rows, parentID) {
1705
1705
  const children = rows.filter((row) => row.parentID === parentID);
@@ -1715,8 +1715,8 @@ function filledCells({
1715
1715
  return started ? Math.round(reported / started * width) : 0;
1716
1716
  }
1717
1717
  function bar(counts, width = 10, filled = filledCells(counts, width)) {
1718
- const cells2 = Math.max(0, Math.min(width, Math.round(filled)));
1719
- return `[${"\u2588".repeat(cells2)}${"\u2591".repeat(width - cells2)}] ${counts.reported}/${counts.started} reported`;
1718
+ const cells = Math.max(0, Math.min(width, Math.round(filled)));
1719
+ return `[${"\u2588".repeat(cells)}${"\u2591".repeat(width - cells)}] ${counts.reported}/${counts.started} reported`;
1720
1720
  }
1721
1721
  function waitLine(wait) {
1722
1722
  if (wait.type === "permission")
@@ -1727,7 +1727,7 @@ function statusLine(row) {
1727
1727
  return statusParts(row).map((part) => part.text).join(" \xB7 ");
1728
1728
  }
1729
1729
  function statusParts(row) {
1730
- const parts2 = [{
1730
+ const parts = [{
1731
1731
  text: stateWord(row),
1732
1732
  kind: "state"
1733
1733
  }, {
@@ -1740,7 +1740,7 @@ function statusParts(row) {
1740
1740
  text: row.isolated ? "isolated" : "",
1741
1741
  kind: "plain"
1742
1742
  }];
1743
- return parts2.filter((part) => part.text);
1743
+ return parts.filter((part) => part.text);
1744
1744
  }
1745
1745
  var SPINNER = ["\u280B", "\u2819", "\u2839", "\u2838", "\u283C", "\u2834", "\u2826", "\u2827", "\u2807", "\u280F"];
1746
1746
  function rowMark(row, theme, frame) {
@@ -1796,16 +1796,16 @@ function stateWord(row) {
1796
1796
  return row.outcome;
1797
1797
  return row.state;
1798
1798
  }
1799
- function reportWords(report2, stopped) {
1800
- if (!report2)
1799
+ function reportWords(report, stopped) {
1800
+ if (!report)
1801
1801
  return "";
1802
- if (report2.state === "owed")
1802
+ if (report.state === "owed")
1803
1803
  return "owes a report";
1804
- if (report2.state === "ended")
1804
+ if (report.state === "ended")
1805
1805
  return stopped ? "" : "ended without a report";
1806
- return ["reported", report2.status].filter(Boolean).join(" ");
1806
+ return ["reported", report.status].filter(Boolean).join(" ");
1807
1807
  }
1808
- var counted = (count2, noun) => `${count2} ${noun}${count2 === 1 ? "" : "s"}`;
1808
+ var counted = (count, noun) => `${count} ${noun}${count === 1 ? "" : "s"}`;
1809
1809
  var CONTEXT_WARN = 0.8;
1810
1810
  function activityParts(row, now) {
1811
1811
  return [`ran ${duration2((row.idle ?? now) - row.created)}`, row.tool ? `last ${row.tool.name} ${duration2(now - row.tool.at)} ago` : "no tool call yet", row.context !== undefined ? `ctx ${Math.round(row.context * 100)}%` : "", row.cost !== undefined ? `$${row.cost.toFixed(2)}` : ""].filter(Boolean);
@@ -1835,7 +1835,7 @@ function courierOf(ctx) {
1835
1835
  var REREAD_MS = 15000;
1836
1836
  function changes(ctx, types, settle) {
1837
1837
  const [changed, setChanged] = createSignal(0);
1838
- const reread = setInterval(() => setChanged((count2) => count2 + 1), REREAD_MS);
1838
+ const reread = setInterval(() => setChanged((count) => count + 1), REREAD_MS);
1839
1839
  let settling;
1840
1840
  const unlisten = ctx.data.listen(({
1841
1841
  details
@@ -1844,7 +1844,7 @@ function changes(ctx, types, settle) {
1844
1844
  return;
1845
1845
  settling = setTimeout(() => {
1846
1846
  settling = undefined;
1847
- setChanged((count2) => count2 + 1);
1847
+ setChanged((count) => count + 1);
1848
1848
  }, settle);
1849
1849
  });
1850
1850
  return [changed, () => {
@@ -2487,9 +2487,9 @@ function TreePanel(props) {
2487
2487
  get each() {
2488
2488
  return statusParts(row);
2489
2489
  },
2490
- children: (part, at2) => [_$createComponent(Show, {
2490
+ children: (part, at) => [_$createComponent(Show, {
2491
2491
  get when() {
2492
- return at2();
2492
+ return at();
2493
2493
  },
2494
2494
  get children() {
2495
2495
  var _el$32 = _$createElement("text");
@@ -2527,7 +2527,7 @@ function TreePanel(props) {
2527
2527
  get each() {
2528
2528
  return line.split(" \xB7 ");
2529
2529
  },
2530
- children: (part, at2) => [_$memo(() => at2() ? " \xB7 " : ""), _$memo(() => _$memo(() => part === `ctx ${Math.round(row.context * 100)}%`)() ? (() => {
2530
+ children: (part, at) => [_$memo(() => at() ? " \xB7 " : ""), _$memo(() => _$memo(() => part === `ctx ${Math.round(row.context * 100)}%`)() ? (() => {
2531
2531
  var _el$37 = _$createElement("span");
2532
2532
  _$insert(_el$37, part);
2533
2533
  _$effect((_$p) => _$setProp(_el$37, "style", {
@@ -2664,7 +2664,7 @@ function addTreePanel(ctx, verbs = {}) {
2664
2664
  return;
2665
2665
  });
2666
2666
  const follow = (from, sessionID) => {
2667
- const token2 = ++attempt;
2667
+ const token = ++attempt;
2668
2668
  const under = (tree) => tree?.rows.some((row) => row.sessionID === sessionID) === true;
2669
2669
  Object.assign(from, {
2670
2670
  at: undefined,
@@ -2675,7 +2675,7 @@ function addTreePanel(ctx, verbs = {}) {
2675
2675
  (async () => {
2676
2676
  const inside = await treeOf(from.root);
2677
2677
  const own = under(inside) ? undefined : await treeOf(sessionID);
2678
- if (token2 !== attempt || following !== from)
2678
+ if (token !== attempt || following !== from)
2679
2679
  return;
2680
2680
  if (under(inside))
2681
2681
  show(from, sessionID, from.root, inside);
@@ -2769,10 +2769,10 @@ function addTreePanel(ctx, verbs = {}) {
2769
2769
  });
2770
2770
  const sessionID = target || await ctx.ui.dialog.select({
2771
2771
  title: "Stop which session?",
2772
- options: rows.map((row2) => ({
2773
- title: row2.title,
2774
- value: row2.sessionID,
2775
- description: `${row2.sessionID} \xB7 ${statusLine(row2)}`
2772
+ options: rows.map((row) => ({
2773
+ title: row.title,
2774
+ value: row.sessionID,
2775
+ description: `${row.sessionID} \xB7 ${statusLine(row)}`
2776
2776
  }))
2777
2777
  });
2778
2778
  if (!sessionID)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "opencode-courier",
3
- "version": "0.10.1",
3
+ "version": "0.10.2",
4
4
  "description": "Multi-session orchestration for OpenCode V2: spawn child sessions, be woken by them. No polling.",
5
5
  "license": "MIT",
6
6
  "author": "Ivo Pogace <71701666+ivopogace@users.noreply.github.com>",
@@ -56,15 +56,15 @@
56
56
  "test:e2e": "e2e/run.sh"
57
57
  },
58
58
  "peerDependencies": {
59
- "@opencode/plugin": "2.1.0",
59
+ "@opencode/plugin": "2.1.2",
60
60
  "effect": "4.0.1"
61
61
  },
62
62
  "devDependencies": {
63
- "@opencode/plugin": "2.1.0",
64
- "@opencode/theme": "2.1.0",
63
+ "@opencode/plugin": "2.1.2",
64
+ "@opencode/theme": "2.1.2",
65
65
  "@opentui/core": "0.6.6",
66
66
  "@opentui/solid": "0.6.6",
67
- "@types/bun": "1.3.14",
67
+ "@types/bun": "1.4.2",
68
68
  "@types/node": "26.6.4",
69
69
  "effect": "4.0.1",
70
70
  "solid-js": "1.9.15",