pi-compact-transcript 0.2.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/README.md +43 -5
- package/extensions/compact-transcript.ts +956 -203
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -3,9 +3,12 @@
|
|
|
3
3
|
A compact transcript extension for [pi](https://pi.dev):
|
|
4
4
|
|
|
5
5
|
- Collapses hidden thinking blocks into `Thinking...` and consecutive thinking blocks into `Thinking... (Nx)`.
|
|
6
|
-
-
|
|
7
|
-
- Collapses
|
|
8
|
-
- Consecutive tool uses
|
|
6
|
+
- Shows only real collapsed thinking labels/counters; it does not invent next-step prose.
|
|
7
|
+
- Collapses tool calls/results into one-line previews, including custom/external tools added by other extensions.
|
|
8
|
+
- Consecutive non-mutating tool uses are summarized by count and kind, e.g. `Used 7 tools: 4 read, 2 grep, 1 bash · latest read src/foo.ts`.
|
|
9
|
+
- Mutating tools (`edit`, `write`, configured mutation tools, and destructive-looking `bash` commands) stay visible as transcript anchors and break tool bursts.
|
|
10
|
+
- Failed tools stay visible by default so errors are not hidden.
|
|
11
|
+
- Expanded tool output still falls back to pi's original renderer, so you can use pi's normal tool expansion when details matter.
|
|
9
12
|
- Minimizes vertical space so long agent runs do not scroll away as quickly.
|
|
10
13
|
|
|
11
14
|
## Install from GitHub
|
|
@@ -47,8 +50,43 @@ The extension works best with hidden thinking and no output padding:
|
|
|
47
50
|
|
|
48
51
|
Set these in `~/.pi/agent/settings.json`, or use `/settings` in pi.
|
|
49
52
|
|
|
53
|
+
## Commands
|
|
54
|
+
|
|
55
|
+
```text
|
|
56
|
+
/compact-transcript # open the settings-style panel
|
|
57
|
+
/compact-transcript status # same as above
|
|
58
|
+
/compact-transcript disabled|balanced|aggressive|debug
|
|
59
|
+
/compact-transcript custom-tools on|off
|
|
60
|
+
/compact-transcript failed on|off
|
|
61
|
+
/compact-transcript bash-mutations on|off
|
|
62
|
+
/compact-transcript always-show <tool[,tool]|/regex/|clear>
|
|
63
|
+
/compact-transcript mutation-tools <tool[,tool]|/regex/|clear>
|
|
64
|
+
/compact-transcript template <tool|/regex/> <template|clear>
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
In interactive pi, `/compact-transcript` opens a focused settings-style panel in the editor area. Press `Enter`/`Space` to toggle values, edit list/template rows inline, and `Esc` to close it.
|
|
68
|
+
|
|
69
|
+
Modes:
|
|
70
|
+
|
|
71
|
+
- `balanced` (default): compact non-mutating bursts, keep mutations/failures visible, and preserve normal hidden-thinking markers.
|
|
72
|
+
- `aggressive`: shorter previews and coalesced thinking-only markers.
|
|
73
|
+
- `debug`: compact rows but do not hide earlier burst rows; useful when tuning rules.
|
|
74
|
+
- `disabled`: fully disable compact-transcript rendering for future rows. (`off` is accepted as a legacy alias.)
|
|
75
|
+
|
|
76
|
+
Switching modes never changes the other toggles (custom tools, failed tools, bash anchors); those are independent settings.
|
|
77
|
+
|
|
78
|
+
Preview templates support `{name}`, `{args}`, top-level argument names like `{path}` and `{command}`, and nested fields like `{arg.query.text}`. In the panel, edit templates as semicolon-separated `tool=template` pairs, so template text itself cannot contain `;`.
|
|
79
|
+
|
|
80
|
+
Examples:
|
|
81
|
+
|
|
82
|
+
```text
|
|
83
|
+
/compact-transcript always-show ask_user_question,/^deploy_/
|
|
84
|
+
/compact-transcript mutation-tools db_query,kubectl
|
|
85
|
+
/compact-transcript template fetch_content fetch {url}
|
|
86
|
+
```
|
|
87
|
+
|
|
50
88
|
## Notes
|
|
51
89
|
|
|
52
|
-
This extension
|
|
90
|
+
This extension changes display only. Tool execution is still handled by pi and any other extensions that registered or override tools.
|
|
53
91
|
|
|
54
|
-
|
|
92
|
+
For built-in and extension tools, compact rendering uses pi's public exported TUI components where available. The cross-tool burst compaction and consecutive-thinking coalescing 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,54 +1,146 @@
|
|
|
1
|
-
import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
2
|
-
import {
|
|
3
|
-
|
|
4
|
-
createEditTool,
|
|
5
|
-
createFindTool,
|
|
6
|
-
createGrepTool,
|
|
7
|
-
createLsTool,
|
|
8
|
-
createReadTool,
|
|
9
|
-
createWriteTool,
|
|
10
|
-
} from "@earendil-works/pi-coding-agent";
|
|
11
|
-
import { Markdown, Spacer, Text } from "@earendil-works/pi-tui";
|
|
12
|
-
import { createRequire } from "node:module";
|
|
1
|
+
import type { ExtensionAPI, ExtensionContext, Theme } from "@earendil-works/pi-coding-agent";
|
|
2
|
+
import { AssistantMessageComponent, getSettingsListTheme, ToolExecutionComponent } from "@earendil-works/pi-coding-agent";
|
|
3
|
+
import { Container, Input, Markdown, type SettingItem, SettingsList, Spacer, Text } from "@earendil-works/pi-tui";
|
|
13
4
|
import { homedir } from "node:os";
|
|
14
|
-
import { dirname, join } from "node:path";
|
|
15
|
-
import { pathToFileURL } from "node:url";
|
|
16
5
|
|
|
17
|
-
|
|
6
|
+
const LABEL = "Thinking...";
|
|
7
|
+
// Older versions of this extension wrote a footer status under this key; it is
|
|
8
|
+
// kept only to clear that status once per session for users upgrading in place.
|
|
9
|
+
const STATUS_KEY = "compact-transcript";
|
|
10
|
+
const CONFIG_ENTRY_TYPE = "compact-transcript-config";
|
|
11
|
+
|
|
12
|
+
const BUILT_INS = new Set(["bash", "read", "write", "edit", "grep", "find", "ls"]);
|
|
13
|
+
|
|
14
|
+
const MIN_PREVIEW_WIDTH = 20;
|
|
15
|
+
const AGGRESSIVE_PREVIEW_WIDTH = 72;
|
|
16
|
+
const DEBUG_PREVIEW_WIDTH = 140;
|
|
17
|
+
const DEFAULT_PREVIEW_WIDTH = 104;
|
|
18
|
+
// Leave room for pi's row gutter/padding so compact lines never wrap.
|
|
19
|
+
const PREVIEW_MARGIN = 6;
|
|
20
|
+
const SETTINGS_LIST_HEIGHT = 9;
|
|
21
|
+
|
|
22
|
+
type Mode = "disabled" | "balanced" | "aggressive" | "debug";
|
|
23
|
+
// "off" is a legacy alias for "disabled" accepted from commands and persisted config.
|
|
24
|
+
type ModeInput = Mode | "off";
|
|
25
|
+
type ToolKind = "always" | "mutation" | "noise";
|
|
26
|
+
|
|
27
|
+
type CompactTranscriptConfig = {
|
|
28
|
+
mode: Mode;
|
|
29
|
+
compactCustomTools: boolean;
|
|
30
|
+
showFailedTools: boolean;
|
|
31
|
+
showBashMutations: boolean;
|
|
32
|
+
alwaysShowTools: string[];
|
|
33
|
+
mutationTools: string[];
|
|
34
|
+
previewTemplates: Record<string, string>;
|
|
35
|
+
};
|
|
18
36
|
|
|
19
37
|
type ToolInfo = {
|
|
20
38
|
id: string;
|
|
21
39
|
name: string;
|
|
22
40
|
args: any;
|
|
23
41
|
preview: string;
|
|
42
|
+
kind: ToolKind;
|
|
43
|
+
hidden?: boolean;
|
|
44
|
+
burstCount?: number;
|
|
45
|
+
burstSummary?: string;
|
|
24
46
|
result?: string;
|
|
47
|
+
isError?: boolean;
|
|
25
48
|
invalidate?: () => void;
|
|
26
49
|
};
|
|
27
50
|
|
|
28
|
-
|
|
29
|
-
|
|
51
|
+
type RuntimeState = {
|
|
52
|
+
config: CompactTranscriptConfig;
|
|
53
|
+
toolsById: Map<string, ToolInfo>;
|
|
54
|
+
currentNoiseBurst: ToolInfo[];
|
|
55
|
+
hiddenToolIds: Set<string>;
|
|
56
|
+
agentActive: boolean;
|
|
57
|
+
lastThinkingSignalComponent?: any;
|
|
58
|
+
thinkingSignalCount: number;
|
|
59
|
+
consecutiveThinking: number;
|
|
60
|
+
currentTheme?: Theme;
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
const DEFAULT_CONFIG: CompactTranscriptConfig = {
|
|
64
|
+
mode: "balanced",
|
|
65
|
+
compactCustomTools: true,
|
|
66
|
+
showFailedTools: true,
|
|
67
|
+
showBashMutations: true,
|
|
68
|
+
alwaysShowTools: [],
|
|
69
|
+
mutationTools: [],
|
|
70
|
+
previewTemplates: {},
|
|
71
|
+
};
|
|
30
72
|
|
|
31
|
-
const
|
|
73
|
+
const STATE_KEY = Symbol.for("pi-compact-transcript.state");
|
|
74
|
+
const TOOL_PATCH_KEY = Symbol.for("pi-compact-transcript.tool-patch");
|
|
75
|
+
const ASSISTANT_PATCH_KEY = Symbol.for("pi-compact-transcript.assistant-patch");
|
|
32
76
|
|
|
33
|
-
function
|
|
77
|
+
function cloneConfig(config: CompactTranscriptConfig): CompactTranscriptConfig {
|
|
34
78
|
return {
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
79
|
+
mode: config.mode,
|
|
80
|
+
compactCustomTools: config.compactCustomTools,
|
|
81
|
+
showFailedTools: config.showFailedTools,
|
|
82
|
+
showBashMutations: config.showBashMutations,
|
|
83
|
+
alwaysShowTools: [...config.alwaysShowTools],
|
|
84
|
+
mutationTools: [...config.mutationTools],
|
|
85
|
+
previewTemplates: { ...config.previewTemplates },
|
|
42
86
|
};
|
|
43
87
|
}
|
|
44
88
|
|
|
45
|
-
function
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
89
|
+
function normalizeConfig(input: unknown): CompactTranscriptConfig {
|
|
90
|
+
const source = (input && typeof input === "object" ? input : {}) as Partial<
|
|
91
|
+
Omit<CompactTranscriptConfig, "mode"> & { mode: ModeInput }
|
|
92
|
+
>;
|
|
93
|
+
const rawMode = source.mode;
|
|
94
|
+
const mode: Mode =
|
|
95
|
+
rawMode === "off" || rawMode === "disabled"
|
|
96
|
+
? "disabled"
|
|
97
|
+
: rawMode === "aggressive" || rawMode === "debug"
|
|
98
|
+
? rawMode
|
|
99
|
+
: "balanced";
|
|
100
|
+
return {
|
|
101
|
+
mode,
|
|
102
|
+
compactCustomTools: typeof source.compactCustomTools === "boolean" ? source.compactCustomTools : DEFAULT_CONFIG.compactCustomTools,
|
|
103
|
+
showFailedTools: typeof source.showFailedTools === "boolean" ? source.showFailedTools : DEFAULT_CONFIG.showFailedTools,
|
|
104
|
+
showBashMutations: typeof source.showBashMutations === "boolean" ? source.showBashMutations : DEFAULT_CONFIG.showBashMutations,
|
|
105
|
+
alwaysShowTools: Array.isArray(source.alwaysShowTools) ? source.alwaysShowTools.filter(isNonEmptyString) : [],
|
|
106
|
+
mutationTools: Array.isArray(source.mutationTools) ? source.mutationTools.filter(isNonEmptyString) : [],
|
|
107
|
+
previewTemplates:
|
|
108
|
+
source.previewTemplates && typeof source.previewTemplates === "object" && !Array.isArray(source.previewTemplates)
|
|
109
|
+
? Object.fromEntries(
|
|
110
|
+
Object.entries(source.previewTemplates).filter(
|
|
111
|
+
([key, value]) => isNonEmptyString(key) && typeof value === "string",
|
|
112
|
+
),
|
|
113
|
+
)
|
|
114
|
+
: {},
|
|
115
|
+
};
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
function getState(): RuntimeState {
|
|
119
|
+
const globalWithState = globalThis as typeof globalThis & { [STATE_KEY]?: RuntimeState };
|
|
120
|
+
globalWithState[STATE_KEY] ??= {
|
|
121
|
+
config: cloneConfig(DEFAULT_CONFIG),
|
|
122
|
+
toolsById: new Map(),
|
|
123
|
+
currentNoiseBurst: [],
|
|
124
|
+
hiddenToolIds: new Set(),
|
|
125
|
+
agentActive: false,
|
|
126
|
+
thinkingSignalCount: 0,
|
|
127
|
+
consecutiveThinking: 0,
|
|
128
|
+
};
|
|
129
|
+
return globalWithState[STATE_KEY]!;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
const state = getState();
|
|
133
|
+
|
|
134
|
+
function isEnabled(): boolean {
|
|
135
|
+
return state.config.mode !== "disabled";
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
function isDebugMode(): boolean {
|
|
139
|
+
return state.config.mode === "debug";
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
function isNonEmptyString(value: unknown): value is string {
|
|
143
|
+
return typeof value === "string" && value.trim().length > 0;
|
|
52
144
|
}
|
|
53
145
|
|
|
54
146
|
function shortenPath(path: unknown): string {
|
|
@@ -63,7 +155,17 @@ function oneLine(value: unknown): string {
|
|
|
63
155
|
.trim();
|
|
64
156
|
}
|
|
65
157
|
|
|
66
|
-
function
|
|
158
|
+
function previewWidth(base = process.stdout.columns || 100): number {
|
|
159
|
+
const modeMax =
|
|
160
|
+
state.config.mode === "aggressive"
|
|
161
|
+
? AGGRESSIVE_PREVIEW_WIDTH
|
|
162
|
+
: state.config.mode === "debug"
|
|
163
|
+
? DEBUG_PREVIEW_WIDTH
|
|
164
|
+
: DEFAULT_PREVIEW_WIDTH;
|
|
165
|
+
return Math.max(MIN_PREVIEW_WIDTH, Math.min(modeMax, base - PREVIEW_MARGIN));
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
function limitPlain(text: string, max = previewWidth()): string {
|
|
67
169
|
const clean = oneLine(text);
|
|
68
170
|
if (clean.length <= max) return clean;
|
|
69
171
|
return `${clean.slice(0, Math.max(0, max - 1))}…`;
|
|
@@ -73,7 +175,99 @@ function quote(s: string): string {
|
|
|
73
175
|
return JSON.stringify(s);
|
|
74
176
|
}
|
|
75
177
|
|
|
178
|
+
function safeJson(value: unknown): string {
|
|
179
|
+
try {
|
|
180
|
+
return JSON.stringify(value ?? {});
|
|
181
|
+
} catch {
|
|
182
|
+
return String(value);
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
function matchRule(rule: string, toolName: string): boolean {
|
|
187
|
+
const trimmed = rule.trim();
|
|
188
|
+
if (!trimmed) return false;
|
|
189
|
+
if (trimmed.startsWith("/") && trimmed.lastIndexOf("/") > 0) {
|
|
190
|
+
const lastSlash = trimmed.lastIndexOf("/");
|
|
191
|
+
const pattern = trimmed.slice(1, lastSlash);
|
|
192
|
+
const flags = trimmed.slice(lastSlash + 1);
|
|
193
|
+
try {
|
|
194
|
+
return new RegExp(pattern, flags).test(toolName);
|
|
195
|
+
} catch {
|
|
196
|
+
return false;
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
if (trimmed.includes("*")) {
|
|
200
|
+
const pattern = `^${trimmed.split("*").map(escapeRegExp).join(".*")}$`;
|
|
201
|
+
return new RegExp(pattern).test(toolName);
|
|
202
|
+
}
|
|
203
|
+
return toolName === trimmed || toolName.endsWith(`.${trimmed}`);
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
function escapeRegExp(value: string): string {
|
|
207
|
+
return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
function matchesAnyRule(rules: string[], toolName: string): boolean {
|
|
211
|
+
return rules.some((rule) => matchRule(rule, toolName));
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
function isMutatingBash(command: unknown): boolean {
|
|
215
|
+
if (typeof command !== "string") return false;
|
|
216
|
+
const compact = command.replace(/#[^\n]*/g, "").trim();
|
|
217
|
+
if (!compact) return false;
|
|
218
|
+
|
|
219
|
+
// Obvious file/process mutations and package/install actions. This is intentionally
|
|
220
|
+
// conservative enough to keep anchors for risky commands without treating every
|
|
221
|
+
// shell pipeline as destructive.
|
|
222
|
+
return /(^|[;&|]\s*)(rm\b|mv\b|cp\b|mkdir\b|rmdir\b|touch\b|chmod\b|chown\b|ln\b|truncate\b|tee\b|npm\s+(i|install|uninstall|remove|publish)\b|pnpm\s+(i|install|add|remove)\b|yarn\s+(add|remove|install|publish)\b|bun\s+(add|remove|install)\b|pip\s+(install|uninstall)\b|git\s+(add|apply|commit|checkout|clean|merge|mv|pull|push|rebase|reset|restore|stash|switch)\b)/.test(
|
|
223
|
+
compact,
|
|
224
|
+
) || /(^|[^2])>>?\s*[^&\s]/.test(compact);
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
function classifyTool(name: string, args: any): ToolKind {
|
|
228
|
+
if (matchesAnyRule(state.config.alwaysShowTools, name)) return "always";
|
|
229
|
+
if (!BUILT_INS.has(name) && !state.config.compactCustomTools) return "always";
|
|
230
|
+
if (name === "edit" || name === "write" || name.endsWith(".edit") || name.endsWith(".write")) return "mutation";
|
|
231
|
+
if (matchesAnyRule(state.config.mutationTools, name)) return "mutation";
|
|
232
|
+
if (state.config.showBashMutations && (name === "bash" || name.endsWith(".bash")) && isMutatingBash(args?.command)) {
|
|
233
|
+
return "mutation";
|
|
234
|
+
}
|
|
235
|
+
return "noise";
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
function findTemplate(name: string): string | undefined {
|
|
239
|
+
if (state.config.previewTemplates[name]) return state.config.previewTemplates[name];
|
|
240
|
+
for (const [rule, template] of Object.entries(state.config.previewTemplates)) {
|
|
241
|
+
if (matchRule(rule, name)) return template;
|
|
242
|
+
}
|
|
243
|
+
return undefined;
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
function getByPath(value: any, path: string): unknown {
|
|
247
|
+
let current = value;
|
|
248
|
+
for (const part of path.split(".")) {
|
|
249
|
+
if (!part) continue;
|
|
250
|
+
if (current == null || typeof current !== "object") return undefined;
|
|
251
|
+
current = current[part];
|
|
252
|
+
}
|
|
253
|
+
return current;
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
function renderTemplate(template: string, name: string, args: any): string {
|
|
257
|
+
return template.replace(/\{(?:arg\.)?([a-zA-Z0-9_.-]+)\}/g, (_match, key: string) => {
|
|
258
|
+
if (key === "name") return name;
|
|
259
|
+
if (key === "args") return safeJson(args);
|
|
260
|
+
const value = getByPath(args, key);
|
|
261
|
+
if (value === undefined || value === null) return "";
|
|
262
|
+
if (typeof value === "string") return value;
|
|
263
|
+
return safeJson(value);
|
|
264
|
+
});
|
|
265
|
+
}
|
|
266
|
+
|
|
76
267
|
function previewFor(name: string, args: any): string {
|
|
268
|
+
const template = findTemplate(name);
|
|
269
|
+
if (template) return renderTemplate(template, name, args);
|
|
270
|
+
|
|
77
271
|
switch (name) {
|
|
78
272
|
case "bash":
|
|
79
273
|
return `$ ${oneLine(args?.command || "...")}`;
|
|
@@ -104,236 +298,795 @@ function previewFor(name: string, args: any): string {
|
|
|
104
298
|
case "ls":
|
|
105
299
|
return `ls ${shortenPath(args?.path) || "."}`;
|
|
106
300
|
default:
|
|
107
|
-
return `${name} ${
|
|
301
|
+
return `${name} ${safeJson(args ?? {})}`;
|
|
108
302
|
}
|
|
109
303
|
}
|
|
110
304
|
|
|
111
|
-
function resultPreview(result: any): string {
|
|
305
|
+
function resultPreview(result: any, isPartial = false): string {
|
|
112
306
|
const text = Array.isArray(result?.content)
|
|
113
307
|
? result.content.find((c: any) => c?.type === "text" && typeof c.text === "string")?.text
|
|
114
308
|
: undefined;
|
|
115
|
-
if (!text) return "";
|
|
309
|
+
if (!text) return isPartial ? "running" : "";
|
|
116
310
|
const lines = String(text).trim().split("\n").filter(Boolean);
|
|
117
|
-
if (lines.length === 0) return "";
|
|
311
|
+
if (lines.length === 0) return isPartial ? "running" : "";
|
|
118
312
|
if (lines.length === 1) return lines[0];
|
|
119
313
|
return `${lines.length} lines`;
|
|
120
314
|
}
|
|
121
315
|
|
|
122
|
-
function
|
|
123
|
-
|
|
316
|
+
function summarizeBurst(tools: ToolInfo[]): string {
|
|
317
|
+
const counts = new Map<string, number>();
|
|
318
|
+
for (const tool of tools) {
|
|
319
|
+
counts.set(tool.name, (counts.get(tool.name) ?? 0) + 1);
|
|
320
|
+
}
|
|
321
|
+
return [...counts.entries()]
|
|
322
|
+
.sort((a, b) => b[1] - a[1] || a[0].localeCompare(b[0]))
|
|
323
|
+
.map(([name, count]) => `${count} ${name}`)
|
|
324
|
+
.join(", ");
|
|
124
325
|
}
|
|
125
326
|
|
|
126
|
-
function
|
|
127
|
-
|
|
327
|
+
function resetToolRun() {
|
|
328
|
+
state.toolsById = new Map();
|
|
329
|
+
state.currentNoiseBurst = [];
|
|
330
|
+
state.hiddenToolIds = new Set();
|
|
128
331
|
}
|
|
129
332
|
|
|
130
|
-
function
|
|
131
|
-
|
|
132
|
-
|
|
333
|
+
function resetThinkingSignals() {
|
|
334
|
+
state.lastThinkingSignalComponent = undefined;
|
|
335
|
+
state.thinkingSignalCount = 0;
|
|
336
|
+
}
|
|
133
337
|
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
338
|
+
function captureTheme(ctx: ExtensionContext) {
|
|
339
|
+
state.currentTheme = ctx.ui.theme;
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
function setToolHidden(info: ToolInfo, hidden: boolean) {
|
|
343
|
+
info.hidden = hidden;
|
|
344
|
+
if (hidden) state.hiddenToolIds.add(info.id);
|
|
345
|
+
else state.hiddenToolIds.delete(info.id);
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
function applyResult(info: ToolInfo, result: any, isError: boolean, isPartial: boolean) {
|
|
349
|
+
const suffix = resultPreview(result, isPartial);
|
|
350
|
+
if (suffix) info.result = suffix;
|
|
351
|
+
if (isError) {
|
|
352
|
+
info.isError = true;
|
|
353
|
+
setToolHidden(info, false);
|
|
138
354
|
}
|
|
139
|
-
const last = tools[tools.length - 1];
|
|
140
|
-
const lastPreview = limitPlain(previewFor(toolCallName(last), toolCallArgs(last)), 70);
|
|
141
|
-
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}.`;
|
|
142
355
|
}
|
|
143
356
|
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
357
|
+
function upsertToolInfo(id: string, name: string, args: any, invalidate?: () => void): ToolInfo {
|
|
358
|
+
let info = state.toolsById.get(id);
|
|
359
|
+
if (!info) {
|
|
360
|
+
info = { id, name, args, preview: previewFor(name, args), kind: classifyTool(name, args) };
|
|
361
|
+
state.toolsById.set(id, info);
|
|
362
|
+
}
|
|
363
|
+
info.name = name;
|
|
364
|
+
info.args = args;
|
|
365
|
+
info.preview = previewFor(name, args);
|
|
366
|
+
info.kind = classifyTool(name, args);
|
|
367
|
+
if (invalidate) info.invalidate = invalidate;
|
|
368
|
+
return info;
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
function beginTool(id: string, name: string, args: any) {
|
|
372
|
+
const previousNoise = state.currentNoiseBurst[state.currentNoiseBurst.length - 1];
|
|
373
|
+
const info = upsertToolInfo(id, name, args);
|
|
374
|
+
setToolHidden(info, false);
|
|
375
|
+
info.burstCount = 1;
|
|
376
|
+
info.burstSummary = undefined;
|
|
377
|
+
|
|
378
|
+
if (!isEnabled() || info.kind !== "noise") {
|
|
379
|
+
state.currentNoiseBurst = [];
|
|
380
|
+
return;
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
if (!state.currentNoiseBurst.some((tool) => tool.id === id)) state.currentNoiseBurst.push(info);
|
|
384
|
+
const summary = summarizeBurst(state.currentNoiseBurst);
|
|
385
|
+
for (const tool of state.currentNoiseBurst) tool.burstSummary = summary;
|
|
386
|
+
|
|
387
|
+
if (!isDebugMode()) {
|
|
388
|
+
for (const tool of state.currentNoiseBurst.slice(0, -1)) setToolHidden(tool, true);
|
|
389
|
+
}
|
|
390
|
+
info.burstCount = state.currentNoiseBurst.length;
|
|
391
|
+
previousNoise?.invalidate?.();
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
function updateToolResult(toolCallId: string, result: any, isError = false, isPartial = false) {
|
|
395
|
+
const info = state.toolsById.get(toolCallId);
|
|
396
|
+
if (!info) return;
|
|
397
|
+
applyResult(info, result, isError, isPartial);
|
|
398
|
+
info.invalidate?.();
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
function compactToolLine(
|
|
402
|
+
toolCallId: string,
|
|
403
|
+
name: string,
|
|
404
|
+
args: any,
|
|
405
|
+
theme: Theme,
|
|
406
|
+
invalidate?: () => void,
|
|
407
|
+
result?: any,
|
|
408
|
+
isError = false,
|
|
409
|
+
isPartial = false,
|
|
410
|
+
): string {
|
|
411
|
+
let info = state.toolsById.get(toolCallId);
|
|
412
|
+
|
|
413
|
+
// Hydration/resume path: compact the individual row, but do not mutate burst
|
|
414
|
+
// state or invalidate older rows while pi is rebuilding chat history.
|
|
415
|
+
if (!state.agentActive && !info) {
|
|
416
|
+
const suffix = resultPreview(result, isPartial);
|
|
417
|
+
const preview = previewFor(name, args);
|
|
418
|
+
const details = suffix ? `${preview} {${oneLine(suffix)}}` : preview;
|
|
419
|
+
const marker = classifyTool(name, args) === "mutation" ? theme.fg("warning", "◆ ") : "";
|
|
420
|
+
return marker + theme.fg("muted", limitPlain(details));
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
info = upsertToolInfo(toolCallId, name, args, invalidate);
|
|
424
|
+
applyResult(info, result, isError, isPartial);
|
|
425
|
+
if (info.hidden) return "";
|
|
426
|
+
|
|
427
|
+
const status = info.result ? ` {${oneLine(info.result)}}` : isPartial ? " {running}" : "";
|
|
428
|
+
const details = `${info.preview}${status}`;
|
|
429
|
+
if (info.kind !== "noise" || (info.burstCount ?? 1) <= 1 || isDebugMode()) {
|
|
430
|
+
const marker = info.kind === "mutation" ? theme.fg("warning", "◆ ") : isDebugMode() ? theme.fg("dim", "· ") : "";
|
|
431
|
+
return marker + theme.fg("muted", limitPlain(details));
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
const summary = info.burstSummary || summarizeBurst(state.currentNoiseBurst);
|
|
435
|
+
const prefix = `Used ${info.burstCount} tools`;
|
|
436
|
+
const middle = summary ? `: ${summary} · latest ` : " ";
|
|
437
|
+
const prefixText = `${prefix}${middle}`;
|
|
438
|
+
return (
|
|
439
|
+
theme.fg("toolTitle", prefixText) +
|
|
440
|
+
theme.fg("muted", limitPlain(details, previewWidth((process.stdout.columns || 100) - prefixText.length)))
|
|
441
|
+
);
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
function shouldUseOriginalToolRow(row: any, info: ToolInfo): boolean {
|
|
445
|
+
if (!isEnabled()) return true;
|
|
446
|
+
if (row.expanded) return true;
|
|
447
|
+
if (info.kind === "always") return true;
|
|
448
|
+
if (info.isError && state.config.showFailedTools) return true;
|
|
449
|
+
return false;
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
function patchToolExecutionComponent() {
|
|
453
|
+
const proto = ToolExecutionComponent.prototype as any;
|
|
454
|
+
if (typeof proto.updateDisplay !== "function" || typeof proto.render !== "function") return;
|
|
455
|
+
const existing = proto[TOOL_PATCH_KEY] as
|
|
456
|
+
| { originalUpdateDisplay: (...args: any[]) => any; originalRender: (...args: any[]) => any }
|
|
457
|
+
| undefined;
|
|
458
|
+
const originalUpdateDisplay = existing?.originalUpdateDisplay ?? proto.updateDisplay;
|
|
459
|
+
const originalRender = existing?.originalRender ?? proto.render;
|
|
460
|
+
|
|
461
|
+
proto.updateDisplay = function patchedUpdateDisplay() {
|
|
462
|
+
if (!this.toolCallId || !this.toolName || !this.selfRenderContainer || typeof this.selfRenderContainer.clear !== "function") {
|
|
463
|
+
this.__compactTranscriptForceSelf = false;
|
|
464
|
+
return originalUpdateDisplay.call(this);
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
const invalidate = () => {
|
|
468
|
+
this.invalidate();
|
|
469
|
+
this.ui?.requestRender?.();
|
|
470
|
+
};
|
|
471
|
+
const info = upsertToolInfo(this.toolCallId, this.toolName, this.args, invalidate);
|
|
472
|
+
applyResult(info, this.result, this.result?.isError ?? false, this.isPartial);
|
|
473
|
+
|
|
474
|
+
if (shouldUseOriginalToolRow(this, info)) {
|
|
475
|
+
setToolHidden(info, false);
|
|
476
|
+
this.__compactTranscriptForceSelf = false;
|
|
477
|
+
return originalUpdateDisplay.call(this);
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
this.__compactTranscriptForceSelf = true;
|
|
481
|
+
this.hideComponent = false;
|
|
482
|
+
this.selfRenderContainer.clear();
|
|
483
|
+
for (const image of this.imageComponents ?? []) this.removeChild?.(image);
|
|
484
|
+
for (const spacer of this.imageSpacers ?? []) this.removeChild?.(spacer);
|
|
485
|
+
this.imageComponents = [];
|
|
486
|
+
this.imageSpacers = [];
|
|
487
|
+
|
|
488
|
+
const theme = state.currentTheme;
|
|
489
|
+
if (!theme) {
|
|
490
|
+
this.__compactTranscriptForceSelf = false;
|
|
491
|
+
return originalUpdateDisplay.call(this);
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
const line = compactToolLine(
|
|
495
|
+
this.toolCallId,
|
|
496
|
+
this.toolName,
|
|
497
|
+
this.args,
|
|
498
|
+
theme,
|
|
499
|
+
invalidate,
|
|
500
|
+
this.result,
|
|
501
|
+
this.result?.isError ?? false,
|
|
502
|
+
this.isPartial,
|
|
154
503
|
);
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
const originalRender = ToolExecutionComponent.prototype.render;
|
|
160
|
-
ToolExecutionComponent.prototype.render = function (width: number) {
|
|
161
|
-
if (this.hideComponent) return [];
|
|
162
|
-
if (this.hasRendererDefinition?.() && this.getRenderShell?.() === "self") {
|
|
163
|
-
const contentLines = this.selfRenderContainer.render(width);
|
|
164
|
-
if (contentLines.length === 0 && this.imageComponents.length === 0) return [];
|
|
165
|
-
const lines = [...contentLines];
|
|
166
|
-
for (let i = 0; i < this.imageComponents.length; i++) {
|
|
167
|
-
const spacer = this.imageSpacers[i];
|
|
168
|
-
if (spacer) lines.push(...spacer.render(width));
|
|
169
|
-
const imageComponent = this.imageComponents[i];
|
|
170
|
-
if (imageComponent) lines.push(...imageComponent.render(width));
|
|
171
|
-
}
|
|
172
|
-
return lines;
|
|
173
|
-
}
|
|
174
|
-
return originalRender.call(this, width);
|
|
175
|
-
};
|
|
176
|
-
ToolExecutionComponent.prototype.__compactTranscriptPatched = true;
|
|
504
|
+
|
|
505
|
+
if (!line) {
|
|
506
|
+
this.hideComponent = true;
|
|
507
|
+
return;
|
|
177
508
|
}
|
|
178
509
|
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
const original = Component.prototype.updateContent;
|
|
510
|
+
this.selfRenderContainer.addChild(new Text(line, 0, 0));
|
|
511
|
+
};
|
|
182
512
|
|
|
183
|
-
|
|
184
|
-
|
|
513
|
+
proto.render = function patchedRender(width: number) {
|
|
514
|
+
if (this.hideComponent) return [];
|
|
515
|
+
if (this.__compactTranscriptForceSelf) return this.selfRenderContainer.render(width);
|
|
516
|
+
return originalRender.call(this, width);
|
|
517
|
+
};
|
|
185
518
|
|
|
186
|
-
|
|
187
|
-
|
|
519
|
+
proto[TOOL_PATCH_KEY] = { originalUpdateDisplay, originalRender };
|
|
520
|
+
}
|
|
188
521
|
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
522
|
+
function patchAssistantMessageComponent() {
|
|
523
|
+
const proto = AssistantMessageComponent.prototype as any;
|
|
524
|
+
if (typeof proto.updateContent !== "function") return;
|
|
525
|
+
const existing = proto[ASSISTANT_PATCH_KEY] as { originalUpdateContent: (...args: any[]) => any } | undefined;
|
|
526
|
+
const originalUpdateContent = existing?.originalUpdateContent ?? proto.updateContent;
|
|
193
527
|
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
528
|
+
proto.updateContent = function patchedUpdateContent(message: any) {
|
|
529
|
+
if (!isEnabled() || !this.hideThinkingBlock || !Array.isArray(message?.content)) {
|
|
530
|
+
this.__compactTranscriptHiddenThinkingSignal = false;
|
|
531
|
+
return originalUpdateContent.call(this, message);
|
|
532
|
+
}
|
|
533
|
+
if (!this.contentContainer || typeof this.contentContainer.clear !== "function") {
|
|
534
|
+
this.__compactTranscriptHiddenThinkingSignal = false;
|
|
535
|
+
return originalUpdateContent.call(this, message);
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
this.lastMessage = message;
|
|
539
|
+
this.contentContainer.clear();
|
|
540
|
+
|
|
541
|
+
const hasText = message.content.some((c: any) => c.type === "text" && c.text?.trim());
|
|
542
|
+
const hasThinking = message.content.some((c: any) => c.type === "thinking" && c.thinking?.trim());
|
|
543
|
+
this.hasToolCalls = message.content.some((c: any) => c.type === "toolCall");
|
|
544
|
+
|
|
545
|
+
// Only aggressive mode coalesces thinking-only assistant messages across
|
|
546
|
+
// tool turns. Balanced mode should preserve normal hidden-thinking markers
|
|
547
|
+
// so users can still see where the model thought before tool use.
|
|
548
|
+
const thinkingSignal = state.agentActive && hasThinking && !hasText;
|
|
549
|
+
const coalesceThinkingSignals = state.config.mode === "aggressive";
|
|
550
|
+
if (!coalesceThinkingSignals) {
|
|
551
|
+
this.__compactTranscriptHiddenThinkingSignal = false;
|
|
552
|
+
if (!thinkingSignal && hasText) resetThinkingSignals();
|
|
553
|
+
} else {
|
|
554
|
+
if (this.__compactTranscriptHiddenThinkingSignal) return;
|
|
555
|
+
if (thinkingSignal && state.lastThinkingSignalComponent !== this) {
|
|
556
|
+
if (state.lastThinkingSignalComponent) {
|
|
557
|
+
state.lastThinkingSignalComponent.__compactTranscriptHiddenThinkingSignal = true;
|
|
558
|
+
state.lastThinkingSignalComponent.invalidate?.();
|
|
208
559
|
}
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
this.contentContainer.addChild(
|
|
214
|
-
new Text(themeModule.theme.italic(themeModule.theme.fg("thinkingText", limitPlain(`${label}${summary}`))), this.outputPad, 0),
|
|
215
|
-
);
|
|
216
|
-
|
|
217
|
-
const hasVisibleAfter = message.content
|
|
218
|
-
.slice(i + 1)
|
|
219
|
-
.some((c: any) => (c.type === "text" && c.text?.trim()) || (c.type === "thinking" && c.thinking?.trim()));
|
|
220
|
-
if (hasVisibleAfter) this.contentContainer.addChild(new Spacer(1));
|
|
560
|
+
state.lastThinkingSignalComponent = this;
|
|
561
|
+
state.thinkingSignalCount++;
|
|
562
|
+
} else if (!thinkingSignal && hasText) {
|
|
563
|
+
resetThinkingSignals();
|
|
221
564
|
}
|
|
565
|
+
}
|
|
222
566
|
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
567
|
+
const visible = message.content.filter(
|
|
568
|
+
(c: any) => (c.type === "text" && c.text?.trim()) || (c.type === "thinking" && c.thinking?.trim()),
|
|
569
|
+
);
|
|
570
|
+
if (visible.length) this.contentContainer.addChild(new Spacer(1));
|
|
571
|
+
|
|
572
|
+
let renderedThinkingSignal = false;
|
|
573
|
+
for (let i = 0; i < message.content.length; i++) {
|
|
574
|
+
const content = message.content[i];
|
|
575
|
+
if (content.type === "text" && content.text?.trim()) {
|
|
576
|
+
this.contentContainer.addChild(new Markdown(content.text.trim(), this.outputPad, 0, this.markdownTheme));
|
|
577
|
+
continue;
|
|
578
|
+
}
|
|
579
|
+
if (content.type !== "thinking" || !content.thinking?.trim()) continue;
|
|
580
|
+
|
|
581
|
+
let count = 1;
|
|
582
|
+
while (message.content[i + count]?.type === "thinking" && message.content[i + count]?.thinking?.trim()) {
|
|
583
|
+
count++;
|
|
584
|
+
}
|
|
585
|
+
i += count - 1;
|
|
586
|
+
|
|
587
|
+
if (coalesceThinkingSignals && thinkingSignal) {
|
|
588
|
+
if (renderedThinkingSignal) continue;
|
|
589
|
+
count = Math.max(count, state.thinkingSignalCount);
|
|
590
|
+
renderedThinkingSignal = true;
|
|
591
|
+
}
|
|
592
|
+
|
|
593
|
+
const label = count > 1 ? `${LABEL} (${count}x)` : LABEL;
|
|
594
|
+
const theme = state.currentTheme;
|
|
595
|
+
if (theme) {
|
|
596
|
+
this.contentContainer.addChild(new Text(theme.italic(theme.fg("thinkingText", label)), this.outputPad, 0));
|
|
597
|
+
} else {
|
|
598
|
+
this.contentContainer.addChild(new Text(label, this.outputPad, 0));
|
|
599
|
+
}
|
|
600
|
+
|
|
601
|
+
const hasVisibleAfter = message.content
|
|
602
|
+
.slice(i + 1)
|
|
603
|
+
.some((c: any) => (c.type === "text" && c.text?.trim()) || (c.type === "thinking" && c.thinking?.trim()));
|
|
604
|
+
if (hasVisibleAfter) this.contentContainer.addChild(new Spacer(1));
|
|
605
|
+
}
|
|
606
|
+
};
|
|
607
|
+
|
|
608
|
+
proto[ASSISTANT_PATCH_KEY] = { originalUpdateContent };
|
|
609
|
+
}
|
|
610
|
+
|
|
611
|
+
function patchRenderers() {
|
|
612
|
+
patchToolExecutionComponent();
|
|
613
|
+
patchAssistantMessageComponent();
|
|
614
|
+
}
|
|
615
|
+
|
|
616
|
+
function thinkingLabel() {
|
|
617
|
+
return state.consecutiveThinking > 1 ? `${LABEL} (${state.consecutiveThinking}x)` : LABEL;
|
|
618
|
+
}
|
|
619
|
+
|
|
620
|
+
function applyThinkingLabel(ctx: ExtensionContext) {
|
|
621
|
+
if (!isEnabled()) {
|
|
622
|
+
ctx.ui.setHiddenThinkingLabel();
|
|
623
|
+
return;
|
|
228
624
|
}
|
|
625
|
+
ctx.ui.setHiddenThinkingLabel(thinkingLabel());
|
|
229
626
|
}
|
|
230
627
|
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
628
|
+
function restoreConfigFromBranch(ctx: ExtensionContext) {
|
|
629
|
+
let nextConfig = cloneConfig(DEFAULT_CONFIG);
|
|
630
|
+
for (const entry of ctx.sessionManager.getBranch()) {
|
|
631
|
+
if (entry.type === "custom" && entry.customType === CONFIG_ENTRY_TYPE) {
|
|
632
|
+
nextConfig = normalizeConfig(entry.data);
|
|
633
|
+
}
|
|
634
|
+
}
|
|
635
|
+
state.config = nextConfig;
|
|
636
|
+
}
|
|
236
637
|
|
|
237
|
-
|
|
238
|
-
|
|
638
|
+
function persistConfig(pi: ExtensionAPI) {
|
|
639
|
+
pi.appendEntry(CONFIG_ENTRY_TYPE, cloneConfig(state.config));
|
|
640
|
+
}
|
|
641
|
+
|
|
642
|
+
function parseBoolean(value: string | undefined): boolean | undefined {
|
|
643
|
+
if (!value) return undefined;
|
|
644
|
+
const normalized = value.trim().toLowerCase();
|
|
645
|
+
if (["on", "true", "yes", "1", "enabled"].includes(normalized)) return true;
|
|
646
|
+
if (["off", "false", "no", "0", "disabled"].includes(normalized)) return false;
|
|
647
|
+
return undefined;
|
|
648
|
+
}
|
|
649
|
+
|
|
650
|
+
function splitList(value: string): string[] {
|
|
651
|
+
return value
|
|
652
|
+
.split(/[\s,]+/)
|
|
653
|
+
.map((item) => item.trim())
|
|
654
|
+
.filter(Boolean);
|
|
655
|
+
}
|
|
656
|
+
|
|
657
|
+
function formatList(values: string[]): string {
|
|
658
|
+
return values.length ? values.join(", ") : "(none)";
|
|
659
|
+
}
|
|
660
|
+
|
|
661
|
+
function formatTemplatesInput(templates = state.config.previewTemplates): string {
|
|
662
|
+
return Object.entries(templates)
|
|
663
|
+
.map(([tool, template]) => `${tool}=${template}`)
|
|
664
|
+
.join("; ");
|
|
665
|
+
}
|
|
666
|
+
|
|
667
|
+
function formatTemplatesDisplay(): string {
|
|
668
|
+
const keys = Object.keys(state.config.previewTemplates);
|
|
669
|
+
return keys.length ? keys.join(", ") : "(none)";
|
|
670
|
+
}
|
|
671
|
+
|
|
672
|
+
function parseTemplatesInput(value: string): Record<string, string> {
|
|
673
|
+
const trimmed = value.trim();
|
|
674
|
+
if (!trimmed || trimmed === "clear") return {};
|
|
675
|
+
const entries: Record<string, string> = {};
|
|
676
|
+
for (const part of trimmed.split(/;\s*/)) {
|
|
677
|
+
const index = part.indexOf("=");
|
|
678
|
+
if (index <= 0) continue;
|
|
679
|
+
const key = part.slice(0, index).trim();
|
|
680
|
+
const template = part.slice(index + 1).trim();
|
|
681
|
+
if (key && template) entries[key] = template;
|
|
239
682
|
}
|
|
683
|
+
return entries;
|
|
684
|
+
}
|
|
240
685
|
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
686
|
+
function describeConfig(): string {
|
|
687
|
+
return [
|
|
688
|
+
`mode: ${state.config.mode}`,
|
|
689
|
+
`custom tools: ${state.config.compactCustomTools ? "on" : "off"}`,
|
|
690
|
+
`failed tools visible: ${state.config.showFailedTools ? "on" : "off"}`,
|
|
691
|
+
`bash mutation anchors: ${state.config.showBashMutations ? "on" : "off"}`,
|
|
692
|
+
`always show: ${formatList(state.config.alwaysShowTools)}`,
|
|
693
|
+
`mutation tools: ${formatList(state.config.mutationTools)}`,
|
|
694
|
+
`templates: ${formatTemplatesDisplay()}`,
|
|
695
|
+
].join("\n");
|
|
696
|
+
}
|
|
697
|
+
|
|
698
|
+
function commandHelp(): string {
|
|
699
|
+
return [
|
|
700
|
+
"/compact-transcript [status]",
|
|
701
|
+
"/compact-transcript disabled|balanced|aggressive|debug",
|
|
702
|
+
"/compact-transcript custom-tools on|off",
|
|
703
|
+
"/compact-transcript failed on|off",
|
|
704
|
+
"/compact-transcript bash-mutations on|off",
|
|
705
|
+
"/compact-transcript always-show <tool[,tool]|/regex/|clear>",
|
|
706
|
+
"/compact-transcript mutation-tools <tool[,tool]|/regex/|clear>",
|
|
707
|
+
"/compact-transcript template <tool|/regex/> <template|clear>",
|
|
708
|
+
" template placeholders: {name}, {args}, {path}, {command}, {arg.foo}",
|
|
709
|
+
].join("\n");
|
|
710
|
+
}
|
|
711
|
+
|
|
712
|
+
// Mode only selects the rendering style; it must not silently rewrite the
|
|
713
|
+
// user's other toggles, or switching modes becomes destructive.
|
|
714
|
+
function setMode(mode: ModeInput) {
|
|
715
|
+
state.config.mode = mode === "off" ? "disabled" : mode;
|
|
716
|
+
}
|
|
717
|
+
|
|
718
|
+
function onOff(value: boolean): "on" | "off" {
|
|
719
|
+
return value ? "on" : "off";
|
|
720
|
+
}
|
|
721
|
+
|
|
722
|
+
function createInputSubmenu(
|
|
723
|
+
title: string,
|
|
724
|
+
prefill: string,
|
|
725
|
+
done: (selectedValue?: string) => void,
|
|
726
|
+
help: string,
|
|
727
|
+
): { render(width: number): string[]; handleInput(data: string): void; invalidate(): void } {
|
|
728
|
+
const input = new Input();
|
|
729
|
+
input.setValue(prefill);
|
|
730
|
+
// Input intentionally keeps cursor position when setValue() is called; for
|
|
731
|
+
// edit-in-place settings, start at the end like a normal command-line field.
|
|
732
|
+
(input as any).cursor = prefill.length;
|
|
733
|
+
input.onSubmit = (value) => done(value.trim());
|
|
734
|
+
input.onEscape = () => done(undefined);
|
|
735
|
+
return {
|
|
736
|
+
render(width: number) {
|
|
737
|
+
const theme = state.currentTheme;
|
|
738
|
+
const container = new Container();
|
|
739
|
+
container.addChild(new Text(theme ? theme.fg("accent", theme.bold(title)) : title, 0, 0));
|
|
740
|
+
container.addChild(new Text(theme ? theme.fg("dim", help) : help, 0, 0));
|
|
741
|
+
container.addChild(new Spacer(1));
|
|
742
|
+
container.addChild(input);
|
|
743
|
+
container.addChild(new Spacer(1));
|
|
744
|
+
container.addChild(new Text(theme ? theme.fg("dim", "Enter saves · Esc cancels") : "Enter saves · Esc cancels", 0, 0));
|
|
745
|
+
return container.render(width);
|
|
746
|
+
},
|
|
747
|
+
handleInput(data: string) {
|
|
748
|
+
input.handleInput(data);
|
|
749
|
+
},
|
|
750
|
+
invalidate() {
|
|
751
|
+
input.invalidate();
|
|
752
|
+
},
|
|
753
|
+
};
|
|
754
|
+
}
|
|
755
|
+
|
|
756
|
+
function buildSettingsItems(): SettingItem[] {
|
|
757
|
+
return [
|
|
758
|
+
{
|
|
759
|
+
id: "mode",
|
|
760
|
+
label: "Mode",
|
|
761
|
+
currentValue: state.config.mode,
|
|
762
|
+
values: ["balanced", "aggressive", "debug", "disabled"],
|
|
763
|
+
description:
|
|
764
|
+
"balanced compacts normal tool bursts; aggressive uses shorter previews; debug keeps rows visible while tuning; disabled turns compact-transcript rendering off.",
|
|
765
|
+
},
|
|
766
|
+
{
|
|
767
|
+
id: "customTools",
|
|
768
|
+
label: "Custom tools",
|
|
769
|
+
currentValue: onOff(state.config.compactCustomTools),
|
|
770
|
+
values: ["on", "off"],
|
|
771
|
+
description: "When on, compact custom/external tools added by extensions. When off, only built-in tools are compacted.",
|
|
772
|
+
},
|
|
773
|
+
{
|
|
774
|
+
id: "failedTools",
|
|
775
|
+
label: "Show failures",
|
|
776
|
+
currentValue: onOff(state.config.showFailedTools),
|
|
777
|
+
values: ["on", "off"],
|
|
778
|
+
description: "Keep failed tools visible even when they are part of an otherwise compacted burst.",
|
|
779
|
+
},
|
|
780
|
+
{
|
|
781
|
+
id: "bashMutations",
|
|
782
|
+
label: "Bash anchors",
|
|
783
|
+
currentValue: onOff(state.config.showBashMutations),
|
|
784
|
+
values: ["on", "off"],
|
|
785
|
+
description: "Keep destructive-looking bash commands visible as anchors and break compact tool bursts around them.",
|
|
786
|
+
},
|
|
787
|
+
{
|
|
788
|
+
id: "alwaysShowTools",
|
|
789
|
+
label: "Always show",
|
|
790
|
+
currentValue: formatList(state.config.alwaysShowTools),
|
|
791
|
+
description: "Press Enter to edit comma-separated tool names, wildcards, or /regex/ rules that should always stay visible.",
|
|
792
|
+
submenu: (_currentValue, done) =>
|
|
793
|
+
createInputSubmenu(
|
|
794
|
+
"Always show tools",
|
|
795
|
+
state.config.alwaysShowTools.join(", "),
|
|
796
|
+
done,
|
|
797
|
+
"Comma-separated names, wildcards, or /regex/. Empty input clears the list.",
|
|
798
|
+
),
|
|
799
|
+
},
|
|
800
|
+
{
|
|
801
|
+
id: "mutationTools",
|
|
802
|
+
label: "Mutation tools",
|
|
803
|
+
currentValue: formatList(state.config.mutationTools),
|
|
804
|
+
description: "Press Enter to edit comma-separated tool names, wildcards, or /regex/ rules that act as mutation anchors.",
|
|
805
|
+
submenu: (_currentValue, done) =>
|
|
806
|
+
createInputSubmenu(
|
|
807
|
+
"Mutation tools",
|
|
808
|
+
state.config.mutationTools.join(", "),
|
|
809
|
+
done,
|
|
810
|
+
"Comma-separated names, wildcards, or /regex/. Empty input clears the list.",
|
|
811
|
+
),
|
|
812
|
+
},
|
|
813
|
+
{
|
|
814
|
+
id: "templates",
|
|
815
|
+
label: "Templates",
|
|
816
|
+
currentValue: formatTemplatesDisplay(),
|
|
817
|
+
description: "Press Enter to edit preview templates as semicolon-separated tool=template pairs.",
|
|
818
|
+
submenu: (_currentValue, done) =>
|
|
819
|
+
createInputSubmenu(
|
|
820
|
+
"Preview templates",
|
|
821
|
+
formatTemplatesInput(),
|
|
822
|
+
done,
|
|
823
|
+
"Use tool=template; /regex/=template. Placeholders: {name}, {args}, {path}, {command}, {arg.foo}. `;` separates pairs, so templates cannot contain it.",
|
|
824
|
+
),
|
|
825
|
+
},
|
|
826
|
+
{
|
|
827
|
+
id: "reset",
|
|
828
|
+
label: "Reset defaults",
|
|
829
|
+
currentValue: "press enter",
|
|
830
|
+
values: ["press enter"],
|
|
831
|
+
description: "Restore the default compact-transcript configuration.",
|
|
832
|
+
},
|
|
833
|
+
];
|
|
834
|
+
}
|
|
835
|
+
|
|
836
|
+
function applySettingsItem(id: string, value: string, pi: ExtensionAPI, ctx: ExtensionContext) {
|
|
837
|
+
switch (id) {
|
|
838
|
+
case "mode":
|
|
839
|
+
setMode(value as ModeInput);
|
|
840
|
+
break;
|
|
841
|
+
case "customTools":
|
|
842
|
+
state.config.compactCustomTools = value === "on";
|
|
843
|
+
break;
|
|
844
|
+
case "failedTools":
|
|
845
|
+
state.config.showFailedTools = value === "on";
|
|
846
|
+
break;
|
|
847
|
+
case "bashMutations":
|
|
848
|
+
state.config.showBashMutations = value === "on";
|
|
849
|
+
break;
|
|
850
|
+
case "alwaysShowTools":
|
|
851
|
+
state.config.alwaysShowTools = splitList(value);
|
|
852
|
+
break;
|
|
853
|
+
case "mutationTools":
|
|
854
|
+
state.config.mutationTools = splitList(value);
|
|
855
|
+
break;
|
|
856
|
+
case "templates":
|
|
857
|
+
state.config.previewTemplates = parseTemplatesInput(value);
|
|
858
|
+
break;
|
|
859
|
+
case "reset":
|
|
860
|
+
state.config = cloneConfig(DEFAULT_CONFIG);
|
|
861
|
+
break;
|
|
862
|
+
default:
|
|
863
|
+
return;
|
|
244
864
|
}
|
|
865
|
+
persistConfig(pi);
|
|
866
|
+
applyThinkingLabel(ctx);
|
|
867
|
+
}
|
|
245
868
|
|
|
246
|
-
|
|
247
|
-
|
|
869
|
+
async function showSettingsPanel(pi: ExtensionAPI, ctx: ExtensionContext): Promise<void> {
|
|
870
|
+
if (ctx.mode !== "tui") {
|
|
871
|
+
ctx.ui.notify(describeConfig(), "info");
|
|
872
|
+
return;
|
|
248
873
|
}
|
|
874
|
+
state.currentTheme = ctx.ui.theme;
|
|
875
|
+
|
|
876
|
+
await ctx.ui.custom<void>((tui, theme, _keybindings, done) => {
|
|
877
|
+
let settingsList: SettingsList;
|
|
878
|
+
const refreshValues = () => {
|
|
879
|
+
for (const item of buildSettingsItems()) {
|
|
880
|
+
settingsList.updateValue(item.id, item.currentValue);
|
|
881
|
+
}
|
|
882
|
+
};
|
|
883
|
+
|
|
884
|
+
settingsList = new SettingsList(
|
|
885
|
+
buildSettingsItems(),
|
|
886
|
+
SETTINGS_LIST_HEIGHT,
|
|
887
|
+
getSettingsListTheme(),
|
|
888
|
+
(id, value) => {
|
|
889
|
+
applySettingsItem(id, value, pi, ctx);
|
|
890
|
+
refreshValues();
|
|
891
|
+
tui.requestRender();
|
|
892
|
+
},
|
|
893
|
+
() => done(undefined),
|
|
894
|
+
);
|
|
895
|
+
|
|
896
|
+
return {
|
|
897
|
+
render(width: number) {
|
|
898
|
+
state.currentTheme = theme;
|
|
899
|
+
const title = theme.fg("accent", theme.bold("Compact Transcript"));
|
|
900
|
+
const subtitle = theme.fg("dim", "Configure transcript compaction. Enter/Space changes selected item; Esc closes.");
|
|
901
|
+
const container = new Container();
|
|
902
|
+
container.addChild(new Text(title, 0, 0));
|
|
903
|
+
container.addChild(new Text(subtitle, 0, 0));
|
|
904
|
+
container.addChild(new Spacer(1));
|
|
905
|
+
container.addChild(settingsList);
|
|
906
|
+
return container.render(width);
|
|
907
|
+
},
|
|
908
|
+
invalidate() {
|
|
909
|
+
settingsList.invalidate();
|
|
910
|
+
},
|
|
911
|
+
handleInput(data: string) {
|
|
912
|
+
settingsList.handleInput?.(data);
|
|
913
|
+
tui.requestRender();
|
|
914
|
+
},
|
|
915
|
+
};
|
|
916
|
+
});
|
|
917
|
+
}
|
|
918
|
+
|
|
919
|
+
function registerCommand(pi: ExtensionAPI) {
|
|
920
|
+
pi.registerCommand("compact-transcript", {
|
|
921
|
+
description: "Configure compact transcript rendering: disabled, balanced, aggressive, debug, and tool rules.",
|
|
922
|
+
getArgumentCompletions(prefix: string) {
|
|
923
|
+
const commands = [
|
|
924
|
+
"status",
|
|
925
|
+
"help",
|
|
926
|
+
"reset",
|
|
927
|
+
"disabled",
|
|
928
|
+
"off",
|
|
929
|
+
"balanced",
|
|
930
|
+
"aggressive",
|
|
931
|
+
"debug",
|
|
932
|
+
"custom-tools on",
|
|
933
|
+
"custom-tools off",
|
|
934
|
+
"failed on",
|
|
935
|
+
"failed off",
|
|
936
|
+
"bash-mutations on",
|
|
937
|
+
"bash-mutations off",
|
|
938
|
+
"always-show clear",
|
|
939
|
+
"mutation-tools clear",
|
|
940
|
+
"template ",
|
|
941
|
+
];
|
|
942
|
+
return commands
|
|
943
|
+
.filter((value) => value.startsWith(prefix.trimStart()))
|
|
944
|
+
.map((value) => ({ value, label: value }));
|
|
945
|
+
},
|
|
946
|
+
handler: async (args, ctx) => {
|
|
947
|
+
captureTheme(ctx);
|
|
948
|
+
const trimmed = args.trim();
|
|
949
|
+
if (trimmed === "help" && ctx.mode !== "tui") {
|
|
950
|
+
ctx.ui.notify(commandHelp(), "info");
|
|
951
|
+
return;
|
|
952
|
+
}
|
|
953
|
+
if (!trimmed || trimmed === "status" || trimmed === "help") {
|
|
954
|
+
await showSettingsPanel(pi, ctx);
|
|
955
|
+
return;
|
|
956
|
+
}
|
|
957
|
+
if (trimmed === "reset") {
|
|
958
|
+
state.config = cloneConfig(DEFAULT_CONFIG);
|
|
959
|
+
persistConfig(pi);
|
|
960
|
+
applyThinkingLabel(ctx);
|
|
961
|
+
await showSettingsPanel(pi, ctx);
|
|
962
|
+
return;
|
|
963
|
+
}
|
|
964
|
+
if (["disabled", "off", "balanced", "aggressive", "debug"].includes(trimmed)) {
|
|
965
|
+
setMode(trimmed as ModeInput);
|
|
966
|
+
persistConfig(pi);
|
|
967
|
+
applyThinkingLabel(ctx);
|
|
968
|
+
await showSettingsPanel(pi, ctx);
|
|
969
|
+
return;
|
|
970
|
+
}
|
|
971
|
+
|
|
972
|
+
const [command, ...rest] = trimmed.split(/\s+/);
|
|
973
|
+
const restText = rest.join(" ").trim();
|
|
974
|
+
if (command === "custom-tools" || command === "failed" || command === "bash-mutations") {
|
|
975
|
+
const value = parseBoolean(rest[0]);
|
|
976
|
+
if (value === undefined) {
|
|
977
|
+
ctx.ui.notify(`Usage: /compact-transcript ${command} on|off`, "error");
|
|
978
|
+
return;
|
|
979
|
+
}
|
|
980
|
+
if (command === "custom-tools") state.config.compactCustomTools = value;
|
|
981
|
+
if (command === "failed") state.config.showFailedTools = value;
|
|
982
|
+
if (command === "bash-mutations") state.config.showBashMutations = value;
|
|
983
|
+
persistConfig(pi);
|
|
984
|
+
await showSettingsPanel(pi, ctx);
|
|
985
|
+
return;
|
|
986
|
+
}
|
|
987
|
+
|
|
988
|
+
if (command === "always-show" || command === "mutation-tools") {
|
|
989
|
+
if (!restText) {
|
|
990
|
+
ctx.ui.notify(`Usage: /compact-transcript ${command} <tool[,tool]|/regex/|clear>`, "error");
|
|
991
|
+
return;
|
|
992
|
+
}
|
|
993
|
+
const values = restText === "clear" ? [] : splitList(restText);
|
|
994
|
+
if (command === "always-show") state.config.alwaysShowTools = values;
|
|
995
|
+
else state.config.mutationTools = values;
|
|
996
|
+
persistConfig(pi);
|
|
997
|
+
await showSettingsPanel(pi, ctx);
|
|
998
|
+
return;
|
|
999
|
+
}
|
|
1000
|
+
|
|
1001
|
+
if (command === "template") {
|
|
1002
|
+
const [tool, ...templateParts] = rest;
|
|
1003
|
+
const template = templateParts.join(" ").trim();
|
|
1004
|
+
if (!tool || !template) {
|
|
1005
|
+
ctx.ui.notify("Usage: /compact-transcript template <tool|/regex/> <template|clear>", "error");
|
|
1006
|
+
return;
|
|
1007
|
+
}
|
|
1008
|
+
if (template === "clear") delete state.config.previewTemplates[tool];
|
|
1009
|
+
else state.config.previewTemplates[tool] = template;
|
|
1010
|
+
persistConfig(pi);
|
|
1011
|
+
await showSettingsPanel(pi, ctx);
|
|
1012
|
+
return;
|
|
1013
|
+
}
|
|
1014
|
+
|
|
1015
|
+
ctx.ui.notify(`Unknown option "${trimmed}".\n${commandHelp()}`, "error");
|
|
1016
|
+
},
|
|
1017
|
+
});
|
|
1018
|
+
}
|
|
1019
|
+
|
|
1020
|
+
export default function compactTranscript(pi: ExtensionAPI) {
|
|
1021
|
+
patchRenderers();
|
|
1022
|
+
registerCommand(pi);
|
|
249
1023
|
|
|
250
1024
|
pi.on("session_start", async (_event, ctx) => {
|
|
1025
|
+
restoreConfigFromBranch(ctx);
|
|
1026
|
+
captureTheme(ctx);
|
|
1027
|
+
applyThinkingLabel(ctx);
|
|
1028
|
+
ctx.ui.setWorkingMessage();
|
|
1029
|
+
// Clear any footer status left behind by pre-0.4 versions of this extension.
|
|
1030
|
+
ctx.ui.setStatus(STATUS_KEY, undefined);
|
|
1031
|
+
});
|
|
1032
|
+
|
|
1033
|
+
pi.on("session_shutdown", async (_event, ctx) => {
|
|
1034
|
+
ctx.ui.setHiddenThinkingLabel();
|
|
1035
|
+
ctx.ui.setStatus(STATUS_KEY, undefined);
|
|
1036
|
+
ctx.ui.setWorkingMessage();
|
|
1037
|
+
});
|
|
1038
|
+
|
|
1039
|
+
pi.on("agent_start", (_event, ctx) => {
|
|
1040
|
+
state.agentActive = true;
|
|
1041
|
+
captureTheme(ctx);
|
|
1042
|
+
resetToolRun();
|
|
1043
|
+
resetThinkingSignals();
|
|
1044
|
+
state.consecutiveThinking = 0;
|
|
251
1045
|
applyThinkingLabel(ctx);
|
|
252
|
-
ctx.ui.setWorkingMessage(LABEL);
|
|
253
1046
|
});
|
|
254
1047
|
|
|
255
|
-
pi.on("
|
|
256
|
-
|
|
1048
|
+
pi.on("agent_end", (_event, _ctx) => {
|
|
1049
|
+
state.agentActive = false;
|
|
1050
|
+
state.currentNoiseBurst = [];
|
|
1051
|
+
resetThinkingSignals();
|
|
257
1052
|
});
|
|
258
1053
|
|
|
259
1054
|
pi.on("turn_start", (_event, ctx) => {
|
|
260
|
-
consecutiveThinking = 0;
|
|
1055
|
+
state.consecutiveThinking = 0;
|
|
1056
|
+
captureTheme(ctx);
|
|
261
1057
|
applyThinkingLabel(ctx);
|
|
262
1058
|
});
|
|
263
1059
|
|
|
264
|
-
pi.on("message_update", (event
|
|
1060
|
+
pi.on("message_update", (event, ctx) => {
|
|
1061
|
+
captureTheme(ctx);
|
|
265
1062
|
const type = event.assistantMessageEvent?.type;
|
|
266
1063
|
if (type === "thinking_start") {
|
|
267
|
-
consecutiveThinking++;
|
|
1064
|
+
state.consecutiveThinking++;
|
|
268
1065
|
applyThinkingLabel(ctx);
|
|
269
1066
|
return;
|
|
270
1067
|
}
|
|
271
1068
|
if (type && !type.startsWith("thinking_")) {
|
|
272
|
-
consecutiveThinking = 0;
|
|
1069
|
+
state.consecutiveThinking = 0;
|
|
273
1070
|
applyThinkingLabel(ctx);
|
|
274
|
-
if (type === "text_delta" || type === "text_start")
|
|
1071
|
+
if (type === "text_delta" || type === "text_start") {
|
|
1072
|
+
resetToolRun();
|
|
1073
|
+
resetThinkingSignals();
|
|
1074
|
+
}
|
|
275
1075
|
}
|
|
276
1076
|
});
|
|
277
1077
|
|
|
278
|
-
pi.on("tool_execution_start", (event
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
id: event.toolCallId,
|
|
282
|
-
name: event.toolName,
|
|
283
|
-
args: event.args,
|
|
284
|
-
preview: previewFor(event.toolName, event.args),
|
|
285
|
-
};
|
|
286
|
-
current.push(info);
|
|
287
|
-
byId.set(info.id, info);
|
|
288
|
-
previous?.invalidate?.();
|
|
1078
|
+
pi.on("tool_execution_start", (event, ctx) => {
|
|
1079
|
+
captureTheme(ctx);
|
|
1080
|
+
beginTool(event.toolCallId, event.toolName, event.args);
|
|
289
1081
|
});
|
|
290
1082
|
|
|
291
|
-
pi.on("
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
const suffix = resultPreview(event.result);
|
|
295
|
-
if (suffix) info.result = suffix;
|
|
296
|
-
info.invalidate?.();
|
|
1083
|
+
pi.on("tool_execution_update", (event, ctx) => {
|
|
1084
|
+
captureTheme(ctx);
|
|
1085
|
+
updateToolResult(event.toolCallId, event.partialResult, false, true);
|
|
297
1086
|
});
|
|
298
1087
|
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
byId.set(toolCallId, info);
|
|
304
|
-
if (!current.some((t) => t.id === toolCallId)) current.push(info);
|
|
305
|
-
}
|
|
306
|
-
info.invalidate = context.invalidate;
|
|
307
|
-
info.args = args;
|
|
308
|
-
info.preview = previewFor(name, args);
|
|
309
|
-
|
|
310
|
-
if (current[current.length - 1]?.id !== toolCallId) return "";
|
|
311
|
-
|
|
312
|
-
const details = info.result ? `${info.preview} {${oneLine(info.result)}}` : info.preview;
|
|
313
|
-
if (current.length === 1) return theme.fg("muted", limitPlain(details));
|
|
314
|
-
const prefix = `Used ${current.length} tools `;
|
|
315
|
-
return theme.fg("toolTitle", prefix) + theme.fg("muted", limitPlain(details, Math.max(20, (process.stdout.columns || 100) - prefix.length - 6)));
|
|
316
|
-
}
|
|
317
|
-
|
|
318
|
-
for (const name of BUILT_INS) {
|
|
319
|
-
const base = getBuiltInTools(process.cwd())[name] as any;
|
|
320
|
-
pi.registerTool({
|
|
321
|
-
...base,
|
|
322
|
-
name,
|
|
323
|
-
label: name,
|
|
324
|
-
renderShell: "self",
|
|
325
|
-
async execute(toolCallId: string, params: any, signal: AbortSignal | undefined, onUpdate: any, ctx: ExtensionContext) {
|
|
326
|
-
const tool = (getBuiltInTools(ctx.cwd) as any)[name];
|
|
327
|
-
return tool.execute(toolCallId, params, signal, onUpdate, ctx);
|
|
328
|
-
},
|
|
329
|
-
renderCall(args: any, theme: any, context: any) {
|
|
330
|
-
const text = (context.lastComponent as Text | undefined) ?? new Text("", 0, 0);
|
|
331
|
-
text.setText(compactLine(context.toolCallId, name, args, theme, context));
|
|
332
|
-
return text;
|
|
333
|
-
},
|
|
334
|
-
renderResult(_result: any, _options: any, _theme: any, _context: any) {
|
|
335
|
-
return new Text("", 0, 0);
|
|
336
|
-
},
|
|
337
|
-
});
|
|
338
|
-
}
|
|
1088
|
+
pi.on("tool_execution_end", (event, ctx) => {
|
|
1089
|
+
captureTheme(ctx);
|
|
1090
|
+
updateToolResult(event.toolCallId, event.result, event.isError, false);
|
|
1091
|
+
});
|
|
339
1092
|
}
|