opencode-courier 0.7.3 → 0.8.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 (52) hide show
  1. package/README.md +109 -135
  2. package/dist/index.js +23 -4
  3. package/dist/memory/domain.d.ts +119 -0
  4. package/dist/memory/domain.js +84 -0
  5. package/dist/memory/events.d.ts +26 -0
  6. package/dist/memory/events.js +46 -0
  7. package/dist/memory/forget.d.ts +10 -0
  8. package/dist/memory/forget.js +20 -0
  9. package/dist/memory/index.d.ts +10 -0
  10. package/dist/memory/index.js +11 -0
  11. package/dist/memory/note.d.ts +37 -0
  12. package/dist/memory/note.js +29 -0
  13. package/dist/memory/ports.d.ts +44 -0
  14. package/dist/memory/ports.js +1 -0
  15. package/dist/memory/recall.d.ts +27 -0
  16. package/dist/memory/recall.js +48 -0
  17. package/dist/memory/record.d.ts +7 -0
  18. package/dist/memory/record.js +92 -0
  19. package/dist/memory/rpc-handlers.d.ts +134 -0
  20. package/dist/memory/rpc-handlers.js +23 -0
  21. package/dist/memory/rpc.d.ts +227 -0
  22. package/dist/memory/rpc.js +60 -0
  23. package/dist/memory/store.d.ts +10 -0
  24. package/dist/memory/store.js +90 -0
  25. package/dist/memory/sweep.d.ts +4 -0
  26. package/dist/memory/sweep.js +12 -0
  27. package/dist/memory/text.d.ts +27 -0
  28. package/dist/memory/text.js +95 -0
  29. package/dist/memory/tools.d.ts +16 -0
  30. package/dist/memory/tools.js +78 -0
  31. package/dist/memory/tui.d.ts +73 -0
  32. package/dist/person/rpc.d.ts +4 -4
  33. package/dist/platform/json.d.ts +1 -0
  34. package/dist/platform/json.js +1 -0
  35. package/dist/rpc.d.ts +1 -0
  36. package/dist/rpc.js +1 -0
  37. package/dist/skill.js +10 -3
  38. package/dist/tree/context.d.ts +2 -2
  39. package/dist/tree/context.js +4 -3
  40. package/dist/tree/index.d.ts +6 -5
  41. package/dist/tree/index.js +5 -4
  42. package/dist/tree/ports.d.ts +4 -2
  43. package/dist/tree/roster.d.ts +17 -0
  44. package/dist/tree/roster.js +24 -0
  45. package/dist/tree/rpc-handlers.d.ts +18 -18
  46. package/dist/tree/rpc.d.ts +18 -18
  47. package/dist/tree/status.d.ts +5 -5
  48. package/dist/tree/subtree.d.ts +2 -2
  49. package/dist/tree/text.js +2 -1
  50. package/dist/tree/tui.d.ts +11 -3
  51. package/dist/tui.js +544 -121
  52. package/package.json +1 -1
package/dist/tui.js CHANGED
@@ -2,58 +2,471 @@
2
2
  // src/tui.tsx
3
3
  import { Plugin } from "@opencode/plugin/tui";
4
4
 
5
- // src/person/rpc.ts
5
+ // src/memory/rpc.ts
6
6
  import { Rpc } from "@opencode/plugin/rpc";
7
7
  import { Schema } from "effect";
8
8
  var Count = Schema.Int.check(Schema.isGreaterThanOrEqualTo(0));
9
- var Session = Schema.Struct({
9
+ var PART_STATUSES = ["done", "partial", "blocked", "failed", "interrupted", "stopped", "stalled", "out"];
10
+ var Part = Schema.Struct({
10
11
  sessionID: Schema.String,
11
12
  title: Schema.String,
12
- parentID: Schema.String,
13
13
  depth: Count,
14
- group: Schema.optionalKey(Schema.String)
14
+ isolated: Schema.Boolean,
15
+ group: Schema.optionalKey(Schema.String),
16
+ status: Schema.Literals(PART_STATUSES),
17
+ duration: Schema.optionalKey(Schema.Number),
18
+ cost: Schema.optionalKey(Schema.Number),
19
+ context: Schema.optionalKey(Schema.Number),
20
+ refused: Schema.optionalKey(Count)
15
21
  });
16
- var Permission = Schema.Struct({
22
+ var Lesson = Schema.Struct({ text: Schema.String, at: Schema.Number, depth: Schema.optionalKey(Count) });
23
+ var Record = Schema.Struct({
17
24
  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
- passesOn: Schema.optionalKey(Schema.Struct({ sessionID: Schema.String, requestID: Schema.String }))
25
+ startedAt: Schema.Number,
26
+ movedAt: Schema.Number,
27
+ title: Schema.String,
28
+ split: Schema.String,
29
+ finished: Schema.Boolean,
30
+ recalled: Schema.optionalKey(Schema.Struct({ at: Schema.optionalKey(Schema.Number), calls: Count })),
31
+ lesson: Schema.optionalKey(Lesson),
32
+ parts: Schema.Array(Part)
23
33
  });
24
- var Prompt = Schema.Struct({
25
- question: Schema.String,
26
- header: Schema.String,
27
- options: Schema.Array(Schema.Struct({ label: Schema.String, description: Schema.String })),
28
- multiple: Schema.optionalKey(Schema.Boolean)
34
+ var Records = Schema.Struct({
35
+ memory: Schema.Boolean,
36
+ projectID: Schema.String,
37
+ records: Schema.Array(Record)
29
38
  });
30
- var Question = Schema.Struct({
31
- sessionID: Schema.String,
32
- requestID: Schema.String,
33
- questions: Schema.Array(Prompt),
34
- stopped: Schema.optionalKey(Schema.Boolean)
39
+ var Forgotten = Schema.Struct({ outcome: Schema.Literals(["record", "lesson", "none"]) });
40
+ var memoryRpc = Rpc.define({
41
+ id: "courier-memory",
42
+ methods: {
43
+ records: {
44
+ input: Schema.toStandardSchemaV1(Schema.Struct({})),
45
+ output: Schema.toStandardSchemaV1(Records)
46
+ },
47
+ forget: {
48
+ input: Schema.toStandardSchemaV1(Schema.Struct({ sessionID: Schema.String, lesson: Schema.optionalKey(Schema.Boolean) })),
49
+ output: Schema.toStandardSchemaV1(Forgotten)
50
+ }
51
+ },
52
+ events: {}
53
+ });
54
+
55
+ // src/kernel/protocol.ts
56
+ var STATUSES = ["done", "partial", "blocked", "failed"];
57
+
58
+ // src/memory/domain.ts
59
+ var RETENTION_MS = 90 * 24 * 60 * 60000;
60
+ var PART_STATUSES2 = [...STATUSES, "interrupted", "stopped", "stalled", "out"];
61
+
62
+ // src/memory/text.ts
63
+ function duration(ms) {
64
+ const minutes = Math.round(ms / 60000);
65
+ if (minutes < 1)
66
+ return `${Math.max(0, Math.round(ms / 1000))} s`;
67
+ if (minutes < 60)
68
+ return `${minutes} min`;
69
+ return `${Math.floor(minutes / 60)} h ${minutes % 60} min`;
70
+ }
71
+ var ONE_LINE = "what the next orchestrator in this project should know about running a tree like this one, with the job it comes from and the reason";
72
+ var NOTE_EMPTY = `Not noted: text is empty. Write one line, ${ONE_LINE}, or nothing.`;
73
+
74
+ // src/memory/tui.tsx
75
+ var MEMORY_COMMAND = "courier.memory";
76
+ var MEMORY_USAGE = "/courier memory lists this project's tree records, to read or forget";
77
+ var MEMORY_OFF = "Memory is off in this project: the plugin's memory option is false, so no record of a session tree is kept or listed here.";
78
+ var NO_RECORDS = "No record of a session tree in this project: one is written as each turn in a tree ends, and kept 90 days.";
79
+ var LESSON_LINE_MAX = 160;
80
+ var ROW_TITLE_MAX = 46;
81
+ var TODAY = "Today";
82
+ var LESSON_ROW = 90;
83
+ var pad = (n) => String(n).padStart(2, "0");
84
+ var localDay = (at) => new Date(at).toDateString();
85
+ var day = (at) => {
86
+ const date = new Date(at);
87
+ return `${date.getFullYear()}-${pad(date.getMonth() + 1)}-${pad(date.getDate())}`;
88
+ };
89
+ var zone = (date) => new Intl.DateTimeFormat("en-US", {
90
+ timeZoneName: "short"
91
+ }).formatToParts(date).find((part) => part.type === "timeZoneName")?.value ?? "local";
92
+ var minute = (at) => {
93
+ const date = new Date(at);
94
+ return `${day(at)} ${pad(date.getHours())}:${pad(date.getMinutes())} ${zone(date)}`;
95
+ };
96
+ var oneLine2 = (text) => text.replace(/\s+/g, " ").trim();
97
+ var clip = (text, max) => text.length > max ? `${text.slice(0, max - 1)}\u2026` : text;
98
+ var count = (n, noun) => `${n} ${noun}${n === 1 ? "" : "s"}`;
99
+ var money = (cost) => `$${cost.toFixed(2)}`;
100
+ var nameOf = (record) => clip(oneLine2(record.title) || record.sessionID, 60);
101
+ function partsCounted(parts) {
102
+ if (!parts.length)
103
+ return "no parts";
104
+ const statuses = PART_STATUSES.flatMap((status) => {
105
+ const n = parts.filter((part) => part.status === status).length;
106
+ return n ? [`${n} ${status}`] : [];
107
+ }).join(", ");
108
+ return `${count(parts.length, "part")}: ${statuses}`;
109
+ }
110
+ function costOf(record) {
111
+ return record.parts.some((part) => part.cost !== undefined) ? record.parts.reduce((sum, part) => sum + (part.cost ?? 0), 0) : undefined;
112
+ }
113
+ function recordTitle(record, open) {
114
+ return `${clip(oneLine2(record.title) || record.sessionID, ROW_TITLE_MAX)}${record.sessionID === open ? " (this session)" : ""}`;
115
+ }
116
+ function recordCategory(record, now = Date.now()) {
117
+ const moved = localDay(record.movedAt);
118
+ return moved === localDay(now) ? TODAY : moved;
119
+ }
120
+ function lessonHeading(lesson) {
121
+ const from = lesson.depth === undefined ? "" : `, from depth ${lesson.depth}`;
122
+ return `lesson (${day(lesson.at)}${from})`;
123
+ }
124
+ function recordDescription(record) {
125
+ const cost = costOf(record);
126
+ return [partsCounted(record.parts), duration(record.movedAt - record.startedAt), cost === undefined ? "" : money(cost), record.finished ? "" : "not finished"].filter(Boolean).join(" \xB7 ");
127
+ }
128
+ function recordDetails(record) {
129
+ return record.lesson ? [clip(`${lessonHeading(record.lesson)}: "${oneLine2(record.lesson.text)}"`, LESSON_LINE_MAX)] : [];
130
+ }
131
+ function partLine(part) {
132
+ return [part.status, `depth ${part.depth}`, part.isolated ? "isolated" : "", part.group ? `group ${part.group}` : "", part.duration === undefined ? "" : duration(part.duration), part.cost === undefined ? "" : money(part.cost), part.context === undefined ? "" : `ctx ${Math.round(part.context * 100)}%`, part.refused ? `${count(part.refused, "spawn")} refused` : ""].filter(Boolean).join(" \xB7 ");
133
+ }
134
+ function wrap(text, width) {
135
+ const rows = [];
136
+ let row = "";
137
+ for (const word of oneLine2(text).split(" ").filter(Boolean)) {
138
+ if (row && row.length + 1 + word.length <= width) {
139
+ row = `${row} ${word}`;
140
+ continue;
141
+ }
142
+ if (row)
143
+ rows.push(row);
144
+ row = word;
145
+ while (row.length > width) {
146
+ rows.push(row.slice(0, width));
147
+ row = row.slice(width);
148
+ }
149
+ }
150
+ if (row)
151
+ rows.push(row);
152
+ return rows;
153
+ }
154
+ var lessonLine = (record) => record.lesson ? {
155
+ kind: "lesson",
156
+ row: 0
157
+ } : {
158
+ kind: "fact",
159
+ key: "lesson"
160
+ };
161
+ function nearby(sessionIDs, sessionID, forgotten) {
162
+ const at = sessionIDs.indexOf(sessionID);
163
+ if (at < 0)
164
+ return forgotten ? [] : [sessionID];
165
+ return [...forgotten ? [] : [sessionID], ...sessionIDs.slice(at + 1), ...sessionIDs.slice(0, at).reverse()];
166
+ }
167
+ function detailOptions(record) {
168
+ const fact = (key, title, description) => ({
169
+ title,
170
+ value: {
171
+ kind: "fact",
172
+ key
173
+ },
174
+ category: "Tree",
175
+ ...description ? {
176
+ description
177
+ } : {}
178
+ });
179
+ const cost = costOf(record);
180
+ const recalled = record.recalled ? fact("recalled", `Recalled ${count(record.recalled.calls, "time")}`, record.recalled.at === undefined ? "by a part only" : `first by the top ${minute(record.recalled.at)}`) : fact("recalled", "Not recalled");
181
+ const lesson = record.lesson ? [{
182
+ title: lessonHeading(record.lesson),
183
+ value: {
184
+ kind: "lesson",
185
+ row: 0
186
+ },
187
+ category: "Lesson"
188
+ }, ...wrap(record.lesson.text, LESSON_ROW).map((row, at) => ({
189
+ title: row,
190
+ value: {
191
+ kind: "lesson",
192
+ row: at + 1
193
+ },
194
+ category: "Lesson"
195
+ }))] : [{
196
+ title: "No lesson",
197
+ value: {
198
+ kind: "fact",
199
+ key: "lesson"
200
+ },
201
+ category: "Lesson",
202
+ description: "courier_note writes one line per tree"
203
+ }];
204
+ return [fact("top", `Top session ${record.sessionID}`), fact("started", `Started ${minute(record.startedAt)}`, `last moved ${minute(record.movedAt)}, ran ${duration(record.movedAt - record.startedAt)}`), fact("split", clip(oneLine2(record.split), 160)), fact("finished", record.finished ? "Finished" : "Not finished", cost === undefined ? undefined : `${money(cost)} in all`), recalled, ...record.parts.length ? record.parts.map((part) => ({
205
+ title: `${" ".repeat(Math.max(0, part.depth - 1))}${clip(oneLine2(part.title) || part.sessionID, 60)}`,
206
+ value: {
207
+ kind: "part",
208
+ sessionID: part.sessionID
209
+ },
210
+ category: "Parts",
211
+ description: partLine(part)
212
+ })) : [{
213
+ title: "No parts",
214
+ value: {
215
+ kind: "fact",
216
+ key: "parts"
217
+ },
218
+ category: "Parts",
219
+ description: "no session was started in this tree"
220
+ }], ...lesson];
221
+ }
222
+ function forgetMessage(record, lesson) {
223
+ const name = `"${nameOf(record)}" (${day(record.movedAt)}, ${partsCounted(record.parts)})`;
224
+ if (lesson)
225
+ return `Forget the lesson of ${name}? The record keeps its facts; courier_recall no longer lists the lesson: "${clip(oneLine2(record.lesson?.text ?? ""), 120)}".`;
226
+ return `Forget the record of ${name}, top session ${record.sessionID}? It leaves this project's memory and courier_recall${record.lesson ? ", its lesson with it" : ""}; the sessions themselves are untouched.`;
227
+ }
228
+ function forgottenLine(record, outcome) {
229
+ const name = `"${nameOf(record)}"`;
230
+ if (outcome === "record")
231
+ return `Forgot the record of ${name}`;
232
+ if (outcome === "lesson")
233
+ return `Forgot the lesson of ${name}; its facts stay`;
234
+ return `Nothing to forget for ${name}: it was already gone`;
235
+ }
236
+ function addMemoryView(ctx) {
237
+ const memory = ctx.client.rpc(memoryRpc);
238
+ const openSession = () => {
239
+ const route = ctx.ui.router.current();
240
+ return route.type === "session" ? route.sessionID : undefined;
241
+ };
242
+ const where = () => {
243
+ const sessionID = openSession();
244
+ return (sessionID ? ctx.data.session.get(sessionID)?.location : undefined) ?? ctx.location ?? ctx.data.location.default();
245
+ };
246
+ const reason = (error) => {
247
+ if (error instanceof Error)
248
+ return error.message;
249
+ return typeof error === "object" && error && "message" in error ? String(error.message) : String(error);
250
+ };
251
+ const fail = (what) => (error) => ctx.ui.toast.show({
252
+ title: "courier",
253
+ message: `${what}: ${reason(error)}`,
254
+ variant: "error"
255
+ });
256
+ const say = (message) => ctx.ui.toast.show({
257
+ title: "courier",
258
+ message,
259
+ variant: "info"
260
+ });
261
+ function show(options, chosen, forget) {
262
+ let request = () => {
263
+ return;
264
+ };
265
+ const requested = new Promise((resolve) => {
266
+ request = resolve;
267
+ });
268
+ const choosing = ctx.ui.dialog.select({
269
+ ...options,
270
+ actions: [{
271
+ bind: "x",
272
+ title: "forget",
273
+ onTrigger: (value) => request(forget(value))
274
+ }]
275
+ });
276
+ ctx.ui.dialog.set({
277
+ size: "xlarge"
278
+ });
279
+ return Promise.race([choosing.then((value) => value === undefined ? {
280
+ kind: "back"
281
+ } : chosen(value)), requested]);
282
+ }
283
+ const showList = (read, current) => {
284
+ const open2 = openSession();
285
+ const now = Date.now();
286
+ return show({
287
+ title: `Memory (${read.records.length})`,
288
+ placeholder: "Search",
289
+ ...current ? {
290
+ current
291
+ } : {},
292
+ options: read.records.map((record) => ({
293
+ title: recordTitle(record, open2),
294
+ value: record,
295
+ category: recordCategory(record, now),
296
+ description: recordDescription(record),
297
+ details: recordDetails(record)
298
+ }))
299
+ }, (record) => ({
300
+ kind: "open",
301
+ record
302
+ }), (record) => ({
303
+ kind: "forget",
304
+ record,
305
+ lesson: false
306
+ }));
307
+ };
308
+ const showRecord = (record, current) => show({
309
+ title: `${localDay(record.movedAt)} ${nameOf(record)}`,
310
+ placeholder: "Search",
311
+ options: detailOptions(record),
312
+ ...current ? {
313
+ current
314
+ } : {}
315
+ }, () => ({
316
+ kind: "back"
317
+ }), (line) => ({
318
+ kind: "forget",
319
+ record,
320
+ lesson: line.kind === "lesson",
321
+ line
322
+ }));
323
+ async function confirmForget(record, lesson) {
324
+ const confirmed = await ctx.ui.dialog.confirm({
325
+ title: lesson ? "Forget lesson" : "Forget record",
326
+ message: forgetMessage(record, lesson),
327
+ label: {
328
+ confirm: "Forget",
329
+ cancel: "Keep"
330
+ }
331
+ });
332
+ if (!confirmed)
333
+ return false;
334
+ try {
335
+ const result = await memory.forget({
336
+ sessionID: record.sessionID,
337
+ lesson
338
+ }, {
339
+ location: where()
340
+ });
341
+ ctx.ui.toast.show({
342
+ title: "courier",
343
+ message: forgottenLine(record, result.outcome),
344
+ variant: result.outcome === "none" ? "warning" : "success"
345
+ });
346
+ return true;
347
+ } catch (error) {
348
+ fail("Not forgotten")(error);
349
+ return false;
350
+ }
351
+ }
352
+ let opened = false;
353
+ async function open() {
354
+ if (opened)
355
+ return;
356
+ opened = true;
357
+ try {
358
+ let viewing;
359
+ let near = [];
360
+ let line;
361
+ for (;; ) {
362
+ const read = await memory.records({}, {
363
+ location: where()
364
+ }).catch(fail("The records could not be read"));
365
+ if (!read)
366
+ return;
367
+ if (!read.memory)
368
+ return say(MEMORY_OFF);
369
+ if (!read.records.length)
370
+ return say(NO_RECORDS);
371
+ const record = viewing === undefined ? undefined : read.records.find((one) => one.sessionID === viewing);
372
+ viewing = record?.sessionID;
373
+ const listed = read.records.map((one) => one.sessionID);
374
+ const current = near.map((sessionID) => read.records.find((one) => one.sessionID === sessionID)).find((one) => one !== undefined);
375
+ const at = line === "lesson" ? record && lessonLine(record) : line;
376
+ const request = await (record ? showRecord(record, at) : showList(read, current));
377
+ line = undefined;
378
+ if (request.kind === "back") {
379
+ if (!record)
380
+ return;
381
+ near = nearby(listed, record.sessionID, false);
382
+ viewing = undefined;
383
+ continue;
384
+ }
385
+ if (request.kind === "open") {
386
+ viewing = request.record.sessionID;
387
+ continue;
388
+ }
389
+ const forgotten = await confirmForget(request.record, request.lesson);
390
+ near = nearby(listed, request.record.sessionID, forgotten && !request.lesson);
391
+ if (record && !forgotten)
392
+ line = request.line;
393
+ else if (request.lesson)
394
+ line = "lesson";
395
+ else
396
+ viewing = undefined;
397
+ }
398
+ } finally {
399
+ opened = false;
400
+ }
401
+ }
402
+ ctx.keymap.layer(() => ({
403
+ mode: "global",
404
+ commands: [{
405
+ id: MEMORY_COMMAND,
406
+ title: "Courier: memory",
407
+ description: "The records of this project's session trees, newest first under the day each last moved: enter for one in full, x to forget one or its lesson",
408
+ group: "Courier",
409
+ palette: true,
410
+ run: () => void open()
411
+ }]
412
+ }));
413
+ return {
414
+ open
415
+ };
416
+ }
417
+
418
+ // src/person/rpc.ts
419
+ import { Rpc as Rpc2 } from "@opencode/plugin/rpc";
420
+ import { Schema as Schema2 } from "effect";
421
+ var Count2 = Schema2.Int.check(Schema2.isGreaterThanOrEqualTo(0));
422
+ var Session = Schema2.Struct({
423
+ sessionID: Schema2.String,
424
+ title: Schema2.String,
425
+ parentID: Schema2.String,
426
+ depth: Count2,
427
+ group: Schema2.optionalKey(Schema2.String)
428
+ });
429
+ var Permission = Schema2.Struct({
430
+ sessionID: Schema2.String,
431
+ requestID: Schema2.String,
432
+ action: Schema2.String,
433
+ resources: Schema2.Array(Schema2.String),
434
+ save: Schema2.optionalKey(Schema2.Array(Schema2.String)),
435
+ passesOn: Schema2.optionalKey(Schema2.Struct({ sessionID: Schema2.String, requestID: Schema2.String }))
35
436
  });
36
- var Asks = Schema.Struct({
37
- top: Schema.String,
38
- sessions: Schema.Array(Session),
39
- permissions: Schema.Array(Permission),
40
- questions: Schema.Array(Question)
437
+ var Prompt = Schema2.Struct({
438
+ question: Schema2.String,
439
+ header: Schema2.String,
440
+ options: Schema2.Array(Schema2.Struct({ label: Schema2.String, description: Schema2.String })),
441
+ multiple: Schema2.optionalKey(Schema2.Boolean)
442
+ });
443
+ var Question = Schema2.Struct({
444
+ sessionID: Schema2.String,
445
+ requestID: Schema2.String,
446
+ questions: Schema2.Array(Prompt),
447
+ stopped: Schema2.optionalKey(Schema2.Boolean)
41
448
  });
42
- var Answered = Schema.Struct({
43
- answered: Schema.Boolean,
44
- by: Schema.optionalKey(Schema.Literals(["result", "message"])),
45
- problem: Schema.optionalKey(Schema.String)
449
+ var Asks = Schema2.Struct({
450
+ top: Schema2.String,
451
+ sessions: Schema2.Array(Session),
452
+ permissions: Schema2.Array(Permission),
453
+ questions: Schema2.Array(Question)
46
454
  });
47
- var personRpc = Rpc.define({
455
+ var Answered = Schema2.Struct({
456
+ answered: Schema2.Boolean,
457
+ by: Schema2.optionalKey(Schema2.Literals(["result", "message"])),
458
+ problem: Schema2.optionalKey(Schema2.String)
459
+ });
460
+ var personRpc = Rpc2.define({
48
461
  id: "courier-person",
49
462
  methods: {
50
463
  asks: {
51
- input: Schema.toStandardSchemaV1(Schema.Struct({ sessionID: Schema.String })),
52
- output: Schema.toStandardSchemaV1(Asks)
464
+ input: Schema2.toStandardSchemaV1(Schema2.Struct({ sessionID: Schema2.String })),
465
+ output: Schema2.toStandardSchemaV1(Asks)
53
466
  },
54
467
  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)
468
+ input: Schema2.toStandardSchemaV1(Schema2.Struct({ sessionID: Schema2.String, requestID: Schema2.String, answers: Schema2.Array(Schema2.Array(Schema2.String)) })),
469
+ output: Schema2.toStandardSchemaV1(Answered)
57
470
  }
58
471
  },
59
472
  events: {}
@@ -103,7 +516,7 @@ function pathOf(asks, sessionID, topTitle, currentTitle = () => {
103
516
  titles.unshift(currentTitle(at.sessionID) || at.title);
104
517
  return [topTitle, ...titles].join(" \u203A ");
105
518
  }
106
- var clip = (text, max = 80) => text.length > max ? `${text.slice(0, max - 1)}\u2026` : text;
519
+ var clip2 = (text, max = 80) => text.length > max ? `${text.slice(0, max - 1)}\u2026` : text;
107
520
  function askTitle(ask) {
108
521
  switch (ask.kind) {
109
522
  case "permission": {
@@ -114,17 +527,17 @@ function askTitle(ask) {
114
527
  } = ask.request;
115
528
  const what = resources.length ? `${action} ${resources.join(", ")}` : action;
116
529
  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}`);
530
+ return clip2(`Permission: ${what}`);
531
+ return clip2(passesOn ? `Permission (top session): courier_answer once for "${what}"` : `Permission (top session): ${what}`);
119
532
  }
120
533
  case "question": {
121
534
  const [first, ...rest] = ask.question.questions;
122
535
  const what = first ? `${first.header}: ${first.question}` : ask.question.requestID;
123
536
  const more = rest.length ? ` (+${rest.length} more)` : "";
124
- return clip(`Question: ${what}${more}`);
537
+ return clip2(`Question: ${what}${more}`);
125
538
  }
126
539
  case "form":
127
- return clip(`Form: ${ask.form.title}`);
540
+ return clip2(`Form: ${ask.form.title}`);
128
541
  }
129
542
  }
130
543
  function askFooter(ask) {
@@ -384,12 +797,12 @@ function addAsksInbox(ctx, follows) {
384
797
  } = ask;
385
798
  const answers = [];
386
799
  for (const [index, prompt] of question.questions.entries()) {
387
- const count = question.questions.length > 1 ? ` (${index + 1}/${question.questions.length})` : "";
800
+ const count2 = question.questions.length > 1 ? ` (${index + 1}/${question.questions.length})` : "";
388
801
  const choices = prompt.options.map((option) => ({
389
802
  ...option,
390
803
  value: option.label
391
804
  }));
392
- const picked = await choose(`${prompt.header}: ${prompt.question}${count}`, choices, {
805
+ const picked = await choose(`${prompt.header}: ${prompt.question}${count2}`, choices, {
393
806
  multiple: !!prompt.multiple,
394
807
  own: true
395
808
  });
@@ -947,74 +1360,74 @@ function art(state, frame) {
947
1360
  }
948
1361
 
949
1362
  // src/tree/rpc.ts
950
- import { Rpc as Rpc2 } from "@opencode/plugin/rpc";
951
- import { Schema as Schema2 } from "effect";
952
- var Count2 = Schema2.Int.check(Schema2.isGreaterThanOrEqualTo(0));
953
- var Group = Schema2.Struct({ name: Schema2.String, reported: Count2, members: Count2 });
954
- var Summary = Schema2.Struct({
955
- running: Count2,
956
- waiting: Count2,
957
- idle: Count2,
958
- stalled: Schema2.optionalKey(Count2),
959
- groups: Schema2.Array(Group)
1363
+ import { Rpc as Rpc3 } from "@opencode/plugin/rpc";
1364
+ import { Schema as Schema3 } from "effect";
1365
+ var Count3 = Schema3.Int.check(Schema3.isGreaterThanOrEqualTo(0));
1366
+ var Group = Schema3.Struct({ name: Schema3.String, reported: Count3, members: Count3 });
1367
+ var Summary = Schema3.Struct({
1368
+ running: Count3,
1369
+ waiting: Count3,
1370
+ idle: Count3,
1371
+ stalled: Schema3.optionalKey(Count3),
1372
+ groups: Schema3.Array(Group)
960
1373
  });
961
- var Wait = Schema2.Union([
962
- Schema2.Struct({ type: Schema2.Literal("permission"), action: Schema2.String, resources: Schema2.Array(Schema2.String) }),
963
- Schema2.Struct({ type: Schema2.Literal("question"), question: Schema2.String }),
964
- Schema2.Struct({ type: Schema2.Literal("form"), title: Schema2.String })
1374
+ var Wait = Schema3.Union([
1375
+ Schema3.Struct({ type: Schema3.Literal("permission"), action: Schema3.String, resources: Schema3.Array(Schema3.String) }),
1376
+ Schema3.Struct({ type: Schema3.Literal("question"), question: Schema3.String }),
1377
+ Schema3.Struct({ type: Schema3.Literal("form"), title: Schema3.String })
965
1378
  ]);
966
- var Row = Schema2.Struct({
967
- sessionID: Schema2.String,
968
- parentID: Schema2.String,
969
- title: Schema2.String,
970
- depth: Count2,
971
- state: Schema2.Literals(["running", "waiting", "idle", "stalled", "gone"]),
972
- stalled: Schema2.optionalKey(Schema2.Number),
973
- outcome: Schema2.optionalKey(Schema2.Literals(["succeeded", "failed", "interrupted"])),
974
- stopped: Schema2.optionalKey(Schema2.Boolean),
975
- report: Schema2.optionalKey(Schema2.Struct({ state: Schema2.Literals(["owed", "reported", "ended"]), status: Schema2.optionalKey(Schema2.String), progressed: Schema2.Boolean })),
976
- group: Schema2.optionalKey(Schema2.Struct({ name: Schema2.String, standing: Schema2.String })),
977
- isolated: Schema2.Boolean,
978
- created: Schema2.Number,
979
- idle: Schema2.optionalKey(Schema2.Number),
980
- cost: Schema2.optionalKey(Schema2.Number),
981
- context: Schema2.optionalKey(Schema2.Number),
982
- tool: Schema2.optionalKey(Schema2.Struct({ name: Schema2.String, at: Schema2.Number })),
983
- waits: Schema2.Array(Wait)
1379
+ var Row = Schema3.Struct({
1380
+ sessionID: Schema3.String,
1381
+ parentID: Schema3.String,
1382
+ title: Schema3.String,
1383
+ depth: Count3,
1384
+ state: Schema3.Literals(["running", "waiting", "idle", "stalled", "gone"]),
1385
+ stalled: Schema3.optionalKey(Schema3.Number),
1386
+ outcome: Schema3.optionalKey(Schema3.Literals(["succeeded", "failed", "interrupted"])),
1387
+ stopped: Schema3.optionalKey(Schema3.Boolean),
1388
+ report: Schema3.optionalKey(Schema3.Struct({ state: Schema3.Literals(["owed", "reported", "ended"]), status: Schema3.optionalKey(Schema3.String), progressed: Schema3.Boolean })),
1389
+ group: Schema3.optionalKey(Schema3.Struct({ name: Schema3.String, standing: Schema3.String })),
1390
+ isolated: Schema3.Boolean,
1391
+ created: Schema3.Number,
1392
+ idle: Schema3.optionalKey(Schema3.Number),
1393
+ cost: Schema3.optionalKey(Schema3.Number),
1394
+ context: Schema3.optionalKey(Schema3.Number),
1395
+ tool: Schema3.optionalKey(Schema3.Struct({ name: Schema3.String, at: Schema3.Number })),
1396
+ waits: Schema3.Array(Wait)
984
1397
  });
985
- var Tree = Schema2.Struct({
986
- rows: Schema2.Array(Row),
987
- truncated: Schema2.optionalKey(Count2),
988
- stalled: Schema2.optionalKey(Schema2.Number)
1398
+ var Tree = Schema3.Struct({
1399
+ rows: Schema3.Array(Row),
1400
+ truncated: Schema3.optionalKey(Count3),
1401
+ stalled: Schema3.optionalKey(Schema3.Number)
989
1402
  });
990
- var Stopped = Schema2.Struct({
991
- stopped: Schema2.Array(Schema2.Struct({ sessionID: Schema2.String, title: Schema2.String, outcome: Schema2.String })),
992
- cancelled: Count2
1403
+ var Stopped = Schema3.Struct({
1404
+ stopped: Schema3.Array(Schema3.Struct({ sessionID: Schema3.String, title: Schema3.String, outcome: Schema3.String })),
1405
+ cancelled: Count3
993
1406
  });
994
- var Forgotten = Schema2.Union([
995
- Schema2.Struct({ outcome: Schema2.Literals(["forgotten", "removed", "gone"]) }),
996
- Schema2.Struct({ outcome: Schema2.Literal("kept"), reason: Schema2.String })
1407
+ var Forgotten2 = Schema3.Union([
1408
+ Schema3.Struct({ outcome: Schema3.Literals(["forgotten", "removed", "gone"]) }),
1409
+ Schema3.Struct({ outcome: Schema3.Literal("kept"), reason: Schema3.String })
997
1410
  ]);
998
- var treeRpc = Rpc2.define({
1411
+ var treeRpc = Rpc3.define({
999
1412
  id: "courier",
1000
1413
  methods: {
1001
1414
  summary: {
1002
- input: Schema2.toStandardSchemaV1(Schema2.Struct({ sessionID: Schema2.String })),
1003
- output: Schema2.toStandardSchemaV1(Summary)
1415
+ input: Schema3.toStandardSchemaV1(Schema3.Struct({ sessionID: Schema3.String })),
1416
+ output: Schema3.toStandardSchemaV1(Summary)
1004
1417
  },
1005
1418
  tree: {
1006
- input: Schema2.toStandardSchemaV1(Schema2.Struct({ sessionID: Schema2.String })),
1007
- output: Schema2.toStandardSchemaV1(Tree)
1419
+ input: Schema3.toStandardSchemaV1(Schema3.Struct({ sessionID: Schema3.String })),
1420
+ output: Schema3.toStandardSchemaV1(Tree)
1008
1421
  },
1009
1422
  stop: {
1010
- input: Schema2.toStandardSchemaV1(Schema2.Struct({ sessionID: Schema2.String, by: Schema2.String })),
1011
- output: Schema2.toStandardSchemaV1(Stopped),
1012
- errors: { refused: Schema2.toStandardSchemaV1(Schema2.Struct({})) }
1423
+ input: Schema3.toStandardSchemaV1(Schema3.Struct({ sessionID: Schema3.String, by: Schema3.String })),
1424
+ output: Schema3.toStandardSchemaV1(Stopped),
1425
+ errors: { refused: Schema3.toStandardSchemaV1(Schema3.Struct({})) }
1013
1426
  },
1014
1427
  cleanup: {
1015
- input: Schema2.toStandardSchemaV1(Schema2.Struct({ sessionID: Schema2.String, by: Schema2.String })),
1016
- output: Schema2.toStandardSchemaV1(Forgotten),
1017
- errors: { refused: Schema2.toStandardSchemaV1(Schema2.Struct({})) }
1428
+ input: Schema3.toStandardSchemaV1(Schema3.Struct({ sessionID: Schema3.String, by: Schema3.String })),
1429
+ output: Schema3.toStandardSchemaV1(Forgotten2),
1430
+ errors: { refused: Schema3.toStandardSchemaV1(Schema3.Struct({})) }
1018
1431
  }
1019
1432
  },
1020
1433
  events: {}
@@ -1053,14 +1466,14 @@ function createMotion(enabled) {
1053
1466
  const now = untrack(moving);
1054
1467
  for (const one of now)
1055
1468
  one.step(FRAME_MS * every);
1056
- setFrame((count) => count + every);
1469
+ setFrame((count2) => count2 + every);
1057
1470
  const left = now.filter((one) => !one.done());
1058
1471
  if (left.length !== now.length)
1059
1472
  setMoving(left);
1060
1473
  }), FRAME_MS * every);
1061
1474
  onCleanup(() => clearInterval(timer));
1062
1475
  });
1063
- const tween = (from, to, duration, ease) => {
1476
+ const tween = (from, to, duration2, ease) => {
1064
1477
  if (!enabled || from === to)
1065
1478
  return {
1066
1479
  value: () => to,
@@ -1070,11 +1483,11 @@ function createMotion(enabled) {
1070
1483
  value: from
1071
1484
  };
1072
1485
  const timeline = new Timeline({
1073
- duration,
1486
+ duration: duration2,
1074
1487
  autoplay: false
1075
1488
  });
1076
1489
  timeline.add(target, {
1077
- duration,
1490
+ duration: duration2,
1078
1491
  ease,
1079
1492
  value: to
1080
1493
  });
@@ -1087,7 +1500,7 @@ function createMotion(enabled) {
1087
1500
  },
1088
1501
  done: () => {
1089
1502
  frame();
1090
- return elapsed >= duration;
1503
+ return elapsed >= duration2;
1091
1504
  },
1092
1505
  step: (ms) => {
1093
1506
  elapsed += ms;
@@ -1186,8 +1599,8 @@ function mascotState(rows, {
1186
1599
  return rows.length && rows.every((row) => row.state === "gone") ? "gone" : "idle";
1187
1600
  }
1188
1601
  function countsLine(rows) {
1189
- const count = (state) => rows.filter((row) => row.state === state).length;
1190
- return [count("waiting") ? `${count("waiting")} waiting on you` : "", count("stalled") ? `${count("stalled")} stalled` : "", count("running") ? `${count("running")} running` : "", count("idle") ? `${count("idle")} idle` : ""].filter(Boolean).join(" \xB7 ");
1602
+ const count2 = (state) => rows.filter((row) => row.state === state).length;
1603
+ return [count2("waiting") ? `${count2("waiting")} waiting on you` : "", count2("stalled") ? `${count2("stalled")} stalled` : "", count2("running") ? `${count2("running")} running` : "", count2("idle") ? `${count2("idle")} idle` : ""].filter(Boolean).join(" \xB7 ");
1191
1604
  }
1192
1605
  var GLYPH_ROWS = 12;
1193
1606
  var GLYPH_COLUMNS = 54;
@@ -1211,14 +1624,14 @@ function summaryLine(summary, maxWidth = Infinity) {
1211
1624
  return line(kept);
1212
1625
  return parts[0];
1213
1626
  }
1214
- function duration(ms) {
1627
+ function duration2(ms) {
1215
1628
  const seconds = Math.max(0, Math.floor(ms / 1000));
1216
- const pad = (value) => String(value).padStart(2, "0");
1629
+ const pad2 = (value) => String(value).padStart(2, "0");
1217
1630
  if (seconds < 60)
1218
1631
  return `${seconds}s`;
1219
1632
  if (seconds < 3600)
1220
- return `${Math.floor(seconds / 60)}m ${pad(seconds % 60)}s`;
1221
- return `${Math.floor(seconds / 3600)}h ${pad(Math.floor(seconds / 60) % 60)}m`;
1633
+ return `${Math.floor(seconds / 60)}m ${pad2(seconds % 60)}s`;
1634
+ return `${Math.floor(seconds / 3600)}h ${pad2(Math.floor(seconds / 60) % 60)}m`;
1222
1635
  }
1223
1636
  function parts(rows, parentID) {
1224
1637
  const children = rows.filter((row) => row.parentID === parentID);
@@ -1324,14 +1737,14 @@ function reportWords(report2, stopped) {
1324
1737
  return stopped ? "" : "ended without a report";
1325
1738
  return ["reported", report2.status].filter(Boolean).join(" ");
1326
1739
  }
1327
- var counted = (count, noun) => `${count} ${noun}${count === 1 ? "" : "s"}`;
1740
+ var counted = (count2, noun) => `${count2} ${noun}${count2 === 1 ? "" : "s"}`;
1328
1741
  var CONTEXT_WARN = 0.8;
1329
1742
  function activityParts(row, now) {
1330
- 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.context !== undefined ? `ctx ${Math.round(row.context * 100)}%` : "", row.cost !== undefined ? `$${row.cost.toFixed(2)}` : ""].filter(Boolean);
1743
+ return [`ran ${duration2((row.idle ?? now) - row.created)}`, row.tool ? `last ${row.tool.name} ${duration2(now - row.tool.at)} ago` : "no tool call yet", row.context !== undefined ? `ctx ${Math.round(row.context * 100)}%` : "", row.cost !== undefined ? `$${row.cost.toFixed(2)}` : ""].filter(Boolean);
1331
1744
  }
1332
1745
  var activityLine = (row, now) => activityParts(row, now).join(" \xB7 ");
1333
1746
  function stalledLine(since, now) {
1334
- return `a message waits in its inbox since ${duration(now - since)}, woken again in vain`;
1747
+ return `a message waits in its inbox since ${duration2(now - since)}, woken again in vain`;
1335
1748
  }
1336
1749
  function detailLines(row, rows, now) {
1337
1750
  const started = parts(rows, row.sessionID);
@@ -1354,7 +1767,7 @@ function courierOf(ctx) {
1354
1767
  var REREAD_MS = 15000;
1355
1768
  function changes(ctx, types, settle) {
1356
1769
  const [changed, setChanged] = createSignal(0);
1357
- const reread = setInterval(() => setChanged((count) => count + 1), REREAD_MS);
1770
+ const reread = setInterval(() => setChanged((count2) => count2 + 1), REREAD_MS);
1358
1771
  let settling;
1359
1772
  const unlisten = ctx.data.listen(({
1360
1773
  details
@@ -1363,7 +1776,7 @@ function changes(ctx, types, settle) {
1363
1776
  return;
1364
1777
  settling = setTimeout(() => {
1365
1778
  settling = undefined;
1366
- setChanged((count) => count + 1);
1779
+ setChanged((count2) => count2 + 1);
1367
1780
  }, settle);
1368
1781
  });
1369
1782
  return [changed, () => {
@@ -1489,8 +1902,8 @@ async function confirmStop(ctx, courier, location, by, target) {
1489
1902
  }
1490
1903
  return true;
1491
1904
  }
1492
- function forgottenLine(title, result) {
1493
- const name = `"${oneLine(title)}"`;
1905
+ function forgottenLine2(title, result) {
1906
+ const name = `"${oneLine3(title)}"`;
1494
1907
  if (result.outcome === "kept")
1495
1908
  return `Kept ${name} and its worktree: ${result.reason}`;
1496
1909
  if (result.outcome === "removed")
@@ -1501,7 +1914,7 @@ async function confirmForget(ctx, courier, location, by, target, done) {
1501
1914
  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.";
1502
1915
  const confirmed = await ctx.ui.dialog.confirm({
1503
1916
  title: "Forget session",
1504
- message: `Forget "${oneLine(target.title)}" (${target.sessionID})? ${what}`,
1917
+ message: `Forget "${oneLine3(target.title)}" (${target.sessionID})? ${what}`,
1505
1918
  label: {
1506
1919
  confirm: "Forget",
1507
1920
  cancel: "Keep"
@@ -1518,7 +1931,7 @@ async function confirmForget(ctx, courier, location, by, target, done) {
1518
1931
  });
1519
1932
  ctx.ui.toast.show({
1520
1933
  title: "courier",
1521
- message: forgottenLine(target.title, result),
1934
+ message: forgottenLine2(target.title, result),
1522
1935
  variant: result.outcome === "kept" ? "warning" : "success"
1523
1936
  });
1524
1937
  if (result.outcome !== "kept")
@@ -1534,7 +1947,7 @@ async function confirmForget(ctx, courier, location, by, target, done) {
1534
1947
  }
1535
1948
  var FOCUS_PROMPT = "pane.focus.left";
1536
1949
  var SPLIT_WIDTH = 108;
1537
- var oneLine = (title) => title.replace(/[\r\n]+/g, " ");
1950
+ var oneLine3 = (title) => title.replace(/[\r\n]+/g, " ");
1538
1951
  var digbyColours = (theme) => ({
1539
1952
  b: theme.text.base,
1540
1953
  k: theme.background.base,
@@ -1682,7 +2095,7 @@ function TreePanel(props) {
1682
2095
  if (row.state !== "gone")
1683
2096
  return ctx.ui.toast.show({
1684
2097
  title: "courier",
1685
- message: `x forgets only a gone session; "${oneLine(row.title)}" is ${row.state}.`,
2098
+ message: `x forgets only a gone session; "${oneLine3(row.title)}" is ${row.state}.`,
1686
2099
  variant: "warning"
1687
2100
  });
1688
2101
  confirmForget(ctx, courier, location, view.root, {
@@ -1996,7 +2409,7 @@ function TreePanel(props) {
1996
2409
  _$setProp(_el$29, "wrapMode", "none");
1997
2410
  _$setProp(_el$29, "flexShrink", 1);
1998
2411
  _$setProp(_el$29, "minWidth", 0);
1999
- _$insert(_el$29, () => oneLine(row.title), null);
2412
+ _$insert(_el$29, () => oneLine3(row.title), null);
2000
2413
  _$insert(_el$29, () => row.sessionID === input.sessionID ? " (on screen)" : "", null);
2001
2414
  _$insertNode(_el$30, _$createTextNode(` `));
2002
2415
  _$setProp(_el$30, "wrapMode", "none");
@@ -2115,7 +2528,8 @@ function TreePanel(props) {
2115
2528
  })();
2116
2529
  }
2117
2530
  var USAGE = "/courier tree opens the session tree; /courier stop [session] stops a session and those under it";
2118
- function addTreePanel(ctx) {
2531
+ var usageWith = (verbs) => [USAGE, ...Object.values(verbs).map((verb) => verb.usage)].join("; ");
2532
+ function addTreePanel(ctx, verbs = {}) {
2119
2533
  const {
2120
2534
  courier,
2121
2535
  location
@@ -2306,7 +2720,7 @@ function addTreePanel(ctx) {
2306
2720
  commands: [{
2307
2721
  id: "courier.command",
2308
2722
  title: "Courier",
2309
- description: USAGE,
2723
+ description: usageWith(verbs),
2310
2724
  group: "Courier",
2311
2725
  slash: {
2312
2726
  name: "courier",
@@ -2318,9 +2732,12 @@ function addTreePanel(ctx) {
2318
2732
  return openPanel();
2319
2733
  if (verb === "stop")
2320
2734
  return stopUnder(target);
2735
+ const further = Object.hasOwn(verbs, verb) ? verbs[verb] : undefined;
2736
+ if (further)
2737
+ return further.run();
2321
2738
  ctx.ui.toast.show({
2322
2739
  title: "courier",
2323
- message: USAGE,
2740
+ message: usageWith(verbs),
2324
2741
  variant: "warning"
2325
2742
  });
2326
2743
  }
@@ -2390,7 +2807,13 @@ var tui_default = Plugin.define({
2390
2807
  id: "courier.tui",
2391
2808
  setup: (ctx) => {
2392
2809
  const inbox = addAsksInbox(ctx, CHANGES);
2393
- const cleanups = [inbox.cleanup, addTreeStatus(ctx, (sessionID) => void inbox.open(sessionID)), addTreePanel(ctx), addTreeAttention(ctx)];
2810
+ const memory = addMemoryView(ctx);
2811
+ const cleanups = [inbox.cleanup, addTreeStatus(ctx, (sessionID) => void inbox.open(sessionID)), addTreePanel(ctx, {
2812
+ memory: {
2813
+ run: () => void memory.open(),
2814
+ usage: MEMORY_USAGE
2815
+ }
2816
+ }), addTreeAttention(ctx)];
2394
2817
  return () => {
2395
2818
  for (const cleanup of cleanups.toReversed())
2396
2819
  cleanup();