@hypercarrier/rarebit 0.2.0 → 0.3.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.
@@ -0,0 +1,426 @@
1
+ import { messageTextBlocks, rarebitMetadata } from "./rarebit-core.mjs";
2
+ import { rarebitOccurrencePresentation } from "./rarebit-visual-language.mjs";
3
+ import { RECAP_ENTRY_TYPE } from "./rarebit-read-checkpoint.mjs";
4
+ import { observedAtLabel } from "./rarebit-recap.mjs";
5
+
6
+ const MODES = ["context", "all", "peek"];
7
+ const TOGGLE_KEYS = ["ctrl+alt+r", "ctrl+super+r"];
8
+ const CYCLE_KEYS = ["ctrl+alt+n", "ctrl+super+n"];
9
+ const DOCK_ROWS = 7;
10
+ let peers;
11
+ async function loadPeers() {
12
+ peers ??= Promise.all([import("@earendil-works/pi-tui"), import("@earendil-works/pi-coding-agent")])
13
+ .then(([tui, agent]) => ({ ...tui, getMarkdownTheme: agent.getMarkdownTheme }));
14
+ return peers;
15
+ }
16
+
17
+ function branchItems(ctx) {
18
+ const branch = ctx.sessionManager.getBranch();
19
+ const cutoffs = new Map();
20
+ for (const entry of branch) {
21
+ if (entry.type !== "compaction") continue;
22
+ const cutoff = entry.firstKeptEntryId ?? entry.id;
23
+ const markers = cutoffs.get(cutoff) ?? [];
24
+ markers.push(entry);
25
+ cutoffs.set(cutoff, markers);
26
+ }
27
+ const items = [];
28
+ branch.forEach((entry, order) => {
29
+ for (const marker of cutoffs.get(entry.id) ?? [])
30
+ items.push({ kind: "compaction", time: observedAtLabel(marker.timestamp, "host") });
31
+ const meta = rarebitMetadata(entry, order);
32
+ if (meta) items.push({ ...meta, kind: "rarebit", text: messageTextBlocks(entry.message).join("\n\n"), time: observedAtLabel(meta.timestamp, "host") });
33
+ });
34
+ return items;
35
+ }
36
+
37
+ /** One disposable reader. Selection stays in the semantic backend. */
38
+ class RarebitPane {
39
+ constructor(tui, theme, ctx, mode, primitives, transitions) {
40
+ Object.assign(this, { tui, theme, ctx, mode, primitives, transitions });
41
+ this.scrollTop = 0;
42
+ this.follow = true;
43
+ this.bodyHeight = 1;
44
+ this.body = [];
45
+ this.cache = new Map();
46
+ }
47
+ lines(items, width) {
48
+ const { theme: t, primitives: p } = this;
49
+ const lines = [];
50
+ const texts = new Set();
51
+ for (const item of items) {
52
+ if (lines.length) lines.push("");
53
+ if (item.kind === "compaction") {
54
+ lines.push(t.fg("muted", `── compaction ${item.time} · context restarts here ──`));
55
+ continue;
56
+ }
57
+ const presentation = rarebitOccurrencePresentation(item);
58
+ const color = item.role === "user" ? "success" : item.outcome === "continuation" ? "accent" : "muted";
59
+ lines.push(` ${t.fg(color, `${presentation.mark} ${presentation.label}`)} ${t.fg("dim", item.time)}`);
60
+ texts.add(item.text);
61
+ let md = this.cache.get(item.text);
62
+ if (!md) this.cache.set(item.text, md = new p.Markdown(item.text, 3, 0, p.getMarkdownTheme()));
63
+ lines.push(...md.render(width));
64
+ }
65
+ for (const text of this.cache.keys()) if (!texts.has(text)) this.cache.delete(text);
66
+ return lines;
67
+ }
68
+ matches() {
69
+ if (!this.search?.query) return [];
70
+ const { stripTerminalSequences, visibleWidth } = this.primitives;
71
+ const sensitive = /[A-Z]/.test(this.search.query);
72
+ const needle = sensitive ? this.search.query : this.search.query.toLowerCase();
73
+ const matches = [];
74
+ this.body.forEach((line, index) => {
75
+ const plain = stripTerminalSequences(line);
76
+ const hay = sensitive ? plain : plain.toLowerCase();
77
+ for (let at = hay.indexOf(needle); at >= 0; at = hay.indexOf(needle, at + Math.max(1, needle.length)))
78
+ matches.push({ line: index, col: visibleWidth(plain.slice(0, at)), width: visibleWidth(plain.slice(at, at + needle.length)) });
79
+ });
80
+ return matches;
81
+ }
82
+ highlight(line, matches, current) {
83
+ const { sliceByColumn, stripTerminalSequences, visibleWidth } = this.primitives;
84
+ let out = "", cursor = 0;
85
+ for (const m of matches) {
86
+ const text = stripTerminalSequences(sliceByColumn(line, m.col, m.width));
87
+ out += sliceByColumn(line, cursor, m.col - cursor) +
88
+ (m === current ? this.theme.inverse(text) : this.theme.bg("searchMatchBg", text));
89
+ cursor = m.col + m.width;
90
+ }
91
+ return out + sliceByColumn(line, cursor, Math.max(0, visibleWidth(line) - cursor));
92
+ }
93
+ render(width) {
94
+ const { theme: t, primitives: p } = this;
95
+ const items = branchItems(this.ctx);
96
+ const rows = Math.max(3, this.tui.terminal.rows);
97
+ const height = this.mode === "peek" ? Math.max(3, rows - Math.min(DOCK_ROWS, rows - 3)) : rows;
98
+ this.body = this.lines(items, width);
99
+ this.bodyHeight = height - 2;
100
+ const maxTop = Math.max(0, this.body.length - this.bodyHeight);
101
+ if (this.follow) this.scrollTop = maxTop;
102
+ this.scrollTop = Math.min(Math.max(0, this.scrollTop), maxTop);
103
+ const title = ` Rarebits · ${items.filter((item) => item.kind === "rarebit").length} on this branch `;
104
+ const top = t.fg("accent", t.bold(title)) + t.fg("dim", "─".repeat(Math.max(0, width - p.visibleWidth(title))));
105
+ const matches = this.matches();
106
+ const current = matches[this.search?.index];
107
+ const view = [];
108
+ for (let i = this.scrollTop; i < Math.min(this.body.length, this.scrollTop + this.bodyHeight); i++) {
109
+ const onLine = matches.filter((m) => m.line === i);
110
+ view.push(onLine.length ? this.highlight(this.body[i], onLine, current) : this.body[i]);
111
+ }
112
+ while (view.length < this.bodyHeight) view.push("");
113
+ let left;
114
+ if (this.prompt) left = t.fg("accent", ` ${this.prompt.forward ? "/" : "?"}${this.prompt.text}`) + "█";
115
+ else if (this.message) left = t.fg("warning", ` ${this.message}`);
116
+ else if (this.search && matches.length) left = t.fg("muted", ` ${this.search.forward ? "/" : "?"}${this.search.query} · ${this.search.index + 1}/${matches.length} · n/N`);
117
+ else left = t.fg("dim", this.mode === "all"
118
+ ? ` j/k · u/d · b/space · g/G · / ? n N search · esc/q close · ${CYCLE_KEYS[0]} next `
119
+ : ` keys go to the editor · wheel scrolls · ${TOGGLE_KEYS[0]} closes · replies appear when finished `);
120
+ const pos = this.body.length > this.bodyHeight ? `${this.scrollTop + 1}-${Math.min(this.body.length, this.scrollTop + this.bodyHeight)}/${this.body.length}` : "";
121
+ const bottom = left + " ".repeat(Math.max(0, width - p.visibleWidth(left) - pos.length)) + t.fg("muted", pos);
122
+ return [top, ...view, bottom].map((line) => {
123
+ const cut = p.truncateToWidth(line, width, "");
124
+ return t.bg("customMessageBg", cut + " ".repeat(Math.max(0, width - p.visibleWidth(cut))));
125
+ });
126
+ }
127
+ scrollBy(delta) {
128
+ const maxTop = Math.max(0, this.body.length - this.bodyHeight);
129
+ this.scrollTop = Math.min(Math.max(0, this.scrollTop + delta), maxTop);
130
+ this.follow = this.scrollTop >= maxTop;
131
+ this.tui.requestRender();
132
+ }
133
+ jump(forward, fromCurrent) {
134
+ const matches = this.matches();
135
+ if (!matches.length) {
136
+ this.message = `Pattern not found: ${this.search.query}`;
137
+ return this.tui.requestRender();
138
+ }
139
+ const current = matches[this.search.index];
140
+ const anchor = fromCurrent && current ? current : { line: forward ? this.scrollTop - 1 : this.scrollTop + this.bodyHeight, col: 0 };
141
+ let index = forward
142
+ ? matches.findIndex((m) => m.line > anchor.line || (m.line === anchor.line && m.col > anchor.col))
143
+ : matches.findLastIndex((m) => m.line < anchor.line || (m.line === anchor.line && m.col < anchor.col));
144
+ if (index < 0) {
145
+ index = forward ? 0 : matches.length - 1;
146
+ this.message = forward ? "search hit BOTTOM, continuing at TOP" : "search hit TOP, continuing at BOTTOM";
147
+ }
148
+ this.search.index = index;
149
+ const line = matches[index].line;
150
+ if (line < this.scrollTop || line >= this.scrollTop + this.bodyHeight)
151
+ this.scrollTop = Math.max(0, line - Math.floor(this.bodyHeight / 3));
152
+ this.follow = false;
153
+ this.tui.requestRender();
154
+ }
155
+ handleInput(data) {
156
+ const p = this.primitives;
157
+ const key = (expected) => p.matchesKey(data, expected);
158
+ if (TOGGLE_KEYS.some(key)) return this.transitions.toggle();
159
+ if (CYCLE_KEYS.some(key)) return this.transitions.cycle();
160
+ const text = p.decodeKittyPrintable(data) ?? (data.startsWith("\x1b") ? undefined : data);
161
+ this.message = undefined;
162
+ if (this.prompt) {
163
+ if (key("escape")) this.prompt = undefined;
164
+ else if (key("enter")) {
165
+ const { text: queryText, forward } = this.prompt;
166
+ this.prompt = undefined;
167
+ const query = queryText || this.search?.query;
168
+ if (query) { this.search = { query, forward, index: -1 }; return this.jump(forward, false); }
169
+ } else if (key("backspace")) {
170
+ if (!this.prompt.text) this.prompt = undefined;
171
+ else this.prompt.text = [...this.prompt.text].slice(0, -1).join("");
172
+ } else if (text && !/[\x00-\x1f\x7f]/.test(text)) this.prompt.text += text;
173
+ return this.tui.requestRender();
174
+ }
175
+ if (text === "/" || text === "?") { this.prompt = { text: "", forward: text === "/" }; return this.tui.requestRender(); }
176
+ if (text === "n" || text === "N") {
177
+ if (this.search) this.jump(text === "n" ? this.search.forward : !this.search.forward, true);
178
+ return;
179
+ }
180
+ if (key("escape") && this.search) { this.search = undefined; return this.tui.requestRender(); }
181
+ if (key("escape") || text === "q") return this.transitions.close();
182
+ if (key("up") || text === "k") return this.scrollBy(-1);
183
+ if (key("down") || text === "j") return this.scrollBy(1);
184
+ const page = Math.max(1, this.bodyHeight - 2), half = Math.max(1, Math.floor(this.bodyHeight / 2));
185
+ if (key("pageUp") || text === "b") return this.scrollBy(-page);
186
+ if (key("pageDown") || text === " ") return this.scrollBy(page);
187
+ if (key("ctrl+u") || text === "u") return this.scrollBy(-half);
188
+ if (key("ctrl+d") || text === "d") return this.scrollBy(half);
189
+ if (key("home") || text === "g") return this.scrollBy(-Infinity);
190
+ if (key("end") || text === "G") return this.scrollBy(Infinity);
191
+ }
192
+ handleMouse(event) {
193
+ if (event.type !== "wheel" || !event.wheelDelta) return undefined;
194
+ this.scrollBy(event.wheelDelta);
195
+ return { handled: true };
196
+ }
197
+ invalidate() { this.cache.clear(); }
198
+ }
199
+
200
+ const typeName = (component) => component?.constructor?.name;
201
+ /**
202
+ * Unsupported by Pi and can break on any release. This adapter relies on:
203
+ * tui.children[0] being the document container and its last child being chat;
204
+ * mutable Container.render/children and mouseLayout; built-in constructor names;
205
+ * AssistantMessageComponent.lastMessage, hideThinkingBlock, hiddenThinkingLabel,
206
+ * setHideThinkingBlock, contentContainer.children; hidden thinking chrome as Text
207
+ * or MouseRegion.child Text; ThemedText (or older Text) notice classes and text;
208
+ * CustomEntryComponent.entry.customType. It changes display
209
+ * only. Pi still owns context, compaction, transcript layout, and Session data.
210
+ */
211
+ function tryAttach(tui, p, unavailable) {
212
+ const document = tui.children?.[0];
213
+ const chat = document?.children?.at(-1);
214
+ const validAssistant = (child) => typeName(child) !== "AssistantMessageComponent" ||
215
+ (child.lastMessage?.role === "assistant" && typeof child.hideThinkingBlock === "boolean" &&
216
+ typeof child.hiddenThinkingLabel === "string" && typeof child.setHideThinkingBlock === "function" &&
217
+ typeName(child.contentContainer) === "Container" && Array.isArray(child.contentContainer.children));
218
+ if (typeName(document) !== "Container" || typeName(chat) !== "Container" || !Array.isArray(chat.children) ||
219
+ typeof chat.render !== "function" || !chat.children.every((child) => typeof child.render === "function" && validAssistant(child))) return;
220
+ const original = chat.render;
221
+ try {
222
+ const lines = original.call(chat, Math.max(1, tui.terminal.columns));
223
+ if (!Array.isArray(lines) || !lines.every((line) => typeof line === "string")) return;
224
+ } catch { return; }
225
+ const isNotice = (child) => ["ThemedText", "Text"].includes(typeName(child));
226
+ const noticeText = (child) => p.stripTerminalSequences(String(child.text ?? ""));
227
+ const initialNotices = new WeakMap(chat.children.filter(isNotice).map((child) => [child, noticeText(child)]));
228
+ const previousMouseLayout = chat.mouseLayout;
229
+ let ownedMouseLayout;
230
+ const thinking = new Map();
231
+ let disposed = false;
232
+ const restore = (child, previous) => {
233
+ if (child.hideThinkingBlock === true) child.setHideThinkingBlock(previous);
234
+ };
235
+ const dispose = () => {
236
+ if (disposed) return;
237
+ disposed = true;
238
+ if (chat.render === wrapper) {
239
+ chat.render = original;
240
+ if (ownedMouseLayout && chat.mouseLayout === ownedMouseLayout) chat.mouseLayout = previousMouseLayout;
241
+ for (const [child, previous] of thinking) if (chat.children.includes(child)) restore(child, previous);
242
+ }
243
+ thinking.clear();
244
+ tui.requestRender(true);
245
+ };
246
+ const renderFiltered = function (width) {
247
+ if (disposed) return original.call(this, width);
248
+ for (const child of thinking.keys()) if (!this.children.includes(child)) thinking.delete(child);
249
+ if (!this.children.every((child) => typeof child.render === "function" && validAssistant(child))) {
250
+ dispose();
251
+ queueMicrotask(unavailable);
252
+ return original.call(this, width);
253
+ }
254
+ const lines = [], mouseChildren = [];
255
+ for (const child of this.children) {
256
+ const kind = typeName(child);
257
+ // Pi reuses its last status component for consecutive notices. Rendering
258
+ // hydrates ThemedText; compare unwrapped prose so theme/width changes do
259
+ // not reveal an unchanged earlier notice.
260
+ const noticeLines = isNotice(child) ? child.render(width) : undefined;
261
+ const freshNotice = noticeLines && (!initialNotices.has(child) || initialNotices.get(child) !== noticeText(child));
262
+ const keep = kind === "UserMessageComponent" || kind === "CompactionSummaryMessageComponent" ||
263
+ freshNotice ||
264
+ (kind === "CustomEntryComponent" && child.entry?.customType === RECAP_ENTRY_TYPE) ||
265
+ (kind === "AssistantMessageComponent" && rarebitMetadata({ type: "message", message: child.lastMessage }) !== null);
266
+ if (keep && kind === "AssistantMessageComponent" && !thinking.has(child)) {
267
+ thinking.set(child, child.hideThinkingBlock);
268
+ child.setHideThinkingBlock(true);
269
+ }
270
+ let childLines = [];
271
+ if (keep && kind === "AssistantMessageComponent") {
272
+ // Pi renders selected prose as Markdown. Thinking placeholders are
273
+ // separate Text children (wrapped by MouseRegion on newer Pi). Exclude
274
+ // those components only while rendering; never compare prose to a label.
275
+ const content = child.contentContainer;
276
+ const originalChildren = content.children;
277
+ const blocks = child.lastMessage.content;
278
+ const hasThinking = blocks.some((block) => block.type === "thinking" && block.thinking.trim());
279
+ const thinkingChrome = (part) => typeName(part) === "Text" ||
280
+ (typeName(part) === "MouseRegion" && typeName(part.child) === "Text");
281
+ const filtered = hasThinking ? originalChildren.filter((part) => !thinkingChrome(part)) : originalChildren;
282
+ const proseBlocks = blocks.filter((block) => block.type === "text" && block.text.trim()).length;
283
+ if (filtered.some((part) => !["Markdown", "Spacer"].includes(typeName(part))) ||
284
+ filtered.filter((part) => typeName(part) === "Markdown").length !== proseBlocks)
285
+ throw new Error("Pi assistant content structure changed");
286
+ content.children = filtered;
287
+ try { childLines = child.render(width); }
288
+ finally { if (content.children === filtered) content.children = originalChildren; }
289
+ } else if (keep) childLines = noticeLines ?? child.render(width);
290
+ if (!Array.isArray(childLines) || !childLines.every((line) => typeof line === "string")) throw new Error("Pi transcript render shape changed");
291
+ if (keep && kind === "AssistantMessageComponent") {
292
+ const plain = (line) => p.stripTerminalSequences(line).trim();
293
+ while (childLines.length && !plain(childLines[0])) childLines.shift();
294
+ while (childLines.length && !plain(childLines.at(-1))) childLines.pop();
295
+ }
296
+ if (childLines.length && lines.length) childLines = ["", ...childLines];
297
+ mouseChildren.push({ component: child, height: childLines.length });
298
+ lines.push(...childLines);
299
+ }
300
+ if ("mouseLayout" in this) this.mouseLayout = ownedMouseLayout = { width, children: mouseChildren };
301
+ return lines;
302
+ };
303
+ const wrapper = function (width) {
304
+ try { return renderFiltered.call(this, width); }
305
+ catch {
306
+ dispose();
307
+ queueMicrotask(unavailable);
308
+ return original.call(this, width);
309
+ }
310
+ };
311
+ chat.render = wrapper;
312
+ return dispose;
313
+ }
314
+
315
+ /** Session-scoped display controller. No Session writes or model operations. */
316
+ export function createRarebitViewController() {
317
+ let preferred = "all", active, activation, epoch = 0;
318
+ const notify = (ctx, message) => ctx?.hasUI && ctx.ui.notify?.(message, "warning");
319
+ const stop = () => {
320
+ epoch++;
321
+ const old = activation;
322
+ activation = undefined;
323
+ active = undefined;
324
+ try { old?.dispose?.(); }
325
+ finally {
326
+ old?.ctx.ui.setStatus?.("rarebit-view", undefined);
327
+ old?.tui?.requestRender(true);
328
+ }
329
+ };
330
+ const activate = async (ctx, mode) => {
331
+ stop();
332
+ if (ctx?.mode !== "tui" || !ctx.hasUI || typeof ctx.ui?.custom !== "function") {
333
+ notify(ctx, "Rarebit view is unavailable on this Pi version");
334
+ return;
335
+ }
336
+ preferred = mode;
337
+ const token = epoch;
338
+ const record = { ctx, dispose: () => {} };
339
+ activation = record;
340
+ const current = () => token === epoch && activation === record;
341
+ try {
342
+ const p = await loadPeers();
343
+ if (!current()) return;
344
+ for (const name of ["Markdown", "matchesKey", "decodeKittyPrintable", "sliceByColumn", "stripTerminalSequences", "truncateToWidth", "visibleWidth", "getMarkdownTheme"])
345
+ if (typeof p[name] !== "function") throw new Error("unavailable");
346
+ if (typeof ctx.sessionManager?.getBranch !== "function") throw new Error("unavailable");
347
+ const markActive = (tui) => {
348
+ if (!current()) return;
349
+ record.tui = tui;
350
+ active = mode;
351
+ ctx.ui.setStatus?.("rarebit-view", `Rarebit view: ${mode} · next ${CYCLE_KEYS[0]} · off ${TOGGLE_KEYS[0]}`);
352
+ tui.requestRender(true);
353
+ };
354
+ const fallback = () => {
355
+ if (!current()) return;
356
+ notify(ctx, "Context view is unavailable on this Pi version; showing all.");
357
+ void activate(ctx, "all");
358
+ };
359
+ if (mode === "context") {
360
+ let tui;
361
+ await ctx.ui.custom((host, _theme, _kb, done) => {
362
+ tui = host;
363
+ done();
364
+ return { render: () => [], invalidate() {} };
365
+ });
366
+ if (!current()) return;
367
+ const dispose = tui && tryAttach(tui, p, fallback);
368
+ if (!dispose) return fallback();
369
+ record.dispose = dispose;
370
+ markActive(tui);
371
+ return;
372
+ }
373
+ let close, timer;
374
+ const ready = new Promise((resolve, reject) => {
375
+ timer = setTimeout(() => reject(new Error("unavailable")), 1000);
376
+ record.dispose = () => { clearTimeout(timer); close?.(); resolve(); };
377
+ const pending = ctx.ui.custom((tui, theme, _kb, done) => {
378
+ close = done;
379
+ if (!current()) done();
380
+ if (typeof tui.showOverlay !== "function" || typeof theme.inverse !== "function") throw new Error("unavailable");
381
+ record.tui = tui;
382
+ return new RarebitPane(tui, theme, ctx, mode, p, {
383
+ close: stop, toggle: () => void controller.toggle(ctx), cycle: () => void controller.cycle(ctx),
384
+ });
385
+ }, {
386
+ overlay: true,
387
+ overlayOptions: { row: 0, col: 0, width: "100%", maxHeight: "100%", nonCapturing: mode === "peek" },
388
+ onHandle: (handle) => {
389
+ if (!current()) { close?.(); return resolve(); }
390
+ if (typeof handle.isFocused !== "function" || (mode === "peek" && handle.isFocused())) return reject(new Error("unavailable"));
391
+ clearTimeout(timer);
392
+ markActive(record.tui);
393
+ resolve();
394
+ },
395
+ });
396
+ Promise.resolve(pending).then(() => { if (current()) stop(); resolve(); }, reject);
397
+ });
398
+ await ready;
399
+ } catch {
400
+ if (current()) { stop(); notify(ctx, "Rarebit view is unavailable on this Pi version"); }
401
+ }
402
+ };
403
+ const controller = {
404
+ get activeMode() { return active ?? "off"; },
405
+ async command(ctx, mode) {
406
+ if (mode === "off") return stop();
407
+ if (!mode) {
408
+ stop();
409
+ if (ctx?.mode !== "tui" || !ctx.hasUI) return;
410
+ const token = epoch;
411
+ const picked = await ctx.ui.select?.("Rarebit view", [
412
+ "all — All Rarebits across compactions", "peek — All Rarebits with the editor live",
413
+ "context — Current context only (experimental)", "off — Normal transcript",
414
+ ]);
415
+ if (token !== epoch || !picked) return;
416
+ mode = picked.split(" ")[0];
417
+ }
418
+ if (MODES.includes(mode)) await activate(ctx, mode);
419
+ },
420
+ async toggle(ctx) { if (activation) stop(); else await activate(ctx, preferred); },
421
+ async cycle(ctx) { await activate(ctx, MODES[(MODES.indexOf(preferred) + 1) % MODES.length]); },
422
+ refresh() { activation?.tui?.requestRender(); },
423
+ dispose: stop,
424
+ };
425
+ return controller;
426
+ }
package/src/types.d.ts CHANGED
@@ -272,6 +272,7 @@ export function composeRarebitSummaryDerivationInput(
272
272
  summaryPrompt?: unknown;
273
273
  lifecycleBoundary?: "owner_request" | "agent_settled" | "manual";
274
274
  maxPromptChars?: number;
275
+ readCheckpoint?: { coveredEntryId: string } | null;
275
276
  }): {
276
277
  prompt: string;
277
278
  promptIdentity: string | null;
@@ -285,6 +286,7 @@ export function composeRarebitSummaryPrompt(
285
286
  summaryPrompt?: unknown;
286
287
  lifecycleBoundary?: "owner_request" | "agent_settled" | "manual";
287
288
  maxPromptChars?: number;
289
+ readCheckpoint?: { coveredEntryId: string } | null;
288
290
  },
289
291
  ): string;
290
292
  export function composeRarebitTitlePrompt(
@@ -380,7 +382,8 @@ type RarebitSummaryReceiptCommon = {
380
382
  implementationVersion:
381
383
  | "hc-rarebit-summary-v4"
382
384
  | "hc-rarebit-summary-v5"
383
- | "hc-rarebit-summary-v6";
385
+ | "hc-rarebit-summary-v6"
386
+ | "hc-rarebit-summary-v7";
384
387
  synthesisMode: "forced" | "automatic";
385
388
  inputCoveragePolicy: RarebitInputCoveragePolicy;
386
389
  promptVersion: string;
@@ -445,7 +448,7 @@ export type RarebitNativeObservation =
445
448
  | { availability: "missing" | "unreadable" };
446
449
 
447
450
  export const RAREBIT_SUMMARY_SCHEMA_VERSION: 4;
448
- export const RAREBIT_SUMMARY_IMPLEMENTATION_VERSION: "hc-rarebit-summary-v6";
451
+ export const RAREBIT_SUMMARY_IMPLEMENTATION_VERSION: "hc-rarebit-summary-v7";
449
452
  export const RAREBIT_INPUT_CHARS_PER_TOKEN: 4;
450
453
  export const DEFAULT_RAREBIT_MAX_PROMPT_CHARS: 256000;
451
454
  export function processRarebitSummary(
@@ -285,12 +285,18 @@ test("only agent_settled ends an active request generation; exact start/manual c
285
285
  });
286
286
  assert.equal(state.syncState, "request_current");
287
287
  }
288
+ const exact = [owner, continuation];
289
+ const settled = receipt({ occurrences: exact, boundary: "agent_settled", observedAt: "2026-07-25T10:03:00.000Z" });
290
+ const newerManual = receipt({ occurrences: exact, boundary: "manual", observedAt: "2026-07-25T10:04:00.000Z" });
291
+ const promoted = projectRarebitArtifactState({
292
+ native: native(exact),
293
+ materialization: { availability: "available", records: [settled, newerManual] },
294
+ });
295
+ assert.equal(promoted.syncState, "assessment_current");
296
+ assert.equal(promoted.receiptRef.lifecycleBoundary, "manual");
288
297
  const manualOnly = projectRarebitArtifactState({
289
298
  native: native([owner]),
290
- materialization: {
291
- availability: "available",
292
- records: [receipt({ boundary: "manual" })],
293
- },
299
+ materialization: { availability: "available", records: [receipt({ boundary: "manual" })] },
294
300
  });
295
301
  assert.equal(manualOnly.syncState, "assessment_current");
296
302
  });
@@ -14,11 +14,11 @@ const values = (prefix) =>
14
14
  test("one grammar derives command description and usage", () => {
15
15
  assert.equal(
16
16
  rarebitCommandDescription(),
17
- "Rarebit menu/settings/help/status/recall/fork/config/auto-title/title/recap/summarize",
17
+ "Rarebit menu/settings/view/help/status/recall/fork/config/auto-title/title/recap/got-it/summarize",
18
18
  );
19
19
  assert.equal(
20
20
  rarebitCommandUsage(),
21
- "Usage: /rarebit [menu|settings|help|status|recall|fork|config|auto-title|title|recap|summarize]",
21
+ "Usage: /rarebit [menu|settings|view|help|status|recall|fork|config|auto-title|title|recap|got-it|summarize]",
22
22
  );
23
23
  assert.equal(
24
24
  rarebitCommandUsage("config"),
@@ -44,6 +44,10 @@ test("parser accepts exactly the command grammar", () => {
44
44
  subcommand: "menu",
45
45
  arguments: [],
46
46
  });
47
+ for (const input of ["view", "view context", "view all", "view peek", "view off"])
48
+ assert.equal(parseRarebitCommand(input).ok, true);
49
+ for (const input of ["view keys", "view native-hide", "view all extra"])
50
+ assert.equal(parseRarebitCommand(input).ok, false);
47
51
  assert.equal(parseRarebitCommand("fork").ok, true);
48
52
  assert.equal(parseRarebitCommand("fork --max-token-length 64000").ok, true);
49
53
  assert.equal(parseRarebitCommand("config").ok, true);
@@ -53,6 +57,8 @@ test("parser accepts exactly the command grammar", () => {
53
57
  assert.equal(parseRarebitCommand("auto-title off").ok, true);
54
58
  assert.equal(parseRarebitCommand("title").ok, true);
55
59
  assert.equal(parseRarebitCommand("summarize").ok, true);
60
+ assert.equal(parseRarebitCommand("got-it").ok, true);
61
+ assert.equal(parseRarebitCommand("got-it now").ok, false);
56
62
  assert.deepEqual(
57
63
  parseRarebitCommand(
58
64
  "recall Review the evidence, then continue exactly. ",
@@ -95,6 +101,7 @@ test("autocomplete covers subcommands and discrete nested arguments", () => {
95
101
  assert.deepEqual(values(""), [
96
102
  "menu",
97
103
  "settings",
104
+ "view",
98
105
  "help",
99
106
  "status",
100
107
  "recall",
@@ -103,8 +110,12 @@ test("autocomplete covers subcommands and discrete nested arguments", () => {
103
110
  "auto-title",
104
111
  "title",
105
112
  "recap",
113
+ "got-it",
106
114
  "summarize",
107
115
  ]);
116
+ assert.deepEqual(values("view "), ["view context", "view all", "view peek", "view off"]);
117
+ assert.deepEqual(values("view p"), ["view peek"]);
118
+ assert.equal(values("view all "), null);
108
119
  assert.deepEqual(values("s"), ["settings", "status", "summarize"]);
109
120
  assert.deepEqual(values("config "), [
110
121
  "config max_rarebit_ratio ",
@@ -160,6 +160,56 @@ test("bounded Summary input retains newest evidence and declares every omission"
160
160
  assert.equal(DEFAULT_RAREBIT_MAX_PROMPT_CHARS, 256_000);
161
161
  });
162
162
 
163
+ test("read checkpoints stay metadata beside a retained evidence suffix and do not replace pending approval", () => {
164
+ const selection = selectRarebits([
165
+ entry("early", { role: "user", content: `EARLY-${"e".repeat(5_000)}` }),
166
+ entry("approval", { role: "assistant", stopReason: "stop", content: `${"p".repeat(5_000)}-APPROVAL-STILL-NEEDED` }),
167
+ ]);
168
+ const input = composeRarebitSummaryDerivationInput(selection, {
169
+ maxPromptChars: 6_000,
170
+ readCheckpoint: { coveredEntryId: "approval", coveredOrder: 1 },
171
+ });
172
+ const lines = input.prompt.split("BEGIN_RAREBIT_MESSAGES_JSONL\n")[1].split("\nEND_RAREBIT_MESSAGES_JSONL")[0].split("\n").map(JSON.parse);
173
+ assert.ok(input.prompt.length <= 6_000);
174
+ assert.equal(input.coverage.totalMessageCount, 2);
175
+ assert.equal(input.coverage.includedMessageCount, 1);
176
+ assert.ok(lines.some((line) => line.type === "recap_read"));
177
+ assert.ok(lines.some((line) => line.text?.includes("APPROVAL-STILL-NEEDED")));
178
+ assert.doesNotMatch(input.prompt, /EARLY-/);
179
+ assert.match(input.prompt, /omitted|trimmed/);
180
+ });
181
+
182
+ test("read boundaries stay before, at, or after the retained evidence suffix", () => {
183
+ const selection = selectRarebits(Array.from({ length: 4 }, (_, index) =>
184
+ entry(`u${index}`, { role: "user", content: `evidence-${index}-${"x".repeat(1_200)}` })));
185
+ for (const coveredIndex of [0, 1, 3]) {
186
+ const input = composeRarebitSummaryDerivationInput(selection, {
187
+ maxPromptChars: 7_500,
188
+ readCheckpoint: { coveredEntryId: `u${coveredIndex}`, coveredOrder: coveredIndex },
189
+ });
190
+ const body = input.prompt.split("BEGIN_RAREBIT_MESSAGES_JSONL\n")[1].split("\nEND_RAREBIT_MESSAGES_JSONL")[0];
191
+ const lines = body.split("\n").map(JSON.parse);
192
+ const ordered = lines.filter((line) => !line.omitted);
193
+ const readIndex = ordered.findIndex((line) => line.type === "recap_read");
194
+ const evidence = ordered.filter((line) => line.type !== "recap_read");
195
+ assert.ok(input.coverage.omittedMessageCount > 0);
196
+ assert.ok(input.prompt.length <= 7_500);
197
+ if (coveredIndex === 0) assert.equal(readIndex, 0, "a boundary before the suffix leads it");
198
+ if (coveredIndex === 1) assert.equal(readIndex, 1, "a boundary at the first retained message follows it");
199
+ if (coveredIndex === 3) assert.equal(readIndex, evidence.length, "a boundary after the suffix follows all retained messages");
200
+ }
201
+ });
202
+
203
+ test("fixed prompt overflow preserves evidence when even empty trimmed input cannot fit", () => {
204
+ const text = "PENDING-APPROVAL-" + "x".repeat(500);
205
+ const selection = selectRarebits([entry("u", { role: "user", content: text })]);
206
+ const input = composeRarebitSummaryDerivationInput(selection, { maxPromptChars: 3_100 });
207
+ assert.ok(input.prompt.length > 3_100);
208
+ assert.equal(input.coverage.omittedTextChars, 0);
209
+ assert.match(input.prompt, /PENDING-APPROVAL/);
210
+ assert.doesNotMatch(input.prompt, /leading characters.*trimmed/);
211
+ });
212
+
163
213
  test("one oversized newest message is tail-bounded with an explicit character omission", () => {
164
214
  const selection = selectRarebits([
165
215
  entry("huge", {