pi-my-compact-ui 0.3.0 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.js +2464 -0
- package/dist/index.js.map +7 -0
- package/dist/src/animation.d.ts +6 -0
- package/dist/src/assistant-patches.d.ts +31 -0
- package/dist/src/compaction.d.ts +8 -0
- package/dist/src/config.d.ts +18 -0
- package/dist/src/constants.d.ts +21 -0
- package/dist/src/extension.d.ts +3 -0
- package/dist/src/grouping.d.ts +16 -0
- package/dist/src/guards.d.ts +12 -0
- package/dist/src/helpers.d.ts +19 -0
- package/dist/src/markdown.d.ts +7 -0
- package/dist/src/modals.d.ts +38 -0
- package/dist/src/settings.d.ts +31 -0
- package/dist/src/state.d.ts +45 -0
- package/dist/src/streaming-tools.d.ts +16 -0
- package/dist/src/thinking.d.ts +5 -0
- package/dist/src/tool-group.d.ts +41 -0
- package/dist/src/types.d.ts +59 -0
- package/package.json +14 -6
- package/src/animation.ts +0 -37
- package/src/assistant-patches.ts +0 -431
- package/src/compaction.ts +0 -75
- package/src/config.ts +0 -31
- package/src/constants.ts +0 -36
- package/src/extension.ts +0 -282
- package/src/grouping.ts +0 -327
- package/src/guards.ts +0 -58
- package/src/helpers.ts +0 -130
- package/src/markdown.ts +0 -98
- package/src/modals.ts +0 -579
- package/src/settings.ts +0 -142
- package/src/state.ts +0 -80
- package/src/streaming-tools.ts +0 -232
- package/src/thinking.ts +0 -62
- package/src/tool-group.ts +0 -449
- package/src/types.ts +0 -52
- /package/{index.ts → dist/index.d.ts} +0 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,2464 @@
|
|
|
1
|
+
// src/guards.ts
|
|
2
|
+
import { Container as Container2, Spacer as Spacer2, Markdown as Markdown2, Text } from "@earendil-works/pi-tui";
|
|
3
|
+
import { AssistantMessageComponent as AssistantMessageComponent2, ToolExecutionComponent } from "@earendil-works/pi-coding-agent";
|
|
4
|
+
|
|
5
|
+
// src/tool-group.ts
|
|
6
|
+
import { Container, Markdown, visibleWidth as visibleWidth2, truncateToWidth as truncateToWidth3 } from "@earendil-works/pi-tui";
|
|
7
|
+
|
|
8
|
+
// src/state.ts
|
|
9
|
+
function createRuntimeState() {
|
|
10
|
+
return {
|
|
11
|
+
currentTheme: null,
|
|
12
|
+
activeThinking: null,
|
|
13
|
+
activeThinkingIndex: null,
|
|
14
|
+
activeThinkingBlocks: /* @__PURE__ */ new Map(),
|
|
15
|
+
unattachedThinking: [],
|
|
16
|
+
thinkingEntrySeq: 0,
|
|
17
|
+
handledTextIndexes: /* @__PURE__ */ new Set(),
|
|
18
|
+
sealedThinkingIndexes: /* @__PURE__ */ new Set(),
|
|
19
|
+
pendingTextSeal: false,
|
|
20
|
+
pendingTextOrdinal: null,
|
|
21
|
+
lastActiveGroup: null,
|
|
22
|
+
lastSealedThinkingEntry: null,
|
|
23
|
+
lastReportedReasoningTokens: 0,
|
|
24
|
+
lastStreamingComp: null,
|
|
25
|
+
lastChatContainer: null,
|
|
26
|
+
toolStarts: /* @__PURE__ */ new Map(),
|
|
27
|
+
toolDurations: /* @__PURE__ */ new Map(),
|
|
28
|
+
turnStartMs: 0,
|
|
29
|
+
preparingByIndex: /* @__PURE__ */ new Map(),
|
|
30
|
+
activeModalHandle: null,
|
|
31
|
+
activeModalComponent: null,
|
|
32
|
+
activeModalCloseFn: null,
|
|
33
|
+
activeUIContext: null,
|
|
34
|
+
activeSelectorHandle: null,
|
|
35
|
+
activeSelectorComponent: null,
|
|
36
|
+
activeSelectorCloseFn: null,
|
|
37
|
+
animTimer: null,
|
|
38
|
+
capturedTui: null,
|
|
39
|
+
groups: /* @__PURE__ */ new Set(),
|
|
40
|
+
assistantContentContainers: /* @__PURE__ */ new WeakSet(),
|
|
41
|
+
assistantContentStates: /* @__PURE__ */ new WeakMap(),
|
|
42
|
+
groupAnchors: /* @__PURE__ */ new WeakMap()
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
var runtime = createRuntimeState();
|
|
46
|
+
|
|
47
|
+
// src/constants.ts
|
|
48
|
+
var SPINNER = ["\u280B", "\u2819", "\u2839", "\u2838", "\u283C", "\u2834", "\u2826", "\u2827", "\u2807", "\u280F"];
|
|
49
|
+
var SPINNER_MS = 100;
|
|
50
|
+
var THINKING_SPINNER = {
|
|
51
|
+
rawFrames: ["\xB7", "\u2022", "\u25CF", "\u2022"],
|
|
52
|
+
intervalMs: 140,
|
|
53
|
+
colorKeys: ["dim", "muted", "accent", "muted"]
|
|
54
|
+
};
|
|
55
|
+
var GROUP_PADDING_X = 1;
|
|
56
|
+
var GROUP_PADDING_RIGHT = 2;
|
|
57
|
+
var spinnerStart = Date.now();
|
|
58
|
+
var PARENT_KEY = /* @__PURE__ */ Symbol.for("compact-ui.group-parent");
|
|
59
|
+
var PATCH_KEY = /* @__PURE__ */ Symbol.for("compact-ui.group-patch");
|
|
60
|
+
var MARKDOWN_RENDER_PATCH_KEY = /* @__PURE__ */ Symbol.for("compact-ui.markdown-render-patch");
|
|
61
|
+
var COMPACTION_STYLE_PATCH_KEY = /* @__PURE__ */ Symbol.for("compact-ui.compaction-style-patch");
|
|
62
|
+
var ASSISTANT_THINKING_PATCH_KEY = /* @__PURE__ */ Symbol.for("compact-ui.assistant-thinking-patch");
|
|
63
|
+
var IS_STREAMING_COMP = /* @__PURE__ */ Symbol.for("compact-ui.is-streaming-comp");
|
|
64
|
+
var GROUP_MOUNT = /* @__PURE__ */ Symbol.for("compact-ui.group-mount");
|
|
65
|
+
var TOOL_RENDER_PATCH_KEY = /* @__PURE__ */ Symbol.for("compact-ui.tool-render-patch");
|
|
66
|
+
var LIVE_ASSISTANT_KEY = /* @__PURE__ */ Symbol.for("compact-ui.live-assistant");
|
|
67
|
+
var CODE_BLOCK_PADDING_X = 1;
|
|
68
|
+
|
|
69
|
+
// src/helpers.ts
|
|
70
|
+
import { homedir } from "os";
|
|
71
|
+
import { isKeyRelease, parseKey, matchesKey } from "@earendil-works/pi-tui";
|
|
72
|
+
function shortenPath(path) {
|
|
73
|
+
const home = homedir();
|
|
74
|
+
return path.startsWith(home) ? `~${path.slice(home.length)}` : path;
|
|
75
|
+
}
|
|
76
|
+
function oneLine(value, max) {
|
|
77
|
+
const text = String(value ?? "").replace(/\s+/g, " ").trim();
|
|
78
|
+
if (typeof max === "number" && text.length > max) {
|
|
79
|
+
return max <= 3 ? "...".slice(0, max) : `${text.slice(0, max - 3)}...`;
|
|
80
|
+
}
|
|
81
|
+
return text;
|
|
82
|
+
}
|
|
83
|
+
function estimateTextTokens(text) {
|
|
84
|
+
return Math.ceil(text.length / 4);
|
|
85
|
+
}
|
|
86
|
+
function formatTokenK(tokens) {
|
|
87
|
+
if (tokens <= 0) return "0.0K";
|
|
88
|
+
if (tokens < 100) return "<0.1K";
|
|
89
|
+
const value = tokens / 1e3;
|
|
90
|
+
return value < 100 ? `${value.toFixed(1)}K` : `${Math.round(value)}K`;
|
|
91
|
+
}
|
|
92
|
+
function thinkingTokenLabel(entry) {
|
|
93
|
+
const tokens = Math.max(0, Math.round(Number(entry?.tokens) || 0));
|
|
94
|
+
if (tokens < 1e3) {
|
|
95
|
+
return `(${tokens}t)`;
|
|
96
|
+
}
|
|
97
|
+
const k = tokens / 1e3;
|
|
98
|
+
const formatted = k < 100 ? k.toFixed(1) : Math.round(k).toString();
|
|
99
|
+
return `(${formatted}k)`;
|
|
100
|
+
}
|
|
101
|
+
function toolSummary(name, args) {
|
|
102
|
+
switch (name) {
|
|
103
|
+
case "bash":
|
|
104
|
+
case "powershell":
|
|
105
|
+
return { name, content: oneLine(args?.command || "...") };
|
|
106
|
+
case "read":
|
|
107
|
+
return { name: "read", content: shortenPath(args?.path || "...") };
|
|
108
|
+
case "write":
|
|
109
|
+
case "edit":
|
|
110
|
+
return { name, content: shortenPath(args?.path || "...") };
|
|
111
|
+
case "find":
|
|
112
|
+
return { name: "find", content: `${oneLine(args?.pattern || "")} in ${shortenPath(args?.path || ".")}` };
|
|
113
|
+
case "grep":
|
|
114
|
+
return { name: "grep", content: `${oneLine(args?.pattern || "")} in ${shortenPath(args?.path || ".")}` };
|
|
115
|
+
case "ls":
|
|
116
|
+
return { name: "ls", content: shortenPath(args?.path || ".") };
|
|
117
|
+
case "web_search":
|
|
118
|
+
return { name: "web_search", content: oneLine(args?.query || "...") };
|
|
119
|
+
case "subagent":
|
|
120
|
+
return { name: "subagent", content: oneLine(args?.agent || args?.task || "...") };
|
|
121
|
+
default: {
|
|
122
|
+
const preferred = args?.path ?? args?.query ?? args?.name ?? args?.description ?? args?.url;
|
|
123
|
+
return { name, content: oneLine(preferred ?? "...") };
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
function toolStatus(tool) {
|
|
128
|
+
if (tool?.result?.isError) return "error";
|
|
129
|
+
if (tool?.isPartial === true || tool?.executionStarted && !tool?.result) return "pending";
|
|
130
|
+
return tool?.result ? "success" : "pending";
|
|
131
|
+
}
|
|
132
|
+
function streamingToolArgs(block) {
|
|
133
|
+
const args = block?.arguments && typeof block.arguments === "object" ? block.arguments : {};
|
|
134
|
+
if (Object.keys(args).length > 0) return args;
|
|
135
|
+
const partial = String(block?.partialJson || block?.partialArgs || "").trim();
|
|
136
|
+
if (!partial) return args;
|
|
137
|
+
const name = String(block?.name || "");
|
|
138
|
+
if (name === "bash" || name === "powershell") return { command: partial };
|
|
139
|
+
if (name === "read" || name === "write" || name === "edit" || name === "ls") return { path: partial };
|
|
140
|
+
return { query: partial };
|
|
141
|
+
}
|
|
142
|
+
function locateStreamingToolCall(content, streamEvent) {
|
|
143
|
+
const index = Number(streamEvent?.contentIndex);
|
|
144
|
+
if (Number.isInteger(index) && content[index]?.type === "toolCall") {
|
|
145
|
+
return { index, block: content[index] };
|
|
146
|
+
}
|
|
147
|
+
const fromEvent = streamEvent?.toolCall;
|
|
148
|
+
if (fromEvent && (fromEvent.type === "toolCall" || fromEvent.name)) {
|
|
149
|
+
const found = content.findIndex(
|
|
150
|
+
(item) => item?.type === "toolCall" && fromEvent.id && item.id === fromEvent.id
|
|
151
|
+
);
|
|
152
|
+
if (found >= 0) return { index: found, block: content[found] };
|
|
153
|
+
return { index: Number.isInteger(index) ? index : content.length, block: fromEvent };
|
|
154
|
+
}
|
|
155
|
+
return void 0;
|
|
156
|
+
}
|
|
157
|
+
function toolResultText(tool) {
|
|
158
|
+
return (tool?.result?.content ?? []).filter((c) => c.type === "text").map((c) => String(c.text)).join("\n").trim();
|
|
159
|
+
}
|
|
160
|
+
function formatWorkedTime(elapsedMs) {
|
|
161
|
+
const totalSec = Math.max(1, Math.round(elapsedMs / 1e3));
|
|
162
|
+
const hours = Math.floor(totalSec / 3600);
|
|
163
|
+
const minutes = Math.floor(totalSec % 3600 / 60);
|
|
164
|
+
const seconds = totalSec % 60;
|
|
165
|
+
if (hours > 0) return minutes > 0 ? `${hours}h ${minutes}m` : `${hours}h`;
|
|
166
|
+
if (minutes > 0) return seconds > 0 ? `${minutes}m ${seconds}s` : `${minutes}m`;
|
|
167
|
+
return `${seconds}s`;
|
|
168
|
+
}
|
|
169
|
+
function isCtrlI(data) {
|
|
170
|
+
if (isKeyRelease(data)) {
|
|
171
|
+
return false;
|
|
172
|
+
}
|
|
173
|
+
return data !== " " && (parseKey(data) === "ctrl+i" || matchesKey(data, "ctrl+i"));
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
// src/assistant-patches.ts
|
|
177
|
+
import { Spacer, truncateToWidth as truncateToWidth2 } from "@earendil-works/pi-tui";
|
|
178
|
+
import { AssistantMessageComponent } from "@earendil-works/pi-coding-agent";
|
|
179
|
+
|
|
180
|
+
// src/markdown.ts
|
|
181
|
+
import { truncateToWidth, visibleWidth, stripTerminalSequences, wrapTextWithAnsi } from "@earendil-works/pi-tui";
|
|
182
|
+
import { getMarkdownTheme } from "@earendil-works/pi-coding-agent";
|
|
183
|
+
function isCompactCodeBlockOpen(visible) {
|
|
184
|
+
return visible === "\u250C\u2500" || visible.startsWith("\u250C\u2500 ");
|
|
185
|
+
}
|
|
186
|
+
function isCompactCodeBlockClose(visible) {
|
|
187
|
+
return visible === "\u2514\u2500";
|
|
188
|
+
}
|
|
189
|
+
function getCompactMarkdownTheme() {
|
|
190
|
+
const base = getMarkdownTheme();
|
|
191
|
+
let insideCodeBlock = false;
|
|
192
|
+
const theme = {
|
|
193
|
+
...base,
|
|
194
|
+
codeBlockIndent: "",
|
|
195
|
+
codeBlockBorder(text) {
|
|
196
|
+
const opening = !insideCodeBlock;
|
|
197
|
+
insideCodeBlock = !insideCodeBlock;
|
|
198
|
+
const language = opening ? text.replace(/^```/, "").trim() : "";
|
|
199
|
+
if (opening) {
|
|
200
|
+
theme.codeBlockIndent = "";
|
|
201
|
+
}
|
|
202
|
+
const border = opening ? `\u250C\u2500${language ? ` ${language}` : ""}` : "\u2514\u2500";
|
|
203
|
+
return base.codeBlockBorder(border);
|
|
204
|
+
}
|
|
205
|
+
};
|
|
206
|
+
return theme;
|
|
207
|
+
}
|
|
208
|
+
function renderCodeBlockBackgroundRow(content, width) {
|
|
209
|
+
const safeWidth = Math.max(1, width);
|
|
210
|
+
const horizontalPadding = Math.min(CODE_BLOCK_PADDING_X, Math.floor((safeWidth - 1) / 2));
|
|
211
|
+
const innerWidth = Math.max(1, safeWidth - horizontalPadding * 2);
|
|
212
|
+
const clipped = truncateToWidth(content, innerWidth, "...");
|
|
213
|
+
const rightFill = " ".repeat(Math.max(0, innerWidth - visibleWidth(clipped)));
|
|
214
|
+
const row = `${" ".repeat(horizontalPadding)}${clipped}${rightFill}${" ".repeat(horizontalPadding)}`;
|
|
215
|
+
return runtime.currentTheme?.bg?.("toolPendingBg", row) ?? row;
|
|
216
|
+
}
|
|
217
|
+
function normalizeCompactCodeBlockLines(lines, width, paddingX = 0) {
|
|
218
|
+
const safeWidth = Math.max(1, width);
|
|
219
|
+
const horizontalPadding = Math.max(0, Math.floor(paddingX));
|
|
220
|
+
const leftPadding = " ".repeat(horizontalPadding);
|
|
221
|
+
const contentWidth = Math.max(1, safeWidth - horizontalPadding * 2);
|
|
222
|
+
const continuationWidth = contentWidth;
|
|
223
|
+
const normalized = [];
|
|
224
|
+
let codeBlockMode = "none";
|
|
225
|
+
for (const originalLine of lines) {
|
|
226
|
+
const withoutLeftPadding = leftPadding.length > 0 && originalLine.startsWith(leftPadding) ? originalLine.slice(leftPadding.length) : originalLine;
|
|
227
|
+
const content = withoutLeftPadding.trimEnd();
|
|
228
|
+
const visible = stripTerminalSequences(content).trimStart();
|
|
229
|
+
if (isCompactCodeBlockOpen(visible)) {
|
|
230
|
+
codeBlockMode = "background";
|
|
231
|
+
const language = visible.replace(/^┌─/, "").trim();
|
|
232
|
+
if (language) {
|
|
233
|
+
const label = runtime.currentTheme?.fg?.("muted", language) ?? language;
|
|
234
|
+
normalized.push(`${leftPadding}${renderCodeBlockBackgroundRow(label, contentWidth)}`);
|
|
235
|
+
}
|
|
236
|
+
continue;
|
|
237
|
+
}
|
|
238
|
+
if (isCompactCodeBlockClose(visible)) {
|
|
239
|
+
codeBlockMode = "none";
|
|
240
|
+
continue;
|
|
241
|
+
}
|
|
242
|
+
if (codeBlockMode === "background") {
|
|
243
|
+
const codeWidth = Math.max(1, continuationWidth - CODE_BLOCK_PADDING_X * 2);
|
|
244
|
+
const wrappedRows = wrapTextWithAnsi(content, codeWidth);
|
|
245
|
+
for (const wrapped of wrappedRows.length > 0 ? wrappedRows : [""]) {
|
|
246
|
+
normalized.push(`${leftPadding}${renderCodeBlockBackgroundRow(wrapped, contentWidth)}`);
|
|
247
|
+
}
|
|
248
|
+
continue;
|
|
249
|
+
}
|
|
250
|
+
normalized.push(truncateToWidth(originalLine, safeWidth, "..."));
|
|
251
|
+
}
|
|
252
|
+
return normalized;
|
|
253
|
+
}
|
|
254
|
+
function installVisibleAssistantMarkdownRendering(component) {
|
|
255
|
+
const markdown = component;
|
|
256
|
+
if (markdown[MARKDOWN_RENDER_PATCH_KEY]) return;
|
|
257
|
+
markdown.theme = getCompactMarkdownTheme();
|
|
258
|
+
const originalRender = markdown.render.bind(markdown);
|
|
259
|
+
markdown.render = (width) => normalizeCompactCodeBlockLines(originalRender(width), width, Number(markdown.paddingX) || 0);
|
|
260
|
+
markdown[MARKDOWN_RENDER_PATCH_KEY] = { originalRender };
|
|
261
|
+
markdown.invalidate();
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
// src/assistant-patches.ts
|
|
265
|
+
function thinkingAlreadyShown(text) {
|
|
266
|
+
const norm = text.trim();
|
|
267
|
+
if (!norm) return true;
|
|
268
|
+
if (runtime.activeThinking && runtime.activeThinking.text.trim() === norm) return true;
|
|
269
|
+
for (const entry of runtime.unattachedThinking) {
|
|
270
|
+
if (entry.text.trim() === norm) return true;
|
|
271
|
+
}
|
|
272
|
+
for (const group of runtime.groups) {
|
|
273
|
+
for (const entry of group.entries) {
|
|
274
|
+
if (entry.kind === "thinking" && entry.text.trim() === norm) return true;
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
return false;
|
|
278
|
+
}
|
|
279
|
+
function stageHistoricalThinking(state, ordinal, texts, message) {
|
|
280
|
+
if (state.anchors.has(ordinal)) return;
|
|
281
|
+
const fresh = texts.filter((text, index) => text.trim() && texts.indexOf(text) === index && !thinkingAlreadyShown(text));
|
|
282
|
+
if (fresh.length === 0) return;
|
|
283
|
+
const group = new ToolGroupComponent();
|
|
284
|
+
group.anchored = true;
|
|
285
|
+
group.sealed = true;
|
|
286
|
+
const reasoningTokens = Number(message?.usage?.reasoning);
|
|
287
|
+
const tokensExact = Number.isFinite(reasoningTokens) && reasoningTokens > 0;
|
|
288
|
+
for (const text of fresh) {
|
|
289
|
+
const entry = {
|
|
290
|
+
kind: "thinking",
|
|
291
|
+
id: runtime.thinkingEntrySeq++,
|
|
292
|
+
text,
|
|
293
|
+
tokens: tokensExact ? reasoningTokens : estimateTextTokens(text),
|
|
294
|
+
tokensExact,
|
|
295
|
+
active: false,
|
|
296
|
+
owner: group
|
|
297
|
+
};
|
|
298
|
+
group.entries.push(entry);
|
|
299
|
+
}
|
|
300
|
+
if (!group.hasVisibleEntries()) return;
|
|
301
|
+
state.anchors.set(ordinal, group);
|
|
302
|
+
runtime.groups.add(group);
|
|
303
|
+
}
|
|
304
|
+
function ensureHistoricalThinkingAnchors(contentContainer, message) {
|
|
305
|
+
const content = Array.isArray(message?.content) ? message.content : void 0;
|
|
306
|
+
if (!content || content.length === 0) return;
|
|
307
|
+
const state = getAssistantContentState(contentContainer);
|
|
308
|
+
let currentThinkingBlocks = [];
|
|
309
|
+
let textOrdinal = 0;
|
|
310
|
+
for (const item of content) {
|
|
311
|
+
if (item?.type === "thinking") {
|
|
312
|
+
const text = String(item.thinking ?? "").trim();
|
|
313
|
+
if (text) currentThinkingBlocks.push(text);
|
|
314
|
+
} else if (item?.type === "text" && String(item.text ?? "").trim()) {
|
|
315
|
+
stageHistoricalThinking(state, textOrdinal, currentThinkingBlocks, message);
|
|
316
|
+
currentThinkingBlocks = [];
|
|
317
|
+
textOrdinal++;
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
const hasToolCalls = content.some((item) => item?.type === "toolCall");
|
|
321
|
+
if (!hasToolCalls && textOrdinal === 0 && currentThinkingBlocks.length > 0) {
|
|
322
|
+
stageHistoricalThinking(state, 0, currentThinkingBlocks, message);
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
function installNativeThinkingSuppression(prototype = AssistantMessageComponent.prototype) {
|
|
326
|
+
if (!prototype || typeof prototype.updateContent !== "function") return;
|
|
327
|
+
const previous = prototype[ASSISTANT_THINKING_PATCH_KEY];
|
|
328
|
+
const originalUpdateContent = previous && prototype.updateContent === previous.installedUpdateContent ? previous.originalUpdateContent : prototype.updateContent;
|
|
329
|
+
const installedUpdateContent = function(message, isStreaming) {
|
|
330
|
+
const contentContainer = this.contentContainer;
|
|
331
|
+
if (contentContainer) {
|
|
332
|
+
runtime.assistantContentContainers.add(contentContainer);
|
|
333
|
+
}
|
|
334
|
+
if (isStreaming === true) {
|
|
335
|
+
this[LIVE_ASSISTANT_KEY] = true;
|
|
336
|
+
}
|
|
337
|
+
const isLive = Boolean(this[LIVE_ASSISTANT_KEY]) || isStreaming === true || this[IS_STREAMING_COMP] === true || this === runtime.lastStreamingComp;
|
|
338
|
+
const content = Array.isArray(message?.content) ? message.content : void 0;
|
|
339
|
+
const hasToolCalls = Boolean(content?.some((item) => item?.type === "toolCall"));
|
|
340
|
+
const hasText = Boolean(content?.some((item) => item?.type === "text" && String(item?.text ?? "").trim()));
|
|
341
|
+
if (!isLive && contentContainer && message?.content) {
|
|
342
|
+
ensureHistoricalThinkingAnchors(contentContainer, message);
|
|
343
|
+
}
|
|
344
|
+
if (!content?.some((item) => item?.type === "thinking")) {
|
|
345
|
+
originalUpdateContent.call(this, message, isStreaming);
|
|
346
|
+
return;
|
|
347
|
+
}
|
|
348
|
+
originalUpdateContent.call(
|
|
349
|
+
this,
|
|
350
|
+
{
|
|
351
|
+
...message,
|
|
352
|
+
content: content.filter((item) => item?.type !== "thinking")
|
|
353
|
+
},
|
|
354
|
+
isStreaming
|
|
355
|
+
);
|
|
356
|
+
if (!isLive && !hasToolCalls && !hasText && contentContainer) {
|
|
357
|
+
const state = getAssistantContentState(contentContainer);
|
|
358
|
+
const group = state.anchors.get(0);
|
|
359
|
+
if (group && group.hasVisibleEntries() && !contentContainer.children.includes(group)) {
|
|
360
|
+
detachGroup(group, contentContainer);
|
|
361
|
+
contentContainer.addChild(new Spacer(1));
|
|
362
|
+
contentContainer.addChild(group);
|
|
363
|
+
contentContainer.addChild(new Spacer(1));
|
|
364
|
+
noteGroupMount(contentContainer, group);
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
};
|
|
368
|
+
prototype.updateContent = installedUpdateContent;
|
|
369
|
+
if (typeof prototype.setHideThinkingBlock === "function") {
|
|
370
|
+
const originalSetHideThinkingBlock = prototype.setHideThinkingBlock;
|
|
371
|
+
prototype.setHideThinkingBlock = function(_hide) {
|
|
372
|
+
this.hiddenThinkingLabel = "";
|
|
373
|
+
originalSetHideThinkingBlock.call(this, true);
|
|
374
|
+
};
|
|
375
|
+
}
|
|
376
|
+
if (typeof prototype.setHiddenThinkingLabel === "function") {
|
|
377
|
+
const originalSetHiddenThinkingLabel = prototype.setHiddenThinkingLabel;
|
|
378
|
+
prototype.setHiddenThinkingLabel = function(_label) {
|
|
379
|
+
this.hiddenThinkingLabel = "";
|
|
380
|
+
originalSetHiddenThinkingLabel.call(this, "");
|
|
381
|
+
};
|
|
382
|
+
}
|
|
383
|
+
prototype[ASSISTANT_THINKING_PATCH_KEY] = {
|
|
384
|
+
originalUpdateContent,
|
|
385
|
+
installedUpdateContent
|
|
386
|
+
};
|
|
387
|
+
}
|
|
388
|
+
function ensureAssistantThinkingPatched(component) {
|
|
389
|
+
const prototype = Object.getPrototypeOf(component);
|
|
390
|
+
const already = Boolean(prototype?.[ASSISTANT_THINKING_PATCH_KEY]);
|
|
391
|
+
installNativeThinkingSuppression(prototype);
|
|
392
|
+
component.hideThinkingBlock = true;
|
|
393
|
+
component.hiddenThinkingLabel = "";
|
|
394
|
+
return already;
|
|
395
|
+
}
|
|
396
|
+
function getAssistantContentState(container) {
|
|
397
|
+
let state = runtime.assistantContentStates.get(container);
|
|
398
|
+
if (!state) {
|
|
399
|
+
state = { anchors: /* @__PURE__ */ new Map(), nextTextOrdinal: 0 };
|
|
400
|
+
runtime.assistantContentStates.set(container, state);
|
|
401
|
+
}
|
|
402
|
+
return state;
|
|
403
|
+
}
|
|
404
|
+
function removeGroupFromContainer(container, group) {
|
|
405
|
+
const children = container?.children;
|
|
406
|
+
if (!Array.isArray(children)) return;
|
|
407
|
+
const index = children.indexOf(group);
|
|
408
|
+
if (index >= 0) children.splice(index, 1);
|
|
409
|
+
if (group[GROUP_MOUNT] === container) delete group[GROUP_MOUNT];
|
|
410
|
+
}
|
|
411
|
+
function noteGroupMount(container, group) {
|
|
412
|
+
group[GROUP_MOUNT] = container;
|
|
413
|
+
}
|
|
414
|
+
function detachGroup(group, keep) {
|
|
415
|
+
const mounted = group[GROUP_MOUNT];
|
|
416
|
+
if (mounted && mounted !== keep) removeGroupFromContainer(mounted, group);
|
|
417
|
+
if (runtime.lastChatContainer && runtime.lastChatContainer !== keep) removeGroupFromContainer(runtime.lastChatContainer, group);
|
|
418
|
+
const anchor = runtime.groupAnchors.get(group);
|
|
419
|
+
if (anchor?.container && anchor.container !== keep) removeGroupFromContainer(anchor.container, group);
|
|
420
|
+
}
|
|
421
|
+
function removeComponentFromContainer(container, component) {
|
|
422
|
+
const children = container?.children;
|
|
423
|
+
if (!Array.isArray(children)) return;
|
|
424
|
+
const index = children.indexOf(component);
|
|
425
|
+
if (index >= 0) children.splice(index, 1);
|
|
426
|
+
}
|
|
427
|
+
var TurnDividerComponent = class {
|
|
428
|
+
timeLabel;
|
|
429
|
+
constructor(timeLabel) {
|
|
430
|
+
this.timeLabel = timeLabel;
|
|
431
|
+
}
|
|
432
|
+
render(width) {
|
|
433
|
+
const theme = runtime.currentTheme;
|
|
434
|
+
const fg = (color, text) => theme?.fg?.(color, text) ?? text;
|
|
435
|
+
const middle = `worked for ${this.timeLabel}`;
|
|
436
|
+
const avail = Math.max(6, width - middle.length - 2);
|
|
437
|
+
const left = Math.floor(avail / 2);
|
|
438
|
+
const right = avail - left;
|
|
439
|
+
const dash = (n) => "\u2500".repeat(Math.max(0, n));
|
|
440
|
+
const line = `${fg("dim", dash(left))} ${fg("muted", middle)} ${fg("dim", dash(right))}`;
|
|
441
|
+
return [truncateToWidth2(line, Math.max(1, width)), ""];
|
|
442
|
+
}
|
|
443
|
+
invalidate() {
|
|
444
|
+
}
|
|
445
|
+
};
|
|
446
|
+
function placeTurnDividerBeforeText(container, target, divider) {
|
|
447
|
+
const targetIndex = container.children.indexOf(target);
|
|
448
|
+
if (targetIndex < 0) return;
|
|
449
|
+
removeComponentFromContainer(container, divider);
|
|
450
|
+
container.children.splice(targetIndex, 0, divider);
|
|
451
|
+
}
|
|
452
|
+
function insertTurnDivider(elapsedMs) {
|
|
453
|
+
if (elapsedMs < 1e3) return;
|
|
454
|
+
let comp = runtime.lastStreamingComp;
|
|
455
|
+
if (!comp || !isAssistantMessage(comp)) {
|
|
456
|
+
if (!runtime.lastChatContainer) return;
|
|
457
|
+
const children = runtime.lastChatContainer?.children;
|
|
458
|
+
if (!Array.isArray(children)) return;
|
|
459
|
+
for (let i = children.length - 1; i >= 0; i--) {
|
|
460
|
+
if (isAssistantMessage(children[i])) {
|
|
461
|
+
comp = children[i];
|
|
462
|
+
break;
|
|
463
|
+
}
|
|
464
|
+
}
|
|
465
|
+
}
|
|
466
|
+
if (!comp) return;
|
|
467
|
+
const contentContainer = comp.contentContainer;
|
|
468
|
+
if (!isContainer(contentContainer)) return;
|
|
469
|
+
const markdowns = contentContainer.children.filter(isVisibleTextMarkdown);
|
|
470
|
+
if (markdowns.length === 0) return;
|
|
471
|
+
const final = markdowns[markdowns.length - 1];
|
|
472
|
+
const finalIndex = contentContainer.children.indexOf(final);
|
|
473
|
+
const hasPriorContent = contentContainer.children.slice(0, finalIndex).some((child) => isToolGroup(child) && child.hasVisibleEntries());
|
|
474
|
+
if (!hasPriorContent) return;
|
|
475
|
+
const state = getAssistantContentState(contentContainer);
|
|
476
|
+
if (state.finalDivider && contentContainer.children.includes(state.finalDivider.component)) return;
|
|
477
|
+
const ordinal = markdowns.length - 1;
|
|
478
|
+
const divider = new TurnDividerComponent(formatWorkedTime(elapsedMs));
|
|
479
|
+
state.finalDivider = { ordinal, component: divider };
|
|
480
|
+
placeTurnDividerBeforeText(contentContainer, final, divider);
|
|
481
|
+
contentContainer.invalidate?.();
|
|
482
|
+
runtime.capturedTui?.requestRender?.();
|
|
483
|
+
}
|
|
484
|
+
function isVisibleTextMarkdown(component) {
|
|
485
|
+
return isMarkdown(component) && !component.defaultTextStyle;
|
|
486
|
+
}
|
|
487
|
+
function placeAnchoredGroupBeforeText(container, target, group) {
|
|
488
|
+
detachGroup(group, container);
|
|
489
|
+
const targetIndex = container.children.indexOf(target);
|
|
490
|
+
if (targetIndex < 0) return;
|
|
491
|
+
let spacerStart = targetIndex;
|
|
492
|
+
while (spacerStart > 0 && isSpacer(container.children[spacerStart - 1])) spacerStart--;
|
|
493
|
+
if (targetIndex > spacerStart) {
|
|
494
|
+
container.children.splice(spacerStart, targetIndex - spacerStart);
|
|
495
|
+
}
|
|
496
|
+
group.anchored = true;
|
|
497
|
+
container.children.splice(spacerStart, 0, new Spacer(1), group, new Spacer(1));
|
|
498
|
+
noteGroupMount(container, group);
|
|
499
|
+
}
|
|
500
|
+
function insertAnchoredGroup(container, ordinal, group) {
|
|
501
|
+
if (!group.hasVisibleEntries()) return;
|
|
502
|
+
const markdowns = container.children.filter(isVisibleTextMarkdown);
|
|
503
|
+
const target = markdowns[ordinal];
|
|
504
|
+
if (!target) return;
|
|
505
|
+
removeGroupFromContainer(container, group);
|
|
506
|
+
placeAnchoredGroupBeforeText(container, target, group);
|
|
507
|
+
}
|
|
508
|
+
function installAssistantExpansion(component, contentContainer) {
|
|
509
|
+
component.setExpanded = (expanded) => {
|
|
510
|
+
const state = runtime.assistantContentStates.get(contentContainer);
|
|
511
|
+
if (!state) return;
|
|
512
|
+
for (const group of state.anchors.values()) group.setExpanded(expanded);
|
|
513
|
+
contentContainer.invalidate();
|
|
514
|
+
};
|
|
515
|
+
}
|
|
516
|
+
function anchorGroupBeforeCurrentText(group, ordinal) {
|
|
517
|
+
if (!runtime.lastStreamingComp || !runtime.lastChatContainer) return;
|
|
518
|
+
const contentContainer = runtime.lastStreamingComp.contentContainer;
|
|
519
|
+
if (!isContainer(contentContainer)) return;
|
|
520
|
+
removeGroupFromContainer(runtime.lastChatContainer, group);
|
|
521
|
+
const previousAnchor = runtime.groupAnchors.get(group);
|
|
522
|
+
if (previousAnchor) {
|
|
523
|
+
const previousState = runtime.assistantContentStates.get(previousAnchor.container);
|
|
524
|
+
if (previousState?.anchors.get(previousAnchor.ordinal) === group) {
|
|
525
|
+
previousState.anchors.delete(previousAnchor.ordinal);
|
|
526
|
+
}
|
|
527
|
+
removeGroupFromContainer(previousAnchor.container, group);
|
|
528
|
+
}
|
|
529
|
+
if (!group.hasVisibleEntries()) {
|
|
530
|
+
runtime.groups.delete(group);
|
|
531
|
+
return;
|
|
532
|
+
}
|
|
533
|
+
const state = getAssistantContentState(contentContainer);
|
|
534
|
+
const replaced = state.anchors.get(ordinal);
|
|
535
|
+
if (replaced && replaced !== group) {
|
|
536
|
+
removeGroupFromContainer(contentContainer, replaced);
|
|
537
|
+
replaced.anchored = false;
|
|
538
|
+
runtime.groupAnchors.delete(replaced);
|
|
539
|
+
}
|
|
540
|
+
state.anchors.set(ordinal, group);
|
|
541
|
+
runtime.groupAnchors.set(group, { container: contentContainer, ordinal });
|
|
542
|
+
insertAnchoredGroup(contentContainer, ordinal, group);
|
|
543
|
+
runtime.lastChatContainer.invalidate?.();
|
|
544
|
+
runtime.capturedTui?.requestRender?.();
|
|
545
|
+
}
|
|
546
|
+
function restoreAssistantAnchor(parent, component) {
|
|
547
|
+
if (!runtime.assistantContentContainers.has(parent) || !isVisibleTextMarkdown(component)) return;
|
|
548
|
+
installVisibleAssistantMarkdownRendering(component);
|
|
549
|
+
const state = getAssistantContentState(parent);
|
|
550
|
+
const ordinal = state.nextTextOrdinal++;
|
|
551
|
+
const group = state.anchors.get(ordinal);
|
|
552
|
+
if (group && group.hasVisibleEntries()) {
|
|
553
|
+
removeGroupFromContainer(parent, group);
|
|
554
|
+
placeAnchoredGroupBeforeText(parent, component, group);
|
|
555
|
+
}
|
|
556
|
+
const divider = state.finalDivider;
|
|
557
|
+
if (divider && divider.ordinal === ordinal) {
|
|
558
|
+
placeTurnDividerBeforeText(parent, component, divider.component);
|
|
559
|
+
}
|
|
560
|
+
}
|
|
561
|
+
function releaseAssistantAnchors(component) {
|
|
562
|
+
if (!isAssistantMessage(component)) return;
|
|
563
|
+
const contentContainer = component.contentContainer;
|
|
564
|
+
if (!isContainer(contentContainer)) return;
|
|
565
|
+
const state = runtime.assistantContentStates.get(contentContainer);
|
|
566
|
+
if (!state) return;
|
|
567
|
+
for (const group of state.anchors.values()) {
|
|
568
|
+
for (const tool of [...group.children]) delete tool[PARENT_KEY];
|
|
569
|
+
runtime.groupAnchors.delete(group);
|
|
570
|
+
runtime.groups.delete(group);
|
|
571
|
+
}
|
|
572
|
+
state.anchors.clear();
|
|
573
|
+
state.finalDivider = void 0;
|
|
574
|
+
}
|
|
575
|
+
function stripAssistantPhantomPadding(parent, component) {
|
|
576
|
+
if (isAssistantMessage(parent) && isContainer(component) && !isAssistantMessage(component)) {
|
|
577
|
+
runtime.assistantContentContainers.add(component);
|
|
578
|
+
getAssistantContentState(component);
|
|
579
|
+
installAssistantExpansion(parent, component);
|
|
580
|
+
return;
|
|
581
|
+
}
|
|
582
|
+
if (!runtime.assistantContentContainers.has(parent)) return;
|
|
583
|
+
if (!isNativeThinkingWidget(component)) return;
|
|
584
|
+
const index = parent.children.indexOf(component);
|
|
585
|
+
if (index >= 0) parent.children.splice(index, 1);
|
|
586
|
+
if (isSpacer(parent.children[index])) parent.children.splice(index, 1);
|
|
587
|
+
while (parent.children.length > 0 && parent.children.every((child) => isSpacer(child))) parent.children.pop();
|
|
588
|
+
}
|
|
589
|
+
function isNativeThinkingWidget(component) {
|
|
590
|
+
if (!component || isToolGroup(component)) return false;
|
|
591
|
+
if (component.constructor?.name === "MouseRegion" && component.child) {
|
|
592
|
+
return isNativeThinkingWidget(component.child);
|
|
593
|
+
}
|
|
594
|
+
if (isMarkdown(component) && component.defaultTextStyle) return true;
|
|
595
|
+
if (!isText(component)) return false;
|
|
596
|
+
const visible = String(component.text ?? "").replace(/\x1b\[[0-9;]*m/g, "").trim();
|
|
597
|
+
return visible === "" || visible === "Thinking..." || visible === "thinking" || visible === "thinking...";
|
|
598
|
+
}
|
|
599
|
+
|
|
600
|
+
// src/streaming-tools.ts
|
|
601
|
+
function toolElapsed(tool) {
|
|
602
|
+
if (typeof tool?._elapsedDuration === "number") {
|
|
603
|
+
return tool._elapsedDuration.toFixed(1);
|
|
604
|
+
}
|
|
605
|
+
const knownDuration = runtime.toolDurations.get(tool.toolCallId);
|
|
606
|
+
if (knownDuration !== void 0) {
|
|
607
|
+
tool._elapsedDuration = knownDuration;
|
|
608
|
+
return knownDuration.toFixed(1);
|
|
609
|
+
}
|
|
610
|
+
if (tool?._groupEndAt !== void 0) {
|
|
611
|
+
const start2 = runtime.toolStarts.get(tool.toolCallId) ?? tool._groupEndAt;
|
|
612
|
+
const dur = Math.max(0, (tool._groupEndAt - start2) / 1e3);
|
|
613
|
+
tool._elapsedDuration = dur;
|
|
614
|
+
return dur.toFixed(1);
|
|
615
|
+
}
|
|
616
|
+
if (tool?.result) {
|
|
617
|
+
const resultTs = Number(tool.result?.timestamp);
|
|
618
|
+
const start2 = runtime.toolStarts.get(tool.toolCallId);
|
|
619
|
+
if (start2 && resultTs && resultTs >= start2) {
|
|
620
|
+
const dur = (resultTs - start2) / 1e3;
|
|
621
|
+
tool._elapsedDuration = dur;
|
|
622
|
+
return dur.toFixed(1);
|
|
623
|
+
}
|
|
624
|
+
tool._elapsedDuration = 0;
|
|
625
|
+
return "0.0";
|
|
626
|
+
}
|
|
627
|
+
const start = runtime.toolStarts.get(tool.toolCallId);
|
|
628
|
+
if (start !== void 0) {
|
|
629
|
+
return Math.max(0, (Date.now() - start) / 1e3).toFixed(1);
|
|
630
|
+
}
|
|
631
|
+
return "0.0";
|
|
632
|
+
}
|
|
633
|
+
function rememberToolStart(toolCallId) {
|
|
634
|
+
if (!toolCallId || runtime.toolStarts.has(toolCallId) || runtime.toolDurations.has(toolCallId)) return;
|
|
635
|
+
runtime.toolStarts.set(toolCallId, Date.now());
|
|
636
|
+
}
|
|
637
|
+
function loadSessionToolTimings(entries) {
|
|
638
|
+
if (!Array.isArray(entries)) return;
|
|
639
|
+
for (const entry of entries) {
|
|
640
|
+
if (entry?.type === "message" && entry.message) {
|
|
641
|
+
const msg = entry.message;
|
|
642
|
+
if (msg.role === "assistant" && Array.isArray(msg.content)) {
|
|
643
|
+
const startTime = Number(msg.timestamp) || (entry.timestamp ? new Date(entry.timestamp).getTime() : 0);
|
|
644
|
+
for (const block of msg.content) {
|
|
645
|
+
if (block.type === "toolCall" && block.id) {
|
|
646
|
+
if (startTime > 0) runtime.toolStarts.set(block.id, startTime);
|
|
647
|
+
}
|
|
648
|
+
}
|
|
649
|
+
} else if (msg.role === "toolResult" && msg.toolCallId) {
|
|
650
|
+
const endTime = Number(msg.timestamp) || (entry.timestamp ? new Date(entry.timestamp).getTime() : 0);
|
|
651
|
+
const startTime = runtime.toolStarts.get(msg.toolCallId);
|
|
652
|
+
if (startTime && endTime >= startTime) {
|
|
653
|
+
runtime.toolDurations.set(msg.toolCallId, (endTime - startTime) / 1e3);
|
|
654
|
+
}
|
|
655
|
+
}
|
|
656
|
+
}
|
|
657
|
+
}
|
|
658
|
+
}
|
|
659
|
+
function findGroupedTool(toolCallId, contentIndex) {
|
|
660
|
+
for (const group of runtime.groups) {
|
|
661
|
+
for (const entry of group.entries) {
|
|
662
|
+
if (entry.kind !== "tool") continue;
|
|
663
|
+
const tool = entry.tool;
|
|
664
|
+
if (toolCallId && tool?.toolCallId === toolCallId) return tool;
|
|
665
|
+
if (tool?._contentIndex === contentIndex) return tool;
|
|
666
|
+
}
|
|
667
|
+
}
|
|
668
|
+
return void 0;
|
|
669
|
+
}
|
|
670
|
+
function invalidateGroupedTool(tool) {
|
|
671
|
+
const group = tool?.owner ?? tool?.[PARENT_KEY];
|
|
672
|
+
if (isToolGroup(group)) {
|
|
673
|
+
if (!group.sealed) runtime.lastActiveGroup = group;
|
|
674
|
+
group.invalidate();
|
|
675
|
+
} else {
|
|
676
|
+
runtime.lastActiveGroup?.invalidate();
|
|
677
|
+
}
|
|
678
|
+
runtime.capturedTui?.requestRender?.();
|
|
679
|
+
}
|
|
680
|
+
function findOpenGroup() {
|
|
681
|
+
if (runtime.lastActiveGroup && !runtime.lastActiveGroup.sealed && runtime.groups.has(runtime.lastActiveGroup)) return runtime.lastActiveGroup;
|
|
682
|
+
for (const group of runtime.groups) {
|
|
683
|
+
if (!group.sealed) return group;
|
|
684
|
+
}
|
|
685
|
+
return null;
|
|
686
|
+
}
|
|
687
|
+
function placeGroupAfter(parent, after, group) {
|
|
688
|
+
const children = parent?.children;
|
|
689
|
+
if (!Array.isArray(children)) return;
|
|
690
|
+
detachGroup(group, parent);
|
|
691
|
+
const existing = children.indexOf(group);
|
|
692
|
+
if (existing >= 0) children.splice(existing, 1);
|
|
693
|
+
const afterIndex = after ? children.indexOf(after) : -1;
|
|
694
|
+
children.splice(afterIndex >= 0 ? afterIndex + 1 : children.length, 0, group);
|
|
695
|
+
noteGroupMount(parent, group);
|
|
696
|
+
runtime.lastActiveGroup = group;
|
|
697
|
+
}
|
|
698
|
+
function openToolGroup() {
|
|
699
|
+
const open = findOpenGroup();
|
|
700
|
+
if (open) {
|
|
701
|
+
if (runtime.lastChatContainer && runtime.lastStreamingComp) placeGroupAfter(runtime.lastChatContainer, runtime.lastStreamingComp, open);
|
|
702
|
+
return open;
|
|
703
|
+
}
|
|
704
|
+
if (!runtime.lastChatContainer || !runtime.lastStreamingComp) return null;
|
|
705
|
+
let group;
|
|
706
|
+
const parent = runtime.lastChatContainer;
|
|
707
|
+
const children = parent.children;
|
|
708
|
+
if (!Array.isArray(children)) return null;
|
|
709
|
+
const idx = children.indexOf(runtime.lastStreamingComp);
|
|
710
|
+
group = new ToolGroupComponent();
|
|
711
|
+
detachGroup(group);
|
|
712
|
+
children.splice(idx >= 0 ? idx + 1 : children.length, 0, group);
|
|
713
|
+
noteGroupMount(parent, group);
|
|
714
|
+
runtime.groups.add(group);
|
|
715
|
+
runtime.lastActiveGroup = group;
|
|
716
|
+
parent.invalidate?.();
|
|
717
|
+
return group;
|
|
718
|
+
}
|
|
719
|
+
function attachPreparingTool(tool) {
|
|
720
|
+
if (tool.owner) return;
|
|
721
|
+
const group = openToolGroup();
|
|
722
|
+
if (!group) return;
|
|
723
|
+
tool.owner = group;
|
|
724
|
+
group.entries.push({ kind: "tool", tool });
|
|
725
|
+
group.invalidate();
|
|
726
|
+
runtime.lastActiveGroup = group;
|
|
727
|
+
}
|
|
728
|
+
function detachPreparingTool(tool) {
|
|
729
|
+
const group = tool.owner;
|
|
730
|
+
if (group) {
|
|
731
|
+
const index = group.entries.findIndex((entry) => entry.kind === "tool" && entry.tool === tool);
|
|
732
|
+
if (index >= 0) group.entries.splice(index, 1);
|
|
733
|
+
}
|
|
734
|
+
tool.owner = null;
|
|
735
|
+
}
|
|
736
|
+
function takePreparingTool(real) {
|
|
737
|
+
const id = String(real?.toolCallId ?? "");
|
|
738
|
+
const name = String(real?.toolName ?? "");
|
|
739
|
+
if (id) {
|
|
740
|
+
for (const [index, candidate] of runtime.preparingByIndex) {
|
|
741
|
+
if (candidate.toolCallId === id) {
|
|
742
|
+
runtime.preparingByIndex.delete(index);
|
|
743
|
+
return candidate;
|
|
744
|
+
}
|
|
745
|
+
}
|
|
746
|
+
}
|
|
747
|
+
const matches = [...runtime.preparingByIndex.entries()].filter(([, candidate]) => candidate.toolName === name);
|
|
748
|
+
if (matches.length === 1) {
|
|
749
|
+
runtime.preparingByIndex.delete(matches[0][0]);
|
|
750
|
+
return matches[0][1];
|
|
751
|
+
}
|
|
752
|
+
return void 0;
|
|
753
|
+
}
|
|
754
|
+
function absorbPreparingTool(real) {
|
|
755
|
+
const preparing = takePreparingTool(real);
|
|
756
|
+
if (!preparing) return;
|
|
757
|
+
const id = String(real?.toolCallId ?? "");
|
|
758
|
+
const start = runtime.toolStarts.get(preparing.toolCallId);
|
|
759
|
+
if (start !== void 0 && id && !runtime.toolStarts.has(id)) runtime.toolStarts.set(id, start);
|
|
760
|
+
real._contentIndex = preparing._contentIndex;
|
|
761
|
+
if ((!real.args || Object.keys(real.args).length === 0) && preparing.args) real.args = preparing.args;
|
|
762
|
+
detachPreparingTool(preparing);
|
|
763
|
+
}
|
|
764
|
+
function clearPreparingTools() {
|
|
765
|
+
for (const tool of runtime.preparingByIndex.values()) detachPreparingTool(tool);
|
|
766
|
+
runtime.preparingByIndex.clear();
|
|
767
|
+
}
|
|
768
|
+
function noteStreamingToolCall(content, streamEvent) {
|
|
769
|
+
const located = locateStreamingToolCall(content, streamEvent);
|
|
770
|
+
if (!located) return;
|
|
771
|
+
const { index, block } = located;
|
|
772
|
+
const id = String(block.id || "");
|
|
773
|
+
const name = String(block.name || "tool");
|
|
774
|
+
const args = streamingToolArgs(block);
|
|
775
|
+
const existing = findGroupedTool(id, index);
|
|
776
|
+
if (existing && !existing._preparing) {
|
|
777
|
+
existing._contentIndex = index;
|
|
778
|
+
existing.args = args;
|
|
779
|
+
rememberToolStart(String(existing.toolCallId || id));
|
|
780
|
+
invalidateGroupedTool(existing);
|
|
781
|
+
return;
|
|
782
|
+
}
|
|
783
|
+
let preparing = runtime.preparingByIndex.get(index);
|
|
784
|
+
if (!preparing) {
|
|
785
|
+
preparing = {
|
|
786
|
+
toolName: name,
|
|
787
|
+
toolCallId: id || `preparing:${index}`,
|
|
788
|
+
args,
|
|
789
|
+
isPartial: true,
|
|
790
|
+
_preparing: true,
|
|
791
|
+
_contentIndex: index,
|
|
792
|
+
owner: null
|
|
793
|
+
};
|
|
794
|
+
runtime.preparingByIndex.set(index, preparing);
|
|
795
|
+
attachPreparingTool(preparing);
|
|
796
|
+
} else {
|
|
797
|
+
if (name && name !== "tool") preparing.toolName = name;
|
|
798
|
+
if (id && preparing.toolCallId !== id) {
|
|
799
|
+
const previous = preparing.toolCallId;
|
|
800
|
+
preparing.toolCallId = id;
|
|
801
|
+
const start = runtime.toolStarts.get(previous);
|
|
802
|
+
if (start !== void 0 && !runtime.toolStarts.has(id)) runtime.toolStarts.set(id, start);
|
|
803
|
+
}
|
|
804
|
+
preparing.args = args;
|
|
805
|
+
if (!preparing.owner) attachPreparingTool(preparing);
|
|
806
|
+
}
|
|
807
|
+
rememberToolStart(preparing.toolCallId);
|
|
808
|
+
invalidateGroupedTool(preparing);
|
|
809
|
+
}
|
|
810
|
+
|
|
811
|
+
// src/animation.ts
|
|
812
|
+
function thinkingSpinnerFrame(now = Date.now()) {
|
|
813
|
+
const index = Math.floor((now - spinnerStart) / THINKING_SPINNER.intervalMs) % THINKING_SPINNER.rawFrames.length;
|
|
814
|
+
return {
|
|
815
|
+
frame: THINKING_SPINNER.rawFrames[index],
|
|
816
|
+
color: THINKING_SPINNER.colorKeys[index]
|
|
817
|
+
};
|
|
818
|
+
}
|
|
819
|
+
function disableMouseTracking() {
|
|
820
|
+
if (process.stdout.isTTY) {
|
|
821
|
+
process.stdout.write("\x1B[?1006l\x1B[?1004l\x1B[?1002l\x1B[?1000l");
|
|
822
|
+
}
|
|
823
|
+
}
|
|
824
|
+
process.on("exit", disableMouseTracking);
|
|
825
|
+
function scheduleAnimation() {
|
|
826
|
+
if (runtime.animTimer) return;
|
|
827
|
+
runtime.animTimer = setTimeout(() => {
|
|
828
|
+
runtime.animTimer = null;
|
|
829
|
+
let any = false;
|
|
830
|
+
for (const g of runtime.groups) {
|
|
831
|
+
if (g.needsAnimation()) {
|
|
832
|
+
any = true;
|
|
833
|
+
break;
|
|
834
|
+
}
|
|
835
|
+
}
|
|
836
|
+
if (any && runtime.capturedTui) {
|
|
837
|
+
runtime.capturedTui.requestRender();
|
|
838
|
+
}
|
|
839
|
+
}, [...runtime.groups].some((group) => group.hasPending()) ? SPINNER_MS : THINKING_SPINNER.intervalMs);
|
|
840
|
+
}
|
|
841
|
+
|
|
842
|
+
// src/config.ts
|
|
843
|
+
import { join } from "path";
|
|
844
|
+
import { homedir as homedir2 } from "os";
|
|
845
|
+
import { readFileSync, writeFileSync } from "fs";
|
|
846
|
+
var CONFIG_PATH = join(homedir2(), ".pi", "agent", "compact-ui.json");
|
|
847
|
+
var DEFAULT_CONFIG = { expandedToolLines: 5, expandedThinkingLines: 10, maxGroupEntries: 5 };
|
|
848
|
+
var config = loadConfig(CONFIG_PATH);
|
|
849
|
+
function saveConfig(values = config, path = CONFIG_PATH, write = writeFileSync) {
|
|
850
|
+
try {
|
|
851
|
+
write(path, JSON.stringify(values, null, 2) + "\n");
|
|
852
|
+
} catch {
|
|
853
|
+
}
|
|
854
|
+
}
|
|
855
|
+
function loadConfig(path, read = readFileSync) {
|
|
856
|
+
try {
|
|
857
|
+
return { ...DEFAULT_CONFIG, ...JSON.parse(read(path, "utf-8")) };
|
|
858
|
+
} catch {
|
|
859
|
+
return { ...DEFAULT_CONFIG };
|
|
860
|
+
}
|
|
861
|
+
}
|
|
862
|
+
|
|
863
|
+
// src/tool-group.ts
|
|
864
|
+
var ToolGroupComponent = class extends Container {
|
|
865
|
+
toolCallId = `compact-group-${Date.now()}-${Math.random().toString(36).slice(2, 7)}`;
|
|
866
|
+
toolName = "group";
|
|
867
|
+
/** Nested at a visible-text boundary rather than rendered at chat level. */
|
|
868
|
+
anchored = false;
|
|
869
|
+
_expanded = false;
|
|
870
|
+
get expanded() {
|
|
871
|
+
return this._expanded;
|
|
872
|
+
}
|
|
873
|
+
/** Sealed: this block was closed by real text output — no further entries. */
|
|
874
|
+
sealed = false;
|
|
875
|
+
/** Thinking runs and tool calls, in the exact order they streamed in. */
|
|
876
|
+
entries = [];
|
|
877
|
+
markdownPreviewCache = /* @__PURE__ */ new Map();
|
|
878
|
+
constructor() {
|
|
879
|
+
super();
|
|
880
|
+
}
|
|
881
|
+
setExpanded(expanded) {
|
|
882
|
+
this._expanded = expanded;
|
|
883
|
+
for (const tool of this.children) tool.setExpanded?.(expanded);
|
|
884
|
+
this.invalidate();
|
|
885
|
+
}
|
|
886
|
+
addTool(tool) {
|
|
887
|
+
absorbPreparingTool(tool);
|
|
888
|
+
hideGroupedToolRender(tool);
|
|
889
|
+
if (!tool?.result && !runtime.toolDurations.has(tool?.toolCallId)) {
|
|
890
|
+
rememberToolStart(String(tool?.toolCallId ?? ""));
|
|
891
|
+
}
|
|
892
|
+
this.children.push(tool);
|
|
893
|
+
if (tool._groupedAt === void 0) tool._groupedAt = Date.now();
|
|
894
|
+
tool[PARENT_KEY] = this;
|
|
895
|
+
this.entries.push({ kind: "tool", tool });
|
|
896
|
+
}
|
|
897
|
+
/** Append a thinking run. Idempotent: a run belongs to exactly one group. */
|
|
898
|
+
addThinking(entry) {
|
|
899
|
+
if (entry.owner) return;
|
|
900
|
+
entry.owner = this;
|
|
901
|
+
this.entries.push(entry);
|
|
902
|
+
}
|
|
903
|
+
removeTool(tool) {
|
|
904
|
+
const index = this.children.indexOf(tool);
|
|
905
|
+
if (index >= 0) this.children.splice(index, 1);
|
|
906
|
+
const entryIndex = this.entries.findIndex((item) => item.kind === "tool" && item.tool === tool);
|
|
907
|
+
if (entryIndex >= 0) this.entries.splice(entryIndex, 1);
|
|
908
|
+
if (tool?.[PARENT_KEY] === this) delete tool[PARENT_KEY];
|
|
909
|
+
}
|
|
910
|
+
/** Return only entries that should be displayed (tools, or thinking with non-empty text). */
|
|
911
|
+
getVisibleEntries() {
|
|
912
|
+
return this.entries.filter((entry) => {
|
|
913
|
+
if (entry.kind === "tool") return true;
|
|
914
|
+
if (entry.kind === "thinking") {
|
|
915
|
+
return entry.text.trim().length > 0;
|
|
916
|
+
}
|
|
917
|
+
return false;
|
|
918
|
+
});
|
|
919
|
+
}
|
|
920
|
+
hasVisibleEntries() {
|
|
921
|
+
return this.getVisibleEntries().length > 0;
|
|
922
|
+
}
|
|
923
|
+
hasPending() {
|
|
924
|
+
return this.getVisibleEntries().some((entry) => entry.kind === "tool" && toolStatus(entry.tool) === "pending");
|
|
925
|
+
}
|
|
926
|
+
/** True while a thinking run in this group is still streaming. */
|
|
927
|
+
hasActiveThinking() {
|
|
928
|
+
return this.getVisibleEntries().some((entry) => entry.kind === "thinking" && entry.active);
|
|
929
|
+
}
|
|
930
|
+
/** True once any thinking run has been added to this group. */
|
|
931
|
+
hasThinking() {
|
|
932
|
+
return this.getVisibleEntries().some((entry) => entry.kind === "thinking");
|
|
933
|
+
}
|
|
934
|
+
/** True while this group should keep its spinner animating. */
|
|
935
|
+
needsAnimation() {
|
|
936
|
+
if (this.sealed) return false;
|
|
937
|
+
return this.hasVisibleEntries() && (this.hasPending() || this.hasActiveThinking());
|
|
938
|
+
}
|
|
939
|
+
invalidate() {
|
|
940
|
+
this.markdownPreviewCache.clear();
|
|
941
|
+
super.invalidate();
|
|
942
|
+
}
|
|
943
|
+
renderMarkdownPreview(cacheKey, source, width, maxLines, defaultTextStyle) {
|
|
944
|
+
const renderWidth = Math.max(1, width);
|
|
945
|
+
const lineLimit = Math.max(1, maxLines);
|
|
946
|
+
const cached = this.markdownPreviewCache.get(cacheKey);
|
|
947
|
+
if (cached && cached.source === source && cached.width === renderWidth && cached.maxLines === lineLimit) {
|
|
948
|
+
return cached;
|
|
949
|
+
}
|
|
950
|
+
const sourceRows = source.split("\n");
|
|
951
|
+
const sourceLineLimit = Math.max(lineLimit * 4, lineLimit + 20);
|
|
952
|
+
const sourceCharLimit = Math.max(4096, lineLimit * Math.max(40, renderWidth) * 4);
|
|
953
|
+
let markdownSource = sourceRows.slice(0, sourceLineLimit).join("\n");
|
|
954
|
+
let sourceTruncated = sourceRows.length > sourceLineLimit;
|
|
955
|
+
if (markdownSource.length > sourceCharLimit) {
|
|
956
|
+
markdownSource = markdownSource.slice(0, sourceCharLimit);
|
|
957
|
+
sourceTruncated = true;
|
|
958
|
+
}
|
|
959
|
+
const markdown = new Markdown(markdownSource, 0, 0, getCompactMarkdownTheme(), defaultTextStyle);
|
|
960
|
+
const rendered = normalizeCompactCodeBlockLines(markdown.render(renderWidth), renderWidth);
|
|
961
|
+
const preview = {
|
|
962
|
+
source,
|
|
963
|
+
width: renderWidth,
|
|
964
|
+
maxLines: lineLimit,
|
|
965
|
+
lines: rendered.slice(0, lineLimit),
|
|
966
|
+
truncated: sourceTruncated || rendered.length > lineLimit
|
|
967
|
+
};
|
|
968
|
+
this.markdownPreviewCache.set(cacheKey, preview);
|
|
969
|
+
return preview;
|
|
970
|
+
}
|
|
971
|
+
iconFor(tool, frame) {
|
|
972
|
+
const st = toolStatus(tool);
|
|
973
|
+
return st === "pending" ? frame : st === "error" ? "\u2717" : "\u2713";
|
|
974
|
+
}
|
|
975
|
+
colorFor(status) {
|
|
976
|
+
return status === "pending" ? "accent" : status === "error" ? "error" : "success";
|
|
977
|
+
}
|
|
978
|
+
// Tool name in bold accent, tool payload in dim, elapsed right-aligned to width.
|
|
979
|
+
toolRow(rail, tool, frame, width, isDim = false) {
|
|
980
|
+
const theme = runtime.currentTheme;
|
|
981
|
+
const fg = (color, text) => theme?.fg?.(color, text) ?? text;
|
|
982
|
+
const bold = theme?.bold ? theme.bold : (t) => t;
|
|
983
|
+
const st = toolStatus(tool);
|
|
984
|
+
const s = toolSummary(tool.toolName, tool.args);
|
|
985
|
+
const prefix = rail ? fg("dim", rail) : "";
|
|
986
|
+
const leftHeader = isDim ? `${prefix}${fg("dim", this.iconFor(tool, frame))} ${fg("dim", s.name)}` : `${prefix}${fg(this.colorFor(st), this.iconFor(tool, frame))} ${fg("toolTitle", bold(s.name))}`;
|
|
987
|
+
const right = fg(isDim ? "dim" : "muted", `(${toolElapsed(tool)}s)`);
|
|
988
|
+
const rightLen = visibleWidth2(right);
|
|
989
|
+
const headerLen = visibleWidth2(leftHeader);
|
|
990
|
+
if (s.content) {
|
|
991
|
+
const maxContentLen = width - headerLen - 1 - rightLen - 1;
|
|
992
|
+
if (maxContentLen > 3) {
|
|
993
|
+
const truncatedContent = truncateToWidth3(s.content, maxContentLen, fg("dim", "..."));
|
|
994
|
+
const left = `${leftHeader} ${fg("dim", truncatedContent)}`;
|
|
995
|
+
const gap2 = Math.max(1, width - visibleWidth2(left) - rightLen);
|
|
996
|
+
return `${left}${" ".repeat(gap2)}${right}`;
|
|
997
|
+
}
|
|
998
|
+
}
|
|
999
|
+
const gap = Math.max(1, width - headerLen - rightLen);
|
|
1000
|
+
return `${leftHeader}${" ".repeat(gap)}${right}`;
|
|
1001
|
+
}
|
|
1002
|
+
// One row per thinking run. The spinner marks the run that is still streaming;
|
|
1003
|
+
// the completion mark is written as soon as the run ends. Token label is right-aligned.
|
|
1004
|
+
thinkingRow(rail, entry, width, showPreview = true, isDim = false) {
|
|
1005
|
+
const theme = runtime.currentTheme;
|
|
1006
|
+
const fg = (color, text) => theme?.fg?.(color, text) ?? text;
|
|
1007
|
+
const spin = entry.active ? thinkingSpinnerFrame() : void 0;
|
|
1008
|
+
const icon = spin?.frame ?? "\u2713";
|
|
1009
|
+
const iconColor = isDim ? "dim" : spin?.color ?? "thinkingText";
|
|
1010
|
+
const prefix = rail ? fg("dim", rail) : "";
|
|
1011
|
+
const preview = showPreview ? entry.text.trim().replace(/[*_#`>]+/g, "") : "";
|
|
1012
|
+
if (showPreview && !preview) {
|
|
1013
|
+
const label = entry.active ? "thinking..." : "thinking";
|
|
1014
|
+
const titleColor2 = isDim ? "dim" : "toolTitle";
|
|
1015
|
+
return `${prefix}${fg(iconColor, icon)} ${fg(titleColor2, label)}`;
|
|
1016
|
+
}
|
|
1017
|
+
const titleColor = isDim ? "dim" : "toolTitle";
|
|
1018
|
+
const leftHeader = `${prefix}${fg(iconColor, icon)} ${fg(titleColor, "thinking")}`;
|
|
1019
|
+
const tokenLabel = thinkingTokenLabel(entry);
|
|
1020
|
+
const right = fg(isDim ? "dim" : "muted", tokenLabel);
|
|
1021
|
+
const rightLen = visibleWidth2(right);
|
|
1022
|
+
const headerLen = visibleWidth2(leftHeader);
|
|
1023
|
+
if (preview) {
|
|
1024
|
+
const maxPreviewLen = width - headerLen - 1 - rightLen - 1;
|
|
1025
|
+
if (maxPreviewLen > 3) {
|
|
1026
|
+
const contentColor = isDim ? "dim" : "thinkingText";
|
|
1027
|
+
const ellipsis = fg(contentColor, "...");
|
|
1028
|
+
const truncated = truncateToWidth3(preview.replace(/\s+/g, " "), maxPreviewLen, ellipsis);
|
|
1029
|
+
const left = `${leftHeader} ${fg(contentColor, truncated)}`;
|
|
1030
|
+
const gap2 = Math.max(1, width - visibleWidth2(left) - rightLen);
|
|
1031
|
+
return `${left}${" ".repeat(gap2)}${right}`;
|
|
1032
|
+
}
|
|
1033
|
+
}
|
|
1034
|
+
const gap = Math.max(1, width - headerLen - rightLen);
|
|
1035
|
+
return `${leftHeader}${" ".repeat(gap)}${right}`;
|
|
1036
|
+
}
|
|
1037
|
+
// Folded: header + one line per entry, in stream order (oldest first).
|
|
1038
|
+
// When there is only 1 visible entry, do not show hierarchy (no header, no tree rail).
|
|
1039
|
+
renderCollapsed(width, visible) {
|
|
1040
|
+
const theme = runtime.currentTheme;
|
|
1041
|
+
const fg = (color, text) => theme?.fg?.(color, text) ?? text;
|
|
1042
|
+
const frame = SPINNER[Math.floor((Date.now() - spinnerStart) / SPINNER_MS) % SPINNER.length];
|
|
1043
|
+
if (visible.length === 1) {
|
|
1044
|
+
const entry = visible[0];
|
|
1045
|
+
if (entry.kind === "tool") {
|
|
1046
|
+
return [this.toolRow("", entry.tool, frame, width)];
|
|
1047
|
+
}
|
|
1048
|
+
return [this.thinkingRow("", entry, width)];
|
|
1049
|
+
}
|
|
1050
|
+
const lines = [];
|
|
1051
|
+
const hasPendingTool = visible.some((e) => e.kind === "tool" && toolStatus(e.tool) === "pending");
|
|
1052
|
+
const isThinking = !this.sealed && visible.some((e) => e.kind === "thinking" && e.active);
|
|
1053
|
+
const hasTools = visible.some((e) => e.kind === "tool");
|
|
1054
|
+
const hasThinking = visible.some((e) => e.kind === "thinking");
|
|
1055
|
+
const openNoTools = !this.sealed && !hasTools;
|
|
1056
|
+
const working = hasPendingTool || isThinking || openNoTools;
|
|
1057
|
+
const state = hasPendingTool ? "tool calling..." : !hasTools ? working ? "thinking..." : "thinking" : isThinking ? "thinking..." : hasThinking ? "tools and thinking done" : "tools done";
|
|
1058
|
+
const stateColor = hasPendingTool ? "accent" : hasTools && !isThinking ? "success" : "thinkingText";
|
|
1059
|
+
const thinkingSpin = !hasPendingTool && working ? thinkingSpinnerFrame() : void 0;
|
|
1060
|
+
const leftIcon = working ? thinkingSpin?.frame ?? frame : "\u2713";
|
|
1061
|
+
const leftColor = thinkingSpin?.color ?? stateColor;
|
|
1062
|
+
const maxEntries = config.maxGroupEntries ?? 5;
|
|
1063
|
+
const hiddenCount = visible.length > maxEntries ? visible.length - maxEntries : 0;
|
|
1064
|
+
const hiddenSuffix = hiddenCount > 0 ? ` ${fg("muted", `(${hiddenCount} hidden)`)}` : "";
|
|
1065
|
+
lines.push(`${fg(leftColor, leftIcon)} ${fg(stateColor, state)}${hiddenSuffix}`);
|
|
1066
|
+
const entriesToRender = hiddenCount > 0 ? visible.slice(hiddenCount) : visible;
|
|
1067
|
+
const total = entriesToRender.length;
|
|
1068
|
+
for (let index = 0; index < total; index++) {
|
|
1069
|
+
const entry = entriesToRender[index];
|
|
1070
|
+
const rail = index === total - 1 ? "\u2514 " : "\u2502 ";
|
|
1071
|
+
const isDim = hiddenCount > 0 && index === 0;
|
|
1072
|
+
lines.push(
|
|
1073
|
+
entry.kind === "thinking" ? this.thinkingRow(rail, entry, width, true, isDim) : this.toolRow(rail, entry.tool, frame, width, isDim)
|
|
1074
|
+
);
|
|
1075
|
+
}
|
|
1076
|
+
return lines;
|
|
1077
|
+
}
|
|
1078
|
+
// Expanded: per-tool detail + every thinking run, in stream order.
|
|
1079
|
+
// When there is only 1 visible entry, do not show hierarchy (no header, no tree rail).
|
|
1080
|
+
renderExpanded(width, visible) {
|
|
1081
|
+
const theme = runtime.currentTheme;
|
|
1082
|
+
const fg = (color, text) => theme?.fg?.(color, text) ?? text;
|
|
1083
|
+
const frame = SPINNER[Math.floor((Date.now() - spinnerStart) / SPINNER_MS) % SPINNER.length];
|
|
1084
|
+
const lines = [];
|
|
1085
|
+
if (visible.length === 1) {
|
|
1086
|
+
const entry = visible[0];
|
|
1087
|
+
const sub = " ";
|
|
1088
|
+
if (entry.kind === "tool") {
|
|
1089
|
+
const tool = entry.tool;
|
|
1090
|
+
lines.push(this.toolRow("", tool, frame, width));
|
|
1091
|
+
const result = toolResultText(tool);
|
|
1092
|
+
if (result) {
|
|
1093
|
+
const markdownWidth = Math.max(1, width - sub.length);
|
|
1094
|
+
const preview = this.renderMarkdownPreview(
|
|
1095
|
+
`tool:${tool.toolCallId ?? 0}`,
|
|
1096
|
+
result,
|
|
1097
|
+
markdownWidth,
|
|
1098
|
+
config.expandedToolLines,
|
|
1099
|
+
{ color: (text) => runtime.currentTheme?.fg?.("toolOutput", text) ?? text }
|
|
1100
|
+
);
|
|
1101
|
+
for (const row of preview.lines) {
|
|
1102
|
+
lines.push(`${fg("dim", sub)}${row}`);
|
|
1103
|
+
}
|
|
1104
|
+
if (preview.truncated) {
|
|
1105
|
+
lines.push(`${fg("dim", sub)}${fg("muted", "...")}`);
|
|
1106
|
+
}
|
|
1107
|
+
}
|
|
1108
|
+
return lines;
|
|
1109
|
+
}
|
|
1110
|
+
lines.push(this.thinkingRow("", entry, width, false));
|
|
1111
|
+
const tText = entry.text.trim();
|
|
1112
|
+
if (tText) {
|
|
1113
|
+
const markdownWidth = Math.max(1, width - sub.length);
|
|
1114
|
+
const preview = this.renderMarkdownPreview(
|
|
1115
|
+
`thinking:${entry.id}`,
|
|
1116
|
+
tText,
|
|
1117
|
+
markdownWidth,
|
|
1118
|
+
config.expandedThinkingLines,
|
|
1119
|
+
{ color: (text) => runtime.currentTheme?.fg?.("thinkingText", text) ?? text, italic: true }
|
|
1120
|
+
);
|
|
1121
|
+
for (const row of preview.lines) {
|
|
1122
|
+
lines.push(`${fg("dim", sub)}${row}`);
|
|
1123
|
+
}
|
|
1124
|
+
if (preview.truncated) {
|
|
1125
|
+
lines.push(`${fg("dim", sub)}${fg("muted", "...")}`);
|
|
1126
|
+
}
|
|
1127
|
+
}
|
|
1128
|
+
return lines;
|
|
1129
|
+
}
|
|
1130
|
+
const hasPendingTool = visible.some((e) => e.kind === "tool" && toolStatus(e.tool) === "pending");
|
|
1131
|
+
const isThinking = !this.sealed && visible.some((e) => e.kind === "thinking" && e.active);
|
|
1132
|
+
const hasTools = visible.some((e) => e.kind === "tool");
|
|
1133
|
+
const hasThinking = visible.some((e) => e.kind === "thinking");
|
|
1134
|
+
const openNoTools = !this.sealed && !hasTools;
|
|
1135
|
+
const working = hasPendingTool || isThinking || openNoTools;
|
|
1136
|
+
const state = hasPendingTool ? "tool calling..." : !hasTools ? working ? "thinking..." : "thinking" : isThinking ? "thinking..." : hasThinking ? "tools and thinking done" : "tools done";
|
|
1137
|
+
const stateColor = hasPendingTool ? "accent" : hasTools && !isThinking ? "success" : "thinkingText";
|
|
1138
|
+
const thinkingSpin = !hasPendingTool && working ? thinkingSpinnerFrame() : void 0;
|
|
1139
|
+
const leftIcon = working ? thinkingSpin?.frame ?? frame : "\u2713";
|
|
1140
|
+
const leftColor = thinkingSpin?.color ?? stateColor;
|
|
1141
|
+
lines.push(`${fg(leftColor, leftIcon)} ${fg(stateColor, state)}`);
|
|
1142
|
+
const total = visible.length;
|
|
1143
|
+
for (let index = 0; index < total; index++) {
|
|
1144
|
+
const entry = visible[index];
|
|
1145
|
+
const isLast = index === total - 1;
|
|
1146
|
+
const rail = isLast ? "\u2514\u2500 " : "\u251C\u2500 ";
|
|
1147
|
+
const sub = isLast ? " " : "\u2502 ";
|
|
1148
|
+
if (entry.kind === "tool") {
|
|
1149
|
+
const tool = entry.tool;
|
|
1150
|
+
lines.push(this.toolRow(rail, tool, frame, width));
|
|
1151
|
+
const result = toolResultText(tool);
|
|
1152
|
+
if (result) {
|
|
1153
|
+
const markdownWidth = Math.max(1, width - sub.length);
|
|
1154
|
+
const preview = this.renderMarkdownPreview(
|
|
1155
|
+
`tool:${tool.toolCallId ?? index}`,
|
|
1156
|
+
result,
|
|
1157
|
+
markdownWidth,
|
|
1158
|
+
config.expandedToolLines,
|
|
1159
|
+
{ color: (text) => runtime.currentTheme?.fg?.("toolOutput", text) ?? text }
|
|
1160
|
+
);
|
|
1161
|
+
for (const row of preview.lines) {
|
|
1162
|
+
lines.push(`${fg("dim", sub)}${row}`);
|
|
1163
|
+
}
|
|
1164
|
+
if (preview.truncated) {
|
|
1165
|
+
lines.push(`${fg("dim", sub)}${fg("muted", "...")}`);
|
|
1166
|
+
}
|
|
1167
|
+
}
|
|
1168
|
+
continue;
|
|
1169
|
+
}
|
|
1170
|
+
lines.push(this.thinkingRow(rail, entry, width, false));
|
|
1171
|
+
const tText = entry.text.trim();
|
|
1172
|
+
if (tText) {
|
|
1173
|
+
const markdownWidth = Math.max(1, width - sub.length);
|
|
1174
|
+
const preview = this.renderMarkdownPreview(
|
|
1175
|
+
`thinking:${entry.id}`,
|
|
1176
|
+
tText,
|
|
1177
|
+
markdownWidth,
|
|
1178
|
+
config.expandedThinkingLines,
|
|
1179
|
+
{ color: (text) => runtime.currentTheme?.fg?.("thinkingText", text) ?? text, italic: true }
|
|
1180
|
+
);
|
|
1181
|
+
for (const row of preview.lines) {
|
|
1182
|
+
lines.push(`${fg("dim", sub)}${row}`);
|
|
1183
|
+
}
|
|
1184
|
+
if (preview.truncated) {
|
|
1185
|
+
lines.push(`${fg("dim", sub)}${fg("muted", "...")}`);
|
|
1186
|
+
}
|
|
1187
|
+
}
|
|
1188
|
+
}
|
|
1189
|
+
return lines;
|
|
1190
|
+
}
|
|
1191
|
+
render(width) {
|
|
1192
|
+
const visible = this.getVisibleEntries();
|
|
1193
|
+
if (visible.length === 0) return [];
|
|
1194
|
+
if (this.needsAnimation()) scheduleAnimation();
|
|
1195
|
+
const padding = " ".repeat(Math.min(GROUP_PADDING_X, Math.max(0, width - 1)));
|
|
1196
|
+
const contentWidth = Math.max(1, width - padding.length - GROUP_PADDING_RIGHT);
|
|
1197
|
+
const lines = this._expanded ? this.renderExpanded(contentWidth, visible) : this.renderCollapsed(contentWidth, visible);
|
|
1198
|
+
if (lines.length === 0) return [];
|
|
1199
|
+
const rendered = lines.map((line) => padding + truncateToWidth3(line, contentWidth, "..."));
|
|
1200
|
+
return this.anchored ? rendered : ["", ...rendered];
|
|
1201
|
+
}
|
|
1202
|
+
};
|
|
1203
|
+
|
|
1204
|
+
// src/guards.ts
|
|
1205
|
+
function isContainer(val) {
|
|
1206
|
+
return Boolean(
|
|
1207
|
+
val && (val instanceof Container2 || val.constructor?.name === "Container" || Array.isArray(val.children) && typeof val.addChild === "function")
|
|
1208
|
+
);
|
|
1209
|
+
}
|
|
1210
|
+
function isAssistantMessage(val) {
|
|
1211
|
+
return Boolean(
|
|
1212
|
+
val && (val instanceof AssistantMessageComponent2 || val.constructor?.name === "AssistantMessageComponent")
|
|
1213
|
+
);
|
|
1214
|
+
}
|
|
1215
|
+
function isToolExecution(val) {
|
|
1216
|
+
return Boolean(
|
|
1217
|
+
val && (val instanceof ToolExecutionComponent || val.constructor?.name === "ToolExecutionComponent")
|
|
1218
|
+
);
|
|
1219
|
+
}
|
|
1220
|
+
function isSpacer(val) {
|
|
1221
|
+
return Boolean(val && (val instanceof Spacer2 || val.constructor?.name === "Spacer"));
|
|
1222
|
+
}
|
|
1223
|
+
function isMarkdown(val) {
|
|
1224
|
+
return Boolean(val && (val instanceof Markdown2 || val.constructor?.name === "Markdown"));
|
|
1225
|
+
}
|
|
1226
|
+
function isText(val) {
|
|
1227
|
+
return Boolean(val && (val instanceof Text || val.constructor?.name === "Text"));
|
|
1228
|
+
}
|
|
1229
|
+
function isToolGroup(val) {
|
|
1230
|
+
return Boolean(val && (val instanceof ToolGroupComponent || val.constructor?.name === "ToolGroupComponent"));
|
|
1231
|
+
}
|
|
1232
|
+
function hideGroupedToolRender(tool) {
|
|
1233
|
+
const prototype = tool ? Object.getPrototypeOf(tool) : void 0;
|
|
1234
|
+
if (!prototype || typeof prototype.render !== "function" || prototype[TOOL_RENDER_PATCH_KEY]) return;
|
|
1235
|
+
const original = prototype.render;
|
|
1236
|
+
const installed = function(width) {
|
|
1237
|
+
if (this?.[PARENT_KEY]) return [];
|
|
1238
|
+
return original.call(this, width);
|
|
1239
|
+
};
|
|
1240
|
+
prototype.render = installed;
|
|
1241
|
+
prototype[TOOL_RENDER_PATCH_KEY] = { original, installed };
|
|
1242
|
+
}
|
|
1243
|
+
|
|
1244
|
+
// src/thinking.ts
|
|
1245
|
+
function updateActiveThinkingTokens(message) {
|
|
1246
|
+
const entry = runtime.activeThinking;
|
|
1247
|
+
if (!entry) return;
|
|
1248
|
+
entry.text = [...runtime.activeThinkingBlocks.values()].filter((text) => text.trim()).join("\n\n");
|
|
1249
|
+
const reported = Number(message?.usage?.reasoning);
|
|
1250
|
+
if (Number.isFinite(reported) && reported > 0) {
|
|
1251
|
+
entry.tokens = reported;
|
|
1252
|
+
entry.tokensExact = true;
|
|
1253
|
+
runtime.lastReportedReasoningTokens = reported;
|
|
1254
|
+
return;
|
|
1255
|
+
}
|
|
1256
|
+
entry.tokens = estimateTextTokens(entry.text);
|
|
1257
|
+
entry.tokensExact = false;
|
|
1258
|
+
}
|
|
1259
|
+
function createThinkingEntry() {
|
|
1260
|
+
const entry = {
|
|
1261
|
+
kind: "thinking",
|
|
1262
|
+
id: runtime.thinkingEntrySeq++,
|
|
1263
|
+
text: "",
|
|
1264
|
+
tokens: 0,
|
|
1265
|
+
tokensExact: false,
|
|
1266
|
+
active: true,
|
|
1267
|
+
owner: null
|
|
1268
|
+
};
|
|
1269
|
+
runtime.activeThinking = entry;
|
|
1270
|
+
runtime.activeThinkingBlocks.clear();
|
|
1271
|
+
runtime.unattachedThinking.push(entry);
|
|
1272
|
+
return entry;
|
|
1273
|
+
}
|
|
1274
|
+
function finalizeActiveThinking() {
|
|
1275
|
+
const entry = runtime.activeThinking;
|
|
1276
|
+
if (!entry) return;
|
|
1277
|
+
entry.active = false;
|
|
1278
|
+
entry.owner?.invalidate();
|
|
1279
|
+
if (runtime.activeThinkingIndex !== null) runtime.sealedThinkingIndexes.add(runtime.activeThinkingIndex);
|
|
1280
|
+
runtime.lastSealedThinkingEntry = entry;
|
|
1281
|
+
runtime.activeThinking = null;
|
|
1282
|
+
runtime.activeThinkingIndex = null;
|
|
1283
|
+
runtime.activeThinkingBlocks.clear();
|
|
1284
|
+
}
|
|
1285
|
+
function refreshSealedThinkingTokens(reportedReasoning) {
|
|
1286
|
+
const reported = Number(reportedReasoning);
|
|
1287
|
+
if (Number.isFinite(reported) && reported > 0) runtime.lastReportedReasoningTokens = reported;
|
|
1288
|
+
const entry = runtime.lastSealedThinkingEntry;
|
|
1289
|
+
if (!entry || entry.tokensExact) return;
|
|
1290
|
+
if (!entry.text.trim()) return;
|
|
1291
|
+
if (!(runtime.lastReportedReasoningTokens > 0)) return;
|
|
1292
|
+
entry.tokens = runtime.lastReportedReasoningTokens;
|
|
1293
|
+
entry.tokensExact = true;
|
|
1294
|
+
entry.owner?.invalidate();
|
|
1295
|
+
runtime.capturedTui?.requestRender?.();
|
|
1296
|
+
}
|
|
1297
|
+
|
|
1298
|
+
// src/grouping.ts
|
|
1299
|
+
import { Container as Container3 } from "@earendil-works/pi-tui";
|
|
1300
|
+
import { AssistantMessageComponent as AssistantMessageComponent3 } from "@earendil-works/pi-coding-agent";
|
|
1301
|
+
function isGroupable(value) {
|
|
1302
|
+
return isToolExecution(value);
|
|
1303
|
+
}
|
|
1304
|
+
function previousGroupable(children, start) {
|
|
1305
|
+
for (let i = start; i >= 0; i--) {
|
|
1306
|
+
const child = children[i];
|
|
1307
|
+
if (isSpacer(child)) continue;
|
|
1308
|
+
if (isAssistantMessage(child)) continue;
|
|
1309
|
+
if (isToolGroup(child) && child.sealed) continue;
|
|
1310
|
+
if (isToolGroup(child) || isGroupable(child)) return { child, index: i };
|
|
1311
|
+
}
|
|
1312
|
+
return void 0;
|
|
1313
|
+
}
|
|
1314
|
+
function assistantHasVisibleText(component) {
|
|
1315
|
+
const content = component?.lastMessage?.content;
|
|
1316
|
+
if (!Array.isArray(content)) return false;
|
|
1317
|
+
return content.some((item) => item?.type === "text" && String(item.text ?? "").trim());
|
|
1318
|
+
}
|
|
1319
|
+
function absorbThinkingBeforeText(group, message) {
|
|
1320
|
+
const content = message?.content;
|
|
1321
|
+
if (!Array.isArray(content)) return;
|
|
1322
|
+
for (const item of content) {
|
|
1323
|
+
if (item?.type === "text" && String(item.text ?? "").trim()) break;
|
|
1324
|
+
if (item?.type !== "thinking") continue;
|
|
1325
|
+
const text = String(item.thinking ?? "").trim();
|
|
1326
|
+
if (!text || thinkingAlreadyShown(text)) continue;
|
|
1327
|
+
group.entries.push({
|
|
1328
|
+
kind: "thinking",
|
|
1329
|
+
id: runtime.thinkingEntrySeq++,
|
|
1330
|
+
text,
|
|
1331
|
+
tokens: estimateTextTokens(text),
|
|
1332
|
+
tokensExact: false,
|
|
1333
|
+
active: false,
|
|
1334
|
+
owner: group
|
|
1335
|
+
});
|
|
1336
|
+
}
|
|
1337
|
+
}
|
|
1338
|
+
function sealOpenGroupAtAssistantText(assistant) {
|
|
1339
|
+
const open = findOpenGroup();
|
|
1340
|
+
if (!open) return;
|
|
1341
|
+
absorbThinkingBeforeText(open, assistant.lastMessage);
|
|
1342
|
+
open.sealed = true;
|
|
1343
|
+
finalizeActiveThinking();
|
|
1344
|
+
const contentContainer = assistant.contentContainer;
|
|
1345
|
+
if (isContainer(contentContainer) && contentContainer.children.some(isVisibleTextMarkdown)) {
|
|
1346
|
+
const previous = runtime.lastStreamingComp;
|
|
1347
|
+
runtime.lastStreamingComp = assistant;
|
|
1348
|
+
anchorGroupBeforeCurrentText(open, 0);
|
|
1349
|
+
runtime.lastStreamingComp = previous ?? assistant;
|
|
1350
|
+
}
|
|
1351
|
+
open.invalidate();
|
|
1352
|
+
}
|
|
1353
|
+
function ensureThinkingGroup() {
|
|
1354
|
+
if (runtime.unattachedThinking.length === 0) return;
|
|
1355
|
+
let group = findOpenGroup();
|
|
1356
|
+
if (group && runtime.lastChatContainer && runtime.lastStreamingComp) {
|
|
1357
|
+
placeGroupAfter(runtime.lastChatContainer, runtime.lastStreamingComp, group);
|
|
1358
|
+
}
|
|
1359
|
+
if (!group) {
|
|
1360
|
+
if (!runtime.lastChatContainer || !runtime.lastStreamingComp) return;
|
|
1361
|
+
const parent = runtime.lastChatContainer;
|
|
1362
|
+
const children = parent.children;
|
|
1363
|
+
if (!Array.isArray(children)) return;
|
|
1364
|
+
const idx = children.indexOf(runtime.lastStreamingComp);
|
|
1365
|
+
group = new ToolGroupComponent();
|
|
1366
|
+
detachGroup(group);
|
|
1367
|
+
children.splice(idx >= 0 ? idx + 1 : children.length, 0, group);
|
|
1368
|
+
noteGroupMount(parent, group);
|
|
1369
|
+
runtime.groups.add(group);
|
|
1370
|
+
runtime.lastActiveGroup = group;
|
|
1371
|
+
parent.invalidate?.();
|
|
1372
|
+
}
|
|
1373
|
+
while (runtime.unattachedThinking.length > 0) group.addThinking(runtime.unattachedThinking.shift());
|
|
1374
|
+
group.invalidate();
|
|
1375
|
+
runtime.capturedTui?.requestRender?.();
|
|
1376
|
+
}
|
|
1377
|
+
function flushPendingTextSeal() {
|
|
1378
|
+
if (!runtime.pendingTextSeal) return;
|
|
1379
|
+
if (runtime.activeThinking || runtime.unattachedThinking.length > 0) ensureThinkingGroup();
|
|
1380
|
+
if (runtime.lastActiveGroup && !runtime.lastActiveGroup.sealed) {
|
|
1381
|
+
if (runtime.lastActiveGroup.hasVisibleEntries()) {
|
|
1382
|
+
if (runtime.pendingTextOrdinal !== null) {
|
|
1383
|
+
anchorGroupBeforeCurrentText(runtime.lastActiveGroup, runtime.pendingTextOrdinal);
|
|
1384
|
+
}
|
|
1385
|
+
} else {
|
|
1386
|
+
if (runtime.lastChatContainer) removeGroupFromContainer(runtime.lastChatContainer, runtime.lastActiveGroup);
|
|
1387
|
+
runtime.groups.delete(runtime.lastActiveGroup);
|
|
1388
|
+
}
|
|
1389
|
+
runtime.lastActiveGroup.sealed = true;
|
|
1390
|
+
finalizeActiveThinking();
|
|
1391
|
+
runtime.lastActiveGroup.invalidate();
|
|
1392
|
+
runtime.pendingTextSeal = false;
|
|
1393
|
+
runtime.pendingTextOrdinal = null;
|
|
1394
|
+
return;
|
|
1395
|
+
}
|
|
1396
|
+
runtime.pendingTextSeal = false;
|
|
1397
|
+
runtime.pendingTextOrdinal = null;
|
|
1398
|
+
}
|
|
1399
|
+
function getPrecedingAssistantThinking(children, beforeIndex) {
|
|
1400
|
+
for (let i = beforeIndex; i >= 0; i--) {
|
|
1401
|
+
const child = children[i];
|
|
1402
|
+
if (isSpacer(child)) continue;
|
|
1403
|
+
if (isAssistantMessage(child)) {
|
|
1404
|
+
const content = child.lastMessage?.content;
|
|
1405
|
+
if (!Array.isArray(content)) return [];
|
|
1406
|
+
const texts = [];
|
|
1407
|
+
for (const item of content) {
|
|
1408
|
+
if (item?.type === "thinking") {
|
|
1409
|
+
const t = String(item.thinking ?? "").trim();
|
|
1410
|
+
if (t && !thinkingAlreadyShown(t)) texts.push(t);
|
|
1411
|
+
}
|
|
1412
|
+
}
|
|
1413
|
+
return texts;
|
|
1414
|
+
}
|
|
1415
|
+
break;
|
|
1416
|
+
}
|
|
1417
|
+
return [];
|
|
1418
|
+
}
|
|
1419
|
+
function attachPrecedingThinkingToGroup(group, children, beforeIndex) {
|
|
1420
|
+
const texts = getPrecedingAssistantThinking(children, beforeIndex);
|
|
1421
|
+
if (texts.length === 0) return;
|
|
1422
|
+
for (const text of texts) {
|
|
1423
|
+
if (thinkingAlreadyShown(text)) continue;
|
|
1424
|
+
const entry = {
|
|
1425
|
+
kind: "thinking",
|
|
1426
|
+
id: runtime.thinkingEntrySeq++,
|
|
1427
|
+
text,
|
|
1428
|
+
tokens: estimateTextTokens(text),
|
|
1429
|
+
tokensExact: false,
|
|
1430
|
+
active: false,
|
|
1431
|
+
owner: group
|
|
1432
|
+
};
|
|
1433
|
+
group.entries.push(entry);
|
|
1434
|
+
}
|
|
1435
|
+
}
|
|
1436
|
+
function joinOpenGroup(parent, index, component) {
|
|
1437
|
+
if (!runtime.lastChatContainer || parent !== runtime.lastChatContainer) return false;
|
|
1438
|
+
const open = findOpenGroup();
|
|
1439
|
+
if (!open) return false;
|
|
1440
|
+
const children = parent.children;
|
|
1441
|
+
children.splice(index, 1);
|
|
1442
|
+
if (runtime.lastStreamingComp && children.includes(runtime.lastStreamingComp)) {
|
|
1443
|
+
placeGroupAfter(parent, runtime.lastStreamingComp, open);
|
|
1444
|
+
} else if (!children.includes(open)) {
|
|
1445
|
+
children.push(open);
|
|
1446
|
+
noteGroupMount(parent, open);
|
|
1447
|
+
}
|
|
1448
|
+
attachPrecedingThinkingToGroup(open, children, children.indexOf(open) - 1);
|
|
1449
|
+
open.addTool(component);
|
|
1450
|
+
runtime.lastActiveGroup = open;
|
|
1451
|
+
open.invalidate();
|
|
1452
|
+
return true;
|
|
1453
|
+
}
|
|
1454
|
+
function maybeGroup(parent, component) {
|
|
1455
|
+
if (!isGroupable(component) || isToolGroup(parent)) return;
|
|
1456
|
+
const children = parent?.children;
|
|
1457
|
+
if (!Array.isArray(children)) return;
|
|
1458
|
+
const index = children.indexOf(component);
|
|
1459
|
+
if (index < 0) return;
|
|
1460
|
+
if (joinOpenGroup(parent, index, component)) return;
|
|
1461
|
+
const prior = previousGroupable(children, index - 1);
|
|
1462
|
+
if (isToolGroup(prior?.child) && !prior.child.sealed) {
|
|
1463
|
+
attachPrecedingThinkingToGroup(prior.child, children, index - 1);
|
|
1464
|
+
children.splice(index, 1);
|
|
1465
|
+
prior.child.addTool(component);
|
|
1466
|
+
runtime.lastActiveGroup = prior.child;
|
|
1467
|
+
return;
|
|
1468
|
+
}
|
|
1469
|
+
if (prior && isGroupable(prior.child)) {
|
|
1470
|
+
const group2 = new ToolGroupComponent();
|
|
1471
|
+
attachPrecedingThinkingToGroup(group2, children, prior.index - 1);
|
|
1472
|
+
group2.addTool(prior.child);
|
|
1473
|
+
attachPrecedingThinkingToGroup(group2, children, index - 1);
|
|
1474
|
+
group2.addTool(component);
|
|
1475
|
+
parent.children[prior.index] = group2;
|
|
1476
|
+
children.splice(index, 1);
|
|
1477
|
+
noteGroupMount(parent, group2);
|
|
1478
|
+
runtime.groups.add(group2);
|
|
1479
|
+
runtime.lastActiveGroup = group2;
|
|
1480
|
+
return;
|
|
1481
|
+
}
|
|
1482
|
+
const group = new ToolGroupComponent();
|
|
1483
|
+
attachPrecedingThinkingToGroup(group, children, index - 1);
|
|
1484
|
+
group.addTool(component);
|
|
1485
|
+
parent.children[index] = group;
|
|
1486
|
+
noteGroupMount(parent, group);
|
|
1487
|
+
runtime.groups.add(group);
|
|
1488
|
+
runtime.lastActiveGroup = group;
|
|
1489
|
+
}
|
|
1490
|
+
function installGrouping() {
|
|
1491
|
+
const host = globalThis;
|
|
1492
|
+
const prototypes = [Container3.prototype];
|
|
1493
|
+
const agentContainerProto = Object.getPrototypeOf(AssistantMessageComponent3.prototype);
|
|
1494
|
+
if (agentContainerProto && agentContainerProto !== Object.prototype && !prototypes.includes(agentContainerProto)) {
|
|
1495
|
+
prototypes.push(agentContainerProto);
|
|
1496
|
+
}
|
|
1497
|
+
const patchKey = PATCH_KEY;
|
|
1498
|
+
const previous = host[patchKey];
|
|
1499
|
+
const stateMap = /* @__PURE__ */ new Map();
|
|
1500
|
+
for (const prototype of prototypes) {
|
|
1501
|
+
const prev = previous instanceof Map ? previous.get(prototype) : previous;
|
|
1502
|
+
const original = {
|
|
1503
|
+
addChild: prev && prototype.addChild === prev.installed.addChild ? prev.original.addChild : prototype.addChild,
|
|
1504
|
+
removeChild: prev && prototype.removeChild === prev.installed.removeChild ? prev.original.removeChild : prototype.removeChild,
|
|
1505
|
+
clear: prev && prototype.clear === prev.installed.clear ? prev.original.clear : prototype.clear
|
|
1506
|
+
};
|
|
1507
|
+
const state = {
|
|
1508
|
+
active: true,
|
|
1509
|
+
prototype,
|
|
1510
|
+
original,
|
|
1511
|
+
installed: void 0
|
|
1512
|
+
};
|
|
1513
|
+
state.installed = {
|
|
1514
|
+
addChild: function(component) {
|
|
1515
|
+
const result = state.original.addChild.call(this, component);
|
|
1516
|
+
if (component && typeof component === "object") {
|
|
1517
|
+
if (isAssistantMessage(component)) {
|
|
1518
|
+
const assistant = component;
|
|
1519
|
+
ensureAssistantThinkingPatched(assistant);
|
|
1520
|
+
const streaming = assistant.isStreaming === true;
|
|
1521
|
+
if (streaming) {
|
|
1522
|
+
assistant[LIVE_ASSISTANT_KEY] = true;
|
|
1523
|
+
}
|
|
1524
|
+
runtime.lastChatContainer = this;
|
|
1525
|
+
runtime.lastStreamingComp = assistant;
|
|
1526
|
+
assistant[IS_STREAMING_COMP] = streaming;
|
|
1527
|
+
if (assistantHasVisibleText(assistant)) {
|
|
1528
|
+
sealOpenGroupAtAssistantText(assistant);
|
|
1529
|
+
} else {
|
|
1530
|
+
const open = findOpenGroup();
|
|
1531
|
+
if (open) placeGroupAfter(this, assistant, open);
|
|
1532
|
+
}
|
|
1533
|
+
if (runtime.unattachedThinking.length > 0) ensureThinkingGroup();
|
|
1534
|
+
flushPendingTextSeal();
|
|
1535
|
+
}
|
|
1536
|
+
maybeGroup(this, component);
|
|
1537
|
+
stripAssistantPhantomPadding(this, component);
|
|
1538
|
+
restoreAssistantAnchor(this, component);
|
|
1539
|
+
}
|
|
1540
|
+
return result;
|
|
1541
|
+
},
|
|
1542
|
+
removeChild: function(component) {
|
|
1543
|
+
const group = component?.[PARENT_KEY];
|
|
1544
|
+
if (isToolGroup(group) && group[PARENT_KEY] === this) {
|
|
1545
|
+
group.removeTool(component);
|
|
1546
|
+
if (group.children.length === 0 && group.entries.length === 0) runtime.groups.delete(group);
|
|
1547
|
+
return;
|
|
1548
|
+
}
|
|
1549
|
+
releaseAssistantAnchors(component);
|
|
1550
|
+
return state.original.removeChild.call(this, component);
|
|
1551
|
+
},
|
|
1552
|
+
clear: function() {
|
|
1553
|
+
if (runtime.assistantContentContainers.has(this)) {
|
|
1554
|
+
getAssistantContentState(this).nextTextOrdinal = 0;
|
|
1555
|
+
return state.original.clear.call(this);
|
|
1556
|
+
}
|
|
1557
|
+
for (const child of [...this.children ?? []]) {
|
|
1558
|
+
if (isToolGroup(child)) {
|
|
1559
|
+
for (const tool of [...child.children]) delete tool[PARENT_KEY];
|
|
1560
|
+
runtime.groups.delete(child);
|
|
1561
|
+
}
|
|
1562
|
+
releaseAssistantAnchors(child);
|
|
1563
|
+
}
|
|
1564
|
+
return state.original.clear.call(this);
|
|
1565
|
+
}
|
|
1566
|
+
};
|
|
1567
|
+
prototype.addChild = state.installed.addChild;
|
|
1568
|
+
prototype.removeChild = state.installed.removeChild;
|
|
1569
|
+
prototype.clear = state.installed.clear;
|
|
1570
|
+
stateMap.set(prototype, state);
|
|
1571
|
+
}
|
|
1572
|
+
host[patchKey] = stateMap;
|
|
1573
|
+
}
|
|
1574
|
+
|
|
1575
|
+
// src/compaction.ts
|
|
1576
|
+
import { visibleWidth as visibleWidth3, truncateToWidth as truncateToWidth4 } from "@earendil-works/pi-tui";
|
|
1577
|
+
import { CompactionSummaryMessageComponent } from "@earendil-works/pi-coding-agent";
|
|
1578
|
+
var CompactionHeaderComponent = class {
|
|
1579
|
+
constructor(tokensBefore) {
|
|
1580
|
+
this.tokensBefore = tokensBefore;
|
|
1581
|
+
}
|
|
1582
|
+
tokensBefore;
|
|
1583
|
+
render(width) {
|
|
1584
|
+
const theme = runtime.currentTheme;
|
|
1585
|
+
const fg = (color, text) => theme?.fg?.(color, text) ?? text;
|
|
1586
|
+
const exactTokens = Math.max(0, this.tokensBefore).toLocaleString();
|
|
1587
|
+
const icon = fg("success", "\u203A\u2039");
|
|
1588
|
+
const title = fg("success", "Context compacted");
|
|
1589
|
+
const detail = fg("muted", ` \u2022 ${exactTokens} tokens \u2192 summary`);
|
|
1590
|
+
const full = `${icon} ${title}${detail}`;
|
|
1591
|
+
if (visibleWidth3(full) <= width) return [full];
|
|
1592
|
+
const compactTitle = fg("success", "Compacted");
|
|
1593
|
+
const compactDetail = fg("muted", ` \u2022 ${formatTokenK(this.tokensBefore)} tok`);
|
|
1594
|
+
return [truncateToWidth4(`${icon} ${compactTitle}${compactDetail}`, Math.max(1, width), "...")];
|
|
1595
|
+
}
|
|
1596
|
+
invalidate() {
|
|
1597
|
+
}
|
|
1598
|
+
};
|
|
1599
|
+
function installCompactionSummaryRendering() {
|
|
1600
|
+
const prototype = CompactionSummaryMessageComponent.prototype;
|
|
1601
|
+
const previous = prototype[COMPACTION_STYLE_PATCH_KEY];
|
|
1602
|
+
const previousInstalledUpdate = previous?.installedUpdateDisplay ?? previous?.installed;
|
|
1603
|
+
const originalUpdateDisplay = previous && previousInstalledUpdate && prototype.updateDisplay === previousInstalledUpdate ? previous.originalUpdateDisplay ?? previous.original : prototype.updateDisplay;
|
|
1604
|
+
const originalSetExpanded = previous?.installedSetExpanded && prototype.setExpanded === previous.installedSetExpanded ? previous.originalSetExpanded : prototype.setExpanded;
|
|
1605
|
+
const installedUpdateDisplay = function() {
|
|
1606
|
+
this.paddingX = GROUP_PADDING_X;
|
|
1607
|
+
this.paddingY = 0;
|
|
1608
|
+
this.setBgFn(void 0);
|
|
1609
|
+
this.clear();
|
|
1610
|
+
const tokensBefore = Number(this.message?.tokensBefore);
|
|
1611
|
+
const safeTokens = Number.isFinite(tokensBefore) && tokensBefore > 0 ? tokensBefore : 0;
|
|
1612
|
+
this.addChild(new CompactionHeaderComponent(safeTokens));
|
|
1613
|
+
};
|
|
1614
|
+
const installedSetExpanded = function(_expanded) {
|
|
1615
|
+
};
|
|
1616
|
+
prototype.updateDisplay = installedUpdateDisplay;
|
|
1617
|
+
prototype.setExpanded = installedSetExpanded;
|
|
1618
|
+
prototype[COMPACTION_STYLE_PATCH_KEY] = {
|
|
1619
|
+
originalUpdateDisplay,
|
|
1620
|
+
originalSetExpanded,
|
|
1621
|
+
installedUpdateDisplay,
|
|
1622
|
+
installedSetExpanded
|
|
1623
|
+
};
|
|
1624
|
+
}
|
|
1625
|
+
|
|
1626
|
+
// src/modals.ts
|
|
1627
|
+
import { matchesKey as matchesKey2, Key, wrapTextWithAnsi as wrapTextWithAnsi2, visibleWidth as visibleWidth4, truncateToWidth as truncateToWidth5 } from "@earendil-works/pi-tui";
|
|
1628
|
+
var DetailModalComponent = class {
|
|
1629
|
+
entry;
|
|
1630
|
+
scrollTop = 0;
|
|
1631
|
+
followingEnd = true;
|
|
1632
|
+
lastContentLineCount = 0;
|
|
1633
|
+
lastInnerHeight = 10;
|
|
1634
|
+
pollTimer = null;
|
|
1635
|
+
constructor(entry) {
|
|
1636
|
+
this.entry = entry;
|
|
1637
|
+
this.startPollingIfNeeded();
|
|
1638
|
+
}
|
|
1639
|
+
isEntryActive() {
|
|
1640
|
+
if (this.entry.kind === "thinking") {
|
|
1641
|
+
return this.entry.active;
|
|
1642
|
+
}
|
|
1643
|
+
return toolStatus(this.entry.tool) === "pending";
|
|
1644
|
+
}
|
|
1645
|
+
startPollingIfNeeded() {
|
|
1646
|
+
if (this.isEntryActive()) {
|
|
1647
|
+
const interval = this.entry.kind === "thinking" ? THINKING_SPINNER.intervalMs : SPINNER_MS;
|
|
1648
|
+
this.pollTimer = setInterval(() => {
|
|
1649
|
+
runtime.capturedTui?.requestRender?.();
|
|
1650
|
+
if (!this.isEntryActive() && this.pollTimer) {
|
|
1651
|
+
clearInterval(this.pollTimer);
|
|
1652
|
+
this.pollTimer = null;
|
|
1653
|
+
}
|
|
1654
|
+
}, interval);
|
|
1655
|
+
}
|
|
1656
|
+
}
|
|
1657
|
+
dispose() {
|
|
1658
|
+
if (this.pollTimer) {
|
|
1659
|
+
clearInterval(this.pollTimer);
|
|
1660
|
+
this.pollTimer = null;
|
|
1661
|
+
}
|
|
1662
|
+
}
|
|
1663
|
+
invalidate() {
|
|
1664
|
+
}
|
|
1665
|
+
scrollBy(delta) {
|
|
1666
|
+
this.scrollTo(this.scrollTop + delta);
|
|
1667
|
+
}
|
|
1668
|
+
scrollTo(newTop) {
|
|
1669
|
+
const maxScroll = Math.max(0, this.lastContentLineCount - this.lastInnerHeight);
|
|
1670
|
+
this.scrollTop = Math.max(0, Math.min(maxScroll, newTop));
|
|
1671
|
+
if (this.scrollTop >= maxScroll) {
|
|
1672
|
+
this.followingEnd = true;
|
|
1673
|
+
} else {
|
|
1674
|
+
this.followingEnd = false;
|
|
1675
|
+
}
|
|
1676
|
+
runtime.capturedTui?.requestRender?.();
|
|
1677
|
+
}
|
|
1678
|
+
scrollToEnd() {
|
|
1679
|
+
this.followingEnd = true;
|
|
1680
|
+
this.scrollTop = Math.max(0, this.lastContentLineCount - this.lastInnerHeight);
|
|
1681
|
+
runtime.capturedTui?.requestRender?.();
|
|
1682
|
+
}
|
|
1683
|
+
handleInput(data) {
|
|
1684
|
+
if (matchesKey2(data, Key.escape) || data === "q" || data === "Q") {
|
|
1685
|
+
closeDetailModal();
|
|
1686
|
+
return true;
|
|
1687
|
+
}
|
|
1688
|
+
if (matchesKey2(data, Key.up) || data === "k" || data === "K") {
|
|
1689
|
+
this.scrollBy(-1);
|
|
1690
|
+
return true;
|
|
1691
|
+
}
|
|
1692
|
+
if (matchesKey2(data, Key.down) || data === "j" || data === "J") {
|
|
1693
|
+
this.scrollBy(1);
|
|
1694
|
+
return true;
|
|
1695
|
+
}
|
|
1696
|
+
if (matchesKey2(data, Key.pageUp) || matchesKey2(data, Key.left)) {
|
|
1697
|
+
this.scrollBy(-10);
|
|
1698
|
+
return true;
|
|
1699
|
+
}
|
|
1700
|
+
if (matchesKey2(data, Key.pageDown) || matchesKey2(data, Key.right) || matchesKey2(data, Key.space)) {
|
|
1701
|
+
this.scrollBy(10);
|
|
1702
|
+
return true;
|
|
1703
|
+
}
|
|
1704
|
+
if (matchesKey2(data, Key.home)) {
|
|
1705
|
+
this.scrollTo(0);
|
|
1706
|
+
return true;
|
|
1707
|
+
}
|
|
1708
|
+
if (matchesKey2(data, Key.end)) {
|
|
1709
|
+
this.scrollToEnd();
|
|
1710
|
+
return true;
|
|
1711
|
+
}
|
|
1712
|
+
return false;
|
|
1713
|
+
}
|
|
1714
|
+
render(width) {
|
|
1715
|
+
const theme = runtime.currentTheme;
|
|
1716
|
+
const fg = (color, t) => theme?.fg?.(color, t) ?? t;
|
|
1717
|
+
const bold = theme?.bold ? theme.bold : (t) => t;
|
|
1718
|
+
const borderFg = (t) => fg("borderAccent", fg("border", t));
|
|
1719
|
+
const frame = SPINNER[Math.floor((Date.now() - spinnerStart) / SPINNER_MS) % SPINNER.length];
|
|
1720
|
+
const termHeight = runtime.capturedTui?.terminal?.rows ?? process.stdout?.rows ?? 24;
|
|
1721
|
+
const maxInnerHeight = Math.max(5, Math.floor(termHeight * 0.8) - 2);
|
|
1722
|
+
const innerWidth = Math.max(10, width - 4);
|
|
1723
|
+
let title = "";
|
|
1724
|
+
const contentLines = [];
|
|
1725
|
+
if (this.entry.kind === "thinking") {
|
|
1726
|
+
const spin = this.entry.active ? thinkingSpinnerFrame() : void 0;
|
|
1727
|
+
const icon = spin?.frame ?? "\u2713";
|
|
1728
|
+
const tokenStr = thinkingTokenLabel(this.entry);
|
|
1729
|
+
title = `${fg(spin?.color ?? "thinkingText", icon)} ${fg("toolTitle", "Thinking")} ${fg("muted", tokenStr)}`;
|
|
1730
|
+
const rawText = this.entry.text.trim();
|
|
1731
|
+
if (!rawText) {
|
|
1732
|
+
contentLines.push(fg("dim", "(thinking...)"));
|
|
1733
|
+
} else {
|
|
1734
|
+
for (const rawLine of rawText.split("\n")) {
|
|
1735
|
+
const wrapped = wrapTextWithAnsi2(rawLine, innerWidth);
|
|
1736
|
+
if (wrapped.length === 0) {
|
|
1737
|
+
contentLines.push("");
|
|
1738
|
+
} else {
|
|
1739
|
+
for (const wl of wrapped) {
|
|
1740
|
+
contentLines.push(fg("thinkingText", wl));
|
|
1741
|
+
}
|
|
1742
|
+
}
|
|
1743
|
+
}
|
|
1744
|
+
}
|
|
1745
|
+
} else {
|
|
1746
|
+
const tool = this.entry.tool;
|
|
1747
|
+
const st = toolStatus(tool);
|
|
1748
|
+
const icon = st === "pending" ? frame : st === "error" ? "\u2717" : "\u2713";
|
|
1749
|
+
const color = st === "pending" ? "accent" : st === "error" ? "error" : "success";
|
|
1750
|
+
const elapsed = `(${toolElapsed(tool)}s)`;
|
|
1751
|
+
title = `${fg(color, icon)} ${fg("toolTitle", bold(tool.toolName))} ${fg("muted", elapsed)}`;
|
|
1752
|
+
if (tool.toolName === "bash" || tool.toolName === "powershell") {
|
|
1753
|
+
const cmd = tool.args?.command || "...";
|
|
1754
|
+
for (const wl of wrapTextWithAnsi2(`$ ${cmd}`, innerWidth)) {
|
|
1755
|
+
contentLines.push(fg("accent", wl));
|
|
1756
|
+
}
|
|
1757
|
+
} else if (tool.args?.path) {
|
|
1758
|
+
contentLines.push(`${fg("dim", "Path:")} ${tool.args.path}`);
|
|
1759
|
+
} else if (tool.args) {
|
|
1760
|
+
try {
|
|
1761
|
+
const jsonStr = JSON.stringify(tool.args, null, 2);
|
|
1762
|
+
for (const l of jsonStr.split("\n")) {
|
|
1763
|
+
for (const wl of wrapTextWithAnsi2(l, innerWidth)) {
|
|
1764
|
+
contentLines.push(fg("dim", wl));
|
|
1765
|
+
}
|
|
1766
|
+
}
|
|
1767
|
+
} catch {
|
|
1768
|
+
contentLines.push(fg("dim", String(tool.args)));
|
|
1769
|
+
}
|
|
1770
|
+
}
|
|
1771
|
+
const resultText = toolResultText(tool);
|
|
1772
|
+
contentLines.push("");
|
|
1773
|
+
contentLines.push(fg("dim", "\u2500\u2500 Output \u2500\u2500"));
|
|
1774
|
+
if (resultText) {
|
|
1775
|
+
for (const rawLine of resultText.split("\n")) {
|
|
1776
|
+
const wrapped = wrapTextWithAnsi2(rawLine, innerWidth);
|
|
1777
|
+
if (wrapped.length === 0) {
|
|
1778
|
+
contentLines.push("");
|
|
1779
|
+
} else {
|
|
1780
|
+
for (const wl of wrapped) {
|
|
1781
|
+
contentLines.push(fg("toolOutput", wl));
|
|
1782
|
+
}
|
|
1783
|
+
}
|
|
1784
|
+
}
|
|
1785
|
+
} else {
|
|
1786
|
+
contentLines.push(fg("dim", st === "pending" ? "(running, waiting for output...)" : "(no output)"));
|
|
1787
|
+
}
|
|
1788
|
+
}
|
|
1789
|
+
const totalLines = contentLines.length;
|
|
1790
|
+
const innerHeight = Math.min(Math.max(totalLines, 5), maxInnerHeight);
|
|
1791
|
+
this.lastContentLineCount = totalLines;
|
|
1792
|
+
this.lastInnerHeight = innerHeight;
|
|
1793
|
+
const maxScroll = Math.max(0, totalLines - innerHeight);
|
|
1794
|
+
if (this.followingEnd) {
|
|
1795
|
+
this.scrollTop = maxScroll;
|
|
1796
|
+
} else {
|
|
1797
|
+
this.scrollTop = Math.max(0, Math.min(maxScroll, this.scrollTop));
|
|
1798
|
+
}
|
|
1799
|
+
const closeHint = "[Esc to close]";
|
|
1800
|
+
const visClose = visibleWidth4(closeHint);
|
|
1801
|
+
const maxTitleWidth = Math.max(10, width - visClose - 12);
|
|
1802
|
+
const clampedTitle = truncateToWidth5(title, maxTitleWidth, "...");
|
|
1803
|
+
const visTitle = visibleWidth4(clampedTitle);
|
|
1804
|
+
const fillerLen = Math.max(1, width - visTitle - visClose - 8);
|
|
1805
|
+
const topBorder = borderFg("\u250C\u2500 ") + clampedTitle + borderFg(" " + "\u2500".repeat(fillerLen) + " ") + fg("muted", closeHint) + borderFg(" \u2500\u2510");
|
|
1806
|
+
const result = [truncateToWidth5(topBorder, width)];
|
|
1807
|
+
const slice = contentLines.slice(this.scrollTop, this.scrollTop + innerHeight);
|
|
1808
|
+
while (slice.length < innerHeight) {
|
|
1809
|
+
slice.push("");
|
|
1810
|
+
}
|
|
1811
|
+
const showScrollbar = totalLines > innerHeight;
|
|
1812
|
+
const thumbY = showScrollbar && maxScroll > 0 ? Math.round(this.scrollTop / maxScroll * (innerHeight - 1)) : -1;
|
|
1813
|
+
for (let i = 0; i < innerHeight; i++) {
|
|
1814
|
+
const line = slice[i] ?? "";
|
|
1815
|
+
const vis = visibleWidth4(line);
|
|
1816
|
+
const truncated = vis > innerWidth ? truncateToWidth5(line, innerWidth, "...") : line;
|
|
1817
|
+
const truncVis = visibleWidth4(truncated);
|
|
1818
|
+
const pad = Math.max(0, innerWidth - truncVis);
|
|
1819
|
+
const rightChar = showScrollbar && i === thumbY ? fg("scrollbarThumb", "\u2588") : borderFg("\u2502");
|
|
1820
|
+
result.push(borderFg("\u2502 ") + truncated + " ".repeat(pad) + " " + rightChar);
|
|
1821
|
+
}
|
|
1822
|
+
const scrollInfo = totalLines > innerHeight ? ` [${this.scrollTop + 1}-${Math.min(totalLines, this.scrollTop + innerHeight)}/${totalLines}] ` : "";
|
|
1823
|
+
const visInfo = visibleWidth4(scrollInfo);
|
|
1824
|
+
const botFiller = Math.max(1, width - 2 - visInfo);
|
|
1825
|
+
const botBorder = borderFg("\u2514" + "\u2500".repeat(botFiller)) + (scrollInfo ? fg("dim", scrollInfo) : "") + borderFg("\u2518");
|
|
1826
|
+
result.push(truncateToWidth5(botBorder, width));
|
|
1827
|
+
return result;
|
|
1828
|
+
}
|
|
1829
|
+
};
|
|
1830
|
+
function openDetailModal(entry) {
|
|
1831
|
+
closeDetailModal();
|
|
1832
|
+
const modal = new DetailModalComponent(entry);
|
|
1833
|
+
runtime.activeModalComponent = modal;
|
|
1834
|
+
if (runtime.capturedTui) {
|
|
1835
|
+
runtime.activeModalHandle = runtime.capturedTui.showOverlay(modal, {
|
|
1836
|
+
anchor: "center",
|
|
1837
|
+
width: "85%",
|
|
1838
|
+
maxHeight: "80%"
|
|
1839
|
+
});
|
|
1840
|
+
runtime.capturedTui.requestRender();
|
|
1841
|
+
return;
|
|
1842
|
+
}
|
|
1843
|
+
if (runtime.activeUIContext?.custom) {
|
|
1844
|
+
runtime.activeUIContext.custom(
|
|
1845
|
+
(tui, _theme, _keybindings, done) => {
|
|
1846
|
+
runtime.capturedTui = tui;
|
|
1847
|
+
runtime.activeModalCloseFn = done;
|
|
1848
|
+
return modal;
|
|
1849
|
+
},
|
|
1850
|
+
{
|
|
1851
|
+
overlay: true,
|
|
1852
|
+
overlayOptions: { anchor: "center", width: "85%", maxHeight: "80%" },
|
|
1853
|
+
onHandle: (handle) => {
|
|
1854
|
+
runtime.activeModalHandle = handle;
|
|
1855
|
+
}
|
|
1856
|
+
}
|
|
1857
|
+
);
|
|
1858
|
+
}
|
|
1859
|
+
}
|
|
1860
|
+
function closeDetailModal() {
|
|
1861
|
+
if (runtime.activeModalComponent) {
|
|
1862
|
+
runtime.activeModalComponent.dispose();
|
|
1863
|
+
runtime.activeModalComponent = null;
|
|
1864
|
+
}
|
|
1865
|
+
if (runtime.activeModalHandle) {
|
|
1866
|
+
const handle = runtime.activeModalHandle;
|
|
1867
|
+
runtime.activeModalHandle = null;
|
|
1868
|
+
handle.hide();
|
|
1869
|
+
}
|
|
1870
|
+
if (runtime.activeModalCloseFn) {
|
|
1871
|
+
const done = runtime.activeModalCloseFn;
|
|
1872
|
+
runtime.activeModalCloseFn = null;
|
|
1873
|
+
done();
|
|
1874
|
+
}
|
|
1875
|
+
runtime.capturedTui?.requestRender?.();
|
|
1876
|
+
}
|
|
1877
|
+
function getAllInspectableEntries() {
|
|
1878
|
+
const results = [];
|
|
1879
|
+
for (const g of runtime.groups) {
|
|
1880
|
+
for (const entry of g.getVisibleEntries()) {
|
|
1881
|
+
results.push(entry);
|
|
1882
|
+
}
|
|
1883
|
+
}
|
|
1884
|
+
return results;
|
|
1885
|
+
}
|
|
1886
|
+
var InspectSelectorModal = class {
|
|
1887
|
+
entries = [];
|
|
1888
|
+
selectedIndex = 0;
|
|
1889
|
+
scrollTop = 0;
|
|
1890
|
+
visibleHeight = 10;
|
|
1891
|
+
constructor() {
|
|
1892
|
+
this.refreshEntries(true);
|
|
1893
|
+
}
|
|
1894
|
+
refreshEntries(isInitial = false) {
|
|
1895
|
+
const wasAtEnd = isInitial || this.selectedIndex >= this.entries.length - 1;
|
|
1896
|
+
this.entries = getAllInspectableEntries();
|
|
1897
|
+
if (this.entries.length === 0) {
|
|
1898
|
+
this.selectedIndex = 0;
|
|
1899
|
+
this.scrollTop = 0;
|
|
1900
|
+
return;
|
|
1901
|
+
}
|
|
1902
|
+
if (wasAtEnd) {
|
|
1903
|
+
this.selectedIndex = this.entries.length - 1;
|
|
1904
|
+
} else {
|
|
1905
|
+
this.selectedIndex = Math.max(0, Math.min(this.selectedIndex, this.entries.length - 1));
|
|
1906
|
+
}
|
|
1907
|
+
this.adjustScroll();
|
|
1908
|
+
}
|
|
1909
|
+
adjustScroll() {
|
|
1910
|
+
if (this.entries.length === 0) {
|
|
1911
|
+
this.scrollTop = 0;
|
|
1912
|
+
return;
|
|
1913
|
+
}
|
|
1914
|
+
if (this.selectedIndex < this.scrollTop) {
|
|
1915
|
+
this.scrollTop = this.selectedIndex;
|
|
1916
|
+
} else if (this.selectedIndex >= this.scrollTop + this.visibleHeight) {
|
|
1917
|
+
this.scrollTop = this.selectedIndex - this.visibleHeight + 1;
|
|
1918
|
+
}
|
|
1919
|
+
const maxScroll = Math.max(0, this.entries.length - this.visibleHeight);
|
|
1920
|
+
this.scrollTop = Math.max(0, Math.min(this.scrollTop, maxScroll));
|
|
1921
|
+
}
|
|
1922
|
+
handleInput(data) {
|
|
1923
|
+
if (matchesKey2(data, Key.escape) || data === "q" || data === "Q") {
|
|
1924
|
+
closeInspectSelectorModal();
|
|
1925
|
+
return true;
|
|
1926
|
+
}
|
|
1927
|
+
if (matchesKey2(data, Key.enter) || data === "\r" || data === "\n") {
|
|
1928
|
+
const selected = this.entries[this.selectedIndex];
|
|
1929
|
+
if (selected) {
|
|
1930
|
+
openDetailModal(selected);
|
|
1931
|
+
}
|
|
1932
|
+
return true;
|
|
1933
|
+
}
|
|
1934
|
+
if (matchesKey2(data, Key.up) || data === "k" || data === "K") {
|
|
1935
|
+
if (this.entries.length > 0) {
|
|
1936
|
+
this.selectedIndex = Math.max(0, this.selectedIndex - 1);
|
|
1937
|
+
this.adjustScroll();
|
|
1938
|
+
runtime.capturedTui?.requestRender?.();
|
|
1939
|
+
}
|
|
1940
|
+
return true;
|
|
1941
|
+
}
|
|
1942
|
+
if (matchesKey2(data, Key.down) || data === "j" || data === "J") {
|
|
1943
|
+
if (this.entries.length > 0) {
|
|
1944
|
+
this.selectedIndex = Math.min(this.entries.length - 1, this.selectedIndex + 1);
|
|
1945
|
+
this.adjustScroll();
|
|
1946
|
+
runtime.capturedTui?.requestRender?.();
|
|
1947
|
+
}
|
|
1948
|
+
return true;
|
|
1949
|
+
}
|
|
1950
|
+
if (matchesKey2(data, Key.pageUp)) {
|
|
1951
|
+
if (this.entries.length > 0) {
|
|
1952
|
+
this.selectedIndex = Math.max(0, this.selectedIndex - this.visibleHeight);
|
|
1953
|
+
this.adjustScroll();
|
|
1954
|
+
runtime.capturedTui?.requestRender?.();
|
|
1955
|
+
}
|
|
1956
|
+
return true;
|
|
1957
|
+
}
|
|
1958
|
+
if (matchesKey2(data, Key.pageDown)) {
|
|
1959
|
+
if (this.entries.length > 0) {
|
|
1960
|
+
this.selectedIndex = Math.min(this.entries.length - 1, this.selectedIndex + this.visibleHeight);
|
|
1961
|
+
this.adjustScroll();
|
|
1962
|
+
runtime.capturedTui?.requestRender?.();
|
|
1963
|
+
}
|
|
1964
|
+
return true;
|
|
1965
|
+
}
|
|
1966
|
+
if (matchesKey2(data, Key.home)) {
|
|
1967
|
+
if (this.entries.length > 0) {
|
|
1968
|
+
this.selectedIndex = 0;
|
|
1969
|
+
this.adjustScroll();
|
|
1970
|
+
runtime.capturedTui?.requestRender?.();
|
|
1971
|
+
}
|
|
1972
|
+
return true;
|
|
1973
|
+
}
|
|
1974
|
+
if (matchesKey2(data, Key.end)) {
|
|
1975
|
+
if (this.entries.length > 0) {
|
|
1976
|
+
this.selectedIndex = this.entries.length - 1;
|
|
1977
|
+
this.adjustScroll();
|
|
1978
|
+
runtime.capturedTui?.requestRender?.();
|
|
1979
|
+
}
|
|
1980
|
+
return true;
|
|
1981
|
+
}
|
|
1982
|
+
return false;
|
|
1983
|
+
}
|
|
1984
|
+
render(width) {
|
|
1985
|
+
const theme = runtime.currentTheme;
|
|
1986
|
+
const fg = (color, t) => theme?.fg?.(color, t) ?? t;
|
|
1987
|
+
const bold = theme?.bold ? theme.bold : (t) => t;
|
|
1988
|
+
const borderFg = (t) => fg("borderAccent", fg("border", t));
|
|
1989
|
+
const frame = SPINNER[Math.floor((Date.now() - spinnerStart) / SPINNER_MS) % SPINNER.length];
|
|
1990
|
+
const termHeight = runtime.capturedTui?.terminal?.rows ?? process.stdout?.rows ?? 24;
|
|
1991
|
+
const maxInnerHeight = Math.max(5, Math.floor(termHeight * 0.7) - 2);
|
|
1992
|
+
const innerHeight = this.entries.length === 0 ? 5 : Math.min(this.entries.length, maxInnerHeight);
|
|
1993
|
+
this.visibleHeight = innerHeight;
|
|
1994
|
+
this.adjustScroll();
|
|
1995
|
+
const innerWidth = Math.max(10, width - 4);
|
|
1996
|
+
const lines = [];
|
|
1997
|
+
const title = ` ${fg("accent", bold("Inspect"))} ${fg("dim", "(\u2191/\u2193 select \xB7 Enter view \xB7 Esc exit)")} `;
|
|
1998
|
+
const visTitle = visibleWidth4(title);
|
|
1999
|
+
const topFiller = Math.max(1, width - 2 - visTitle);
|
|
2000
|
+
const topBorder = borderFg("\u250C" + "\u2500".repeat(topFiller)) + title + borderFg("\u2510");
|
|
2001
|
+
lines.push(truncateToWidth5(topBorder, width));
|
|
2002
|
+
if (this.entries.length === 0) {
|
|
2003
|
+
const emptyMsg = " (no tool calls or thinking entries)";
|
|
2004
|
+
for (let i = 0; i < innerHeight; i++) {
|
|
2005
|
+
const row = i === Math.floor(innerHeight / 2) ? fg("dim", emptyMsg) : "";
|
|
2006
|
+
const pad = Math.max(0, innerWidth - visibleWidth4(row));
|
|
2007
|
+
lines.push(borderFg("\u2502 ") + row + " ".repeat(pad) + borderFg(" \u2502"));
|
|
2008
|
+
}
|
|
2009
|
+
} else {
|
|
2010
|
+
for (let i = 0; i < innerHeight; i++) {
|
|
2011
|
+
const entryIdx = this.scrollTop + i;
|
|
2012
|
+
const entry = this.entries[entryIdx];
|
|
2013
|
+
if (!entry) {
|
|
2014
|
+
lines.push(borderFg("\u2502 ") + " ".repeat(innerWidth) + borderFg(" \u2502"));
|
|
2015
|
+
continue;
|
|
2016
|
+
}
|
|
2017
|
+
const isSelected = entryIdx === this.selectedIndex;
|
|
2018
|
+
const prefix = isSelected ? fg("accent", bold("\u25B6 ")) : " ";
|
|
2019
|
+
let leftContent = "";
|
|
2020
|
+
let rightStr = "";
|
|
2021
|
+
if (entry.kind === "tool") {
|
|
2022
|
+
const tool = entry.tool;
|
|
2023
|
+
const st = toolStatus(tool);
|
|
2024
|
+
const icon = st === "pending" ? frame : st === "error" ? "\u2717" : "\u2713";
|
|
2025
|
+
const iconColor = st === "pending" ? "accent" : st === "error" ? "error" : "success";
|
|
2026
|
+
const s = toolSummary(tool.toolName, tool.args);
|
|
2027
|
+
const elapsed = `(${toolElapsed(tool)}s)`;
|
|
2028
|
+
const toolLabel = fg("toolTitle", bold(s.name));
|
|
2029
|
+
const contentText = fg("dim", s.content || "...");
|
|
2030
|
+
leftContent = `${prefix}${fg(iconColor, icon)} ${toolLabel} ${contentText}`;
|
|
2031
|
+
rightStr = fg("muted", elapsed);
|
|
2032
|
+
} else {
|
|
2033
|
+
const spin = entry.active ? thinkingSpinnerFrame() : void 0;
|
|
2034
|
+
const icon = spin?.frame ?? "\u2713";
|
|
2035
|
+
const tokenLabel = thinkingTokenLabel(entry);
|
|
2036
|
+
const tText = entry.text.trim().replace(/[*_#`>]+/g, "").replace(/\s+/g, " ");
|
|
2037
|
+
const thinkingLabel = fg("toolTitle", "thinking");
|
|
2038
|
+
const contentText = fg("thinkingText", tText || "(thinking...)");
|
|
2039
|
+
leftContent = `${prefix}${fg(spin?.color ?? "thinkingText", icon)} ${thinkingLabel} ${contentText}`;
|
|
2040
|
+
rightStr = fg("muted", tokenLabel);
|
|
2041
|
+
}
|
|
2042
|
+
const maxLeft = Math.max(1, innerWidth - visibleWidth4(rightStr) - 1);
|
|
2043
|
+
const ellipsis = entry.kind === "thinking" ? fg("thinkingText", "...") : fg("dim", "...");
|
|
2044
|
+
const leftTrunc = truncateToWidth5(leftContent, maxLeft, ellipsis);
|
|
2045
|
+
const gap = Math.max(1, innerWidth - visibleWidth4(leftTrunc) - visibleWidth4(rightStr));
|
|
2046
|
+
let rowLine = `${leftTrunc}${" ".repeat(gap)}${rightStr}`;
|
|
2047
|
+
const rowVis = visibleWidth4(rowLine);
|
|
2048
|
+
if (rowVis < innerWidth) {
|
|
2049
|
+
rowLine += " ".repeat(innerWidth - rowVis);
|
|
2050
|
+
} else if (rowVis > innerWidth) {
|
|
2051
|
+
rowLine = truncateToWidth5(rowLine, innerWidth);
|
|
2052
|
+
}
|
|
2053
|
+
if (isSelected) {
|
|
2054
|
+
if (theme?.bg) {
|
|
2055
|
+
rowLine = theme.bg("selectedBg", rowLine);
|
|
2056
|
+
} else if (theme?.inverse) {
|
|
2057
|
+
rowLine = theme.inverse(rowLine);
|
|
2058
|
+
}
|
|
2059
|
+
}
|
|
2060
|
+
lines.push(borderFg("\u2502 ") + rowLine + borderFg(" \u2502"));
|
|
2061
|
+
}
|
|
2062
|
+
}
|
|
2063
|
+
const countInfo = this.entries.length > 0 ? ` [${this.selectedIndex + 1}/${this.entries.length}] ` : "";
|
|
2064
|
+
const visCount = visibleWidth4(countInfo);
|
|
2065
|
+
const botFiller = Math.max(1, width - 2 - visCount);
|
|
2066
|
+
const botBorder = borderFg("\u2514" + "\u2500".repeat(botFiller)) + (countInfo ? fg("dim", countInfo) : "") + borderFg("\u2518");
|
|
2067
|
+
lines.push(truncateToWidth5(botBorder, width));
|
|
2068
|
+
return lines;
|
|
2069
|
+
}
|
|
2070
|
+
dispose() {
|
|
2071
|
+
}
|
|
2072
|
+
invalidate() {
|
|
2073
|
+
}
|
|
2074
|
+
};
|
|
2075
|
+
function openInspectSelectorModal() {
|
|
2076
|
+
if (runtime.activeModalHandle) {
|
|
2077
|
+
closeDetailModal();
|
|
2078
|
+
}
|
|
2079
|
+
closeInspectSelectorModal();
|
|
2080
|
+
const modal = new InspectSelectorModal();
|
|
2081
|
+
runtime.activeSelectorComponent = modal;
|
|
2082
|
+
if (runtime.capturedTui) {
|
|
2083
|
+
runtime.activeSelectorHandle = runtime.capturedTui.showOverlay(modal, {
|
|
2084
|
+
anchor: "center",
|
|
2085
|
+
width: "85%",
|
|
2086
|
+
maxHeight: "70%"
|
|
2087
|
+
});
|
|
2088
|
+
runtime.capturedTui.requestRender();
|
|
2089
|
+
return;
|
|
2090
|
+
}
|
|
2091
|
+
if (runtime.activeUIContext?.custom) {
|
|
2092
|
+
runtime.activeUIContext.custom(
|
|
2093
|
+
(tui, _theme, _keybindings, done) => {
|
|
2094
|
+
runtime.capturedTui = tui;
|
|
2095
|
+
runtime.activeSelectorCloseFn = done;
|
|
2096
|
+
return modal;
|
|
2097
|
+
},
|
|
2098
|
+
{
|
|
2099
|
+
overlay: true,
|
|
2100
|
+
overlayOptions: { anchor: "center", width: "85%", maxHeight: "70%" },
|
|
2101
|
+
onHandle: (handle) => {
|
|
2102
|
+
runtime.activeSelectorHandle = handle;
|
|
2103
|
+
}
|
|
2104
|
+
}
|
|
2105
|
+
);
|
|
2106
|
+
}
|
|
2107
|
+
}
|
|
2108
|
+
function closeInspectSelectorModal() {
|
|
2109
|
+
if (runtime.activeSelectorHandle) {
|
|
2110
|
+
const handle = runtime.activeSelectorHandle;
|
|
2111
|
+
runtime.activeSelectorHandle = null;
|
|
2112
|
+
handle.hide();
|
|
2113
|
+
}
|
|
2114
|
+
if (runtime.activeSelectorCloseFn) {
|
|
2115
|
+
const done = runtime.activeSelectorCloseFn;
|
|
2116
|
+
runtime.activeSelectorCloseFn = null;
|
|
2117
|
+
done();
|
|
2118
|
+
}
|
|
2119
|
+
if (runtime.activeSelectorComponent) {
|
|
2120
|
+
runtime.activeSelectorComponent = null;
|
|
2121
|
+
}
|
|
2122
|
+
runtime.capturedTui?.requestRender?.();
|
|
2123
|
+
}
|
|
2124
|
+
|
|
2125
|
+
// src/extension.ts
|
|
2126
|
+
import { isKeyRelease as isKeyRelease2, matchesKey as matchesKey4, Key as Key3 } from "@earendil-works/pi-tui";
|
|
2127
|
+
|
|
2128
|
+
// src/settings.ts
|
|
2129
|
+
import { getSettingsListTheme } from "@earendil-works/pi-coding-agent";
|
|
2130
|
+
import { Key as Key2, SettingsList, matchesKey as matchesKey3, truncateToWidth as truncateToWidth6 } from "@earendil-works/pi-tui";
|
|
2131
|
+
var CONFIG_KEYS = [
|
|
2132
|
+
{
|
|
2133
|
+
id: "expandedToolLines",
|
|
2134
|
+
label: "Expanded tool lines",
|
|
2135
|
+
description: "Result lines shown per tool when expanded",
|
|
2136
|
+
min: 1,
|
|
2137
|
+
max: 50,
|
|
2138
|
+
step: 1
|
|
2139
|
+
},
|
|
2140
|
+
{
|
|
2141
|
+
id: "expandedThinkingLines",
|
|
2142
|
+
label: "Expanded thinking lines",
|
|
2143
|
+
description: "Thinking lines shown when expanded",
|
|
2144
|
+
min: 1,
|
|
2145
|
+
max: 100,
|
|
2146
|
+
step: 1
|
|
2147
|
+
},
|
|
2148
|
+
{
|
|
2149
|
+
id: "maxGroupEntries",
|
|
2150
|
+
label: "Max collapsed entries",
|
|
2151
|
+
description: "Maximum entries shown in collapsed group",
|
|
2152
|
+
min: 1,
|
|
2153
|
+
max: 30,
|
|
2154
|
+
step: 1
|
|
2155
|
+
}
|
|
2156
|
+
];
|
|
2157
|
+
function makeStepper(title, initial, meta, theme, done) {
|
|
2158
|
+
let value = initial;
|
|
2159
|
+
let cachedWidth;
|
|
2160
|
+
let cachedLines;
|
|
2161
|
+
const fg = (color, t) => theme?.fg?.(color, t) ?? t;
|
|
2162
|
+
return {
|
|
2163
|
+
render(width) {
|
|
2164
|
+
if (cachedLines && cachedWidth === width) return cachedLines;
|
|
2165
|
+
const barLen = Math.max(1, Math.min(width - 10, 40));
|
|
2166
|
+
const ratio = (value - meta.min) / Math.max(1, meta.max - meta.min);
|
|
2167
|
+
const filled = Math.round(ratio * barLen);
|
|
2168
|
+
const bar = "\u2588".repeat(filled) + "\u2591".repeat(Math.max(0, barLen - filled));
|
|
2169
|
+
const titleText = theme?.bold ? theme.bold(title) : title;
|
|
2170
|
+
cachedLines = [
|
|
2171
|
+
fg("accent", titleText),
|
|
2172
|
+
"",
|
|
2173
|
+
` ${fg("accent", String(value))}`,
|
|
2174
|
+
` ${fg("muted", bar)}`,
|
|
2175
|
+
"",
|
|
2176
|
+
fg("dim", " \u25C0 \u25B6 / \u2212 + adjust Enter save Esc cancel")
|
|
2177
|
+
].map((line) => truncateToWidth6(line, Math.max(1, width)));
|
|
2178
|
+
cachedWidth = width;
|
|
2179
|
+
return cachedLines;
|
|
2180
|
+
},
|
|
2181
|
+
handleInput(data) {
|
|
2182
|
+
if (matchesKey3(data, Key2.left) || matchesKey3(data, Key2.down) || data === "-" || data === "_") {
|
|
2183
|
+
value = Math.max(meta.min, value - meta.step);
|
|
2184
|
+
} else if (matchesKey3(data, Key2.right) || matchesKey3(data, Key2.up) || data === "+" || data === "=") {
|
|
2185
|
+
value = Math.min(meta.max, value + meta.step);
|
|
2186
|
+
} else if (matchesKey3(data, Key2.enter) || matchesKey3(data, Key2.space)) {
|
|
2187
|
+
done(String(value));
|
|
2188
|
+
return;
|
|
2189
|
+
} else if (matchesKey3(data, Key2.escape)) {
|
|
2190
|
+
done(void 0);
|
|
2191
|
+
return;
|
|
2192
|
+
}
|
|
2193
|
+
cachedWidth = void 0;
|
|
2194
|
+
},
|
|
2195
|
+
invalidate() {
|
|
2196
|
+
cachedWidth = void 0;
|
|
2197
|
+
}
|
|
2198
|
+
};
|
|
2199
|
+
}
|
|
2200
|
+
function registerConfigCommand(pi) {
|
|
2201
|
+
pi.registerCommand("compact-ui-config", {
|
|
2202
|
+
description: "Interactive compact-ui settings (arrows to select, Enter to adjust, Esc to close)",
|
|
2203
|
+
handler: async (_args, ctx) => {
|
|
2204
|
+
if (!ctx.hasUI) {
|
|
2205
|
+
ctx.ui.notify(
|
|
2206
|
+
`compact: expandedToolLines=${config.expandedToolLines}, expandedThinkingLines=${config.expandedThinkingLines}`,
|
|
2207
|
+
"info"
|
|
2208
|
+
);
|
|
2209
|
+
return;
|
|
2210
|
+
}
|
|
2211
|
+
const changed = await ctx.ui.custom((tui, theme, _keybindings, done) => {
|
|
2212
|
+
let anyChanged = false;
|
|
2213
|
+
const items = CONFIG_KEYS.map((meta) => ({
|
|
2214
|
+
id: meta.id,
|
|
2215
|
+
label: meta.label,
|
|
2216
|
+
currentValue: String(config[meta.id]),
|
|
2217
|
+
description: meta.description,
|
|
2218
|
+
submenu: (currentValue, subDone) => makeStepper(meta.label, Number(currentValue), meta, theme, subDone)
|
|
2219
|
+
}));
|
|
2220
|
+
const settingsList = new SettingsList(
|
|
2221
|
+
items,
|
|
2222
|
+
Math.min(items.length, 15),
|
|
2223
|
+
getSettingsListTheme(),
|
|
2224
|
+
(id, newValue) => {
|
|
2225
|
+
config[id] = Number(newValue);
|
|
2226
|
+
saveConfig();
|
|
2227
|
+
anyChanged = true;
|
|
2228
|
+
for (const g of runtime.groups) g.invalidate();
|
|
2229
|
+
},
|
|
2230
|
+
() => done(anyChanged)
|
|
2231
|
+
);
|
|
2232
|
+
return {
|
|
2233
|
+
render(width) {
|
|
2234
|
+
return settingsList.render(width);
|
|
2235
|
+
},
|
|
2236
|
+
invalidate() {
|
|
2237
|
+
settingsList.invalidate();
|
|
2238
|
+
},
|
|
2239
|
+
handleInput(data) {
|
|
2240
|
+
settingsList.handleInput?.(data);
|
|
2241
|
+
tui.requestRender();
|
|
2242
|
+
}
|
|
2243
|
+
};
|
|
2244
|
+
});
|
|
2245
|
+
if (changed) {
|
|
2246
|
+
ctx.ui.notify("compact-ui settings saved", "info");
|
|
2247
|
+
}
|
|
2248
|
+
}
|
|
2249
|
+
});
|
|
2250
|
+
}
|
|
2251
|
+
|
|
2252
|
+
// src/extension.ts
|
|
2253
|
+
function extension_default(pi) {
|
|
2254
|
+
installGrouping();
|
|
2255
|
+
installNativeThinkingSuppression();
|
|
2256
|
+
installCompactionSummaryRendering();
|
|
2257
|
+
pi.registerCommand("compact-inspect", {
|
|
2258
|
+
description: "Select a tool call or thinking entry to inspect details (Shortcut: Ctrl+I)",
|
|
2259
|
+
handler: async () => {
|
|
2260
|
+
openInspectSelectorModal();
|
|
2261
|
+
}
|
|
2262
|
+
});
|
|
2263
|
+
pi.on("session_start", async (_event, ctx) => {
|
|
2264
|
+
runtime.currentTheme = ctx.ui.theme;
|
|
2265
|
+
runtime.activeUIContext = ctx.ui;
|
|
2266
|
+
ctx.ui.setHiddenThinkingLabel("");
|
|
2267
|
+
ctx.ui.onTerminalInput((data) => {
|
|
2268
|
+
if (isKeyRelease2(data)) {
|
|
2269
|
+
return runtime.activeModalHandle || runtime.activeSelectorHandle ? { consume: true } : void 0;
|
|
2270
|
+
}
|
|
2271
|
+
if (runtime.activeModalHandle) {
|
|
2272
|
+
if (matchesKey4(data, Key3.escape) || data === "q" || data === "Q" || isCtrlI(data)) {
|
|
2273
|
+
closeDetailModal();
|
|
2274
|
+
return { consume: true };
|
|
2275
|
+
}
|
|
2276
|
+
return void 0;
|
|
2277
|
+
}
|
|
2278
|
+
if (runtime.activeSelectorHandle) {
|
|
2279
|
+
if (matchesKey4(data, Key3.escape) || data === "q" || data === "Q" || isCtrlI(data)) {
|
|
2280
|
+
closeInspectSelectorModal();
|
|
2281
|
+
return { consume: true };
|
|
2282
|
+
}
|
|
2283
|
+
return void 0;
|
|
2284
|
+
}
|
|
2285
|
+
if (isCtrlI(data)) {
|
|
2286
|
+
openInspectSelectorModal();
|
|
2287
|
+
return { consume: true };
|
|
2288
|
+
}
|
|
2289
|
+
return void 0;
|
|
2290
|
+
});
|
|
2291
|
+
ctx.ui.setWidget("compact-anim", (tui) => {
|
|
2292
|
+
runtime.capturedTui = tui;
|
|
2293
|
+
return { render: () => [], invalidate() {
|
|
2294
|
+
} };
|
|
2295
|
+
});
|
|
2296
|
+
installGrouping();
|
|
2297
|
+
installNativeThinkingSuppression();
|
|
2298
|
+
installCompactionSummaryRendering();
|
|
2299
|
+
if (ctx?.sessionManager?.getEntries) {
|
|
2300
|
+
loadSessionToolTimings(ctx.sessionManager.getEntries());
|
|
2301
|
+
}
|
|
2302
|
+
queueMicrotask(() => {
|
|
2303
|
+
for (const g of runtime.groups) {
|
|
2304
|
+
g.sealed = true;
|
|
2305
|
+
for (const t of g.children) {
|
|
2306
|
+
if (t.result && t._groupEndAt === void 0) {
|
|
2307
|
+
t._groupEndAt = Date.now();
|
|
2308
|
+
}
|
|
2309
|
+
}
|
|
2310
|
+
g.invalidate();
|
|
2311
|
+
}
|
|
2312
|
+
runtime.lastActiveGroup = null;
|
|
2313
|
+
if (runtime.animTimer) {
|
|
2314
|
+
clearTimeout(runtime.animTimer);
|
|
2315
|
+
runtime.animTimer = null;
|
|
2316
|
+
}
|
|
2317
|
+
runtime.capturedTui?.requestRender?.();
|
|
2318
|
+
});
|
|
2319
|
+
});
|
|
2320
|
+
pi.on("session_shutdown", async () => {
|
|
2321
|
+
closeDetailModal();
|
|
2322
|
+
closeInspectSelectorModal();
|
|
2323
|
+
});
|
|
2324
|
+
pi.on("tool_execution_start", async (event) => {
|
|
2325
|
+
rememberToolStart(event.toolCallId);
|
|
2326
|
+
runtime.lastActiveGroup?.invalidate();
|
|
2327
|
+
runtime.activeSelectorComponent?.refreshEntries();
|
|
2328
|
+
runtime.capturedTui?.requestRender?.();
|
|
2329
|
+
});
|
|
2330
|
+
pi.on("tool_execution_end", async (event) => {
|
|
2331
|
+
const end = Date.now();
|
|
2332
|
+
const start = runtime.toolStarts.get(event.toolCallId);
|
|
2333
|
+
if (start !== void 0 && !runtime.toolDurations.has(event.toolCallId)) {
|
|
2334
|
+
runtime.toolDurations.set(event.toolCallId, Math.max(0, (end - start) / 1e3));
|
|
2335
|
+
}
|
|
2336
|
+
for (const g of runtime.groups) {
|
|
2337
|
+
for (const t of g.children) {
|
|
2338
|
+
if (t.toolCallId === event.toolCallId) t._groupEndAt = end;
|
|
2339
|
+
}
|
|
2340
|
+
}
|
|
2341
|
+
runtime.lastActiveGroup?.invalidate();
|
|
2342
|
+
runtime.activeSelectorComponent?.refreshEntries();
|
|
2343
|
+
runtime.capturedTui?.requestRender?.();
|
|
2344
|
+
});
|
|
2345
|
+
pi.on("message_start", async (event) => {
|
|
2346
|
+
const role = event.message?.role;
|
|
2347
|
+
if (role === "user" && runtime.lastActiveGroup && !runtime.lastActiveGroup.sealed) {
|
|
2348
|
+
runtime.lastActiveGroup.sealed = true;
|
|
2349
|
+
finalizeActiveThinking();
|
|
2350
|
+
runtime.lastActiveGroup.invalidate();
|
|
2351
|
+
}
|
|
2352
|
+
if (role === "user") {
|
|
2353
|
+
for (const g of [...runtime.groups]) {
|
|
2354
|
+
if (!g.hasVisibleEntries()) {
|
|
2355
|
+
if (runtime.lastChatContainer) removeGroupFromContainer(runtime.lastChatContainer, g);
|
|
2356
|
+
runtime.groups.delete(g);
|
|
2357
|
+
}
|
|
2358
|
+
}
|
|
2359
|
+
runtime.turnStartMs = Date.now();
|
|
2360
|
+
runtime.activeThinking = null;
|
|
2361
|
+
runtime.activeThinkingIndex = null;
|
|
2362
|
+
runtime.activeThinkingBlocks.clear();
|
|
2363
|
+
runtime.unattachedThinking.length = 0;
|
|
2364
|
+
clearPreparingTools();
|
|
2365
|
+
runtime.lastSealedThinkingEntry = null;
|
|
2366
|
+
runtime.lastReportedReasoningTokens = 0;
|
|
2367
|
+
runtime.handledTextIndexes.clear();
|
|
2368
|
+
runtime.sealedThinkingIndexes.clear();
|
|
2369
|
+
runtime.pendingTextSeal = false;
|
|
2370
|
+
runtime.pendingTextOrdinal = null;
|
|
2371
|
+
runtime.lastStreamingComp = null;
|
|
2372
|
+
} else if (role === "assistant") {
|
|
2373
|
+
finalizeActiveThinking();
|
|
2374
|
+
runtime.handledTextIndexes.clear();
|
|
2375
|
+
runtime.sealedThinkingIndexes.clear();
|
|
2376
|
+
runtime.lastSealedThinkingEntry = null;
|
|
2377
|
+
runtime.lastReportedReasoningTokens = 0;
|
|
2378
|
+
runtime.pendingTextSeal = false;
|
|
2379
|
+
runtime.pendingTextOrdinal = null;
|
|
2380
|
+
runtime.lastStreamingComp = null;
|
|
2381
|
+
}
|
|
2382
|
+
});
|
|
2383
|
+
pi.on("message_update", async (event) => {
|
|
2384
|
+
const msg = event.message;
|
|
2385
|
+
if (!msg || msg.role !== "assistant") return;
|
|
2386
|
+
const content = Array.isArray(msg.content) ? msg.content : [];
|
|
2387
|
+
const streamEvent = event.assistantMessageEvent;
|
|
2388
|
+
const streamType = String(streamEvent?.type ?? "");
|
|
2389
|
+
if (streamType.startsWith("thinking_")) {
|
|
2390
|
+
const contentIndex = Number(streamEvent.contentIndex);
|
|
2391
|
+
const hasIndex = Number.isInteger(contentIndex);
|
|
2392
|
+
if (hasIndex && runtime.sealedThinkingIndexes.has(contentIndex)) return;
|
|
2393
|
+
const startsRun = streamType === "thinking_start" || !runtime.activeThinking || hasIndex && runtime.activeThinkingIndex !== contentIndex;
|
|
2394
|
+
if (startsRun) {
|
|
2395
|
+
finalizeActiveThinking();
|
|
2396
|
+
runtime.activeThinkingIndex = hasIndex ? contentIndex : null;
|
|
2397
|
+
createThinkingEntry();
|
|
2398
|
+
}
|
|
2399
|
+
const block = hasIndex ? content[contentIndex] : void 0;
|
|
2400
|
+
const blockText = block?.type === "thinking" ? String(block.thinking ?? "") : streamType === "thinking_end" ? String(streamEvent.content ?? "") : "";
|
|
2401
|
+
if (hasIndex) runtime.activeThinkingBlocks.set(contentIndex, blockText);
|
|
2402
|
+
updateActiveThinkingTokens(msg);
|
|
2403
|
+
ensureThinkingGroup();
|
|
2404
|
+
if (streamType === "thinking_end") finalizeActiveThinking();
|
|
2405
|
+
} else if (streamType.startsWith("text_")) {
|
|
2406
|
+
const contentIndex = Number(streamEvent.contentIndex);
|
|
2407
|
+
const block = Number.isInteger(contentIndex) ? content[contentIndex] : void 0;
|
|
2408
|
+
const text = block?.type === "text" ? String(block.text ?? "").trim() : "";
|
|
2409
|
+
if (text.length > 0 && !runtime.handledTextIndexes.has(contentIndex)) {
|
|
2410
|
+
if (runtime.activeThinking) updateActiveThinkingTokens(msg);
|
|
2411
|
+
runtime.handledTextIndexes.add(contentIndex);
|
|
2412
|
+
runtime.pendingTextSeal = true;
|
|
2413
|
+
runtime.pendingTextOrdinal = content.slice(0, Number.isInteger(contentIndex) ? contentIndex + 1 : content.length).filter((item) => item?.type === "text" && String(item.text ?? "").trim()).length - 1;
|
|
2414
|
+
flushPendingTextSeal();
|
|
2415
|
+
}
|
|
2416
|
+
} else if (streamType.startsWith("toolcall_")) {
|
|
2417
|
+
if (runtime.activeThinking) updateActiveThinkingTokens(msg);
|
|
2418
|
+
noteStreamingToolCall(content, streamEvent);
|
|
2419
|
+
} else if (streamType === "done" || streamType === "error") {
|
|
2420
|
+
if (runtime.activeThinking) updateActiveThinkingTokens(msg);
|
|
2421
|
+
finalizeActiveThinking();
|
|
2422
|
+
refreshSealedThinkingTokens(msg?.usage?.reasoning);
|
|
2423
|
+
}
|
|
2424
|
+
runtime.lastActiveGroup?.invalidate();
|
|
2425
|
+
runtime.activeSelectorComponent?.refreshEntries();
|
|
2426
|
+
runtime.capturedTui?.requestRender?.();
|
|
2427
|
+
});
|
|
2428
|
+
pi.on("agent_end", async () => {
|
|
2429
|
+
clearPreparingTools();
|
|
2430
|
+
if (runtime.lastActiveGroup && !runtime.lastActiveGroup.sealed) {
|
|
2431
|
+
runtime.lastActiveGroup.sealed = true;
|
|
2432
|
+
finalizeActiveThinking();
|
|
2433
|
+
runtime.lastActiveGroup.invalidate();
|
|
2434
|
+
}
|
|
2435
|
+
for (const g of [...runtime.groups]) {
|
|
2436
|
+
if (!g.hasVisibleEntries()) {
|
|
2437
|
+
if (runtime.lastChatContainer) removeGroupFromContainer(runtime.lastChatContainer, g);
|
|
2438
|
+
runtime.groups.delete(g);
|
|
2439
|
+
}
|
|
2440
|
+
}
|
|
2441
|
+
refreshSealedThinkingTokens();
|
|
2442
|
+
runtime.activeSelectorComponent?.refreshEntries();
|
|
2443
|
+
runtime.capturedTui?.requestRender?.();
|
|
2444
|
+
const elapsedMs = Date.now() - runtime.turnStartMs;
|
|
2445
|
+
insertTurnDivider(elapsedMs);
|
|
2446
|
+
runtime.activeThinking = null;
|
|
2447
|
+
runtime.activeThinkingIndex = null;
|
|
2448
|
+
runtime.activeThinkingBlocks.clear();
|
|
2449
|
+
runtime.unattachedThinking.length = 0;
|
|
2450
|
+
runtime.lastSealedThinkingEntry = null;
|
|
2451
|
+
runtime.lastReportedReasoningTokens = 0;
|
|
2452
|
+
runtime.handledTextIndexes.clear();
|
|
2453
|
+
runtime.sealedThinkingIndexes.clear();
|
|
2454
|
+
runtime.pendingTextSeal = false;
|
|
2455
|
+
runtime.pendingTextOrdinal = null;
|
|
2456
|
+
});
|
|
2457
|
+
registerConfigCommand(pi);
|
|
2458
|
+
}
|
|
2459
|
+
export {
|
|
2460
|
+
extension_default as default,
|
|
2461
|
+
getCompactMarkdownTheme,
|
|
2462
|
+
normalizeCompactCodeBlockLines
|
|
2463
|
+
};
|
|
2464
|
+
//# sourceMappingURL=index.js.map
|