pi-compact-transcript 0.2.1 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +43 -5
- package/extensions/compact-transcript.ts +938 -206
- 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,57 +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
|
+
};
|
|
30
62
|
|
|
31
|
-
|
|
32
|
-
|
|
63
|
+
const DEFAULT_CONFIG: CompactTranscriptConfig = {
|
|
64
|
+
mode: "balanced",
|
|
65
|
+
compactCustomTools: true,
|
|
66
|
+
showFailedTools: true,
|
|
67
|
+
showBashMutations: true,
|
|
68
|
+
alwaysShowTools: [],
|
|
69
|
+
mutationTools: [],
|
|
70
|
+
previewTemplates: {},
|
|
71
|
+
};
|
|
33
72
|
|
|
34
|
-
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");
|
|
35
76
|
|
|
36
|
-
function
|
|
77
|
+
function cloneConfig(config: CompactTranscriptConfig): CompactTranscriptConfig {
|
|
37
78
|
return {
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
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 },
|
|
45
86
|
};
|
|
46
87
|
}
|
|
47
88
|
|
|
48
|
-
function
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
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;
|
|
55
144
|
}
|
|
56
145
|
|
|
57
146
|
function shortenPath(path: unknown): string {
|
|
@@ -66,7 +155,17 @@ function oneLine(value: unknown): string {
|
|
|
66
155
|
.trim();
|
|
67
156
|
}
|
|
68
157
|
|
|
69
|
-
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 {
|
|
70
169
|
const clean = oneLine(text);
|
|
71
170
|
if (clean.length <= max) return clean;
|
|
72
171
|
return `${clean.slice(0, Math.max(0, max - 1))}…`;
|
|
@@ -76,7 +175,99 @@ function quote(s: string): string {
|
|
|
76
175
|
return JSON.stringify(s);
|
|
77
176
|
}
|
|
78
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
|
+
|
|
79
267
|
function previewFor(name: string, args: any): string {
|
|
268
|
+
const template = findTemplate(name);
|
|
269
|
+
if (template) return renderTemplate(template, name, args);
|
|
270
|
+
|
|
80
271
|
switch (name) {
|
|
81
272
|
case "bash":
|
|
82
273
|
return `$ ${oneLine(args?.command || "...")}`;
|
|
@@ -107,254 +298,795 @@ function previewFor(name: string, args: any): string {
|
|
|
107
298
|
case "ls":
|
|
108
299
|
return `ls ${shortenPath(args?.path) || "."}`;
|
|
109
300
|
default:
|
|
110
|
-
return `${name} ${
|
|
301
|
+
return `${name} ${safeJson(args ?? {})}`;
|
|
111
302
|
}
|
|
112
303
|
}
|
|
113
304
|
|
|
114
|
-
function resultPreview(result: any): string {
|
|
305
|
+
function resultPreview(result: any, isPartial = false): string {
|
|
115
306
|
const text = Array.isArray(result?.content)
|
|
116
307
|
? result.content.find((c: any) => c?.type === "text" && typeof c.text === "string")?.text
|
|
117
308
|
: undefined;
|
|
118
|
-
if (!text) return "";
|
|
309
|
+
if (!text) return isPartial ? "running" : "";
|
|
119
310
|
const lines = String(text).trim().split("\n").filter(Boolean);
|
|
120
|
-
if (lines.length === 0) return "";
|
|
311
|
+
if (lines.length === 0) return isPartial ? "running" : "";
|
|
121
312
|
if (lines.length === 1) return lines[0];
|
|
122
313
|
return `${lines.length} lines`;
|
|
123
314
|
}
|
|
124
315
|
|
|
125
|
-
function
|
|
126
|
-
|
|
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(", ");
|
|
127
325
|
}
|
|
128
326
|
|
|
129
|
-
function
|
|
130
|
-
|
|
327
|
+
function resetToolRun() {
|
|
328
|
+
state.toolsById = new Map();
|
|
329
|
+
state.currentNoiseBurst = [];
|
|
330
|
+
state.hiddenToolIds = new Set();
|
|
131
331
|
}
|
|
132
332
|
|
|
133
|
-
function
|
|
134
|
-
|
|
135
|
-
|
|
333
|
+
function resetThinkingSignals() {
|
|
334
|
+
state.lastThinkingSignalComponent = undefined;
|
|
335
|
+
state.thinkingSignalCount = 0;
|
|
336
|
+
}
|
|
337
|
+
|
|
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);
|
|
354
|
+
}
|
|
136
355
|
}
|
|
137
356
|
|
|
138
357
|
function upsertToolInfo(id: string, name: string, args: any, invalidate?: () => void): ToolInfo {
|
|
139
|
-
let info = toolsById.get(id);
|
|
358
|
+
let info = state.toolsById.get(id);
|
|
140
359
|
if (!info) {
|
|
141
|
-
info = { id, name, args, preview: previewFor(name, args) };
|
|
142
|
-
toolsById.set(id, info);
|
|
143
|
-
if (!currentTools.some((tool) => tool.id === id)) currentTools.push(info);
|
|
360
|
+
info = { id, name, args, preview: previewFor(name, args), kind: classifyTool(name, args) };
|
|
361
|
+
state.toolsById.set(id, info);
|
|
144
362
|
}
|
|
145
363
|
info.name = name;
|
|
146
364
|
info.args = args;
|
|
147
365
|
info.preview = previewFor(name, args);
|
|
366
|
+
info.kind = classifyTool(name, args);
|
|
148
367
|
if (invalidate) info.invalidate = invalidate;
|
|
149
368
|
return info;
|
|
150
369
|
}
|
|
151
370
|
|
|
152
|
-
function
|
|
153
|
-
const
|
|
154
|
-
const
|
|
155
|
-
|
|
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
|
+
}
|
|
156
382
|
|
|
157
|
-
if (
|
|
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
|
+
}
|
|
158
393
|
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
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?.();
|
|
163
399
|
}
|
|
164
400
|
|
|
165
|
-
function
|
|
166
|
-
|
|
167
|
-
|
|
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);
|
|
168
412
|
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
if (
|
|
172
|
-
|
|
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));
|
|
173
421
|
}
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
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
|
+
);
|
|
177
442
|
}
|
|
178
443
|
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
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,
|
|
189
503
|
);
|
|
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
504
|
|
|
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;
|
|
505
|
+
if (!line) {
|
|
506
|
+
this.hideComponent = true;
|
|
507
|
+
return;
|
|
227
508
|
}
|
|
228
509
|
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
const original = Component.prototype.updateContent;
|
|
510
|
+
this.selfRenderContainer.addChild(new Text(line, 0, 0));
|
|
511
|
+
};
|
|
232
512
|
|
|
233
|
-
|
|
234
|
-
|
|
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
|
+
};
|
|
235
518
|
|
|
236
|
-
|
|
237
|
-
|
|
519
|
+
proto[TOOL_PATCH_KEY] = { originalUpdateDisplay, originalRender };
|
|
520
|
+
}
|
|
238
521
|
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
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;
|
|
243
527
|
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
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?.();
|
|
258
559
|
}
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
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));
|
|
560
|
+
state.lastThinkingSignalComponent = this;
|
|
561
|
+
state.thinkingSignalCount++;
|
|
562
|
+
} else if (!thinkingSignal && hasText) {
|
|
563
|
+
resetThinkingSignals();
|
|
271
564
|
}
|
|
565
|
+
}
|
|
272
566
|
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
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;
|
|
624
|
+
}
|
|
625
|
+
ctx.ui.setHiddenThinkingLabel(thinkingLabel());
|
|
626
|
+
}
|
|
627
|
+
|
|
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
|
+
}
|
|
278
634
|
}
|
|
635
|
+
state.config = nextConfig;
|
|
279
636
|
}
|
|
280
637
|
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
638
|
+
function persistConfig(pi: ExtensionAPI) {
|
|
639
|
+
pi.appendEntry(CONFIG_ENTRY_TYPE, cloneConfig(state.config));
|
|
640
|
+
}
|
|
284
641
|
|
|
285
|
-
|
|
286
|
-
|
|
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;
|
|
287
682
|
}
|
|
683
|
+
return entries;
|
|
684
|
+
}
|
|
288
685
|
|
|
289
|
-
|
|
290
|
-
|
|
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;
|
|
291
864
|
}
|
|
865
|
+
persistConfig(pi);
|
|
866
|
+
applyThinkingLabel(ctx);
|
|
867
|
+
}
|
|
292
868
|
|
|
293
|
-
|
|
294
|
-
|
|
869
|
+
async function showSettingsPanel(pi: ExtensionAPI, ctx: ExtensionContext): Promise<void> {
|
|
870
|
+
if (ctx.mode !== "tui") {
|
|
871
|
+
ctx.ui.notify(describeConfig(), "info");
|
|
872
|
+
return;
|
|
295
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);
|
|
296
1023
|
|
|
297
1024
|
pi.on("session_start", async (_event, ctx) => {
|
|
1025
|
+
restoreConfigFromBranch(ctx);
|
|
1026
|
+
captureTheme(ctx);
|
|
298
1027
|
applyThinkingLabel(ctx);
|
|
299
|
-
ctx.ui.setWorkingMessage(
|
|
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);
|
|
300
1031
|
});
|
|
301
1032
|
|
|
302
|
-
pi.on("
|
|
303
|
-
|
|
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;
|
|
1045
|
+
applyThinkingLabel(ctx);
|
|
1046
|
+
});
|
|
1047
|
+
|
|
1048
|
+
pi.on("agent_end", (_event, _ctx) => {
|
|
1049
|
+
state.agentActive = false;
|
|
1050
|
+
state.currentNoiseBurst = [];
|
|
1051
|
+
resetThinkingSignals();
|
|
304
1052
|
});
|
|
305
1053
|
|
|
306
1054
|
pi.on("turn_start", (_event, ctx) => {
|
|
307
|
-
consecutiveThinking = 0;
|
|
1055
|
+
state.consecutiveThinking = 0;
|
|
1056
|
+
captureTheme(ctx);
|
|
308
1057
|
applyThinkingLabel(ctx);
|
|
309
1058
|
});
|
|
310
1059
|
|
|
311
|
-
pi.on("message_update", (event
|
|
1060
|
+
pi.on("message_update", (event, ctx) => {
|
|
1061
|
+
captureTheme(ctx);
|
|
312
1062
|
const type = event.assistantMessageEvent?.type;
|
|
313
1063
|
if (type === "thinking_start") {
|
|
314
|
-
consecutiveThinking++;
|
|
1064
|
+
state.consecutiveThinking++;
|
|
315
1065
|
applyThinkingLabel(ctx);
|
|
316
1066
|
return;
|
|
317
1067
|
}
|
|
318
1068
|
if (type && !type.startsWith("thinking_")) {
|
|
319
|
-
consecutiveThinking = 0;
|
|
1069
|
+
state.consecutiveThinking = 0;
|
|
320
1070
|
applyThinkingLabel(ctx);
|
|
321
|
-
if (type === "text_delta" || type === "text_start")
|
|
1071
|
+
if (type === "text_delta" || type === "text_start") {
|
|
1072
|
+
resetToolRun();
|
|
1073
|
+
resetThinkingSignals();
|
|
1074
|
+
}
|
|
322
1075
|
}
|
|
323
1076
|
});
|
|
324
1077
|
|
|
325
|
-
pi.on("tool_execution_start", (event
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
previous?.invalidate?.();
|
|
1078
|
+
pi.on("tool_execution_start", (event, ctx) => {
|
|
1079
|
+
captureTheme(ctx);
|
|
1080
|
+
beginTool(event.toolCallId, event.toolName, event.args);
|
|
329
1081
|
});
|
|
330
1082
|
|
|
331
|
-
pi.on("
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
const suffix = resultPreview(event.result);
|
|
335
|
-
if (suffix) info.result = suffix;
|
|
336
|
-
info.invalidate?.();
|
|
1083
|
+
pi.on("tool_execution_update", (event, ctx) => {
|
|
1084
|
+
captureTheme(ctx);
|
|
1085
|
+
updateToolResult(event.toolCallId, event.partialResult, false, true);
|
|
337
1086
|
});
|
|
338
1087
|
|
|
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
|
-
}
|
|
1088
|
+
pi.on("tool_execution_end", (event, ctx) => {
|
|
1089
|
+
captureTheme(ctx);
|
|
1090
|
+
updateToolResult(event.toolCallId, event.result, event.isError, false);
|
|
1091
|
+
});
|
|
360
1092
|
}
|