pi-compact-transcript 0.2.1 → 0.5.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/README.md +32 -8
- package/extensions/compact-transcript.ts +624 -216
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,11 +1,23 @@
|
|
|
1
1
|
# pi-compact-transcript
|
|
2
2
|
|
|
3
|
-
A compact transcript extension for [pi](https://pi.dev)
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
3
|
+
A compact transcript extension for [pi](https://pi.dev).
|
|
4
|
+
|
|
5
|
+
| Without the extension | With the extension |
|
|
6
|
+
|---|---|
|
|
7
|
+
|  |  |
|
|
8
|
+
|
|
9
|
+
What it does:
|
|
10
|
+
|
|
11
|
+
- Collapses every tool call/result into a one-line preview, including custom/external tools added by other extensions.
|
|
12
|
+
- Each tool row carries a status diamond: blinking gray `◆` while the tool runs, solid green `◆` on success, solid red `◆` on failure.
|
|
13
|
+
- Consecutive uses of the same tool coalesce into a single row, e.g. `◆ 4× read src/foo.ts {12 lines · 8s}`; a different tool starts a new row with its own diamond.
|
|
14
|
+
- Tool rows show durations when they take a second or longer; grouped rows show the total.
|
|
15
|
+
- Failed tools always get their own visible row (red diamond) and end the current burst.
|
|
16
|
+
- Tool rows render dimmed so assistant text stands out.
|
|
17
|
+
- Hidden thinking blocks are suppressed entirely — no `Thinking...` markers.
|
|
18
|
+
- Each agent run ends with a one-line summary, e.g. `Read 6 files, edited 2, ran 3 commands, 1 failed · 42s`.
|
|
19
|
+
- Unknown tools preview their most meaningful string argument (command, code, query, path, url, ...) instead of dumping raw JSON args.
|
|
20
|
+
- Expanded tool output still falls back to pi's original renderer, so you can use pi's normal tool expansion when details matter.
|
|
9
21
|
- Minimizes vertical space so long agent runs do not scroll away as quickly.
|
|
10
22
|
|
|
11
23
|
## Install from GitHub
|
|
@@ -47,8 +59,20 @@ The extension works best with hidden thinking and no output padding:
|
|
|
47
59
|
|
|
48
60
|
Set these in `~/.pi/agent/settings.json`, or use `/settings` in pi.
|
|
49
61
|
|
|
62
|
+
Thinking suppression only applies when `hideThinkingBlock` is on; with it off, pi renders thinking traces normally.
|
|
63
|
+
|
|
64
|
+
## Commands
|
|
65
|
+
|
|
66
|
+
```text
|
|
67
|
+
/compact-transcript # toggle on/off
|
|
68
|
+
/compact-transcript on|off # set explicitly
|
|
69
|
+
/compact-transcript status # show current state
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
Toggling applies to the visible transcript immediately — existing rows re-render, no reload needed. The pre-0.5 mode names (`balanced`, `aggressive`, `debug`, `disabled`) are accepted as legacy aliases for `on`/`off`.
|
|
73
|
+
|
|
50
74
|
## Notes
|
|
51
75
|
|
|
52
|
-
This extension
|
|
76
|
+
This extension changes display only. Tool execution is still handled by pi and any other extensions that registered or override tools.
|
|
53
77
|
|
|
54
|
-
|
|
78
|
+
For built-in and extension tools, compact rendering uses pi's public exported TUI components where available. The cross-tool burst compaction and thinking suppression still rely on pi's current TUI component internals, so if pi changes those internals in a future release, the extension falls back to pi's normal rendering behavior for the affected rows.
|
|
@@ -1,57 +1,134 @@
|
|
|
1
|
-
import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
2
|
-
import {
|
|
3
|
-
createBashTool,
|
|
4
|
-
createEditTool,
|
|
5
|
-
createFindTool,
|
|
6
|
-
createGrepTool,
|
|
7
|
-
createLsTool,
|
|
8
|
-
createReadTool,
|
|
9
|
-
createWriteTool,
|
|
10
|
-
} from "@earendil-works/pi-coding-agent";
|
|
1
|
+
import type { ExtensionAPI, ExtensionContext, Theme } from "@earendil-works/pi-coding-agent";
|
|
2
|
+
import { AssistantMessageComponent, ToolExecutionComponent } from "@earendil-works/pi-coding-agent";
|
|
11
3
|
import { Markdown, Spacer, Text } from "@earendil-works/pi-tui";
|
|
12
|
-
import { createRequire } from "node:module";
|
|
13
4
|
import { homedir } from "node:os";
|
|
14
|
-
import { dirname, join } from "node:path";
|
|
15
|
-
import { pathToFileURL } from "node:url";
|
|
16
5
|
|
|
17
|
-
|
|
6
|
+
// Older versions of this extension wrote a footer status under this key; it is
|
|
7
|
+
// kept only to clear that status once per session for users upgrading in place.
|
|
8
|
+
const STATUS_KEY = "compact-transcript";
|
|
9
|
+
const CONFIG_ENTRY_TYPE = "compact-transcript-config";
|
|
10
|
+
const SUMMARY_ENTRY_TYPE = "compact-transcript-summary";
|
|
11
|
+
|
|
12
|
+
const MIN_PREVIEW_WIDTH = 20;
|
|
13
|
+
const MAX_PREVIEW_WIDTH = 104;
|
|
14
|
+
// Leave room for pi's row gutter/padding so compact lines never wrap.
|
|
15
|
+
const PREVIEW_MARGIN = 6;
|
|
16
|
+
const BLINK_INTERVAL_MS = 400;
|
|
17
|
+
// Status marker is two cells wide ("◆ ").
|
|
18
|
+
const MARKER_WIDTH = 2;
|
|
19
|
+
|
|
20
|
+
type CompactTranscriptConfig = {
|
|
21
|
+
enabled: boolean;
|
|
22
|
+
};
|
|
18
23
|
|
|
19
24
|
type ToolInfo = {
|
|
20
25
|
id: string;
|
|
21
26
|
name: string;
|
|
22
27
|
args: any;
|
|
23
28
|
preview: string;
|
|
29
|
+
hidden?: boolean;
|
|
30
|
+
running?: boolean;
|
|
31
|
+
burstCount?: number;
|
|
32
|
+
startedAt?: number;
|
|
33
|
+
durationMs?: number;
|
|
34
|
+
burstDurationMs?: number;
|
|
24
35
|
result?: string;
|
|
36
|
+
isError?: boolean;
|
|
25
37
|
invalidate?: () => void;
|
|
26
38
|
};
|
|
27
39
|
|
|
28
|
-
|
|
29
|
-
|
|
40
|
+
type RunStats = {
|
|
41
|
+
startedAt: number;
|
|
42
|
+
toolCount: number;
|
|
43
|
+
readFiles: Set<string>;
|
|
44
|
+
editFiles: Set<string>;
|
|
45
|
+
commandCount: number;
|
|
46
|
+
otherCount: number;
|
|
47
|
+
failedCount: number;
|
|
48
|
+
};
|
|
30
49
|
|
|
31
|
-
|
|
32
|
-
|
|
50
|
+
type SummaryData = {
|
|
51
|
+
reads: number;
|
|
52
|
+
edits: number;
|
|
53
|
+
commands: number;
|
|
54
|
+
others: number;
|
|
55
|
+
failed: number;
|
|
56
|
+
durationMs: number;
|
|
57
|
+
};
|
|
33
58
|
|
|
34
|
-
|
|
59
|
+
type RuntimeState = {
|
|
60
|
+
config: CompactTranscriptConfig;
|
|
61
|
+
toolsById: Map<string, ToolInfo>;
|
|
62
|
+
currentBurst: ToolInfo[];
|
|
63
|
+
hiddenToolIds: Set<string>;
|
|
64
|
+
runningToolIds: Set<string>;
|
|
65
|
+
agentActive: boolean;
|
|
66
|
+
blinkOn: boolean;
|
|
67
|
+
blinkTimer?: ReturnType<typeof setInterval>;
|
|
68
|
+
runStats: RunStats;
|
|
69
|
+
// Live transcript components, so toggling can re-render existing rows.
|
|
70
|
+
toolComponents: Set<any>;
|
|
71
|
+
assistantComponents: Set<any>;
|
|
72
|
+
currentTheme?: Theme;
|
|
73
|
+
};
|
|
35
74
|
|
|
36
|
-
|
|
75
|
+
const DEFAULT_CONFIG: CompactTranscriptConfig = {
|
|
76
|
+
enabled: true,
|
|
77
|
+
};
|
|
78
|
+
|
|
79
|
+
const STATE_KEY = Symbol.for("pi-compact-transcript.state");
|
|
80
|
+
const TOOL_PATCH_KEY = Symbol.for("pi-compact-transcript.tool-patch");
|
|
81
|
+
const ASSISTANT_PATCH_KEY = Symbol.for("pi-compact-transcript.assistant-patch");
|
|
82
|
+
|
|
83
|
+
function newRunStats(): RunStats {
|
|
37
84
|
return {
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
85
|
+
startedAt: Date.now(),
|
|
86
|
+
toolCount: 0,
|
|
87
|
+
readFiles: new Set(),
|
|
88
|
+
editFiles: new Set(),
|
|
89
|
+
commandCount: 0,
|
|
90
|
+
otherCount: 0,
|
|
91
|
+
failedCount: 0,
|
|
45
92
|
};
|
|
46
93
|
}
|
|
47
94
|
|
|
48
|
-
function
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
95
|
+
function normalizeConfig(input: unknown): CompactTranscriptConfig {
|
|
96
|
+
const source = (input && typeof input === "object" ? input : {}) as Record<string, unknown>;
|
|
97
|
+
let enabled = DEFAULT_CONFIG.enabled;
|
|
98
|
+
if (typeof source.enabled === "boolean") {
|
|
99
|
+
enabled = source.enabled;
|
|
100
|
+
} else if (typeof source.mode === "string") {
|
|
101
|
+
// Pre-0.5 config persisted a mode string instead of an enabled flag.
|
|
102
|
+
enabled = source.mode !== "disabled" && source.mode !== "off";
|
|
53
103
|
}
|
|
54
|
-
return
|
|
104
|
+
return { enabled };
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function getState(): RuntimeState {
|
|
108
|
+
const globalWithState = globalThis as typeof globalThis & { [STATE_KEY]?: RuntimeState };
|
|
109
|
+
globalWithState[STATE_KEY] ??= {
|
|
110
|
+
config: { ...DEFAULT_CONFIG },
|
|
111
|
+
toolsById: new Map(),
|
|
112
|
+
currentBurst: [],
|
|
113
|
+
hiddenToolIds: new Set(),
|
|
114
|
+
runningToolIds: new Set(),
|
|
115
|
+
agentActive: false,
|
|
116
|
+
blinkOn: true,
|
|
117
|
+
runStats: newRunStats(),
|
|
118
|
+
toolComponents: new Set(),
|
|
119
|
+
assistantComponents: new Set(),
|
|
120
|
+
};
|
|
121
|
+
return globalWithState[STATE_KEY]!;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
const state = getState();
|
|
125
|
+
|
|
126
|
+
function isEnabled(): boolean {
|
|
127
|
+
return state.config.enabled;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
function isNonEmptyString(value: unknown): value is string {
|
|
131
|
+
return typeof value === "string" && value.trim().length > 0;
|
|
55
132
|
}
|
|
56
133
|
|
|
57
134
|
function shortenPath(path: unknown): string {
|
|
@@ -66,7 +143,11 @@ function oneLine(value: unknown): string {
|
|
|
66
143
|
.trim();
|
|
67
144
|
}
|
|
68
145
|
|
|
69
|
-
function
|
|
146
|
+
function previewWidth(base = process.stdout.columns || 100): number {
|
|
147
|
+
return Math.max(MIN_PREVIEW_WIDTH, Math.min(MAX_PREVIEW_WIDTH, base - PREVIEW_MARGIN));
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
function limitPlain(text: string, max = previewWidth()): string {
|
|
70
151
|
const clean = oneLine(text);
|
|
71
152
|
if (clean.length <= max) return clean;
|
|
72
153
|
return `${clean.slice(0, Math.max(0, max - 1))}…`;
|
|
@@ -76,6 +157,43 @@ function quote(s: string): string {
|
|
|
76
157
|
return JSON.stringify(s);
|
|
77
158
|
}
|
|
78
159
|
|
|
160
|
+
function safeJson(value: unknown): string {
|
|
161
|
+
try {
|
|
162
|
+
return JSON.stringify(value ?? {});
|
|
163
|
+
} catch {
|
|
164
|
+
return String(value);
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
// Sub-second durations render as "" so fast tools stay clutter-free.
|
|
169
|
+
function formatDuration(ms: number): string {
|
|
170
|
+
if (!Number.isFinite(ms) || ms < 1000) return "";
|
|
171
|
+
const totalSeconds = Math.round(ms / 1000);
|
|
172
|
+
if (totalSeconds < 60) return `${totalSeconds}s`;
|
|
173
|
+
const minutes = Math.floor(totalSeconds / 60);
|
|
174
|
+
const seconds = totalSeconds % 60;
|
|
175
|
+
return seconds ? `${minutes}m${seconds}s` : `${minutes}m`;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
// Checked in order; earlier keys are more likely to be the argument a human
|
|
179
|
+
// would recognize the call by.
|
|
180
|
+
const PREFERRED_ARG_KEYS = [
|
|
181
|
+
"command",
|
|
182
|
+
"code",
|
|
183
|
+
"query",
|
|
184
|
+
"pattern",
|
|
185
|
+
"path",
|
|
186
|
+
"file_path",
|
|
187
|
+
"filePath",
|
|
188
|
+
"file",
|
|
189
|
+
"url",
|
|
190
|
+
"prompt",
|
|
191
|
+
"text",
|
|
192
|
+
"description",
|
|
193
|
+
"name",
|
|
194
|
+
];
|
|
195
|
+
const PATH_ARG_KEYS = new Set(["path", "file_path", "filePath", "file"]);
|
|
196
|
+
|
|
79
197
|
function previewFor(name: string, args: any): string {
|
|
80
198
|
switch (name) {
|
|
81
199
|
case "bash":
|
|
@@ -106,41 +224,97 @@ function previewFor(name: string, args: any): string {
|
|
|
106
224
|
return `find ${args?.pattern ? quote(String(args.pattern)) : "..."} ${shortenPath(args?.path) || "."}`;
|
|
107
225
|
case "ls":
|
|
108
226
|
return `ls ${shortenPath(args?.path) || "."}`;
|
|
109
|
-
default:
|
|
110
|
-
|
|
227
|
+
default: {
|
|
228
|
+
// Unknown tools: show the most meaningful string argument instead of
|
|
229
|
+
// dumping the whole args object as JSON.
|
|
230
|
+
if (args && typeof args === "object") {
|
|
231
|
+
for (const key of PREFERRED_ARG_KEYS) {
|
|
232
|
+
const value = (args as Record<string, unknown>)[key];
|
|
233
|
+
if (isNonEmptyString(value)) {
|
|
234
|
+
const rendered = PATH_ARG_KEYS.has(key) ? shortenPath(value) : oneLine(value);
|
|
235
|
+
return `${name} ${rendered}`;
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
const firstString = Object.values(args).find(isNonEmptyString);
|
|
239
|
+
if (firstString) return `${name} ${oneLine(firstString)}`;
|
|
240
|
+
}
|
|
241
|
+
return `${name} ${safeJson(args ?? {})}`;
|
|
242
|
+
}
|
|
111
243
|
}
|
|
112
244
|
}
|
|
113
245
|
|
|
114
|
-
function resultPreview(result: any): string {
|
|
246
|
+
function resultPreview(result: any, isPartial = false): string {
|
|
115
247
|
const text = Array.isArray(result?.content)
|
|
116
248
|
? result.content.find((c: any) => c?.type === "text" && typeof c.text === "string")?.text
|
|
117
249
|
: undefined;
|
|
118
|
-
if (!text) return "";
|
|
250
|
+
if (!text) return isPartial ? "running" : "";
|
|
119
251
|
const lines = String(text).trim().split("\n").filter(Boolean);
|
|
120
|
-
if (lines.length === 0) return "";
|
|
252
|
+
if (lines.length === 0) return isPartial ? "running" : "";
|
|
121
253
|
if (lines.length === 1) return lines[0];
|
|
122
254
|
return `${lines.length} lines`;
|
|
123
255
|
}
|
|
124
256
|
|
|
125
|
-
function
|
|
126
|
-
|
|
257
|
+
function resetToolRun() {
|
|
258
|
+
state.toolsById = new Map();
|
|
259
|
+
state.currentBurst = [];
|
|
260
|
+
state.hiddenToolIds = new Set();
|
|
261
|
+
state.runningToolIds = new Set();
|
|
262
|
+
stopBlinkTimer();
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
function captureTheme(ctx: ExtensionContext) {
|
|
266
|
+
state.currentTheme = ctx.ui.theme;
|
|
127
267
|
}
|
|
128
268
|
|
|
129
|
-
function
|
|
130
|
-
|
|
269
|
+
function setToolHidden(info: ToolInfo, hidden: boolean) {
|
|
270
|
+
info.hidden = hidden;
|
|
271
|
+
if (hidden) state.hiddenToolIds.add(info.id);
|
|
272
|
+
else state.hiddenToolIds.delete(info.id);
|
|
131
273
|
}
|
|
132
274
|
|
|
133
|
-
function
|
|
134
|
-
|
|
135
|
-
|
|
275
|
+
function applyResult(info: ToolInfo, result: any, isError: boolean, isPartial: boolean) {
|
|
276
|
+
const suffix = resultPreview(result, isPartial);
|
|
277
|
+
if (suffix) info.result = suffix;
|
|
278
|
+
if (isError) {
|
|
279
|
+
info.isError = true;
|
|
280
|
+
// A failed tool always gets its own visible row, even if a burst had
|
|
281
|
+
// hidden it; render it as itself rather than as a burst summary.
|
|
282
|
+
info.burstCount = 1;
|
|
283
|
+
setToolHidden(info, false);
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
function ensureBlinkTimer() {
|
|
288
|
+
if (state.blinkTimer || state.runningToolIds.size === 0) return;
|
|
289
|
+
state.blinkTimer = setInterval(() => {
|
|
290
|
+
if (state.runningToolIds.size === 0) {
|
|
291
|
+
stopBlinkTimer();
|
|
292
|
+
return;
|
|
293
|
+
}
|
|
294
|
+
state.blinkOn = !state.blinkOn;
|
|
295
|
+
for (const id of state.runningToolIds) state.toolsById.get(id)?.invalidate?.();
|
|
296
|
+
}, BLINK_INTERVAL_MS);
|
|
297
|
+
state.blinkTimer.unref?.();
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
function stopBlinkTimer() {
|
|
301
|
+
if (state.blinkTimer) clearInterval(state.blinkTimer);
|
|
302
|
+
state.blinkTimer = undefined;
|
|
303
|
+
state.blinkOn = true;
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
function statusMarker(theme: Theme, opts: { running?: boolean; isError?: boolean; hasResult?: boolean }): string {
|
|
307
|
+
if (opts.isError) return theme.fg("error", "◆ ");
|
|
308
|
+
if (opts.running) return theme.fg("dim", state.blinkOn ? "◆ " : "◇ ");
|
|
309
|
+
if (opts.hasResult) return theme.fg("success", "◆ ");
|
|
310
|
+
return theme.fg("dim", "◆ ");
|
|
136
311
|
}
|
|
137
312
|
|
|
138
313
|
function upsertToolInfo(id: string, name: string, args: any, invalidate?: () => void): ToolInfo {
|
|
139
|
-
let info = toolsById.get(id);
|
|
314
|
+
let info = state.toolsById.get(id);
|
|
140
315
|
if (!info) {
|
|
141
316
|
info = { id, name, args, preview: previewFor(name, args) };
|
|
142
|
-
toolsById.set(id, info);
|
|
143
|
-
if (!currentTools.some((tool) => tool.id === id)) currentTools.push(info);
|
|
317
|
+
state.toolsById.set(id, info);
|
|
144
318
|
}
|
|
145
319
|
info.name = name;
|
|
146
320
|
info.args = args;
|
|
@@ -149,212 +323,446 @@ function upsertToolInfo(id: string, name: string, args: any, invalidate?: () =>
|
|
|
149
323
|
return info;
|
|
150
324
|
}
|
|
151
325
|
|
|
152
|
-
function
|
|
153
|
-
const
|
|
154
|
-
|
|
155
|
-
if (
|
|
326
|
+
function recordToolStart(name: string, args: any) {
|
|
327
|
+
const base = name.split(".").pop() ?? name;
|
|
328
|
+
state.runStats.toolCount++;
|
|
329
|
+
if (base === "read") {
|
|
330
|
+
if (isNonEmptyString(args?.path)) state.runStats.readFiles.add(args.path);
|
|
331
|
+
} else if (base === "edit" || base === "write") {
|
|
332
|
+
if (isNonEmptyString(args?.path)) state.runStats.editFiles.add(args.path);
|
|
333
|
+
} else if (base === "bash") {
|
|
334
|
+
state.runStats.commandCount++;
|
|
335
|
+
} else {
|
|
336
|
+
state.runStats.otherCount++;
|
|
337
|
+
}
|
|
338
|
+
}
|
|
156
339
|
|
|
157
|
-
|
|
340
|
+
function joinBurst(info: ToolInfo) {
|
|
341
|
+
const previous = state.currentBurst[state.currentBurst.length - 1];
|
|
342
|
+
|
|
343
|
+
if (!isEnabled()) {
|
|
344
|
+
state.currentBurst = [];
|
|
345
|
+
return;
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
// Bursts only group repeats of the same tool; a different tool starts a
|
|
349
|
+
// fresh row (and leaves the previous row visible with its count).
|
|
350
|
+
if (state.currentBurst.length && state.currentBurst[state.currentBurst.length - 1].name !== info.name) {
|
|
351
|
+
state.currentBurst = [];
|
|
352
|
+
}
|
|
158
353
|
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
354
|
+
if (!state.currentBurst.some((tool) => tool.id === info.id)) state.currentBurst.push(info);
|
|
355
|
+
for (const tool of state.currentBurst.slice(0, -1)) setToolHidden(tool, true);
|
|
356
|
+
info.burstCount = state.currentBurst.length;
|
|
357
|
+
previous?.invalidate?.();
|
|
163
358
|
}
|
|
164
359
|
|
|
165
|
-
function
|
|
166
|
-
const
|
|
167
|
-
|
|
360
|
+
function beginTool(id: string, name: string, args: any) {
|
|
361
|
+
const info = upsertToolInfo(id, name, args);
|
|
362
|
+
setToolHidden(info, false);
|
|
363
|
+
info.burstCount = 1;
|
|
364
|
+
info.running = true;
|
|
365
|
+
info.isError = false;
|
|
366
|
+
info.startedAt = Date.now();
|
|
367
|
+
state.runningToolIds.add(id);
|
|
368
|
+
ensureBlinkTimer();
|
|
369
|
+
recordToolStart(name, args);
|
|
370
|
+
joinBurst(info);
|
|
371
|
+
// The component may already be on screen from argument streaming; repaint
|
|
372
|
+
// now so the running marker and burst count appear immediately instead of
|
|
373
|
+
// waiting for the next result event or blink tick.
|
|
374
|
+
info.invalidate?.();
|
|
375
|
+
}
|
|
168
376
|
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
377
|
+
// A tool row rendered without a live tool_execution_start event — pi is
|
|
378
|
+
// rebuilding chat history after resume/reload. Reconstruct burst grouping so
|
|
379
|
+
// scrollback coalesces the same way the live view did, but skip timers,
|
|
380
|
+
// stats, and running state.
|
|
381
|
+
function hydrateTool(id: string, name: string, args: any, isError: boolean): ToolInfo {
|
|
382
|
+
const info = upsertToolInfo(id, name, args);
|
|
383
|
+
setToolHidden(info, false);
|
|
384
|
+
info.burstCount = 1;
|
|
385
|
+
if (isError) {
|
|
386
|
+
info.isError = true;
|
|
387
|
+
state.currentBurst = [];
|
|
388
|
+
return info;
|
|
173
389
|
}
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
return ` Next, I’ll run ${tools.length} tool calls, starting with ${preview}. I’ll use the latest result to continue, ending this batch around ${lastPreview}.`;
|
|
390
|
+
joinBurst(info);
|
|
391
|
+
return info;
|
|
177
392
|
}
|
|
178
393
|
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
394
|
+
function updateToolResult(toolCallId: string, result: any, isError = false, isPartial = false) {
|
|
395
|
+
if (!isPartial) {
|
|
396
|
+
state.runningToolIds.delete(toolCallId);
|
|
397
|
+
if (state.runningToolIds.size === 0) stopBlinkTimer();
|
|
398
|
+
}
|
|
399
|
+
const info = state.toolsById.get(toolCallId);
|
|
400
|
+
if (!info) return;
|
|
401
|
+
if (!isPartial) {
|
|
402
|
+
info.running = false;
|
|
403
|
+
if (info.startedAt) info.durationMs = Date.now() - info.startedAt;
|
|
404
|
+
if (state.currentBurst.includes(info) && state.currentBurst.length > 1) {
|
|
405
|
+
info.burstDurationMs = state.currentBurst.reduce((total, tool) => total + (tool.durationMs ?? 0), 0);
|
|
406
|
+
}
|
|
407
|
+
if (isError) state.runStats.failedCount++;
|
|
408
|
+
}
|
|
409
|
+
applyResult(info, result, isError, isPartial);
|
|
410
|
+
// A failure ends the current burst so the red row stays visible and the
|
|
411
|
+
// next tool starts a fresh count.
|
|
412
|
+
if (isError) state.currentBurst = [];
|
|
413
|
+
info.invalidate?.();
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
function compactToolLine(
|
|
417
|
+
toolCallId: string,
|
|
418
|
+
name: string,
|
|
419
|
+
args: any,
|
|
420
|
+
theme: Theme,
|
|
421
|
+
invalidate?: () => void,
|
|
422
|
+
result?: any,
|
|
423
|
+
isError = false,
|
|
424
|
+
isPartial = false,
|
|
425
|
+
): string {
|
|
426
|
+
if (!state.toolsById.has(toolCallId)) hydrateTool(toolCallId, name, args, isError);
|
|
427
|
+
const info = upsertToolInfo(toolCallId, name, args, invalidate);
|
|
428
|
+
applyResult(info, result, isError, isPartial);
|
|
429
|
+
if (info.hidden) return "";
|
|
430
|
+
|
|
431
|
+
const isBurst = (info.burstCount ?? 1) > 1;
|
|
432
|
+
const durationText = formatDuration((isBurst ? (info.burstDurationMs ?? info.durationMs) : info.durationMs) ?? 0);
|
|
433
|
+
const inner = [info.result ? oneLine(info.result) : "", durationText].filter(Boolean).join(" · ");
|
|
434
|
+
const status = inner ? ` {${inner}}` : info.running ? " {running}" : "";
|
|
435
|
+
const details = `${info.preview}${status}`;
|
|
436
|
+
const marker = statusMarker(theme, {
|
|
437
|
+
running: info.running,
|
|
438
|
+
isError: info.isError,
|
|
439
|
+
hasResult: result != null || !!info.result,
|
|
440
|
+
});
|
|
441
|
+
if (!isBurst) {
|
|
442
|
+
return marker + theme.fg("muted", limitPlain(details));
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
const prefix = `${info.burstCount}× `;
|
|
446
|
+
const budget = previewWidth((process.stdout.columns || 100) - prefix.length - MARKER_WIDTH);
|
|
447
|
+
return marker + theme.fg("muted", prefix + limitPlain(details, budget));
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
function patchToolExecutionComponent() {
|
|
451
|
+
const proto = ToolExecutionComponent.prototype as any;
|
|
452
|
+
if (typeof proto.updateDisplay !== "function" || typeof proto.render !== "function") return;
|
|
453
|
+
const existing = proto[TOOL_PATCH_KEY] as
|
|
454
|
+
| { originalUpdateDisplay: (...args: any[]) => any; originalRender: (...args: any[]) => any }
|
|
455
|
+
| undefined;
|
|
456
|
+
const originalUpdateDisplay = existing?.originalUpdateDisplay ?? proto.updateDisplay;
|
|
457
|
+
const originalRender = existing?.originalRender ?? proto.render;
|
|
458
|
+
|
|
459
|
+
proto.updateDisplay = function patchedUpdateDisplay() {
|
|
460
|
+
if (!this.toolCallId || !this.toolName || !this.selfRenderContainer || typeof this.selfRenderContainer.clear !== "function") {
|
|
461
|
+
this.__compactTranscriptForceSelf = false;
|
|
462
|
+
this.__compactTranscriptHidden = false;
|
|
463
|
+
return originalUpdateDisplay.call(this);
|
|
464
|
+
}
|
|
465
|
+
state.toolComponents.add(this);
|
|
466
|
+
|
|
467
|
+
const invalidate = () => {
|
|
468
|
+
this.invalidate();
|
|
469
|
+
this.ui?.requestRender?.();
|
|
470
|
+
};
|
|
471
|
+
// Must run before upsertToolInfo, which would make the row look
|
|
472
|
+
// already-known and skip burst reconstruction for rehydrated history.
|
|
473
|
+
if (!state.toolsById.has(this.toolCallId)) {
|
|
474
|
+
hydrateTool(this.toolCallId, this.toolName, this.args, this.result?.isError ?? false);
|
|
475
|
+
}
|
|
476
|
+
const info = upsertToolInfo(this.toolCallId, this.toolName, this.args, invalidate);
|
|
477
|
+
applyResult(info, this.result, this.result?.isError ?? false, this.isPartial);
|
|
478
|
+
|
|
479
|
+
if (!isEnabled() || this.expanded) {
|
|
480
|
+
setToolHidden(info, false);
|
|
481
|
+
this.__compactTranscriptForceSelf = false;
|
|
482
|
+
this.__compactTranscriptHidden = false;
|
|
483
|
+
return originalUpdateDisplay.call(this);
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
this.__compactTranscriptForceSelf = true;
|
|
487
|
+
this.__compactTranscriptHidden = false;
|
|
488
|
+
this.selfRenderContainer.clear();
|
|
489
|
+
for (const image of this.imageComponents ?? []) this.removeChild?.(image);
|
|
490
|
+
for (const spacer of this.imageSpacers ?? []) this.removeChild?.(spacer);
|
|
491
|
+
this.imageComponents = [];
|
|
492
|
+
this.imageSpacers = [];
|
|
493
|
+
|
|
494
|
+
const theme = state.currentTheme;
|
|
495
|
+
if (!theme) {
|
|
496
|
+
this.__compactTranscriptForceSelf = false;
|
|
497
|
+
this.__compactTranscriptHidden = false;
|
|
498
|
+
return originalUpdateDisplay.call(this);
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
const line = compactToolLine(
|
|
502
|
+
this.toolCallId,
|
|
503
|
+
this.toolName,
|
|
504
|
+
this.args,
|
|
505
|
+
theme,
|
|
506
|
+
invalidate,
|
|
507
|
+
this.result,
|
|
508
|
+
this.result?.isError ?? false,
|
|
509
|
+
this.isPartial,
|
|
189
510
|
);
|
|
190
|
-
const themeModule = await import(pathToFileURL(join(distDir, "modes/interactive/theme/theme.js")).href);
|
|
191
|
-
|
|
192
|
-
const ToolExecutionComponent = toolExecutionModule.ToolExecutionComponent;
|
|
193
|
-
if (ToolExecutionComponent?.prototype && !ToolExecutionComponent.prototype.__compactTranscriptPatched) {
|
|
194
|
-
const originalRender = ToolExecutionComponent.prototype.render;
|
|
195
|
-
ToolExecutionComponent.prototype.updateDisplay = function () {
|
|
196
|
-
this.__compactTranscriptForceSelf = true;
|
|
197
|
-
this.hideComponent = false;
|
|
198
|
-
this.selfRenderContainer.clear();
|
|
199
|
-
this.imageComponents = [];
|
|
200
|
-
this.imageSpacers = [];
|
|
201
|
-
|
|
202
|
-
const line = compactToolLine(
|
|
203
|
-
this.toolCallId,
|
|
204
|
-
this.toolName,
|
|
205
|
-
this.args,
|
|
206
|
-
themeModule.theme,
|
|
207
|
-
() => {
|
|
208
|
-
this.invalidate();
|
|
209
|
-
this.ui?.requestRender?.();
|
|
210
|
-
},
|
|
211
|
-
this.result,
|
|
212
|
-
);
|
|
213
|
-
|
|
214
|
-
if (!line) {
|
|
215
|
-
this.hideComponent = true;
|
|
216
|
-
return;
|
|
217
|
-
}
|
|
218
511
|
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
if (this.hideComponent) return [];
|
|
223
|
-
if (this.__compactTranscriptForceSelf) return this.selfRenderContainer.render(width);
|
|
224
|
-
return originalRender.call(this, width);
|
|
225
|
-
};
|
|
226
|
-
ToolExecutionComponent.prototype.__compactTranscriptPatched = true;
|
|
512
|
+
if (!line) {
|
|
513
|
+
this.__compactTranscriptHidden = true;
|
|
514
|
+
return;
|
|
227
515
|
}
|
|
228
516
|
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
const original = Component.prototype.updateContent;
|
|
517
|
+
this.selfRenderContainer.addChild(new Text(line, 0, 0));
|
|
518
|
+
};
|
|
232
519
|
|
|
233
|
-
|
|
234
|
-
|
|
520
|
+
proto.render = function patchedRender(width: number) {
|
|
521
|
+
if (this.hideComponent || this.__compactTranscriptHidden) return [];
|
|
522
|
+
if (this.__compactTranscriptForceSelf) return this.selfRenderContainer.render(width);
|
|
523
|
+
return originalRender.call(this, width);
|
|
524
|
+
};
|
|
235
525
|
|
|
236
|
-
|
|
237
|
-
|
|
526
|
+
proto[TOOL_PATCH_KEY] = { originalUpdateDisplay, originalRender };
|
|
527
|
+
}
|
|
238
528
|
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
529
|
+
function patchAssistantMessageComponent() {
|
|
530
|
+
const proto = AssistantMessageComponent.prototype as any;
|
|
531
|
+
if (typeof proto.updateContent !== "function") return;
|
|
532
|
+
const existing = proto[ASSISTANT_PATCH_KEY] as { originalUpdateContent: (...args: any[]) => any } | undefined;
|
|
533
|
+
const originalUpdateContent = existing?.originalUpdateContent ?? proto.updateContent;
|
|
243
534
|
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
let count = 1;
|
|
253
|
-
while (
|
|
254
|
-
message.content[i + count]?.type === "thinking" &&
|
|
255
|
-
message.content[i + count]?.thinking?.trim()
|
|
256
|
-
) {
|
|
257
|
-
count++;
|
|
258
|
-
}
|
|
259
|
-
i += count - 1;
|
|
260
|
-
|
|
261
|
-
const label = count > 1 ? `${LABEL} (${count}x)` : LABEL;
|
|
262
|
-
const summary = nextStepSummary(message.content, i);
|
|
263
|
-
this.contentContainer.addChild(
|
|
264
|
-
new Text(themeModule.theme.italic(themeModule.theme.fg("thinkingText", limitPlain(`${label}${summary}`))), this.outputPad, 0),
|
|
265
|
-
);
|
|
266
|
-
|
|
267
|
-
const hasVisibleAfter = message.content
|
|
268
|
-
.slice(i + 1)
|
|
269
|
-
.some((c: any) => (c.type === "text" && c.text?.trim()) || (c.type === "thinking" && c.thinking?.trim()));
|
|
270
|
-
if (hasVisibleAfter) this.contentContainer.addChild(new Spacer(1));
|
|
271
|
-
}
|
|
535
|
+
proto.updateContent = function patchedUpdateContent(message: any) {
|
|
536
|
+
state.assistantComponents.add(this);
|
|
537
|
+
if (!isEnabled() || !this.hideThinkingBlock || !Array.isArray(message?.content)) {
|
|
538
|
+
return originalUpdateContent.call(this, message);
|
|
539
|
+
}
|
|
540
|
+
if (!this.contentContainer || typeof this.contentContainer.clear !== "function") {
|
|
541
|
+
return originalUpdateContent.call(this, message);
|
|
542
|
+
}
|
|
272
543
|
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
//
|
|
278
|
-
|
|
544
|
+
this.lastMessage = message;
|
|
545
|
+
this.contentContainer.clear();
|
|
546
|
+
this.hasToolCalls = message.content.some((c: any) => c.type === "toolCall");
|
|
547
|
+
|
|
548
|
+
// Thinking blocks are suppressed entirely; only real text is rendered.
|
|
549
|
+
const texts = message.content.filter((c: any) => c.type === "text" && c.text?.trim());
|
|
550
|
+
if (texts.length === 0) return;
|
|
551
|
+
|
|
552
|
+
// Assistant text ends a tool burst. The live path also does this via
|
|
553
|
+
// message_update events; doing it here too keeps hydrated history from
|
|
554
|
+
// grouping tool rows across turn boundaries.
|
|
555
|
+
state.currentBurst = [];
|
|
556
|
+
|
|
557
|
+
this.contentContainer.addChild(new Spacer(1));
|
|
558
|
+
for (const content of texts) {
|
|
559
|
+
this.contentContainer.addChild(new Markdown(content.text.trim(), this.outputPad, 0, this.markdownTheme));
|
|
560
|
+
}
|
|
561
|
+
};
|
|
562
|
+
|
|
563
|
+
proto[ASSISTANT_PATCH_KEY] = { originalUpdateContent };
|
|
279
564
|
}
|
|
280
565
|
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
566
|
+
function patchRenderers() {
|
|
567
|
+
patchToolExecutionComponent();
|
|
568
|
+
patchAssistantMessageComponent();
|
|
569
|
+
}
|
|
284
570
|
|
|
285
|
-
|
|
286
|
-
|
|
571
|
+
// Re-render every transcript row we have touched so toggling applies to the
|
|
572
|
+
// visible transcript immediately instead of only to future rows.
|
|
573
|
+
function refreshTranscript() {
|
|
574
|
+
let ui: any;
|
|
575
|
+
for (const component of state.toolComponents) {
|
|
576
|
+
try {
|
|
577
|
+
// ToolExecutionComponent.invalidate() re-runs updateDisplay.
|
|
578
|
+
component.invalidate?.();
|
|
579
|
+
ui ??= component.ui;
|
|
580
|
+
} catch {
|
|
581
|
+
state.toolComponents.delete(component);
|
|
582
|
+
}
|
|
287
583
|
}
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
584
|
+
for (const component of state.assistantComponents) {
|
|
585
|
+
try {
|
|
586
|
+
if (component.lastMessage) component.updateContent?.(component.lastMessage);
|
|
587
|
+
component.invalidate?.();
|
|
588
|
+
} catch {
|
|
589
|
+
state.assistantComponents.delete(component);
|
|
590
|
+
}
|
|
291
591
|
}
|
|
592
|
+
ui?.requestRender?.();
|
|
593
|
+
}
|
|
594
|
+
|
|
595
|
+
function summaryLine(data: SummaryData): string {
|
|
596
|
+
const plural = (count: number) => (count === 1 ? "" : "s");
|
|
597
|
+
const parts: string[] = [];
|
|
598
|
+
if (data.reads) parts.push(`read ${data.reads} file${plural(data.reads)}`);
|
|
599
|
+
if (data.edits) parts.push(`edited ${data.edits} file${plural(data.edits)}`);
|
|
600
|
+
if (data.commands) parts.push(`ran ${data.commands} command${plural(data.commands)}`);
|
|
601
|
+
if (data.others) parts.push(`${data.others} other tool${plural(data.others)}`);
|
|
602
|
+
if (data.failed) parts.push(`${data.failed} failed`);
|
|
603
|
+
if (parts.length === 0) return "";
|
|
604
|
+
const text = parts.join(", ");
|
|
605
|
+
const capitalized = text[0].toUpperCase() + text.slice(1);
|
|
606
|
+
const duration = formatDuration(data.durationMs);
|
|
607
|
+
return duration ? `${capitalized} · ${duration}` : capitalized;
|
|
608
|
+
}
|
|
609
|
+
|
|
610
|
+
function normalizeSummary(input: unknown): SummaryData {
|
|
611
|
+
const source = (input && typeof input === "object" ? input : {}) as Record<string, unknown>;
|
|
612
|
+
const num = (value: unknown) => (typeof value === "number" && Number.isFinite(value) && value > 0 ? value : 0);
|
|
613
|
+
return {
|
|
614
|
+
reads: num(source.reads),
|
|
615
|
+
edits: num(source.edits),
|
|
616
|
+
commands: num(source.commands),
|
|
617
|
+
others: num(source.others),
|
|
618
|
+
failed: num(source.failed),
|
|
619
|
+
durationMs: num(source.durationMs),
|
|
620
|
+
};
|
|
621
|
+
}
|
|
292
622
|
|
|
293
|
-
|
|
294
|
-
|
|
623
|
+
function appendRunSummary(pi: ExtensionAPI) {
|
|
624
|
+
const stats = state.runStats;
|
|
625
|
+
// Single-tool runs are self-evident from the transcript; a summary line
|
|
626
|
+
// would just repeat the row above it.
|
|
627
|
+
if (!isEnabled() || stats.toolCount < 2) return;
|
|
628
|
+
const data: SummaryData = {
|
|
629
|
+
reads: stats.readFiles.size,
|
|
630
|
+
edits: stats.editFiles.size,
|
|
631
|
+
commands: stats.commandCount,
|
|
632
|
+
others: stats.otherCount,
|
|
633
|
+
failed: stats.failedCount,
|
|
634
|
+
durationMs: Date.now() - stats.startedAt,
|
|
635
|
+
};
|
|
636
|
+
pi.appendEntry(SUMMARY_ENTRY_TYPE, data);
|
|
637
|
+
}
|
|
638
|
+
|
|
639
|
+
function restoreConfigFromBranch(ctx: ExtensionContext) {
|
|
640
|
+
let nextConfig = { ...DEFAULT_CONFIG };
|
|
641
|
+
for (const entry of ctx.sessionManager.getBranch()) {
|
|
642
|
+
if (entry.type === "custom" && entry.customType === CONFIG_ENTRY_TYPE) {
|
|
643
|
+
nextConfig = normalizeConfig(entry.data);
|
|
644
|
+
}
|
|
295
645
|
}
|
|
646
|
+
state.config = nextConfig;
|
|
647
|
+
}
|
|
648
|
+
|
|
649
|
+
function setEnabled(enabled: boolean, pi: ExtensionAPI, ctx: ExtensionContext) {
|
|
650
|
+
state.config.enabled = enabled;
|
|
651
|
+
pi.appendEntry(CONFIG_ENTRY_TYPE, { ...state.config });
|
|
652
|
+
refreshTranscript();
|
|
653
|
+
ctx.ui.notify(`Compact transcript: ${enabled ? "on" : "off"}`, "info");
|
|
654
|
+
}
|
|
655
|
+
|
|
656
|
+
function parseBoolean(value: string | undefined): boolean | undefined {
|
|
657
|
+
if (!value) return undefined;
|
|
658
|
+
const normalized = value.trim().toLowerCase();
|
|
659
|
+
if (["on", "true", "yes", "1", "enabled"].includes(normalized)) return true;
|
|
660
|
+
if (["off", "false", "no", "0", "disabled"].includes(normalized)) return false;
|
|
661
|
+
return undefined;
|
|
662
|
+
}
|
|
663
|
+
|
|
664
|
+
function registerCommand(pi: ExtensionAPI) {
|
|
665
|
+
pi.registerCommand("compact-transcript", {
|
|
666
|
+
description: "Toggle compact transcript rendering (on/off).",
|
|
667
|
+
getArgumentCompletions(prefix: string) {
|
|
668
|
+
return ["on", "off", "status"]
|
|
669
|
+
.filter((value) => value.startsWith(prefix.trimStart()))
|
|
670
|
+
.map((value) => ({ value, label: value }));
|
|
671
|
+
},
|
|
672
|
+
handler: async (args, ctx) => {
|
|
673
|
+
captureTheme(ctx);
|
|
674
|
+
const trimmed = args.trim().toLowerCase();
|
|
675
|
+
if (trimmed === "status") {
|
|
676
|
+
ctx.ui.notify(`Compact transcript: ${isEnabled() ? "on" : "off"}`, "info");
|
|
677
|
+
return;
|
|
678
|
+
}
|
|
679
|
+
if (!trimmed) {
|
|
680
|
+
setEnabled(!isEnabled(), pi, ctx);
|
|
681
|
+
return;
|
|
682
|
+
}
|
|
683
|
+
// on/off, plus pre-0.5 mode names as legacy aliases.
|
|
684
|
+
const legacyOn = ["balanced", "aggressive", "debug"].includes(trimmed);
|
|
685
|
+
const parsed = legacyOn ? true : parseBoolean(trimmed);
|
|
686
|
+
if (parsed !== undefined) {
|
|
687
|
+
setEnabled(parsed, pi, ctx);
|
|
688
|
+
return;
|
|
689
|
+
}
|
|
690
|
+
ctx.ui.notify(`Unknown option "${trimmed}". Usage: /compact-transcript [on|off|status]`, "error");
|
|
691
|
+
},
|
|
692
|
+
});
|
|
693
|
+
}
|
|
694
|
+
|
|
695
|
+
export default function compactTranscript(pi: ExtensionAPI) {
|
|
696
|
+
patchRenderers();
|
|
697
|
+
registerCommand(pi);
|
|
698
|
+
|
|
699
|
+
pi.registerEntryRenderer<SummaryData>(SUMMARY_ENTRY_TYPE, (entry, _options, theme) => {
|
|
700
|
+
const line = summaryLine(normalizeSummary(entry.data));
|
|
701
|
+
if (!line) return undefined;
|
|
702
|
+
return new Text(theme.fg("muted", line), 0, 0);
|
|
703
|
+
});
|
|
296
704
|
|
|
297
705
|
pi.on("session_start", async (_event, ctx) => {
|
|
298
|
-
|
|
299
|
-
ctx
|
|
706
|
+
restoreConfigFromBranch(ctx);
|
|
707
|
+
captureTheme(ctx);
|
|
708
|
+
// Drop all per-tool state and component registries from the previous
|
|
709
|
+
// session; stale burst counts otherwise corrupt the rebuilt transcript
|
|
710
|
+
// after resume or /reload.
|
|
711
|
+
resetToolRun();
|
|
712
|
+
state.runStats = newRunStats();
|
|
713
|
+
state.toolComponents = new Set();
|
|
714
|
+
state.assistantComponents = new Set();
|
|
715
|
+
ctx.ui.setWorkingMessage();
|
|
716
|
+
// Clear any footer status left behind by pre-0.4 versions of this extension.
|
|
717
|
+
ctx.ui.setStatus(STATUS_KEY, undefined);
|
|
718
|
+
});
|
|
719
|
+
|
|
720
|
+
pi.on("session_shutdown", async (_event, ctx) => {
|
|
721
|
+
stopBlinkTimer();
|
|
722
|
+
ctx.ui.setStatus(STATUS_KEY, undefined);
|
|
723
|
+
ctx.ui.setWorkingMessage();
|
|
724
|
+
});
|
|
725
|
+
|
|
726
|
+
pi.on("agent_start", (_event, ctx) => {
|
|
727
|
+
state.agentActive = true;
|
|
728
|
+
captureTheme(ctx);
|
|
729
|
+
resetToolRun();
|
|
730
|
+
state.runStats = newRunStats();
|
|
300
731
|
});
|
|
301
732
|
|
|
302
|
-
pi.on("
|
|
303
|
-
|
|
733
|
+
pi.on("agent_end", (_event, _ctx) => {
|
|
734
|
+
state.agentActive = false;
|
|
735
|
+
state.currentBurst = [];
|
|
736
|
+
state.runningToolIds.clear();
|
|
737
|
+
stopBlinkTimer();
|
|
738
|
+
appendRunSummary(pi);
|
|
304
739
|
});
|
|
305
740
|
|
|
306
741
|
pi.on("turn_start", (_event, ctx) => {
|
|
307
|
-
|
|
308
|
-
applyThinkingLabel(ctx);
|
|
742
|
+
captureTheme(ctx);
|
|
309
743
|
});
|
|
310
744
|
|
|
311
|
-
pi.on("message_update", (event
|
|
745
|
+
pi.on("message_update", (event, ctx) => {
|
|
746
|
+
captureTheme(ctx);
|
|
312
747
|
const type = event.assistantMessageEvent?.type;
|
|
313
|
-
if (type === "
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
return;
|
|
317
|
-
}
|
|
318
|
-
if (type && !type.startsWith("thinking_")) {
|
|
319
|
-
consecutiveThinking = 0;
|
|
320
|
-
applyThinkingLabel(ctx);
|
|
321
|
-
if (type === "text_delta" || type === "text_start") resetTools();
|
|
748
|
+
if (type === "text_delta" || type === "text_start") {
|
|
749
|
+
// Visible assistant text ends the current tool burst.
|
|
750
|
+
state.currentBurst = [];
|
|
322
751
|
}
|
|
323
752
|
});
|
|
324
753
|
|
|
325
|
-
pi.on("tool_execution_start", (event
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
previous?.invalidate?.();
|
|
754
|
+
pi.on("tool_execution_start", (event, ctx) => {
|
|
755
|
+
captureTheme(ctx);
|
|
756
|
+
beginTool(event.toolCallId, event.toolName, event.args);
|
|
329
757
|
});
|
|
330
758
|
|
|
331
|
-
pi.on("
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
const suffix = resultPreview(event.result);
|
|
335
|
-
if (suffix) info.result = suffix;
|
|
336
|
-
info.invalidate?.();
|
|
759
|
+
pi.on("tool_execution_update", (event, ctx) => {
|
|
760
|
+
captureTheme(ctx);
|
|
761
|
+
updateToolResult(event.toolCallId, event.partialResult, false, true);
|
|
337
762
|
});
|
|
338
763
|
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
name,
|
|
344
|
-
label: name,
|
|
345
|
-
renderShell: "self",
|
|
346
|
-
async execute(toolCallId: string, params: any, signal: AbortSignal | undefined, onUpdate: any, ctx: ExtensionContext) {
|
|
347
|
-
const tool = (getBuiltInTools(ctx.cwd) as any)[name];
|
|
348
|
-
return tool.execute(toolCallId, params, signal, onUpdate, ctx);
|
|
349
|
-
},
|
|
350
|
-
renderCall(args: any, theme: any, context: any) {
|
|
351
|
-
const text = (context.lastComponent as Text | undefined) ?? new Text("", 0, 0);
|
|
352
|
-
text.setText(compactToolLine(context.toolCallId, name, args, theme, context.invalidate));
|
|
353
|
-
return text;
|
|
354
|
-
},
|
|
355
|
-
renderResult(_result: any, _options: any, _theme: any, _context: any) {
|
|
356
|
-
return new Text("", 0, 0);
|
|
357
|
-
},
|
|
358
|
-
});
|
|
359
|
-
}
|
|
764
|
+
pi.on("tool_execution_end", (event, ctx) => {
|
|
765
|
+
captureTheme(ctx);
|
|
766
|
+
updateToolResult(event.toolCallId, event.result, event.isError, false);
|
|
767
|
+
});
|
|
360
768
|
}
|