pi-focus-mode 0.1.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/LICENSE +19 -0
- package/README.md +203 -0
- package/focus-mode.js +2097 -0
- package/package.json +38 -0
package/focus-mode.js
ADDED
|
@@ -0,0 +1,2097 @@
|
|
|
1
|
+
// extensions/focus-mode/focus-mode.ts
|
|
2
|
+
import { AssistantMessageComponent, ToolExecutionComponent } from "@earendil-works/pi-coding-agent";
|
|
3
|
+
|
|
4
|
+
// shared/announce.ts
|
|
5
|
+
var stderrAnnounced = /* @__PURE__ */ new Set();
|
|
6
|
+
function announce(ctx, message, level, reason = message) {
|
|
7
|
+
if (ctx?.hasUI === false) {
|
|
8
|
+
if (stderrAnnounced.has(reason)) return;
|
|
9
|
+
stderrAnnounced.add(reason);
|
|
10
|
+
try {
|
|
11
|
+
process.stderr.write(`${message}
|
|
12
|
+
`);
|
|
13
|
+
} catch {
|
|
14
|
+
}
|
|
15
|
+
return;
|
|
16
|
+
}
|
|
17
|
+
try {
|
|
18
|
+
ctx?.ui?.notify?.(message, level);
|
|
19
|
+
} catch {
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
// shared/settings.ts
|
|
24
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
25
|
+
import { join } from "node:path";
|
|
26
|
+
import { CONFIG_DIR_NAME, getAgentDir } from "@earendil-works/pi-coding-agent";
|
|
27
|
+
var announcedConfigErrors = /* @__PURE__ */ new Set();
|
|
28
|
+
function readConfig(path, ctx) {
|
|
29
|
+
if (!existsSync(path)) return {};
|
|
30
|
+
try {
|
|
31
|
+
return JSON.parse(readFileSync(path, "utf8"));
|
|
32
|
+
} catch (error) {
|
|
33
|
+
if (!announcedConfigErrors.has(path)) {
|
|
34
|
+
announcedConfigErrors.add(path);
|
|
35
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
36
|
+
announce(ctx, `settings: could not parse ${path} (${message}); using defaults.`, "warning", `settings-parse:${path}`);
|
|
37
|
+
}
|
|
38
|
+
return {};
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
function resolveSettings(name, definitions, context, runtime = {}) {
|
|
42
|
+
const environment = runtime.environment ?? process.env;
|
|
43
|
+
const globalPath = join(runtime.agentDir ?? getAgentDir(), `${name}.json`);
|
|
44
|
+
const projectPath = join(context.cwd, CONFIG_DIR_NAME, `${name}.json`);
|
|
45
|
+
const globalConfig = readConfig(globalPath, context);
|
|
46
|
+
const projectConfig = context.isProjectTrusted() ? readConfig(projectPath, context) : {};
|
|
47
|
+
const resolved = {};
|
|
48
|
+
for (const key of Object.keys(definitions)) {
|
|
49
|
+
const definition = definitions[key];
|
|
50
|
+
let value = definition.default;
|
|
51
|
+
let provenance = { source: "default" };
|
|
52
|
+
if (definition.discover) {
|
|
53
|
+
try {
|
|
54
|
+
const discovered = definition.discover();
|
|
55
|
+
if (discovered !== void 0) {
|
|
56
|
+
value = discovered.value;
|
|
57
|
+
provenance = { source: "discovered", name: definition.discoverName ?? "discovery" };
|
|
58
|
+
}
|
|
59
|
+
} catch {
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
if (Object.hasOwn(globalConfig, key)) {
|
|
63
|
+
value = globalConfig[key];
|
|
64
|
+
provenance = { source: "global", path: globalPath };
|
|
65
|
+
}
|
|
66
|
+
if (Object.hasOwn(projectConfig, key)) {
|
|
67
|
+
value = projectConfig[key];
|
|
68
|
+
provenance = { source: "project", path: projectPath };
|
|
69
|
+
}
|
|
70
|
+
const environmentValue = environment[definition.env];
|
|
71
|
+
if (environmentValue !== void 0) {
|
|
72
|
+
value = definition.parseEnv ? definition.parseEnv(environmentValue) : environmentValue;
|
|
73
|
+
provenance = { source: "environment", name: definition.env };
|
|
74
|
+
}
|
|
75
|
+
resolved[key] = { value, provenance };
|
|
76
|
+
}
|
|
77
|
+
return resolved;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// extensions/focus-mode/src/fold-picker.ts
|
|
81
|
+
import { Key, matchesKey, truncateToWidth as truncateToWidth2, visibleWidth as visibleWidth2 } from "@earendil-works/pi-tui";
|
|
82
|
+
|
|
83
|
+
// extensions/focus-mode/src/thinking-width.ts
|
|
84
|
+
import { truncateToWidth, visibleWidth } from "@earendil-works/pi-tui";
|
|
85
|
+
function fitThinkingLine(text, width) {
|
|
86
|
+
const start = Math.max(-1, ...[...text.matchAll(/◈ (?!\d+(?: |$))/g)].map((match) => match.index));
|
|
87
|
+
const suffixStart = start < 0 ? -1 : text.indexOf(" \xB7 ", start);
|
|
88
|
+
if (suffixStart < 0) return truncateToWidth(text, width, "\u2026").replace(/\x1b\[0m(?=…)/g, "");
|
|
89
|
+
const suffix = text.slice(suffixStart);
|
|
90
|
+
if (visibleWidth(suffix) >= width) return truncateToWidth(text, width, "\u2026").replace(/\x1b\[0m(?=…)/g, "");
|
|
91
|
+
const available = Math.max(0, width - visibleWidth(suffix));
|
|
92
|
+
if (visibleWidth(text) <= width) return text;
|
|
93
|
+
return truncateToWidth(text.slice(0, suffixStart), available, "\u2026").replace(/\x1b\[0m(?=…)/g, "") + suffix;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// extensions/focus-mode/src/fold-picker.ts
|
|
97
|
+
var FoldPicker = class {
|
|
98
|
+
selected = 0;
|
|
99
|
+
model;
|
|
100
|
+
getTheme;
|
|
101
|
+
requestRender;
|
|
102
|
+
done;
|
|
103
|
+
constructor(model, getTheme, requestRender, done) {
|
|
104
|
+
this.model = model;
|
|
105
|
+
this.getTheme = getTheme;
|
|
106
|
+
this.requestRender = requestRender;
|
|
107
|
+
this.done = done;
|
|
108
|
+
}
|
|
109
|
+
items() {
|
|
110
|
+
const items = [];
|
|
111
|
+
let exchange = 0;
|
|
112
|
+
for (const process2 of this.model.processes()) {
|
|
113
|
+
if (process2.exchange !== exchange) {
|
|
114
|
+
exchange = process2.exchange;
|
|
115
|
+
const id = exchange;
|
|
116
|
+
items.push({ label: `${this.model.isExchangeOpen(id) ? "\u25BE" : "\u25B8"} Exchange ${id}`, toggle: () => {
|
|
117
|
+
this.model.toggleExchange(id);
|
|
118
|
+
} });
|
|
119
|
+
}
|
|
120
|
+
items.push({ label: ` ${this.model.processLine(process2.id)}`, toggle: () => {
|
|
121
|
+
this.model.toggleProcess(process2.id);
|
|
122
|
+
} });
|
|
123
|
+
for (const block of process2.blocks) {
|
|
124
|
+
if (block.kind === "thinking") {
|
|
125
|
+
items.push({ label: ` ${this.model.isThinkingOpen(block.message, block.index) ? "\u25BE" : "\u25B8"} ${this.model.thinkingTitle(block.message, block.index, block.trace, false)}`, toggle: () => {
|
|
126
|
+
if (this.model.toggleThinking(block.message, block.index) && !this.model.isProcessOpen(process2.id)) this.model.toggleProcess(process2.id);
|
|
127
|
+
} });
|
|
128
|
+
} else {
|
|
129
|
+
const id = block.key.slice(5);
|
|
130
|
+
const parts = this.model.titleParts(id);
|
|
131
|
+
items.push({ label: ` ${this.model.isOpen(id) ? "\u25BE" : "\u25B8"} \u2699 ${parts?.name ?? "tool"} ${parts?.argument ?? ""} ${parts?.stats ?? ""}`, toggle: () => {
|
|
132
|
+
if (this.model.toggle(id) && !this.model.isProcessOpen(process2.id)) this.model.toggleProcess(process2.id);
|
|
133
|
+
} });
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
return items;
|
|
138
|
+
}
|
|
139
|
+
render(width) {
|
|
140
|
+
const rows = this.items();
|
|
141
|
+
const theme = this.getTheme();
|
|
142
|
+
const fit = (content) => {
|
|
143
|
+
const clipped = truncateToWidth2(content, width, "\u2026");
|
|
144
|
+
return theme.bg("customMessageBg", clipped) + theme.bg("customMessageBg", " ".repeat(Math.max(0, width - visibleWidth2(clipped))));
|
|
145
|
+
};
|
|
146
|
+
const styleText = (content, availableWidth = width) => {
|
|
147
|
+
const clipped = truncateToWidth2(content, availableWidth, "\u2026").replaceAll("\x1B[0m", "");
|
|
148
|
+
const colored = theme.fg("dim", clipped);
|
|
149
|
+
return theme.italic?.(colored) ?? colored;
|
|
150
|
+
};
|
|
151
|
+
const border = fit(theme.fg("dim", "\u2500".repeat(width)));
|
|
152
|
+
const lines = [border, fit(styleText("Fold exchange / process / block"))];
|
|
153
|
+
if (!rows.length) return [...lines, fit(styleText(" No blocks yet")), border];
|
|
154
|
+
this.selected = Math.min(this.selected, rows.length - 1);
|
|
155
|
+
return [...lines, ...rows.map((row, index) => {
|
|
156
|
+
const mark = index === this.selected ? ">" : " ";
|
|
157
|
+
return fit(mark + styleText(fitThinkingLine(` ${row.label}`, Math.max(0, width - 1)), Math.max(0, width - 1)));
|
|
158
|
+
}), border];
|
|
159
|
+
}
|
|
160
|
+
handleInput(data) {
|
|
161
|
+
const rows = this.items();
|
|
162
|
+
if (matchesKey(data, Key.escape)) {
|
|
163
|
+
this.done();
|
|
164
|
+
return;
|
|
165
|
+
}
|
|
166
|
+
if (matchesKey(data, Key.up)) this.selected = Math.max(0, this.selected - 1);
|
|
167
|
+
else if (matchesKey(data, Key.down)) this.selected = Math.min(rows.length - 1, this.selected + 1);
|
|
168
|
+
else if (matchesKey(data, Key.enter)) rows[this.selected]?.toggle();
|
|
169
|
+
else return;
|
|
170
|
+
this.requestRender();
|
|
171
|
+
}
|
|
172
|
+
invalidate() {
|
|
173
|
+
}
|
|
174
|
+
};
|
|
175
|
+
|
|
176
|
+
// extensions/focus-mode/src/transcript-cursor.ts
|
|
177
|
+
import { Key as Key2, matchesKey as matchesKey2 } from "@earendil-works/pi-tui";
|
|
178
|
+
var TranscriptCursor = class {
|
|
179
|
+
model;
|
|
180
|
+
changed;
|
|
181
|
+
done;
|
|
182
|
+
constructor(model, changed, done) {
|
|
183
|
+
this.model = model;
|
|
184
|
+
this.changed = changed;
|
|
185
|
+
this.done = done;
|
|
186
|
+
}
|
|
187
|
+
// The overlay owns focus while the model's existing renderers draw the highlight.
|
|
188
|
+
render() {
|
|
189
|
+
return [];
|
|
190
|
+
}
|
|
191
|
+
invalidate() {
|
|
192
|
+
}
|
|
193
|
+
handleInput(data) {
|
|
194
|
+
if (matchesKey2(data, Key2.escape)) {
|
|
195
|
+
this.model.stopCursor();
|
|
196
|
+
this.changed();
|
|
197
|
+
this.done();
|
|
198
|
+
} else if (matchesKey2(data, Key2.up)) {
|
|
199
|
+
this.model.cursorMove(-1);
|
|
200
|
+
this.changed();
|
|
201
|
+
} else if (matchesKey2(data, Key2.down)) {
|
|
202
|
+
this.model.cursorMove(1);
|
|
203
|
+
this.changed();
|
|
204
|
+
} else if (matchesKey2(data, Key2.enter)) {
|
|
205
|
+
this.model.cursorToggle();
|
|
206
|
+
this.changed();
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
};
|
|
210
|
+
|
|
211
|
+
// extensions/focus-mode/src/headline-scheduler.ts
|
|
212
|
+
var TOKEN_INTERVAL = 400;
|
|
213
|
+
var TIME_INTERVAL_MS = 6e3;
|
|
214
|
+
var TRACE_CHARS = 1500;
|
|
215
|
+
var HeadlineLengthError = class extends Error {
|
|
216
|
+
};
|
|
217
|
+
var HeadlineScheduler = class {
|
|
218
|
+
blocks = /* @__PURE__ */ new Map();
|
|
219
|
+
now;
|
|
220
|
+
timeoutMs;
|
|
221
|
+
sequence = 0;
|
|
222
|
+
inFlight = false;
|
|
223
|
+
abort;
|
|
224
|
+
timer;
|
|
225
|
+
disposed = false;
|
|
226
|
+
announcedFailure = false;
|
|
227
|
+
options;
|
|
228
|
+
constructor(options) {
|
|
229
|
+
this.options = options;
|
|
230
|
+
this.now = options.now ?? Date.now;
|
|
231
|
+
this.timeoutMs = options.timeoutMs ?? 8e3;
|
|
232
|
+
}
|
|
233
|
+
observe(key, trace) {
|
|
234
|
+
if (this.disposed || !trace) return;
|
|
235
|
+
const previous = this.blocks.get(key);
|
|
236
|
+
if (previous?.settled) return;
|
|
237
|
+
const tokens = Math.ceil(trace.length / 4);
|
|
238
|
+
if (previous) {
|
|
239
|
+
previous.trace = trace;
|
|
240
|
+
previous.tokens = tokens;
|
|
241
|
+
} else {
|
|
242
|
+
this.blocks.set(key, { trace, tokens, startedAt: this.now(), lastRequestedTokens: 0, settled: false, finalRequested: false, appliedSequence: 0 });
|
|
243
|
+
}
|
|
244
|
+
this.tick();
|
|
245
|
+
}
|
|
246
|
+
settle(key) {
|
|
247
|
+
const block = this.blocks.get(key);
|
|
248
|
+
if (!block || block.settled || this.disposed) return;
|
|
249
|
+
block.settled = true;
|
|
250
|
+
this.tick();
|
|
251
|
+
}
|
|
252
|
+
tick() {
|
|
253
|
+
if (this.disposed || this.inFlight) return;
|
|
254
|
+
const at = this.now();
|
|
255
|
+
for (const [key, block] of this.blocks) {
|
|
256
|
+
if (block.settled ? !block.finalRequested : block.tokens - block.lastRequestedTokens >= TOKEN_INTERVAL || at - (block.lastRequestedAt ?? block.startedAt) >= TIME_INTERVAL_MS) {
|
|
257
|
+
this.request(key, block, at);
|
|
258
|
+
return;
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
request(key, block, at) {
|
|
263
|
+
this.inFlight = true;
|
|
264
|
+
block.lastRequestedAt = at;
|
|
265
|
+
block.lastRequestedTokens = block.tokens;
|
|
266
|
+
if (block.settled) block.finalRequested = true;
|
|
267
|
+
const sequence = ++this.sequence;
|
|
268
|
+
const controller = new AbortController();
|
|
269
|
+
this.abort = controller;
|
|
270
|
+
const trace = block.trace.slice(-TRACE_CHARS);
|
|
271
|
+
let timer;
|
|
272
|
+
const timeout = new Promise((_resolve, reject) => {
|
|
273
|
+
timer = setTimeout(() => {
|
|
274
|
+
controller.abort();
|
|
275
|
+
reject(new Error("headline timeout"));
|
|
276
|
+
}, this.timeoutMs);
|
|
277
|
+
this.timer = timer;
|
|
278
|
+
});
|
|
279
|
+
const call = Promise.resolve().then(() => this.options.summarize(trace, controller.signal));
|
|
280
|
+
void Promise.race([call, timeout]).then((text) => {
|
|
281
|
+
if (this.disposed || sequence < block.appliedSequence) return;
|
|
282
|
+
const words = text.trim().split(/\s+/).filter(Boolean).slice(0, 10);
|
|
283
|
+
if (!words.length) throw new Error("empty headline");
|
|
284
|
+
block.appliedSequence = sequence;
|
|
285
|
+
this.options.onHeadline(key, `\u2248 ${words.join(" ")}`);
|
|
286
|
+
}).catch((error) => {
|
|
287
|
+
if (this.disposed || sequence < block.appliedSequence) return;
|
|
288
|
+
block.appliedSequence = sequence;
|
|
289
|
+
this.options.onHeadline(key, void 0);
|
|
290
|
+
if (!this.announcedFailure) {
|
|
291
|
+
this.announcedFailure = true;
|
|
292
|
+
this.options.onFailure(
|
|
293
|
+
error instanceof HeadlineLengthError ? "length" : controller.signal.aborted && error instanceof Error && error.message === "headline timeout" ? "timeout" : "error",
|
|
294
|
+
error instanceof Error ? error.message : String(error)
|
|
295
|
+
);
|
|
296
|
+
}
|
|
297
|
+
}).finally(() => {
|
|
298
|
+
if (timer) clearTimeout(timer);
|
|
299
|
+
if (this.timer === timer) this.timer = void 0;
|
|
300
|
+
if (this.abort === controller) this.abort = void 0;
|
|
301
|
+
this.inFlight = false;
|
|
302
|
+
if (block.settled && block.finalRequested) this.blocks.delete(key);
|
|
303
|
+
this.tick();
|
|
304
|
+
});
|
|
305
|
+
}
|
|
306
|
+
dispose() {
|
|
307
|
+
this.disposed = true;
|
|
308
|
+
this.abort?.abort();
|
|
309
|
+
if (this.timer) clearTimeout(this.timer);
|
|
310
|
+
this.blocks.clear();
|
|
311
|
+
}
|
|
312
|
+
};
|
|
313
|
+
|
|
314
|
+
// extensions/focus-mode/src/tool-fold.ts
|
|
315
|
+
var MAX_THINKING_MESSAGES = 256;
|
|
316
|
+
function elapsed(ms) {
|
|
317
|
+
return ms < 1e3 ? `${Math.round(ms)}ms` : `${(ms / 1e3).toFixed(1)}s`;
|
|
318
|
+
}
|
|
319
|
+
function headline(trace, streaming) {
|
|
320
|
+
const plain = trace.replace(/^\s*(?:[-*+]|\d+\.)\s+/gm, "").replace(/\*\*|`/g, "");
|
|
321
|
+
let start = 0;
|
|
322
|
+
let last = "Thinking";
|
|
323
|
+
const boundaries = streaming ? /[.?!]+(?=\s)|[。?!]+|\r?\n/g : /[.?!]+(?=\s|$)|[。?!]+|\r?\n/g;
|
|
324
|
+
for (const end of plain.matchAll(boundaries)) {
|
|
325
|
+
const sentence = plain.slice(start, end.index + end[0].length).trim().replace(/\s+/g, " ");
|
|
326
|
+
if (sentence) last = sentence;
|
|
327
|
+
start = end.index + end[0].length;
|
|
328
|
+
}
|
|
329
|
+
return last;
|
|
330
|
+
}
|
|
331
|
+
var traceHeadline = headline;
|
|
332
|
+
var ThinkingFoldModel = class {
|
|
333
|
+
blocks = /* @__PURE__ */ new Map();
|
|
334
|
+
untimedBlocks = /* @__PURE__ */ new WeakMap();
|
|
335
|
+
now;
|
|
336
|
+
constructor(now = Date.now) {
|
|
337
|
+
this.now = now;
|
|
338
|
+
}
|
|
339
|
+
forMessage(message, create) {
|
|
340
|
+
if (typeof message.timestamp !== "number" || !Number.isFinite(message.timestamp)) {
|
|
341
|
+
const existing2 = this.untimedBlocks.get(message);
|
|
342
|
+
if (existing2 || !create) return existing2;
|
|
343
|
+
const blocks2 = /* @__PURE__ */ new Map();
|
|
344
|
+
this.untimedBlocks.set(message, blocks2);
|
|
345
|
+
return blocks2;
|
|
346
|
+
}
|
|
347
|
+
const existing = this.blocks.get(message.timestamp);
|
|
348
|
+
if (existing || !create) return existing;
|
|
349
|
+
if (this.blocks.size >= MAX_THINKING_MESSAGES) this.blocks.delete(this.blocks.keys().next().value);
|
|
350
|
+
const blocks = /* @__PURE__ */ new Map();
|
|
351
|
+
this.blocks.set(message.timestamp, blocks);
|
|
352
|
+
return blocks;
|
|
353
|
+
}
|
|
354
|
+
observe(message, event, reasoningTokens) {
|
|
355
|
+
const blocks = this.forMessage(message, event.type === "thinking_delta");
|
|
356
|
+
if (!blocks) return;
|
|
357
|
+
const at = this.now();
|
|
358
|
+
if (event.type === "thinking_delta" && event.contentIndex !== void 0) {
|
|
359
|
+
const block = blocks.get(event.contentIndex) ?? { startedAt: at, characters: 0 };
|
|
360
|
+
block.characters += event.delta?.length ?? 0;
|
|
361
|
+
if (reasoningTokens !== void 0 && reasoningTokens > 0) block.tokens = reasoningTokens;
|
|
362
|
+
blocks.set(event.contentIndex, block);
|
|
363
|
+
} else if (event.type === "thinking_end") {
|
|
364
|
+
const block = event.contentIndex === void 0 ? void 0 : blocks.get(event.contentIndex);
|
|
365
|
+
if (block && block.endedAt === void 0) block.endedAt = at;
|
|
366
|
+
} else if (event.type !== "thinking_start") {
|
|
367
|
+
for (const block of blocks.values()) {
|
|
368
|
+
if (block.endedAt === void 0) block.endedAt = at;
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
settle(message) {
|
|
373
|
+
for (const block of this.forMessage(message, false)?.values() ?? []) {
|
|
374
|
+
if (block.endedAt === void 0) block.endedAt = this.now();
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
setHeadline(message, index, headline2) {
|
|
378
|
+
const block = this.forMessage(message, false)?.get(index);
|
|
379
|
+
if (block) block.headline = headline2;
|
|
380
|
+
}
|
|
381
|
+
record(message, index, trace) {
|
|
382
|
+
const block = this.forMessage(message, false)?.get(index);
|
|
383
|
+
return {
|
|
384
|
+
kind: "thinking",
|
|
385
|
+
id: `thinking:${message.timestamp}:${index}`,
|
|
386
|
+
durationMs: block?.endedAt === void 0 ? void 0 : Math.max(0, block.endedAt - block.startedAt),
|
|
387
|
+
startedAt: block?.startedAt,
|
|
388
|
+
endedAt: block?.endedAt,
|
|
389
|
+
status: "done",
|
|
390
|
+
wordCount: trace.trim().split(/\s+/).filter(Boolean).length,
|
|
391
|
+
headline: block?.headline ?? headline(trace, false),
|
|
392
|
+
headlineSource: block?.headline ? "model" : "trace"
|
|
393
|
+
};
|
|
394
|
+
}
|
|
395
|
+
title(message, index, trace, streaming, saved) {
|
|
396
|
+
const block = this.forMessage(message, false)?.get(index);
|
|
397
|
+
const ms = saved?.durationMs ?? (block ? Math.max(0, (block.endedAt ?? this.now()) - block.startedAt) : void 0);
|
|
398
|
+
const label = saved?.kind === "thinking" ? saved.headline : block?.headline ?? headline(trace, streaming && block?.endedAt === void 0);
|
|
399
|
+
if (!streaming || block?.endedAt !== void 0) {
|
|
400
|
+
const words = saved?.kind === "thinking" ? saved.wordCount : trace.trim().split(/\s+/).filter(Boolean).length;
|
|
401
|
+
return `\u25C8 ${label} \xB7 ${ms === void 0 ? "\u2014" : elapsed(ms)} \xB7 ${words} ${words === 1 ? "word" : "words"}`;
|
|
402
|
+
}
|
|
403
|
+
const count = block?.tokens ?? Math.max(1, Math.ceil((block?.characters ?? trace.length) / 4));
|
|
404
|
+
const prefix = block?.tokens === void 0 ? "~" : "";
|
|
405
|
+
const rate = ms !== void 0 && ms > 0 ? Math.round(count / (ms / 1e3)) : 0;
|
|
406
|
+
return `\u25C8 ${label} \xB7 ${ms === void 0 ? "\u2014" : elapsed(ms)} \xB7 ${prefix}${count} tok \xB7 ${prefix}${rate} tok/s`;
|
|
407
|
+
}
|
|
408
|
+
timing(message, index) {
|
|
409
|
+
const block = this.forMessage(message, false)?.get(index);
|
|
410
|
+
return block && { start: block.startedAt, end: block.endedAt };
|
|
411
|
+
}
|
|
412
|
+
};
|
|
413
|
+
function duration(ms) {
|
|
414
|
+
if (ms === void 0) return "\u2014";
|
|
415
|
+
if (ms < 1e3) return `${Math.round(ms)}ms`;
|
|
416
|
+
return `${(ms / 1e3).toFixed(1)}s`;
|
|
417
|
+
}
|
|
418
|
+
function summary(args) {
|
|
419
|
+
if (!args) return "";
|
|
420
|
+
for (const key of ["command", "path", "pattern"]) {
|
|
421
|
+
if (typeof args[key] === "string") return args[key].replace(/\s+/g, " ").trim();
|
|
422
|
+
}
|
|
423
|
+
const value = Object.values(args).find((item) => typeof item === "string");
|
|
424
|
+
return typeof value === "string" ? value.replace(/\s+/g, " ").trim() : "";
|
|
425
|
+
}
|
|
426
|
+
function lineCount(result) {
|
|
427
|
+
const output = result?.content?.filter((item) => item.type === "text").map((item) => item.text ?? "").join("\n") ?? "";
|
|
428
|
+
if (!output) return 0;
|
|
429
|
+
return output.replace(/\r?\n$/, "").split(/\r?\n/).length;
|
|
430
|
+
}
|
|
431
|
+
var ToolFoldModel = class {
|
|
432
|
+
blocks = /* @__PURE__ */ new Map();
|
|
433
|
+
processList = [];
|
|
434
|
+
processById = /* @__PURE__ */ new Map();
|
|
435
|
+
processByBlock = /* @__PURE__ */ new Map();
|
|
436
|
+
seenContent = /* @__PURE__ */ new Map();
|
|
437
|
+
exchangeItems = [];
|
|
438
|
+
progress = /* @__PURE__ */ new Map();
|
|
439
|
+
progressByItem = /* @__PURE__ */ new Map();
|
|
440
|
+
openProcess;
|
|
441
|
+
openThinking = /* @__PURE__ */ new Set();
|
|
442
|
+
currentExchange = 0;
|
|
443
|
+
now;
|
|
444
|
+
thinking;
|
|
445
|
+
saved;
|
|
446
|
+
pendingThinking = /* @__PURE__ */ new Map();
|
|
447
|
+
cursorActive = false;
|
|
448
|
+
cursorKey;
|
|
449
|
+
constructor(now = Date.now) {
|
|
450
|
+
this.now = now;
|
|
451
|
+
this.thinking = new ThinkingFoldModel(now);
|
|
452
|
+
}
|
|
453
|
+
/** Rebuilds process membership from the active branch while keeping restored stats in session entries. */
|
|
454
|
+
clear(saved) {
|
|
455
|
+
this.blocks.clear();
|
|
456
|
+
this.processList = [];
|
|
457
|
+
this.processById.clear();
|
|
458
|
+
this.processByBlock.clear();
|
|
459
|
+
this.seenContent.clear();
|
|
460
|
+
this.exchangeItems = [];
|
|
461
|
+
this.progress.clear();
|
|
462
|
+
this.progressByItem.clear();
|
|
463
|
+
this.openProcess = void 0;
|
|
464
|
+
this.openThinking.clear();
|
|
465
|
+
this.currentExchange = 0;
|
|
466
|
+
this.thinking = new ThinkingFoldModel(this.now);
|
|
467
|
+
this.pendingThinking.clear();
|
|
468
|
+
this.cursorActive = false;
|
|
469
|
+
this.cursorKey = void 0;
|
|
470
|
+
this.saved = saved;
|
|
471
|
+
}
|
|
472
|
+
/**
|
|
473
|
+
* Returns the saved display record of each block in the exchange: timing, status
|
|
474
|
+
* and counts, plus each thinking block's headline. That headline is the model
|
|
475
|
+
* summary or the trace's last complete sentence, so it can repeat trace text;
|
|
476
|
+
* the rest of the trace, tool arguments and results stay in Pi messages.
|
|
477
|
+
*/
|
|
478
|
+
recordsForExchange(exchange) {
|
|
479
|
+
return this.processList.filter((process2) => process2.exchange === exchange).flatMap((process2) => process2.blocks.map((block) => {
|
|
480
|
+
if (block.kind === "thinking") return this.pendingThinking.get(block.key) ?? this.thinking.record(block.message, block.index, block.trace);
|
|
481
|
+
const tool = this.blocks.get(block.key.slice(5));
|
|
482
|
+
return {
|
|
483
|
+
kind: "tool",
|
|
484
|
+
id: block.key,
|
|
485
|
+
durationMs: tool?.startedAt === void 0 || tool.endedAt === void 0 ? void 0 : Math.max(0, tool.endedAt - tool.startedAt),
|
|
486
|
+
startedAt: tool?.startedAt,
|
|
487
|
+
endedAt: tool?.endedAt,
|
|
488
|
+
status: !tool?.endedAt ? "unknown" : tool.isError ? "error" : "ok",
|
|
489
|
+
lineCount: lineCount(tool?.result)
|
|
490
|
+
};
|
|
491
|
+
}));
|
|
492
|
+
}
|
|
493
|
+
releaseExchangeRecords() {
|
|
494
|
+
this.pendingThinking.clear();
|
|
495
|
+
}
|
|
496
|
+
/** Adds newly visible content in message order; repeated snapshots keep existing membership. */
|
|
497
|
+
ingest(message) {
|
|
498
|
+
if (typeof message?.timestamp !== "number" || !Number.isFinite(message.timestamp) || !Array.isArray(message.content)) return;
|
|
499
|
+
const seen = this.seenContent.get(message.timestamp) ?? /* @__PURE__ */ new Set();
|
|
500
|
+
this.seenContent.set(message.timestamp, seen);
|
|
501
|
+
for (let index = 0; index < message.content.length; index++) {
|
|
502
|
+
const item = message.content[index];
|
|
503
|
+
if (item.type === "thinking") {
|
|
504
|
+
const start = index;
|
|
505
|
+
const traces = [];
|
|
506
|
+
while (message.content[index]?.type === "thinking") {
|
|
507
|
+
if (message.content[index].thinking?.trim()) traces.push(message.content[index].thinking.trim());
|
|
508
|
+
index++;
|
|
509
|
+
}
|
|
510
|
+
index--;
|
|
511
|
+
if (!traces.length) continue;
|
|
512
|
+
const key = `thinking:${message.timestamp}:${start}`;
|
|
513
|
+
const existing = this.processByBlock.get(key)?.blocks.find((block) => block.key === key);
|
|
514
|
+
if (existing?.kind === "thinking") existing.trace = traces.join("\n\n");
|
|
515
|
+
else this.append({ kind: "thinking", key, message: { timestamp: message.timestamp }, index: start, trace: traces.join("\n\n") });
|
|
516
|
+
continue;
|
|
517
|
+
}
|
|
518
|
+
if (seen.has(index)) continue;
|
|
519
|
+
if (item.type === "text") {
|
|
520
|
+
if (!item.text?.trim()) continue;
|
|
521
|
+
this.exchangeItems.push({ key: `text:${message.timestamp}:${index}`, exchange: Math.max(1, this.currentExchange), startedAt: this.now(), kind: "text" });
|
|
522
|
+
this.openProcess = void 0;
|
|
523
|
+
}
|
|
524
|
+
if (item.type === "toolCall" && item.id) {
|
|
525
|
+
this.append({ kind: "tool", key: `tool:${item.id}` });
|
|
526
|
+
this.observe(item.id, item.name ?? "tool", item.arguments ?? {});
|
|
527
|
+
}
|
|
528
|
+
if (item.type === "toolCall" && !item.id) continue;
|
|
529
|
+
seen.add(index);
|
|
530
|
+
}
|
|
531
|
+
}
|
|
532
|
+
append(block) {
|
|
533
|
+
if (this.processByBlock.has(block.key)) return;
|
|
534
|
+
if (!this.openProcess) {
|
|
535
|
+
this.openProcess = { id: block.key, exchange: Math.max(1, this.currentExchange), blocks: [], open: false, settled: false };
|
|
536
|
+
this.processList.push(this.openProcess);
|
|
537
|
+
this.processById.set(this.openProcess.id, this.openProcess);
|
|
538
|
+
}
|
|
539
|
+
this.openProcess.blocks.push(block);
|
|
540
|
+
this.processByBlock.set(block.key, this.openProcess);
|
|
541
|
+
this.exchangeItems.push({ key: block.key, exchange: this.openProcess.exchange, startedAt: this.now(), kind: "block" });
|
|
542
|
+
}
|
|
543
|
+
processes() {
|
|
544
|
+
return this.processList;
|
|
545
|
+
}
|
|
546
|
+
hasTextImmediatelyBefore(key) {
|
|
547
|
+
const index = this.exchangeItems.findIndex((item2) => item2.key === key);
|
|
548
|
+
const item = this.exchangeItems[index];
|
|
549
|
+
const previous = index > 0 ? this.exchangeItems[index - 1] : void 0;
|
|
550
|
+
return previous?.exchange === item?.exchange && previous?.kind === "text";
|
|
551
|
+
}
|
|
552
|
+
progressParts(exchange) {
|
|
553
|
+
const items = this.exchangeItems.filter((item) => item.exchange === exchange);
|
|
554
|
+
const lastBlock = items.findLastIndex((item) => item.kind === "block");
|
|
555
|
+
const final = items[lastBlock + 1];
|
|
556
|
+
return lastBlock >= 0 && final?.kind === "text" ? { items: items.slice(0, lastBlock + 1), final } : void 0;
|
|
557
|
+
}
|
|
558
|
+
progressDurationForExchange(exchange) {
|
|
559
|
+
const parts = this.progressParts(exchange);
|
|
560
|
+
if (!parts) return void 0;
|
|
561
|
+
const first = parts.items.find((item) => item.kind === "block") ?? parts.items[0];
|
|
562
|
+
const block = first.kind === "block" ? this.processByBlock.get(first.key)?.blocks.find((item) => item.key === first.key) : void 0;
|
|
563
|
+
const saved = this.pendingThinking.get(first.key) ?? this.saved?.(first.key);
|
|
564
|
+
const start = saved?.startedAt ?? (block?.kind === "thinking" ? this.thinking.timing(block.message, block.index)?.start : block?.kind === "tool" ? this.blocks.get(block.key.slice(5))?.startedAt : void 0) ?? first.startedAt;
|
|
565
|
+
return Math.max(0, parts.final.startedAt - start);
|
|
566
|
+
}
|
|
567
|
+
progressForItem(key) {
|
|
568
|
+
const progress = this.progressByItem.get(key);
|
|
569
|
+
return progress && { exchange: progress.exchange, lead: progress.lead === key, open: progress.open };
|
|
570
|
+
}
|
|
571
|
+
/** Returns the settled progress branch for an item, grouping consecutive blocks by process. */
|
|
572
|
+
progressGuideForItem(key) {
|
|
573
|
+
const progress = this.progressByItem.get(key);
|
|
574
|
+
if (!progress?.open) return void 0;
|
|
575
|
+
const parts = this.progressParts(progress.exchange);
|
|
576
|
+
if (!parts) return void 0;
|
|
577
|
+
const children = [];
|
|
578
|
+
let previousProcess;
|
|
579
|
+
for (const item2 of parts.items) {
|
|
580
|
+
if (item2.kind === "text") {
|
|
581
|
+
children.push(item2.key);
|
|
582
|
+
previousProcess = void 0;
|
|
583
|
+
continue;
|
|
584
|
+
}
|
|
585
|
+
const process2 = this.processByBlock.get(item2.key);
|
|
586
|
+
const child2 = process2?.id ?? item2.key;
|
|
587
|
+
if (child2 !== previousProcess) children.push(child2);
|
|
588
|
+
previousProcess = child2;
|
|
589
|
+
}
|
|
590
|
+
const item = parts.items.find((candidate) => candidate.key === key);
|
|
591
|
+
const child = item?.kind === "text" ? item.key : item && this.processByBlock.get(item.key)?.id;
|
|
592
|
+
const index = child === void 0 ? -1 : children.indexOf(child);
|
|
593
|
+
if (index < 0) return void 0;
|
|
594
|
+
const last = index === children.length - 1;
|
|
595
|
+
return last ? { branch: "\u2514 ", continuation: " " } : { branch: "\u251C ", continuation: "\u2502 " };
|
|
596
|
+
}
|
|
597
|
+
/** Returns the branch for a block title and the continuation for its opened output. */
|
|
598
|
+
processBlockGuide(key) {
|
|
599
|
+
const process2 = this.processByBlock.get(key);
|
|
600
|
+
const index = process2?.blocks.findIndex((block) => block.key === key) ?? -1;
|
|
601
|
+
if (!process2 || index < 0) return void 0;
|
|
602
|
+
const last = index === process2.blocks.length - 1;
|
|
603
|
+
return last ? { branch: "\u2514 ", continuation: " " } : { branch: "\u251C ", continuation: "\u2502 " };
|
|
604
|
+
}
|
|
605
|
+
/** Returns the guide columns that continue into an item from its previous transcript item. */
|
|
606
|
+
guideBeforeItem(key) {
|
|
607
|
+
const index = this.exchangeItems.findIndex((item) => item.key === key);
|
|
608
|
+
if (index <= 0) return void 0;
|
|
609
|
+
const current = this.exchangeItems[index];
|
|
610
|
+
const previous = this.exchangeItems[index - 1];
|
|
611
|
+
if (previous.exchange !== current.exchange) return void 0;
|
|
612
|
+
const progress = this.progressByItem.get(key);
|
|
613
|
+
const outer = progress?.open ? this.progressGuideForItem(previous.key)?.continuation ?? "" : "";
|
|
614
|
+
const previousProcess = previous.kind === "block" ? this.processByBlock.get(previous.key) : void 0;
|
|
615
|
+
const currentProcess = current.kind === "block" ? this.processByBlock.get(current.key) : void 0;
|
|
616
|
+
if (previousProcess && currentProcess?.id === previousProcess.id) {
|
|
617
|
+
if (!previousProcess.open) return void 0;
|
|
618
|
+
return outer + (this.processBlockGuide(previous.key)?.continuation ?? "");
|
|
619
|
+
}
|
|
620
|
+
return outer || void 0;
|
|
621
|
+
}
|
|
622
|
+
progressLine(exchange) {
|
|
623
|
+
const progress = this.progress.get(exchange);
|
|
624
|
+
if (!progress) return "";
|
|
625
|
+
const notes = progress.notes ? ` \xB7 ${progress.notes} ${progress.notes === 1 ? "note" : "notes"}` : "";
|
|
626
|
+
return `${progress.open ? "\u25BE" : "\u25B8"} Worked for ${elapsed(progress.durationMs)} \xB7 \u25C8 ${progress.thinking} \u2699 ${progress.tools}${notes}`;
|
|
627
|
+
}
|
|
628
|
+
toggleProgress(exchange) {
|
|
629
|
+
const progress = this.progress.get(exchange);
|
|
630
|
+
if (!progress) return void 0;
|
|
631
|
+
progress.open = !progress.open;
|
|
632
|
+
return progress.open;
|
|
633
|
+
}
|
|
634
|
+
/** Toggles a fold row and sets its direct children to the same open state. */
|
|
635
|
+
toggleOneLevel(control) {
|
|
636
|
+
if (control.kind === "progress") {
|
|
637
|
+
const open = this.toggleProgress(control.exchange);
|
|
638
|
+
if (open === void 0) return void 0;
|
|
639
|
+
for (const process2 of this.processList) {
|
|
640
|
+
if (process2.exchange === control.exchange && process2.open !== open) this.toggleProcess(process2.id);
|
|
641
|
+
}
|
|
642
|
+
return open;
|
|
643
|
+
}
|
|
644
|
+
if (control.kind === "process") {
|
|
645
|
+
const open = this.toggleProcess(control.id);
|
|
646
|
+
if (open === void 0) return void 0;
|
|
647
|
+
for (const block of this.processById.get(control.id).blocks) {
|
|
648
|
+
if (block.kind === "thinking") {
|
|
649
|
+
if (this.isThinkingOpen(block.message, block.index) !== open) this.toggleThinking(block.message, block.index);
|
|
650
|
+
} else {
|
|
651
|
+
const id = block.key.slice(5);
|
|
652
|
+
if (this.isOpen(id) !== open) this.toggle(id);
|
|
653
|
+
}
|
|
654
|
+
}
|
|
655
|
+
return open;
|
|
656
|
+
}
|
|
657
|
+
if (control.kind === "thinking") return this.toggleThinking(control.message, control.index);
|
|
658
|
+
return this.toggle(control.id);
|
|
659
|
+
}
|
|
660
|
+
cursorRows() {
|
|
661
|
+
const rows = [];
|
|
662
|
+
for (const process2 of this.processList) {
|
|
663
|
+
const progress = this.progress.get(process2.exchange);
|
|
664
|
+
if (progress && !rows.some((row) => row.key === `exchange:${process2.exchange}`)) rows.push({ key: `exchange:${process2.exchange}`, title: this.progressLine(process2.exchange), toggle: () => {
|
|
665
|
+
this.toggleProgress(process2.exchange);
|
|
666
|
+
} });
|
|
667
|
+
if (progress && !progress.open) continue;
|
|
668
|
+
const last = process2.blocks.at(-1);
|
|
669
|
+
const detail = last?.kind === "thinking" ? this.thinkingTitle(last.message, last.index, last.trace, false) : last?.kind === "tool" ? this.titleParts(last.key.slice(5)) : void 0;
|
|
670
|
+
const label = typeof detail === "string" ? detail : detail ? `\u2699 ${detail.name} ${detail.argument}`.trim() : "";
|
|
671
|
+
rows.push({ key: `process:${process2.id}`, title: `${this.processLine(process2.id)}${label ? ` \xB7 ${label}` : ""}`, toggle: () => {
|
|
672
|
+
this.toggleProcess(process2.id);
|
|
673
|
+
} });
|
|
674
|
+
if (!process2.open) continue;
|
|
675
|
+
for (const block of process2.blocks) {
|
|
676
|
+
if (block.kind === "thinking") rows.push({ key: block.key, title: this.thinkingTitle(block.message, block.index, block.trace, false), toggle: () => {
|
|
677
|
+
this.toggleThinking(block.message, block.index);
|
|
678
|
+
} });
|
|
679
|
+
else {
|
|
680
|
+
const id = block.key.slice(5);
|
|
681
|
+
const parts = this.titleParts(id);
|
|
682
|
+
rows.push({ key: block.key, title: `\u2699 ${parts?.name ?? "tool"} ${parts?.argument ?? ""}`.trim(), toggle: () => {
|
|
683
|
+
this.toggle(id);
|
|
684
|
+
} });
|
|
685
|
+
}
|
|
686
|
+
}
|
|
687
|
+
}
|
|
688
|
+
return rows;
|
|
689
|
+
}
|
|
690
|
+
/** Selects settled progress lines, process lines, and block titles in transcript order. */
|
|
691
|
+
startCursor() {
|
|
692
|
+
const first = this.cursorRows()[0];
|
|
693
|
+
if (!first) return false;
|
|
694
|
+
this.cursorActive = true;
|
|
695
|
+
this.cursorKey = first.key;
|
|
696
|
+
return true;
|
|
697
|
+
}
|
|
698
|
+
stopCursor() {
|
|
699
|
+
this.cursorActive = false;
|
|
700
|
+
this.cursorKey = void 0;
|
|
701
|
+
}
|
|
702
|
+
cursorMove(delta) {
|
|
703
|
+
const rows = this.cursorRows();
|
|
704
|
+
if (!rows.length) {
|
|
705
|
+
this.cursorKey = void 0;
|
|
706
|
+
return;
|
|
707
|
+
}
|
|
708
|
+
const index = rows.findIndex((row) => row.key === this.cursorKey);
|
|
709
|
+
this.cursorKey = rows[Math.max(0, Math.min(rows.length - 1, Math.max(0, index) + delta))].key;
|
|
710
|
+
}
|
|
711
|
+
cursorToggle() {
|
|
712
|
+
const row = this.cursorRows().find((item) => item.key === this.cursorKey);
|
|
713
|
+
row?.toggle();
|
|
714
|
+
}
|
|
715
|
+
cursorTitle() {
|
|
716
|
+
return this.cursorActive ? this.cursorRows().find((item) => item.key === this.cursorKey)?.title : void 0;
|
|
717
|
+
}
|
|
718
|
+
isCursorHighlighted(key) {
|
|
719
|
+
return this.cursorActive && this.cursorKey === key;
|
|
720
|
+
}
|
|
721
|
+
beginExchange(index = this.currentExchange + 1) {
|
|
722
|
+
this.currentExchange = index;
|
|
723
|
+
this.openProcess = void 0;
|
|
724
|
+
}
|
|
725
|
+
/** Closes the open process and folds answered progress after the final text is known. */
|
|
726
|
+
endExchange(durationMs, answered = true) {
|
|
727
|
+
if (answered) {
|
|
728
|
+
const parts = this.progressParts(this.currentExchange);
|
|
729
|
+
if (parts) {
|
|
730
|
+
const progress = {
|
|
731
|
+
exchange: this.currentExchange,
|
|
732
|
+
lead: parts.items[0].key,
|
|
733
|
+
open: false,
|
|
734
|
+
durationMs: durationMs ?? this.progressDurationForExchange(this.currentExchange) ?? 0,
|
|
735
|
+
thinking: parts.items.filter((item) => item.key.startsWith("thinking:")).length,
|
|
736
|
+
tools: parts.items.filter((item) => item.key.startsWith("tool:")).length,
|
|
737
|
+
notes: parts.items.filter((item) => item.kind === "text").length
|
|
738
|
+
};
|
|
739
|
+
this.progress.set(this.currentExchange, progress);
|
|
740
|
+
for (const item of parts.items) this.progressByItem.set(item.key, progress);
|
|
741
|
+
}
|
|
742
|
+
}
|
|
743
|
+
this.openProcess = void 0;
|
|
744
|
+
for (let index = this.processList.length - 1; index >= 0 && !this.processList[index].settled; index--) this.processList[index].settled = true;
|
|
745
|
+
}
|
|
746
|
+
toggleLatestProcess() {
|
|
747
|
+
const process2 = this.processList.at(-1);
|
|
748
|
+
return process2 && this.toggleProcess(process2.id);
|
|
749
|
+
}
|
|
750
|
+
toggleExchange(exchange) {
|
|
751
|
+
if (this.progress.has(exchange)) return this.toggleProgress(exchange);
|
|
752
|
+
const processes = this.processList.filter((process2) => process2.exchange === exchange);
|
|
753
|
+
if (!processes.length) return void 0;
|
|
754
|
+
const open = processes.some((process2) => !process2.open);
|
|
755
|
+
for (const process2 of processes) process2.open = open;
|
|
756
|
+
return open;
|
|
757
|
+
}
|
|
758
|
+
isExchangeOpen(exchange) {
|
|
759
|
+
const progress = this.progress.get(exchange);
|
|
760
|
+
if (progress) return progress.open;
|
|
761
|
+
const processes = this.processList.filter((process2) => process2.exchange === exchange);
|
|
762
|
+
return processes.length ? processes.every((process2) => process2.open) : void 0;
|
|
763
|
+
}
|
|
764
|
+
toggleLatestExchange() {
|
|
765
|
+
const process2 = this.processList.at(-1);
|
|
766
|
+
return process2 && this.toggleExchange(process2.exchange);
|
|
767
|
+
}
|
|
768
|
+
processForThinking(message, index) {
|
|
769
|
+
return this.processByBlock.get(`thinking:${message.timestamp}:${index}`);
|
|
770
|
+
}
|
|
771
|
+
processForTool(id) {
|
|
772
|
+
return this.processByBlock.get(`tool:${id}`);
|
|
773
|
+
}
|
|
774
|
+
/** Whether this model has ingested or run the tool call, so its session owns the component. */
|
|
775
|
+
ownsTool(id) {
|
|
776
|
+
return this.processByBlock.has(`tool:${id}`) || this.blocks.has(id);
|
|
777
|
+
}
|
|
778
|
+
/** Whether this model has ingested the assistant message, so its session owns the component. */
|
|
779
|
+
ownsMessage(message) {
|
|
780
|
+
return typeof message?.timestamp === "number" && this.seenContent.has(message.timestamp);
|
|
781
|
+
}
|
|
782
|
+
/**
|
|
783
|
+
* Whether a thinking run or tool call in the message matches what this model ingested
|
|
784
|
+
* for that timestamp. Used to tell apart sessions whose messages share a timestamp.
|
|
785
|
+
*/
|
|
786
|
+
ingestedContentOf(message) {
|
|
787
|
+
const content = message?.content ?? [];
|
|
788
|
+
for (let index = 0; index < content.length; index++) {
|
|
789
|
+
const item = content[index];
|
|
790
|
+
if (item.type === "toolCall" && item.id && this.processByBlock.has(`tool:${item.id}`)) return true;
|
|
791
|
+
if (item.type !== "thinking" || content[index - 1]?.type === "thinking") continue;
|
|
792
|
+
const traces = [];
|
|
793
|
+
for (let run = index; content[run]?.type === "thinking"; run++) if (content[run].thinking?.trim()) traces.push(content[run].thinking.trim());
|
|
794
|
+
const key = `thinking:${message.timestamp}:${index}`;
|
|
795
|
+
const block = this.processByBlock.get(key)?.blocks.find((candidate) => candidate.key === key);
|
|
796
|
+
if (block?.kind === "thinking" && block.trace === traces.join("\n\n")) return true;
|
|
797
|
+
}
|
|
798
|
+
return false;
|
|
799
|
+
}
|
|
800
|
+
isProcessLead(id, key) {
|
|
801
|
+
return this.processById.get(id)?.blocks[0]?.key === key;
|
|
802
|
+
}
|
|
803
|
+
toggleProcess(id) {
|
|
804
|
+
const process2 = this.processById.get(id);
|
|
805
|
+
if (!process2) return void 0;
|
|
806
|
+
process2.open = !process2.open;
|
|
807
|
+
return process2.open;
|
|
808
|
+
}
|
|
809
|
+
isProcessOpen(id) {
|
|
810
|
+
return this.processById.get(id)?.open ?? false;
|
|
811
|
+
}
|
|
812
|
+
toggleThinking(message, index) {
|
|
813
|
+
const key = `thinking:${message.timestamp}:${index}`;
|
|
814
|
+
if (this.openThinking.has(key)) {
|
|
815
|
+
this.openThinking.delete(key);
|
|
816
|
+
return false;
|
|
817
|
+
}
|
|
818
|
+
this.openThinking.add(key);
|
|
819
|
+
return true;
|
|
820
|
+
}
|
|
821
|
+
isThinkingOpen(message, index) {
|
|
822
|
+
return this.openThinking.has(`thinking:${message.timestamp}:${index}`);
|
|
823
|
+
}
|
|
824
|
+
processLine(id) {
|
|
825
|
+
const process2 = this.processById.get(id);
|
|
826
|
+
if (!process2) return "";
|
|
827
|
+
let first;
|
|
828
|
+
let last;
|
|
829
|
+
let activity = "";
|
|
830
|
+
let queued = "";
|
|
831
|
+
for (const block of process2.blocks) {
|
|
832
|
+
const tool = block.kind === "tool" ? this.blocks.get(block.key.slice(5)) : void 0;
|
|
833
|
+
const saved = this.pendingThinking.get(block.key) ?? this.saved?.(block.key);
|
|
834
|
+
const timing = saved?.startedAt !== void 0 ? { start: saved.startedAt, end: saved.endedAt } : block.kind === "thinking" ? this.thinking.timing(block.message, block.index) : tool?.startedAt === void 0 ? void 0 : { start: tool.startedAt, end: tool.endedAt };
|
|
835
|
+
if (!timing && !process2.settled && block.kind === "tool" && tool?.endedAt === void 0 && !saved) queued ||= `\u2699 ${tool?.name ?? "tool"} queued`;
|
|
836
|
+
if (!timing) continue;
|
|
837
|
+
first = first === void 0 ? timing.start : Math.min(first, timing.start);
|
|
838
|
+
last = Math.max(last ?? timing.start, timing.end ?? (process2.settled ? timing.start : this.now()));
|
|
839
|
+
if (timing.end === void 0 && !process2.settled) activity = block.kind === "tool" ? `\u2699 ${tool?.name ?? "tool"} running ${elapsed(this.now() - timing.start)}` : this.thinking.title(block.message, block.index, block.trace, true);
|
|
840
|
+
}
|
|
841
|
+
const count = `\u25C8 ${process2.blocks.filter((block) => block.kind === "thinking").length} \u2699 ${process2.blocks.filter((block) => block.kind === "tool").length}`;
|
|
842
|
+
const total = first === void 0 || last === void 0 ? "0ms" : elapsed(Math.max(0, last - first));
|
|
843
|
+
return `${process2.open ? "\u25BE" : "\u25B8"} ${count} \xB7 ${activity || queued || total}`;
|
|
844
|
+
}
|
|
845
|
+
observe(id, name, args, result) {
|
|
846
|
+
const block = this.blocks.get(id);
|
|
847
|
+
if (block) {
|
|
848
|
+
block.args = args;
|
|
849
|
+
if (result) block.result = result;
|
|
850
|
+
} else {
|
|
851
|
+
this.blocks.set(id, { name, args, result, isError: false, open: false });
|
|
852
|
+
}
|
|
853
|
+
}
|
|
854
|
+
start(id, name, at = this.now()) {
|
|
855
|
+
const block = this.blocks.get(id) ?? { name, isError: false, open: false };
|
|
856
|
+
block.name = name;
|
|
857
|
+
block.startedAt = at;
|
|
858
|
+
this.blocks.set(id, block);
|
|
859
|
+
}
|
|
860
|
+
end(id, isError, result, at = this.now()) {
|
|
861
|
+
const block = this.blocks.get(id) ?? { name: "tool", isError: false, open: false };
|
|
862
|
+
block.endedAt = at;
|
|
863
|
+
block.isError = isError;
|
|
864
|
+
if (result) block.result = result;
|
|
865
|
+
this.blocks.set(id, block);
|
|
866
|
+
}
|
|
867
|
+
observeThinking(message, event, reasoningTokens) {
|
|
868
|
+
this.thinking.observe(message, event, reasoningTokens);
|
|
869
|
+
}
|
|
870
|
+
settleThinking(message) {
|
|
871
|
+
this.thinking.settle(message);
|
|
872
|
+
for (let index = 0; index < (message.content?.length ?? 0); index++) {
|
|
873
|
+
if (message.content?.[index].type !== "thinking" || message.content?.[index - 1]?.type === "thinking") continue;
|
|
874
|
+
const key = `thinking:${message.timestamp}:${index}`;
|
|
875
|
+
const block = this.processByBlock.get(key)?.blocks.find((item) => item.key === key);
|
|
876
|
+
if (block?.kind === "thinking") this.pendingThinking.set(key, this.thinking.record(message, index, block.trace));
|
|
877
|
+
}
|
|
878
|
+
}
|
|
879
|
+
thinkingTitle(message, index, trace, streaming) {
|
|
880
|
+
const key = `thinking:${message.timestamp}:${index}`;
|
|
881
|
+
return this.thinking.title(message, index, trace, streaming, this.pendingThinking.get(key) ?? this.saved?.(key));
|
|
882
|
+
}
|
|
883
|
+
/**
|
|
884
|
+
* Applies a model headline, or `undefined` to fall back to the trace sentence.
|
|
885
|
+
* Returns the correction to persist when the block's exchange record was already
|
|
886
|
+
* saved with a different headline; otherwise the change is live or still pending.
|
|
887
|
+
*/
|
|
888
|
+
setThinkingHeadline(message, index, headline2) {
|
|
889
|
+
const key = `thinking:${message.timestamp}:${index}`;
|
|
890
|
+
this.thinking.setHeadline(message, index, headline2);
|
|
891
|
+
const block = this.processByBlock.get(key)?.blocks.find((item) => item.key === key);
|
|
892
|
+
const next = headline2 ? { headline: headline2, headlineSource: "model" } : block?.kind === "thinking" ? { headline: traceHeadline(block.trace, false), headlineSource: "trace" } : void 0;
|
|
893
|
+
const pending = this.pendingThinking.get(key);
|
|
894
|
+
if (pending?.kind === "thinking") {
|
|
895
|
+
if (next) Object.assign(pending, next);
|
|
896
|
+
return void 0;
|
|
897
|
+
}
|
|
898
|
+
const saved = this.saved?.(key);
|
|
899
|
+
if (!next || saved?.kind !== "thinking" || saved.headline === next.headline && saved.headlineSource === next.headlineSource) return void 0;
|
|
900
|
+
return { id: key, ...next };
|
|
901
|
+
}
|
|
902
|
+
toggle(id) {
|
|
903
|
+
const block = this.blocks.get(id);
|
|
904
|
+
if (!block) return void 0;
|
|
905
|
+
block.open = !block.open;
|
|
906
|
+
return block.open;
|
|
907
|
+
}
|
|
908
|
+
isOpen(id) {
|
|
909
|
+
return this.blocks.get(id)?.open ?? false;
|
|
910
|
+
}
|
|
911
|
+
titleParts(id) {
|
|
912
|
+
const block = this.blocks.get(id);
|
|
913
|
+
if (!block) return void 0;
|
|
914
|
+
const saved = this.saved?.(`tool:${id}`);
|
|
915
|
+
const argument = summary(block.args);
|
|
916
|
+
const settled = this.processByBlock.get(`tool:${id}`)?.settled ?? false;
|
|
917
|
+
const noResult = saved?.kind === "tool" ? saved.status === "unknown" : block.endedAt === void 0 && settled;
|
|
918
|
+
if (noResult) return { name: block.name, argument, stats: "no result" };
|
|
919
|
+
const elapsed2 = saved?.durationMs ?? (block.startedAt === void 0 ? void 0 : Math.max(0, (block.endedAt ?? this.now()) - block.startedAt));
|
|
920
|
+
const status = saved?.kind === "tool" ? saved.status === "error" ? "\u2717" : "\u2713" : block.endedAt === void 0 ? "running" : block.isError ? "\u2717" : "\u2713";
|
|
921
|
+
const count = saved?.kind === "tool" ? saved.lineCount : lineCount(block.result);
|
|
922
|
+
const lines = saved?.kind === "tool" || block.endedAt !== void 0 ? ` \xB7 ${count} ${count === 1 ? "line" : "lines"}` : "";
|
|
923
|
+
return { name: block.name, argument, stats: `${status} ${duration(elapsed2)}${lines}` };
|
|
924
|
+
}
|
|
925
|
+
};
|
|
926
|
+
|
|
927
|
+
// extensions/focus-mode/src/tool-render.ts
|
|
928
|
+
import { MouseRegion, Text, truncateToWidth as truncateToWidth3, visibleWidth as visibleWidth3 } from "@earendil-works/pi-tui";
|
|
929
|
+
import { homedir } from "node:os";
|
|
930
|
+
var hoveredControl;
|
|
931
|
+
function setHover(model, key) {
|
|
932
|
+
const next = model && key !== void 0 ? { model, key } : void 0;
|
|
933
|
+
if (hoveredControl?.model === next?.model && hoveredControl?.key === next?.key) return false;
|
|
934
|
+
hoveredControl = next;
|
|
935
|
+
return true;
|
|
936
|
+
}
|
|
937
|
+
function isHovered(model, key) {
|
|
938
|
+
return hoveredControl?.model === model && hoveredControl.key === key;
|
|
939
|
+
}
|
|
940
|
+
var repaint = { handled: true, render: true };
|
|
941
|
+
function endHoverOnMove(event) {
|
|
942
|
+
return event.type === "move" && setHover(void 0, void 0) ? repaint : void 0;
|
|
943
|
+
}
|
|
944
|
+
function styleControl(theme, model, key, text) {
|
|
945
|
+
if (!theme) return text;
|
|
946
|
+
const color = model.isCursorHighlighted(key) ? "accent" : isHovered(model, key) ? "muted" : "dim";
|
|
947
|
+
const colored = theme.fg(color, text);
|
|
948
|
+
return theme.italic?.(colored) ?? colored;
|
|
949
|
+
}
|
|
950
|
+
function hoverRows(theme, model, key, lines, leading) {
|
|
951
|
+
if (!theme) return lines;
|
|
952
|
+
const cursor = model.isCursorHighlighted(key);
|
|
953
|
+
const hovered = isHovered(model, key);
|
|
954
|
+
const color = cursor ? "accent" : hovered ? "muted" : "dim";
|
|
955
|
+
const colorOpen = theme.fg(color, "\0").split("\0")[0];
|
|
956
|
+
const italicOpen = theme.italic?.("\0").split("\0")[0] ?? "";
|
|
957
|
+
const restoredStyle = colorOpen + italicOpen;
|
|
958
|
+
const selected = hovered && !cursor && theme.bg;
|
|
959
|
+
if (!selected) return restoredStyle ? lines.map((line) => line.replaceAll("\x1B[0m", `\x1B[0m${restoredStyle}`)) : lines;
|
|
960
|
+
const [open, close] = theme.bg("selectedBg", "\0").split("\0");
|
|
961
|
+
const bare = " ".repeat(leading);
|
|
962
|
+
return lines.map((line) => {
|
|
963
|
+
const cut = line.startsWith(bare) ? leading : 0;
|
|
964
|
+
const body = line.slice(cut).replaceAll("\x1B[0m", `\x1B[0m${open}${restoredStyle}`).replaceAll("\x1B[49m", `\x1B[49m${open}`);
|
|
965
|
+
return line.slice(0, cut) + open + body + close;
|
|
966
|
+
});
|
|
967
|
+
}
|
|
968
|
+
var toolOwners = /* @__PURE__ */ new WeakMap();
|
|
969
|
+
var thinkingOwners = /* @__PURE__ */ new WeakMap();
|
|
970
|
+
var BLOCK_INDENT = 2;
|
|
971
|
+
function withGuide(theme, prefix, lines) {
|
|
972
|
+
if (!prefix) return lines;
|
|
973
|
+
const aligned = prefix.replace(/([│├└]) /g, " $1");
|
|
974
|
+
const guide = theme?.fg("dim", aligned) ?? aligned;
|
|
975
|
+
return lines.map((line) => guide + line);
|
|
976
|
+
}
|
|
977
|
+
function withoutBlankEdges(lines) {
|
|
978
|
+
const blank = (line) => line.replace(/\x1b\[[0-?]*[ -/]*[@-~]/g, "").replace(/\x1b\][^\x07]*(?:\x07|\x1b\\\\)/g, "").trim() === "";
|
|
979
|
+
let start = 0;
|
|
980
|
+
let end = lines.length;
|
|
981
|
+
while (start < end && blank(lines[start])) start++;
|
|
982
|
+
while (end > start && blank(lines[end - 1])) end--;
|
|
983
|
+
return { lines: lines.slice(start, end), leading: start };
|
|
984
|
+
}
|
|
985
|
+
function truncateProcessLine(text, width) {
|
|
986
|
+
return fitThinkingLine(text, width);
|
|
987
|
+
}
|
|
988
|
+
function progressRow(model, exchange, theme, padding, width) {
|
|
989
|
+
const key = `exchange:${exchange}`;
|
|
990
|
+
const text = truncateProcessLine(model.progressLine(exchange), width - padding * 2);
|
|
991
|
+
return hoverRows(theme, model, key, new Text(styleControl(theme, model, key, text), padding, 0).render(width), padding);
|
|
992
|
+
}
|
|
993
|
+
function pathTail(path, width) {
|
|
994
|
+
if (visibleWidth3(path) <= width) return path;
|
|
995
|
+
const slash = path.lastIndexOf("/");
|
|
996
|
+
if (slash < 0) return "\u2026" + Array.from(path).reverse().reduce((tail2, character) => visibleWidth3("\u2026" + character + tail2) <= width ? character + tail2 : tail2, "");
|
|
997
|
+
const prefix = path.startsWith("~/") ? "~/\u2026/" : "\u2026/";
|
|
998
|
+
const tail = path.slice(slash + 1);
|
|
999
|
+
let result = prefix + tail;
|
|
1000
|
+
if (visibleWidth3(result) > width) {
|
|
1001
|
+
result = "\u2026";
|
|
1002
|
+
for (const character of Array.from(tail).reverse()) {
|
|
1003
|
+
if (visibleWidth3(result) + visibleWidth3(character) > width) break;
|
|
1004
|
+
result = "\u2026" + character + result.slice(1);
|
|
1005
|
+
}
|
|
1006
|
+
return result;
|
|
1007
|
+
}
|
|
1008
|
+
for (const segment of path.slice(path.startsWith("~/") ? 2 : 0, slash).split("/").reverse()) {
|
|
1009
|
+
if (!segment || visibleWidth3(prefix + segment + "/" + result.slice(prefix.length)) > width) break;
|
|
1010
|
+
result = prefix + segment + "/" + result.slice(prefix.length);
|
|
1011
|
+
}
|
|
1012
|
+
return result;
|
|
1013
|
+
}
|
|
1014
|
+
function installToolFold(componentClass, model, getTheme = () => void 0, getOutputPad = () => 1, getHomeDirectory = homedir) {
|
|
1015
|
+
const prototype = componentClass?.prototype;
|
|
1016
|
+
const shared = prototype && toolOwners.get(prototype);
|
|
1017
|
+
if (shared) return shared.add({ model, getTheme, getOutputPad, getHomeDirectory });
|
|
1018
|
+
const original = prototype?.render;
|
|
1019
|
+
const originalMouse = prototype?.handleMouse;
|
|
1020
|
+
const hadOwnMouse = prototype && Object.hasOwn(prototype, "handleMouse");
|
|
1021
|
+
if (typeof original !== "function") return { installed: false, restore() {
|
|
1022
|
+
} };
|
|
1023
|
+
const ownerFor = (component) => {
|
|
1024
|
+
const all = Array.from(owners);
|
|
1025
|
+
return all.findLast((owner) => owner.model.ownsTool(component.toolCallId)) ?? all.at(-1);
|
|
1026
|
+
};
|
|
1027
|
+
const nativeOffset = /* @__PURE__ */ new WeakMap();
|
|
1028
|
+
function folded(width) {
|
|
1029
|
+
nativeOffset.delete(this);
|
|
1030
|
+
if (owners.size === 0) return original.call(this, width);
|
|
1031
|
+
try {
|
|
1032
|
+
const { model: model2, getTheme: getTheme2, getOutputPad: getOutputPad2, getHomeDirectory: getHomeDirectory2 } = ownerFor(this);
|
|
1033
|
+
model2.observe(this.toolCallId, this.toolName, this.args, this.result);
|
|
1034
|
+
const key = `tool:${this.toolCallId}`;
|
|
1035
|
+
const process2 = model2.processForTool(this.toolCallId);
|
|
1036
|
+
const processLead = process2 && model2.isProcessLead(process2.id, key);
|
|
1037
|
+
const progress = model2.progressForItem(key);
|
|
1038
|
+
const progressGuide = model2.progressGuideForItem(key);
|
|
1039
|
+
const processGuide = model2.processBlockGuide(key);
|
|
1040
|
+
const gapBefore = model2.hasTextImmediatelyBefore(`tool:${this.toolCallId}`) && (processLead || progress?.lead);
|
|
1041
|
+
const compactProgress = progress?.open === true;
|
|
1042
|
+
const padding = Math.min(getOutputPad2(), Math.max(0, Math.floor((width - 1) / 2)));
|
|
1043
|
+
const progressLine = progress?.lead ? progressRow(model2, progress.exchange, getTheme2(), padding, width) : [];
|
|
1044
|
+
const withGap = (rows2) => gapBefore && !compactProgress && rows2.length ? [...withGuide(getTheme2(), progressGuide ? "\u2502 " : "", [""]), ...rows2] : rows2;
|
|
1045
|
+
if (progress && !progress.open) return withGap(progressLine);
|
|
1046
|
+
if (process2 && !model2.isProcessOpen(process2.id) && !model2.isProcessLead(process2.id, `tool:${this.toolCallId}`)) return [];
|
|
1047
|
+
const contentWidth = width - padding * 2 - (progress ? BLOCK_INDENT : 0);
|
|
1048
|
+
const processText = process2 && model2.isProcessLead(process2.id, `tool:${this.toolCallId}`) ? truncateProcessLine(model2.processLine(process2.id), contentWidth) : void 0;
|
|
1049
|
+
const processKey = `process:${process2?.id}`;
|
|
1050
|
+
const processLine = processText === void 0 ? [] : hoverRows(getTheme2(), model2, processKey, new Text(styleControl(getTheme2(), model2, processKey, processText), padding, 0).render(width - (progress ? BLOCK_INDENT : 0)), padding);
|
|
1051
|
+
const processRows = progressGuide && processLead ? withGuide(getTheme2(), progressGuide.branch, processLine) : processLine;
|
|
1052
|
+
if (process2 && !model2.isProcessOpen(process2.id)) return withGap([...progressLine, ...progressLine.length && processRows.length && !compactProgress ? withGuide(getTheme2(), progressGuide?.continuation ?? "", [""]) : [], ...processRows]);
|
|
1053
|
+
const parts = model2.titleParts(this.toolCallId);
|
|
1054
|
+
if (!parts || width <= 0) return original.call(this, width);
|
|
1055
|
+
const indent = process2 ? BLOCK_INDENT : 0;
|
|
1056
|
+
const titleWidth = contentWidth - indent;
|
|
1057
|
+
const name = truncateToWidth3(`\u2699 ${parts.name}`, titleWidth, "");
|
|
1058
|
+
const remaining = titleWidth - visibleWidth3(name);
|
|
1059
|
+
const stats = truncateToWidth3(` ${parts.stats}`, remaining, "");
|
|
1060
|
+
const argumentWidth = Math.max(0, remaining - visibleWidth3(` ${parts.stats}`));
|
|
1061
|
+
const home = getHomeDirectory2();
|
|
1062
|
+
const path = typeof this.args?.path === "string" && (parts.argument === this.args.path || parts.argument === this.args.path.replace(/\s+/g, " ").trim()) ? parts.argument.startsWith(`${home}/`) ? `~${parts.argument.slice(home.length)}` : parts.argument : void 0;
|
|
1063
|
+
const argument = argumentWidth > 2 && parts.argument ? (path === void 0 ? truncateToWidth3(` ${parts.argument}`, argumentWidth, "\u2026") : ` ${pathTail(path, argumentWidth - 2)}`).replace(/\x1b\[0m/g, "") : "";
|
|
1064
|
+
const text = name + argument + stats;
|
|
1065
|
+
const styled = styleControl(getTheme2(), model2, `tool:${this.toolCallId}`, text);
|
|
1066
|
+
const titleRows = hoverRows(getTheme2(), model2, `tool:${this.toolCallId}`, new Text(styled, padding, 0).render(width - indent - (progress ? BLOCK_INDENT : 0)), padding);
|
|
1067
|
+
const titlePrefix = (progressGuide?.continuation ?? "") + (process2 ? processGuide?.branch ?? " " : "");
|
|
1068
|
+
const title = withGuide(getTheme2(), titlePrefix, titleRows);
|
|
1069
|
+
const rows = [...processRows, ...title];
|
|
1070
|
+
if (!model2.isOpen(this.toolCallId)) return withGap([...progressLine, ...progressLine.length && rows.length && !compactProgress ? withGuide(getTheme2(), progressGuide?.continuation ?? "", [""]) : [], ...rows]);
|
|
1071
|
+
const native = original.call(this, width - indent * 2 - (progress ? BLOCK_INDENT : 0));
|
|
1072
|
+
const trimmed = withoutBlankEdges(native);
|
|
1073
|
+
nativeOffset.set(this, { rows: processLine.length + title.length - trimmed.leading, columns: indent * 2 });
|
|
1074
|
+
const bodyPrefix = (progressGuide?.continuation ?? "") + (process2 ? processGuide?.continuation ?? " " : "") + (indent ? " ".repeat(BLOCK_INDENT) : "");
|
|
1075
|
+
const body = withGuide(getTheme2(), bodyPrefix, trimmed.lines);
|
|
1076
|
+
return withGap([...progressLine, ...progressLine.length && rows.length && !compactProgress ? withGuide(getTheme2(), progressGuide?.continuation ?? "", [""]) : [], ...rows, ...body]);
|
|
1077
|
+
} catch {
|
|
1078
|
+
return original.call(this, width);
|
|
1079
|
+
}
|
|
1080
|
+
}
|
|
1081
|
+
prototype.render = folded;
|
|
1082
|
+
function foldedMouse(event) {
|
|
1083
|
+
if (owners.size === 0) return originalMouse?.call(this, event);
|
|
1084
|
+
const { model: model2 } = ownerFor(this);
|
|
1085
|
+
const key = `tool:${this.toolCallId}`;
|
|
1086
|
+
const progress = model2.progressForItem(key);
|
|
1087
|
+
const process2 = model2.processForTool(this.toolCallId);
|
|
1088
|
+
const processLead = process2 && model2.isProcessLead(process2.id, key);
|
|
1089
|
+
const gapBefore = model2.hasTextImmediatelyBefore(key) && (processLead || progress?.lead);
|
|
1090
|
+
const gapRows = gapBefore && progress?.open !== true ? 1 : 0;
|
|
1091
|
+
if (progress?.lead && event.y === gapRows) {
|
|
1092
|
+
if (event.type === "move") return setHover(model2, `exchange:${progress.exchange}`) ? repaint : void 0;
|
|
1093
|
+
if (event.type === "click" && event.button === "left") {
|
|
1094
|
+
const control = { kind: "progress", exchange: progress.exchange };
|
|
1095
|
+
if (event.alt) model2.toggleOneLevel(control);
|
|
1096
|
+
else model2.toggleProgress(progress.exchange);
|
|
1097
|
+
this.ui?.requestRender();
|
|
1098
|
+
return { handled: true, render: false };
|
|
1099
|
+
}
|
|
1100
|
+
}
|
|
1101
|
+
if (progress && !progress.open) return event.type === "move" && setHover(void 0, void 0) ? repaint : void 0;
|
|
1102
|
+
const rowOffset = gapRows + (progress?.lead ? 1 : 0);
|
|
1103
|
+
if (progress || rowOffset) event = {
|
|
1104
|
+
...event,
|
|
1105
|
+
y: event.y - rowOffset,
|
|
1106
|
+
x: event.x === void 0 ? void 0 : event.x - (progress ? BLOCK_INDENT : 0),
|
|
1107
|
+
width: event.width - (progress ? BLOCK_INDENT : 0),
|
|
1108
|
+
height: event.height - rowOffset
|
|
1109
|
+
};
|
|
1110
|
+
const lead = process2 && model2.isProcessLead(process2.id, `tool:${this.toolCallId}`);
|
|
1111
|
+
if (event.type === "move") {
|
|
1112
|
+
const titleRow = lead ? 1 : 0;
|
|
1113
|
+
const key2 = lead && event.y === 0 ? `process:${process2.id}` : event.y === titleRow && (!process2 || model2.isProcessOpen(process2.id)) && model2.titleParts(this.toolCallId) ? `tool:${this.toolCallId}` : void 0;
|
|
1114
|
+
const changed = setHover(model2, key2);
|
|
1115
|
+
const offset2 = key2 ? void 0 : nativeOffset.get(this);
|
|
1116
|
+
const native = offset2 && event.y >= offset2.rows ? originalMouse?.call(this, { ...event, y: event.y - offset2.rows, x: event.x === void 0 ? void 0 : event.x - offset2.columns, width: event.width - offset2.columns, height: event.height - offset2.rows }) : void 0;
|
|
1117
|
+
return native ?? (changed ? repaint : void 0);
|
|
1118
|
+
}
|
|
1119
|
+
if (event.type === "click" && event.button === "left" && event.y === 0 && process2 && model2.isProcessLead(process2.id, `tool:${this.toolCallId}`)) {
|
|
1120
|
+
const control = { kind: "process", id: process2.id };
|
|
1121
|
+
if (event.alt) model2.toggleOneLevel(control);
|
|
1122
|
+
else model2.toggleProcess(process2.id);
|
|
1123
|
+
this.ui?.requestRender();
|
|
1124
|
+
return { handled: true, render: false };
|
|
1125
|
+
}
|
|
1126
|
+
const titleY = process2 && model2.isProcessLead(process2.id, `tool:${this.toolCallId}`) ? 1 : 0;
|
|
1127
|
+
if (event.type === "click" && event.button === "left" && event.y === titleY && (!process2 || model2.isProcessOpen(process2.id))) {
|
|
1128
|
+
const control = { kind: "tool", id: this.toolCallId };
|
|
1129
|
+
const toggled = event.alt ? model2.toggleOneLevel(control) : model2.toggle(this.toolCallId);
|
|
1130
|
+
if (toggled !== void 0) {
|
|
1131
|
+
this.ui?.requestRender();
|
|
1132
|
+
return { handled: true, render: false };
|
|
1133
|
+
}
|
|
1134
|
+
}
|
|
1135
|
+
const offset = nativeOffset.get(this);
|
|
1136
|
+
if (!offset) return originalMouse?.call(this, event);
|
|
1137
|
+
if (event.y < offset.rows) return void 0;
|
|
1138
|
+
return originalMouse?.call(this, {
|
|
1139
|
+
...event,
|
|
1140
|
+
y: event.y - offset.rows,
|
|
1141
|
+
x: event.x === void 0 ? void 0 : event.x - offset.columns,
|
|
1142
|
+
width: event.width - offset.columns,
|
|
1143
|
+
height: event.height - offset.rows
|
|
1144
|
+
});
|
|
1145
|
+
}
|
|
1146
|
+
prototype.handleMouse = foldedMouse;
|
|
1147
|
+
const owners = /* @__PURE__ */ new Set();
|
|
1148
|
+
const add = (owner) => {
|
|
1149
|
+
if (owners.size === 0) {
|
|
1150
|
+
if (prototype.render === original) prototype.render = folded;
|
|
1151
|
+
if (prototype.handleMouse === originalMouse) prototype.handleMouse = foldedMouse;
|
|
1152
|
+
}
|
|
1153
|
+
owners.add(owner);
|
|
1154
|
+
return { installed: true, restore() {
|
|
1155
|
+
if (!owners.delete(owner)) return;
|
|
1156
|
+
if (hoveredControl?.model === owner.model) setHover(void 0, void 0);
|
|
1157
|
+
if (owners.size > 0) return;
|
|
1158
|
+
if (prototype.render === folded && prototype.handleMouse === foldedMouse) toolOwners.delete(prototype);
|
|
1159
|
+
if (prototype.render === folded) prototype.render = original;
|
|
1160
|
+
if (prototype.handleMouse === foldedMouse) {
|
|
1161
|
+
if (hadOwnMouse) prototype.handleMouse = originalMouse;
|
|
1162
|
+
else delete prototype.handleMouse;
|
|
1163
|
+
}
|
|
1164
|
+
} };
|
|
1165
|
+
};
|
|
1166
|
+
toolOwners.set(prototype, { add });
|
|
1167
|
+
return add({ model, getTheme, getOutputPad, getHomeDirectory });
|
|
1168
|
+
}
|
|
1169
|
+
function installThinkingFold(componentClass, model, getTheme = () => void 0, requestRender = () => {
|
|
1170
|
+
}, observeOutputPad = () => {
|
|
1171
|
+
}) {
|
|
1172
|
+
const prototype = componentClass?.prototype;
|
|
1173
|
+
const shared = prototype && thinkingOwners.get(prototype);
|
|
1174
|
+
if (shared) return shared.add({ model, getTheme, requestRender, observeOutputPad });
|
|
1175
|
+
const original = prototype?.updateContent;
|
|
1176
|
+
if (typeof original !== "function") return { installed: false, restore() {
|
|
1177
|
+
} };
|
|
1178
|
+
const originalMouse = prototype.handleMouse;
|
|
1179
|
+
const hadOwnMouse = Object.hasOwn(prototype, "handleMouse");
|
|
1180
|
+
let claimedMove = false;
|
|
1181
|
+
const textControls = /* @__PURE__ */ new WeakMap();
|
|
1182
|
+
const ownerFor = (message) => {
|
|
1183
|
+
const all = Array.from(owners);
|
|
1184
|
+
const claimants = all.filter((owner) => owner.model.ownsMessage(message));
|
|
1185
|
+
if (claimants.length > 1) return claimants.findLast((owner) => owner.model.ingestedContentOf(message)) ?? claimants.at(-1);
|
|
1186
|
+
return claimants[0] ?? all.at(-1);
|
|
1187
|
+
};
|
|
1188
|
+
function foldContent(message, isStreaming) {
|
|
1189
|
+
const { model: model2, getTheme: getTheme2, requestRender: requestRender2, observeOutputPad: observeOutputPad2 } = ownerFor(message);
|
|
1190
|
+
observeOutputPad2(this.outputPad);
|
|
1191
|
+
if (!Array.isArray(message?.content) || !this.contentContainer?.children) {
|
|
1192
|
+
original.call(this, message, isStreaming);
|
|
1193
|
+
return;
|
|
1194
|
+
}
|
|
1195
|
+
const hidden = this.hideThinkingBlock;
|
|
1196
|
+
const overrides = this.thinkingVisibilityOverrides;
|
|
1197
|
+
try {
|
|
1198
|
+
this.hideThinkingBlock = false;
|
|
1199
|
+
this.thinkingVisibilityOverrides = /* @__PURE__ */ new Map();
|
|
1200
|
+
original.call(this, message, isStreaming);
|
|
1201
|
+
} finally {
|
|
1202
|
+
this.hideThinkingBlock = hidden;
|
|
1203
|
+
this.thinkingVisibilityOverrides = overrides;
|
|
1204
|
+
}
|
|
1205
|
+
const runs = [];
|
|
1206
|
+
for (let index = 0; index < message.content.length; ) {
|
|
1207
|
+
if (message.content[index].type !== "thinking") {
|
|
1208
|
+
index++;
|
|
1209
|
+
continue;
|
|
1210
|
+
}
|
|
1211
|
+
const start = index;
|
|
1212
|
+
const parts = [];
|
|
1213
|
+
while (message.content[index]?.type === "thinking") {
|
|
1214
|
+
const trace = message.content[index].thinking?.trim();
|
|
1215
|
+
if (trace) parts.push(trace);
|
|
1216
|
+
index++;
|
|
1217
|
+
}
|
|
1218
|
+
if (parts.length) runs.push({ index: start, trace: parts.join("\n\n") });
|
|
1219
|
+
}
|
|
1220
|
+
const children = this.contentContainer.children;
|
|
1221
|
+
textControls.set(this, []);
|
|
1222
|
+
const childKeys = /* @__PURE__ */ new Map();
|
|
1223
|
+
const processByKey = /* @__PURE__ */ new Map();
|
|
1224
|
+
const regions = children.map((child, index) => child.constructor.name === "MouseRegion" ? index : -1).filter((index) => index >= 0);
|
|
1225
|
+
const texts = message.content.flatMap((part, index) => part.type === "text" && part.text?.trim() ? [index] : []);
|
|
1226
|
+
const markdown = children.map((child, index) => child.constructor.name === "Markdown" ? index : -1).filter((index) => index >= 0);
|
|
1227
|
+
if (regions.length !== runs.length || markdown.length !== texts.length) {
|
|
1228
|
+
original.call(this, message, isStreaming);
|
|
1229
|
+
return;
|
|
1230
|
+
}
|
|
1231
|
+
texts.forEach((index, textIndex) => {
|
|
1232
|
+
const native = children[markdown[textIndex]];
|
|
1233
|
+
const key = `text:${message.timestamp}:${index}`;
|
|
1234
|
+
const originalRender = native.render.bind(native);
|
|
1235
|
+
const component = this;
|
|
1236
|
+
native.render = (width) => {
|
|
1237
|
+
const progress = model2.progressForItem(key);
|
|
1238
|
+
if (!progress) return originalRender(width);
|
|
1239
|
+
const line = progress.lead ? progressRow(model2, progress.exchange, getTheme2(), component.outputPad, width) : [];
|
|
1240
|
+
const guide = model2.progressGuideForItem(key);
|
|
1241
|
+
const continuation = guide?.continuation ?? " ";
|
|
1242
|
+
const rows = progress.open ? withGuide(getTheme2(), continuation, originalRender(width - BLOCK_INDENT)) : [];
|
|
1243
|
+
return progress.open ? [...line, ...rows] : line;
|
|
1244
|
+
};
|
|
1245
|
+
childKeys.set(native, key);
|
|
1246
|
+
const controls = textControls.get(this) ?? [];
|
|
1247
|
+
controls.push({ child: native, key });
|
|
1248
|
+
textControls.set(this, controls);
|
|
1249
|
+
});
|
|
1250
|
+
const progressKeys = [
|
|
1251
|
+
...texts.map((index) => `text:${message.timestamp}:${index}`),
|
|
1252
|
+
...runs.map((run) => `thinking:${message.timestamp}:${run.index}`)
|
|
1253
|
+
];
|
|
1254
|
+
const firstContentKey = [
|
|
1255
|
+
...texts.map((index) => ({ index, key: `text:${message.timestamp}:${index}` })),
|
|
1256
|
+
...runs.map((run) => ({ index: run.index, key: `thinking:${message.timestamp}:${run.index}` }))
|
|
1257
|
+
].sort((left, right) => left.index - right.index)[0]?.key;
|
|
1258
|
+
const messageProgress = () => progressKeys.map((key) => model2.progressForItem(key)).find(Boolean);
|
|
1259
|
+
const hasProgressLead = () => progressKeys.some((key) => model2.progressForItem(key)?.lead);
|
|
1260
|
+
const hasFinalText = () => texts.some((index) => !model2.progressForItem(`text:${message.timestamp}:${index}`));
|
|
1261
|
+
const compactItem = (key) => {
|
|
1262
|
+
if (model2.progressForItem(key)?.open) return true;
|
|
1263
|
+
const process2 = processByKey.get(key);
|
|
1264
|
+
return process2 !== void 0 && model2.isProcessOpen(process2.id);
|
|
1265
|
+
};
|
|
1266
|
+
const compactLead = (key) => {
|
|
1267
|
+
const progress = model2.progressForItem(key);
|
|
1268
|
+
if (progress) return progress.lead;
|
|
1269
|
+
const process2 = processByKey.get(key);
|
|
1270
|
+
return process2 !== void 0 && model2.isProcessLead(process2.id, key);
|
|
1271
|
+
};
|
|
1272
|
+
const sameCompactGroup = (left, right) => {
|
|
1273
|
+
const leftProgress = model2.progressForItem(left);
|
|
1274
|
+
const rightProgress = model2.progressForItem(right);
|
|
1275
|
+
if (leftProgress?.open && rightProgress?.open && leftProgress.exchange === rightProgress.exchange) return true;
|
|
1276
|
+
const leftProcess = processByKey.get(left);
|
|
1277
|
+
const rightProcess = processByKey.get(right);
|
|
1278
|
+
return leftProcess !== void 0 && leftProcess.id === rightProcess?.id && model2.isProcessOpen(leftProcess.id);
|
|
1279
|
+
};
|
|
1280
|
+
const initialSpacer = children[0];
|
|
1281
|
+
if (initialSpacer?.constructor.name === "Spacer") {
|
|
1282
|
+
children[0] = {
|
|
1283
|
+
constructor: initialSpacer.constructor,
|
|
1284
|
+
render(width) {
|
|
1285
|
+
if (firstContentKey && compactItem(firstContentKey) && !compactLead(firstContentKey)) return [];
|
|
1286
|
+
const progress = messageProgress();
|
|
1287
|
+
if (progress && !progress.open && !hasProgressLead() && !hasFinalText()) return [];
|
|
1288
|
+
const prefix = (!progress || progress.open) && firstContentKey ? model2.guideBeforeItem(firstContentKey) : void 0;
|
|
1289
|
+
if (prefix) {
|
|
1290
|
+
const lines = initialSpacer.render(width - visibleWidth3(prefix));
|
|
1291
|
+
return withGuide(getTheme2(), prefix, lines);
|
|
1292
|
+
}
|
|
1293
|
+
return initialSpacer.render(width);
|
|
1294
|
+
}
|
|
1295
|
+
};
|
|
1296
|
+
}
|
|
1297
|
+
runs.forEach((run, runIndex) => {
|
|
1298
|
+
const native = children[regions[runIndex]];
|
|
1299
|
+
const process2 = model2.processForThinking(message, run.index);
|
|
1300
|
+
const key = `thinking:${message.timestamp}:${run.index}`;
|
|
1301
|
+
if (process2) processByKey.set(key, process2);
|
|
1302
|
+
const lead = process2 && model2.isProcessLead(process2.id, key);
|
|
1303
|
+
const gapBefore = model2.hasTextImmediatelyBefore(key) && (lead || model2.progressForItem(key)?.lead);
|
|
1304
|
+
const preceding = children[regions[runIndex] - 1];
|
|
1305
|
+
if (process2 && !lead && preceding?.constructor.name === "Spacer") {
|
|
1306
|
+
children[regions[runIndex] - 1] = {
|
|
1307
|
+
constructor: preceding.constructor,
|
|
1308
|
+
render() {
|
|
1309
|
+
return [];
|
|
1310
|
+
}
|
|
1311
|
+
};
|
|
1312
|
+
}
|
|
1313
|
+
const separatorIndex = regions[runIndex] + 1;
|
|
1314
|
+
const separator = children[separatorIndex];
|
|
1315
|
+
if (separator?.constructor.name === "Spacer") {
|
|
1316
|
+
const following = children[separatorIndex + 1];
|
|
1317
|
+
const finalText = textControls.get(this)?.find((control) => control.child === following);
|
|
1318
|
+
children[separatorIndex] = {
|
|
1319
|
+
constructor: separator.constructor,
|
|
1320
|
+
render(width) {
|
|
1321
|
+
const progress = model2.progressForItem(key);
|
|
1322
|
+
if (progress && !progress.open && (!finalText || model2.progressForItem(finalText.key))) return [];
|
|
1323
|
+
return separator.render(width);
|
|
1324
|
+
}
|
|
1325
|
+
};
|
|
1326
|
+
}
|
|
1327
|
+
const onMouse = (event) => {
|
|
1328
|
+
const progress = model2.progressForItem(key);
|
|
1329
|
+
const gapRows = gapBefore && progress?.open !== true ? 1 : 0;
|
|
1330
|
+
if (progress?.lead && event.y === gapRows) {
|
|
1331
|
+
if (event.type === "move") {
|
|
1332
|
+
claimedMove = true;
|
|
1333
|
+
return setHover(model2, `exchange:${progress.exchange}`) ? repaint : void 0;
|
|
1334
|
+
}
|
|
1335
|
+
if (event.type === "click" && event.button === "left") {
|
|
1336
|
+
const control = { kind: "progress", exchange: progress.exchange };
|
|
1337
|
+
if (event.alt) model2.toggleOneLevel(control);
|
|
1338
|
+
else model2.toggleProgress(progress.exchange);
|
|
1339
|
+
folded.call(this, message, isStreaming);
|
|
1340
|
+
requestRender2();
|
|
1341
|
+
return { handled: true, render: false };
|
|
1342
|
+
}
|
|
1343
|
+
}
|
|
1344
|
+
if (progress && !progress.open) return void 0;
|
|
1345
|
+
const y = event.y - gapRows - (progress?.lead ? 1 : 0);
|
|
1346
|
+
if (event.type === "move") {
|
|
1347
|
+
const titleRow = lead ? 1 : 0;
|
|
1348
|
+
const key2 = lead && y === 0 ? `process:${process2.id}` : y === titleRow && (!process2 || model2.isProcessOpen(process2.id)) ? `thinking:${message.timestamp}:${run.index}` : void 0;
|
|
1349
|
+
claimedMove = key2 !== void 0;
|
|
1350
|
+
return setHover(model2, key2) ? repaint : void 0;
|
|
1351
|
+
}
|
|
1352
|
+
if (event.type !== "click" || event.button !== "left") return void 0;
|
|
1353
|
+
if (process2 && lead && y === 0) {
|
|
1354
|
+
const control = { kind: "process", id: process2.id };
|
|
1355
|
+
if (event.alt) model2.toggleOneLevel(control);
|
|
1356
|
+
else model2.toggleProcess(process2.id);
|
|
1357
|
+
} else if (process2 && !model2.isProcessOpen(process2.id)) {
|
|
1358
|
+
const control = { kind: "process", id: process2.id };
|
|
1359
|
+
if (event.alt) model2.toggleOneLevel(control);
|
|
1360
|
+
else model2.toggleProcess(process2.id);
|
|
1361
|
+
} else if (event.alt) {
|
|
1362
|
+
const control = { kind: "thinking", message, index: run.index };
|
|
1363
|
+
model2.toggleOneLevel(control);
|
|
1364
|
+
} else model2.toggleThinking(message, run.index);
|
|
1365
|
+
folded.call(this, message, isStreaming);
|
|
1366
|
+
requestRender2();
|
|
1367
|
+
return { handled: true, render: false };
|
|
1368
|
+
};
|
|
1369
|
+
const component = this;
|
|
1370
|
+
const child = {
|
|
1371
|
+
render(width) {
|
|
1372
|
+
const current = model2.progressForItem(key);
|
|
1373
|
+
const progressLine = current?.lead ? progressRow(model2, current.exchange, getTheme2(), component.outputPad, width) : [];
|
|
1374
|
+
const progressGuide = current?.open ? model2.progressGuideForItem(key) : void 0;
|
|
1375
|
+
const processGuide = process2 ? model2.processBlockGuide(key) : void 0;
|
|
1376
|
+
const withGap = (rows2) => gapBefore && current?.open !== true && rows2.length ? [...withGuide(getTheme2(), progressGuide ? "\u2502 " : "", [""]), ...rows2] : rows2;
|
|
1377
|
+
if (current && !current.open) return withGap(progressLine);
|
|
1378
|
+
if (process2 && !model2.isProcessOpen(process2.id) && !lead) return [];
|
|
1379
|
+
const innerWidth = width - (current ? BLOCK_INDENT : 0);
|
|
1380
|
+
const padding = Math.min(component.outputPad, Math.max(0, Math.floor((width - 1) / 2)));
|
|
1381
|
+
const processText = lead ? truncateProcessLine(model2.processLine(process2.id), innerWidth - padding * 2) : void 0;
|
|
1382
|
+
const processLine = processText === void 0 ? [] : hoverRows(getTheme2(), model2, `process:${process2.id}`, new Text(styleControl(getTheme2(), model2, `process:${process2.id}`, processText), component.outputPad, 0).render(innerWidth), component.outputPad);
|
|
1383
|
+
const processRows = progressGuide && lead ? withGuide(getTheme2(), progressGuide.branch, processLine) : processLine;
|
|
1384
|
+
if (process2 && !model2.isProcessOpen(process2.id)) {
|
|
1385
|
+
const gap2 = progressLine.length && processRows.length && current?.open !== true ? withGuide(getTheme2(), progressGuide?.continuation ?? "", [""]) : [];
|
|
1386
|
+
return withGap([...progressLine, ...gap2, ...processRows]);
|
|
1387
|
+
}
|
|
1388
|
+
const indent = process2 ? BLOCK_INDENT : 0;
|
|
1389
|
+
const title = fitThinkingLine(model2.thinkingTitle(message, run.index, run.trace, component.isStreaming), innerWidth - indent - padding * 2);
|
|
1390
|
+
const styled = styleControl(getTheme2(), model2, `thinking:${message.timestamp}:${run.index}`, title);
|
|
1391
|
+
const titlePrefix = (progressGuide?.continuation ?? "") + (process2 ? processGuide?.branch ?? " " : "");
|
|
1392
|
+
const titleRows = withGuide(getTheme2(), titlePrefix, hoverRows(
|
|
1393
|
+
getTheme2(),
|
|
1394
|
+
model2,
|
|
1395
|
+
key,
|
|
1396
|
+
new Text(styled, component.outputPad, 0).render(innerWidth - indent),
|
|
1397
|
+
component.outputPad
|
|
1398
|
+
));
|
|
1399
|
+
const bodyPrefix = (progressGuide?.continuation ?? "") + (process2 ? processGuide?.continuation ?? " " : "") + (indent ? " ".repeat(BLOCK_INDENT) : "");
|
|
1400
|
+
const body = model2.isThinkingOpen(message, run.index) ? withGuide(getTheme2(), bodyPrefix, native.child.render(innerWidth - indent * 2)) : [];
|
|
1401
|
+
const rows = [...processRows, ...titleRows, ...body];
|
|
1402
|
+
const gap = progressLine.length && rows.length && current?.open !== true ? withGuide(getTheme2(), progressGuide?.continuation ?? "", [""]) : [];
|
|
1403
|
+
return withGap([...progressLine, ...gap, ...rows]);
|
|
1404
|
+
},
|
|
1405
|
+
invalidate() {
|
|
1406
|
+
}
|
|
1407
|
+
};
|
|
1408
|
+
const region = new MouseRegion(child, onMouse);
|
|
1409
|
+
children[regions[runIndex]] = region;
|
|
1410
|
+
childKeys.set(region, key);
|
|
1411
|
+
});
|
|
1412
|
+
const continuationFor = (key) => {
|
|
1413
|
+
const progressPrefix = model2.progressGuideForItem(key)?.continuation ?? "";
|
|
1414
|
+
const process2 = processByKey.get(key);
|
|
1415
|
+
const blockPrefix = process2 && model2.isProcessOpen(process2.id) ? model2.processBlockGuide(key)?.continuation ?? "" : "";
|
|
1416
|
+
return progressPrefix + blockPrefix;
|
|
1417
|
+
};
|
|
1418
|
+
for (let index = 1; index < children.length; index++) {
|
|
1419
|
+
const spacer = children[index];
|
|
1420
|
+
if (spacer.constructor.name !== "Spacer") continue;
|
|
1421
|
+
let previousKey;
|
|
1422
|
+
for (let previous = index - 1; previous >= 0; previous--) {
|
|
1423
|
+
const sibling = children[previous];
|
|
1424
|
+
previousKey = childKeys.get(sibling);
|
|
1425
|
+
if (previousKey || sibling.constructor.name !== "Spacer") break;
|
|
1426
|
+
}
|
|
1427
|
+
let nextKey;
|
|
1428
|
+
for (let next = index + 1; next < children.length; next++) {
|
|
1429
|
+
const sibling = children[next];
|
|
1430
|
+
nextKey = childKeys.get(sibling);
|
|
1431
|
+
if (nextKey || sibling.constructor.name !== "Spacer") break;
|
|
1432
|
+
}
|
|
1433
|
+
const key = previousKey ?? nextKey;
|
|
1434
|
+
if (!key) continue;
|
|
1435
|
+
const originalRender = spacer.render.bind(spacer);
|
|
1436
|
+
children[index] = {
|
|
1437
|
+
constructor: spacer.constructor,
|
|
1438
|
+
render(width) {
|
|
1439
|
+
if (previousKey && nextKey && sameCompactGroup(previousKey, nextKey)) return [];
|
|
1440
|
+
if (previousKey && nextKey && compactItem(previousKey) !== compactItem(nextKey)) return originalRender(width);
|
|
1441
|
+
const prefix = continuationFor(key);
|
|
1442
|
+
return prefix ? withGuide(getTheme2(), prefix, originalRender(Math.max(0, width - visibleWidth3(prefix)))) : originalRender(width);
|
|
1443
|
+
}
|
|
1444
|
+
};
|
|
1445
|
+
}
|
|
1446
|
+
}
|
|
1447
|
+
function folded(message, isStreaming) {
|
|
1448
|
+
if (owners.size === 0) {
|
|
1449
|
+
original.call(this, message, isStreaming);
|
|
1450
|
+
return;
|
|
1451
|
+
}
|
|
1452
|
+
try {
|
|
1453
|
+
foldContent.call(this, message, isStreaming);
|
|
1454
|
+
} catch {
|
|
1455
|
+
original.call(this, message, isStreaming);
|
|
1456
|
+
}
|
|
1457
|
+
}
|
|
1458
|
+
prototype.updateContent = folded;
|
|
1459
|
+
function hoverMouse(event) {
|
|
1460
|
+
claimedMove = false;
|
|
1461
|
+
const pointer = event;
|
|
1462
|
+
if (owners.size > 0 && pointer.y !== void 0 && pointer.width !== void 0) {
|
|
1463
|
+
let row = 0;
|
|
1464
|
+
for (const child of this.contentContainer.children) {
|
|
1465
|
+
const control = textControls.get(this)?.find((item) => item.child === child);
|
|
1466
|
+
const owner = this.lastMessage ? ownerFor(this.lastMessage) : void 0;
|
|
1467
|
+
const progress = control && owner?.model.progressForItem(control.key);
|
|
1468
|
+
if (progress?.lead && pointer.y === row) {
|
|
1469
|
+
if (event.type === "move") return setHover(owner.model, `exchange:${progress.exchange}`) ? repaint : void 0;
|
|
1470
|
+
if (event.type === "click" && pointer.button === "left") {
|
|
1471
|
+
const control2 = { kind: "progress", exchange: progress.exchange };
|
|
1472
|
+
if (pointer.alt) owner?.model.toggleOneLevel(control2);
|
|
1473
|
+
else owner?.model.toggleProgress(progress.exchange);
|
|
1474
|
+
if (this.lastMessage) this.updateContent(this.lastMessage);
|
|
1475
|
+
owner?.requestRender();
|
|
1476
|
+
return { handled: true, render: false };
|
|
1477
|
+
}
|
|
1478
|
+
}
|
|
1479
|
+
row += child.render(pointer.width).length;
|
|
1480
|
+
}
|
|
1481
|
+
}
|
|
1482
|
+
const result = originalMouse?.call(this, event);
|
|
1483
|
+
if (event.type === "move" && owners.size > 0 && !claimedMove && setHover(void 0, void 0)) return result ?? repaint;
|
|
1484
|
+
return result;
|
|
1485
|
+
}
|
|
1486
|
+
prototype.handleMouse = hoverMouse;
|
|
1487
|
+
const owners = /* @__PURE__ */ new Set();
|
|
1488
|
+
const add = (owner) => {
|
|
1489
|
+
owners.add(owner);
|
|
1490
|
+
return { installed: true, restore() {
|
|
1491
|
+
if (!owners.delete(owner)) return;
|
|
1492
|
+
if (hoveredControl?.model === owner.model) setHover(void 0, void 0);
|
|
1493
|
+
if (owners.size > 0) return;
|
|
1494
|
+
if (prototype.updateContent === folded) thinkingOwners.delete(prototype);
|
|
1495
|
+
if (prototype.updateContent === folded) prototype.updateContent = original;
|
|
1496
|
+
if (prototype.handleMouse === hoverMouse && !thinkingOwners.has(prototype)) {
|
|
1497
|
+
if (hadOwnMouse) prototype.handleMouse = originalMouse;
|
|
1498
|
+
else delete prototype.handleMouse;
|
|
1499
|
+
}
|
|
1500
|
+
} };
|
|
1501
|
+
};
|
|
1502
|
+
thinkingOwners.set(prototype, { add });
|
|
1503
|
+
return add({ model, getTheme, requestRender, observeOutputPad });
|
|
1504
|
+
}
|
|
1505
|
+
|
|
1506
|
+
// extensions/focus-mode/focus-mode.ts
|
|
1507
|
+
import { Box, Text as Text2 } from "@earendil-works/pi-tui";
|
|
1508
|
+
var ENTRY_TYPE = "exchange-stats";
|
|
1509
|
+
var HEADLINE_ENTRY_TYPE = "exchange-stats-headline";
|
|
1510
|
+
var STATUS_KEY = "exchange";
|
|
1511
|
+
var TICK_MS = 1e3;
|
|
1512
|
+
var TOOL_SHARE_NOTE = 0.25;
|
|
1513
|
+
var FOLD_KEYS = {
|
|
1514
|
+
processKey: { default: "ctrl+alt+f", env: "PI_FOCUS_MODE_PROCESS_KEY" },
|
|
1515
|
+
exchangeKey: { default: "ctrl+alt+e", env: "PI_FOCUS_MODE_EXCHANGE_KEY" },
|
|
1516
|
+
pickerKey: { default: "ctrl+alt+s", env: "PI_FOCUS_MODE_PICKER_KEY" },
|
|
1517
|
+
cursorKey: { default: "ctrl+alt+g", env: "PI_FOCUS_MODE_CURSOR_KEY" }
|
|
1518
|
+
};
|
|
1519
|
+
var CURSOR_SETTING = { cursorMode: { default: false, env: "PI_FOCUS_MODE_CURSOR_MODE", parseEnv: (value) => value === "true" } };
|
|
1520
|
+
var SUMMARY_SETTING = { summaryModel: { default: "", env: "PI_FOCUS_MODE_SUMMARY_MODEL" } };
|
|
1521
|
+
var emptyTotals = () => ({
|
|
1522
|
+
input: 0,
|
|
1523
|
+
output: 0,
|
|
1524
|
+
reasoning: 0,
|
|
1525
|
+
cacheRead: 0,
|
|
1526
|
+
cacheWrite: 0,
|
|
1527
|
+
totalTokens: 0,
|
|
1528
|
+
cost: 0
|
|
1529
|
+
});
|
|
1530
|
+
function fmtDuration(ms) {
|
|
1531
|
+
if (!Number.isFinite(ms) || ms < 0) return "\u2014";
|
|
1532
|
+
if (ms < 1e3) return `${Math.round(ms)}ms`;
|
|
1533
|
+
const s = ms / 1e3;
|
|
1534
|
+
if (s < 60) return `${s.toFixed(1)}s`;
|
|
1535
|
+
const m = Math.floor(s / 60);
|
|
1536
|
+
const rem = Math.round(s % 60);
|
|
1537
|
+
return rem > 0 ? `${m}m${rem}s` : `${m}m`;
|
|
1538
|
+
}
|
|
1539
|
+
function fmtLocalFinishTime(at) {
|
|
1540
|
+
const date = new Date(at);
|
|
1541
|
+
const time = new Intl.DateTimeFormat(void 0, {
|
|
1542
|
+
hour: "2-digit",
|
|
1543
|
+
minute: "2-digit",
|
|
1544
|
+
second: "2-digit",
|
|
1545
|
+
hourCycle: "h23"
|
|
1546
|
+
}).format(date);
|
|
1547
|
+
const calendarDate = new Intl.DateTimeFormat(void 0, { year: "numeric", month: "numeric", day: "numeric" });
|
|
1548
|
+
if (calendarDate.format(date) === calendarDate.format(new Date(Date.now()))) return time;
|
|
1549
|
+
return new Intl.DateTimeFormat(void 0, { dateStyle: "medium" }).format(date) + " " + time;
|
|
1550
|
+
}
|
|
1551
|
+
function fmtTokens(n) {
|
|
1552
|
+
if (!Number.isFinite(n) || n <= 0) return "0";
|
|
1553
|
+
if (n < 1e3) return `${Math.round(n)}`;
|
|
1554
|
+
if (n < 1e6) return `${(n / 1e3).toFixed(1)}k`;
|
|
1555
|
+
return `${(n / 1e6).toFixed(2)}M`;
|
|
1556
|
+
}
|
|
1557
|
+
function fmtCost(cost) {
|
|
1558
|
+
if (!Number.isFinite(cost) || cost <= 0) return "$0";
|
|
1559
|
+
return cost < 0.01 ? `$${cost.toFixed(5)}` : `$${cost.toFixed(4)}`;
|
|
1560
|
+
}
|
|
1561
|
+
function fmtCacheUsage(cacheRead, cacheWrite) {
|
|
1562
|
+
const read = cacheRead > 0 ? fmtTokens(cacheRead) : void 0;
|
|
1563
|
+
const written = cacheWrite > 0 ? fmtTokens(cacheWrite) : void 0;
|
|
1564
|
+
if (read && written) return `cache ${read} / ${written} written`;
|
|
1565
|
+
if (read) return `cache ${read}`;
|
|
1566
|
+
if (written) return `cache ${written} written`;
|
|
1567
|
+
return void 0;
|
|
1568
|
+
}
|
|
1569
|
+
function plural(n, one, many = `${one}s`) {
|
|
1570
|
+
return n === 1 ? `${n} ${one}` : `${n} ${many}`;
|
|
1571
|
+
}
|
|
1572
|
+
function openToolText(spans, now) {
|
|
1573
|
+
let longest;
|
|
1574
|
+
for (const span of spans.values()) {
|
|
1575
|
+
if (!longest || now - span.start > now - longest.start) longest = span;
|
|
1576
|
+
}
|
|
1577
|
+
if (!longest) return void 0;
|
|
1578
|
+
const extra = spans.size > 1 ? ` (+${spans.size - 1})` : "";
|
|
1579
|
+
return `${longest.name} ${fmtDuration(now - longest.start)}${extra}`;
|
|
1580
|
+
}
|
|
1581
|
+
function unionMs(runs) {
|
|
1582
|
+
if (runs.length === 0) return 0;
|
|
1583
|
+
const sorted = [...runs].sort((a, b) => a.start - b.start);
|
|
1584
|
+
let total = 0;
|
|
1585
|
+
let start = sorted[0].start;
|
|
1586
|
+
let end = sorted[0].end;
|
|
1587
|
+
for (const run of sorted.slice(1)) {
|
|
1588
|
+
if (run.start > end) {
|
|
1589
|
+
total += end - start;
|
|
1590
|
+
start = run.start;
|
|
1591
|
+
end = run.end;
|
|
1592
|
+
} else if (run.end > end) {
|
|
1593
|
+
end = run.end;
|
|
1594
|
+
}
|
|
1595
|
+
}
|
|
1596
|
+
return total + (end - start);
|
|
1597
|
+
}
|
|
1598
|
+
function registerExchangeStats(pi, toolComponent = ToolExecutionComponent, settingsRuntime = {}) {
|
|
1599
|
+
let toolFold = new ToolFoldModel();
|
|
1600
|
+
let themeContext;
|
|
1601
|
+
const getTitleTheme = () => themeContext?.ui.theme;
|
|
1602
|
+
let outputPad = 1;
|
|
1603
|
+
let lastStatus = "";
|
|
1604
|
+
const requestRender = () => {
|
|
1605
|
+
if (themeContext) themeContext.ui.setStatus(STATUS_KEY, toolFold.cursorTitle() ? `${lastStatus} \xB7 Cursor ${toolFold.cursorTitle()}` : lastStatus);
|
|
1606
|
+
};
|
|
1607
|
+
let toolPatch;
|
|
1608
|
+
let thinkingPatch;
|
|
1609
|
+
let sessionActive = false;
|
|
1610
|
+
const resolvedKeys = resolveSettings("focus-mode", FOLD_KEYS, {
|
|
1611
|
+
cwd: process.cwd(),
|
|
1612
|
+
hasUI: true,
|
|
1613
|
+
isProjectTrusted: () => false
|
|
1614
|
+
}, settingsRuntime);
|
|
1615
|
+
const cursorMode = resolveSettings("focus-mode", CURSOR_SETTING, {
|
|
1616
|
+
cwd: process.cwd(),
|
|
1617
|
+
hasUI: true,
|
|
1618
|
+
isProjectTrusted: () => false
|
|
1619
|
+
}, settingsRuntime).cursorMode.value === true;
|
|
1620
|
+
const validKey = (value) => {
|
|
1621
|
+
if (typeof value !== "string") return false;
|
|
1622
|
+
const parts = value.split("+");
|
|
1623
|
+
const base = parts.pop();
|
|
1624
|
+
return parts.length > 0 && new Set(parts).size === parts.length && parts.every((part) => ["ctrl", "alt", "shift", "super"].includes(part)) && base !== void 0 && (/^[a-z0-9]$/.test(base) || ["enter", "escape", "tab", "space", "backspace", "delete", "up", "down", "left", "right", "home", "end"].includes(base));
|
|
1625
|
+
};
|
|
1626
|
+
const keys = Object.fromEntries(Object.entries(FOLD_KEYS).map(([name, definition]) => {
|
|
1627
|
+
const value = resolvedKeys[name].value;
|
|
1628
|
+
return [name, validKey(value) ? value : definition.default];
|
|
1629
|
+
}));
|
|
1630
|
+
const invalidKey = Object.entries(FOLD_KEYS).some(([name]) => !validKey(resolvedKeys[name].value));
|
|
1631
|
+
let warnedAboutKey = false;
|
|
1632
|
+
let warnedAboutToolFold = false;
|
|
1633
|
+
let warnedAboutThinkingFold = false;
|
|
1634
|
+
let summaryScheduler;
|
|
1635
|
+
let summarySession = 0;
|
|
1636
|
+
let restoredEntries = [];
|
|
1637
|
+
let sessionTotals = {
|
|
1638
|
+
...emptyTotals(),
|
|
1639
|
+
exchanges: 0,
|
|
1640
|
+
turnCount: 0,
|
|
1641
|
+
durationMs: 0,
|
|
1642
|
+
toolMs: 0,
|
|
1643
|
+
waitingMs: 0
|
|
1644
|
+
};
|
|
1645
|
+
let sessionStartedAt = 0;
|
|
1646
|
+
let running = false;
|
|
1647
|
+
let startedAt = 0;
|
|
1648
|
+
let promptCount = 0;
|
|
1649
|
+
let exchangeIndex = 0;
|
|
1650
|
+
let model = "unknown";
|
|
1651
|
+
let stopReason = "stop";
|
|
1652
|
+
let turns = [];
|
|
1653
|
+
let totals = emptyTotals();
|
|
1654
|
+
let waitingMs = 0;
|
|
1655
|
+
let waitingStart;
|
|
1656
|
+
let liveTimer;
|
|
1657
|
+
let liveRefresh;
|
|
1658
|
+
let activeTurn;
|
|
1659
|
+
let activeToolRuns = [];
|
|
1660
|
+
function setStatus(text, ctx) {
|
|
1661
|
+
if (!ctx.hasUI) return;
|
|
1662
|
+
lastStatus = text;
|
|
1663
|
+
ctx.ui.setStatus(STATUS_KEY, toolFold.cursorTitle() ? `${text} \xB7 Cursor ${toolFold.cursorTitle()}` : text);
|
|
1664
|
+
}
|
|
1665
|
+
pi.registerShortcut(keys.processKey, { description: "Toggle latest process", handler: (ctx) => {
|
|
1666
|
+
if (toolFold.toggleLatestProcess() !== void 0) {
|
|
1667
|
+
themeContext = ctx;
|
|
1668
|
+
requestRender();
|
|
1669
|
+
}
|
|
1670
|
+
} });
|
|
1671
|
+
pi.registerShortcut(keys.exchangeKey, { description: "Toggle latest exchange", handler: (ctx) => {
|
|
1672
|
+
if (toolFold.toggleLatestExchange() !== void 0) {
|
|
1673
|
+
themeContext = ctx;
|
|
1674
|
+
requestRender();
|
|
1675
|
+
}
|
|
1676
|
+
} });
|
|
1677
|
+
pi.registerShortcut(keys.pickerKey, { description: "Choose exchange, process or block to fold", handler: async (ctx) => {
|
|
1678
|
+
if (!ctx.hasUI) return;
|
|
1679
|
+
await ctx.ui.custom((tui, theme, _keybindings, done) => new FoldPicker(toolFold, () => ctx.ui.theme ?? theme, () => tui.requestRender(), () => done(void 0)), { overlay: true });
|
|
1680
|
+
} });
|
|
1681
|
+
if (cursorMode) pi.registerShortcut(keys.cursorKey, { description: "Move through transcript folds", handler: async (ctx) => {
|
|
1682
|
+
if (!ctx.hasUI || !toolFold.startCursor()) return;
|
|
1683
|
+
themeContext = ctx;
|
|
1684
|
+
requestRender();
|
|
1685
|
+
try {
|
|
1686
|
+
await ctx.ui.custom((_tui, _theme, _keybindings, done) => new TranscriptCursor(toolFold, requestRender, () => done(void 0)), { overlay: true });
|
|
1687
|
+
} finally {
|
|
1688
|
+
toolFold.stopCursor();
|
|
1689
|
+
requestRender();
|
|
1690
|
+
}
|
|
1691
|
+
} });
|
|
1692
|
+
function liveStatusText() {
|
|
1693
|
+
const now = Date.now();
|
|
1694
|
+
const parts = [`\u23F1 ${fmtDuration(now - startedAt)}`];
|
|
1695
|
+
if (turns.length > 0) parts.push(`${plural(turns.length, "turn")} done`);
|
|
1696
|
+
const toolText = activeTurn ? openToolText(activeTurn.spans, now) : void 0;
|
|
1697
|
+
if (toolText) parts.push(toolText);
|
|
1698
|
+
else if (activeTurn) parts.push(`turn ${activeTurn.index}`);
|
|
1699
|
+
return parts.join(" \xB7 ");
|
|
1700
|
+
}
|
|
1701
|
+
function stopLiveTimer() {
|
|
1702
|
+
if (liveTimer) clearInterval(liveTimer);
|
|
1703
|
+
liveTimer = void 0;
|
|
1704
|
+
liveRefresh = void 0;
|
|
1705
|
+
}
|
|
1706
|
+
function resetExchangeState() {
|
|
1707
|
+
running = false;
|
|
1708
|
+
startedAt = 0;
|
|
1709
|
+
promptCount = 0;
|
|
1710
|
+
turns = [];
|
|
1711
|
+
totals = emptyTotals();
|
|
1712
|
+
waitingMs = 0;
|
|
1713
|
+
waitingStart = void 0;
|
|
1714
|
+
activeTurn = void 0;
|
|
1715
|
+
activeToolRuns = [];
|
|
1716
|
+
stopLiveTimer();
|
|
1717
|
+
}
|
|
1718
|
+
function restoreSession(ctx) {
|
|
1719
|
+
const entries = ctx.sessionManager?.getBranch() ?? [];
|
|
1720
|
+
restoredEntries = entries;
|
|
1721
|
+
toolFold.clear((id) => {
|
|
1722
|
+
let correction;
|
|
1723
|
+
for (let index = restoredEntries.length - 1; index >= 0; index--) {
|
|
1724
|
+
const entry = restoredEntries[index];
|
|
1725
|
+
if (entry.type !== "custom") continue;
|
|
1726
|
+
const patch = entry.customType === HEADLINE_ENTRY_TYPE ? entry.data : void 0;
|
|
1727
|
+
if (!correction && patch?.id === id) correction = patch;
|
|
1728
|
+
if (entry.customType !== ENTRY_TYPE) continue;
|
|
1729
|
+
const found = entry.data?.blocks?.find((block) => block.id === id);
|
|
1730
|
+
if (found) return found.kind === "thinking" && correction ? { ...found, headline: correction.headline, headlineSource: correction.headlineSource } : found;
|
|
1731
|
+
}
|
|
1732
|
+
return void 0;
|
|
1733
|
+
});
|
|
1734
|
+
const recordedIndex = entries.reduce((highest, entry) => entry.type === "custom" && entry.customType === ENTRY_TYPE && entry.data?.kind === "exchange" ? Math.max(highest, entry.data.index) : highest, 0);
|
|
1735
|
+
if (!running) exchangeIndex = recordedIndex;
|
|
1736
|
+
const lastCompaction = entries.findLastIndex((entry) => entry.type === "compaction");
|
|
1737
|
+
const active = entries.slice(lastCompaction + 1);
|
|
1738
|
+
let nextExchange = active.find((entry) => entry.type === "custom" && entry.customType === ENTRY_TYPE && entry.data?.kind === "exchange")?.data?.index ?? (running ? exchangeIndex : exchangeIndex + 1);
|
|
1739
|
+
let inExchange = false;
|
|
1740
|
+
for (const entry of active) {
|
|
1741
|
+
if (entry.type === "custom" && entry.customType === ENTRY_TYPE && entry.data?.kind === "exchange") {
|
|
1742
|
+
toolFold.endExchange(entry.data.progressDurationMs ?? entry.data.durationMs);
|
|
1743
|
+
inExchange = false;
|
|
1744
|
+
nextExchange = entry.data.index + 1;
|
|
1745
|
+
} else if (entry.type === "message" && entry.message?.role === "assistant" && Array.isArray(entry.message.content)) {
|
|
1746
|
+
if (!inExchange) {
|
|
1747
|
+
toolFold.beginExchange(nextExchange);
|
|
1748
|
+
inExchange = true;
|
|
1749
|
+
}
|
|
1750
|
+
toolFold.ingest(entry.message);
|
|
1751
|
+
} else if (entry.type === "message" && entry.message?.role === "toolResult" && entry.message.toolCallId) {
|
|
1752
|
+
toolFold.end(entry.message.toolCallId, false, { content: entry.message.content });
|
|
1753
|
+
}
|
|
1754
|
+
}
|
|
1755
|
+
if (!running && inExchange) toolFold.endExchange();
|
|
1756
|
+
else if (running && !inExchange) toolFold.beginExchange(exchangeIndex);
|
|
1757
|
+
}
|
|
1758
|
+
pi.registerEntryRenderer(ENTRY_TYPE, (entry, _options, theme) => {
|
|
1759
|
+
const data = entry.data;
|
|
1760
|
+
const box = new Box(1, 1, (text) => theme.bg("customMessageBg", text));
|
|
1761
|
+
const dimText = (text) => {
|
|
1762
|
+
const colored = theme.fg("dim", text);
|
|
1763
|
+
return theme.italic?.(colored) ?? colored;
|
|
1764
|
+
};
|
|
1765
|
+
const boxMouse = box.handleMouse.bind(box);
|
|
1766
|
+
box.handleMouse = (event) => endHoverOnMove(event) ?? boxMouse(event);
|
|
1767
|
+
if (!data) {
|
|
1768
|
+
box.addChild(new Text2(dimText("(no stats)"), 0, 0));
|
|
1769
|
+
return box;
|
|
1770
|
+
}
|
|
1771
|
+
const isSession = data.kind === "session";
|
|
1772
|
+
const finishTime = data.endedAt === void 0 ? "" : ` \xB7 ${fmtLocalFinishTime(data.endedAt)}`;
|
|
1773
|
+
const headline2 = isSession ? `\u{1F4CA} Session \xB7 ${plural(data.turnCount, "turn")} across ${plural(data.index, "exchange")}` : `\u23F1 ${fmtDuration(data.durationMs)}${finishTime}`;
|
|
1774
|
+
const summary2 = [`in ${fmtTokens(data.input)}`, `out ${fmtTokens(data.output)}`];
|
|
1775
|
+
const cache = fmtCacheUsage(data.cacheRead, data.cacheWrite);
|
|
1776
|
+
if (cache) summary2.push(cache);
|
|
1777
|
+
if (data.waitingMs > 0) summary2.push(`waiting ${fmtDuration(data.waitingMs)}`);
|
|
1778
|
+
summary2.push(fmtCost(data.cost));
|
|
1779
|
+
box.addChild(new Text2(dimText(`${headline2} \xB7 ${data.model} (${summary2.join(" \xB7 ")})`), 0, 0));
|
|
1780
|
+
return box;
|
|
1781
|
+
});
|
|
1782
|
+
pi.on("session_start", (_event, ctx) => {
|
|
1783
|
+
if (sessionActive) return;
|
|
1784
|
+
sessionActive = true;
|
|
1785
|
+
resetExchangeState();
|
|
1786
|
+
toolFold = new ToolFoldModel();
|
|
1787
|
+
outputPad = 1;
|
|
1788
|
+
lastStatus = "";
|
|
1789
|
+
restoredEntries = [];
|
|
1790
|
+
sessionTotals = { ...emptyTotals(), exchanges: 0, turnCount: 0, durationMs: 0, toolMs: 0, waitingMs: 0 };
|
|
1791
|
+
toolPatch = installToolFold(toolComponent, toolFold, getTitleTheme, () => outputPad);
|
|
1792
|
+
thinkingPatch = installThinkingFold(AssistantMessageComponent, toolFold, getTitleTheme, requestRender, (padding) => {
|
|
1793
|
+
outputPad = padding;
|
|
1794
|
+
});
|
|
1795
|
+
themeContext = ctx;
|
|
1796
|
+
const session = ++summarySession;
|
|
1797
|
+
summaryScheduler?.dispose();
|
|
1798
|
+
summaryScheduler = void 0;
|
|
1799
|
+
const summaryValue = resolveSettings("focus-mode", SUMMARY_SETTING, {
|
|
1800
|
+
cwd: ctx.cwd ?? process.cwd(),
|
|
1801
|
+
hasUI: ctx.hasUI,
|
|
1802
|
+
isProjectTrusted: () => ctx.isProjectTrusted?.() ?? false
|
|
1803
|
+
}, settingsRuntime).summaryModel.value;
|
|
1804
|
+
const configuredModel = typeof summaryValue === "string" ? summaryValue.trim() : "";
|
|
1805
|
+
if (configuredModel) {
|
|
1806
|
+
const slash = configuredModel.indexOf("/");
|
|
1807
|
+
const chosen = slash > 0 ? ctx.modelRegistry?.find(configuredModel.slice(0, slash), configuredModel.slice(slash + 1)) : void 0;
|
|
1808
|
+
if (!chosen) {
|
|
1809
|
+
announce(ctx, `focus-mode: unknown summaryModel ${configuredModel}; using trace sentence`, "warning", `focus-mode:unknown-summary:${session}`);
|
|
1810
|
+
} else {
|
|
1811
|
+
summaryScheduler = new HeadlineScheduler({
|
|
1812
|
+
summarize: async (trace, signal) => {
|
|
1813
|
+
const response = await ctx.modelRegistry.streamSimple(chosen, {
|
|
1814
|
+
systemPrompt: "Summarize this live thinking trace as a headline of at most 10 words. Return only the headline.",
|
|
1815
|
+
messages: [{ role: "user", content: [{ type: "text", text: trace }], timestamp: Date.now() }]
|
|
1816
|
+
}, { reasoning: "off", maxTokens: 32, signal }).result();
|
|
1817
|
+
if (response.stopReason === "length" && !response.content.some((part) => part.type === "text" && part.text.trim())) {
|
|
1818
|
+
throw new HeadlineLengthError("model did not answer within its output cap");
|
|
1819
|
+
}
|
|
1820
|
+
if (response.stopReason !== "stop") throw new Error(response.errorMessage ?? "headline request failed");
|
|
1821
|
+
return response.content.filter((part) => part.type === "text").map((part) => part.text).join(" ");
|
|
1822
|
+
},
|
|
1823
|
+
onHeadline: (key, headline2) => {
|
|
1824
|
+
const [timestamp, index] = key.split(":").map(Number);
|
|
1825
|
+
const patch = toolFold.setThinkingHeadline({ timestamp }, index, headline2);
|
|
1826
|
+
if (patch) {
|
|
1827
|
+
try {
|
|
1828
|
+
pi.appendEntry(HEADLINE_ENTRY_TYPE, patch);
|
|
1829
|
+
restoredEntries.push({ type: "custom", customType: HEADLINE_ENTRY_TYPE, data: patch });
|
|
1830
|
+
} catch {
|
|
1831
|
+
}
|
|
1832
|
+
}
|
|
1833
|
+
requestRender();
|
|
1834
|
+
},
|
|
1835
|
+
onFailure: (reason, message) => {
|
|
1836
|
+
const detail = reason === "length" ? 'model did not answer within its output cap; set thinkingLevelMap.off to "none" for this model' : reason === "timeout" ? "request timed out" : (message ?? "headline request failed").replace(/\s+/g, " ").trim().slice(0, 120);
|
|
1837
|
+
announce(ctx, `focus-mode: headline summary ${configuredModel}: ${detail}; using trace sentence`, "warning", `focus-mode:headline-failure:${session}`);
|
|
1838
|
+
}
|
|
1839
|
+
});
|
|
1840
|
+
}
|
|
1841
|
+
}
|
|
1842
|
+
if (invalidKey && !warnedAboutKey) {
|
|
1843
|
+
announce(ctx, "focus-mode: invalid fold shortcut; using default key", "warning", "focus-mode:invalid-fold-key");
|
|
1844
|
+
warnedAboutKey = true;
|
|
1845
|
+
}
|
|
1846
|
+
if (!toolPatch.installed && !warnedAboutToolFold) {
|
|
1847
|
+
announce(ctx, "focus-mode: tool folding unavailable; Pi tool rows remain native", "warning");
|
|
1848
|
+
warnedAboutToolFold = true;
|
|
1849
|
+
}
|
|
1850
|
+
if (!thinkingPatch.installed && !warnedAboutThinkingFold) {
|
|
1851
|
+
announce(ctx, "focus-mode: thinking folding unavailable; Pi thinking remains native", "warning");
|
|
1852
|
+
warnedAboutThinkingFold = true;
|
|
1853
|
+
}
|
|
1854
|
+
restoreSession(ctx);
|
|
1855
|
+
sessionStartedAt = Date.now();
|
|
1856
|
+
setStatus("\u23F1 ready", ctx);
|
|
1857
|
+
});
|
|
1858
|
+
pi.on("session_shutdown", () => {
|
|
1859
|
+
if (!sessionActive) return;
|
|
1860
|
+
sessionActive = false;
|
|
1861
|
+
themeContext = void 0;
|
|
1862
|
+
toolFold.stopCursor();
|
|
1863
|
+
summaryScheduler?.dispose();
|
|
1864
|
+
summaryScheduler = void 0;
|
|
1865
|
+
toolPatch?.restore();
|
|
1866
|
+
thinkingPatch?.restore();
|
|
1867
|
+
toolPatch = void 0;
|
|
1868
|
+
thinkingPatch = void 0;
|
|
1869
|
+
resetExchangeState();
|
|
1870
|
+
toolFold = new ToolFoldModel();
|
|
1871
|
+
restoredEntries = [];
|
|
1872
|
+
});
|
|
1873
|
+
pi.on("session_compact", (_event, ctx) => {
|
|
1874
|
+
restoreSession(ctx);
|
|
1875
|
+
requestRender();
|
|
1876
|
+
});
|
|
1877
|
+
pi.on("session_tree", (_event, ctx) => {
|
|
1878
|
+
restoreSession(ctx);
|
|
1879
|
+
requestRender();
|
|
1880
|
+
});
|
|
1881
|
+
pi.on("message_update", (event) => {
|
|
1882
|
+
if (event.message.role !== "assistant") return;
|
|
1883
|
+
toolFold.observeThinking(event.message, event.assistantMessageEvent, event.message.usage?.reasoning);
|
|
1884
|
+
toolFold.ingest(event.message);
|
|
1885
|
+
const update = event.assistantMessageEvent;
|
|
1886
|
+
if (update.type === "thinking_delta" && update.contentIndex !== void 0) {
|
|
1887
|
+
let start = update.contentIndex;
|
|
1888
|
+
while (start > 0 && event.message.content[start - 1]?.type === "thinking") start--;
|
|
1889
|
+
let end = start;
|
|
1890
|
+
while (event.message.content[end]?.type === "thinking") end++;
|
|
1891
|
+
const trace = event.message.content.slice(start, end).map((item) => item.type === "thinking" ? item.thinking.trim() : "").filter(Boolean).join("\n\n");
|
|
1892
|
+
if (trace) summaryScheduler?.observe(`${event.message.timestamp}:${start}`, trace);
|
|
1893
|
+
} else if (update.type !== "thinking_start") {
|
|
1894
|
+
for (let index = 0; index < event.message.content.length; index++) {
|
|
1895
|
+
if (event.message.content[index].type === "thinking") summaryScheduler?.settle(`${event.message.timestamp}:${index}`);
|
|
1896
|
+
}
|
|
1897
|
+
}
|
|
1898
|
+
});
|
|
1899
|
+
pi.on("message_end", (event) => {
|
|
1900
|
+
if (event.message.role === "assistant") {
|
|
1901
|
+
toolFold.ingest(event.message);
|
|
1902
|
+
toolFold.settleThinking(event.message);
|
|
1903
|
+
for (let index = 0; index < event.message.content.length; index++) {
|
|
1904
|
+
if (event.message.content[index].type === "thinking") summaryScheduler?.settle(`${event.message.timestamp}:${index}`);
|
|
1905
|
+
}
|
|
1906
|
+
}
|
|
1907
|
+
});
|
|
1908
|
+
pi.on("before_agent_start", (_event, ctx) => {
|
|
1909
|
+
if (running) {
|
|
1910
|
+
promptCount++;
|
|
1911
|
+
return;
|
|
1912
|
+
}
|
|
1913
|
+
running = true;
|
|
1914
|
+
toolFold.beginExchange(exchangeIndex + 1);
|
|
1915
|
+
startedAt = Date.now();
|
|
1916
|
+
promptCount = 1;
|
|
1917
|
+
exchangeIndex++;
|
|
1918
|
+
turns = [];
|
|
1919
|
+
totals = emptyTotals();
|
|
1920
|
+
waitingMs = 0;
|
|
1921
|
+
waitingStart = void 0;
|
|
1922
|
+
activeTurn = void 0;
|
|
1923
|
+
activeToolRuns = [];
|
|
1924
|
+
model = ctx.model?.id ?? "unknown";
|
|
1925
|
+
stopReason = "stop";
|
|
1926
|
+
stopLiveTimer();
|
|
1927
|
+
setStatus(liveStatusText(), ctx);
|
|
1928
|
+
liveRefresh = () => setStatus(liveStatusText(), ctx);
|
|
1929
|
+
liveTimer = setInterval(() => {
|
|
1930
|
+
try {
|
|
1931
|
+
liveRefresh?.();
|
|
1932
|
+
summaryScheduler?.tick();
|
|
1933
|
+
} catch {
|
|
1934
|
+
stopLiveTimer();
|
|
1935
|
+
}
|
|
1936
|
+
}, TICK_MS);
|
|
1937
|
+
});
|
|
1938
|
+
pi.on("turn_start", (event, _ctx) => {
|
|
1939
|
+
if (!running) return;
|
|
1940
|
+
activeTurn = { index: event.turnIndex ?? turns.length + 1, startedAt: Date.now(), spans: /* @__PURE__ */ new Map() };
|
|
1941
|
+
activeToolRuns = [];
|
|
1942
|
+
});
|
|
1943
|
+
pi.on("tool_execution_start", (event, _ctx) => {
|
|
1944
|
+
toolFold.start(event.toolCallId, event.toolName);
|
|
1945
|
+
if (!running || !activeTurn) return;
|
|
1946
|
+
activeTurn.spans.set(event.toolCallId, { name: event.toolName, start: Date.now() });
|
|
1947
|
+
});
|
|
1948
|
+
pi.on("tool_execution_end", (event, _ctx) => {
|
|
1949
|
+
toolFold.end(event.toolCallId, Boolean(event.isError), event.result);
|
|
1950
|
+
if (!running || !activeTurn) return;
|
|
1951
|
+
const open = activeTurn.spans.get(event.toolCallId);
|
|
1952
|
+
if (open) {
|
|
1953
|
+
activeTurn.spans.delete(event.toolCallId);
|
|
1954
|
+
activeToolRuns.push({
|
|
1955
|
+
name: event.toolName ?? open.name,
|
|
1956
|
+
start: open.start,
|
|
1957
|
+
end: Date.now(),
|
|
1958
|
+
isError: Boolean(event.isError)
|
|
1959
|
+
});
|
|
1960
|
+
return;
|
|
1961
|
+
}
|
|
1962
|
+
const at = Date.now();
|
|
1963
|
+
activeToolRuns.push({ name: event.toolName, start: at, end: at, isError: Boolean(event.isError) });
|
|
1964
|
+
});
|
|
1965
|
+
pi.on("turn_end", (event, _ctx) => {
|
|
1966
|
+
if (!running || !activeTurn) return;
|
|
1967
|
+
const endedAt = Date.now();
|
|
1968
|
+
for (const [id, open] of activeTurn.spans) {
|
|
1969
|
+
activeToolRuns.push({ name: open.name, start: open.start, end: endedAt, isError: false });
|
|
1970
|
+
activeTurn.spans.delete(id);
|
|
1971
|
+
}
|
|
1972
|
+
const usage = event.message?.role === "assistant" ? event.message.usage : void 0;
|
|
1973
|
+
const durationMs = endedAt - activeTurn.startedAt;
|
|
1974
|
+
const toolMs = unionMs(activeToolRuns);
|
|
1975
|
+
const output = usage?.output ?? 0;
|
|
1976
|
+
turns.push({
|
|
1977
|
+
index: activeTurn.index,
|
|
1978
|
+
durationMs,
|
|
1979
|
+
toolMs,
|
|
1980
|
+
modelMs: Math.max(0, durationMs - toolMs),
|
|
1981
|
+
outputPerSec: durationMs > 0 ? output / (durationMs / 1e3) : 0,
|
|
1982
|
+
tools: activeToolRuns.map((run) => ({ name: run.name, ms: run.end - run.start, isError: run.isError })),
|
|
1983
|
+
input: usage?.input ?? 0,
|
|
1984
|
+
output,
|
|
1985
|
+
reasoning: usage?.reasoning ?? 0,
|
|
1986
|
+
cacheRead: usage?.cacheRead ?? 0,
|
|
1987
|
+
cacheWrite: usage?.cacheWrite ?? 0,
|
|
1988
|
+
totalTokens: usage?.totalTokens ?? 0,
|
|
1989
|
+
cost: usage?.cost?.total ?? 0
|
|
1990
|
+
});
|
|
1991
|
+
totals.input += usage?.input ?? 0;
|
|
1992
|
+
totals.output += output;
|
|
1993
|
+
totals.reasoning += usage?.reasoning ?? 0;
|
|
1994
|
+
totals.cacheRead += usage?.cacheRead ?? 0;
|
|
1995
|
+
totals.cacheWrite += usage?.cacheWrite ?? 0;
|
|
1996
|
+
totals.totalTokens += usage?.totalTokens ?? 0;
|
|
1997
|
+
totals.cost += usage?.cost?.total ?? 0;
|
|
1998
|
+
if (event.message?.stopReason) stopReason = event.message.stopReason;
|
|
1999
|
+
activeTurn = void 0;
|
|
2000
|
+
activeToolRuns = [];
|
|
2001
|
+
});
|
|
2002
|
+
pi.on("ui_prompt_start", (_event, _ctx) => {
|
|
2003
|
+
if (!running || waitingStart !== void 0) return;
|
|
2004
|
+
waitingStart = Date.now();
|
|
2005
|
+
});
|
|
2006
|
+
pi.on("ui_prompt_end", (_event, _ctx) => {
|
|
2007
|
+
if (waitingStart === void 0) return;
|
|
2008
|
+
waitingMs += Date.now() - waitingStart;
|
|
2009
|
+
waitingStart = void 0;
|
|
2010
|
+
});
|
|
2011
|
+
pi.on("agent_settled", (_event, ctx) => {
|
|
2012
|
+
if (!running) return;
|
|
2013
|
+
const endedAt = Date.now();
|
|
2014
|
+
if (waitingStart !== void 0) {
|
|
2015
|
+
waitingMs += endedAt - waitingStart;
|
|
2016
|
+
waitingStart = void 0;
|
|
2017
|
+
}
|
|
2018
|
+
const durationMs = endedAt - startedAt;
|
|
2019
|
+
const progressDurationMs = toolFold.progressDurationForExchange(exchangeIndex);
|
|
2020
|
+
const toolMs = turns.reduce((sum, turn) => sum + turn.toolMs, 0);
|
|
2021
|
+
const record = {
|
|
2022
|
+
...totals,
|
|
2023
|
+
kind: "exchange",
|
|
2024
|
+
index: exchangeIndex,
|
|
2025
|
+
promptCount,
|
|
2026
|
+
turnCount: turns.length,
|
|
2027
|
+
turns,
|
|
2028
|
+
startedAt,
|
|
2029
|
+
endedAt,
|
|
2030
|
+
durationMs,
|
|
2031
|
+
...progressDurationMs === void 0 ? {} : { progressDurationMs },
|
|
2032
|
+
waitingMs,
|
|
2033
|
+
toolMs,
|
|
2034
|
+
model,
|
|
2035
|
+
stopReason,
|
|
2036
|
+
blocks: toolFold.recordsForExchange(exchangeIndex)
|
|
2037
|
+
};
|
|
2038
|
+
stopLiveTimer();
|
|
2039
|
+
sessionTotals.input += totals.input;
|
|
2040
|
+
sessionTotals.output += totals.output;
|
|
2041
|
+
sessionTotals.reasoning += totals.reasoning;
|
|
2042
|
+
sessionTotals.cacheRead += totals.cacheRead;
|
|
2043
|
+
sessionTotals.cacheWrite += totals.cacheWrite;
|
|
2044
|
+
sessionTotals.totalTokens += totals.totalTokens;
|
|
2045
|
+
sessionTotals.cost += totals.cost;
|
|
2046
|
+
sessionTotals.exchanges++;
|
|
2047
|
+
sessionTotals.turnCount += turns.length;
|
|
2048
|
+
sessionTotals.durationMs += durationMs;
|
|
2049
|
+
sessionTotals.toolMs += toolMs;
|
|
2050
|
+
sessionTotals.waitingMs += waitingMs;
|
|
2051
|
+
try {
|
|
2052
|
+
pi.appendEntry(ENTRY_TYPE, record);
|
|
2053
|
+
if (!restoredEntries.some((entry) => entry.data === record)) restoredEntries.push({ type: "custom", customType: ENTRY_TYPE, data: record });
|
|
2054
|
+
} catch {
|
|
2055
|
+
}
|
|
2056
|
+
const parts = [`\u23F1 ${fmtDuration(durationMs)}`, plural(turns.length, "turn")];
|
|
2057
|
+
if (toolMs > 0 && durationMs > 0 && toolMs / durationMs >= TOOL_SHARE_NOTE) {
|
|
2058
|
+
parts.push(`tools ${fmtDuration(toolMs)}`);
|
|
2059
|
+
}
|
|
2060
|
+
parts.push(`out ${fmtTokens(totals.output)}`);
|
|
2061
|
+
if (totals.cost > 0) parts.push(fmtCost(totals.cost));
|
|
2062
|
+
if (waitingMs > 0) parts.push(`waiting ${fmtDuration(waitingMs)}`);
|
|
2063
|
+
setStatus(parts.join(" \xB7 "), ctx);
|
|
2064
|
+
toolFold.endExchange();
|
|
2065
|
+
toolFold.releaseExchangeRecords();
|
|
2066
|
+
resetExchangeState();
|
|
2067
|
+
});
|
|
2068
|
+
pi.registerCommand("exstats", {
|
|
2069
|
+
description: "Append a cumulative session timing card",
|
|
2070
|
+
handler: () => {
|
|
2071
|
+
const record = {
|
|
2072
|
+
...sessionTotals,
|
|
2073
|
+
kind: "session",
|
|
2074
|
+
index: sessionTotals.exchanges,
|
|
2075
|
+
promptCount: sessionTotals.exchanges,
|
|
2076
|
+
turnCount: sessionTotals.turnCount,
|
|
2077
|
+
turns: [],
|
|
2078
|
+
startedAt: sessionStartedAt,
|
|
2079
|
+
endedAt: Date.now(),
|
|
2080
|
+
durationMs: sessionTotals.durationMs,
|
|
2081
|
+
waitingMs: sessionTotals.waitingMs,
|
|
2082
|
+
toolMs: sessionTotals.toolMs,
|
|
2083
|
+
model: `${plural(sessionTotals.turnCount, "turn")} tracked`,
|
|
2084
|
+
stopReason: "session"
|
|
2085
|
+
};
|
|
2086
|
+
try {
|
|
2087
|
+
pi.appendEntry(ENTRY_TYPE, record);
|
|
2088
|
+
} catch {
|
|
2089
|
+
}
|
|
2090
|
+
}
|
|
2091
|
+
});
|
|
2092
|
+
}
|
|
2093
|
+
var focus_mode_default = registerExchangeStats;
|
|
2094
|
+
export {
|
|
2095
|
+
focus_mode_default as default,
|
|
2096
|
+
registerExchangeStats
|
|
2097
|
+
};
|