opencode-courier 0.8.0 → 0.10.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: {}
@@ -54,10 +61,13 @@ var memoryRpc = Rpc.define({
54
61
 
55
62
  // src/kernel/protocol.ts
56
63
  var STATUSES = ["done", "partial", "blocked", "failed"];
64
+ var SPLIT_LINE = "split: no, because \u2026; considered: <the parts you weighed> or split: <n> parts: \u2026 (one phrase per part)";
65
+ var ONE_PART_LINE = "split: 1 part: <the task>, to keep my context";
66
+ var ROOT_SPLIT_LINE = `${SPLIT_LINE} or ${ONE_PART_LINE}`;
57
67
 
58
68
  // src/memory/domain.ts
59
69
  var RETENTION_MS = 90 * 24 * 60 * 60000;
60
- var PART_STATUSES2 = [...STATUSES, "interrupted", "stopped", "stalled", "out"];
70
+ var PART_STATUSES2 = [...STATUSES, "interrupted", "stopped", "gone", "stalled", "out"];
61
71
 
62
72
  // src/memory/text.ts
63
73
  function duration(ms) {
@@ -73,12 +83,13 @@ var NOTE_EMPTY = `Not noted: text is empty. Write one line, ${ONE_LINE}, or noth
73
83
 
74
84
  // src/memory/tui.tsx
75
85
  var MEMORY_COMMAND = "courier.memory";
76
- var MEMORY_USAGE = "/courier memory lists this project's tree records, to read or forget";
86
+ var MEMORY_USAGE = "/courier memory lists this project's tree records, to read, pin or forget";
77
87
  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
88
  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
89
  var LESSON_LINE_MAX = 160;
80
90
  var ROW_TITLE_MAX = 46;
81
91
  var TODAY = "Today";
92
+ var PINNED = "Pinned";
82
93
  var LESSON_ROW = 90;
83
94
  var pad = (n) => String(n).padStart(2, "0");
84
95
  var localDay = (at) => new Date(at).toDateString();
@@ -96,6 +107,7 @@ var minute = (at) => {
96
107
  var oneLine2 = (text) => text.replace(/\s+/g, " ").trim();
97
108
  var clip = (text, max) => text.length > max ? `${text.slice(0, max - 1)}\u2026` : text;
98
109
  var count = (n, noun) => `${n} ${noun}${n === 1 ? "" : "s"}`;
110
+ var times = (n) => n === 1 ? "once" : `${n} times`;
99
111
  var money = (cost) => `$${cost.toFixed(2)}`;
100
112
  var nameOf = (record) => clip(oneLine2(record.title) || record.sessionID, 60);
101
113
  function partsCounted(parts) {
@@ -114,16 +126,21 @@ function recordTitle(record, open) {
114
126
  return `${clip(oneLine2(record.title) || record.sessionID, ROW_TITLE_MAX)}${record.sessionID === open ? " (this session)" : ""}`;
115
127
  }
116
128
  function recordCategory(record, now = Date.now()) {
129
+ if (record.pinned)
130
+ return PINNED;
117
131
  const moved = localDay(record.movedAt);
118
132
  return moved === localDay(now) ? TODAY : moved;
119
133
  }
134
+ function readsLine(reads) {
135
+ return [reads.shown ? `shown ${times(reads.shown)}` : "", reads.read ? `opened ${times(reads.read)}` : ""].filter(Boolean).join(", ");
136
+ }
120
137
  function lessonHeading(lesson) {
121
138
  const from = lesson.depth === undefined ? "" : `, from depth ${lesson.depth}`;
122
139
  return `lesson (${day(lesson.at)}${from})`;
123
140
  }
124
141
  function recordDescription(record) {
125
142
  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 ");
143
+ 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
144
  }
128
145
  function recordDetails(record) {
129
146
  return record.lesson ? [clip(`${lessonHeading(record.lesson)}: "${oneLine2(record.lesson.text)}"`, LESSON_LINE_MAX)] : [];
@@ -178,6 +195,8 @@ function detailOptions(record) {
178
195
  });
179
196
  const cost = costOf(record);
180
197
  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");
198
+ 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");
199
+ 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
200
  const lesson = record.lesson ? [{
182
201
  title: lessonHeading(record.lesson),
183
202
  value: {
@@ -201,7 +220,7 @@ function detailOptions(record) {
201
220
  category: "Lesson",
202
221
  description: "courier_note writes one line per tree"
203
222
  }];
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) => ({
223
+ 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
224
  title: `${" ".repeat(Math.max(0, part.depth - 1))}${clip(oneLine2(part.title) || part.sessionID, 60)}`,
206
225
  value: {
207
226
  kind: "part",
@@ -233,6 +252,14 @@ function forgottenLine(record, outcome) {
233
252
  return `Forgot the lesson of ${name}; its facts stay`;
234
253
  return `Nothing to forget for ${name}: it was already gone`;
235
254
  }
255
+ function pinnedLine(record, outcome) {
256
+ const name = `"${nameOf(record)}"`;
257
+ if (outcome === "pinned")
258
+ return `Pinned ${name}: listed first, never swept`;
259
+ if (outcome === "unpinned")
260
+ return `Unpinned ${name}: kept as the others are`;
261
+ return `Nothing to pin for ${name}: it was already gone`;
262
+ }
236
263
  function addMemoryView(ctx) {
237
264
  const memory = ctx.client.rpc(memoryRpc);
238
265
  const openSession = () => {
@@ -258,7 +285,7 @@ function addMemoryView(ctx) {
258
285
  message,
259
286
  variant: "info"
260
287
  });
261
- function show(options, chosen, forget) {
288
+ function show(options, chosen, forget, pin) {
262
289
  let request = () => {
263
290
  return;
264
291
  };
@@ -271,6 +298,10 @@ function addMemoryView(ctx) {
271
298
  bind: "x",
272
299
  title: "forget",
273
300
  onTrigger: (value) => request(forget(value))
301
+ }, {
302
+ bind: "p",
303
+ title: pin.title,
304
+ onTrigger: (value) => request(pin.ask(value))
274
305
  }]
275
306
  });
276
307
  ctx.ui.dialog.set({
@@ -303,7 +334,13 @@ function addMemoryView(ctx) {
303
334
  kind: "forget",
304
335
  record,
305
336
  lesson: false
306
- }));
337
+ }), {
338
+ title: "pin",
339
+ ask: (record) => ({
340
+ kind: "pin",
341
+ record
342
+ })
343
+ });
307
344
  };
308
345
  const showRecord = (record, current) => show({
309
346
  title: `${localDay(record.movedAt)} ${nameOf(record)}`,
@@ -319,7 +356,32 @@ function addMemoryView(ctx) {
319
356
  record,
320
357
  lesson: line.kind === "lesson",
321
358
  line
322
- }));
359
+ }), {
360
+ title: record.pinned ? "unpin" : "pin",
361
+ ask: (line) => ({
362
+ kind: "pin",
363
+ record,
364
+ line
365
+ })
366
+ });
367
+ async function togglePin(record) {
368
+ const pinned = !record.pinned;
369
+ try {
370
+ const result = await memory.pin({
371
+ sessionID: record.sessionID,
372
+ pinned
373
+ }, {
374
+ location: where()
375
+ });
376
+ ctx.ui.toast.show({
377
+ title: "courier",
378
+ message: pinnedLine(record, result.outcome),
379
+ variant: result.outcome === "none" ? "warning" : "success"
380
+ });
381
+ } catch (error) {
382
+ fail(pinned ? "Not pinned" : "Not unpinned")(error);
383
+ }
384
+ }
323
385
  async function confirmForget(record, lesson) {
324
386
  const confirmed = await ctx.ui.dialog.confirm({
325
387
  title: lesson ? "Forget lesson" : "Forget record",
@@ -386,6 +448,12 @@ function addMemoryView(ctx) {
386
448
  viewing = request.record.sessionID;
387
449
  continue;
388
450
  }
451
+ if (request.kind === "pin") {
452
+ await togglePin(request.record);
453
+ near = [request.record.sessionID];
454
+ line = request.line;
455
+ continue;
456
+ }
389
457
  const forgotten = await confirmForget(request.record, request.lesson);
390
458
  near = nearby(listed, request.record.sessionID, forgotten && !request.lesson);
391
459
  if (record && !forgotten)
@@ -404,7 +472,7 @@ function addMemoryView(ctx) {
404
472
  commands: [{
405
473
  id: MEMORY_COMMAND,
406
474
  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",
475
+ 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
476
  group: "Courier",
409
477
  palette: true,
410
478
  run: () => void open()
@@ -1295,7 +1363,7 @@ function digby(rise, { top = 6, eyes = "open", slump = false, shift = 0, mound =
1295
1363
  span(head + 2, 8, "k", (column) => !eyeColumns.includes(column), lean(2));
1296
1364
  span(head + 4, 2, "a", undefined, lean(4));
1297
1365
  span(10, SIZE, ".", (column) => column >= 6 && column <= 9);
1298
- span(10, top, mound, (column) => column >= 6 && column <= 9, shift);
1366
+ span(10, top, mound, (column) => column + shift >= 6 && column + shift <= 9, shift);
1299
1367
  span(11, 10, mound, undefined, shift);
1300
1368
  span(12, 12, mound, undefined, shift);
1301
1369
  for (const row of [13, 14, 15])
@@ -1958,6 +2026,7 @@ var digbyColours = (theme) => ({
1958
2026
  e: theme.text.feedback.error.base,
1959
2027
  s: theme.text.feedback.success.base
1960
2028
  });
2029
+ var DIGBY_UNDER = RGBA.fromValues(0, 0, 0, 0);
1961
2030
  function restBackground(theme, {
1962
2031
  selected,
1963
2032
  hovered
@@ -2200,7 +2269,7 @@ function TreePanel(props) {
2200
2269
  _$insert(_el$5, () => one().char);
2201
2270
  _$effect((_$p) => _$setProp(_el$5, "style", {
2202
2271
  fg: one().fg && tokens[one().fg],
2203
- bg: one().bg && tokens[one().bg]
2272
+ bg: one().bg ? tokens[one().bg] : DIGBY_UNDER
2204
2273
  }, _$p));
2205
2274
  return _el$5;
2206
2275
  })()
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "opencode-courier",
3
- "version": "0.8.0",
3
+ "version": "0.10.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>",