@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.
- package/CHANGELOG.md +35 -0
- package/README.md +322 -161
- package/RUNBOOK.md +7 -5
- package/package.json +1 -1
- package/schemas/rarebit.schema.json +2 -1
- package/scripts/e2e-recap-pi.mjs +10 -8
- package/scripts/e2e-v020-pi.mjs +12 -10
- package/scripts/e2e-view-pi.mjs +500 -0
- package/scripts/experiment-recap-fullscreen.mjs +56 -0
- package/src/extension.mjs +85 -15
- package/src/rarebit-artifact-state.mjs +8 -12
- package/src/rarebit-command.mjs +10 -0
- package/src/rarebit-core.mjs +42 -21
- package/src/rarebit-read-checkpoint.mjs +59 -0
- package/src/rarebit-recap.mjs +183 -125
- package/src/rarebit-service.mjs +5 -2
- package/src/rarebit-settings-ui.mjs +2 -0
- package/src/rarebit-view.mjs +426 -0
- package/src/types.d.ts +5 -2
- package/test/rarebit-artifact-state.test.mjs +10 -4
- package/test/rarebit-command.test.mjs +13 -2
- package/test/rarebit-core.test.mjs +50 -0
- package/test/rarebit-recap.test.mjs +157 -8
- package/test/rarebit-settings-ui.test.mjs +34 -22
- package/test/rarebit-sidecar-protocol.test.mjs +20 -0
- package/test/rarebit-v020.test.mjs +16 -11
- package/test/rarebit-view.test.mjs +270 -0
- package/test/recap-read-checkpoint.test.mjs +126 -0
package/src/rarebit-recap.mjs
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { RECAP_ENTRY_TYPE, RECAP_READ_TYPE, latestRecap, recapIsRead } from "./rarebit-read-checkpoint.mjs";
|
|
2
|
+
import { exactSelectionApplies, projectRarebitArtifactState } from "./rarebit-artifact-state.mjs";
|
|
2
3
|
import { selectRarebits } from "./rarebit-core.mjs";
|
|
3
4
|
import {
|
|
4
5
|
DEFAULT_RAREBIT_RECAP_DELAY_MS,
|
|
@@ -28,65 +29,48 @@ function branchSnapshot(ctx) {
|
|
|
28
29
|
return Array.isArray(branch) ? branch.slice() : [];
|
|
29
30
|
}
|
|
30
31
|
|
|
31
|
-
function
|
|
32
|
-
if (receipt?.status !== "ok") return
|
|
32
|
+
function receiptCoverage(receipt, ctx) {
|
|
33
|
+
if (receipt?.status !== "ok") return { state: null, coverage: null };
|
|
33
34
|
const identity = sessionIdentity(ctx);
|
|
34
|
-
if (!identity.sessionId || !identity.sessionFile) return
|
|
35
|
-
|
|
36
|
-
const
|
|
37
|
-
|
|
35
|
+
if (!identity.sessionId || !identity.sessionFile || receipt.sessionId !== identity.sessionId) return { state: null, coverage: null };
|
|
36
|
+
const selection = selectRarebits(branchSnapshot(ctx));
|
|
37
|
+
const current = projectRarebitArtifactState({
|
|
38
|
+
native: { availability: "available", sessionId: identity.sessionId,
|
|
39
|
+
selection: { ...selection, selectorVersion: selection.manifest.selectorVersion } },
|
|
40
|
+
materialization: { availability: "available", records: [receipt] },
|
|
41
|
+
});
|
|
42
|
+
if (current.receiptRef?.jobId !== receipt.jobId ||
|
|
43
|
+
!["request_current", "assessment_current"].includes(current.syncState)) return { state: current, coverage: null };
|
|
44
|
+
if (current.applicability === "exact_selection" && exactSelectionApplies(receipt.selection, selection))
|
|
45
|
+
return { state: current, coverage: { coveredEntryId: selection.occurrences.at(-1)?.sourceEntryId, selectionHash: receipt.selection.manifestHash } };
|
|
46
|
+
if (current.applicability === "request_generation") {
|
|
47
|
+
const covered = selection.occurrences[receipt.selection.occurrenceCount - 1];
|
|
48
|
+
return { state: current, coverage: covered ? { coveredEntryId: covered.sourceEntryId, selectionHash: receipt.selection.manifestHash } : null };
|
|
49
|
+
}
|
|
50
|
+
return { state: current, coverage: null };
|
|
38
51
|
}
|
|
39
52
|
|
|
40
|
-
function observedAtLabel(observedAt, timezone) {
|
|
53
|
+
export function observedAtLabel(observedAt, timezone = DEFAULT_RAREBIT_RECAP_TIMEZONE) {
|
|
54
|
+
if (observedAt === null || observedAt === undefined) return "unknown";
|
|
41
55
|
const date = new Date(observedAt);
|
|
42
|
-
if (Number.isNaN(date.valueOf())) return
|
|
56
|
+
if (Number.isNaN(date.valueOf())) return "unknown";
|
|
43
57
|
const usesHostTimezone = timezone === DEFAULT_RAREBIT_RECAP_TIMEZONE;
|
|
44
|
-
const
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
};
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
dateText = new Intl.DateTimeFormat(undefined, dateOptions).format(date);
|
|
52
|
-
} catch {
|
|
53
|
-
dateText = new Intl.DateTimeFormat(undefined, {
|
|
54
|
-
dateStyle: "medium",
|
|
55
|
-
timeStyle: "short",
|
|
56
|
-
}).format(date);
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
let zoneText;
|
|
58
|
+
const options = usesHostTimezone ? {} : { timeZone: timezone };
|
|
59
|
+
const parts = new Intl.DateTimeFormat("en-US", {
|
|
60
|
+
month: "2-digit", day: "2-digit", hour: "2-digit", minute: "2-digit",
|
|
61
|
+
hourCycle: "h23", ...options,
|
|
62
|
+
}).formatToParts(date);
|
|
63
|
+
const value = (type) => parts.find((part) => part.type === type)?.value ?? "00";
|
|
64
|
+
let offset = "GMT";
|
|
60
65
|
try {
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
timeZoneName: "shortOffset",
|
|
66
|
-
...(usesHostTimezone ? {} : { timeZone: timezone }),
|
|
67
|
-
};
|
|
68
|
-
zoneText = new Intl.DateTimeFormat(undefined, zoneOptions)
|
|
69
|
-
.formatToParts(date)
|
|
70
|
-
.find((part) => part.type === "timeZoneName")?.value;
|
|
71
|
-
} catch {
|
|
72
|
-
zoneText = undefined;
|
|
73
|
-
}
|
|
74
|
-
let resolvedTimezone;
|
|
75
|
-
try {
|
|
76
|
-
resolvedTimezone = new Intl.DateTimeFormat(
|
|
77
|
-
undefined,
|
|
78
|
-
usesHostTimezone ? {} : { timeZone: timezone },
|
|
79
|
-
).resolvedOptions().timeZone;
|
|
80
|
-
} catch {
|
|
81
|
-
resolvedTimezone = undefined;
|
|
82
|
-
}
|
|
83
|
-
const zoneLabel = usesHostTimezone
|
|
84
|
-
? `host${resolvedTimezone ? `: ${resolvedTimezone}` : ""}`
|
|
85
|
-
: resolvedTimezone ?? timezone;
|
|
86
|
-
return `${dateText} (${zoneLabel}${zoneText ? `, ${zoneText}` : ""})`;
|
|
66
|
+
offset = new Intl.DateTimeFormat("en", { timeZoneName: "shortOffset", ...options })
|
|
67
|
+
.formatToParts(date).find((part) => part.type === "timeZoneName")?.value ?? "GMT";
|
|
68
|
+
} catch {}
|
|
69
|
+
return `${value("month")}/${value("day")} ${value("hour")}:${value("minute")}(${offset})`;
|
|
87
70
|
}
|
|
88
71
|
|
|
89
72
|
let recapWidgetComponents;
|
|
73
|
+
let loadedComponents;
|
|
90
74
|
|
|
91
75
|
async function loadRecapWidgetComponents() {
|
|
92
76
|
if (!recapWidgetComponents) {
|
|
@@ -97,7 +81,9 @@ async function loadRecapWidgetComponents() {
|
|
|
97
81
|
Container: tui.Container,
|
|
98
82
|
Text: tui.Text,
|
|
99
83
|
DynamicBorder: agent.DynamicBorder,
|
|
100
|
-
|
|
84
|
+
truncateToWidth: tui.truncateToWidth,
|
|
85
|
+
visibleWidth: tui.visibleWidth,
|
|
86
|
+
})).then((components) => (loadedComponents = components));
|
|
101
87
|
}
|
|
102
88
|
return recapWidgetComponents;
|
|
103
89
|
}
|
|
@@ -107,12 +93,70 @@ function summaryContent(
|
|
|
107
93
|
{ timezone = DEFAULT_RAREBIT_RECAP_TIMEZONE } = {},
|
|
108
94
|
) {
|
|
109
95
|
const presentation = rarebitSummaryPresentation(receipt.sessionStatus);
|
|
110
|
-
const mark = presentation.mark
|
|
111
|
-
const heading = `${mark}Recap
|
|
96
|
+
const mark = presentation.mark ?? "";
|
|
97
|
+
const heading = `${mark}Recap·${presentation.label.replaceAll(" ", "")}·${observedAtLabel(receipt.observedAt, timezone)}`;
|
|
112
98
|
const summary = String(receipt.summary ?? "").trim();
|
|
113
99
|
return { heading, summary };
|
|
114
100
|
}
|
|
115
101
|
|
|
102
|
+
function recapComponent(content, theme) {
|
|
103
|
+
const border = (text) => theme.fg("borderMuted", text);
|
|
104
|
+
const widget = new loadedComponents.Container();
|
|
105
|
+
const summary = new loadedComponents.Text("", 1, 0);
|
|
106
|
+
const compactSummary = new loadedComponents.Text("", 0, 0);
|
|
107
|
+
let previousSummary;
|
|
108
|
+
let previousCompactSummary;
|
|
109
|
+
const themed = (text, color, component, previous) => {
|
|
110
|
+
const rendered = theme.fg(color, text);
|
|
111
|
+
if (rendered !== previous.value) {
|
|
112
|
+
previous.value = rendered;
|
|
113
|
+
component.setText(rendered);
|
|
114
|
+
}
|
|
115
|
+
};
|
|
116
|
+
const themedHeading = {
|
|
117
|
+
render(width) {
|
|
118
|
+
const action = content.action
|
|
119
|
+
? loadedComponents.truncateToWidth(theme.fg("dim", content.action), Math.max(0, width), "")
|
|
120
|
+
: "";
|
|
121
|
+
const actionWidth = loadedComponents.visibleWidth(action);
|
|
122
|
+
const heading = loadedComponents.truncateToWidth(
|
|
123
|
+
theme.fg("dim", content.heading), Math.max(0, width - actionWidth), "",
|
|
124
|
+
);
|
|
125
|
+
return [`${heading}${action}`];
|
|
126
|
+
},
|
|
127
|
+
invalidate() {},
|
|
128
|
+
};
|
|
129
|
+
const themedSummary = {
|
|
130
|
+
render(width) {
|
|
131
|
+
if (width < 3) {
|
|
132
|
+
themed(content.summary, "muted", compactSummary, previousCompactSummary ??= {});
|
|
133
|
+
return compactSummary.render(width);
|
|
134
|
+
}
|
|
135
|
+
themed(content.summary, "muted", summary, previousSummary ??= {});
|
|
136
|
+
return summary.render(width);
|
|
137
|
+
},
|
|
138
|
+
invalidate() {
|
|
139
|
+
previousSummary = undefined;
|
|
140
|
+
previousCompactSummary = undefined;
|
|
141
|
+
summary.invalidate();
|
|
142
|
+
compactSummary.invalidate();
|
|
143
|
+
},
|
|
144
|
+
};
|
|
145
|
+
widget.addChild(new loadedComponents.DynamicBorder(border));
|
|
146
|
+
widget.addChild(themedHeading);
|
|
147
|
+
widget.addChild(themedSummary);
|
|
148
|
+
widget.addChild(new loadedComponents.DynamicBorder(border));
|
|
149
|
+
return {
|
|
150
|
+
render(width) {
|
|
151
|
+
const lines = widget.render(width);
|
|
152
|
+
return width < 3
|
|
153
|
+
? lines.map((line) => loadedComponents.truncateToWidth(line, Math.max(0, width), ""))
|
|
154
|
+
: lines;
|
|
155
|
+
},
|
|
156
|
+
invalidate() { widget.invalidate(); },
|
|
157
|
+
};
|
|
158
|
+
}
|
|
159
|
+
|
|
116
160
|
function currentContextOptions(ctx, options) {
|
|
117
161
|
return {
|
|
118
162
|
sessionFile: sessionIdentity(ctx).sessionFile,
|
|
@@ -133,13 +177,14 @@ function sameIdentity(left, right) {
|
|
|
133
177
|
|
|
134
178
|
/**
|
|
135
179
|
* Private Pi-only presentation controller. It consumes existing receipts and
|
|
136
|
-
*
|
|
137
|
-
*
|
|
180
|
+
* uses native custom entries for scrollable, human-only Recaps and read state.
|
|
181
|
+
* Older Pi versions retain the widget fallback. It never invokes a model.
|
|
138
182
|
*/
|
|
139
183
|
export function createRarebitRecapController({
|
|
140
184
|
delayMs = DEFAULT_RAREBIT_RECAP_DELAY_MS,
|
|
141
185
|
timezone = DEFAULT_RAREBIT_RECAP_TIMEZONE,
|
|
142
186
|
readCurrent = readRarebitCurrent,
|
|
187
|
+
appendEntry,
|
|
143
188
|
setTimeoutFn = setTimeout,
|
|
144
189
|
clearTimeoutFn = clearTimeout,
|
|
145
190
|
} = {}) {
|
|
@@ -223,88 +268,65 @@ export function createRarebitRecapController({
|
|
|
223
268
|
...currentContextOptions(ctx, options),
|
|
224
269
|
});
|
|
225
270
|
const receipt = current?.receipt;
|
|
226
|
-
if (!receiptAppliesToContext(receipt, ctx)) return null;
|
|
227
271
|
const artifactState = current?.artifactState;
|
|
228
|
-
if (
|
|
229
|
-
|
|
230
|
-
artifactState?.
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
)
|
|
238
|
-
return null;
|
|
239
|
-
return receipt;
|
|
272
|
+
if (!receipt || !["request_current", "assessment_current"].includes(artifactState?.syncState) ||
|
|
273
|
+
!["request_generation", "exact_selection"].includes(artifactState?.applicability) ||
|
|
274
|
+
artifactState?.receiptRef?.jobId !== receipt.jobId ||
|
|
275
|
+
artifactState?.projection?.status !== receipt.sessionStatus)
|
|
276
|
+
return { receipt: null, reason: artifactState?.projection?.reason ?? artifactState?.retry?.reason ?? artifactState?.syncState ?? "no_summary" };
|
|
277
|
+
const projection = receiptCoverage(receipt, ctx);
|
|
278
|
+
return projection.coverage ? { receipt, coverage: projection.coverage } : {
|
|
279
|
+
receipt: null, reason: projection.state?.retry?.reason ?? projection.state?.projection?.reason ?? projection.state?.syncState ?? "stale_or_divergent",
|
|
280
|
+
};
|
|
240
281
|
};
|
|
241
282
|
|
|
242
283
|
const display = async (
|
|
243
284
|
ctx,
|
|
244
285
|
receipt,
|
|
245
|
-
{ timezone = recapTimezone, token, expectedOfferEpoch } = {},
|
|
286
|
+
{ timezone = recapTimezone, token, expectedOfferEpoch, storeOptions } = {},
|
|
246
287
|
) => {
|
|
247
288
|
const target = tuiContext(ctx);
|
|
248
|
-
|
|
289
|
+
let coverage = receiptCoverage(receipt, ctx).coverage;
|
|
290
|
+
if (!target || !coverage) return false;
|
|
249
291
|
const observedControllerGeneration = controllerGeneration;
|
|
250
292
|
const observedLifecycleGeneration = liveLifecycleGeneration;
|
|
251
293
|
const observedOfferEpoch = expectedOfferEpoch ?? offerEpoch;
|
|
252
294
|
const observedIdentity = sessionIdentity(ctx);
|
|
253
295
|
try {
|
|
254
|
-
|
|
296
|
+
await loadRecapWidgetComponents();
|
|
297
|
+
const current = await readReceipt(liveContext, storeOptions).catch(() => null);
|
|
255
298
|
if (
|
|
256
299
|
observedControllerGeneration !== controllerGeneration ||
|
|
257
300
|
observedLifecycleGeneration !== liveLifecycleGeneration ||
|
|
258
301
|
observedOfferEpoch !== offerEpoch ||
|
|
259
302
|
(token && !tokenIsCurrent(token)) ||
|
|
260
303
|
!sameIdentity(observedIdentity, sessionIdentity(liveContext)) ||
|
|
261
|
-
!
|
|
304
|
+
!current?.receipt || current.receipt.jobId !== receipt.jobId ||
|
|
305
|
+
current.coverage.coveredEntryId !== coverage.coveredEntryId ||
|
|
306
|
+
current.coverage.selectionHash !== coverage.selectionHash
|
|
262
307
|
)
|
|
263
308
|
return false;
|
|
309
|
+
receipt = current.receipt;
|
|
310
|
+
coverage = current.coverage;
|
|
264
311
|
const content = summaryContent(receipt, {
|
|
265
312
|
timezone: normalizeRarebitRecapTimezone(timezone),
|
|
266
313
|
});
|
|
314
|
+
if (typeof appendEntry === "function") {
|
|
315
|
+
const existing = branchSnapshot(ctx).findLast((entry) =>
|
|
316
|
+
entry.type === "custom" && entry.customType === RECAP_ENTRY_TYPE &&
|
|
317
|
+
entry.data?.version === 1 && entry.data.jobId === receipt.jobId &&
|
|
318
|
+
entry.data.coveredEntryId === coverage.coveredEntryId &&
|
|
319
|
+
entry.data.selectionHash === coverage.selectionHash);
|
|
320
|
+
if (!existing) appendEntry(RECAP_ENTRY_TYPE, {
|
|
321
|
+
version: 1, ...content, jobId: receipt.jobId,
|
|
322
|
+
coveredEntryId: coverage.coveredEntryId, selectionHash: coverage.selectionHash,
|
|
323
|
+
});
|
|
324
|
+
return true;
|
|
325
|
+
}
|
|
267
326
|
target.ui.setWidget(
|
|
268
327
|
RAREBIT_RECAP_WIDGET_KEY,
|
|
269
328
|
(_tui, theme) => {
|
|
270
|
-
|
|
271
|
-
const widget = new components.Container();
|
|
272
|
-
const heading = new components.Text("", 1, 0);
|
|
273
|
-
const summary = new components.Text("", 1, 0);
|
|
274
|
-
let previousHeading;
|
|
275
|
-
let previousSummary;
|
|
276
|
-
const themed = (text, color, component, previous) => {
|
|
277
|
-
const rendered = theme.fg(color, text);
|
|
278
|
-
if (rendered !== previous.value) {
|
|
279
|
-
previous.value = rendered;
|
|
280
|
-
component.setText(rendered);
|
|
281
|
-
}
|
|
282
|
-
};
|
|
283
|
-
const themedHeading = {
|
|
284
|
-
render: (width) => {
|
|
285
|
-
themed(content.heading, "dim", heading, previousHeading ??= {});
|
|
286
|
-
return heading.render(width);
|
|
287
|
-
},
|
|
288
|
-
invalidate: () => {
|
|
289
|
-
previousHeading = undefined;
|
|
290
|
-
heading.invalidate();
|
|
291
|
-
},
|
|
292
|
-
};
|
|
293
|
-
const themedSummary = {
|
|
294
|
-
render: (width) => {
|
|
295
|
-
themed(content.summary, "muted", summary, previousSummary ??= {});
|
|
296
|
-
return summary.render(width);
|
|
297
|
-
},
|
|
298
|
-
invalidate: () => {
|
|
299
|
-
previousSummary = undefined;
|
|
300
|
-
summary.invalidate();
|
|
301
|
-
},
|
|
302
|
-
};
|
|
303
|
-
widget.addChild(new components.DynamicBorder(border));
|
|
304
|
-
widget.addChild(themedHeading);
|
|
305
|
-
widget.addChild(themedSummary);
|
|
306
|
-
widget.addChild(new components.DynamicBorder(border));
|
|
307
|
-
return widget;
|
|
329
|
+
return recapComponent(content, theme);
|
|
308
330
|
},
|
|
309
331
|
{ placement: "aboveEditor" },
|
|
310
332
|
);
|
|
@@ -321,25 +343,23 @@ export function createRarebitRecapController({
|
|
|
321
343
|
const observedGeneration = controllerGeneration;
|
|
322
344
|
const observedOfferEpoch = offerEpoch;
|
|
323
345
|
const identity = sessionIdentity(ctx);
|
|
324
|
-
const
|
|
346
|
+
const current = await readReceipt(ctx, options).catch(() => null);
|
|
325
347
|
if (
|
|
326
348
|
observedGeneration !== controllerGeneration ||
|
|
327
349
|
observedOfferEpoch !== offerEpoch ||
|
|
328
350
|
!sameIdentity(identity, sessionIdentity(liveContext))
|
|
329
351
|
)
|
|
330
352
|
return { shown: false, reason: "stale" };
|
|
331
|
-
if (!receipt) {
|
|
353
|
+
if (!current?.receipt) {
|
|
332
354
|
clearWidget(ctx);
|
|
333
|
-
return { shown: false, reason: "no_current_summary" };
|
|
355
|
+
return { shown: false, reason: current?.reason ?? "no_current_summary" };
|
|
334
356
|
}
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
receipt,
|
|
342
|
-
};
|
|
357
|
+
const shown = await display(ctx, current.receipt, {
|
|
358
|
+
storeOptions: options,
|
|
359
|
+
timezone: options.timezone ?? options.recapTimezone ?? recapTimezone,
|
|
360
|
+
expectedOfferEpoch: observedOfferEpoch,
|
|
361
|
+
});
|
|
362
|
+
return { shown, reason: shown ? "current_summary" : "stale_or_unavailable", receipt: current.receipt };
|
|
343
363
|
};
|
|
344
364
|
|
|
345
365
|
const offer = ({ ctx, result, token, ...options }) => {
|
|
@@ -379,7 +399,7 @@ export function createRarebitRecapController({
|
|
|
379
399
|
const current = liveContext;
|
|
380
400
|
const currentIdentity = sessionIdentity(current);
|
|
381
401
|
if (!sameIdentity(offerState.identity, currentIdentity)) return;
|
|
382
|
-
const
|
|
402
|
+
const currentReceipt = await readReceipt(current, offerState.options).catch(
|
|
383
403
|
() => null,
|
|
384
404
|
);
|
|
385
405
|
if (
|
|
@@ -387,8 +407,9 @@ export function createRarebitRecapController({
|
|
|
387
407
|
!tokenIsCurrent(offerState.token)
|
|
388
408
|
)
|
|
389
409
|
return;
|
|
390
|
-
if (!receipt || receipt.jobId !== offerState.record.jobId) return;
|
|
391
|
-
if (await display(current, receipt, {
|
|
410
|
+
if (!currentReceipt?.receipt || currentReceipt.receipt.jobId !== offerState.record.jobId) return;
|
|
411
|
+
if (await display(current, currentReceipt.receipt, {
|
|
412
|
+
storeOptions: offerState.options,
|
|
392
413
|
token: offerState.token,
|
|
393
414
|
expectedOfferEpoch: offerState.offerEpoch,
|
|
394
415
|
timezone:
|
|
@@ -406,12 +427,49 @@ export function createRarebitRecapController({
|
|
|
406
427
|
return true;
|
|
407
428
|
};
|
|
408
429
|
|
|
430
|
+
const renderEntry = (entry, _options, theme) => {
|
|
431
|
+
if (entry.data?.version !== 1 || typeof entry.data.summary !== "string" ||
|
|
432
|
+
typeof entry.data.heading !== "string") return undefined;
|
|
433
|
+
let component;
|
|
434
|
+
// Resume can render entries before the optional Pi imports settle. Keep a
|
|
435
|
+
// placeholder while the optional components load, then request a redraw.
|
|
436
|
+
if (!loadedComponents)
|
|
437
|
+
void loadRecapWidgetComponents().then(() => clearWidget()).catch(() => {});
|
|
438
|
+
return {
|
|
439
|
+
render(width) {
|
|
440
|
+
if (!loadedComponents) return [theme.fg("dim", "Recap loading…".slice(0, width))];
|
|
441
|
+
const read = recapIsRead(branchSnapshot(liveContext), entry);
|
|
442
|
+
component = recapComponent({ ...entry.data,
|
|
443
|
+
action: read ? " ✓ got it" : " [got it]" }, theme);
|
|
444
|
+
return component.render(width);
|
|
445
|
+
},
|
|
446
|
+
invalidate() { component?.invalidate(); },
|
|
447
|
+
};
|
|
448
|
+
};
|
|
449
|
+
|
|
450
|
+
const acknowledge = (ctx) => {
|
|
451
|
+
if (typeof appendEntry !== "function" || !sameIdentity(sessionIdentity(ctx), sessionIdentity(liveContext)))
|
|
452
|
+
return { acknowledged: false, reason: "unavailable" };
|
|
453
|
+
const branch = branchSnapshot(ctx);
|
|
454
|
+
const recap = latestRecap(branch);
|
|
455
|
+
if (!recap) return { acknowledged: false, reason: "no_recap" };
|
|
456
|
+
if (recapIsRead(branch, recap))
|
|
457
|
+
return { acknowledged: false, reason: "already_read" };
|
|
458
|
+
appendEntry(RECAP_READ_TYPE, { version: 1, recapEntryId: recap.id,
|
|
459
|
+
jobId: recap.data.jobId, coveredEntryId: recap.data.coveredEntryId,
|
|
460
|
+
selectionHash: recap.data.selectionHash });
|
|
461
|
+
clearWidget(ctx);
|
|
462
|
+
return { acknowledged: true };
|
|
463
|
+
};
|
|
464
|
+
|
|
409
465
|
const dispose = () => {
|
|
410
466
|
invalidate(undefined, undefined);
|
|
411
467
|
liveContext = undefined;
|
|
412
468
|
};
|
|
413
469
|
|
|
414
470
|
return {
|
|
471
|
+
renderEntry,
|
|
472
|
+
acknowledge,
|
|
415
473
|
captureMaterialization,
|
|
416
474
|
offer,
|
|
417
475
|
showExisting,
|
package/src/rarebit-service.mjs
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { recapReadCheckpoint } from "./rarebit-read-checkpoint.mjs";
|
|
1
2
|
import {
|
|
2
3
|
DEFAULT_RAREBIT_SUMMARY_POLICY,
|
|
3
4
|
DEFAULT_RAREBIT_SUMMARY_PROMPT_GUIDANCE,
|
|
@@ -35,7 +36,7 @@ import {
|
|
|
35
36
|
} from "./automatic-summary-policy.mjs";
|
|
36
37
|
|
|
37
38
|
export const RAREBIT_TITLE_IMPLEMENTATION_VERSION = "hc-rarebit-title-v4";
|
|
38
|
-
export const RAREBIT_SUMMARY_IMPLEMENTATION_VERSION = "hc-rarebit-summary-
|
|
39
|
+
export const RAREBIT_SUMMARY_IMPLEMENTATION_VERSION = "hc-rarebit-summary-v7";
|
|
39
40
|
export const RAREBIT_SUMMARY_SCHEMA_VERSION = 4;
|
|
40
41
|
export const RAREBIT_INPUT_CHARS_PER_TOKEN = 4;
|
|
41
42
|
export const DEFAULT_RAREBIT_MAX_PROMPT_CHARS =
|
|
@@ -151,6 +152,7 @@ export async function processRarebitSummary(ctx, config = {}) {
|
|
|
151
152
|
const sessionFile = config.sessionFile ?? sessionFileFrom(ctx);
|
|
152
153
|
const sessionId = config.sessionId ?? sessionIdFrom(ctx);
|
|
153
154
|
const selection = selectRarebits(branch);
|
|
155
|
+
const readCheckpoint = recapReadCheckpoint(branch, selection);
|
|
154
156
|
const measurement = measureRarebits(branch, selection);
|
|
155
157
|
const policy = normalizeRarebitSummaryPolicy(
|
|
156
158
|
config.summaryPolicy ?? DEFAULT_RAREBIT_SUMMARY_POLICY,
|
|
@@ -227,7 +229,7 @@ export async function processRarebitSummary(ctx, config = {}) {
|
|
|
227
229
|
branch: branchRef,
|
|
228
230
|
selection,
|
|
229
231
|
policy,
|
|
230
|
-
inputPolicy: inputCoveragePolicy,
|
|
232
|
+
inputPolicy: { ...inputCoveragePolicy, ...(readCheckpoint ? { readCheckpoint } : {}) },
|
|
231
233
|
lifecycleBoundary,
|
|
232
234
|
promptVersion,
|
|
233
235
|
promptIdentity: summaryPrompt.promptIdentity,
|
|
@@ -322,6 +324,7 @@ export async function processRarebitSummary(ctx, config = {}) {
|
|
|
322
324
|
const derivationInput = composeRarebitSummaryDerivationInput(selection, {
|
|
323
325
|
promptVersion,
|
|
324
326
|
summaryPrompt: config.summaryPrompt,
|
|
327
|
+
readCheckpoint,
|
|
325
328
|
lifecycleBoundary,
|
|
326
329
|
maxPromptChars,
|
|
327
330
|
});
|
|
@@ -200,6 +200,7 @@ export const RAREBIT_SETTINGS_FIELDS = Object.freeze([
|
|
|
200
200
|
]);
|
|
201
201
|
|
|
202
202
|
const paletteItems = [
|
|
203
|
+
{ id: "view", tab: "Actions", label: "View", description: "Read Rarebits in the TUI." },
|
|
203
204
|
{ id: "settings", tab: "Actions", label: "Settings", description: "Open the interactive Rarebit settings editor." },
|
|
204
205
|
{ id: "status", tab: "Actions", label: "Status", description: "Show effective Rarebit configuration." },
|
|
205
206
|
{ id: "help", tab: "Actions", label: "Command help", description: "Show direct /rarebit commands." },
|
|
@@ -466,6 +467,7 @@ export async function openRarebitSettings(ctx, {
|
|
|
466
467
|
|
|
467
468
|
export function rarebitPaletteCommand(selection) {
|
|
468
469
|
switch (selection?.id) {
|
|
470
|
+
case "view": return "view";
|
|
469
471
|
case "settings": case "summary_settings": case "recap_settings": case "session_settings": return "settings";
|
|
470
472
|
case "summary_status": return "status";
|
|
471
473
|
case "recap": return "recap";
|