@bunshin-cc/bunshin 0.6.0 → 0.6.1

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.
@@ -3,7 +3,14 @@
3
3
  Bunshin's hooks talk to the model; this mod shows the person what they did. It draws the CLI's
4
4
  session feed (`.bunshin/v2/ui/<session>.json`, written by `packages/cli/src/v2/ui.ts`):
5
5
 
6
- - **above the prompt**: `◆ bunshin show-me skill picked by Jev (98%) lib@207 · 3 served · 1 learned`
6
+ - **above the prompt**: always one row, never wrapped. It shows the document served and why on the left, the
7
+ session's counts on the right, and reviews waiting in yellow:
8
+
9
+ ◆ bunshin show-me skill picked by Jev (98%) lib@207 · 3 served · 1 learned
10
+ ◇ bunshin maybe agent-grid · manage-agent-fleet skill lib@209 · 7 to review
11
+
12
+ On narrow terminals it drops whole pieces, least useful first: the counts, then the reason. It never drops
13
+ the reviews waiting, and only then trims the name with "…".
7
14
  - **`/bunshin`**: a pane with everything served this session and why, the lessons taken from it,
8
15
  recent events, and Learn now (`l`), Sync (`s`), Review (`r`)
9
16
  - **toasts**: library updates that reached the session, lessons learned, reviews waiting, the
@@ -224,22 +224,69 @@ function counts(feed) {
224
224
  ].filter(Boolean);
225
225
  }
226
226
 
227
- /** The band above the prompt: ◆ bunshin <latest served> why · counts. One line, two when narrow. */
227
+ /** The reason in the band's few columns: the feed's sentence, without its filler. */
228
+ const shortWhy = (why) =>
229
+ String(why ?? "")
230
+ .replace(/^both embedders agree, and Jev$/, "embedders + Jev agree")
231
+ .replace(/^both embedders agree$/, "embedders agree")
232
+ .replace(/, over the embedders$/, "");
233
+
234
+ /**
235
+ * The band above the prompt, always exactly one row (it never wraps into fragments):
236
+ *
237
+ * ◆ bunshin complete-ticket skill embedders agree lib@209 · 1 served · 7 to review
238
+ * ◇ bunshin maybe agent-grid · manage-agent-fleet skill lib@209 · 7 to review
239
+ *
240
+ * What does not fit is dropped whole, least useful first: the quiet counts, then the reason (never
241
+ * the reviews waiting, in yellow); only then does the name end in "…".
242
+ */
228
243
  function band(Box, Text, feed, columns) {
229
244
  const latest = feed.served.find((s) => s.how !== "hint");
230
245
  const hint = feed.served[0]?.how === "hint" ? feed.served.filter((s) => s.how === "hint" && s.at === feed.served[0].at) : [];
231
- const meta = counts(feed).join(" · ");
232
- const parts = [Text({ color: BRAND, bold: true, children: "◆ bunshin" })];
246
+ const served = feed.served.filter((s) => s.how !== "hint").length;
247
+ const quiet = [feed.library ? `lib@${feed.library}` : "", served ? `${served} served` : "", feed.lessons.length ? `${feed.lessons.length} learned` : ""].filter(Boolean).join(" · ");
248
+ const review = feed.review ? `${feed.review} to review` : "";
249
+ const name = latest ? label(latest) : hint.length ? `maybe ${hint.map(label).join(" · ")}` : "listening";
250
+ const why = latest?.why ? ` ${shortWhy(latest.why)}` : "";
251
+ // The cells each piece takes: "◆ bunshin " (11), the name, the reason; the counts plus a 2-cell gap.
252
+ const room = columns - 2;
253
+ const fits = (withWhy, withQuiet) => {
254
+ const right = [withQuiet ? quiet : "", review].filter(Boolean).join(" · ");
255
+ return 11 + name.length + (withWhy ? why.length : 0) + (right ? right.length + 2 : 0) <= room;
256
+ };
257
+ // Keep, in order: the name, the reviews waiting, the reason, the quiet counts.
258
+ const showWhy = !!why && fits(true, false);
259
+ const showQuiet = !!quiet && fits(showWhy, true);
260
+
261
+ const left = [];
233
262
  if (latest) {
234
- parts.push(Text({ bold: true, children: ` ${clip(label(latest), 40)}` }));
235
- if (columns >= 70) parts.push(Text({ dimColor: true, children: ` ${clip(latest.why, Math.max(12, columns - 70))}` }));
263
+ left.push(Text({ color: BRAND, bold: true, children: "◆ bunshin " }));
264
+ left.push(Text({ bold: true, children: label(latest) }));
265
+ if (showWhy) left.push(Text({ dimColor: true, children: why }));
236
266
  } else if (hint.length) {
237
- parts.push(Text({ dimColor: true, children: ` maybe: ${clip(hint.map(label).join(", "), Math.max(20, columns - 40))}` }));
267
+ left.push(Text({ color: BRAND, children: "◇ bunshin " }));
268
+ left.push(Text({ dimColor: true, italic: true, children: "maybe " }));
269
+ left.push(Text({ dimColor: true, children: hint.map(label).join(" · ") }));
238
270
  } else {
239
- parts.push(Text({ dimColor: true, children: " listening" }));
271
+ left.push(Text({ color: BRAND, children: "◇ bunshin " }));
272
+ left.push(Text({ dimColor: true, italic: true, children: "listening" }));
273
+ }
274
+ const right = [];
275
+ if (showQuiet) right.push(Text({ dimColor: true, children: quiet }));
276
+ if (review) {
277
+ if (right.length) right.push(Text({ dimColor: true, children: " · " }));
278
+ right.push(Text({ color: "yellow", children: review }));
240
279
  }
241
- if (meta && columns >= 50) parts.push(Text({ dimColor: true, children: ` ${meta}` }));
242
- return Box({ flexDirection: "row", paddingX: 1, children: parts });
280
+ return Box({
281
+ flexDirection: "row",
282
+ justifyContent: "space-between",
283
+ width: columns,
284
+ paddingX: 1,
285
+ children: [
286
+ Box({ flexShrink: 1, flexGrow: 1, overflow: "hidden", children: [Text({ wrap: "truncate-end", children: left })] }),
287
+ ...(right.length ? [Box({ flexShrink: 0, marginLeft: 2, children: [Text({ wrap: "truncate-end", children: right })] })] : []),
288
+ ],
289
+ });
243
290
  }
244
291
 
245
292
  function plainSummary(feed) {
@@ -268,14 +315,16 @@ function pane({ Box, Text, Button }, feed, panel, columns, rows) {
268
315
  body.push(heading(Text, "Served this session"));
269
316
  const served = feed.served.slice(0, Math.max(3, Math.floor(rows / 3)));
270
317
  if (!served.length) body.push(Text({ dimColor: true, children: " nothing yet" }));
318
+ // One line per row, never wrapped into fragments: time and how, the document, why.
271
319
  for (const s of served) {
272
320
  body.push(
273
- Box({
274
- flexDirection: "row",
321
+ Text({
322
+ wrap: "truncate-end",
275
323
  children: [
276
- Text({ dimColor: true, children: ` ${hhmm(s.at)} ${s.how.padEnd(4)} ` }),
277
- Text({ bold: s.how !== "hint", dimColor: s.how === "hint", children: clip(label(s), 36) }),
278
- Text({ dimColor: true, children: ` ${clip(s.why, Math.max(10, width - 52))}` }),
324
+ Text({ dimColor: true, children: ` ${hhmm(s.at)} ` }),
325
+ Text({ color: s.how === "hint" ? undefined : BRAND, dimColor: s.how === "hint", children: `${s.how === "hint" ? "◇" : "◆"} ` }),
326
+ Text({ bold: s.how !== "hint", dimColor: s.how === "hint", children: label(s) }),
327
+ Text({ dimColor: true, children: s.why ? ` ${shortWhy(s.why)}` : s.how === "hint" ? " maybe" : "" }),
279
328
  ],
280
329
  }),
281
330
  );
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bunshin-cc/bunshin",
3
- "version": "0.6.0",
3
+ "version": "0.6.1",
4
4
  "description": "Shared memory for company AI agents. Connect Codex, Claude Code, or Pi to Bunshin.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -16,15 +16,15 @@
16
16
  "node": ">=20"
17
17
  },
18
18
  "dependencies": {
19
- "@anthropic-ai/sdk": "^0.127.0",
19
+ "@anthropic-ai/sdk": "^0.129.0",
20
20
  "yaml": "^2.8.0"
21
21
  },
22
22
  "devDependencies": {
23
- "@types/node": "^22.15.0",
23
+ "@types/node": "^26.6.3",
24
24
  "esbuild": "^0.28.1",
25
- "tsx": "^4.20.0",
26
- "typescript": "^5.8.0",
27
- "vitest": "^3.2.0",
25
+ "tsx": "^4.23.15",
26
+ "typescript": "^7.0.2",
27
+ "vitest": "^5.0.2",
28
28
  "@bunshin/core": "0.1.0"
29
29
  },
30
30
  "license": "MIT",