opencode-courier 0.5.0 → 0.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/tui.js CHANGED
@@ -18,7 +18,8 @@ var Permission = Schema.Struct({
18
18
  requestID: Schema.String,
19
19
  action: Schema.String,
20
20
  resources: Schema.Array(Schema.String),
21
- save: Schema.optionalKey(Schema.Array(Schema.String))
21
+ save: Schema.optionalKey(Schema.Array(Schema.String)),
22
+ passesOn: Schema.optionalKey(Schema.Struct({ sessionID: Schema.String, requestID: Schema.String }))
22
23
  });
23
24
  var Prompt = Schema.Struct({
24
25
  question: Schema.String,
@@ -36,8 +37,7 @@ var Asks = Schema.Struct({
36
37
  top: Schema.String,
37
38
  sessions: Schema.Array(Session),
38
39
  permissions: Schema.Array(Permission),
39
- questions: Schema.Array(Question),
40
- truncated: Schema.optionalKey(Count)
40
+ questions: Schema.Array(Question)
41
41
  });
42
42
  var Answered = Schema.Struct({
43
43
  answered: Schema.Boolean,
@@ -66,12 +66,22 @@ var REJECTED = "Refused by the session that started you. Do without it, or repor
66
66
  var QUESTION_FORM = "question";
67
67
  var INBOX_COMMAND = "courier.asks";
68
68
  var SETTLE_MS = 250;
69
+ var topSession = (asks) => ({
70
+ sessionID: asks.top,
71
+ title: "",
72
+ parentID: "",
73
+ depth: 0
74
+ });
69
75
  function asksOf(asks, forms) {
70
- return asks.sessions.flatMap((session) => [...asks.permissions.filter((request) => request.sessionID === session.sessionID).map((request) => ({
76
+ const prompting = new Set(asks.permissions.flatMap((request) => request.sessionID === asks.top && request.passesOn ? [request.passesOn.requestID] : []));
77
+ return [topSession(asks), ...asks.sessions].flatMap((session) => [...asks.permissions.filter((request) => request.sessionID === session.sessionID).map((request) => ({
71
78
  kind: "permission",
72
79
  key: `permission ${request.requestID}`,
73
80
  session,
74
- request
81
+ request,
82
+ ...session.depth > 0 && prompting.has(request.requestID) ? {
83
+ prompted: true
84
+ } : {}
75
85
  })), ...asks.questions.filter((question) => question.sessionID === session.sessionID).map((question) => ({
76
86
  kind: "question",
77
87
  key: `question ${question.requestID}`,
@@ -84,11 +94,13 @@ function asksOf(asks, forms) {
84
94
  form
85
95
  }))]);
86
96
  }
87
- function pathOf(asks, sessionID, topTitle) {
97
+ function pathOf(asks, sessionID, topTitle, currentTitle = () => {
98
+ return;
99
+ }) {
88
100
  const byID = new Map(asks.sessions.map((session) => [session.sessionID, session]));
89
101
  const titles = [];
90
102
  for (let at = byID.get(sessionID);at && titles.length <= byID.size; at = byID.get(at.parentID))
91
- titles.unshift(at.title);
103
+ titles.unshift(currentTitle(at.sessionID) || at.title);
92
104
  return [topTitle, ...titles].join(" \u203A ");
93
105
  }
94
106
  var clip = (text, max = 80) => text.length > max ? `${text.slice(0, max - 1)}\u2026` : text;
@@ -97,9 +109,13 @@ function askTitle(ask) {
97
109
  case "permission": {
98
110
  const {
99
111
  action,
100
- resources
112
+ resources,
113
+ passesOn
101
114
  } = ask.request;
102
- return clip(resources.length ? `Permission: ${action} ${resources.join(", ")}` : `Permission: ${action}`);
115
+ const what = resources.length ? `${action} ${resources.join(", ")}` : action;
116
+ if (ask.session.depth > 0)
117
+ return clip(`Permission: ${what}`);
118
+ return clip(passesOn ? `Permission (top session): courier_answer once for "${what}"` : `Permission (top session): ${what}`);
103
119
  }
104
120
  case "question": {
105
121
  const [first, ...rest] = ask.question.questions;
@@ -112,14 +128,89 @@ function askTitle(ask) {
112
128
  }
113
129
  }
114
130
  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 ");
131
+ const top = ask.session.depth === 0;
132
+ return [top ? "top session" : `depth ${ask.session.depth}`, ask.session.group ? `group ${ask.session.group}` : "", ask.kind === "permission" && ask.prompted ? "the top's prompt asks" : "", ask.kind === "question" && ask.question.stopped ? "cut off, answered by message" : ""].filter(Boolean).join(" \xB7 ");
116
133
  }
117
- var soundOf = (ask) => ask.kind === "permission" ? "permission" : "question";
118
- function shown(field, answers) {
119
- if ("hidden" in field && field.hidden)
120
- return false;
134
+ function active(field, answers) {
121
135
  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);
136
+ return when.every(({
137
+ key,
138
+ op,
139
+ value
140
+ }) => {
141
+ const answer = answers[key];
142
+ if (answer === undefined)
143
+ return false;
144
+ const hit = Array.isArray(answer) ? answer.some((item) => item === value) : answer === value;
145
+ return op === "eq" ? hit : !hit;
146
+ });
147
+ }
148
+ var EMAIL = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
149
+ var isDate = (text) => /^\d{4}-\d{2}-\d{2}$/.test(text) && new Date(`${text}T00:00:00.000Z`).toISOString().slice(0, 10) === text;
150
+ function misfit(field, value) {
151
+ if (field.type === "string") {
152
+ if (typeof value !== "string")
153
+ return "Expected text";
154
+ if (field.required && !value)
155
+ return "Answer required";
156
+ if (field.minLength !== undefined && value.length < field.minLength)
157
+ return `Must be at least ${field.minLength} characters`;
158
+ if (field.maxLength !== undefined && value.length > field.maxLength)
159
+ return `Must be at most ${field.maxLength} characters`;
160
+ if (field.pattern !== undefined) {
161
+ try {
162
+ if (!new RegExp(field.pattern).test(value))
163
+ return `Must match pattern: ${field.pattern}`;
164
+ } catch {
165
+ return `Invalid pattern: ${field.pattern}`;
166
+ }
167
+ }
168
+ if (field.format === "email" && !EMAIL.test(value))
169
+ return "Expected an email address";
170
+ if (field.format === "uri" && !URL.canParse(value))
171
+ return "Expected a URL";
172
+ if (field.format === "date" && !isDate(value))
173
+ return "Expected a date (YYYY-MM-DD)";
174
+ if (field.format === "date-time" && Number.isNaN(new Date(value).getTime()))
175
+ return "Expected a date and time";
176
+ return;
177
+ }
178
+ if (field.type === "number" || field.type === "integer") {
179
+ if (typeof value !== "number" || !Number.isFinite(value))
180
+ return "Expected a number";
181
+ if (field.type === "integer" && !Number.isInteger(value))
182
+ return "Expected an integer";
183
+ if (typeof field.minimum === "number" && value < field.minimum)
184
+ return `Must be at least ${field.minimum}`;
185
+ if (typeof field.maximum === "number" && value > field.maximum)
186
+ return `Must be at most ${field.maximum}`;
187
+ return;
188
+ }
189
+ if (field.type === "multiselect") {
190
+ if (!Array.isArray(value))
191
+ return "Expected selections";
192
+ if (field.required && !value.length)
193
+ return "Select at least one option";
194
+ if (field.minItems !== undefined && value.length < field.minItems)
195
+ return `Select at least ${field.minItems}`;
196
+ if (field.maxItems !== undefined && value.length > field.maxItems)
197
+ return `Select at most ${field.maxItems}`;
198
+ }
199
+ return;
200
+ }
201
+ var numberOf = (limit) => typeof limit === "number" ? limit : undefined;
202
+ function limitsOf(field) {
203
+ const range = (low, high, unit = "") => {
204
+ if (low !== undefined && high !== undefined)
205
+ return `${low}-${high}${unit}`;
206
+ if (low !== undefined)
207
+ return `at least ${low}${unit}`;
208
+ if (high !== undefined)
209
+ return `at most ${high}${unit}`;
210
+ return;
211
+ };
212
+ const parts = field.type === "number" || field.type === "integer" ? [range(numberOf(field.minimum), numberOf(field.maximum))] : field.type === "string" ? [range(field.minLength, field.maxLength, " characters"), field.format, field.pattern ? `/${field.pattern}/` : undefined] : [];
213
+ return parts.filter(Boolean).join(", ") || undefined;
123
214
  }
124
215
  function chooseOptions(choices, picked, {
125
216
  multiple = false,
@@ -163,8 +254,10 @@ function toggle(picked, value) {
163
254
  var OWN = Symbol("own");
164
255
  var DONE = Symbol("done");
165
256
  var SKIP = Symbol("skip");
166
- function addAsksInbox(ctx) {
257
+ var NOTE = Symbol("note");
258
+ function addAsksInbox(ctx, follows) {
167
259
  const person = ctx.client.rpc(personRpc);
260
+ const currentTitle = (sessionID) => ctx.data.session.get(sessionID)?.title;
168
261
  const location = (sessionID) => ctx.data.session.get(sessionID)?.location ?? ctx.location ?? ctx.data.location.default();
169
262
  const openSession = () => {
170
263
  const route = ctx.ui.router.current();
@@ -186,31 +279,46 @@ function addAsksInbox(ctx) {
186
279
  for (const ask of listed)
187
280
  if (!firstSeen.has(ask.key))
188
281
  firstSeen.set(ask.key, seen++);
189
- listed.sort((a, b) => firstSeen.get(a.key) - firstSeen.get(b.key));
282
+ const rank = (ask) => ask.session.depth === 0 ? 0 : 1;
283
+ listed.sort((a, b) => rank(a) - rank(b) || firstSeen.get(a.key) - firstSeen.get(b.key));
190
284
  return {
191
285
  asks,
192
286
  listed
193
287
  };
194
288
  }
289
+ const reason = (error) => {
290
+ if (error instanceof Error)
291
+ return error.message;
292
+ return typeof error === "object" && error && "message" in error ? String(error.message) : String(error);
293
+ };
195
294
  const fail = (what) => (error) => ctx.ui.toast.show({
196
295
  title: "courier",
197
- message: `${what}: ${error instanceof Error ? error.message : String(error)}`,
296
+ message: `${what}: ${reason(error)}`,
198
297
  variant: "error"
199
298
  });
200
299
  async function choose(title, choices, {
201
300
  multiple = false,
202
301
  own = false,
203
- optional = false
302
+ optional = false,
303
+ asks = "",
304
+ start = []
204
305
  } = {}) {
205
- const picked = [];
306
+ const picked = [...start];
206
307
  for (;; ) {
207
308
  const value = await ctx.ui.dialog.select({
208
309
  title,
209
- options: chooseOptions(choices, picked, {
310
+ ...!multiple && start[0] !== undefined ? {
311
+ current: start[0]
312
+ } : {},
313
+ options: [...asks ? [{
314
+ title: asks,
315
+ value: NOTE,
316
+ disabled: true
317
+ }] : [], ...chooseOptions(choices, picked, {
210
318
  multiple,
211
319
  own,
212
320
  optional
213
- })
321
+ })]
214
322
  });
215
323
  if (value === undefined)
216
324
  return;
@@ -313,6 +421,9 @@ function addAsksInbox(ctx) {
313
421
  ...field.description ? {
314
422
  description: field.description
315
423
  } : {},
424
+ ...field.placeholder ? {
425
+ placeholder: field.placeholder
426
+ } : {},
316
427
  ...field.default ? {
317
428
  value: field.default
318
429
  } : {}
@@ -324,11 +435,16 @@ function addAsksInbox(ctx) {
324
435
  }
325
436
  }
326
437
  async function numeric(title, field, optional) {
438
+ const label = field.title ?? field.key;
327
439
  const whole = field.type === "integer";
328
440
  for (;; ) {
329
441
  const text = await typed(title, {
330
442
  ...field.description ? {
331
443
  description: field.description
444
+ } : {},
445
+ ...placeholder(field),
446
+ ...field.default !== undefined ? {
447
+ default: String(field.default)
332
448
  } : {}
333
449
  }, optional);
334
450
  if (text === undefined || text === SKIP)
@@ -338,13 +454,36 @@ function addAsksInbox(ctx) {
338
454
  return value;
339
455
  ctx.ui.toast.show({
340
456
  title: "courier",
341
- message: `${title} takes ${whole ? "a whole number" : "a number"}`,
457
+ message: `${label} takes ${whole ? "a whole number" : "a number"}`,
342
458
  variant: "warning"
343
459
  });
344
460
  }
345
461
  }
346
- async function answerField(field) {
347
- const title = field.title ?? field.key;
462
+ const placeholder = (field) => {
463
+ const limits = limitsOf(field);
464
+ return limits ? {
465
+ placeholder: limits
466
+ } : {};
467
+ };
468
+ async function answerField(form, field) {
469
+ for (;; ) {
470
+ const value = await askField(form, field);
471
+ if (value === undefined || value === SKIP)
472
+ return value;
473
+ const problem = misfit(field, value);
474
+ if (!problem)
475
+ return value;
476
+ ctx.ui.toast.show({
477
+ title: "courier",
478
+ message: `${field.title ?? field.key}: ${problem}`,
479
+ variant: "warning"
480
+ });
481
+ }
482
+ }
483
+ async function askField(form, field) {
484
+ const label = field.title ?? field.key;
485
+ const title = form.title ? `${form.title} \xB7 ${label}` : label;
486
+ const asks = field.description ?? "";
348
487
  const optional = !(("required" in field) && field.required);
349
488
  switch (field.type) {
350
489
  case "external":
@@ -356,7 +495,14 @@ function addAsksInbox(ctx) {
356
495
  case "boolean":
357
496
  return ctx.ui.dialog.select({
358
497
  title,
359
- options: [{
498
+ ...field.default !== undefined ? {
499
+ current: field.default
500
+ } : {},
501
+ options: [...asks ? [{
502
+ title: asks,
503
+ value: NOTE,
504
+ disabled: true
505
+ }] : [], {
360
506
  title: "Yes",
361
507
  value: true
362
508
  }, {
@@ -367,7 +513,9 @@ function addAsksInbox(ctx) {
367
513
  case "multiselect": {
368
514
  const picked = await choose(title, field.options, {
369
515
  multiple: true,
370
- own: !!field.custom
516
+ own: !!field.custom,
517
+ asks,
518
+ start: field.default ?? []
371
519
  });
372
520
  if (picked?.length === 0 && optional)
373
521
  return SKIP;
@@ -375,10 +523,15 @@ function addAsksInbox(ctx) {
375
523
  }
376
524
  case "string": {
377
525
  if (!field.options?.length)
378
- return typed(title, field, optional);
526
+ return typed(title, {
527
+ ...field,
528
+ ...placeholder(field)
529
+ }, optional);
379
530
  const picked = await choose(title, field.options, {
380
531
  own: !!field.custom,
381
- optional
532
+ optional,
533
+ asks,
534
+ start: field.default === undefined ? [] : [field.default]
382
535
  });
383
536
  return picked && (picked[0] ?? SKIP);
384
537
  }
@@ -393,19 +546,36 @@ function addAsksInbox(ctx) {
393
546
  } = ask;
394
547
  const answer2 = {};
395
548
  for (const field of form.fields) {
396
- if (!shown(field, answer2))
549
+ if (!active(field, answer2))
397
550
  continue;
398
- const value = await answerField(field);
551
+ if ("hidden" in field && field.hidden) {
552
+ if ("default" in field && field.default !== undefined)
553
+ answer2[field.key] = field.default;
554
+ continue;
555
+ }
556
+ const value = await answerField(form, field);
399
557
  if (value === undefined)
400
558
  return false;
401
559
  if (value !== SKIP)
402
560
  answer2[field.key] = value;
403
561
  }
404
- await ctx.data.session.form.reply({
405
- sessionID: form.sessionID,
406
- formID: form.id,
407
- answer: answer2
408
- });
562
+ try {
563
+ await ctx.client.session.form.reply({
564
+ sessionID: form.sessionID,
565
+ formID: form.id,
566
+ answer: answer2
567
+ });
568
+ } catch (error) {
569
+ const tag = error?._tag;
570
+ if (tag !== "FormNotFoundError" && tag !== "FormAlreadySettledError")
571
+ throw error;
572
+ ctx.ui.toast.show({
573
+ title: "courier",
574
+ message: "It no longer waits: answered or settled some other way.",
575
+ variant: "warning"
576
+ });
577
+ return false;
578
+ }
409
579
  return true;
410
580
  }
411
581
  const answer = (ask) => {
@@ -418,6 +588,68 @@ function addAsksInbox(ctx) {
418
588
  return answerForm(ask);
419
589
  }
420
590
  };
591
+ let listing;
592
+ let following;
593
+ const keysOf = (listed) => listed.map((ask) => ask.key).join(`
594
+ `);
595
+ let reads = 0;
596
+ const follow = async () => {
597
+ const sessionID = listing?.sessionID;
598
+ if (!sessionID)
599
+ return;
600
+ const ticket = ++reads;
601
+ const found = await read(sessionID);
602
+ const shown = listing;
603
+ if (ticket === reads && shown?.sessionID === sessionID && keysOf(found.listed) !== shown.keys)
604
+ shown.replace(found);
605
+ };
606
+ async function showList(sessionID, {
607
+ asks,
608
+ listed
609
+ }) {
610
+ let replace = () => {
611
+ return;
612
+ };
613
+ const newer = new Promise((resolve) => {
614
+ replace = resolve;
615
+ });
616
+ listing = {
617
+ sessionID,
618
+ keys: keysOf(listed),
619
+ replace
620
+ };
621
+ const top = ctx.data.session.get(asks.top)?.title ?? asks.top;
622
+ const choosing = ctx.ui.dialog.select({
623
+ title: `Waiting on you (${listed.length})`,
624
+ placeholder: "Search",
625
+ options: listed.map((ask) => ({
626
+ title: askTitle(ask),
627
+ value: ask,
628
+ description: pathOf(asks, ask.session.sessionID, top, currentTitle),
629
+ footer: askFooter(ask)
630
+ }))
631
+ });
632
+ ctx.ui.dialog.set({
633
+ size: "large"
634
+ });
635
+ try {
636
+ return await Promise.race([choosing.then((chosen) => ({
637
+ chosen
638
+ })), newer.then((read2) => ({
639
+ read: read2
640
+ }))]);
641
+ } finally {
642
+ listing = undefined;
643
+ }
644
+ }
645
+ async function answerChosen(chosen) {
646
+ const done = await answer(chosen).catch((error) => {
647
+ fail("The answer was not passed on")(error);
648
+ return false;
649
+ });
650
+ if (done)
651
+ answered.add(chosen.key);
652
+ }
421
653
  let opened = false;
422
654
  async function open(sessionID = openSession()) {
423
655
  if (!sessionID) {
@@ -432,83 +664,95 @@ function addAsksInbox(ctx) {
432
664
  return;
433
665
  opened = true;
434
666
  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;
667
+ let found = await read(sessionID).catch(fail("The asks could not be read"));
668
+ let first = true;
669
+ let shown = false;
670
+ while (found?.listed.length) {
671
+ first = false;
672
+ const next = await showList(sessionID, found);
673
+ shown = "read" in next;
674
+ if ("read" in next) {
675
+ found = next.read;
676
+ continue;
451
677
  }
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)
678
+ if (!next.chosen)
468
679
  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);
680
+ await answerChosen(next.chosen);
681
+ found = await read(sessionID).catch(fail("The asks could not be read"));
475
682
  }
683
+ if (!found)
684
+ return;
685
+ if (first)
686
+ ctx.ui.toast.show({
687
+ title: "courier",
688
+ message: "Nothing in this courier tree waits on you.",
689
+ variant: "info"
690
+ });
691
+ if (shown)
692
+ ctx.ui.dialog.clear();
476
693
  } finally {
477
694
  opened = false;
695
+ clearTimeout(following);
696
+ following = undefined;
478
697
  }
479
698
  }
480
699
  const sounded = new Set;
700
+ const readTops = new Set;
701
+ let named = new Set;
481
702
  let settling;
482
703
  const sound = async () => {
483
704
  const sessionID = openSession();
484
- if (!sessionID)
705
+ if (!sessionID) {
706
+ named.clear();
485
707
  return;
708
+ }
486
709
  const {
487
710
  asks,
488
711
  listed
489
712
  } = await read(sessionID);
713
+ const eventSessions = named;
714
+ named = new Set;
490
715
  const top = ctx.data.session.get(asks.top)?.title ?? asks.top;
716
+ const first = !readTops.has(asks.top);
717
+ readTops.add(asks.top);
491
718
  for (const ask of listed) {
492
719
  if (sounded.has(ask.key))
493
720
  continue;
494
721
  sounded.add(ask.key);
722
+ if (first && !eventSessions.has(ask.session.sessionID))
723
+ continue;
724
+ if (!ctx.data.session.get(ask.session.sessionID)?.parentID)
725
+ continue;
495
726
  ctx.attention.notify({
496
- title: pathOf(asks, ask.session.sessionID, top),
727
+ title: pathOf(asks, ask.session.sessionID, top, currentTitle),
497
728
  message: askTitle(ask),
498
729
  notification: {
499
730
  when: "blurred"
500
731
  },
501
- sound: {
502
- name: soundOf(ask),
503
- when: "always"
504
- }
732
+ sound: false
505
733
  });
506
734
  }
507
735
  };
736
+ sound().catch(() => {
737
+ return;
738
+ });
508
739
  const unlisten = ctx.data.listen(({
509
740
  details
510
741
  }) => {
511
- if (details.type !== "permission.asked" && details.type !== "form.created" || settling)
742
+ if (listing && follows.has(details.type) && !following)
743
+ following = setTimeout(() => {
744
+ following = undefined;
745
+ follow().catch(() => {
746
+ return;
747
+ });
748
+ }, SETTLE_MS);
749
+ if (details.type !== "permission.asked" && details.type !== "form.created")
750
+ return;
751
+ const data = details.data;
752
+ const eventSession = data?.sessionID ?? data?.form?.sessionID;
753
+ if (eventSession)
754
+ named.add(eventSession);
755
+ if (settling)
512
756
  return;
513
757
  settling = setTimeout(() => {
514
758
  settling = undefined;
@@ -537,6 +781,7 @@ function addAsksInbox(ctx) {
537
781
  open,
538
782
  cleanup: () => {
539
783
  clearTimeout(settling);
784
+ clearTimeout(following);
540
785
  unlisten();
541
786
  }
542
787
  };
@@ -552,7 +797,7 @@ import { createTextNode as _$createTextNode } from "@opentui/solid";
552
797
  import { insertNode as _$insertNode } from "@opentui/solid";
553
798
  import { setProp as _$setProp } from "@opentui/solid";
554
799
  import { createElement as _$createElement } from "@opentui/solid";
555
- import { createEffect, createResource, createRoot, createSignal, For, on, onCleanup, onMount, Show, untrack } from "solid-js";
800
+ import { createEffect, createMemo, createResource, createRoot, createSignal, For, on, onCleanup, onMount, Show, untrack } from "solid-js";
556
801
 
557
802
  // src/tree/rpc.ts
558
803
  import { Rpc as Rpc2 } from "@opencode/plugin/rpc";
@@ -563,12 +808,12 @@ var Summary = Schema2.Struct({
563
808
  running: Count2,
564
809
  waiting: Count2,
565
810
  idle: Count2,
566
- groups: Schema2.Array(Group),
567
- truncated: Schema2.optionalKey(Count2)
811
+ groups: Schema2.Array(Group)
568
812
  });
569
813
  var Wait = Schema2.Union([
570
814
  Schema2.Struct({ type: Schema2.Literal("permission"), action: Schema2.String, resources: Schema2.Array(Schema2.String) }),
571
- Schema2.Struct({ type: Schema2.Literal("question"), question: Schema2.String })
815
+ Schema2.Struct({ type: Schema2.Literal("question"), question: Schema2.String }),
816
+ Schema2.Struct({ type: Schema2.Literal("form"), title: Schema2.String })
572
817
  ]);
573
818
  var Row = Schema2.Struct({
574
819
  sessionID: Schema2.String,
@@ -577,6 +822,7 @@ var Row = Schema2.Struct({
577
822
  depth: Count2,
578
823
  state: Schema2.Literals(["running", "waiting", "idle", "gone"]),
579
824
  outcome: Schema2.optionalKey(Schema2.Literals(["succeeded", "failed", "interrupted"])),
825
+ stopped: Schema2.optionalKey(Schema2.Boolean),
580
826
  report: Schema2.optionalKey(Schema2.Struct({ state: Schema2.Literals(["owed", "reported", "ended"]), status: Schema2.optionalKey(Schema2.String), progressed: Schema2.Boolean })),
581
827
  group: Schema2.optionalKey(Schema2.Struct({ name: Schema2.String, standing: Schema2.String })),
582
828
  isolated: Schema2.Boolean,
@@ -594,6 +840,10 @@ var Stopped = Schema2.Struct({
594
840
  stopped: Schema2.Array(Schema2.Struct({ sessionID: Schema2.String, title: Schema2.String, outcome: Schema2.String })),
595
841
  cancelled: Count2
596
842
  });
843
+ var Forgotten = Schema2.Union([
844
+ Schema2.Struct({ outcome: Schema2.Literals(["forgotten", "removed", "gone"]) }),
845
+ Schema2.Struct({ outcome: Schema2.Literal("kept"), reason: Schema2.String })
846
+ ]);
597
847
  var treeRpc = Rpc2.define({
598
848
  id: "courier",
599
849
  methods: {
@@ -609,6 +859,11 @@ var treeRpc = Rpc2.define({
609
859
  input: Schema2.toStandardSchemaV1(Schema2.Struct({ sessionID: Schema2.String, by: Schema2.String })),
610
860
  output: Schema2.toStandardSchemaV1(Stopped),
611
861
  errors: { refused: Schema2.toStandardSchemaV1(Schema2.Struct({})) }
862
+ },
863
+ cleanup: {
864
+ input: Schema2.toStandardSchemaV1(Schema2.Struct({ sessionID: Schema2.String, by: Schema2.String })),
865
+ output: Schema2.toStandardSchemaV1(Forgotten),
866
+ errors: { refused: Schema2.toStandardSchemaV1(Schema2.Struct({})) }
612
867
  }
613
868
  },
614
869
  events: {}
@@ -621,9 +876,19 @@ var SETTLE_MS2 = 250;
621
876
  var PANEL_SETTLE_MS = 1000;
622
877
  var PANEL = "courier-tree";
623
878
  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 ")}` : "";
879
+ function summaryLine(summary, maxWidth = Infinity) {
880
+ const groups = summary.groups.map((group) => `group ${group.name} ${group.reported}/${group.members}`);
881
+ const parts = [summary.waiting ? `${summary.waiting} waiting on you` : "", summary.running ? `${summary.running} running` : "", ...groups, summary.idle ? `${summary.idle} idle` : ""].filter(Boolean);
882
+ if (!parts.length)
883
+ return "";
884
+ const line = (kept) => {
885
+ const dropped = groups.filter((group) => parts.indexOf(group) >= kept).length;
886
+ return ["courier", parts.slice(0, kept).join(" \xB7 "), dropped ? `\xB7 +${counted(dropped, "group")}` : ""].filter(Boolean).join(" ");
887
+ };
888
+ for (let kept = parts.length;kept > 0; kept--)
889
+ if (line(kept).length <= maxWidth)
890
+ return line(kept);
891
+ return parts[0];
627
892
  }
628
893
  function duration(ms) {
629
894
  const seconds = Math.max(0, Math.floor(ms / 1000));
@@ -649,25 +914,29 @@ function bar({
649
914
  return `[${"\u2588".repeat(filled)}${"\u2591".repeat(width - filled)}] ${reported}/${started} reported`;
650
915
  }
651
916
  function waitLine(wait) {
652
- return wait.type === "permission" ? `asks permission: ${[wait.action, ...wait.resources].join(" ")}` : `asks: ${wait.question}`;
917
+ if (wait.type === "permission")
918
+ return `asks permission: ${[wait.action, ...wait.resources].join(" ")}`;
919
+ return wait.type === "form" ? `shows a form: ${wait.title}` : `asks: ${wait.question}`;
653
920
  }
654
921
  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 ");
922
+ return [stateWord(row), reportWords(row.report, row.stopped), row.group ? `group ${row.group.name} ${row.group.standing}` : "", row.isolated ? "isolated" : ""].filter(Boolean).join(" \xB7 ");
656
923
  }
657
924
  function stateWord(row) {
658
925
  if (row.state === "waiting")
659
926
  return "waiting on you";
927
+ if (row.state === "idle" && row.stopped)
928
+ return "stopped";
660
929
  if (row.state === "idle" && (row.outcome === "failed" || row.outcome === "interrupted"))
661
930
  return row.outcome;
662
931
  return row.state;
663
932
  }
664
- function reportWords(report) {
933
+ function reportWords(report, stopped) {
665
934
  if (!report)
666
935
  return "";
667
936
  if (report.state === "owed")
668
937
  return "owes a report";
669
938
  if (report.state === "ended")
670
- return "ended without a report";
939
+ return stopped ? "" : "ended without a report";
671
940
  return ["reported", report.status].filter(Boolean).join(" ");
672
941
  }
673
942
  var counted = (count, noun) => `${count} ${noun}${count === 1 ? "" : "s"}`;
@@ -733,34 +1002,44 @@ function addTreeStatus(ctx, onWaiting) {
733
1002
  location: location(sessionID)
734
1003
  }).then((summary) => ({
735
1004
  sessionID,
736
- line: summaryLine(summary),
1005
+ summary,
737
1006
  waiting: summary.waiting
738
1007
  }), () => ({
739
1008
  sessionID,
740
- line: "",
1009
+ summary: undefined,
741
1010
  waiting: 0
742
1011
  })));
743
- const shown2 = () => {
1012
+ const shown = () => {
744
1013
  const latest = read.state === "ready" || read.state === "refreshing" ? read.latest : undefined;
745
1014
  return latest && latest.sessionID === input.sessionID ? latest : undefined;
746
1015
  };
747
- const line = () => shown2()?.line ?? "";
1016
+ const [width, setWidth] = createSignal(0);
1017
+ const present = () => shown()?.summary ? summaryLine(shown().summary) : "";
1018
+ const line = () => width() && shown()?.summary ? summaryLine(shown().summary, width() - 1) : "";
748
1019
  const click = () => {
749
- const latest = shown2();
1020
+ const latest = shown();
750
1021
  if (latest?.waiting)
751
1022
  onWaiting?.(latest.sessionID);
752
1023
  };
753
1024
  return _$createComponent(Show, {
754
1025
  get when() {
755
- return line();
1026
+ return present();
756
1027
  },
757
1028
  get children() {
758
- var _el$ = _$createElement("text"), _el$2 = _$createTextNode(` `);
1029
+ var _el$ = _$createElement("box"), _el$2 = _$createElement("text"), _el$3 = _$createTextNode(` `);
759
1030
  _$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));
1031
+ _$setProp(_el$, "flexGrow", 1);
1032
+ _$setProp(_el$, "flexShrink", 1);
1033
+ _$setProp(_el$, "minWidth", 0);
1034
+ _$setProp(_el$, "onSizeChange", function() {
1035
+ if (this.width > 0)
1036
+ setWidth(this.width);
1037
+ });
1038
+ _$insertNode(_el$2, _el$3);
1039
+ _$setProp(_el$2, "wrapMode", "none");
1040
+ _$setProp(_el$2, "onMouseUp", click);
1041
+ _$insert(_el$2, line, null);
1042
+ _$effect((_$p) => _$setProp(_el$2, "fg", ctx.theme.text.muted, _$p));
764
1043
  return _el$;
765
1044
  }
766
1045
  });
@@ -815,7 +1094,52 @@ async function confirmStop(ctx, courier, location, by, target) {
815
1094
  }
816
1095
  return true;
817
1096
  }
1097
+ function forgottenLine(title, result) {
1098
+ const name = `"${oneLine(title)}"`;
1099
+ if (result.outcome === "kept")
1100
+ return `Kept ${name} and its worktree: ${result.reason}`;
1101
+ if (result.outcome === "removed")
1102
+ return `Forgot ${name} and removed its worktree`;
1103
+ return result.outcome === "gone" ? `Forgot ${name}; its worktree was already gone` : `Forgot ${name}`;
1104
+ }
1105
+ async function confirmForget(ctx, courier, location, by, target, done) {
1106
+ const what = target.isolated ? "Its worktree is removed unless it has uncommitted changes or commits found nowhere else." : "It leaves this tree and courier_children.";
1107
+ const confirmed = await ctx.ui.dialog.confirm({
1108
+ title: "Forget session",
1109
+ message: `Forget "${oneLine(target.title)}" (${target.sessionID})? ${what}`,
1110
+ label: {
1111
+ confirm: "Forget",
1112
+ cancel: "Keep"
1113
+ }
1114
+ });
1115
+ if (!confirmed)
1116
+ return false;
1117
+ try {
1118
+ const result = await courier.cleanup({
1119
+ sessionID: target.sessionID,
1120
+ by
1121
+ }, {
1122
+ location: location(by)
1123
+ });
1124
+ ctx.ui.toast.show({
1125
+ title: "courier",
1126
+ message: forgottenLine(target.title, result),
1127
+ variant: result.outcome === "kept" ? "warning" : "success"
1128
+ });
1129
+ if (result.outcome !== "kept")
1130
+ done();
1131
+ } catch (error) {
1132
+ ctx.ui.toast.show({
1133
+ title: "courier forget",
1134
+ message: reason(error),
1135
+ variant: "error"
1136
+ });
1137
+ }
1138
+ return true;
1139
+ }
818
1140
  var FOCUS_PROMPT = "pane.focus.left";
1141
+ var SPLIT_WIDTH = 80;
1142
+ var oneLine = (title) => title.replace(/[\r\n]+/g, " ");
819
1143
  function TreePanel(props) {
820
1144
  const {
821
1145
  ctx,
@@ -855,10 +1179,18 @@ function TreePanel(props) {
855
1179
  tree
856
1180
  });
857
1181
  });
858
- createEffect(() => view.note({
859
- presentation: input.presentation,
860
- focused: input.focused
861
- }));
1182
+ createEffect(() => {
1183
+ const {
1184
+ presentation,
1185
+ focused
1186
+ } = input;
1187
+ view.note(ctx.renderer.width > SPLIT_WIDTH ? {
1188
+ presentation,
1189
+ focused
1190
+ } : {
1191
+ focused
1192
+ });
1193
+ });
862
1194
  onMount(() => {
863
1195
  if (!view.refocus)
864
1196
  return;
@@ -868,23 +1200,39 @@ function TreePanel(props) {
868
1200
  const [now, setNow] = createSignal(Date.now());
869
1201
  const tick = setInterval(() => setNow(Date.now()), TICK_MS);
870
1202
  onCleanup(() => clearInterval(tick));
871
- const [selected, setSelected] = createSignal(0);
1203
+ const [selectedID, setSelectedID] = createSignal();
872
1204
  let placed = false;
873
1205
  createEffect(() => {
874
- const index = placed ? -1 : rows().findIndex((row) => row.sessionID === input.sessionID);
875
- if (index < 0)
1206
+ const found = placed ? undefined : rows().find((row) => row.sessionID === input.sessionID);
1207
+ if (!found)
876
1208
  return;
877
1209
  placed = true;
878
- setSelected(index);
1210
+ setSelectedID(found.sessionID);
879
1211
  });
880
- const current = () => rows()[Math.min(selected(), rows().length - 1)];
1212
+ let at = 0;
1213
+ const selected = createMemo(() => {
1214
+ const id = selectedID();
1215
+ const found = rows().findIndex((row) => row.sessionID === id);
1216
+ const index = found >= 0 ? found : Math.min(at, rows().length - 1);
1217
+ at = Math.max(index, 0);
1218
+ return index;
1219
+ });
1220
+ createEffect(() => {
1221
+ const row = rows()[selected()];
1222
+ if (row && row.sessionID !== selectedID())
1223
+ setSelectedID(row.sessionID);
1224
+ });
1225
+ const current = () => rows()[selected()];
881
1226
  let scroll;
882
1227
  createEffect(() => {
883
- const index = Math.min(selected(), rows().length - 1);
884
- if (index >= 0)
885
- scroll?.scrollChildIntoView(`courier-row-${index}`);
1228
+ if (selected() >= 0)
1229
+ scroll?.scrollChildIntoView(`courier-row-${selected()}`);
886
1230
  });
887
- const move = (by) => setSelected((index) => Math.max(0, Math.min(rows().length - 1, Math.min(index, rows().length - 1) + by)));
1231
+ const move = (by) => {
1232
+ const next = rows()[Math.max(0, Math.min(rows().length - 1, selected() + by))];
1233
+ if (next)
1234
+ setSelectedID(next.sessionID);
1235
+ };
888
1236
  const open = () => {
889
1237
  const row = current();
890
1238
  if (row)
@@ -902,6 +1250,22 @@ function TreePanel(props) {
902
1250
  under: reach(rows(), row.sessionID) - 1
903
1251
  });
904
1252
  };
1253
+ const forgetSelected = () => {
1254
+ const row = current();
1255
+ if (!row)
1256
+ return;
1257
+ if (row.state !== "gone")
1258
+ return ctx.ui.toast.show({
1259
+ title: "courier",
1260
+ message: `x forgets only a gone session; "${oneLine(row.title)}" is ${row.state}.`,
1261
+ variant: "warning"
1262
+ });
1263
+ confirmForget(ctx, courier, location, view.root, {
1264
+ sessionID: row.sessionID,
1265
+ title: row.title,
1266
+ isolated: row.isolated
1267
+ }, () => void refetch());
1268
+ };
905
1269
  ctx.keymap.layer(() => ({
906
1270
  priority: 10,
907
1271
  enabled: () => input.focused,
@@ -935,6 +1299,11 @@ function TreePanel(props) {
935
1299
  title: "Stop session and those under it",
936
1300
  group: "Courier",
937
1301
  run: stopSelected
1302
+ }, {
1303
+ bind: "x",
1304
+ title: "Forget gone session",
1305
+ group: "Courier",
1306
+ run: forgetSelected
938
1307
  }, {
939
1308
  bind: "r",
940
1309
  title: "Refresh",
@@ -959,47 +1328,46 @@ function TreePanel(props) {
959
1328
  return row.state === "gone" ? theme.text.muted : theme.text.base;
960
1329
  };
961
1330
  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);
1331
+ var _el$4 = _$createElement("box"), _el$5 = _$createElement("box"), _el$6 = _$createElement("text"), _el$13 = _$createElement("scrollbox"), _el$14 = _$createElement("text");
970
1332
  _$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, {
1333
+ _$insertNode(_el$4, _el$13);
1334
+ _$insertNode(_el$4, _el$14);
1335
+ _$setProp(_el$4, "flexDirection", "column");
1336
+ _$setProp(_el$4, "flexGrow", 1);
1337
+ _$setProp(_el$4, "paddingLeft", 1);
1338
+ _$setProp(_el$4, "paddingRight", 1);
1339
+ _$insertNode(_el$5, _el$6);
1340
+ _$setProp(_el$5, "flexDirection", "row");
1341
+ _$setProp(_el$5, "flexShrink", 0);
1342
+ _$setProp(_el$5, "gap", 2);
1343
+ _$insertNode(_el$6, _$createTextNode(`courier tree`));
1344
+ _$setProp(_el$6, "wrapMode", "none");
1345
+ _$insert(_el$5, _$createComponent(Show, {
977
1346
  get when() {
978
1347
  return root().started;
979
1348
  },
980
1349
  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;
1350
+ var _el$8 = _$createElement("text");
1351
+ _$setProp(_el$8, "wrapMode", "none");
1352
+ _$insert(_el$8, () => bar(root()));
1353
+ _$effect((_$p) => _$setProp(_el$8, "fg", theme.text.muted, _$p));
1354
+ return _el$8;
986
1355
  }
987
1356
  }), null);
988
- _$insert(_el$4, _$createComponent(Show, {
1357
+ _$insert(_el$5, _$createComponent(Show, {
989
1358
  get when() {
990
1359
  return latest()?.tree?.truncated;
991
1360
  },
992
1361
  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;
1362
+ var _el$9 = _$createElement("text"), _el$0 = _$createTextNode(` older left out`);
1363
+ _$insertNode(_el$9, _el$0);
1364
+ _$setProp(_el$9, "wrapMode", "none");
1365
+ _$insert(_el$9, () => latest()?.tree?.truncated, _el$0);
1366
+ _$effect((_$p) => _$setProp(_el$9, "fg", theme.text.feedback.warning.base, _$p));
1367
+ return _el$9;
1000
1368
  }
1001
1369
  }), null);
1002
- _$insert(_el$3, _$createComponent(Show, {
1370
+ _$insert(_el$4, _$createComponent(Show, {
1003
1371
  get when() {
1004
1372
  return latest()?.error;
1005
1373
  },
@@ -1011,7 +1379,7 @@ function TreePanel(props) {
1011
1379
  return _el$1;
1012
1380
  }
1013
1381
  }), _el$13);
1014
- _$insert(_el$3, _$createComponent(Show, {
1382
+ _$insert(_el$4, _$createComponent(Show, {
1015
1383
  get when() {
1016
1384
  return _$memo(() => !!latest()?.tree)() && !rows().length;
1017
1385
  },
@@ -1029,43 +1397,48 @@ function TreePanel(props) {
1029
1397
  return rows();
1030
1398
  },
1031
1399
  children: (row, index) => {
1032
- const chosen = () => index() === Math.min(selected(), rows().length - 1);
1400
+ const chosen = () => index() === selected();
1033
1401
  const indent = " ".repeat(row.depth - 1);
1034
1402
  return (() => {
1035
- var _el$16 = _$createElement("box"), _el$17 = _$createElement("text"), _el$18 = _$createTextNode(` `), _el$19 = _$createElement("span");
1403
+ var _el$16 = _$createElement("box"), _el$17 = _$createElement("box"), _el$18 = _$createElement("text"), _el$19 = _$createElement("text"), _el$20 = _$createTextNode(` `);
1036
1404
  _$insertNode(_el$16, _el$17);
1037
1405
  _$setProp(_el$16, "flexDirection", "column");
1038
1406
  _$setProp(_el$16, "flexShrink", 0);
1039
1407
  _$insertNode(_el$17, _el$18);
1040
1408
  _$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));
1409
+ _$setProp(_el$17, "flexDirection", "row");
1410
+ _$setProp(_el$17, "flexShrink", 0);
1411
+ _$setProp(_el$18, "wrapMode", "none");
1412
+ _$setProp(_el$18, "flexShrink", 1);
1413
+ _$setProp(_el$18, "minWidth", 0);
1414
+ _$insert(_el$18, indent, null);
1415
+ _$insert(_el$18, () => chosen() ? "\u25B8 " : " ", null);
1416
+ _$insert(_el$18, () => oneLine(row.title), null);
1417
+ _$insert(_el$18, () => row.sessionID === input.sessionID ? " (on screen)" : "", null);
1418
+ _$insertNode(_el$19, _el$20);
1419
+ _$setProp(_el$19, "wrapMode", "none");
1420
+ _$setProp(_el$19, "flexShrink", 0);
1421
+ _$insert(_el$19, () => statusLine(row), null);
1047
1422
  _$insert(_el$16, _$createComponent(For, {
1048
1423
  get each() {
1049
1424
  return detailLines(row, rows(), now());
1050
1425
  },
1051
1426
  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;
1427
+ var _el$21 = _$createElement("text"), _el$22 = _$createTextNode(` `);
1428
+ _$insertNode(_el$21, _el$22);
1429
+ _$setProp(_el$21, "wrapMode", "none");
1430
+ _$insert(_el$21, indent, _el$22);
1431
+ _$insert(_el$21, line, null);
1432
+ _$effect((_$p) => _$setProp(_el$21, "fg", theme.text.muted, _$p));
1433
+ return _el$21;
1059
1434
  })()
1060
1435
  }), null);
1061
1436
  _$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
- };
1437
+ var _v$3 = `courier-row-${index()}`, _v$4 = chosen() && input.focused ? theme.background.raised.high : undefined, _v$5 = rowColor(row), _v$6 = theme.text.muted;
1065
1438
  _v$3 !== _p$.e && (_p$.e = _$setProp(_el$16, "id", _v$3, _p$.e));
1066
1439
  _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));
1440
+ _v$5 !== _p$.a && (_p$.a = _$setProp(_el$18, "fg", _v$5, _p$.a));
1441
+ _v$6 !== _p$.o && (_p$.o = _$setProp(_el$19, "fg", _v$6, _p$.o));
1069
1442
  return _p$;
1070
1443
  }, {
1071
1444
  e: undefined,
@@ -1077,19 +1450,19 @@ function TreePanel(props) {
1077
1450
  })();
1078
1451
  }
1079
1452
  }));
1080
- _$insertNode(_el$14, _$createTextNode(`\u2191\u2193 select \xB7 enter open \xB7 s stop \xB7 r refresh \xB7 f fullscreen \xB7 q close`));
1453
+ _$insertNode(_el$14, _$createTextNode(`\u2191\u2193 select \xB7 enter open \xB7 s stop \xB7 x forget \xB7 r refresh \xB7 f fullscreen \xB7 q close`));
1081
1454
  _$setProp(_el$14, "wrapMode", "none");
1082
1455
  _$setProp(_el$14, "flexShrink", 0);
1083
1456
  _$effect((_p$) => {
1084
1457
  var _v$ = theme.text.base, _v$2 = theme.text.muted;
1085
- _v$ !== _p$.e && (_p$.e = _$setProp(_el$5, "fg", _v$, _p$.e));
1458
+ _v$ !== _p$.e && (_p$.e = _$setProp(_el$6, "fg", _v$, _p$.e));
1086
1459
  _v$2 !== _p$.t && (_p$.t = _$setProp(_el$14, "fg", _v$2, _p$.t));
1087
1460
  return _p$;
1088
1461
  }, {
1089
1462
  e: undefined,
1090
1463
  t: undefined
1091
1464
  });
1092
- return _el$3;
1465
+ return _el$4;
1093
1466
  })();
1094
1467
  }
1095
1468
  var USAGE = "/courier tree opens the session tree; /courier stop [session] stops a session and those under it";
@@ -1187,11 +1560,11 @@ function addTreePanel(ctx) {
1187
1560
  createEffect(() => {
1188
1561
  const route = ctx.ui.router.current();
1189
1562
  const sessionID = route.type === "session" ? route.sessionID : undefined;
1190
- const shown2 = ctx.ui.panel.current();
1563
+ const shown = ctx.ui.panel.current();
1191
1564
  untrack(() => {
1192
- if (!following || shown2 && shown2.sessionID === sessionID || !following.at && following.to === sessionID)
1565
+ if (!following || shown && shown.sessionID === sessionID || !following.at && following.to === sessionID)
1193
1566
  return;
1194
- if (!shown2 && sessionID && following.at === sessionID)
1567
+ if (!shown && sessionID && following.at === sessionID)
1195
1568
  following = undefined;
1196
1569
  else if (!sessionID)
1197
1570
  hide(following);
@@ -1237,11 +1610,29 @@ function addTreePanel(ctx) {
1237
1610
  message: "Open a session first",
1238
1611
  variant: "warning"
1239
1612
  });
1240
- const rows = await courier.tree({
1613
+ const read = await courier.tree({
1241
1614
  sessionID: route.sessionID
1242
1615
  }, {
1243
1616
  location: location(route.sessionID)
1244
- }).then((tree) => tree.rows, () => []);
1617
+ }).then((tree) => ({
1618
+ rows: tree.rows
1619
+ }), (error) => ({
1620
+ rows: [],
1621
+ error
1622
+ }));
1623
+ const rows = read.rows;
1624
+ if (!target && "error" in read)
1625
+ return ctx.ui.toast.show({
1626
+ title: "courier stop",
1627
+ message: reason(read.error),
1628
+ variant: "error"
1629
+ });
1630
+ if (!target && !rows.length)
1631
+ return ctx.ui.toast.show({
1632
+ title: "courier stop",
1633
+ message: "No session started with courier_spawn under this one.",
1634
+ variant: "warning"
1635
+ });
1245
1636
  const sessionID = target || await ctx.ui.dialog.select({
1246
1637
  title: "Stop which session?",
1247
1638
  options: rows.map((row2) => ({
@@ -1314,6 +1705,8 @@ function addTreeAttention(ctx) {
1314
1705
  const from = typeof metadata.from === "string" ? metadata.from : "";
1315
1706
  if (!group && !status)
1316
1707
  return;
1708
+ if (!group && ctx.data.session.get(from)?.parentID !== undefined)
1709
+ return;
1317
1710
  notified.add(inboxID);
1318
1711
  if (notified.size > NOTIFIED_MAX)
1319
1712
  notified.delete(notified.values().next().value);
@@ -1345,7 +1738,7 @@ function addTreeAttention(ctx) {
1345
1738
  var tui_default = Plugin.define({
1346
1739
  id: "courier.tui",
1347
1740
  setup: (ctx) => {
1348
- const inbox = addAsksInbox(ctx);
1741
+ const inbox = addAsksInbox(ctx, CHANGES);
1349
1742
  const cleanups = [inbox.cleanup, addTreeStatus(ctx, (sessionID) => void inbox.open(sessionID)), addTreePanel(ctx), addTreeAttention(ctx)];
1350
1743
  return () => {
1351
1744
  for (const cleanup of cleanups.toReversed())