opencode-courier 0.2.3 → 0.4.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.
Files changed (143) hide show
  1. package/README.md +198 -193
  2. package/dist/index.d.ts +4 -8
  3. package/dist/index.js +66 -85
  4. package/dist/kernel/protocol.d.ts +20 -0
  5. package/dist/kernel/protocol.js +36 -0
  6. package/dist/kernel/tool.d.ts +5 -0
  7. package/dist/kernel/tool.js +5 -0
  8. package/dist/person/events.d.ts +23 -0
  9. package/dist/person/events.js +113 -0
  10. package/dist/person/form.d.ts +26 -0
  11. package/dist/person/form.js +64 -0
  12. package/dist/person/index.d.ts +17 -0
  13. package/dist/person/index.js +5 -0
  14. package/dist/{relay.d.ts → person/permission.d.ts} +29 -25
  15. package/dist/{relay.js → person/permission.js} +11 -9
  16. package/dist/person/ports.d.ts +58 -0
  17. package/dist/person/ports.js +2 -0
  18. package/dist/{question → person/question}/answer.d.ts +33 -4
  19. package/dist/{question → person/question}/answer.js +49 -17
  20. package/dist/person/question/index.d.ts +7 -0
  21. package/dist/{question → person/question}/index.js +3 -3
  22. package/dist/person/question/lifecycle.d.ts +43 -0
  23. package/dist/{question → person/question}/lifecycle.js +10 -19
  24. package/dist/{question → person/question}/pure.d.ts +4 -6
  25. package/dist/{question → person/question}/pure.js +3 -4
  26. package/dist/{question → person/question}/relay.d.ts +1 -1
  27. package/dist/{question → person/question}/relay.js +27 -35
  28. package/dist/{question → person/question}/runtime.d.ts +2 -3
  29. package/dist/{question → person/question}/runtime.js +2 -3
  30. package/dist/{question → person/question}/shared.d.ts +13 -3
  31. package/dist/person/question/shared.js +52 -0
  32. package/dist/person/question/types.d.ts +121 -0
  33. package/dist/person/question/types.js +1 -0
  34. package/dist/person/rpc-handlers.d.ts +160 -0
  35. package/dist/person/rpc-handlers.js +62 -0
  36. package/dist/person/rpc.d.ts +227 -0
  37. package/dist/person/rpc.js +70 -0
  38. package/dist/person/text.d.ts +68 -0
  39. package/dist/{notices.js → person/text.js} +53 -190
  40. package/dist/person/tools.d.ts +20 -0
  41. package/dist/person/tools.js +55 -0
  42. package/dist/person/tui.d.ts +46 -0
  43. package/dist/platform/bounded.d.ts +12 -0
  44. package/dist/{bounded.js → platform/bounded.js} +6 -4
  45. package/dist/platform/events.d.ts +34 -0
  46. package/dist/platform/events.js +121 -0
  47. package/dist/platform/hub.d.ts +195 -0
  48. package/dist/{hub.js → platform/hub.js} +78 -119
  49. package/dist/platform/json.d.ts +8 -0
  50. package/dist/platform/json.js +15 -0
  51. package/dist/platform/version.d.ts +16 -0
  52. package/dist/{version.js → platform/version.js} +5 -11
  53. package/dist/rpc.d.ts +2 -0
  54. package/dist/rpc.js +3 -0
  55. package/dist/scheduling/index.d.ts +6 -0
  56. package/dist/scheduling/index.js +5 -0
  57. package/dist/{later.d.ts → scheduling/later.d.ts} +13 -9
  58. package/dist/{later.js → scheduling/later.js} +18 -9
  59. package/dist/scheduling/text.d.ts +6 -0
  60. package/dist/scheduling/text.js +10 -0
  61. package/dist/scheduling/tools.d.ts +14 -0
  62. package/dist/scheduling/tools.js +48 -0
  63. package/dist/skill.d.ts +17 -0
  64. package/dist/skill.js +62 -0
  65. package/dist/{cleanup.d.ts → tree/cleanup.d.ts} +5 -26
  66. package/dist/{cleanup.js → tree/cleanup.js} +20 -15
  67. package/dist/tree/context.d.ts +15 -0
  68. package/dist/tree/context.js +34 -0
  69. package/dist/tree/domain.d.ts +101 -0
  70. package/dist/tree/domain.js +68 -0
  71. package/dist/tree/events.d.ts +71 -0
  72. package/dist/tree/events.js +204 -0
  73. package/dist/tree/forget.d.ts +24 -0
  74. package/dist/tree/forget.js +59 -0
  75. package/dist/tree/group.d.ts +82 -0
  76. package/dist/tree/group.js +203 -0
  77. package/dist/tree/index.d.ts +10 -0
  78. package/dist/tree/index.js +11 -0
  79. package/dist/tree/limits.d.ts +10 -0
  80. package/dist/tree/limits.js +60 -0
  81. package/dist/tree/panel.d.ts +13 -0
  82. package/dist/tree/panel.js +75 -0
  83. package/dist/tree/ports.d.ts +276 -0
  84. package/dist/tree/ports.js +1 -0
  85. package/dist/tree/report.d.ts +23 -0
  86. package/dist/tree/report.js +33 -0
  87. package/dist/tree/roster.d.ts +27 -0
  88. package/dist/tree/roster.js +62 -0
  89. package/dist/tree/rpc-handlers.d.ts +201 -0
  90. package/dist/tree/rpc-handlers.js +20 -0
  91. package/dist/tree/rpc.d.ts +344 -0
  92. package/dist/tree/rpc.js +83 -0
  93. package/dist/tree/send.d.ts +14 -0
  94. package/dist/tree/send.js +99 -0
  95. package/dist/tree/spawn.d.ts +21 -0
  96. package/dist/tree/spawn.js +173 -0
  97. package/dist/tree/status.d.ts +128 -0
  98. package/dist/tree/status.js +62 -0
  99. package/dist/tree/stop.d.ts +33 -0
  100. package/dist/tree/stop.js +116 -0
  101. package/dist/tree/store.d.ts +24 -0
  102. package/dist/tree/store.js +264 -0
  103. package/dist/tree/subtree.d.ts +101 -0
  104. package/dist/tree/subtree.js +60 -0
  105. package/dist/tree/summary.d.ts +7 -0
  106. package/dist/tree/summary.js +35 -0
  107. package/dist/tree/text.d.ts +77 -0
  108. package/dist/tree/text.js +287 -0
  109. package/dist/tree/tools.d.ts +54 -0
  110. package/dist/tree/tools.js +167 -0
  111. package/dist/tree/tui.d.ts +53 -0
  112. package/dist/tui.d.ts +3 -0
  113. package/dist/tui.js +1219 -0
  114. package/dist/webhook/index.d.ts +5 -0
  115. package/dist/webhook/index.js +3 -0
  116. package/dist/{webhook.d.ts → webhook/receiver.d.ts} +25 -36
  117. package/dist/{webhook.js → webhook/receiver.js} +66 -67
  118. package/dist/webhook/subscriptions.d.ts +29 -0
  119. package/dist/webhook/subscriptions.js +52 -0
  120. package/dist/webhook/text.d.ts +23 -0
  121. package/dist/webhook/text.js +82 -0
  122. package/dist/webhook/tools.d.ts +19 -0
  123. package/dist/webhook/tools.js +46 -0
  124. package/package.json +18 -7
  125. package/dist/bounded.d.ts +0 -10
  126. package/dist/courier.d.ts +0 -95
  127. package/dist/courier.js +0 -131
  128. package/dist/hub.d.ts +0 -325
  129. package/dist/json.d.ts +0 -4
  130. package/dist/json.js +0 -4
  131. package/dist/notices.d.ts +0 -246
  132. package/dist/question/index.d.ts +0 -6
  133. package/dist/question/lifecycle.d.ts +0 -52
  134. package/dist/question/shared.js +0 -24
  135. package/dist/roster.d.ts +0 -81
  136. package/dist/roster.js +0 -175
  137. package/dist/tools.d.ts +0 -74
  138. package/dist/tools.js +0 -228
  139. package/dist/version.d.ts +0 -22
  140. package/dist/watch.d.ts +0 -61
  141. package/dist/watch.js +0 -289
  142. /package/dist/{storage.d.ts → platform/storage.d.ts} +0 -0
  143. /package/dist/{storage.js → platform/storage.js} +0 -0
package/dist/tui.js ADDED
@@ -0,0 +1,1219 @@
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
+ commands: [{
522
+ id: INBOX_COMMAND,
523
+ title: "Courier: what waits on you",
524
+ description: "Answer the permission requests, questions and forms of the sessions started with courier",
525
+ group: "Courier",
526
+ bind: "<leader>k",
527
+ palette: true,
528
+ slash: {
529
+ name: "asks"
530
+ },
531
+ run: () => void open()
532
+ }]
533
+ }));
534
+ return {
535
+ open,
536
+ cleanup: () => {
537
+ clearTimeout(settling);
538
+ unlisten();
539
+ }
540
+ };
541
+ }
542
+
543
+ // src/tree/tui.tsx
544
+ import { use as _$use } from "@opentui/solid";
545
+ import { memo as _$memo } from "@opentui/solid";
546
+ import { createComponent as _$createComponent } from "@opentui/solid";
547
+ import { effect as _$effect } from "@opentui/solid";
548
+ import { insert as _$insert } from "@opentui/solid";
549
+ import { createTextNode as _$createTextNode } from "@opentui/solid";
550
+ import { insertNode as _$insertNode } from "@opentui/solid";
551
+ import { setProp as _$setProp } from "@opentui/solid";
552
+ import { createElement as _$createElement } from "@opentui/solid";
553
+ import { createEffect, createResource, createSignal, For, onCleanup, Show } from "solid-js";
554
+
555
+ // src/tree/rpc.ts
556
+ import { Rpc as Rpc2 } from "@opencode/plugin/rpc";
557
+ import { Schema as Schema2 } from "effect";
558
+ var Count2 = Schema2.Int.check(Schema2.isGreaterThanOrEqualTo(0));
559
+ var Group = Schema2.Struct({ name: Schema2.String, reported: Count2, members: Count2 });
560
+ var Summary = Schema2.Struct({
561
+ running: Count2,
562
+ waiting: Count2,
563
+ idle: Count2,
564
+ groups: Schema2.Array(Group),
565
+ truncated: Schema2.optionalKey(Count2)
566
+ });
567
+ var Wait = Schema2.Union([
568
+ Schema2.Struct({ type: Schema2.Literal("permission"), action: Schema2.String, resources: Schema2.Array(Schema2.String) }),
569
+ Schema2.Struct({ type: Schema2.Literal("question"), question: Schema2.String })
570
+ ]);
571
+ var Row = Schema2.Struct({
572
+ sessionID: Schema2.String,
573
+ parentID: Schema2.String,
574
+ title: Schema2.String,
575
+ depth: Count2,
576
+ state: Schema2.Literals(["running", "waiting", "idle", "gone"]),
577
+ outcome: Schema2.optionalKey(Schema2.Literals(["succeeded", "failed", "interrupted"])),
578
+ report: Schema2.optionalKey(Schema2.Struct({ state: Schema2.Literals(["owed", "reported", "ended"]), status: Schema2.optionalKey(Schema2.String), progressed: Schema2.Boolean })),
579
+ group: Schema2.optionalKey(Schema2.Struct({ name: Schema2.String, standing: Schema2.String })),
580
+ isolated: Schema2.Boolean,
581
+ created: Schema2.Number,
582
+ idle: Schema2.optionalKey(Schema2.Number),
583
+ cost: Schema2.optionalKey(Schema2.Number),
584
+ tool: Schema2.optionalKey(Schema2.Struct({ name: Schema2.String, at: Schema2.Number })),
585
+ waits: Schema2.Array(Wait)
586
+ });
587
+ var Tree = Schema2.Struct({
588
+ rows: Schema2.Array(Row),
589
+ truncated: Schema2.optionalKey(Count2)
590
+ });
591
+ var Stopped = Schema2.Struct({
592
+ stopped: Schema2.Array(Schema2.Struct({ sessionID: Schema2.String, title: Schema2.String, outcome: Schema2.String })),
593
+ cancelled: Count2
594
+ });
595
+ var treeRpc = Rpc2.define({
596
+ id: "courier",
597
+ methods: {
598
+ summary: {
599
+ input: Schema2.toStandardSchemaV1(Schema2.Struct({ sessionID: Schema2.String })),
600
+ output: Schema2.toStandardSchemaV1(Summary)
601
+ },
602
+ tree: {
603
+ input: Schema2.toStandardSchemaV1(Schema2.Struct({ sessionID: Schema2.String })),
604
+ output: Schema2.toStandardSchemaV1(Tree)
605
+ },
606
+ stop: {
607
+ input: Schema2.toStandardSchemaV1(Schema2.Struct({ sessionID: Schema2.String, by: Schema2.String })),
608
+ output: Schema2.toStandardSchemaV1(Stopped),
609
+ errors: { refused: Schema2.toStandardSchemaV1(Schema2.Struct({})) }
610
+ }
611
+ },
612
+ events: {}
613
+ });
614
+
615
+ // src/tree/tui.tsx
616
+ 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"]);
617
+ var PANEL_CHANGES = new Set([...CHANGES, "session.tool.called", "session.tool.success", "session.tool.failed", "session.usage.updated", "session.renamed"]);
618
+ var SETTLE_MS2 = 250;
619
+ var PANEL_SETTLE_MS = 1000;
620
+ var PANEL = "courier-tree";
621
+ var TICK_MS = 1000;
622
+ function summaryLine(summary) {
623
+ 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);
624
+ return parts.length ? `courier ${parts.join(" \xB7 ")}` : "";
625
+ }
626
+ function duration(ms) {
627
+ const seconds = Math.max(0, Math.floor(ms / 1000));
628
+ const pad = (value) => String(value).padStart(2, "0");
629
+ if (seconds < 60)
630
+ return `${seconds}s`;
631
+ if (seconds < 3600)
632
+ return `${Math.floor(seconds / 60)}m ${pad(seconds % 60)}s`;
633
+ return `${Math.floor(seconds / 3600)}h ${pad(Math.floor(seconds / 60) % 60)}m`;
634
+ }
635
+ function parts(rows, parentID) {
636
+ const children = rows.filter((row) => row.parentID === parentID);
637
+ return {
638
+ reported: children.filter((row) => row.report?.state === "reported").length,
639
+ started: children.length
640
+ };
641
+ }
642
+ function bar({
643
+ reported,
644
+ started
645
+ }, width = 10) {
646
+ const filled = started ? Math.round(reported / started * width) : 0;
647
+ return `[${"\u2588".repeat(filled)}${"\u2591".repeat(width - filled)}] ${reported}/${started} reported`;
648
+ }
649
+ function waitLine(wait) {
650
+ return wait.type === "permission" ? `asks permission: ${[wait.action, ...wait.resources].join(" ")}` : `asks: ${wait.question}`;
651
+ }
652
+ function statusLine(row) {
653
+ return [stateWord(row), reportWords(row.report), row.group ? `group ${row.group.name} ${row.group.standing}` : "", row.isolated ? "isolated" : ""].filter(Boolean).join(" \xB7 ");
654
+ }
655
+ function stateWord(row) {
656
+ if (row.state === "waiting")
657
+ return "waiting on you";
658
+ if (row.state === "idle" && (row.outcome === "failed" || row.outcome === "interrupted"))
659
+ return row.outcome;
660
+ return row.state;
661
+ }
662
+ function reportWords(report) {
663
+ if (!report)
664
+ return "";
665
+ if (report.state === "owed")
666
+ return "owes a report";
667
+ if (report.state === "ended")
668
+ return "ended without a report";
669
+ return ["reported", report.status].filter(Boolean).join(" ");
670
+ }
671
+ var counted = (count, noun) => `${count} ${noun}${count === 1 ? "" : "s"}`;
672
+ function activityLine(row, now) {
673
+ 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 ");
674
+ }
675
+ function detailLines(row, rows, now) {
676
+ const started = parts(rows, row.sessionID);
677
+ if (row.waits.length)
678
+ return [...started.started ? [bar(started)] : [], ...row.waits.map(waitLine)];
679
+ if (started.started)
680
+ return [bar(started)];
681
+ return row.state === "gone" ? [] : [activityLine(row, now)];
682
+ }
683
+ function courierOf(ctx) {
684
+ const courier = ctx.client.rpc(treeRpc);
685
+ const location = (sessionID) => ctx.data.session.get(sessionID)?.location ?? ctx.location ?? ctx.data.location.default();
686
+ return {
687
+ courier,
688
+ location
689
+ };
690
+ }
691
+ function changes(ctx, types, settle) {
692
+ const [changed, setChanged] = createSignal(0);
693
+ let settling;
694
+ const unlisten = ctx.data.listen(({
695
+ details
696
+ }) => {
697
+ if (!types.has(details.type) || settling)
698
+ return;
699
+ settling = setTimeout(() => {
700
+ settling = undefined;
701
+ setChanged((count) => count + 1);
702
+ }, settle);
703
+ });
704
+ return [changed, () => {
705
+ clearTimeout(settling);
706
+ unlisten();
707
+ }];
708
+ }
709
+ function reason(error) {
710
+ if (error instanceof Error)
711
+ return error.message;
712
+ return typeof error === "object" && error && "message" in error ? String(error.message) : String(error);
713
+ }
714
+ function addTreeStatus(ctx, onWaiting) {
715
+ const {
716
+ courier,
717
+ location
718
+ } = courierOf(ctx);
719
+ const [changed, stopListening] = changes(ctx, CHANGES, SETTLE_MS2);
720
+ const release = ctx.ui.slot({
721
+ append: "prompt.footer.status",
722
+ render: (input) => {
723
+ const [read] = createResource(() => input.sessionID ? {
724
+ sessionID: input.sessionID,
725
+ changed: changed()
726
+ } : undefined, ({
727
+ sessionID
728
+ }) => courier.summary({
729
+ sessionID
730
+ }, {
731
+ location: location(sessionID)
732
+ }).then((summary) => ({
733
+ sessionID,
734
+ line: summaryLine(summary),
735
+ waiting: summary.waiting
736
+ }), () => ({
737
+ sessionID,
738
+ line: "",
739
+ waiting: 0
740
+ })));
741
+ const shown2 = () => {
742
+ const latest = read.state === "ready" || read.state === "refreshing" ? read.latest : undefined;
743
+ return latest && latest.sessionID === input.sessionID ? latest : undefined;
744
+ };
745
+ const line = () => shown2()?.line ?? "";
746
+ const click = () => {
747
+ const latest = shown2();
748
+ if (latest?.waiting)
749
+ onWaiting?.(latest.sessionID);
750
+ };
751
+ return _$createComponent(Show, {
752
+ get when() {
753
+ return line();
754
+ },
755
+ get children() {
756
+ var _el$ = _$createElement("text"), _el$2 = _$createTextNode(` `);
757
+ _$insertNode(_el$, _el$2);
758
+ _$setProp(_el$, "wrapMode", "none");
759
+ _$setProp(_el$, "onMouseUp", click);
760
+ _$insert(_el$, line, null);
761
+ _$effect((_$p) => _$setProp(_el$, "fg", ctx.theme.text.muted, _$p));
762
+ return _el$;
763
+ }
764
+ });
765
+ }
766
+ });
767
+ return () => {
768
+ stopListening();
769
+ release();
770
+ };
771
+ }
772
+ function reach(rows, sessionID) {
773
+ return 1 + rows.filter((row) => row.parentID === sessionID).reduce((total, row) => total + reach(rows, row.sessionID), 0);
774
+ }
775
+ function stoppedLine({
776
+ stopped,
777
+ cancelled
778
+ }) {
779
+ const interrupted = stopped.filter((one) => one.outcome === "interrupted").length;
780
+ const failed = stopped.filter((one) => one.outcome === "failed").length;
781
+ return [`Stopped ${counted(stopped.length, "session")}: ${interrupted} interrupted`, failed ? `${failed} failed to stop` : "", cancelled ? `${counted(cancelled, "scheduled message")} cancelled` : ""].filter(Boolean).join(", ");
782
+ }
783
+ async function confirmStop(ctx, courier, location, by, target) {
784
+ const under = target.under ? ` and the ${counted(target.under, "session")} under it` : "";
785
+ const confirmed = await ctx.ui.dialog.confirm({
786
+ title: "Stop session",
787
+ message: `Stop "${target.title}" (${target.sessionID})${under}? Their turns are interrupted and their scheduled messages cancelled, as courier_stop does.`,
788
+ label: {
789
+ confirm: "Stop",
790
+ cancel: "Keep running"
791
+ }
792
+ });
793
+ if (!confirmed)
794
+ return false;
795
+ try {
796
+ const result = await courier.stop({
797
+ sessionID: target.sessionID,
798
+ by
799
+ }, {
800
+ location: location(by)
801
+ });
802
+ ctx.ui.toast.show({
803
+ title: "courier",
804
+ message: stoppedLine(result),
805
+ variant: "success"
806
+ });
807
+ } catch (error) {
808
+ ctx.ui.toast.show({
809
+ title: "courier stop",
810
+ message: reason(error),
811
+ variant: "error"
812
+ });
813
+ }
814
+ return true;
815
+ }
816
+ function TreePanel(props) {
817
+ const {
818
+ ctx,
819
+ input,
820
+ courier,
821
+ location
822
+ } = props;
823
+ const theme = ctx.theme;
824
+ const [read, {
825
+ refetch
826
+ }] = createResource(() => ({
827
+ sessionID: input.sessionID,
828
+ changed: props.changed()
829
+ }), ({
830
+ sessionID
831
+ }) => courier.tree({
832
+ sessionID
833
+ }, {
834
+ location: location(sessionID)
835
+ }).then((tree) => ({
836
+ sessionID,
837
+ tree
838
+ }), (error) => ({
839
+ sessionID,
840
+ error: reason(error)
841
+ })));
842
+ const latest = () => read.state === "ready" || read.state === "refreshing" ? read.latest : undefined;
843
+ const rows = () => latest()?.tree?.rows ?? [];
844
+ const [now, setNow] = createSignal(Date.now());
845
+ const tick = setInterval(() => setNow(Date.now()), TICK_MS);
846
+ onCleanup(() => clearInterval(tick));
847
+ const [selected, setSelected] = createSignal(0);
848
+ const current = () => rows()[Math.min(selected(), rows().length - 1)];
849
+ let scroll;
850
+ createEffect(() => {
851
+ const index = Math.min(selected(), rows().length - 1);
852
+ if (index >= 0)
853
+ scroll?.scrollChildIntoView(`courier-row-${index}`);
854
+ });
855
+ const move = (by) => setSelected((index) => Math.max(0, Math.min(rows().length - 1, Math.min(index, rows().length - 1) + by)));
856
+ const open = () => {
857
+ const row = current();
858
+ if (row)
859
+ ctx.ui.router.navigate({
860
+ type: "session",
861
+ sessionID: row.sessionID
862
+ });
863
+ };
864
+ const stopSelected = () => {
865
+ const row = current();
866
+ if (row)
867
+ confirmStop(ctx, courier, location, input.sessionID, {
868
+ sessionID: row.sessionID,
869
+ title: row.title,
870
+ under: reach(rows(), row.sessionID) - 1
871
+ });
872
+ };
873
+ ctx.keymap.layer(() => ({
874
+ priority: 10,
875
+ enabled: () => input.focused,
876
+ commands: [{
877
+ bind: "up",
878
+ title: "Previous session",
879
+ group: "Courier",
880
+ run: () => move(-1)
881
+ }, {
882
+ bind: "k",
883
+ title: "Previous session",
884
+ group: "Courier",
885
+ run: () => move(-1)
886
+ }, {
887
+ bind: "down",
888
+ title: "Next session",
889
+ group: "Courier",
890
+ run: () => move(1)
891
+ }, {
892
+ bind: "j",
893
+ title: "Next session",
894
+ group: "Courier",
895
+ run: () => move(1)
896
+ }, {
897
+ bind: "return",
898
+ title: "Open session",
899
+ group: "Courier",
900
+ run: open
901
+ }, {
902
+ bind: "s",
903
+ title: "Stop session and those under it",
904
+ group: "Courier",
905
+ run: stopSelected
906
+ }, {
907
+ bind: "r",
908
+ title: "Refresh",
909
+ group: "Courier",
910
+ run: () => void refetch()
911
+ }, {
912
+ bind: "f",
913
+ title: "Toggle fullscreen",
914
+ group: "Courier",
915
+ run: () => input.toggleFullscreen()
916
+ }, {
917
+ bind: "q",
918
+ title: "Close courier tree",
919
+ group: "Courier",
920
+ run: () => input.close()
921
+ }]
922
+ }));
923
+ const root = () => parts(rows(), input.sessionID);
924
+ const rowColor = (row) => {
925
+ if (row.state === "waiting")
926
+ return theme.text.feedback.warning.base;
927
+ return row.state === "gone" ? theme.text.muted : theme.text.base;
928
+ };
929
+ return (() => {
930
+ var _el$3 = _$createElement("box"), _el$4 = _$createElement("box"), _el$5 = _$createElement("text"), _el$13 = _$createElement("scrollbox"), _el$14 = _$createElement("text");
931
+ _$insertNode(_el$3, _el$4);
932
+ _$insertNode(_el$3, _el$13);
933
+ _$insertNode(_el$3, _el$14);
934
+ _$setProp(_el$3, "flexDirection", "column");
935
+ _$setProp(_el$3, "flexGrow", 1);
936
+ _$setProp(_el$3, "paddingLeft", 1);
937
+ _$setProp(_el$3, "paddingRight", 1);
938
+ _$insertNode(_el$4, _el$5);
939
+ _$setProp(_el$4, "flexDirection", "row");
940
+ _$setProp(_el$4, "flexShrink", 0);
941
+ _$setProp(_el$4, "gap", 2);
942
+ _$insertNode(_el$5, _$createTextNode(`courier tree`));
943
+ _$setProp(_el$5, "wrapMode", "none");
944
+ _$insert(_el$4, _$createComponent(Show, {
945
+ get when() {
946
+ return root().started;
947
+ },
948
+ get children() {
949
+ var _el$7 = _$createElement("text");
950
+ _$setProp(_el$7, "wrapMode", "none");
951
+ _$insert(_el$7, () => bar(root()));
952
+ _$effect((_$p) => _$setProp(_el$7, "fg", theme.text.muted, _$p));
953
+ return _el$7;
954
+ }
955
+ }), null);
956
+ _$insert(_el$4, _$createComponent(Show, {
957
+ get when() {
958
+ return latest()?.tree?.truncated;
959
+ },
960
+ get children() {
961
+ var _el$8 = _$createElement("text"), _el$9 = _$createTextNode(`first `), _el$0 = _$createTextNode(` shown`);
962
+ _$insertNode(_el$8, _el$9);
963
+ _$insertNode(_el$8, _el$0);
964
+ _$setProp(_el$8, "wrapMode", "none");
965
+ _$insert(_el$8, () => latest()?.tree?.truncated, _el$0);
966
+ _$effect((_$p) => _$setProp(_el$8, "fg", theme.text.feedback.warning.base, _$p));
967
+ return _el$8;
968
+ }
969
+ }), null);
970
+ _$insert(_el$3, _$createComponent(Show, {
971
+ get when() {
972
+ return latest()?.error;
973
+ },
974
+ get children() {
975
+ var _el$1 = _$createElement("text"), _el$10 = _$createTextNode(`courier RPC failed: `);
976
+ _$insertNode(_el$1, _el$10);
977
+ _$insert(_el$1, () => latest()?.error, null);
978
+ _$effect((_$p) => _$setProp(_el$1, "fg", theme.text.feedback.error.base, _$p));
979
+ return _el$1;
980
+ }
981
+ }), _el$13);
982
+ _$insert(_el$3, _$createComponent(Show, {
983
+ get when() {
984
+ return _$memo(() => !!latest()?.tree)() && !rows().length;
985
+ },
986
+ get children() {
987
+ var _el$11 = _$createElement("text");
988
+ _$insertNode(_el$11, _$createTextNode(`No session started with courier_spawn under this one.`));
989
+ _$effect((_$p) => _$setProp(_el$11, "fg", theme.text.muted, _$p));
990
+ return _el$11;
991
+ }
992
+ }), _el$13);
993
+ _$use((value) => scroll = value, _el$13);
994
+ _$setProp(_el$13, "flexGrow", 1);
995
+ _$insert(_el$13, _$createComponent(For, {
996
+ get each() {
997
+ return rows();
998
+ },
999
+ children: (row, index) => {
1000
+ const chosen = () => index() === Math.min(selected(), rows().length - 1);
1001
+ const indent = " ".repeat(row.depth - 1);
1002
+ return (() => {
1003
+ var _el$16 = _$createElement("box"), _el$17 = _$createElement("text"), _el$18 = _$createTextNode(` `), _el$19 = _$createElement("span");
1004
+ _$insertNode(_el$16, _el$17);
1005
+ _$setProp(_el$16, "flexDirection", "column");
1006
+ _$setProp(_el$16, "flexShrink", 0);
1007
+ _$insertNode(_el$17, _el$18);
1008
+ _$insertNode(_el$17, _el$19);
1009
+ _$setProp(_el$17, "wrapMode", "none");
1010
+ _$insert(_el$17, indent, _el$18);
1011
+ _$insert(_el$17, () => chosen() ? "\u25B8 " : " ", _el$18);
1012
+ _$insert(_el$17, () => row.title, _el$18);
1013
+ _$insert(_el$19, () => statusLine(row));
1014
+ _$insert(_el$16, _$createComponent(For, {
1015
+ get each() {
1016
+ return detailLines(row, rows(), now());
1017
+ },
1018
+ children: (line) => (() => {
1019
+ var _el$20 = _$createElement("text"), _el$21 = _$createTextNode(` `);
1020
+ _$insertNode(_el$20, _el$21);
1021
+ _$setProp(_el$20, "wrapMode", "none");
1022
+ _$insert(_el$20, indent, _el$21);
1023
+ _$insert(_el$20, line, null);
1024
+ _$effect((_$p) => _$setProp(_el$20, "fg", theme.text.muted, _$p));
1025
+ return _el$20;
1026
+ })()
1027
+ }), null);
1028
+ _$effect((_p$) => {
1029
+ var _v$3 = `courier-row-${index()}`, _v$4 = chosen() && input.focused ? theme.background.raised.high : undefined, _v$5 = rowColor(row), _v$6 = {
1030
+ fg: theme.text.muted
1031
+ };
1032
+ _v$3 !== _p$.e && (_p$.e = _$setProp(_el$16, "id", _v$3, _p$.e));
1033
+ _v$4 !== _p$.t && (_p$.t = _$setProp(_el$16, "backgroundColor", _v$4, _p$.t));
1034
+ _v$5 !== _p$.a && (_p$.a = _$setProp(_el$17, "fg", _v$5, _p$.a));
1035
+ _v$6 !== _p$.o && (_p$.o = _$setProp(_el$19, "style", _v$6, _p$.o));
1036
+ return _p$;
1037
+ }, {
1038
+ e: undefined,
1039
+ t: undefined,
1040
+ a: undefined,
1041
+ o: undefined
1042
+ });
1043
+ return _el$16;
1044
+ })();
1045
+ }
1046
+ }));
1047
+ _$insertNode(_el$14, _$createTextNode(`\u2191\u2193 select \xB7 enter open \xB7 s stop \xB7 r refresh \xB7 f fullscreen \xB7 q close`));
1048
+ _$setProp(_el$14, "wrapMode", "none");
1049
+ _$setProp(_el$14, "flexShrink", 0);
1050
+ _$effect((_p$) => {
1051
+ var _v$ = theme.text.base, _v$2 = theme.text.muted;
1052
+ _v$ !== _p$.e && (_p$.e = _$setProp(_el$5, "fg", _v$, _p$.e));
1053
+ _v$2 !== _p$.t && (_p$.t = _$setProp(_el$14, "fg", _v$2, _p$.t));
1054
+ return _p$;
1055
+ }, {
1056
+ e: undefined,
1057
+ t: undefined
1058
+ });
1059
+ return _el$3;
1060
+ })();
1061
+ }
1062
+ var USAGE = "/courier tree opens the session tree; /courier stop [session] stops a session and those under it";
1063
+ function addTreePanel(ctx) {
1064
+ const {
1065
+ courier,
1066
+ location
1067
+ } = courierOf(ctx);
1068
+ const [changed, stopListening] = changes(ctx, PANEL_CHANGES, PANEL_SETTLE_MS);
1069
+ const release = ctx.ui.slot({
1070
+ append: "session.panel",
1071
+ render: (input) => _$createComponent(Show, {
1072
+ get when() {
1073
+ return input.name === PANEL;
1074
+ },
1075
+ get children() {
1076
+ return _$createComponent(TreePanel, {
1077
+ ctx,
1078
+ input,
1079
+ courier,
1080
+ location,
1081
+ changed
1082
+ });
1083
+ }
1084
+ })
1085
+ });
1086
+ const openPanel = () => {
1087
+ if (!ctx.ui.panel.open(PANEL))
1088
+ ctx.ui.toast.show({
1089
+ title: "courier",
1090
+ message: "Open a session first",
1091
+ variant: "warning"
1092
+ });
1093
+ };
1094
+ const stopUnder = async (target) => {
1095
+ const route = ctx.ui.router.current();
1096
+ if (route.type !== "session")
1097
+ return ctx.ui.toast.show({
1098
+ title: "courier",
1099
+ message: "Open a session first",
1100
+ variant: "warning"
1101
+ });
1102
+ const rows = await courier.tree({
1103
+ sessionID: route.sessionID
1104
+ }, {
1105
+ location: location(route.sessionID)
1106
+ }).then((tree) => tree.rows, () => []);
1107
+ const sessionID = target || await ctx.ui.dialog.select({
1108
+ title: "Stop which session?",
1109
+ options: rows.map((row2) => ({
1110
+ title: row2.title,
1111
+ value: row2.sessionID,
1112
+ description: `${row2.sessionID} \xB7 ${statusLine(row2)}`
1113
+ }))
1114
+ });
1115
+ if (!sessionID)
1116
+ return;
1117
+ const row = rows.find((one) => one.sessionID === sessionID);
1118
+ await confirmStop(ctx, courier, location, route.sessionID, {
1119
+ sessionID,
1120
+ title: row?.title ?? sessionID,
1121
+ under: row ? reach(rows, sessionID) - 1 : 0
1122
+ });
1123
+ };
1124
+ ctx.keymap.layer(() => ({
1125
+ mode: "global",
1126
+ commands: [{
1127
+ id: "courier.command",
1128
+ title: "Courier",
1129
+ description: USAGE,
1130
+ group: "Courier",
1131
+ slash: {
1132
+ name: "courier",
1133
+ arguments: true
1134
+ },
1135
+ run: async (input) => {
1136
+ const [verb = "tree", target = ""] = (input ?? "").trim().split(/\s+/).filter(Boolean);
1137
+ if (verb === "tree")
1138
+ return openPanel();
1139
+ if (verb === "stop")
1140
+ return stopUnder(target);
1141
+ ctx.ui.toast.show({
1142
+ title: "courier",
1143
+ message: USAGE,
1144
+ variant: "warning"
1145
+ });
1146
+ }
1147
+ }, {
1148
+ id: "courier.tree",
1149
+ title: "Courier session tree",
1150
+ group: "Courier",
1151
+ palette: true,
1152
+ run: openPanel
1153
+ }]
1154
+ }));
1155
+ return () => {
1156
+ stopListening();
1157
+ release();
1158
+ };
1159
+ }
1160
+ var NOTIFIED_MAX = 500;
1161
+ function addTreeAttention(ctx) {
1162
+ const notified = new Set;
1163
+ const title = (sessionID) => ctx.data.session.get(sessionID)?.title;
1164
+ return ctx.data.on("session.inbox.enqueued", (event) => {
1165
+ const {
1166
+ sessionID,
1167
+ inboxID,
1168
+ item
1169
+ } = event.data;
1170
+ const metadata = item.type === "synthetic" ? item.payload.metadata ?? {} : {};
1171
+ if (metadata.source !== "courier" || notified.has(inboxID))
1172
+ return;
1173
+ const group = typeof metadata.group === "string" ? metadata.group : undefined;
1174
+ const status = typeof metadata.status === "string" ? metadata.status : undefined;
1175
+ const from = typeof metadata.from === "string" ? metadata.from : "";
1176
+ if (!group && !status)
1177
+ return;
1178
+ notified.add(inboxID);
1179
+ if (notified.size > NOTIFIED_MAX)
1180
+ notified.delete(notified.values().next().value);
1181
+ (group ? ctx.attention.notify({
1182
+ title: title(sessionID),
1183
+ message: `Group ${group}: ${typeof metadata.reports === "number" ? counted(metadata.reports, "report") : "its reports"} arrived`,
1184
+ notification: {
1185
+ when: "blurred"
1186
+ },
1187
+ sound: {
1188
+ name: "done",
1189
+ when: "always"
1190
+ }
1191
+ }) : ctx.attention.notify({
1192
+ title: title(sessionID),
1193
+ message: `${title(from) ?? from} reported ${status}`,
1194
+ notification: false,
1195
+ sound: {
1196
+ name: "subagent_done",
1197
+ when: "always"
1198
+ }
1199
+ })).catch(() => {
1200
+ return;
1201
+ });
1202
+ });
1203
+ }
1204
+
1205
+ // src/tui.tsx
1206
+ var tui_default = Plugin.define({
1207
+ id: "courier.tui",
1208
+ setup: (ctx) => {
1209
+ const inbox = addAsksInbox(ctx);
1210
+ const cleanups = [inbox.cleanup, addTreeStatus(ctx, (sessionID) => void inbox.open(sessionID)), addTreePanel(ctx), addTreeAttention(ctx)];
1211
+ return () => {
1212
+ for (const cleanup of cleanups.toReversed())
1213
+ cleanup();
1214
+ };
1215
+ }
1216
+ });
1217
+ export {
1218
+ tui_default as default
1219
+ };