opencode-courier 0.3.0 → 0.4.1

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.
Files changed (141) hide show
  1. package/README.md +180 -274
  2. package/dist/index.js +47 -73
  3. package/dist/kernel/protocol.d.ts +20 -0
  4. package/dist/kernel/protocol.js +36 -0
  5. package/dist/kernel/tool.d.ts +5 -0
  6. package/dist/kernel/tool.js +5 -0
  7. package/dist/person/events.d.ts +23 -0
  8. package/dist/person/events.js +113 -0
  9. package/dist/person/form.d.ts +26 -0
  10. package/dist/person/form.js +64 -0
  11. package/dist/person/index.d.ts +17 -0
  12. package/dist/person/index.js +5 -0
  13. package/dist/{relay.d.ts → person/permission.d.ts} +27 -21
  14. package/dist/{relay.js → person/permission.js} +9 -5
  15. package/dist/person/ports.d.ts +58 -0
  16. package/dist/person/ports.js +2 -0
  17. package/dist/{question → person/question}/answer.d.ts +33 -4
  18. package/dist/{question → person/question}/answer.js +43 -8
  19. package/dist/person/question/index.d.ts +7 -0
  20. package/dist/{question → person/question}/index.js +3 -3
  21. package/dist/{question → person/question}/lifecycle.d.ts +1 -1
  22. package/dist/{question → person/question}/pure.d.ts +1 -2
  23. package/dist/{question → person/question}/pure.js +1 -1
  24. package/dist/{question → person/question}/relay.d.ts +1 -1
  25. package/dist/{question → person/question}/relay.js +14 -8
  26. package/dist/{question → person/question}/shared.d.ts +13 -3
  27. package/dist/person/question/shared.js +52 -0
  28. package/dist/person/question/types.d.ts +121 -0
  29. package/dist/person/question/types.js +1 -0
  30. package/dist/person/rpc-handlers.d.ts +160 -0
  31. package/dist/person/rpc-handlers.js +62 -0
  32. package/dist/person/rpc.d.ts +227 -0
  33. package/dist/person/rpc.js +70 -0
  34. package/dist/person/text.d.ts +68 -0
  35. package/dist/person/text.js +262 -0
  36. package/dist/person/tools.d.ts +20 -0
  37. package/dist/person/tools.js +55 -0
  38. package/dist/person/tui.d.ts +46 -0
  39. package/dist/{bounded.d.ts → platform/bounded.d.ts} +4 -0
  40. package/dist/{bounded.js → platform/bounded.js} +4 -0
  41. package/dist/platform/events.d.ts +34 -0
  42. package/dist/platform/events.js +121 -0
  43. package/dist/platform/hub.d.ts +195 -0
  44. package/dist/{hub.js → platform/hub.js} +27 -71
  45. package/dist/{version.js → platform/version.js} +1 -1
  46. package/dist/rpc.d.ts +2 -0
  47. package/dist/rpc.js +3 -0
  48. package/dist/scheduling/index.d.ts +6 -0
  49. package/dist/scheduling/index.js +5 -0
  50. package/dist/{later.d.ts → scheduling/later.d.ts} +7 -3
  51. package/dist/{later.js → scheduling/later.js} +12 -4
  52. package/dist/scheduling/text.d.ts +6 -0
  53. package/dist/scheduling/text.js +10 -0
  54. package/dist/scheduling/tools.d.ts +14 -0
  55. package/dist/scheduling/tools.js +48 -0
  56. package/dist/skill.d.ts +4 -0
  57. package/dist/skill.js +54 -1
  58. package/dist/{cleanup.d.ts → tree/cleanup.d.ts} +1 -27
  59. package/dist/{cleanup.js → tree/cleanup.js} +9 -16
  60. package/dist/tree/context.d.ts +15 -0
  61. package/dist/tree/context.js +34 -0
  62. package/dist/tree/domain.d.ts +101 -0
  63. package/dist/tree/domain.js +68 -0
  64. package/dist/{watch.d.ts → tree/events.d.ts} +10 -37
  65. package/dist/tree/events.js +204 -0
  66. package/dist/tree/forget.d.ts +24 -0
  67. package/dist/tree/forget.js +59 -0
  68. package/dist/tree/group.d.ts +82 -0
  69. package/dist/tree/group.js +203 -0
  70. package/dist/tree/index.d.ts +10 -0
  71. package/dist/tree/index.js +11 -0
  72. package/dist/tree/limits.d.ts +10 -0
  73. package/dist/{limits.js → tree/limits.js} +6 -55
  74. package/dist/tree/panel.d.ts +13 -0
  75. package/dist/tree/panel.js +75 -0
  76. package/dist/tree/ports.d.ts +276 -0
  77. package/dist/tree/ports.js +1 -0
  78. package/dist/tree/report.d.ts +23 -0
  79. package/dist/tree/report.js +33 -0
  80. package/dist/tree/roster.d.ts +27 -0
  81. package/dist/tree/roster.js +62 -0
  82. package/dist/tree/rpc-handlers.d.ts +201 -0
  83. package/dist/tree/rpc-handlers.js +20 -0
  84. package/dist/tree/rpc.d.ts +344 -0
  85. package/dist/tree/rpc.js +83 -0
  86. package/dist/tree/send.d.ts +14 -0
  87. package/dist/tree/send.js +99 -0
  88. package/dist/tree/spawn.d.ts +21 -0
  89. package/dist/tree/spawn.js +173 -0
  90. package/dist/tree/status.d.ts +128 -0
  91. package/dist/tree/status.js +62 -0
  92. package/dist/{stop.d.ts → tree/stop.d.ts} +1 -9
  93. package/dist/{stop.js → tree/stop.js} +9 -13
  94. package/dist/tree/store.d.ts +24 -0
  95. package/dist/tree/store.js +264 -0
  96. package/dist/{tree.d.ts → tree/subtree.d.ts} +5 -6
  97. package/dist/{tree.js → tree/subtree.js} +7 -5
  98. package/dist/tree/summary.d.ts +7 -0
  99. package/dist/tree/summary.js +35 -0
  100. package/dist/tree/text.d.ts +77 -0
  101. package/dist/tree/text.js +287 -0
  102. package/dist/tree/tools.d.ts +54 -0
  103. package/dist/{tools.js → tree/tools.js} +12 -142
  104. package/dist/tree/tui.d.ts +53 -0
  105. package/dist/tui.d.ts +3 -0
  106. package/dist/tui.js +1342 -0
  107. package/dist/webhook/index.d.ts +5 -0
  108. package/dist/webhook/index.js +3 -0
  109. package/dist/{webhook.d.ts → webhook/receiver.d.ts} +17 -23
  110. package/dist/{webhook.js → webhook/receiver.js} +58 -50
  111. package/dist/webhook/subscriptions.d.ts +29 -0
  112. package/dist/webhook/subscriptions.js +52 -0
  113. package/dist/webhook/text.d.ts +23 -0
  114. package/dist/webhook/text.js +82 -0
  115. package/dist/webhook/tools.d.ts +19 -0
  116. package/dist/webhook/tools.js +46 -0
  117. package/package.json +18 -7
  118. package/dist/courier.d.ts +0 -200
  119. package/dist/courier.js +0 -315
  120. package/dist/group.d.ts +0 -86
  121. package/dist/group.js +0 -247
  122. package/dist/hub.d.ts +0 -300
  123. package/dist/limits.d.ts +0 -65
  124. package/dist/notices.d.ts +0 -356
  125. package/dist/notices.js +0 -678
  126. package/dist/question/index.d.ts +0 -6
  127. package/dist/question/shared.js +0 -24
  128. package/dist/report.d.ts +0 -48
  129. package/dist/report.js +0 -88
  130. package/dist/roster.d.ts +0 -93
  131. package/dist/roster.js +0 -206
  132. package/dist/tools.d.ts +0 -94
  133. package/dist/watch.js +0 -474
  134. /package/dist/{question → person/question}/lifecycle.js +0 -0
  135. /package/dist/{question → person/question}/runtime.d.ts +0 -0
  136. /package/dist/{question → person/question}/runtime.js +0 -0
  137. /package/dist/{json.d.ts → platform/json.d.ts} +0 -0
  138. /package/dist/{json.js → platform/json.js} +0 -0
  139. /package/dist/{storage.d.ts → platform/storage.d.ts} +0 -0
  140. /package/dist/{storage.js → platform/storage.js} +0 -0
  141. /package/dist/{version.d.ts → platform/version.d.ts} +0 -0
package/dist/tui.js ADDED
@@ -0,0 +1,1342 @@
1
+ // @bun
2
+ // src/tui.tsx
3
+ import { Plugin } from "@opencode/plugin/tui";
4
+
5
+ // src/person/rpc.ts
6
+ import { Rpc } from "@opencode/plugin/rpc";
7
+ import { Schema } from "effect";
8
+ var Count = Schema.Int.check(Schema.isGreaterThanOrEqualTo(0));
9
+ var Session = Schema.Struct({
10
+ sessionID: Schema.String,
11
+ title: Schema.String,
12
+ parentID: Schema.String,
13
+ depth: Count,
14
+ group: Schema.optionalKey(Schema.String)
15
+ });
16
+ var Permission = Schema.Struct({
17
+ sessionID: Schema.String,
18
+ requestID: Schema.String,
19
+ action: Schema.String,
20
+ resources: Schema.Array(Schema.String),
21
+ save: Schema.optionalKey(Schema.Array(Schema.String))
22
+ });
23
+ var Prompt = Schema.Struct({
24
+ question: Schema.String,
25
+ header: Schema.String,
26
+ options: Schema.Array(Schema.Struct({ label: Schema.String, description: Schema.String })),
27
+ multiple: Schema.optionalKey(Schema.Boolean)
28
+ });
29
+ var Question = Schema.Struct({
30
+ sessionID: Schema.String,
31
+ requestID: Schema.String,
32
+ questions: Schema.Array(Prompt),
33
+ stopped: Schema.optionalKey(Schema.Boolean)
34
+ });
35
+ var Asks = Schema.Struct({
36
+ top: Schema.String,
37
+ sessions: Schema.Array(Session),
38
+ permissions: Schema.Array(Permission),
39
+ questions: Schema.Array(Question),
40
+ truncated: Schema.optionalKey(Count)
41
+ });
42
+ var Answered = Schema.Struct({
43
+ answered: Schema.Boolean,
44
+ by: Schema.optionalKey(Schema.Literals(["result", "message"])),
45
+ problem: Schema.optionalKey(Schema.String)
46
+ });
47
+ var personRpc = Rpc.define({
48
+ id: "courier-person",
49
+ methods: {
50
+ asks: {
51
+ input: Schema.toStandardSchemaV1(Schema.Struct({ sessionID: Schema.String })),
52
+ output: Schema.toStandardSchemaV1(Asks)
53
+ },
54
+ answer: {
55
+ input: Schema.toStandardSchemaV1(Schema.Struct({ sessionID: Schema.String, requestID: Schema.String, answers: Schema.Array(Schema.Array(Schema.String)) })),
56
+ output: Schema.toStandardSchemaV1(Answered)
57
+ }
58
+ },
59
+ events: {}
60
+ });
61
+
62
+ // src/person/text.ts
63
+ var REJECTED = "Refused by the session that started you. Do without it, or report back why you cannot.";
64
+
65
+ // src/person/tui.tsx
66
+ var QUESTION_FORM = "question";
67
+ var INBOX_COMMAND = "courier.asks";
68
+ var SETTLE_MS = 250;
69
+ function asksOf(asks, forms) {
70
+ return asks.sessions.flatMap((session) => [...asks.permissions.filter((request) => request.sessionID === session.sessionID).map((request) => ({
71
+ kind: "permission",
72
+ key: `permission ${request.requestID}`,
73
+ session,
74
+ request
75
+ })), ...asks.questions.filter((question) => question.sessionID === session.sessionID).map((question) => ({
76
+ kind: "question",
77
+ key: `question ${question.requestID}`,
78
+ session,
79
+ question
80
+ })), ...(forms.get(session.sessionID) ?? []).filter((form) => form.metadata?.kind !== QUESTION_FORM).map((form) => ({
81
+ kind: "form",
82
+ key: `form ${form.id}`,
83
+ session,
84
+ form
85
+ }))]);
86
+ }
87
+ function pathOf(asks, sessionID, topTitle) {
88
+ const byID = new Map(asks.sessions.map((session) => [session.sessionID, session]));
89
+ const titles = [];
90
+ for (let at = byID.get(sessionID);at && titles.length <= byID.size; at = byID.get(at.parentID))
91
+ titles.unshift(at.title);
92
+ return [topTitle, ...titles].join(" \u203A ");
93
+ }
94
+ var clip = (text, max = 80) => text.length > max ? `${text.slice(0, max - 1)}\u2026` : text;
95
+ function askTitle(ask) {
96
+ switch (ask.kind) {
97
+ case "permission": {
98
+ const {
99
+ action,
100
+ resources
101
+ } = ask.request;
102
+ return clip(resources.length ? `Permission: ${action} ${resources.join(", ")}` : `Permission: ${action}`);
103
+ }
104
+ case "question": {
105
+ const [first, ...rest] = ask.question.questions;
106
+ const what = first ? `${first.header}: ${first.question}` : ask.question.requestID;
107
+ const more = rest.length ? ` (+${rest.length} more)` : "";
108
+ return clip(`Question: ${what}${more}`);
109
+ }
110
+ case "form":
111
+ return clip(`Form: ${ask.form.title}`);
112
+ }
113
+ }
114
+ function askFooter(ask) {
115
+ return [`depth ${ask.session.depth}`, ask.session.group ? `group ${ask.session.group}` : "", ask.kind === "question" && ask.question.stopped ? "cut off, answered by message" : ""].filter(Boolean).join(" \xB7 ");
116
+ }
117
+ var soundOf = (ask) => ask.kind === "permission" ? "permission" : "question";
118
+ function shown(field, answers) {
119
+ if ("hidden" in field && field.hidden)
120
+ return false;
121
+ const when = "when" in field ? field.when ?? [] : [];
122
+ return when.every((condition) => condition.op === "eq" ? answers[condition.key] === condition.value : answers[condition.key] !== condition.value);
123
+ }
124
+ function chooseOptions(choices, picked, {
125
+ multiple = false,
126
+ own = false,
127
+ optional = false
128
+ }) {
129
+ const mark = (value) => {
130
+ if (!multiple)
131
+ return "";
132
+ return picked.includes(value) ? "[x] " : "[ ] ";
133
+ };
134
+ const theirs = picked.filter((value) => !choices.some((choice) => choice.value === value));
135
+ return [...choices.map((choice) => ({
136
+ title: `${mark(choice.value)}${choice.label}`,
137
+ value: choice.value,
138
+ ...choice.description ? {
139
+ description: choice.description
140
+ } : {}
141
+ })), ...theirs.map((value) => ({
142
+ title: `[x] ${value}`,
143
+ value
144
+ })), ...own ? [{
145
+ title: "Type your own answer\u2026",
146
+ value: OWN
147
+ }] : [], ...multiple ? [{
148
+ title: "Done",
149
+ value: DONE,
150
+ description: picked.length ? picked.join(", ") : "nothing chosen"
151
+ }] : [], ...optional && !multiple ? [{
152
+ title: "Skip",
153
+ value: SKIP
154
+ }] : []];
155
+ }
156
+ function toggle(picked, value) {
157
+ const at = picked.indexOf(value);
158
+ if (at >= 0)
159
+ picked.splice(at, 1);
160
+ else
161
+ picked.push(value);
162
+ }
163
+ var OWN = Symbol("own");
164
+ var DONE = Symbol("done");
165
+ var SKIP = Symbol("skip");
166
+ function addAsksInbox(ctx) {
167
+ const person = ctx.client.rpc(personRpc);
168
+ const location = (sessionID) => ctx.data.session.get(sessionID)?.location ?? ctx.location ?? ctx.data.location.default();
169
+ const openSession = () => {
170
+ const route = ctx.ui.router.current();
171
+ return route.type === "session" ? route.sessionID : undefined;
172
+ };
173
+ const firstSeen = new Map;
174
+ const answered = new Set;
175
+ let seen = 0;
176
+ async function read(sessionID) {
177
+ const asks = await person.asks({
178
+ sessionID
179
+ }, {
180
+ location: location(sessionID)
181
+ });
182
+ const forms = new Map(await Promise.all(asks.sessions.map(async (session) => [session.sessionID, await ctx.client.session.form.list({
183
+ sessionID: session.sessionID
184
+ }).catch(() => [])])));
185
+ const listed = asksOf(asks, forms).filter((ask) => !answered.has(ask.key));
186
+ for (const ask of listed)
187
+ if (!firstSeen.has(ask.key))
188
+ firstSeen.set(ask.key, seen++);
189
+ listed.sort((a, b) => firstSeen.get(a.key) - firstSeen.get(b.key));
190
+ return {
191
+ asks,
192
+ listed
193
+ };
194
+ }
195
+ const fail = (what) => (error) => ctx.ui.toast.show({
196
+ title: "courier",
197
+ message: `${what}: ${error instanceof Error ? error.message : String(error)}`,
198
+ variant: "error"
199
+ });
200
+ async function choose(title, choices, {
201
+ multiple = false,
202
+ own = false,
203
+ optional = false
204
+ } = {}) {
205
+ const picked = [];
206
+ for (;; ) {
207
+ const value = await ctx.ui.dialog.select({
208
+ title,
209
+ options: chooseOptions(choices, picked, {
210
+ multiple,
211
+ own,
212
+ optional
213
+ })
214
+ });
215
+ if (value === undefined)
216
+ return;
217
+ if (value === DONE)
218
+ return picked;
219
+ if (value === SKIP)
220
+ return [];
221
+ const chosen = value === OWN ? (await ctx.ui.dialog.prompt({
222
+ title,
223
+ placeholder: "Your answer"
224
+ }))?.trim() : value;
225
+ if (!chosen)
226
+ continue;
227
+ if (!multiple)
228
+ return [chosen];
229
+ toggle(picked, chosen);
230
+ }
231
+ }
232
+ async function answerPermission(ask) {
233
+ const {
234
+ request
235
+ } = ask;
236
+ const decision = await ctx.ui.dialog.select({
237
+ title: askTitle(ask),
238
+ options: [{
239
+ title: "Allow once",
240
+ value: "once"
241
+ }, ...request.save?.length ? [{
242
+ title: "Always allow",
243
+ value: "always",
244
+ description: request.save.join(", ")
245
+ }] : [], {
246
+ title: "Reject",
247
+ value: "reject",
248
+ description: "the session's call fails and it carries on"
249
+ }, {
250
+ title: "Reject with a reason\u2026",
251
+ value: "reason"
252
+ }]
253
+ });
254
+ if (decision === undefined)
255
+ return false;
256
+ const message = decision === "reason" ? await ctx.ui.dialog.prompt({
257
+ title: "Why not?",
258
+ placeholder: "Passed on to the session"
259
+ }) : undefined;
260
+ if (decision === "reason" && message === undefined)
261
+ return false;
262
+ const reply = decision === "reason" ? "reject" : decision;
263
+ await ctx.client.permission.reply({
264
+ sessionID: request.sessionID,
265
+ requestID: request.requestID,
266
+ decision: reply,
267
+ ...reply === "reject" ? {
268
+ message: message?.trim() || REJECTED
269
+ } : {}
270
+ });
271
+ return true;
272
+ }
273
+ async function answerQuestion(ask) {
274
+ const {
275
+ question
276
+ } = ask;
277
+ const answers = [];
278
+ for (const [index, prompt] of question.questions.entries()) {
279
+ const count = question.questions.length > 1 ? ` (${index + 1}/${question.questions.length})` : "";
280
+ const choices = prompt.options.map((option) => ({
281
+ ...option,
282
+ value: option.label
283
+ }));
284
+ const picked = await choose(`${prompt.header}: ${prompt.question}${count}`, choices, {
285
+ multiple: !!prompt.multiple,
286
+ own: true
287
+ });
288
+ if (picked === undefined)
289
+ return false;
290
+ answers.push(picked);
291
+ }
292
+ const result = await person.answer({
293
+ sessionID: question.sessionID,
294
+ requestID: question.requestID,
295
+ answers
296
+ }, {
297
+ location: location(question.sessionID)
298
+ });
299
+ if (result.problem)
300
+ throw new Error(result.problem);
301
+ if (!result.answered)
302
+ ctx.ui.toast.show({
303
+ title: "courier",
304
+ message: "It no longer waits: answered or settled some other way.",
305
+ variant: "warning"
306
+ });
307
+ return true;
308
+ }
309
+ async function typed(title, field, optional) {
310
+ for (;; ) {
311
+ const text = await ctx.ui.dialog.prompt({
312
+ title,
313
+ ...field.description ? {
314
+ description: field.description
315
+ } : {},
316
+ ...field.default ? {
317
+ value: field.default
318
+ } : {}
319
+ });
320
+ if (text === undefined)
321
+ return;
322
+ if (text || optional)
323
+ return text || SKIP;
324
+ }
325
+ }
326
+ async function numeric(title, field, optional) {
327
+ const whole = field.type === "integer";
328
+ for (;; ) {
329
+ const text = await typed(title, {
330
+ ...field.description ? {
331
+ description: field.description
332
+ } : {}
333
+ }, optional);
334
+ if (text === undefined || text === SKIP)
335
+ return text;
336
+ const value = Number(text);
337
+ if (Number.isFinite(value) && (!whole || Number.isInteger(value)))
338
+ return value;
339
+ ctx.ui.toast.show({
340
+ title: "courier",
341
+ message: `${title} takes ${whole ? "a whole number" : "a number"}`,
342
+ variant: "warning"
343
+ });
344
+ }
345
+ }
346
+ async function answerField(field) {
347
+ const title = field.title ?? field.key;
348
+ const optional = !(("required" in field) && field.required);
349
+ switch (field.type) {
350
+ case "external":
351
+ await ctx.ui.dialog.alert({
352
+ title,
353
+ message: `This form opens ${field.url}: answer it in its own session.`
354
+ });
355
+ return;
356
+ case "boolean":
357
+ return ctx.ui.dialog.select({
358
+ title,
359
+ options: [{
360
+ title: "Yes",
361
+ value: true
362
+ }, {
363
+ title: "No",
364
+ value: false
365
+ }]
366
+ });
367
+ case "multiselect": {
368
+ const picked = await choose(title, field.options, {
369
+ multiple: true,
370
+ own: !!field.custom
371
+ });
372
+ if (picked?.length === 0 && optional)
373
+ return SKIP;
374
+ return picked;
375
+ }
376
+ case "string": {
377
+ if (!field.options?.length)
378
+ return typed(title, field, optional);
379
+ const picked = await choose(title, field.options, {
380
+ own: !!field.custom,
381
+ optional
382
+ });
383
+ return picked && (picked[0] ?? SKIP);
384
+ }
385
+ case "number":
386
+ case "integer":
387
+ return numeric(title, field, optional);
388
+ }
389
+ }
390
+ async function answerForm(ask) {
391
+ const {
392
+ form
393
+ } = ask;
394
+ const answer2 = {};
395
+ for (const field of form.fields) {
396
+ if (!shown(field, answer2))
397
+ continue;
398
+ const value = await answerField(field);
399
+ if (value === undefined)
400
+ return false;
401
+ if (value !== SKIP)
402
+ answer2[field.key] = value;
403
+ }
404
+ await ctx.data.session.form.reply({
405
+ sessionID: form.sessionID,
406
+ formID: form.id,
407
+ answer: answer2
408
+ });
409
+ return true;
410
+ }
411
+ const answer = (ask) => {
412
+ switch (ask.kind) {
413
+ case "permission":
414
+ return answerPermission(ask);
415
+ case "question":
416
+ return answerQuestion(ask);
417
+ case "form":
418
+ return answerForm(ask);
419
+ }
420
+ };
421
+ let opened = false;
422
+ async function open(sessionID = openSession()) {
423
+ if (!sessionID) {
424
+ ctx.ui.toast.show({
425
+ title: "courier",
426
+ message: "Open a session to see what its sessions ask you.",
427
+ variant: "info"
428
+ });
429
+ return;
430
+ }
431
+ if (opened)
432
+ return;
433
+ opened = true;
434
+ try {
435
+ for (let first = true;; first = false) {
436
+ const found = await read(sessionID).catch(fail("The asks could not be read"));
437
+ if (!found)
438
+ return;
439
+ const {
440
+ asks,
441
+ listed
442
+ } = found;
443
+ if (!listed.length) {
444
+ if (first)
445
+ ctx.ui.toast.show({
446
+ title: "courier",
447
+ message: "Nothing in this courier tree waits on you.",
448
+ variant: "info"
449
+ });
450
+ return;
451
+ }
452
+ const top = ctx.data.session.get(asks.top)?.title ?? asks.top;
453
+ const choosing = ctx.ui.dialog.select({
454
+ title: `Waiting on you (${listed.length})`,
455
+ placeholder: "Search",
456
+ options: listed.map((ask) => ({
457
+ title: askTitle(ask),
458
+ value: ask,
459
+ description: pathOf(asks, ask.session.sessionID, top),
460
+ footer: askFooter(ask)
461
+ }))
462
+ });
463
+ ctx.ui.dialog.set({
464
+ size: "large"
465
+ });
466
+ const chosen = await choosing;
467
+ if (!chosen)
468
+ return;
469
+ const done = await answer(chosen).catch((error) => {
470
+ fail("The answer was not passed on")(error);
471
+ return false;
472
+ });
473
+ if (done)
474
+ answered.add(chosen.key);
475
+ }
476
+ } finally {
477
+ opened = false;
478
+ }
479
+ }
480
+ const sounded = new Set;
481
+ let settling;
482
+ const sound = async () => {
483
+ const sessionID = openSession();
484
+ if (!sessionID)
485
+ return;
486
+ const {
487
+ asks,
488
+ listed
489
+ } = await read(sessionID);
490
+ const top = ctx.data.session.get(asks.top)?.title ?? asks.top;
491
+ for (const ask of listed) {
492
+ if (sounded.has(ask.key))
493
+ continue;
494
+ sounded.add(ask.key);
495
+ ctx.attention.notify({
496
+ title: pathOf(asks, ask.session.sessionID, top),
497
+ message: askTitle(ask),
498
+ notification: {
499
+ when: "blurred"
500
+ },
501
+ sound: {
502
+ name: soundOf(ask),
503
+ when: "always"
504
+ }
505
+ });
506
+ }
507
+ };
508
+ const unlisten = ctx.data.listen(({
509
+ details
510
+ }) => {
511
+ if (details.type !== "permission.asked" && details.type !== "form.created" || settling)
512
+ return;
513
+ settling = setTimeout(() => {
514
+ settling = undefined;
515
+ sound().catch(() => {
516
+ return;
517
+ });
518
+ }, SETTLE_MS);
519
+ });
520
+ ctx.keymap.layer(() => ({
521
+ mode: "global",
522
+ commands: [{
523
+ id: INBOX_COMMAND,
524
+ title: "Courier: what waits on you",
525
+ description: "Answer the permission requests, questions and forms of the sessions started with courier",
526
+ group: "Courier",
527
+ bind: "<leader>k",
528
+ palette: true,
529
+ slash: {
530
+ name: "asks",
531
+ aliases: ["ask"]
532
+ },
533
+ run: () => void open()
534
+ }]
535
+ }));
536
+ return {
537
+ open,
538
+ cleanup: () => {
539
+ clearTimeout(settling);
540
+ unlisten();
541
+ }
542
+ };
543
+ }
544
+
545
+ // src/tree/tui.tsx
546
+ import { use as _$use } from "@opentui/solid";
547
+ import { memo as _$memo } from "@opentui/solid";
548
+ import { createComponent as _$createComponent } from "@opentui/solid";
549
+ import { effect as _$effect } from "@opentui/solid";
550
+ import { insert as _$insert } from "@opentui/solid";
551
+ import { createTextNode as _$createTextNode } from "@opentui/solid";
552
+ import { insertNode as _$insertNode } from "@opentui/solid";
553
+ import { setProp as _$setProp } from "@opentui/solid";
554
+ import { createElement as _$createElement } from "@opentui/solid";
555
+ import { createEffect, createResource, createRoot, createSignal, For, onCleanup, onMount, Show, untrack } from "solid-js";
556
+
557
+ // src/tree/rpc.ts
558
+ import { Rpc as Rpc2 } from "@opencode/plugin/rpc";
559
+ import { Schema as Schema2 } from "effect";
560
+ var Count2 = Schema2.Int.check(Schema2.isGreaterThanOrEqualTo(0));
561
+ var Group = Schema2.Struct({ name: Schema2.String, reported: Count2, members: Count2 });
562
+ var Summary = Schema2.Struct({
563
+ running: Count2,
564
+ waiting: Count2,
565
+ idle: Count2,
566
+ groups: Schema2.Array(Group),
567
+ truncated: Schema2.optionalKey(Count2)
568
+ });
569
+ var Wait = Schema2.Union([
570
+ Schema2.Struct({ type: Schema2.Literal("permission"), action: Schema2.String, resources: Schema2.Array(Schema2.String) }),
571
+ Schema2.Struct({ type: Schema2.Literal("question"), question: Schema2.String })
572
+ ]);
573
+ var Row = Schema2.Struct({
574
+ sessionID: Schema2.String,
575
+ parentID: Schema2.String,
576
+ title: Schema2.String,
577
+ depth: Count2,
578
+ state: Schema2.Literals(["running", "waiting", "idle", "gone"]),
579
+ outcome: Schema2.optionalKey(Schema2.Literals(["succeeded", "failed", "interrupted"])),
580
+ report: Schema2.optionalKey(Schema2.Struct({ state: Schema2.Literals(["owed", "reported", "ended"]), status: Schema2.optionalKey(Schema2.String), progressed: Schema2.Boolean })),
581
+ group: Schema2.optionalKey(Schema2.Struct({ name: Schema2.String, standing: Schema2.String })),
582
+ isolated: Schema2.Boolean,
583
+ created: Schema2.Number,
584
+ idle: Schema2.optionalKey(Schema2.Number),
585
+ cost: Schema2.optionalKey(Schema2.Number),
586
+ tool: Schema2.optionalKey(Schema2.Struct({ name: Schema2.String, at: Schema2.Number })),
587
+ waits: Schema2.Array(Wait)
588
+ });
589
+ var Tree = Schema2.Struct({
590
+ rows: Schema2.Array(Row),
591
+ truncated: Schema2.optionalKey(Count2)
592
+ });
593
+ var Stopped = Schema2.Struct({
594
+ stopped: Schema2.Array(Schema2.Struct({ sessionID: Schema2.String, title: Schema2.String, outcome: Schema2.String })),
595
+ cancelled: Count2
596
+ });
597
+ var treeRpc = Rpc2.define({
598
+ id: "courier",
599
+ methods: {
600
+ summary: {
601
+ input: Schema2.toStandardSchemaV1(Schema2.Struct({ sessionID: Schema2.String })),
602
+ output: Schema2.toStandardSchemaV1(Summary)
603
+ },
604
+ tree: {
605
+ input: Schema2.toStandardSchemaV1(Schema2.Struct({ sessionID: Schema2.String })),
606
+ output: Schema2.toStandardSchemaV1(Tree)
607
+ },
608
+ stop: {
609
+ input: Schema2.toStandardSchemaV1(Schema2.Struct({ sessionID: Schema2.String, by: Schema2.String })),
610
+ output: Schema2.toStandardSchemaV1(Stopped),
611
+ errors: { refused: Schema2.toStandardSchemaV1(Schema2.Struct({})) }
612
+ }
613
+ },
614
+ events: {}
615
+ });
616
+
617
+ // src/tree/tui.tsx
618
+ 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"]);
619
+ var PANEL_CHANGES = new Set([...CHANGES, "session.tool.called", "session.tool.success", "session.tool.failed", "session.usage.updated", "session.renamed"]);
620
+ var SETTLE_MS2 = 250;
621
+ var PANEL_SETTLE_MS = 1000;
622
+ var PANEL = "courier-tree";
623
+ var TICK_MS = 1000;
624
+ function summaryLine(summary) {
625
+ const parts = [summary.running ? `${summary.running} running` : "", summary.waiting ? `${summary.waiting} waiting on you` : "", summary.idle ? `${summary.idle} idle` : "", ...summary.groups.map((group) => `group ${group.name} ${group.reported}/${group.members}`), summary.truncated ? `first ${summary.truncated} counted` : ""].filter(Boolean);
626
+ return parts.length ? `courier ${parts.join(" \xB7 ")}` : "";
627
+ }
628
+ function duration(ms) {
629
+ const seconds = Math.max(0, Math.floor(ms / 1000));
630
+ const pad = (value) => String(value).padStart(2, "0");
631
+ if (seconds < 60)
632
+ return `${seconds}s`;
633
+ if (seconds < 3600)
634
+ return `${Math.floor(seconds / 60)}m ${pad(seconds % 60)}s`;
635
+ return `${Math.floor(seconds / 3600)}h ${pad(Math.floor(seconds / 60) % 60)}m`;
636
+ }
637
+ function parts(rows, parentID) {
638
+ const children = rows.filter((row) => row.parentID === parentID);
639
+ return {
640
+ reported: children.filter((row) => row.report?.state === "reported").length,
641
+ started: children.length
642
+ };
643
+ }
644
+ function bar({
645
+ reported,
646
+ started
647
+ }, width = 10) {
648
+ const filled = started ? Math.round(reported / started * width) : 0;
649
+ return `[${"\u2588".repeat(filled)}${"\u2591".repeat(width - filled)}] ${reported}/${started} reported`;
650
+ }
651
+ function waitLine(wait) {
652
+ return wait.type === "permission" ? `asks permission: ${[wait.action, ...wait.resources].join(" ")}` : `asks: ${wait.question}`;
653
+ }
654
+ function statusLine(row) {
655
+ return [stateWord(row), reportWords(row.report), row.group ? `group ${row.group.name} ${row.group.standing}` : "", row.isolated ? "isolated" : ""].filter(Boolean).join(" \xB7 ");
656
+ }
657
+ function stateWord(row) {
658
+ if (row.state === "waiting")
659
+ return "waiting on you";
660
+ if (row.state === "idle" && (row.outcome === "failed" || row.outcome === "interrupted"))
661
+ return row.outcome;
662
+ return row.state;
663
+ }
664
+ function reportWords(report) {
665
+ if (!report)
666
+ return "";
667
+ if (report.state === "owed")
668
+ return "owes a report";
669
+ if (report.state === "ended")
670
+ return "ended without a report";
671
+ return ["reported", report.status].filter(Boolean).join(" ");
672
+ }
673
+ var counted = (count, noun) => `${count} ${noun}${count === 1 ? "" : "s"}`;
674
+ function activityLine(row, now) {
675
+ return [`ran ${duration((row.idle ?? now) - row.created)}`, row.tool ? `last ${row.tool.name} ${duration(now - row.tool.at)} ago` : "no tool call yet", row.cost !== undefined ? `$${row.cost.toFixed(2)}` : ""].filter(Boolean).join(" \xB7 ");
676
+ }
677
+ function detailLines(row, rows, now) {
678
+ const started = parts(rows, row.sessionID);
679
+ if (row.waits.length)
680
+ return [...started.started ? [bar(started)] : [], ...row.waits.map(waitLine)];
681
+ if (started.started)
682
+ return [bar(started)];
683
+ return row.state === "gone" ? [] : [activityLine(row, now)];
684
+ }
685
+ function courierOf(ctx) {
686
+ const courier = ctx.client.rpc(treeRpc);
687
+ const location = (sessionID) => ctx.data.session.get(sessionID)?.location ?? ctx.location ?? ctx.data.location.default();
688
+ return {
689
+ courier,
690
+ location
691
+ };
692
+ }
693
+ function changes(ctx, types, settle) {
694
+ const [changed, setChanged] = createSignal(0);
695
+ let settling;
696
+ const unlisten = ctx.data.listen(({
697
+ details
698
+ }) => {
699
+ if (!types.has(details.type) || settling)
700
+ return;
701
+ settling = setTimeout(() => {
702
+ settling = undefined;
703
+ setChanged((count) => count + 1);
704
+ }, settle);
705
+ });
706
+ return [changed, () => {
707
+ clearTimeout(settling);
708
+ unlisten();
709
+ }];
710
+ }
711
+ function reason(error) {
712
+ if (error instanceof Error)
713
+ return error.message;
714
+ return typeof error === "object" && error && "message" in error ? String(error.message) : String(error);
715
+ }
716
+ function addTreeStatus(ctx, onWaiting) {
717
+ const {
718
+ courier,
719
+ location
720
+ } = courierOf(ctx);
721
+ const [changed, stopListening] = changes(ctx, CHANGES, SETTLE_MS2);
722
+ const release = ctx.ui.slot({
723
+ append: "prompt.footer.status",
724
+ render: (input) => {
725
+ const [read] = createResource(() => input.sessionID ? {
726
+ sessionID: input.sessionID,
727
+ changed: changed()
728
+ } : undefined, ({
729
+ sessionID
730
+ }) => courier.summary({
731
+ sessionID
732
+ }, {
733
+ location: location(sessionID)
734
+ }).then((summary) => ({
735
+ sessionID,
736
+ line: summaryLine(summary),
737
+ waiting: summary.waiting
738
+ }), () => ({
739
+ sessionID,
740
+ line: "",
741
+ waiting: 0
742
+ })));
743
+ const shown2 = () => {
744
+ const latest = read.state === "ready" || read.state === "refreshing" ? read.latest : undefined;
745
+ return latest && latest.sessionID === input.sessionID ? latest : undefined;
746
+ };
747
+ const line = () => shown2()?.line ?? "";
748
+ const click = () => {
749
+ const latest = shown2();
750
+ if (latest?.waiting)
751
+ onWaiting?.(latest.sessionID);
752
+ };
753
+ return _$createComponent(Show, {
754
+ get when() {
755
+ return line();
756
+ },
757
+ get children() {
758
+ var _el$ = _$createElement("text"), _el$2 = _$createTextNode(` `);
759
+ _$insertNode(_el$, _el$2);
760
+ _$setProp(_el$, "wrapMode", "none");
761
+ _$setProp(_el$, "onMouseUp", click);
762
+ _$insert(_el$, line, null);
763
+ _$effect((_$p) => _$setProp(_el$, "fg", ctx.theme.text.muted, _$p));
764
+ return _el$;
765
+ }
766
+ });
767
+ }
768
+ });
769
+ return () => {
770
+ stopListening();
771
+ release();
772
+ };
773
+ }
774
+ function reach(rows, sessionID) {
775
+ return 1 + rows.filter((row) => row.parentID === sessionID).reduce((total, row) => total + reach(rows, row.sessionID), 0);
776
+ }
777
+ function stoppedLine({
778
+ stopped,
779
+ cancelled
780
+ }) {
781
+ const interrupted = stopped.filter((one) => one.outcome === "interrupted").length;
782
+ const failed = stopped.filter((one) => one.outcome === "failed").length;
783
+ return [`Stopped ${counted(stopped.length, "session")}: ${interrupted} interrupted`, failed ? `${failed} failed to stop` : "", cancelled ? `${counted(cancelled, "scheduled message")} cancelled` : ""].filter(Boolean).join(", ");
784
+ }
785
+ async function confirmStop(ctx, courier, location, by, target) {
786
+ const under = target.under ? ` and the ${counted(target.under, "session")} under it` : "";
787
+ const confirmed = await ctx.ui.dialog.confirm({
788
+ title: "Stop session",
789
+ message: `Stop "${target.title}" (${target.sessionID})${under}? Their turns are interrupted and their scheduled messages cancelled, as courier_stop does.`,
790
+ label: {
791
+ confirm: "Stop",
792
+ cancel: "Keep running"
793
+ }
794
+ });
795
+ if (!confirmed)
796
+ return false;
797
+ try {
798
+ const result = await courier.stop({
799
+ sessionID: target.sessionID,
800
+ by
801
+ }, {
802
+ location: location(by)
803
+ });
804
+ ctx.ui.toast.show({
805
+ title: "courier",
806
+ message: stoppedLine(result),
807
+ variant: "success"
808
+ });
809
+ } catch (error) {
810
+ ctx.ui.toast.show({
811
+ title: "courier stop",
812
+ message: reason(error),
813
+ variant: "error"
814
+ });
815
+ }
816
+ return true;
817
+ }
818
+ var FOCUS_PROMPT = "pane.focus.left";
819
+ function TreePanel(props) {
820
+ const {
821
+ ctx,
822
+ input,
823
+ view,
824
+ courier,
825
+ location
826
+ } = props;
827
+ const theme = ctx.theme;
828
+ const [read, {
829
+ refetch
830
+ }] = createResource(() => ({
831
+ sessionID: view.root,
832
+ changed: props.changed()
833
+ }), ({
834
+ sessionID
835
+ }) => courier.tree({
836
+ sessionID
837
+ }, {
838
+ location: location(sessionID)
839
+ }).then((tree) => ({
840
+ sessionID,
841
+ tree
842
+ }), (error) => ({
843
+ sessionID,
844
+ error: reason(error)
845
+ })));
846
+ const latest = () => read.state === "ready" || read.state === "refreshing" ? read.latest : view.tree && {
847
+ sessionID: view.root,
848
+ tree: view.tree
849
+ };
850
+ const rows = () => latest()?.tree?.rows ?? [];
851
+ createEffect(() => {
852
+ const tree = latest()?.tree;
853
+ if (tree)
854
+ view.note({
855
+ tree
856
+ });
857
+ });
858
+ createEffect(() => view.note({
859
+ presentation: input.presentation,
860
+ focused: input.focused
861
+ }));
862
+ onMount(() => {
863
+ if (!view.refocus)
864
+ return;
865
+ const giveBack = setTimeout(() => ctx.keymap.dispatch(FOCUS_PROMPT));
866
+ onCleanup(() => clearTimeout(giveBack));
867
+ });
868
+ const [now, setNow] = createSignal(Date.now());
869
+ const tick = setInterval(() => setNow(Date.now()), TICK_MS);
870
+ onCleanup(() => clearInterval(tick));
871
+ const [selected, setSelected] = createSignal(0);
872
+ let placed = false;
873
+ createEffect(() => {
874
+ const index = placed ? -1 : rows().findIndex((row) => row.sessionID === input.sessionID);
875
+ if (index < 0)
876
+ return;
877
+ placed = true;
878
+ setSelected(index);
879
+ });
880
+ const current = () => rows()[Math.min(selected(), rows().length - 1)];
881
+ let scroll;
882
+ createEffect(() => {
883
+ const index = Math.min(selected(), rows().length - 1);
884
+ if (index >= 0)
885
+ scroll?.scrollChildIntoView(`courier-row-${index}`);
886
+ });
887
+ const move = (by) => setSelected((index) => Math.max(0, Math.min(rows().length - 1, Math.min(index, rows().length - 1) + by)));
888
+ const open = () => {
889
+ const row = current();
890
+ if (row)
891
+ ctx.ui.router.navigate({
892
+ type: "session",
893
+ sessionID: row.sessionID
894
+ });
895
+ };
896
+ const stopSelected = () => {
897
+ const row = current();
898
+ if (row)
899
+ confirmStop(ctx, courier, location, view.root, {
900
+ sessionID: row.sessionID,
901
+ title: row.title,
902
+ under: reach(rows(), row.sessionID) - 1
903
+ });
904
+ };
905
+ ctx.keymap.layer(() => ({
906
+ priority: 10,
907
+ enabled: () => input.focused,
908
+ commands: [{
909
+ bind: "up",
910
+ title: "Previous session",
911
+ group: "Courier",
912
+ run: () => move(-1)
913
+ }, {
914
+ bind: "k",
915
+ title: "Previous session",
916
+ group: "Courier",
917
+ run: () => move(-1)
918
+ }, {
919
+ bind: "down",
920
+ title: "Next session",
921
+ group: "Courier",
922
+ run: () => move(1)
923
+ }, {
924
+ bind: "j",
925
+ title: "Next session",
926
+ group: "Courier",
927
+ run: () => move(1)
928
+ }, {
929
+ bind: "return",
930
+ title: "Open session",
931
+ group: "Courier",
932
+ run: open
933
+ }, {
934
+ bind: "s",
935
+ title: "Stop session and those under it",
936
+ group: "Courier",
937
+ run: stopSelected
938
+ }, {
939
+ bind: "r",
940
+ title: "Refresh",
941
+ group: "Courier",
942
+ run: () => void refetch()
943
+ }, {
944
+ bind: "f",
945
+ title: "Toggle fullscreen",
946
+ group: "Courier",
947
+ run: () => input.toggleFullscreen()
948
+ }, {
949
+ bind: "q",
950
+ title: "Close courier tree",
951
+ group: "Courier",
952
+ run: () => input.close()
953
+ }]
954
+ }));
955
+ const root = () => parts(rows(), view.root);
956
+ const rowColor = (row) => {
957
+ if (row.state === "waiting")
958
+ return theme.text.feedback.warning.base;
959
+ return row.state === "gone" ? theme.text.muted : theme.text.base;
960
+ };
961
+ return (() => {
962
+ var _el$3 = _$createElement("box"), _el$4 = _$createElement("box"), _el$5 = _$createElement("text"), _el$13 = _$createElement("scrollbox"), _el$14 = _$createElement("text");
963
+ _$insertNode(_el$3, _el$4);
964
+ _$insertNode(_el$3, _el$13);
965
+ _$insertNode(_el$3, _el$14);
966
+ _$setProp(_el$3, "flexDirection", "column");
967
+ _$setProp(_el$3, "flexGrow", 1);
968
+ _$setProp(_el$3, "paddingLeft", 1);
969
+ _$setProp(_el$3, "paddingRight", 1);
970
+ _$insertNode(_el$4, _el$5);
971
+ _$setProp(_el$4, "flexDirection", "row");
972
+ _$setProp(_el$4, "flexShrink", 0);
973
+ _$setProp(_el$4, "gap", 2);
974
+ _$insertNode(_el$5, _$createTextNode(`courier tree`));
975
+ _$setProp(_el$5, "wrapMode", "none");
976
+ _$insert(_el$4, _$createComponent(Show, {
977
+ get when() {
978
+ return root().started;
979
+ },
980
+ get children() {
981
+ var _el$7 = _$createElement("text");
982
+ _$setProp(_el$7, "wrapMode", "none");
983
+ _$insert(_el$7, () => bar(root()));
984
+ _$effect((_$p) => _$setProp(_el$7, "fg", theme.text.muted, _$p));
985
+ return _el$7;
986
+ }
987
+ }), null);
988
+ _$insert(_el$4, _$createComponent(Show, {
989
+ get when() {
990
+ return latest()?.tree?.truncated;
991
+ },
992
+ get children() {
993
+ var _el$8 = _$createElement("text"), _el$9 = _$createTextNode(`first `), _el$0 = _$createTextNode(` shown`);
994
+ _$insertNode(_el$8, _el$9);
995
+ _$insertNode(_el$8, _el$0);
996
+ _$setProp(_el$8, "wrapMode", "none");
997
+ _$insert(_el$8, () => latest()?.tree?.truncated, _el$0);
998
+ _$effect((_$p) => _$setProp(_el$8, "fg", theme.text.feedback.warning.base, _$p));
999
+ return _el$8;
1000
+ }
1001
+ }), null);
1002
+ _$insert(_el$3, _$createComponent(Show, {
1003
+ get when() {
1004
+ return latest()?.error;
1005
+ },
1006
+ get children() {
1007
+ var _el$1 = _$createElement("text"), _el$10 = _$createTextNode(`courier RPC failed: `);
1008
+ _$insertNode(_el$1, _el$10);
1009
+ _$insert(_el$1, () => latest()?.error, null);
1010
+ _$effect((_$p) => _$setProp(_el$1, "fg", theme.text.feedback.error.base, _$p));
1011
+ return _el$1;
1012
+ }
1013
+ }), _el$13);
1014
+ _$insert(_el$3, _$createComponent(Show, {
1015
+ get when() {
1016
+ return _$memo(() => !!latest()?.tree)() && !rows().length;
1017
+ },
1018
+ get children() {
1019
+ var _el$11 = _$createElement("text");
1020
+ _$insertNode(_el$11, _$createTextNode(`No session started with courier_spawn under this one.`));
1021
+ _$effect((_$p) => _$setProp(_el$11, "fg", theme.text.muted, _$p));
1022
+ return _el$11;
1023
+ }
1024
+ }), _el$13);
1025
+ _$use((value) => scroll = value, _el$13);
1026
+ _$setProp(_el$13, "flexGrow", 1);
1027
+ _$insert(_el$13, _$createComponent(For, {
1028
+ get each() {
1029
+ return rows();
1030
+ },
1031
+ children: (row, index) => {
1032
+ const chosen = () => index() === Math.min(selected(), rows().length - 1);
1033
+ const indent = " ".repeat(row.depth - 1);
1034
+ return (() => {
1035
+ var _el$16 = _$createElement("box"), _el$17 = _$createElement("text"), _el$18 = _$createTextNode(` `), _el$19 = _$createElement("span");
1036
+ _$insertNode(_el$16, _el$17);
1037
+ _$setProp(_el$16, "flexDirection", "column");
1038
+ _$setProp(_el$16, "flexShrink", 0);
1039
+ _$insertNode(_el$17, _el$18);
1040
+ _$insertNode(_el$17, _el$19);
1041
+ _$setProp(_el$17, "wrapMode", "none");
1042
+ _$insert(_el$17, indent, _el$18);
1043
+ _$insert(_el$17, () => chosen() ? "\u25B8 " : " ", _el$18);
1044
+ _$insert(_el$17, () => row.title, _el$18);
1045
+ _$insert(_el$17, () => row.sessionID === input.sessionID ? " (on screen)" : "", _el$18);
1046
+ _$insert(_el$19, () => statusLine(row));
1047
+ _$insert(_el$16, _$createComponent(For, {
1048
+ get each() {
1049
+ return detailLines(row, rows(), now());
1050
+ },
1051
+ children: (line) => (() => {
1052
+ var _el$20 = _$createElement("text"), _el$21 = _$createTextNode(` `);
1053
+ _$insertNode(_el$20, _el$21);
1054
+ _$setProp(_el$20, "wrapMode", "none");
1055
+ _$insert(_el$20, indent, _el$21);
1056
+ _$insert(_el$20, line, null);
1057
+ _$effect((_$p) => _$setProp(_el$20, "fg", theme.text.muted, _$p));
1058
+ return _el$20;
1059
+ })()
1060
+ }), null);
1061
+ _$effect((_p$) => {
1062
+ var _v$3 = `courier-row-${index()}`, _v$4 = chosen() && input.focused ? theme.background.raised.high : undefined, _v$5 = rowColor(row), _v$6 = {
1063
+ fg: theme.text.muted
1064
+ };
1065
+ _v$3 !== _p$.e && (_p$.e = _$setProp(_el$16, "id", _v$3, _p$.e));
1066
+ _v$4 !== _p$.t && (_p$.t = _$setProp(_el$16, "backgroundColor", _v$4, _p$.t));
1067
+ _v$5 !== _p$.a && (_p$.a = _$setProp(_el$17, "fg", _v$5, _p$.a));
1068
+ _v$6 !== _p$.o && (_p$.o = _$setProp(_el$19, "style", _v$6, _p$.o));
1069
+ return _p$;
1070
+ }, {
1071
+ e: undefined,
1072
+ t: undefined,
1073
+ a: undefined,
1074
+ o: undefined
1075
+ });
1076
+ return _el$16;
1077
+ })();
1078
+ }
1079
+ }));
1080
+ _$insertNode(_el$14, _$createTextNode(`\u2191\u2193 select \xB7 enter open \xB7 s stop \xB7 r refresh \xB7 f fullscreen \xB7 q close`));
1081
+ _$setProp(_el$14, "wrapMode", "none");
1082
+ _$setProp(_el$14, "flexShrink", 0);
1083
+ _$effect((_p$) => {
1084
+ var _v$ = theme.text.base, _v$2 = theme.text.muted;
1085
+ _v$ !== _p$.e && (_p$.e = _$setProp(_el$5, "fg", _v$, _p$.e));
1086
+ _v$2 !== _p$.t && (_p$.t = _$setProp(_el$14, "fg", _v$2, _p$.t));
1087
+ return _p$;
1088
+ }, {
1089
+ e: undefined,
1090
+ t: undefined
1091
+ });
1092
+ return _el$3;
1093
+ })();
1094
+ }
1095
+ var USAGE = "/courier tree opens the session tree; /courier stop [session] stops a session and those under it";
1096
+ function addTreePanel(ctx) {
1097
+ const {
1098
+ courier,
1099
+ location
1100
+ } = courierOf(ctx);
1101
+ const [changed, stopListening] = changes(ctx, PANEL_CHANGES, PANEL_SETTLE_MS);
1102
+ let following;
1103
+ let attempt = 0;
1104
+ const viewOf = (sessionID) => {
1105
+ const here = following?.at === sessionID ? following : undefined;
1106
+ const refocus = here?.refocus === true;
1107
+ if (here)
1108
+ here.refocus = false;
1109
+ return {
1110
+ root: here?.root ?? sessionID,
1111
+ tree: here?.tree,
1112
+ refocus,
1113
+ note: (fields) => {
1114
+ if (here && following === here)
1115
+ Object.assign(here, fields);
1116
+ }
1117
+ };
1118
+ };
1119
+ const release = ctx.ui.slot({
1120
+ append: "session.panel",
1121
+ render: (input) => _$createComponent(Show, {
1122
+ get when() {
1123
+ return input.name === PANEL;
1124
+ },
1125
+ get children() {
1126
+ return _$createComponent(TreePanel, {
1127
+ ctx,
1128
+ input,
1129
+ get view() {
1130
+ return viewOf(input.sessionID);
1131
+ },
1132
+ courier,
1133
+ location,
1134
+ changed
1135
+ });
1136
+ }
1137
+ })
1138
+ });
1139
+ const show = (from, at, root, tree) => {
1140
+ following = {
1141
+ ...from,
1142
+ root,
1143
+ at,
1144
+ to: undefined,
1145
+ tree,
1146
+ refocus: !from.focused
1147
+ };
1148
+ ctx.ui.panel.open(PANEL, {
1149
+ presentation: from.presentation
1150
+ });
1151
+ };
1152
+ const treeOf = (sessionID) => courier.tree({
1153
+ sessionID
1154
+ }, {
1155
+ location: location(sessionID)
1156
+ }).catch(() => {
1157
+ return;
1158
+ });
1159
+ const follow = (from, sessionID) => {
1160
+ const token = ++attempt;
1161
+ const under = (tree) => tree?.rows.some((row) => row.sessionID === sessionID) === true;
1162
+ Object.assign(from, {
1163
+ at: undefined,
1164
+ to: sessionID
1165
+ });
1166
+ if (sessionID === from.root || under(from.tree))
1167
+ return show(from, sessionID, from.root, from.tree);
1168
+ (async () => {
1169
+ const inside = await treeOf(from.root);
1170
+ const own = under(inside) ? undefined : await treeOf(sessionID);
1171
+ if (token !== attempt || following !== from)
1172
+ return;
1173
+ if (under(inside))
1174
+ show(from, sessionID, from.root, inside);
1175
+ else if (own?.rows.length)
1176
+ show(from, sessionID, sessionID, own);
1177
+ else
1178
+ following = undefined;
1179
+ })();
1180
+ };
1181
+ const stopFollowing = createRoot((dispose) => {
1182
+ createEffect(() => {
1183
+ const route = ctx.ui.router.current();
1184
+ const sessionID = route.type === "session" ? route.sessionID : undefined;
1185
+ const shown2 = ctx.ui.panel.current();
1186
+ untrack(() => {
1187
+ if (!following || shown2 && shown2.sessionID === sessionID || !following.at && following.to === sessionID)
1188
+ return;
1189
+ if (!shown2 && following.at === sessionID || !sessionID)
1190
+ following = undefined;
1191
+ else
1192
+ follow(following, sessionID);
1193
+ });
1194
+ });
1195
+ return dispose;
1196
+ });
1197
+ const openPanel = () => {
1198
+ const route = ctx.ui.router.current();
1199
+ const before = following;
1200
+ if (route.type === "session" && following?.at !== route.sessionID)
1201
+ following = {
1202
+ root: route.sessionID,
1203
+ at: route.sessionID,
1204
+ presentation: "panel",
1205
+ focused: true
1206
+ };
1207
+ if (ctx.ui.panel.open(PANEL))
1208
+ return;
1209
+ following = before;
1210
+ ctx.ui.toast.show({
1211
+ title: "courier",
1212
+ message: "Open a session first",
1213
+ variant: "warning"
1214
+ });
1215
+ };
1216
+ const stopUnder = async (target) => {
1217
+ const route = ctx.ui.router.current();
1218
+ if (route.type !== "session")
1219
+ return ctx.ui.toast.show({
1220
+ title: "courier",
1221
+ message: "Open a session first",
1222
+ variant: "warning"
1223
+ });
1224
+ const rows = await courier.tree({
1225
+ sessionID: route.sessionID
1226
+ }, {
1227
+ location: location(route.sessionID)
1228
+ }).then((tree) => tree.rows, () => []);
1229
+ const sessionID = target || await ctx.ui.dialog.select({
1230
+ title: "Stop which session?",
1231
+ options: rows.map((row2) => ({
1232
+ title: row2.title,
1233
+ value: row2.sessionID,
1234
+ description: `${row2.sessionID} \xB7 ${statusLine(row2)}`
1235
+ }))
1236
+ });
1237
+ if (!sessionID)
1238
+ return;
1239
+ const row = rows.find((one) => one.sessionID === sessionID);
1240
+ await confirmStop(ctx, courier, location, route.sessionID, {
1241
+ sessionID,
1242
+ title: row?.title ?? sessionID,
1243
+ under: row ? reach(rows, sessionID) - 1 : 0
1244
+ });
1245
+ };
1246
+ ctx.keymap.layer(() => ({
1247
+ mode: "global",
1248
+ commands: [{
1249
+ id: "courier.command",
1250
+ title: "Courier",
1251
+ description: USAGE,
1252
+ group: "Courier",
1253
+ slash: {
1254
+ name: "courier",
1255
+ arguments: true
1256
+ },
1257
+ run: async (input) => {
1258
+ const [verb = "tree", target = ""] = (input ?? "").trim().split(/\s+/).filter(Boolean);
1259
+ if (verb === "tree")
1260
+ return openPanel();
1261
+ if (verb === "stop")
1262
+ return stopUnder(target);
1263
+ ctx.ui.toast.show({
1264
+ title: "courier",
1265
+ message: USAGE,
1266
+ variant: "warning"
1267
+ });
1268
+ }
1269
+ }, {
1270
+ id: "courier.tree",
1271
+ title: "Courier session tree",
1272
+ group: "Courier",
1273
+ palette: true,
1274
+ run: openPanel
1275
+ }]
1276
+ }));
1277
+ return () => {
1278
+ stopFollowing();
1279
+ stopListening();
1280
+ release();
1281
+ };
1282
+ }
1283
+ var NOTIFIED_MAX = 500;
1284
+ function addTreeAttention(ctx) {
1285
+ const notified = new Set;
1286
+ const title = (sessionID) => ctx.data.session.get(sessionID)?.title;
1287
+ return ctx.data.on("session.inbox.enqueued", (event) => {
1288
+ const {
1289
+ sessionID,
1290
+ inboxID,
1291
+ item
1292
+ } = event.data;
1293
+ const metadata = item.type === "synthetic" ? item.payload.metadata ?? {} : {};
1294
+ if (metadata.source !== "courier" || notified.has(inboxID))
1295
+ return;
1296
+ const group = typeof metadata.group === "string" ? metadata.group : undefined;
1297
+ const status = typeof metadata.status === "string" ? metadata.status : undefined;
1298
+ const from = typeof metadata.from === "string" ? metadata.from : "";
1299
+ if (!group && !status)
1300
+ return;
1301
+ notified.add(inboxID);
1302
+ if (notified.size > NOTIFIED_MAX)
1303
+ notified.delete(notified.values().next().value);
1304
+ (group ? ctx.attention.notify({
1305
+ title: title(sessionID),
1306
+ message: `Group ${group}: ${typeof metadata.reports === "number" ? counted(metadata.reports, "report") : "its reports"} arrived`,
1307
+ notification: {
1308
+ when: "blurred"
1309
+ },
1310
+ sound: {
1311
+ name: "done",
1312
+ when: "always"
1313
+ }
1314
+ }) : ctx.attention.notify({
1315
+ title: title(sessionID),
1316
+ message: `${title(from) ?? from} reported ${status}`,
1317
+ notification: false,
1318
+ sound: {
1319
+ name: "subagent_done",
1320
+ when: "always"
1321
+ }
1322
+ })).catch(() => {
1323
+ return;
1324
+ });
1325
+ });
1326
+ }
1327
+
1328
+ // src/tui.tsx
1329
+ var tui_default = Plugin.define({
1330
+ id: "courier.tui",
1331
+ setup: (ctx) => {
1332
+ const inbox = addAsksInbox(ctx);
1333
+ const cleanups = [inbox.cleanup, addTreeStatus(ctx, (sessionID) => void inbox.open(sessionID)), addTreePanel(ctx), addTreeAttention(ctx)];
1334
+ return () => {
1335
+ for (const cleanup of cleanups.toReversed())
1336
+ cleanup();
1337
+ };
1338
+ }
1339
+ });
1340
+ export {
1341
+ tui_default as default
1342
+ };