opencode-courier 0.8.0 → 0.9.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
@@ -6,7 +6,7 @@ import { Plugin } from "@opencode/plugin/tui";
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 PART_STATUSES = ["done", "partial", "blocked", "failed", "interrupted", "stopped", "stalled", "out"];
9
+ var PART_STATUSES = ["done", "partial", "blocked", "failed", "interrupted", "stopped", "gone", "stalled", "out"];
10
10
  var Part = Schema.Struct({
11
11
  sessionID: Schema.String,
12
12
  title: Schema.String,
@@ -29,6 +29,8 @@ var Record = Schema.Struct({
29
29
  finished: Schema.Boolean,
30
30
  recalled: Schema.optionalKey(Schema.Struct({ at: Schema.optionalKey(Schema.Number), calls: Count })),
31
31
  lesson: Schema.optionalKey(Lesson),
32
+ reads: Schema.optionalKey(Schema.Struct({ shown: Count, read: Count, at: Schema.Number })),
33
+ pinned: Schema.optionalKey(Schema.Struct({ at: Schema.Number })),
32
34
  parts: Schema.Array(Part)
33
35
  });
34
36
  var Records = Schema.Struct({
@@ -37,6 +39,7 @@ var Records = Schema.Struct({
37
39
  records: Schema.Array(Record)
38
40
  });
39
41
  var Forgotten = Schema.Struct({ outcome: Schema.Literals(["record", "lesson", "none"]) });
42
+ var Pinned = Schema.Struct({ outcome: Schema.Literals(["pinned", "unpinned", "none"]) });
40
43
  var memoryRpc = Rpc.define({
41
44
  id: "courier-memory",
42
45
  methods: {
@@ -47,6 +50,10 @@ var memoryRpc = Rpc.define({
47
50
  forget: {
48
51
  input: Schema.toStandardSchemaV1(Schema.Struct({ sessionID: Schema.String, lesson: Schema.optionalKey(Schema.Boolean) })),
49
52
  output: Schema.toStandardSchemaV1(Forgotten)
53
+ },
54
+ pin: {
55
+ input: Schema.toStandardSchemaV1(Schema.Struct({ sessionID: Schema.String, pinned: Schema.Boolean })),
56
+ output: Schema.toStandardSchemaV1(Pinned)
50
57
  }
51
58
  },
52
59
  events: {}
@@ -57,7 +64,7 @@ var STATUSES = ["done", "partial", "blocked", "failed"];
57
64
 
58
65
  // src/memory/domain.ts
59
66
  var RETENTION_MS = 90 * 24 * 60 * 60000;
60
- var PART_STATUSES2 = [...STATUSES, "interrupted", "stopped", "stalled", "out"];
67
+ var PART_STATUSES2 = [...STATUSES, "interrupted", "stopped", "gone", "stalled", "out"];
61
68
 
62
69
  // src/memory/text.ts
63
70
  function duration(ms) {
@@ -73,12 +80,13 @@ var NOTE_EMPTY = `Not noted: text is empty. Write one line, ${ONE_LINE}, or noth
73
80
 
74
81
  // src/memory/tui.tsx
75
82
  var MEMORY_COMMAND = "courier.memory";
76
- var MEMORY_USAGE = "/courier memory lists this project's tree records, to read or forget";
83
+ var MEMORY_USAGE = "/courier memory lists this project's tree records, to read, pin or forget";
77
84
  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
85
  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
86
  var LESSON_LINE_MAX = 160;
80
87
  var ROW_TITLE_MAX = 46;
81
88
  var TODAY = "Today";
89
+ var PINNED = "Pinned";
82
90
  var LESSON_ROW = 90;
83
91
  var pad = (n) => String(n).padStart(2, "0");
84
92
  var localDay = (at) => new Date(at).toDateString();
@@ -96,6 +104,7 @@ var minute = (at) => {
96
104
  var oneLine2 = (text) => text.replace(/\s+/g, " ").trim();
97
105
  var clip = (text, max) => text.length > max ? `${text.slice(0, max - 1)}\u2026` : text;
98
106
  var count = (n, noun) => `${n} ${noun}${n === 1 ? "" : "s"}`;
107
+ var times = (n) => n === 1 ? "once" : `${n} times`;
99
108
  var money = (cost) => `$${cost.toFixed(2)}`;
100
109
  var nameOf = (record) => clip(oneLine2(record.title) || record.sessionID, 60);
101
110
  function partsCounted(parts) {
@@ -114,16 +123,21 @@ function recordTitle(record, open) {
114
123
  return `${clip(oneLine2(record.title) || record.sessionID, ROW_TITLE_MAX)}${record.sessionID === open ? " (this session)" : ""}`;
115
124
  }
116
125
  function recordCategory(record, now = Date.now()) {
126
+ if (record.pinned)
127
+ return PINNED;
117
128
  const moved = localDay(record.movedAt);
118
129
  return moved === localDay(now) ? TODAY : moved;
119
130
  }
131
+ function readsLine(reads) {
132
+ return [reads.shown ? `shown ${times(reads.shown)}` : "", reads.read ? `opened ${times(reads.read)}` : ""].filter(Boolean).join(", ");
133
+ }
120
134
  function lessonHeading(lesson) {
121
135
  const from = lesson.depth === undefined ? "" : `, from depth ${lesson.depth}`;
122
136
  return `lesson (${day(lesson.at)}${from})`;
123
137
  }
124
138
  function recordDescription(record) {
125
139
  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 ");
140
+ return [partsCounted(record.parts), duration(record.movedAt - record.startedAt), cost === undefined ? "" : money(cost), record.finished ? "" : "not finished", record.reads ? readsLine(record.reads) : ""].filter(Boolean).join(" \xB7 ");
127
141
  }
128
142
  function recordDetails(record) {
129
143
  return record.lesson ? [clip(`${lessonHeading(record.lesson)}: "${oneLine2(record.lesson.text)}"`, LESSON_LINE_MAX)] : [];
@@ -178,6 +192,8 @@ function detailOptions(record) {
178
192
  });
179
193
  const cost = costOf(record);
180
194
  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");
195
+ const reads = record.reads ? fact("reads", `${readsLine(record.reads).replace(/^./, (first) => first.toUpperCase())}`, `by other trees' courier_recall, last ${minute(record.reads.at)}`) : fact("reads", "Never shown", "no other tree's courier_recall has listed it yet");
196
+ const pinned = record.pinned ? fact("pinned", "Pinned", `since ${minute(record.pinned.at)}: listed first, never swept; p unpins`) : fact("pinned", "Not pinned", "p pins it: listed first, never swept");
181
197
  const lesson = record.lesson ? [{
182
198
  title: lessonHeading(record.lesson),
183
199
  value: {
@@ -201,7 +217,7 @@ function detailOptions(record) {
201
217
  category: "Lesson",
202
218
  description: "courier_note writes one line per tree"
203
219
  }];
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) => ({
220
+ 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, reads, pinned, ...record.parts.length ? record.parts.map((part) => ({
205
221
  title: `${" ".repeat(Math.max(0, part.depth - 1))}${clip(oneLine2(part.title) || part.sessionID, 60)}`,
206
222
  value: {
207
223
  kind: "part",
@@ -233,6 +249,14 @@ function forgottenLine(record, outcome) {
233
249
  return `Forgot the lesson of ${name}; its facts stay`;
234
250
  return `Nothing to forget for ${name}: it was already gone`;
235
251
  }
252
+ function pinnedLine(record, outcome) {
253
+ const name = `"${nameOf(record)}"`;
254
+ if (outcome === "pinned")
255
+ return `Pinned ${name}: listed first, never swept`;
256
+ if (outcome === "unpinned")
257
+ return `Unpinned ${name}: kept as the others are`;
258
+ return `Nothing to pin for ${name}: it was already gone`;
259
+ }
236
260
  function addMemoryView(ctx) {
237
261
  const memory = ctx.client.rpc(memoryRpc);
238
262
  const openSession = () => {
@@ -258,7 +282,7 @@ function addMemoryView(ctx) {
258
282
  message,
259
283
  variant: "info"
260
284
  });
261
- function show(options, chosen, forget) {
285
+ function show(options, chosen, forget, pin) {
262
286
  let request = () => {
263
287
  return;
264
288
  };
@@ -271,6 +295,10 @@ function addMemoryView(ctx) {
271
295
  bind: "x",
272
296
  title: "forget",
273
297
  onTrigger: (value) => request(forget(value))
298
+ }, {
299
+ bind: "p",
300
+ title: pin.title,
301
+ onTrigger: (value) => request(pin.ask(value))
274
302
  }]
275
303
  });
276
304
  ctx.ui.dialog.set({
@@ -303,7 +331,13 @@ function addMemoryView(ctx) {
303
331
  kind: "forget",
304
332
  record,
305
333
  lesson: false
306
- }));
334
+ }), {
335
+ title: "pin",
336
+ ask: (record) => ({
337
+ kind: "pin",
338
+ record
339
+ })
340
+ });
307
341
  };
308
342
  const showRecord = (record, current) => show({
309
343
  title: `${localDay(record.movedAt)} ${nameOf(record)}`,
@@ -319,7 +353,32 @@ function addMemoryView(ctx) {
319
353
  record,
320
354
  lesson: line.kind === "lesson",
321
355
  line
322
- }));
356
+ }), {
357
+ title: record.pinned ? "unpin" : "pin",
358
+ ask: (line) => ({
359
+ kind: "pin",
360
+ record,
361
+ line
362
+ })
363
+ });
364
+ async function togglePin(record) {
365
+ const pinned = !record.pinned;
366
+ try {
367
+ const result = await memory.pin({
368
+ sessionID: record.sessionID,
369
+ pinned
370
+ }, {
371
+ location: where()
372
+ });
373
+ ctx.ui.toast.show({
374
+ title: "courier",
375
+ message: pinnedLine(record, result.outcome),
376
+ variant: result.outcome === "none" ? "warning" : "success"
377
+ });
378
+ } catch (error) {
379
+ fail(pinned ? "Not pinned" : "Not unpinned")(error);
380
+ }
381
+ }
323
382
  async function confirmForget(record, lesson) {
324
383
  const confirmed = await ctx.ui.dialog.confirm({
325
384
  title: lesson ? "Forget lesson" : "Forget record",
@@ -386,6 +445,12 @@ function addMemoryView(ctx) {
386
445
  viewing = request.record.sessionID;
387
446
  continue;
388
447
  }
448
+ if (request.kind === "pin") {
449
+ await togglePin(request.record);
450
+ near = [request.record.sessionID];
451
+ line = request.line;
452
+ continue;
453
+ }
389
454
  const forgotten = await confirmForget(request.record, request.lesson);
390
455
  near = nearby(listed, request.record.sessionID, forgotten && !request.lesson);
391
456
  if (record && !forgotten)
@@ -404,7 +469,7 @@ function addMemoryView(ctx) {
404
469
  commands: [{
405
470
  id: MEMORY_COMMAND,
406
471
  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",
472
+ description: "The records of this project's session trees, pinned first, then newest first under the day each last moved: enter for one in full, p to pin or unpin one, x to forget one or its lesson",
408
473
  group: "Courier",
409
474
  palette: true,
410
475
  run: () => void open()
@@ -1295,7 +1360,7 @@ function digby(rise, { top = 6, eyes = "open", slump = false, shift = 0, mound =
1295
1360
  span(head + 2, 8, "k", (column) => !eyeColumns.includes(column), lean(2));
1296
1361
  span(head + 4, 2, "a", undefined, lean(4));
1297
1362
  span(10, SIZE, ".", (column) => column >= 6 && column <= 9);
1298
- span(10, top, mound, (column) => column >= 6 && column <= 9, shift);
1363
+ span(10, top, mound, (column) => column + shift >= 6 && column + shift <= 9, shift);
1299
1364
  span(11, 10, mound, undefined, shift);
1300
1365
  span(12, 12, mound, undefined, shift);
1301
1366
  for (const row of [13, 14, 15])
@@ -1958,6 +2023,7 @@ var digbyColours = (theme) => ({
1958
2023
  e: theme.text.feedback.error.base,
1959
2024
  s: theme.text.feedback.success.base
1960
2025
  });
2026
+ var DIGBY_UNDER = RGBA.fromValues(0, 0, 0, 0);
1961
2027
  function restBackground(theme, {
1962
2028
  selected,
1963
2029
  hovered
@@ -2200,7 +2266,7 @@ function TreePanel(props) {
2200
2266
  _$insert(_el$5, () => one().char);
2201
2267
  _$effect((_$p) => _$setProp(_el$5, "style", {
2202
2268
  fg: one().fg && tokens[one().fg],
2203
- bg: one().bg && tokens[one().bg]
2269
+ bg: one().bg ? tokens[one().bg] : DIGBY_UNDER
2204
2270
  }, _$p));
2205
2271
  return _el$5;
2206
2272
  })()
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "opencode-courier",
3
- "version": "0.8.0",
3
+ "version": "0.9.0",
4
4
  "description": "Multi-session orchestration for OpenCode V2: spawn child sessions, be woken by them. No polling.",
5
5
  "license": "MIT",
6
6
  "author": "Ivo Pogace <71701666+ivopogace@users.noreply.github.com>",