@hypercarrier/rarebit 0.2.1 → 0.4.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 +23 -0
- package/README.md +41 -1
- package/RUNBOOK.md +7 -5
- package/package.json +1 -1
- package/scripts/e2e-recall-pi0842.mjs +31 -13
- package/scripts/e2e-view-pi.mjs +500 -0
- package/src/extension.mjs +50 -14
- package/src/rarebit-command.mjs +5 -0
- package/src/rarebit-recall.mjs +39 -27
- package/src/rarebit-recap.mjs +2 -1
- package/src/rarebit-settings-ui.mjs +2 -0
- package/src/rarebit-view.mjs +426 -0
- package/test/rarebit-command.test.mjs +10 -2
- package/test/rarebit-recall.test.mjs +84 -41
- package/test/rarebit-view.test.mjs +270 -0
|
@@ -0,0 +1,500 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Real Pi TUI acceptance harness for the Rarebit view feature.
|
|
5
|
+
*
|
|
6
|
+
* Run from any directory with `node packages/hc-rarebit/scripts/e2e-view-pi.mjs`
|
|
7
|
+
* (or `node scripts/e2e-view-pi.mjs` from this package). The local Pi peer is
|
|
8
|
+
* the default. Set RAREBIT_PI_ROOT to another Pi package root; set
|
|
9
|
+
* RAREBIT_TUI_MODE=fullscreen to cover Pi 1.0 mouse-wheel routing. It starts
|
|
10
|
+
* Pi in a unique detached tmux server, loads only this package's extension, and uses
|
|
11
|
+
* a fully mocked Session. It sends no model prompts and never reads user
|
|
12
|
+
* Sessions. Pi runs against disposable agent state and the script kills its
|
|
13
|
+
* tmux server and removes its temporary files on exit.
|
|
14
|
+
*
|
|
15
|
+
* Assertions: context filters tool/thinking rows and older Rarebits, keeps
|
|
16
|
+
* selected prose and Recaps, and resets across reload/session changes. All reads
|
|
17
|
+
* across compactions, both Kitty Ctrl+Super byte sequences toggle/cycle views,
|
|
18
|
+
* search wrap, status, footer, resize, peek input, layering, and cleanup are
|
|
19
|
+
* checked against real Pi without model prompts.
|
|
20
|
+
*/
|
|
21
|
+
|
|
22
|
+
import assert from "node:assert/strict";
|
|
23
|
+
import { mkdir, mkdtemp, readFile, rm, writeFile } from "node:fs/promises";
|
|
24
|
+
import { tmpdir } from "node:os";
|
|
25
|
+
import { dirname, join, resolve } from "node:path";
|
|
26
|
+
import { fileURLToPath, pathToFileURL } from "node:url";
|
|
27
|
+
import { spawn } from "node:child_process";
|
|
28
|
+
import { execFile as execFileCallback } from "node:child_process";
|
|
29
|
+
import { promisify } from "node:util";
|
|
30
|
+
import { selectRarebits } from "../src/rarebit-core.mjs";
|
|
31
|
+
import { latestRecap } from "../src/rarebit-read-checkpoint.mjs";
|
|
32
|
+
|
|
33
|
+
const execFile = promisify(execFileCallback);
|
|
34
|
+
const packageRoot = resolve(dirname(fileURLToPath(import.meta.url)), "..");
|
|
35
|
+
const piEntry = fileURLToPath(await import.meta.resolve("@earendil-works/pi-coding-agent"));
|
|
36
|
+
const piRoot = resolve(process.env.RAREBIT_PI_ROOT ?? dirname(dirname(piEntry)));
|
|
37
|
+
const piVersion = JSON.parse(await readFile(join(piRoot, "package.json"), "utf8")).version;
|
|
38
|
+
const tuiMode = process.env.RAREBIT_TUI_MODE ?? "regular";
|
|
39
|
+
const piCli = join(piRoot, "dist/cli.js");
|
|
40
|
+
const tmux = process.env.TMUX_BIN ?? "tmux";
|
|
41
|
+
const sleep = (ms) => new Promise((resolvePromise) => setTimeout(resolvePromise, ms));
|
|
42
|
+
|
|
43
|
+
async function pane(socket, session) {
|
|
44
|
+
try {
|
|
45
|
+
return (await execFile(tmux, ["-L", socket, "capture-pane", "-p", "-J", "-t", session, "-S", "-240"])).stdout;
|
|
46
|
+
} catch {
|
|
47
|
+
return "";
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
async function keys(socket, session, text) {
|
|
51
|
+
await execFile(tmux, ["-L", socket, "send-keys", "-l", "-t", session, text]);
|
|
52
|
+
}
|
|
53
|
+
async function press(socket, session, key) {
|
|
54
|
+
await execFile(tmux, ["-L", socket, "send-keys", "-t", session, key]);
|
|
55
|
+
}
|
|
56
|
+
async function resizeWindow(socket, session, width, height) {
|
|
57
|
+
await execFile(tmux, ["-L", socket, "resize-window", "-t", session, "-x", String(width), "-y", String(height)]);
|
|
58
|
+
}
|
|
59
|
+
async function waitFor(label, predicate, timeout = 10_000, interval = 100) {
|
|
60
|
+
const deadline = Date.now() + timeout;
|
|
61
|
+
while (Date.now() < deadline) {
|
|
62
|
+
const value = await predicate();
|
|
63
|
+
if (value) return value;
|
|
64
|
+
await sleep(interval);
|
|
65
|
+
}
|
|
66
|
+
const diagnostic = await pane(socket, session);
|
|
67
|
+
throw new Error(`Timed out waiting for ${label}${diagnostic ? `; last pane:\n${diagnostic}` : ""}`);
|
|
68
|
+
}
|
|
69
|
+
async function submitSlash(socket, session, command) {
|
|
70
|
+
await keys(socket, session, command);
|
|
71
|
+
// Accept Pi's selected slash-command completion, then submit the command.
|
|
72
|
+
await sleep(500);
|
|
73
|
+
await press(socket, session, "Tab");
|
|
74
|
+
await sleep(100);
|
|
75
|
+
await press(socket, session, "Enter");
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
const temp = await mkdtemp(join(tmpdir(), "rarebit-view-pi-e2e-"));
|
|
79
|
+
const agentDir = join(temp, "agent");
|
|
80
|
+
const projectDir = join(temp, "project");
|
|
81
|
+
const sessionDir = join(temp, "sessions");
|
|
82
|
+
const sessionFile = join(sessionDir, "rarebit-view-first.jsonl");
|
|
83
|
+
const secondSessionFile = join(sessionDir, "rarebit-view-second.jsonl");
|
|
84
|
+
const socket = `rarebit-view-${process.pid}`;
|
|
85
|
+
const session = "rarebit-view-e2e";
|
|
86
|
+
const fixtureExtension = pathToFileURL(join(packageRoot, "src/extension.mjs")).href;
|
|
87
|
+
const defects = [];
|
|
88
|
+
|
|
89
|
+
function mockSession(cwd) {
|
|
90
|
+
const entries = [];
|
|
91
|
+
let parentId = null;
|
|
92
|
+
let id = 0;
|
|
93
|
+
const tick = (n) => new Date(Date.UTC(2026, 0, 1, 12, n)).toISOString();
|
|
94
|
+
const push = (type, fields = {}) => {
|
|
95
|
+
const entry = { type, id: String(++id).padStart(8, "0"), parentId, timestamp: tick(id), ...fields };
|
|
96
|
+
entries.push(entry);
|
|
97
|
+
parentId = entry.id;
|
|
98
|
+
return entry.id;
|
|
99
|
+
};
|
|
100
|
+
const message = (role, content, stopReason) => push("message", {
|
|
101
|
+
message: {
|
|
102
|
+
role,
|
|
103
|
+
content,
|
|
104
|
+
timestamp: Date.parse(tick(id + 1)),
|
|
105
|
+
...(stopReason ? { stopReason } : {}),
|
|
106
|
+
...(role === "assistant" ? { api: "mock", provider: "mock", model: "mock", usage: { input: 1, output: 1, cacheRead: 0, cacheWrite: 0, totalTokens: 2, cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, total: 0 } } } : {}),
|
|
107
|
+
},
|
|
108
|
+
});
|
|
109
|
+
entries.push({ type: "session", version: 3, id: "rarebit-view-session", timestamp: tick(0), cwd });
|
|
110
|
+
|
|
111
|
+
message("user", "PRECOMPACTION_RAREBIT: earlier conversation sentinel");
|
|
112
|
+
message("assistant", [{ type: "text", text: "PRECOMPACTION_RAREBIT: earlier assistant sentinel" }], "stop");
|
|
113
|
+
for (let n = 1; n <= 12; n++) {
|
|
114
|
+
message("user", `PRECOMPACTION_HISTORY_${String(n).padStart(2, "0")}: ${Array.from({ length: 5 }, (_, line) => `mock line ${line + 1}`).join("\n")}`);
|
|
115
|
+
}
|
|
116
|
+
const compactionId = push("compaction", {
|
|
117
|
+
summary: "Mock context summary. No private session data.",
|
|
118
|
+
firstKeptEntryId: null,
|
|
119
|
+
tokensBefore: 1000,
|
|
120
|
+
});
|
|
121
|
+
entries.find((entry) => entry.id === compactionId).firstKeptEntryId = message("user", "CURRENT_CONTEXT_USER: request after compaction");
|
|
122
|
+
message("user", "CURRENT_CONTEXT_RAREBIT: keep this conversational sentinel");
|
|
123
|
+
message("assistant", [
|
|
124
|
+
{ type: "thinking", thinking: "CURRENT_THINKING_SENTINEL: hidden internal text" },
|
|
125
|
+
{ type: "text", text: "CURRENT_CONTEXT_ASSISTANT_RAREBIT: current answer sentinel" },
|
|
126
|
+
{ type: "toolCall", id: "mock-call", name: "mock_tool", arguments: {} },
|
|
127
|
+
], "toolUse");
|
|
128
|
+
message("assistant", [
|
|
129
|
+
{ type: "thinking", thinking: "THINKING_BLOCK_SENTINEL: hide this reasoning" },
|
|
130
|
+
{ type: "text", text: "Thinking..." },
|
|
131
|
+
], "stop");
|
|
132
|
+
message("assistant", [{ type: "text", text: "Thinking..." }], "stop");
|
|
133
|
+
push("message", { message: {
|
|
134
|
+
role: "toolResult", toolCallId: "mock-call", toolName: "mock_tool",
|
|
135
|
+
content: [{ type: "text", text: "CURRENT_TOOL_OUTPUT_SENTINEL: hidden tool output" }],
|
|
136
|
+
isError: false, timestamp: Date.parse(tick(id + 1)),
|
|
137
|
+
} });
|
|
138
|
+
const coveredEntryId = message("assistant", [{ type: "text", text: "CURRENT_FINAL_RAREBIT: final answer sentinel" }], "stop");
|
|
139
|
+
const selection = selectRarebits(entries);
|
|
140
|
+
const recapId = push("custom", { customType: "rarebit-recap", data: {
|
|
141
|
+
version: 1, jobId: "mock-recap-job", coveredEntryId, selectionHash: selection.manifestHash,
|
|
142
|
+
heading: "Mock Recap", summary: "RECAP_SHORTCUT_SENTINEL: generic mock recap", sessionStatus: "finished",
|
|
143
|
+
statusReason: "all_requests_accomplished", observedAt: Date.parse(tick(id + 1)),
|
|
144
|
+
} });
|
|
145
|
+
assert.equal(latestRecap(entries)?.id, recapId, "the generic Recap fixture is valid");
|
|
146
|
+
return `${entries.map((entry) => JSON.stringify(entry)).join("\n")}\n`;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
try {
|
|
150
|
+
await mkdir(agentDir, { recursive: true });
|
|
151
|
+
await mkdir(projectDir, { recursive: true });
|
|
152
|
+
await mkdir(sessionDir, { recursive: true });
|
|
153
|
+
const firstSession = mockSession(projectDir);
|
|
154
|
+
await writeFile(sessionFile, firstSession, { mode: 0o600 });
|
|
155
|
+
const secondSession = firstSession
|
|
156
|
+
.replaceAll("rarebit-view-session", "rarebit-second-session")
|
|
157
|
+
.replaceAll("PRECOMPACTION_RAREBIT", "SECOND_PRECOMPACTION_RAREBIT")
|
|
158
|
+
.replaceAll("PRECOMPACTION_HISTORY", "SECOND_PRECOMPACTION_HISTORY")
|
|
159
|
+
.replaceAll("CURRENT_CONTEXT_USER", "SECOND_CONTEXT_USER")
|
|
160
|
+
.replaceAll("CURRENT_CONTEXT_RAREBIT", "SECOND_SESSION_RAREBIT")
|
|
161
|
+
.replaceAll("CURRENT_CONTEXT_ASSISTANT_RAREBIT", "SECOND_CONTEXT_ASSISTANT_RAREBIT")
|
|
162
|
+
.replaceAll("CURRENT_FINAL_RAREBIT", "SECOND_FINAL_RAREBIT")
|
|
163
|
+
.replaceAll("CURRENT_TOOL_OUTPUT_SENTINEL", "SECOND_TOOL_OUTPUT_SENTINEL")
|
|
164
|
+
.replaceAll("CURRENT_THINKING_SENTINEL", "SECOND_THINKING_SENTINEL");
|
|
165
|
+
await writeFile(secondSessionFile, secondSession, { mode: 0o600 });
|
|
166
|
+
const childEnv = {
|
|
167
|
+
...process.env,
|
|
168
|
+
PI_CODING_AGENT_DIR: agentDir,
|
|
169
|
+
PI_OFFLINE: "1",
|
|
170
|
+
PI_TELEMETRY: "0",
|
|
171
|
+
};
|
|
172
|
+
const piCommand = [
|
|
173
|
+
"env", `PI_CODING_AGENT_DIR=${agentDir}`, "PI_OFFLINE=1", "PI_TELEMETRY=0",
|
|
174
|
+
process.execPath, piCli, "--no-extensions", "--no-skills", "--no-prompt-templates",
|
|
175
|
+
"--no-themes", "--no-context-files", "--approve", "--extension", fixtureExtension,
|
|
176
|
+
"--session-dir", sessionDir, "--session", sessionFile, "--tui-mode", tuiMode,
|
|
177
|
+
];
|
|
178
|
+
const quote = (value) => `'${value.replaceAll("'", "'\\''")}'`;
|
|
179
|
+
const guardedCommand = `${piCommand.map(quote).join(" ")}; result=$?; printf '\\nPI_EXIT:%s\\n' "$result"; sleep 30; exit "$result"`;
|
|
180
|
+
const command = [
|
|
181
|
+
"-L", socket, "new-session", "-d", "-x", "120", "-y", "36", "-s", session,
|
|
182
|
+
"/bin/sh", "-c", guardedCommand,
|
|
183
|
+
];
|
|
184
|
+
await new Promise((resolvePromise, reject) => {
|
|
185
|
+
const child = spawn(tmux, command, { cwd: projectDir, env: childEnv, stdio: "ignore" });
|
|
186
|
+
child.once("error", reject);
|
|
187
|
+
child.once("exit", (code) => code === 0 ? resolvePromise() : reject(new Error(`tmux new-session exited ${code}`)));
|
|
188
|
+
});
|
|
189
|
+
await waitFor("mock Session transcript", async () => {
|
|
190
|
+
const output = await pane(socket, session);
|
|
191
|
+
return output.includes("CURRENT_FINAL_RAREBIT") ? output : false;
|
|
192
|
+
});
|
|
193
|
+
await sleep(500);
|
|
194
|
+
|
|
195
|
+
const inactive = await pane(socket, session);
|
|
196
|
+
assert.doesNotMatch(inactive, /Rarebit view:/, "the view has no status while off");
|
|
197
|
+
const kitty = (code) => `\u001b[${code};13u`;
|
|
198
|
+
await keys(socket, session, kitty(114));
|
|
199
|
+
const firstToggle = await waitFor("first Ctrl+Super+R opens all", async () => {
|
|
200
|
+
const output = await pane(socket, session);
|
|
201
|
+
return output.includes("Rarebits ·") ? output : false;
|
|
202
|
+
});
|
|
203
|
+
assert.match(firstToggle, /Rarebits ·/);
|
|
204
|
+
await keys(socket, session, kitty(110));
|
|
205
|
+
const cycledPeek = await waitFor("Ctrl+Super+N cycles all to peek", async () => {
|
|
206
|
+
const output = await pane(socket, session);
|
|
207
|
+
return output.includes("Rarebit view: peek · next ctrl+alt+n · off ctrl+alt+r") ? output : false;
|
|
208
|
+
});
|
|
209
|
+
assert.match(cycledPeek, /Rarebits ·/);
|
|
210
|
+
await press(socket, session, "C-u");
|
|
211
|
+
await submitSlash(socket, session, "/rarebit view off");
|
|
212
|
+
await keys(socket, session, kitty(114));
|
|
213
|
+
const rememberedPeek = await waitFor("toggle reopens the last used mode", async () => {
|
|
214
|
+
const output = await pane(socket, session);
|
|
215
|
+
return output.includes("Rarebit view: peek · next ctrl+alt+n · off ctrl+alt+r") ? output : false;
|
|
216
|
+
});
|
|
217
|
+
assert.match(rememberedPeek, /Rarebits ·/);
|
|
218
|
+
await press(socket, session, "C-u");
|
|
219
|
+
await submitSlash(socket, session, "/rarebit view off");
|
|
220
|
+
await keys(socket, session, kitty(110));
|
|
221
|
+
const cycledContext = await waitFor("Ctrl+Super+N cycles off to context", async () => {
|
|
222
|
+
const output = await pane(socket, session);
|
|
223
|
+
return output.includes("Rarebit view: context · next ctrl+alt+n · off ctrl+alt+r") ? output : false;
|
|
224
|
+
});
|
|
225
|
+
assert.match(cycledContext, /CURRENT_CONTEXT_RAREBIT/);
|
|
226
|
+
await keys(socket, session, kitty(114));
|
|
227
|
+
await waitFor("Ctrl+Super+R toggles context off", async () => !(await pane(socket, session)).includes("Rarebit view:"));
|
|
228
|
+
await keys(socket, session, kitty(114));
|
|
229
|
+
await waitFor("toggle remembers context", async () => (await pane(socket, session)).includes("Rarebit view: context · next ctrl+alt+n · off ctrl+alt+r"));
|
|
230
|
+
await submitSlash(socket, session, "/rarebit view off");
|
|
231
|
+
|
|
232
|
+
await submitSlash(socket, session, "/rarebit help");
|
|
233
|
+
await waitFor("pre-attach command notice", async () => (await pane(socket, session)).includes("Usage: /rarebit"));
|
|
234
|
+
await submitSlash(socket, session, "/rarebit view context");
|
|
235
|
+
const context = await waitFor("context view status", async () => {
|
|
236
|
+
const output = await pane(socket, session);
|
|
237
|
+
return output.includes("Rarebit view: context") ? output : false;
|
|
238
|
+
});
|
|
239
|
+
assert.match(context, /CURRENT_CONTEXT_RAREBIT/);
|
|
240
|
+
assert.doesNotMatch(context, /Usage: \/rarebit/, "notices from before context attached remain hidden");
|
|
241
|
+
assert.match(context, /CURRENT_FINAL_RAREBIT/);
|
|
242
|
+
assert.equal([...context.matchAll(/Thinking\.\.\./g)].length, 2, "selected prose survives when it equals Pi's thinking label, with and without a thinking block");
|
|
243
|
+
assert.doesNotMatch(context, /THINKING_BLOCK_SENTINEL/);
|
|
244
|
+
assert.match(context, /Compacted from 1,000 tokens/);
|
|
245
|
+
assert.match(context, /RECAP_SHORTCUT_SENTINEL/);
|
|
246
|
+
assert.ok(context.includes("Rarebit view: context · next ctrl+alt+n · off ctrl+alt+r"), "status names the active mode and first shortcuts");
|
|
247
|
+
assert.doesNotMatch(context, /PRECOMPACTION_RAREBIT/);
|
|
248
|
+
assert.doesNotMatch(context, /CURRENT_TOOL_OUTPUT_SENTINEL|CURRENT_THINKING_SENTINEL/);
|
|
249
|
+
await submitSlash(socket, session, "/rarebit status");
|
|
250
|
+
const contextStatusNotice = await waitFor("status notice remains visible in context", async () => {
|
|
251
|
+
const output = await pane(socket, session);
|
|
252
|
+
return output.includes("Rarebit status:") ? output : false;
|
|
253
|
+
}, 1_500).catch(() => null);
|
|
254
|
+
if (!contextStatusNotice) defects.push("/rarebit status emits no visible status notice while context is active");
|
|
255
|
+
else assert.match(contextStatusNotice, /Rarebit status: view=context/);
|
|
256
|
+
|
|
257
|
+
await submitSlash(socket, session, "/rarebit view off");
|
|
258
|
+
const off = await waitFor("view disabled", async () => {
|
|
259
|
+
const output = await pane(socket, session);
|
|
260
|
+
return !output.includes("Rarebit view: context") && output.includes("CURRENT_TOOL_OUTPUT_SENTINEL") ? output : false;
|
|
261
|
+
});
|
|
262
|
+
assert.doesNotMatch(off, /Rarebit view:/, "off clears the view status line");
|
|
263
|
+
|
|
264
|
+
await submitSlash(socket, session, "/rarebit view all");
|
|
265
|
+
const initialAll = await waitFor("all view", async () => {
|
|
266
|
+
const output = await pane(socket, session);
|
|
267
|
+
return output.includes("Rarebits ·") ? output : false;
|
|
268
|
+
});
|
|
269
|
+
assert.match(initialAll, /ctrl\+alt\+n next/, "all footer advertises the cycle shortcut");
|
|
270
|
+
const initialPosition = initialAll.match(/(\d+-\d+\/\d+)\s*$/m)?.[1];
|
|
271
|
+
await resizeWindow(socket, session, 120, 44);
|
|
272
|
+
const resizedAll = await waitFor("all view adapts to a taller terminal", async () => {
|
|
273
|
+
const output = await pane(socket, session);
|
|
274
|
+
const position = output.match(/(\d+-\d+\/\d+)\s*$/m)?.[1];
|
|
275
|
+
return position && position !== initialPosition ? output : false;
|
|
276
|
+
});
|
|
277
|
+
assert.match(resizedAll, /Rarebits ·/);
|
|
278
|
+
await resizeWindow(socket, session, 120, 36);
|
|
279
|
+
await waitFor("all view adapts after terminal shrinks", async () => {
|
|
280
|
+
const output = await pane(socket, session);
|
|
281
|
+
const position = output.match(/(\d+-\d+\/\d+)\s*$/m)?.[1];
|
|
282
|
+
return position === initialPosition ? output : false;
|
|
283
|
+
});
|
|
284
|
+
await press(socket, session, "C-M-g");
|
|
285
|
+
assert.ok(await waitFor("Recap shortcut does not close all", async () => (await pane(socket, session)).includes("Rarebits ·")));
|
|
286
|
+
await sleep(250);
|
|
287
|
+
const entriesAfterRecapKey = (await readFile(sessionFile, "utf8")).trim().split("\n").map(JSON.parse);
|
|
288
|
+
assert.ok(!entriesAfterRecapKey.some((entry) => entry.customType === "rarebit-recap-read"), "Ctrl+Alt+G does not acknowledge a Recap while all is focused");
|
|
289
|
+
await keys(socket, session, "\u001b[114;13u");
|
|
290
|
+
await waitFor("Ctrl+Super+R closes focused all", async () => !(await pane(socket, session)).includes("Rarebits ·"));
|
|
291
|
+
await keys(socket, session, "\u001b[114;13u");
|
|
292
|
+
await waitFor("Ctrl+Super+R reopens focused all", async () => (await pane(socket, session)).includes("Rarebits ·"));
|
|
293
|
+
if (piVersion === "1.0.0" && tuiMode === "fullscreen") {
|
|
294
|
+
await press(socket, session, "g");
|
|
295
|
+
const atStart = (await pane(socket, session)).match(/(\d+-\d+\/\d+)\s*$/m)?.[1];
|
|
296
|
+
await keys(socket, session, "\u001b[<65;60;20M");
|
|
297
|
+
const wheel = await waitFor("fullscreen Pi 1.0 routes wheel scroll to all", async () => {
|
|
298
|
+
const output = await pane(socket, session);
|
|
299
|
+
const position = output.match(/(\d+-\d+\/\d+)\s*$/m)?.[1];
|
|
300
|
+
return position && position !== atStart ? output : false;
|
|
301
|
+
});
|
|
302
|
+
assert.match(wheel, /Rarebits ·/);
|
|
303
|
+
}
|
|
304
|
+
await press(socket, session, "g");
|
|
305
|
+
const all = await waitFor("all view shows older entries and compaction marker", async () => {
|
|
306
|
+
const output = await pane(socket, session);
|
|
307
|
+
return output.includes("PRECOMPACTION_RAREBIT") && /compaction/i.test(output) ? output : false;
|
|
308
|
+
});
|
|
309
|
+
assert.match(all, /PRECOMPACTION_RAREBIT/);
|
|
310
|
+
assert.match(all, /compaction/i);
|
|
311
|
+
|
|
312
|
+
// Search must accept both directions, preserve the query for n/N, and show
|
|
313
|
+
// the live query/count in the overlay footer. The source sentinels make the
|
|
314
|
+
// search interaction deterministic without a model or external fixture.
|
|
315
|
+
await press(socket, session, "/");
|
|
316
|
+
await keys(socket, session, "PRECOMPACTION_RAREBIT");
|
|
317
|
+
await press(socket, session, "Enter");
|
|
318
|
+
const forwardSearch = await waitFor("forward search result", async () => {
|
|
319
|
+
const output = await pane(socket, session);
|
|
320
|
+
return output.includes("/PRECOMPACTION_RAREBIT · 1/2 · n/N") ? output : false;
|
|
321
|
+
});
|
|
322
|
+
assert.match(forwardSearch, /PRECOMPACTION_RAREBIT/);
|
|
323
|
+
await press(socket, session, "n");
|
|
324
|
+
const nextMatch = await waitFor("n advances search match", async () => {
|
|
325
|
+
const output = await pane(socket, session);
|
|
326
|
+
return output.includes("/PRECOMPACTION_RAREBIT · 2/2 · n/N") ? output : false;
|
|
327
|
+
});
|
|
328
|
+
assert.match(nextMatch, /2\/2/);
|
|
329
|
+
await press(socket, session, "N");
|
|
330
|
+
const previousMatch = await waitFor("N reverses search match", async () => {
|
|
331
|
+
const output = await pane(socket, session);
|
|
332
|
+
return output.includes("/PRECOMPACTION_RAREBIT · 1/2 · n/N") ? output : false;
|
|
333
|
+
});
|
|
334
|
+
assert.match(previousMatch, /1\/2/);
|
|
335
|
+
await press(socket, session, "N");
|
|
336
|
+
const topWrap = await waitFor("N wrap notice", async () => {
|
|
337
|
+
const output = await pane(socket, session);
|
|
338
|
+
return output.includes("search hit TOP, continuing at BOTTOM") ? output : false;
|
|
339
|
+
});
|
|
340
|
+
assert.match(topWrap, /search hit TOP, continuing at BOTTOM/);
|
|
341
|
+
await press(socket, session, "n");
|
|
342
|
+
const bottomWrap = await waitFor("n wrap notice", async () => {
|
|
343
|
+
const output = await pane(socket, session);
|
|
344
|
+
return output.includes("search hit BOTTOM, continuing at TOP") ? output : false;
|
|
345
|
+
});
|
|
346
|
+
assert.match(bottomWrap, /search hit BOTTOM, continuing at TOP/);
|
|
347
|
+
await press(socket, session, "q");
|
|
348
|
+
await waitFor("close view after wrap checks", async () => !(await pane(socket, session)).includes("Rarebits ·"));
|
|
349
|
+
await submitSlash(socket, session, "/rarebit view all");
|
|
350
|
+
await waitFor("reopen all for reverse search", async () => (await pane(socket, session)).includes("Rarebits ·"));
|
|
351
|
+
await press(socket, session, "?");
|
|
352
|
+
await keys(socket, session, "CURRENT_FINAL_RAREBIT");
|
|
353
|
+
await press(socket, session, "Enter");
|
|
354
|
+
const reverseSearch = await waitFor("reverse search result", async () => {
|
|
355
|
+
const output = await pane(socket, session);
|
|
356
|
+
return output.includes("?CURRENT_FINAL_RAREBIT · 1/1 · n/N") ? output : false;
|
|
357
|
+
});
|
|
358
|
+
assert.match(reverseSearch, /CURRENT_FINAL_RAREBIT/);
|
|
359
|
+
|
|
360
|
+
const beforeScroll = await pane(socket, session);
|
|
361
|
+
await press(socket, session, "u");
|
|
362
|
+
const up = await waitFor("up half-page movement", async () => {
|
|
363
|
+
const output = await pane(socket, session);
|
|
364
|
+
return output !== beforeScroll ? output : false;
|
|
365
|
+
});
|
|
366
|
+
await press(socket, session, "d");
|
|
367
|
+
const down = await waitFor("down half-page movement", async () => {
|
|
368
|
+
const output = await pane(socket, session);
|
|
369
|
+
return output !== up ? output : false;
|
|
370
|
+
});
|
|
371
|
+
assert.notEqual(down, up);
|
|
372
|
+
|
|
373
|
+
// With the search active, Esc first clears it. The next Esc closes the view.
|
|
374
|
+
await press(socket, session, "Escape");
|
|
375
|
+
const cleared = await waitFor("search cleared without closing overlay", async () => {
|
|
376
|
+
const output = await pane(socket, session);
|
|
377
|
+
return output.includes("Rarebits") && output.includes("j/k · u/d") && !output.includes("?CURRENT_FINAL_RAREBIT ·") ? output : false;
|
|
378
|
+
});
|
|
379
|
+
assert.match(cleared, /Rarebits/);
|
|
380
|
+
await press(socket, session, "Escape");
|
|
381
|
+
await waitFor("Escape closes overlay", async () => !(await pane(socket, session)).includes("Rarebits ·"));
|
|
382
|
+
|
|
383
|
+
await submitSlash(socket, session, "/rarebit view all");
|
|
384
|
+
await waitFor("all view reopens", async () => (await pane(socket, session)).includes("Rarebits ·"));
|
|
385
|
+
await press(socket, session, "q");
|
|
386
|
+
await waitFor("q closes capturing overlay", async () => !(await pane(socket, session)).includes("Rarebits ·"));
|
|
387
|
+
|
|
388
|
+
await submitSlash(socket, session, "/rarebit view peek");
|
|
389
|
+
const peekInitial = await waitFor("peek overlay", async () => {
|
|
390
|
+
const output = await pane(socket, session);
|
|
391
|
+
return output.includes("Rarebits ·") && output.includes("Rarebit view: peek") ? output : false;
|
|
392
|
+
});
|
|
393
|
+
assert.match(peekInitial, /keys go to the editor · wheel scrolls · ctrl\+alt\+r closes · replies appear when finished/);
|
|
394
|
+
await submitSlash(socket, session, "/rarebit view");
|
|
395
|
+
const picker = await waitFor("view picker closes peek first", async () => {
|
|
396
|
+
const output = await pane(socket, session);
|
|
397
|
+
return output.includes("Rarebit view") && !output.includes("Rarebits ·") ? output : false;
|
|
398
|
+
});
|
|
399
|
+
assert.match(picker, /all — All Rarebits/);
|
|
400
|
+
await press(socket, session, "Escape");
|
|
401
|
+
await waitFor("view picker closes", async () => !(await pane(socket, session)).includes("all — All Rarebits"));
|
|
402
|
+
|
|
403
|
+
await submitSlash(socket, session, "/rarebit view peek");
|
|
404
|
+
await waitFor("peek reopens before Rarebit palette", async () => (await pane(socket, session)).includes("Rarebits ·"));
|
|
405
|
+
await submitSlash(socket, session, "/rarebit");
|
|
406
|
+
const palette = await waitFor("Rarebit palette closes peek first", async () => {
|
|
407
|
+
const output = await pane(socket, session);
|
|
408
|
+
return output.includes("Human-only command palette") && !output.includes("Rarebits ·") ? output : false;
|
|
409
|
+
});
|
|
410
|
+
assert.match(palette, /Command help/);
|
|
411
|
+
await press(socket, session, "Escape");
|
|
412
|
+
await waitFor("Rarebit palette closes", async () => !(await pane(socket, session)).includes("Human-only command palette"));
|
|
413
|
+
|
|
414
|
+
await submitSlash(socket, session, "/rarebit view peek");
|
|
415
|
+
await waitFor("peek reopens before settings", async () => (await pane(socket, session)).includes("Rarebits ·"));
|
|
416
|
+
await submitSlash(socket, session, "/rarebit settings");
|
|
417
|
+
const settings = await waitFor("Rarebit settings closes peek first", async () => {
|
|
418
|
+
const output = await pane(socket, session);
|
|
419
|
+
return output.includes("Rarebit Settings") && !output.includes("Rarebits ·") ? output : false;
|
|
420
|
+
});
|
|
421
|
+
assert.match(settings, /Summary/);
|
|
422
|
+
await press(socket, session, "Escape");
|
|
423
|
+
await waitFor("settings closes", async () => !(await pane(socket, session)).includes("Rarebit Settings"));
|
|
424
|
+
|
|
425
|
+
await submitSlash(socket, session, "/rarebit view peek");
|
|
426
|
+
await waitFor("peek reopens for editor check", async () => (await pane(socket, session)).includes("Rarebits ·"));
|
|
427
|
+
await keys(socket, session, "PEEK_EDITOR_INPUT_SENTINEL");
|
|
428
|
+
const peek = await waitFor("editor accepts input with peek open", async () => {
|
|
429
|
+
const output = await pane(socket, session);
|
|
430
|
+
return output.includes("PEEK_EDITOR_INPUT_SENTINEL") && output.includes("Rarebits ·") ? output : false;
|
|
431
|
+
});
|
|
432
|
+
assert.match(peek, /PEEK_EDITOR_INPUT_SENTINEL/);
|
|
433
|
+
assert.match(peek, /Rarebits ·/);
|
|
434
|
+
await press(socket, session, "C-u");
|
|
435
|
+
await submitSlash(socket, session, "/rarebit view off");
|
|
436
|
+
await waitFor("off closes peek view", async () => !(await pane(socket, session)).includes("Rarebits ·"));
|
|
437
|
+
|
|
438
|
+
// Pi lifecycle boundaries must dispose the view before a fresh transcript
|
|
439
|
+
// or extension registration appears. Use only built-in /reload and /resume.
|
|
440
|
+
const assertNormalCurrentSession = async (marker, label) => {
|
|
441
|
+
const output = await waitFor(label, async () => {
|
|
442
|
+
const current = await pane(socket, session);
|
|
443
|
+
return current.includes(marker) && !current.includes("Rarebits ·") && !current.includes("Rarebit view:") ? current : false;
|
|
444
|
+
});
|
|
445
|
+
assert.match(output, new RegExp(marker));
|
|
446
|
+
assert.doesNotMatch(output, /Rarebits ·|Rarebit view:/);
|
|
447
|
+
return output;
|
|
448
|
+
};
|
|
449
|
+
await submitSlash(socket, session, "/rarebit view context");
|
|
450
|
+
await waitFor("context active before reload", async () => (await pane(socket, session)).includes("Rarebit view: context"));
|
|
451
|
+
await submitSlash(socket, session, "/reload");
|
|
452
|
+
await assertNormalCurrentSession("CURRENT_TOOL_OUTPUT_SENTINEL", "reload resets context to normal transcript");
|
|
453
|
+
await submitSlash(socket, session, "/rarebit view context");
|
|
454
|
+
await waitFor("context active before Session switch", async () => (await pane(socket, session)).includes("Rarebit view: context"));
|
|
455
|
+
await submitSlash(socket, session, "/resume");
|
|
456
|
+
await waitFor("Session picker shows second mock", async () => (await pane(socket, session)).includes("SECOND_PRECOMPACTION_RAREBIT"));
|
|
457
|
+
await keys(socket, session, "SECOND_PRECOMPACTION_RAREBIT");
|
|
458
|
+
await sleep(250);
|
|
459
|
+
await press(socket, session, "Enter");
|
|
460
|
+
await assertNormalCurrentSession("SECOND_FINAL_RAREBIT", "Session switch resets context to second normal transcript");
|
|
461
|
+
await submitSlash(socket, session, "/rarebit view context");
|
|
462
|
+
const secondContext = await waitFor("reopened context shows second Session", async () => {
|
|
463
|
+
const output = await pane(socket, session);
|
|
464
|
+
return output.includes("SECOND_SESSION_RAREBIT") && output.includes("Rarebit view: context") ? output : false;
|
|
465
|
+
});
|
|
466
|
+
assert.match(secondContext, /SECOND_SESSION_RAREBIT/);
|
|
467
|
+
assert.doesNotMatch(secondContext, /CURRENT_CONTEXT_RAREBIT|Rarebits ·/);
|
|
468
|
+
await submitSlash(socket, session, "/rarebit view peek");
|
|
469
|
+
await waitFor("peek active before second Session switch", async () => (await pane(socket, session)).includes("Rarebit view: peek"));
|
|
470
|
+
await submitSlash(socket, session, "/resume");
|
|
471
|
+
await waitFor("Session picker shows first mock", async () => (await pane(socket, session)).includes("PRECOMPACTION_RAREBIT"));
|
|
472
|
+
await keys(socket, session, "PRECOMPACTION_RAREBIT");
|
|
473
|
+
await sleep(250);
|
|
474
|
+
await press(socket, session, "Enter");
|
|
475
|
+
await assertNormalCurrentSession("CURRENT_FINAL_RAREBIT", "Session switch resets peek to first normal transcript");
|
|
476
|
+
await submitSlash(socket, session, "/rarebit view peek");
|
|
477
|
+
const firstPeek = await waitFor("reopened peek shows first Session", async () => {
|
|
478
|
+
const output = await pane(socket, session);
|
|
479
|
+
return output.includes("CURRENT_FINAL_RAREBIT") && output.includes("Rarebit view: peek") ? output : false;
|
|
480
|
+
});
|
|
481
|
+
assert.doesNotMatch(firstPeek, /SECOND_FINAL_RAREBIT/);
|
|
482
|
+
await submitSlash(socket, session, "/reload");
|
|
483
|
+
await assertNormalCurrentSession("CURRENT_TOOL_OUTPUT_SENTINEL", "reload resets peek to normal transcript");
|
|
484
|
+
|
|
485
|
+
const finalOff = await waitFor("status absent after off", async () => {
|
|
486
|
+
const output = await pane(socket, session);
|
|
487
|
+
return !output.includes("Rarebit view:") ? output : false;
|
|
488
|
+
});
|
|
489
|
+
assert.doesNotMatch(finalOff, /Rarebit view:/);
|
|
490
|
+
const assertions = ["R1 selected Thinking... prose with and without thinking blocks", "R2 context hides pre-attach notices and keeps /rarebit status", "context filters tool/thinking rows and shows Recap beside compaction", "R3 all footer cycle hint", "R4 search wrap notices and / ? n N", "first toggle defaults to all", "Ctrl+Super+R/N Kitty sequences", "preferred mode restored on toggle", "resize-aware all layout", "u/d scroll", "Esc clears then closes; q closes", "peek editor input and footer", "picker, palette, and settings close view", "R6 /reload and Session switch reset context/peek; reopen shows current Session", "off clears status"];
|
|
491
|
+
if (piVersion === "1.0.0" && tuiMode === "fullscreen") assertions.push("fullscreen wheel scrolling");
|
|
492
|
+
console.log(JSON.stringify({ ok: defects.length === 0, pi: piVersion, tuiMode, assertions, defects }));
|
|
493
|
+
if (defects.length) process.exitCode = 1;
|
|
494
|
+
} catch (error) {
|
|
495
|
+
console.log(JSON.stringify({ ok: false, pi: piVersion, tuiMode, completedAssertions: ["R1 selected Thinking... prose", "R2 pre-attach notice hidden", "context Recap and compaction visible", "R3 footer cycle hint", "R4 both search wrap notices", "R6 /reload reset"], defects: [...defects, error?.message ?? String(error)] }));
|
|
496
|
+
process.exitCode = 1;
|
|
497
|
+
} finally {
|
|
498
|
+
await execFile(tmux, ["-L", socket, "kill-server"]).catch(() => {});
|
|
499
|
+
await rm(temp, { recursive: true, force: true });
|
|
500
|
+
}
|
package/src/extension.mjs
CHANGED
|
@@ -42,6 +42,7 @@ import {
|
|
|
42
42
|
registerRarebitLifecycle,
|
|
43
43
|
} from "./lifecycle.mjs";
|
|
44
44
|
import { createRarebitRecapController } from "./rarebit-recap.mjs";
|
|
45
|
+
import { createRarebitViewController } from "./rarebit-view.mjs";
|
|
45
46
|
import { readRarebitCurrent } from "./rarebit-store.mjs";
|
|
46
47
|
import {
|
|
47
48
|
openRarebitPalette,
|
|
@@ -140,6 +141,13 @@ export default function registerPiRarebit(pi, config = {}) {
|
|
|
140
141
|
delayMs: explicit.recap?.delayMs,
|
|
141
142
|
timezone: explicit.recap?.timezone,
|
|
142
143
|
});
|
|
144
|
+
const viewController = createRarebitViewController();
|
|
145
|
+
for (const key of ["ctrl+alt+r", "ctrl+super+r"])
|
|
146
|
+
pi.registerShortcut?.(key, { description: "Toggle Rarebit view", handler: (ctx) => viewController.toggle(ctx) });
|
|
147
|
+
for (const key of ["ctrl+alt+n", "ctrl+super+n"])
|
|
148
|
+
pi.registerShortcut?.(key, { description: "Next Rarebit view", handler: (ctx) => viewController.cycle(ctx) });
|
|
149
|
+
pi.on("session_compact", () => viewController.refresh());
|
|
150
|
+
pi.on("message_end", () => viewController.refresh());
|
|
143
151
|
pi.registerEntryRenderer?.("rarebit-recap", recapController.renderEntry);
|
|
144
152
|
const acknowledgeRecap = (ctx) => {
|
|
145
153
|
const result = recapController.acknowledge(ctx);
|
|
@@ -455,6 +463,7 @@ export default function registerPiRarebit(pi, config = {}) {
|
|
|
455
463
|
|
|
456
464
|
registerRarebitLifecycle(pi, schedule, {
|
|
457
465
|
onSessionStart: (ctx, generation) => {
|
|
466
|
+
viewController.dispose();
|
|
458
467
|
recapController.invalidate(ctx, generation);
|
|
459
468
|
activeSession = { ...identityFrom(ctx), generation };
|
|
460
469
|
activityReporter.start(projectActivity(ctx));
|
|
@@ -469,6 +478,7 @@ export default function registerPiRarebit(pi, config = {}) {
|
|
|
469
478
|
recapController.invalidate(ctx, generation);
|
|
470
479
|
},
|
|
471
480
|
onSessionTree: (ctx, _event, generation) => {
|
|
481
|
+
viewController.refresh();
|
|
472
482
|
recapController.invalidate(ctx, generation);
|
|
473
483
|
activityReporter.update(projectActivity(ctx));
|
|
474
484
|
},
|
|
@@ -476,11 +486,13 @@ export default function registerPiRarebit(pi, config = {}) {
|
|
|
476
486
|
activityReporter.update(projectActivity(ctx));
|
|
477
487
|
},
|
|
478
488
|
onAgentSettled: (ctx) => {
|
|
489
|
+
viewController.refresh();
|
|
479
490
|
recapController.updateContext(ctx);
|
|
480
491
|
activityReporter.update(projectActivity(ctx));
|
|
481
492
|
},
|
|
482
|
-
onSessionShutdown: (
|
|
483
|
-
|
|
493
|
+
onSessionShutdown: () => {
|
|
494
|
+
viewController.dispose();
|
|
495
|
+
recapController.dispose();
|
|
484
496
|
activeSession = undefined;
|
|
485
497
|
activityReporter.stop();
|
|
486
498
|
},
|
|
@@ -531,25 +543,43 @@ export default function registerPiRarebit(pi, config = {}) {
|
|
|
531
543
|
};
|
|
532
544
|
const recallMessage = (recall, requestText) => `# Rarebit Recall
|
|
533
545
|
|
|
534
|
-
*Use these local private
|
|
535
|
-
|
|
536
|
-
## How to use this bundle
|
|
537
|
-
|
|
538
|
-
1. Treat **Current request** below as the exact current user request.
|
|
539
|
-
2. Read **Conversation** first for meaning.
|
|
540
|
-
3. Use **Detailed evidence** only for source, Session, branch, or lineage facts.
|
|
541
|
-
4. Answer the current request using this evidence.
|
|
546
|
+
*Answer the **Current request** below. Use these local private files to recover earlier context from this Session.*
|
|
542
547
|
|
|
543
|
-
##
|
|
548
|
+
## Files
|
|
544
549
|
|
|
545
|
-
**Conversation** — \`rarebit_conversation/v${RAREBIT_CONVERSATION_SCHEMA_VERSION}
|
|
550
|
+
**Conversation** — \`rarebit_conversation/v${RAREBIT_CONVERSATION_SCHEMA_VERSION}\`. Read this first.
|
|
546
551
|
|
|
547
552
|
${fence(recall.conversationPath)}
|
|
548
553
|
|
|
549
|
-
**Detailed evidence** — \`rarebit_message_recall/v${RAREBIT_RECALL_SCHEMA_VERSION}\`
|
|
554
|
+
**Detailed evidence** — \`rarebit_message_recall/v${RAREBIT_RECALL_SCHEMA_VERSION}\`. Use it only for source, Session, branch, or lineage facts. Join on \`seq\`.
|
|
550
555
|
|
|
551
556
|
${fence(recall.detailedPath)}
|
|
552
557
|
|
|
558
|
+
## Conversation schema
|
|
559
|
+
|
|
560
|
+
\`\`\`text
|
|
561
|
+
{ messageCount, lastRound, messages: [{ seq, round, role, kind, time, text }] }
|
|
562
|
+
seq 1-based, oldest first
|
|
563
|
+
round increments at each user message; agent messages before the first user message have round 0
|
|
564
|
+
role "user" | "agent"
|
|
565
|
+
kind "request" (user) | "progress" (agent text before a tool call) | "reply" (agent final answer)
|
|
566
|
+
time UTC "YYYY-MM-DDTHH:MM:SSZ", or null
|
|
567
|
+
\`\`\`
|
|
568
|
+
|
|
569
|
+
Messages contain user and agent text only. Tool calls and tool results are excluded.
|
|
570
|
+
|
|
571
|
+
## Example queries
|
|
572
|
+
|
|
573
|
+
Set \`F\` to the Conversation path. Read the smallest slice that answers the request.
|
|
574
|
+
|
|
575
|
+
\`\`\`sh
|
|
576
|
+
jq '.messages[-20:]' "$F" # last 20 messages
|
|
577
|
+
jq '[.messages[] | select(.role == "user")][-3:]' "$F" # last 3 user messages
|
|
578
|
+
jq '[.messages[] | select(.kind == "reply")][-10:]' "$F" # last 10 agent replies
|
|
579
|
+
jq '.lastRound as $r | [.messages[] | select(.round > $r - 5)]' "$F" # last 5 rounds
|
|
580
|
+
jq '(now - 8*3600 | todate) as $t | [.messages[] | select(.time >= $t)]' "$F" # last 8 hours
|
|
581
|
+
\`\`\`
|
|
582
|
+
|
|
553
583
|
## Current request
|
|
554
584
|
|
|
555
585
|
${fence(requestText)}`;
|
|
@@ -561,6 +591,7 @@ ${fence(requestText)}`;
|
|
|
561
591
|
let commandInput = String(args ?? "");
|
|
562
592
|
const trimmedCommandInput = commandInput.trim();
|
|
563
593
|
if (!trimmedCommandInput || trimmedCommandInput === "menu") {
|
|
594
|
+
viewController.dispose();
|
|
564
595
|
const currentUiState = () => ({
|
|
565
596
|
...identityFrom(ctx),
|
|
566
597
|
branchLeafId: ctx?.sessionManager?.getBranch?.()?.at(-1)?.id ?? null,
|
|
@@ -611,7 +642,12 @@ ${fence(requestText)}`;
|
|
|
611
642
|
);
|
|
612
643
|
return;
|
|
613
644
|
}
|
|
645
|
+
if (subcommand === "view") {
|
|
646
|
+
await viewController.command(ctx, rest[0]);
|
|
647
|
+
return;
|
|
648
|
+
}
|
|
614
649
|
if (subcommand === "settings") {
|
|
650
|
+
viewController.dispose();
|
|
615
651
|
const currentUiState = () => ({
|
|
616
652
|
...identityFrom(ctx),
|
|
617
653
|
branchLeafId: ctx?.sessionManager?.getBranch?.()?.at(-1)?.id ?? null,
|
|
@@ -776,7 +812,7 @@ ${fence(requestText)}`;
|
|
|
776
812
|
: "settings_or_default";
|
|
777
813
|
notify(
|
|
778
814
|
ctx,
|
|
779
|
-
`Rarebit ${subcommand}: auto-title=${autoTitleOverride ?? effective.autoTitle ?? true}; recap=${effective.recap?.enabled !== false ? "on" : "off"} after ${effective.recap?.delayMs ?? 60000}ms (${effective.recap?.timezone ?? "host"}); diagnostics=triggered:${effective.diagnostics?.summaryTriggered === true ? "on" : "off"},updated:${effective.diagnostics?.summaryUpdated === true ? "on" : "off"}; max_input_tokens=${effective.maxInputTokens ?? 64000}; max_rarebit_ratio=${policy.maxRarebitRatio} (${ratioSource}); min_total_length=${policy.minTotalLength} estimated tokens via ceil(chars/4) (${lengthSource}); measurement=${policy.measurementVersion}; model=${modelLabel(effective.model)} from ${effective.modelProvenance?.settingsKey ?? "explicit config"}${policyStatus}`,
|
|
815
|
+
`Rarebit ${subcommand}: view=${viewController.activeMode}; auto-title=${autoTitleOverride ?? effective.autoTitle ?? true}; recap=${effective.recap?.enabled !== false ? "on" : "off"} after ${effective.recap?.delayMs ?? 60000}ms (${effective.recap?.timezone ?? "host"}); diagnostics=triggered:${effective.diagnostics?.summaryTriggered === true ? "on" : "off"},updated:${effective.diagnostics?.summaryUpdated === true ? "on" : "off"}; max_input_tokens=${effective.maxInputTokens ?? 64000}; max_rarebit_ratio=${policy.maxRarebitRatio} (${ratioSource}); min_total_length=${policy.minTotalLength} estimated tokens via ceil(chars/4) (${lengthSource}); measurement=${policy.measurementVersion}; model=${modelLabel(effective.model)} from ${effective.modelProvenance?.settingsKey ?? "explicit config"}${policyStatus}`,
|
|
780
816
|
"info",
|
|
781
817
|
);
|
|
782
818
|
return;
|
package/src/rarebit-command.mjs
CHANGED
|
@@ -29,6 +29,11 @@ export const RAREBIT_COMMAND_GRAMMAR = Object.freeze({
|
|
|
29
29
|
description: "Open interactive Rarebit settings",
|
|
30
30
|
forms: [[], [literal("global", "Edit global Pi settings")], [literal("project", "Edit trusted project settings")]],
|
|
31
31
|
},
|
|
32
|
+
{
|
|
33
|
+
name: "view",
|
|
34
|
+
description: "Read Rarebits in the TUI",
|
|
35
|
+
forms: [[], [literal("context", "Current context only (experimental)")], [literal("all", "All Rarebits across compactions")], [literal("peek", "All Rarebits with the editor live")], [literal("off", "Normal transcript")]],
|
|
36
|
+
},
|
|
32
37
|
{
|
|
33
38
|
name: "help",
|
|
34
39
|
description: "Show Rarebit command help",
|