pi-compact-transcript 0.4.0 → 0.6.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 +29 -51
- package/extensions/compact-transcript.ts +488 -685
- package/package.json +1 -1
|
@@ -1,37 +1,24 @@
|
|
|
1
1
|
import type { ExtensionAPI, ExtensionContext, Theme } from "@earendil-works/pi-coding-agent";
|
|
2
|
-
import { AssistantMessageComponent,
|
|
3
|
-
import {
|
|
2
|
+
import { AssistantMessageComponent, ToolExecutionComponent } from "@earendil-works/pi-coding-agent";
|
|
3
|
+
import { Markdown, Spacer, Text } from "@earendil-works/pi-tui";
|
|
4
4
|
import { homedir } from "node:os";
|
|
5
5
|
|
|
6
|
-
const LABEL = "Thinking...";
|
|
7
6
|
// Older versions of this extension wrote a footer status under this key; it is
|
|
8
7
|
// kept only to clear that status once per session for users upgrading in place.
|
|
9
8
|
const STATUS_KEY = "compact-transcript";
|
|
10
9
|
const CONFIG_ENTRY_TYPE = "compact-transcript-config";
|
|
11
|
-
|
|
12
|
-
const BUILT_INS = new Set(["bash", "read", "write", "edit", "grep", "find", "ls"]);
|
|
10
|
+
const SUMMARY_ENTRY_TYPE = "compact-transcript-summary";
|
|
13
11
|
|
|
14
12
|
const MIN_PREVIEW_WIDTH = 20;
|
|
15
|
-
const
|
|
16
|
-
const DEBUG_PREVIEW_WIDTH = 140;
|
|
17
|
-
const DEFAULT_PREVIEW_WIDTH = 104;
|
|
13
|
+
const MAX_PREVIEW_WIDTH = 104;
|
|
18
14
|
// Leave room for pi's row gutter/padding so compact lines never wrap.
|
|
19
15
|
const PREVIEW_MARGIN = 6;
|
|
20
|
-
const
|
|
21
|
-
|
|
22
|
-
|
|
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";
|
|
16
|
+
const BLINK_INTERVAL_MS = 400;
|
|
17
|
+
// Status marker is two cells wide ("◆ ").
|
|
18
|
+
const MARKER_WIDTH = 2;
|
|
26
19
|
|
|
27
20
|
type CompactTranscriptConfig = {
|
|
28
|
-
|
|
29
|
-
compactCustomTools: boolean;
|
|
30
|
-
showFailedTools: boolean;
|
|
31
|
-
showBashMutations: boolean;
|
|
32
|
-
alwaysShowTools: string[];
|
|
33
|
-
mutationTools: string[];
|
|
34
|
-
previewTemplates: Record<string, string>;
|
|
21
|
+
enabled: boolean;
|
|
35
22
|
};
|
|
36
23
|
|
|
37
24
|
type ToolInfo = {
|
|
@@ -39,104 +26,113 @@ type ToolInfo = {
|
|
|
39
26
|
name: string;
|
|
40
27
|
args: any;
|
|
41
28
|
preview: string;
|
|
42
|
-
kind: ToolKind;
|
|
43
29
|
hidden?: boolean;
|
|
30
|
+
running?: boolean;
|
|
44
31
|
burstCount?: number;
|
|
45
|
-
|
|
32
|
+
startedAt?: number;
|
|
33
|
+
durationMs?: number;
|
|
34
|
+
burstDurationMs?: number;
|
|
46
35
|
result?: string;
|
|
47
36
|
isError?: boolean;
|
|
48
37
|
invalidate?: () => void;
|
|
49
38
|
};
|
|
50
39
|
|
|
40
|
+
type RunStats = {
|
|
41
|
+
startedAt: number;
|
|
42
|
+
toolCount: number;
|
|
43
|
+
readFiles: Set<string>;
|
|
44
|
+
editFiles: Set<string>;
|
|
45
|
+
commandCount: number;
|
|
46
|
+
otherCount: number;
|
|
47
|
+
failedCount: number;
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
type SummaryData = {
|
|
51
|
+
reads: number;
|
|
52
|
+
edits: number;
|
|
53
|
+
commands: number;
|
|
54
|
+
others: number;
|
|
55
|
+
failed: number;
|
|
56
|
+
durationMs: number;
|
|
57
|
+
};
|
|
58
|
+
|
|
51
59
|
type RuntimeState = {
|
|
52
60
|
config: CompactTranscriptConfig;
|
|
53
61
|
toolsById: Map<string, ToolInfo>;
|
|
54
|
-
|
|
62
|
+
currentBurst: ToolInfo[];
|
|
55
63
|
hiddenToolIds: Set<string>;
|
|
64
|
+
runningToolIds: Set<string>;
|
|
56
65
|
agentActive: boolean;
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
66
|
+
blinkOn: boolean;
|
|
67
|
+
blinkTimer?: ReturnType<typeof setInterval>;
|
|
68
|
+
runStats: RunStats;
|
|
69
|
+
// Live transcript components, so toggling can re-render existing rows.
|
|
70
|
+
toolComponents: Set<any>;
|
|
71
|
+
assistantComponents: Set<any>;
|
|
60
72
|
currentTheme?: Theme;
|
|
73
|
+
thinkingHidden: boolean;
|
|
74
|
+
currentThoughtHeading?: string;
|
|
75
|
+
thoughtAnchorId?: string;
|
|
61
76
|
};
|
|
62
77
|
|
|
63
78
|
const DEFAULT_CONFIG: CompactTranscriptConfig = {
|
|
64
|
-
|
|
65
|
-
compactCustomTools: true,
|
|
66
|
-
showFailedTools: true,
|
|
67
|
-
showBashMutations: true,
|
|
68
|
-
alwaysShowTools: [],
|
|
69
|
-
mutationTools: [],
|
|
70
|
-
previewTemplates: {},
|
|
79
|
+
enabled: true,
|
|
71
80
|
};
|
|
72
81
|
|
|
73
82
|
const STATE_KEY = Symbol.for("pi-compact-transcript.state");
|
|
74
83
|
const TOOL_PATCH_KEY = Symbol.for("pi-compact-transcript.tool-patch");
|
|
75
84
|
const ASSISTANT_PATCH_KEY = Symbol.for("pi-compact-transcript.assistant-patch");
|
|
76
85
|
|
|
77
|
-
function
|
|
86
|
+
function newRunStats(): RunStats {
|
|
78
87
|
return {
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
88
|
+
startedAt: Date.now(),
|
|
89
|
+
toolCount: 0,
|
|
90
|
+
readFiles: new Set(),
|
|
91
|
+
editFiles: new Set(),
|
|
92
|
+
commandCount: 0,
|
|
93
|
+
otherCount: 0,
|
|
94
|
+
failedCount: 0,
|
|
86
95
|
};
|
|
87
96
|
}
|
|
88
97
|
|
|
89
98
|
function normalizeConfig(input: unknown): CompactTranscriptConfig {
|
|
90
|
-
const source = (input && typeof input === "object" ? input : {}) as
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
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
|
-
};
|
|
99
|
+
const source = (input && typeof input === "object" ? input : {}) as Record<string, unknown>;
|
|
100
|
+
let enabled = DEFAULT_CONFIG.enabled;
|
|
101
|
+
if (typeof source.enabled === "boolean") {
|
|
102
|
+
enabled = source.enabled;
|
|
103
|
+
} else if (typeof source.mode === "string") {
|
|
104
|
+
// Pre-0.5 config persisted a mode string instead of an enabled flag.
|
|
105
|
+
enabled = source.mode !== "disabled" && source.mode !== "off";
|
|
106
|
+
}
|
|
107
|
+
return { enabled };
|
|
116
108
|
}
|
|
117
109
|
|
|
118
110
|
function getState(): RuntimeState {
|
|
119
111
|
const globalWithState = globalThis as typeof globalThis & { [STATE_KEY]?: RuntimeState };
|
|
120
112
|
globalWithState[STATE_KEY] ??= {
|
|
121
|
-
config:
|
|
113
|
+
config: { ...DEFAULT_CONFIG },
|
|
122
114
|
toolsById: new Map(),
|
|
123
|
-
|
|
115
|
+
currentBurst: [],
|
|
124
116
|
hiddenToolIds: new Set(),
|
|
117
|
+
runningToolIds: new Set(),
|
|
125
118
|
agentActive: false,
|
|
126
|
-
|
|
127
|
-
|
|
119
|
+
blinkOn: true,
|
|
120
|
+
runStats: newRunStats(),
|
|
121
|
+
toolComponents: new Set(),
|
|
122
|
+
assistantComponents: new Set(),
|
|
123
|
+
thinkingHidden: true,
|
|
128
124
|
};
|
|
129
|
-
|
|
125
|
+
const runtimeState = globalWithState[STATE_KEY]!;
|
|
126
|
+
// /reload keeps the global object alive; initialize fields added by newer
|
|
127
|
+
// versions when an older extension instance created the state object.
|
|
128
|
+
runtimeState.thinkingHidden ??= true;
|
|
129
|
+
return runtimeState;
|
|
130
130
|
}
|
|
131
131
|
|
|
132
132
|
const state = getState();
|
|
133
133
|
|
|
134
134
|
function isEnabled(): boolean {
|
|
135
|
-
return state.config.
|
|
136
|
-
}
|
|
137
|
-
|
|
138
|
-
function isDebugMode(): boolean {
|
|
139
|
-
return state.config.mode === "debug";
|
|
135
|
+
return state.config.enabled;
|
|
140
136
|
}
|
|
141
137
|
|
|
142
138
|
function isNonEmptyString(value: unknown): value is string {
|
|
@@ -156,13 +152,7 @@ function oneLine(value: unknown): string {
|
|
|
156
152
|
}
|
|
157
153
|
|
|
158
154
|
function previewWidth(base = process.stdout.columns || 100): number {
|
|
159
|
-
|
|
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));
|
|
155
|
+
return Math.max(MIN_PREVIEW_WIDTH, Math.min(MAX_PREVIEW_WIDTH, base - PREVIEW_MARGIN));
|
|
166
156
|
}
|
|
167
157
|
|
|
168
158
|
function limitPlain(text: string, max = previewWidth()): string {
|
|
@@ -183,91 +173,36 @@ function safeJson(value: unknown): string {
|
|
|
183
173
|
}
|
|
184
174
|
}
|
|
185
175
|
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
if (!
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
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
|
-
}
|
|
176
|
+
// Sub-second durations render as "" so fast tools stay clutter-free.
|
|
177
|
+
function formatDuration(ms: number): string {
|
|
178
|
+
if (!Number.isFinite(ms) || ms < 1000) return "";
|
|
179
|
+
const totalSeconds = Math.round(ms / 1000);
|
|
180
|
+
if (totalSeconds < 60) return `${totalSeconds}s`;
|
|
181
|
+
const minutes = Math.floor(totalSeconds / 60);
|
|
182
|
+
const seconds = totalSeconds % 60;
|
|
183
|
+
return seconds ? `${minutes}m${seconds}s` : `${minutes}m`;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
// Checked in order; earlier keys are more likely to be the argument a human
|
|
187
|
+
// would recognize the call by.
|
|
188
|
+
const PREFERRED_ARG_KEYS = [
|
|
189
|
+
"command",
|
|
190
|
+
"code",
|
|
191
|
+
"query",
|
|
192
|
+
"pattern",
|
|
193
|
+
"path",
|
|
194
|
+
"file_path",
|
|
195
|
+
"filePath",
|
|
196
|
+
"file",
|
|
197
|
+
"url",
|
|
198
|
+
"prompt",
|
|
199
|
+
"text",
|
|
200
|
+
"description",
|
|
201
|
+
"name",
|
|
202
|
+
];
|
|
203
|
+
const PATH_ARG_KEYS = new Set(["path", "file_path", "filePath", "file"]);
|
|
266
204
|
|
|
267
205
|
function previewFor(name: string, args: any): string {
|
|
268
|
-
const template = findTemplate(name);
|
|
269
|
-
if (template) return renderTemplate(template, name, args);
|
|
270
|
-
|
|
271
206
|
switch (name) {
|
|
272
207
|
case "bash":
|
|
273
208
|
return `$ ${oneLine(args?.command || "...")}`;
|
|
@@ -297,8 +232,22 @@ function previewFor(name: string, args: any): string {
|
|
|
297
232
|
return `find ${args?.pattern ? quote(String(args.pattern)) : "..."} ${shortenPath(args?.path) || "."}`;
|
|
298
233
|
case "ls":
|
|
299
234
|
return `ls ${shortenPath(args?.path) || "."}`;
|
|
300
|
-
default:
|
|
235
|
+
default: {
|
|
236
|
+
// Unknown tools: show the most meaningful string argument instead of
|
|
237
|
+
// dumping the whole args object as JSON.
|
|
238
|
+
if (args && typeof args === "object") {
|
|
239
|
+
for (const key of PREFERRED_ARG_KEYS) {
|
|
240
|
+
const value = (args as Record<string, unknown>)[key];
|
|
241
|
+
if (isNonEmptyString(value)) {
|
|
242
|
+
const rendered = PATH_ARG_KEYS.has(key) ? shortenPath(value) : oneLine(value);
|
|
243
|
+
return `${name} ${rendered}`;
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
const firstString = Object.values(args).find(isNonEmptyString);
|
|
247
|
+
if (firstString) return `${name} ${oneLine(firstString)}`;
|
|
248
|
+
}
|
|
301
249
|
return `${name} ${safeJson(args ?? {})}`;
|
|
250
|
+
}
|
|
302
251
|
}
|
|
303
252
|
}
|
|
304
253
|
|
|
@@ -313,26 +262,14 @@ function resultPreview(result: any, isPartial = false): string {
|
|
|
313
262
|
return `${lines.length} lines`;
|
|
314
263
|
}
|
|
315
264
|
|
|
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(", ");
|
|
325
|
-
}
|
|
326
|
-
|
|
327
265
|
function resetToolRun() {
|
|
328
266
|
state.toolsById = new Map();
|
|
329
|
-
state.
|
|
267
|
+
state.currentBurst = [];
|
|
330
268
|
state.hiddenToolIds = new Set();
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
state.thinkingSignalCount = 0;
|
|
269
|
+
state.runningToolIds = new Set();
|
|
270
|
+
state.currentThoughtHeading = undefined;
|
|
271
|
+
state.thoughtAnchorId = undefined;
|
|
272
|
+
stopBlinkTimer();
|
|
336
273
|
}
|
|
337
274
|
|
|
338
275
|
function captureTheme(ctx: ExtensionContext) {
|
|
@@ -350,51 +287,244 @@ function applyResult(info: ToolInfo, result: any, isError: boolean, isPartial: b
|
|
|
350
287
|
if (suffix) info.result = suffix;
|
|
351
288
|
if (isError) {
|
|
352
289
|
info.isError = true;
|
|
290
|
+
// A failed tool always gets its own visible row, even if a burst had
|
|
291
|
+
// hidden it; render it as itself rather than as a burst summary.
|
|
292
|
+
info.burstCount = 1;
|
|
353
293
|
setToolHidden(info, false);
|
|
354
294
|
}
|
|
355
295
|
}
|
|
356
296
|
|
|
297
|
+
function ensureBlinkTimer() {
|
|
298
|
+
if (state.blinkTimer || state.runningToolIds.size === 0) return;
|
|
299
|
+
state.blinkTimer = setInterval(() => {
|
|
300
|
+
if (state.runningToolIds.size === 0) {
|
|
301
|
+
stopBlinkTimer();
|
|
302
|
+
return;
|
|
303
|
+
}
|
|
304
|
+
state.blinkOn = !state.blinkOn;
|
|
305
|
+
for (const id of state.runningToolIds) state.toolsById.get(id)?.invalidate?.();
|
|
306
|
+
}, BLINK_INTERVAL_MS);
|
|
307
|
+
state.blinkTimer.unref?.();
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
function stopBlinkTimer() {
|
|
311
|
+
if (state.blinkTimer) clearInterval(state.blinkTimer);
|
|
312
|
+
state.blinkTimer = undefined;
|
|
313
|
+
state.blinkOn = true;
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
function statusMarker(theme: Theme, opts: { running?: boolean; isError?: boolean; hasResult?: boolean }): string {
|
|
317
|
+
if (opts.isError) return theme.fg("error", "◆ ");
|
|
318
|
+
if (opts.running) return theme.fg("dim", state.blinkOn ? "◆ " : "◇ ");
|
|
319
|
+
if (opts.hasResult) return theme.fg("success", "◆ ");
|
|
320
|
+
return theme.fg("dim", "◆ ");
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
function textSignalHasVisibleContent(assistantMessageEvent: any): boolean {
|
|
324
|
+
const type = assistantMessageEvent?.type;
|
|
325
|
+
if (type === "text_delta") {
|
|
326
|
+
return typeof assistantMessageEvent.delta === "string" && assistantMessageEvent.delta.trim().length > 0;
|
|
327
|
+
}
|
|
328
|
+
if (type === "text_end") {
|
|
329
|
+
return typeof assistantMessageEvent.content === "string" && assistantMessageEvent.content.trim().length > 0;
|
|
330
|
+
}
|
|
331
|
+
return false;
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
function thoughtTickerEnabled(): boolean {
|
|
335
|
+
// The ticker is a compact replacement for hidden thinking. When thinking is
|
|
336
|
+
// fully visible, showing the same headline under a tool would be duplicate.
|
|
337
|
+
return isEnabled() && state.thinkingHidden;
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
function cleanThoughtHeading(line: string): string {
|
|
341
|
+
let clean = oneLine(line)
|
|
342
|
+
.replace(/^#{1,6}\s+/, "")
|
|
343
|
+
.replace(/^[-*]\s+/, "")
|
|
344
|
+
.trim();
|
|
345
|
+
clean = clean
|
|
346
|
+
.replace(/^\*\*(.+)\*\*$/, "$1")
|
|
347
|
+
.replace(/^__(.+)__$/, "$1")
|
|
348
|
+
.replace(/^`(.+)`$/, "$1");
|
|
349
|
+
return clean.trim();
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
function extractThoughtHeading(text: unknown): string {
|
|
353
|
+
if (typeof text !== "string") return "";
|
|
354
|
+
const firstLine = text.split(/\r?\n/).find((line) => line.trim().length > 0);
|
|
355
|
+
return firstLine ? cleanThoughtHeading(firstLine) : "";
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
function latestThoughtHeading(message: any): string {
|
|
359
|
+
if (!Array.isArray(message?.content)) return "";
|
|
360
|
+
for (let i = message.content.length - 1; i >= 0; i--) {
|
|
361
|
+
const content = message.content[i];
|
|
362
|
+
if (content?.type === "thinking") return extractThoughtHeading(content.thinking);
|
|
363
|
+
}
|
|
364
|
+
return "";
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
function invalidateToolById(id: string | undefined) {
|
|
368
|
+
if (!id) return;
|
|
369
|
+
state.toolsById.get(id)?.invalidate?.();
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
function latestVisibleTool(): ToolInfo | undefined {
|
|
373
|
+
return Array.from(state.toolsById.values())
|
|
374
|
+
.reverse()
|
|
375
|
+
.find((tool) => !tool.hidden);
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
function clearCurrentThought() {
|
|
379
|
+
if (!state.currentThoughtHeading && !state.thoughtAnchorId) return;
|
|
380
|
+
const previousAnchorId = state.thoughtAnchorId;
|
|
381
|
+
state.currentThoughtHeading = undefined;
|
|
382
|
+
state.thoughtAnchorId = undefined;
|
|
383
|
+
invalidateToolById(previousAnchorId);
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
function setCurrentThought(heading: string) {
|
|
387
|
+
const nextHeading = oneLine(heading);
|
|
388
|
+
if (!thoughtTickerEnabled() || !nextHeading) {
|
|
389
|
+
clearCurrentThought();
|
|
390
|
+
return;
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
const previousAnchorId = state.thoughtAnchorId;
|
|
394
|
+
const nextAnchorId = latestVisibleTool()?.id;
|
|
395
|
+
const changed = state.currentThoughtHeading !== nextHeading || previousAnchorId !== nextAnchorId;
|
|
396
|
+
state.currentThoughtHeading = nextHeading;
|
|
397
|
+
state.thoughtAnchorId = nextAnchorId;
|
|
398
|
+
if (!changed) return;
|
|
399
|
+
|
|
400
|
+
invalidateToolById(previousAnchorId);
|
|
401
|
+
if (nextAnchorId !== previousAnchorId) invalidateToolById(nextAnchorId);
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
function updateCurrentThoughtFromMessage(message: any) {
|
|
405
|
+
const heading = latestThoughtHeading(message);
|
|
406
|
+
// A new thinking block starts empty; keep showing the previous heading until
|
|
407
|
+
// the replacement has real text so the ticker does not blink off/on between
|
|
408
|
+
// tool completion and the next streamed thought.
|
|
409
|
+
if (heading) setCurrentThought(heading);
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
function anchorCurrentThoughtTo(info: ToolInfo) {
|
|
413
|
+
if (!thoughtTickerEnabled() || !state.currentThoughtHeading || state.thoughtAnchorId === info.id) return;
|
|
414
|
+
const previousAnchorId = state.thoughtAnchorId;
|
|
415
|
+
state.thoughtAnchorId = info.id;
|
|
416
|
+
invalidateToolById(previousAnchorId);
|
|
417
|
+
info.invalidate?.();
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
function currentThoughtLine(toolCallId: string, theme: Theme): string {
|
|
421
|
+
if (!thoughtTickerEnabled() || state.thoughtAnchorId !== toolCallId || !state.currentThoughtHeading) return "";
|
|
422
|
+
const prefix = " ↳ ";
|
|
423
|
+
const budget = previewWidth((process.stdout.columns || 100) - prefix.length);
|
|
424
|
+
return theme.fg("dim", prefix) + theme.fg("thinkingText", limitPlain(state.currentThoughtHeading, budget));
|
|
425
|
+
}
|
|
426
|
+
|
|
357
427
|
function upsertToolInfo(id: string, name: string, args: any, invalidate?: () => void): ToolInfo {
|
|
358
428
|
let info = state.toolsById.get(id);
|
|
359
429
|
if (!info) {
|
|
360
|
-
info = { id, name, args, preview: previewFor(name, args)
|
|
430
|
+
info = { id, name, args, preview: previewFor(name, args) };
|
|
361
431
|
state.toolsById.set(id, info);
|
|
362
432
|
}
|
|
363
433
|
info.name = name;
|
|
364
434
|
info.args = args;
|
|
365
435
|
info.preview = previewFor(name, args);
|
|
366
|
-
info.kind = classifyTool(name, args);
|
|
367
436
|
if (invalidate) info.invalidate = invalidate;
|
|
368
437
|
return info;
|
|
369
438
|
}
|
|
370
439
|
|
|
371
|
-
function
|
|
372
|
-
const
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
440
|
+
function recordToolStart(name: string, args: any) {
|
|
441
|
+
const base = name.split(".").pop() ?? name;
|
|
442
|
+
state.runStats.toolCount++;
|
|
443
|
+
if (base === "read") {
|
|
444
|
+
if (isNonEmptyString(args?.path)) state.runStats.readFiles.add(args.path);
|
|
445
|
+
} else if (base === "edit" || base === "write") {
|
|
446
|
+
if (isNonEmptyString(args?.path)) state.runStats.editFiles.add(args.path);
|
|
447
|
+
} else if (base === "bash") {
|
|
448
|
+
state.runStats.commandCount++;
|
|
449
|
+
} else {
|
|
450
|
+
state.runStats.otherCount++;
|
|
451
|
+
}
|
|
452
|
+
}
|
|
377
453
|
|
|
378
|
-
|
|
379
|
-
|
|
454
|
+
function joinBurst(info: ToolInfo) {
|
|
455
|
+
const previous = state.currentBurst[state.currentBurst.length - 1];
|
|
456
|
+
|
|
457
|
+
if (!isEnabled()) {
|
|
458
|
+
state.currentBurst = [];
|
|
380
459
|
return;
|
|
381
460
|
}
|
|
382
461
|
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
462
|
+
// Bursts only group repeats of the same tool; a different tool starts a
|
|
463
|
+
// fresh row (and leaves the previous row visible with its count).
|
|
464
|
+
if (state.currentBurst.length && state.currentBurst[state.currentBurst.length - 1].name !== info.name) {
|
|
465
|
+
state.currentBurst = [];
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
if (!state.currentBurst.some((tool) => tool.id === info.id)) state.currentBurst.push(info);
|
|
469
|
+
for (const tool of state.currentBurst.slice(0, -1)) setToolHidden(tool, true);
|
|
470
|
+
info.burstCount = state.currentBurst.length;
|
|
471
|
+
previous?.invalidate?.();
|
|
472
|
+
}
|
|
386
473
|
|
|
387
|
-
|
|
388
|
-
|
|
474
|
+
function beginTool(id: string, name: string, args: any) {
|
|
475
|
+
const info = upsertToolInfo(id, name, args);
|
|
476
|
+
setToolHidden(info, false);
|
|
477
|
+
info.burstCount = 1;
|
|
478
|
+
info.running = true;
|
|
479
|
+
info.isError = false;
|
|
480
|
+
info.startedAt = Date.now();
|
|
481
|
+
state.runningToolIds.add(id);
|
|
482
|
+
ensureBlinkTimer();
|
|
483
|
+
recordToolStart(name, args);
|
|
484
|
+
joinBurst(info);
|
|
485
|
+
anchorCurrentThoughtTo(info);
|
|
486
|
+
// The component may already be on screen from argument streaming; repaint
|
|
487
|
+
// now so the running marker, burst count, and thought ticker appear
|
|
488
|
+
// immediately instead of waiting for the next result event or blink tick.
|
|
489
|
+
info.invalidate?.();
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
// A tool row rendered without a live tool_execution_start event — pi is
|
|
493
|
+
// rebuilding chat history after resume/reload. Reconstruct burst grouping so
|
|
494
|
+
// scrollback coalesces the same way the live view did, but skip timers,
|
|
495
|
+
// stats, and running state.
|
|
496
|
+
function hydrateTool(id: string, name: string, args: any, isError: boolean): ToolInfo {
|
|
497
|
+
const info = upsertToolInfo(id, name, args);
|
|
498
|
+
setToolHidden(info, false);
|
|
499
|
+
info.burstCount = 1;
|
|
500
|
+
if (isError) {
|
|
501
|
+
info.isError = true;
|
|
502
|
+
state.currentBurst = [];
|
|
503
|
+
return info;
|
|
389
504
|
}
|
|
390
|
-
info
|
|
391
|
-
|
|
505
|
+
joinBurst(info);
|
|
506
|
+
return info;
|
|
392
507
|
}
|
|
393
508
|
|
|
394
509
|
function updateToolResult(toolCallId: string, result: any, isError = false, isPartial = false) {
|
|
510
|
+
if (!isPartial) {
|
|
511
|
+
state.runningToolIds.delete(toolCallId);
|
|
512
|
+
if (state.runningToolIds.size === 0) stopBlinkTimer();
|
|
513
|
+
}
|
|
395
514
|
const info = state.toolsById.get(toolCallId);
|
|
396
515
|
if (!info) return;
|
|
516
|
+
if (!isPartial) {
|
|
517
|
+
info.running = false;
|
|
518
|
+
if (info.startedAt) info.durationMs = Date.now() - info.startedAt;
|
|
519
|
+
if (state.currentBurst.includes(info) && state.currentBurst.length > 1) {
|
|
520
|
+
info.burstDurationMs = state.currentBurst.reduce((total, tool) => total + (tool.durationMs ?? 0), 0);
|
|
521
|
+
}
|
|
522
|
+
if (isError) state.runStats.failedCount++;
|
|
523
|
+
}
|
|
397
524
|
applyResult(info, result, isError, isPartial);
|
|
525
|
+
// A failure ends the current burst so the red row stays visible and the
|
|
526
|
+
// next tool starts a fresh count.
|
|
527
|
+
if (isError) state.currentBurst = [];
|
|
398
528
|
info.invalidate?.();
|
|
399
529
|
}
|
|
400
530
|
|
|
@@ -408,45 +538,28 @@ function compactToolLine(
|
|
|
408
538
|
isError = false,
|
|
409
539
|
isPartial = false,
|
|
410
540
|
): string {
|
|
411
|
-
|
|
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);
|
|
541
|
+
if (!state.toolsById.has(toolCallId)) hydrateTool(toolCallId, name, args, isError);
|
|
542
|
+
const info = upsertToolInfo(toolCallId, name, args, invalidate);
|
|
424
543
|
applyResult(info, result, isError, isPartial);
|
|
425
544
|
if (info.hidden) return "";
|
|
426
545
|
|
|
427
|
-
const
|
|
546
|
+
const isBurst = (info.burstCount ?? 1) > 1;
|
|
547
|
+
const durationText = formatDuration((isBurst ? (info.burstDurationMs ?? info.durationMs) : info.durationMs) ?? 0);
|
|
548
|
+
const inner = [info.result ? oneLine(info.result) : "", durationText].filter(Boolean).join(" · ");
|
|
549
|
+
const status = inner ? ` {${inner}}` : info.running ? " {running}" : "";
|
|
428
550
|
const details = `${info.preview}${status}`;
|
|
429
|
-
|
|
430
|
-
|
|
551
|
+
const marker = statusMarker(theme, {
|
|
552
|
+
running: info.running,
|
|
553
|
+
isError: info.isError,
|
|
554
|
+
hasResult: result != null || !!info.result,
|
|
555
|
+
});
|
|
556
|
+
if (!isBurst) {
|
|
431
557
|
return marker + theme.fg("muted", limitPlain(details));
|
|
432
558
|
}
|
|
433
559
|
|
|
434
|
-
const
|
|
435
|
-
const
|
|
436
|
-
|
|
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;
|
|
560
|
+
const prefix = `${info.burstCount}× `;
|
|
561
|
+
const budget = previewWidth((process.stdout.columns || 100) - prefix.length - MARKER_WIDTH);
|
|
562
|
+
return marker + theme.fg("muted", prefix + limitPlain(details, budget));
|
|
450
563
|
}
|
|
451
564
|
|
|
452
565
|
function patchToolExecutionComponent() {
|
|
@@ -461,24 +574,32 @@ function patchToolExecutionComponent() {
|
|
|
461
574
|
proto.updateDisplay = function patchedUpdateDisplay() {
|
|
462
575
|
if (!this.toolCallId || !this.toolName || !this.selfRenderContainer || typeof this.selfRenderContainer.clear !== "function") {
|
|
463
576
|
this.__compactTranscriptForceSelf = false;
|
|
577
|
+
this.__compactTranscriptHidden = false;
|
|
464
578
|
return originalUpdateDisplay.call(this);
|
|
465
579
|
}
|
|
580
|
+
state.toolComponents.add(this);
|
|
466
581
|
|
|
467
582
|
const invalidate = () => {
|
|
468
583
|
this.invalidate();
|
|
469
584
|
this.ui?.requestRender?.();
|
|
470
585
|
};
|
|
586
|
+
// Must run before upsertToolInfo, which would make the row look
|
|
587
|
+
// already-known and skip burst reconstruction for rehydrated history.
|
|
588
|
+
if (!state.toolsById.has(this.toolCallId)) {
|
|
589
|
+
hydrateTool(this.toolCallId, this.toolName, this.args, this.result?.isError ?? false);
|
|
590
|
+
}
|
|
471
591
|
const info = upsertToolInfo(this.toolCallId, this.toolName, this.args, invalidate);
|
|
472
592
|
applyResult(info, this.result, this.result?.isError ?? false, this.isPartial);
|
|
473
593
|
|
|
474
|
-
if (
|
|
594
|
+
if (!isEnabled() || this.expanded) {
|
|
475
595
|
setToolHidden(info, false);
|
|
476
596
|
this.__compactTranscriptForceSelf = false;
|
|
597
|
+
this.__compactTranscriptHidden = false;
|
|
477
598
|
return originalUpdateDisplay.call(this);
|
|
478
599
|
}
|
|
479
600
|
|
|
480
601
|
this.__compactTranscriptForceSelf = true;
|
|
481
|
-
this.
|
|
602
|
+
this.__compactTranscriptHidden = false;
|
|
482
603
|
this.selfRenderContainer.clear();
|
|
483
604
|
for (const image of this.imageComponents ?? []) this.removeChild?.(image);
|
|
484
605
|
for (const spacer of this.imageSpacers ?? []) this.removeChild?.(spacer);
|
|
@@ -488,6 +609,7 @@ function patchToolExecutionComponent() {
|
|
|
488
609
|
const theme = state.currentTheme;
|
|
489
610
|
if (!theme) {
|
|
490
611
|
this.__compactTranscriptForceSelf = false;
|
|
612
|
+
this.__compactTranscriptHidden = false;
|
|
491
613
|
return originalUpdateDisplay.call(this);
|
|
492
614
|
}
|
|
493
615
|
|
|
@@ -503,15 +625,17 @@ function patchToolExecutionComponent() {
|
|
|
503
625
|
);
|
|
504
626
|
|
|
505
627
|
if (!line) {
|
|
506
|
-
this.
|
|
628
|
+
this.__compactTranscriptHidden = true;
|
|
507
629
|
return;
|
|
508
630
|
}
|
|
509
631
|
|
|
510
632
|
this.selfRenderContainer.addChild(new Text(line, 0, 0));
|
|
633
|
+
const thoughtLine = currentThoughtLine(this.toolCallId, theme);
|
|
634
|
+
if (thoughtLine) this.selfRenderContainer.addChild(new Text(thoughtLine, 0, 0));
|
|
511
635
|
};
|
|
512
636
|
|
|
513
637
|
proto.render = function patchedRender(width: number) {
|
|
514
|
-
if (this.hideComponent) return [];
|
|
638
|
+
if (this.hideComponent || this.__compactTranscriptHidden) return [];
|
|
515
639
|
if (this.__compactTranscriptForceSelf) return this.selfRenderContainer.render(width);
|
|
516
640
|
return originalRender.call(this, width);
|
|
517
641
|
};
|
|
@@ -526,82 +650,33 @@ function patchAssistantMessageComponent() {
|
|
|
526
650
|
const originalUpdateContent = existing?.originalUpdateContent ?? proto.updateContent;
|
|
527
651
|
|
|
528
652
|
proto.updateContent = function patchedUpdateContent(message: any) {
|
|
653
|
+
state.assistantComponents.add(this);
|
|
654
|
+
state.thinkingHidden = !!this.hideThinkingBlock;
|
|
655
|
+
if (!state.thinkingHidden) clearCurrentThought();
|
|
529
656
|
if (!isEnabled() || !this.hideThinkingBlock || !Array.isArray(message?.content)) {
|
|
530
|
-
this.__compactTranscriptHiddenThinkingSignal = false;
|
|
531
657
|
return originalUpdateContent.call(this, message);
|
|
532
658
|
}
|
|
533
659
|
if (!this.contentContainer || typeof this.contentContainer.clear !== "function") {
|
|
534
|
-
this.__compactTranscriptHiddenThinkingSignal = false;
|
|
535
660
|
return originalUpdateContent.call(this, message);
|
|
536
661
|
}
|
|
537
662
|
|
|
538
663
|
this.lastMessage = message;
|
|
539
664
|
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
665
|
this.hasToolCalls = message.content.some((c: any) => c.type === "toolCall");
|
|
544
666
|
|
|
545
|
-
//
|
|
546
|
-
|
|
547
|
-
|
|
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?.();
|
|
559
|
-
}
|
|
560
|
-
state.lastThinkingSignalComponent = this;
|
|
561
|
-
state.thinkingSignalCount++;
|
|
562
|
-
} else if (!thinkingSignal && hasText) {
|
|
563
|
-
resetThinkingSignals();
|
|
564
|
-
}
|
|
565
|
-
}
|
|
566
|
-
|
|
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
|
-
}
|
|
667
|
+
// Thinking blocks are suppressed entirely; only real text is rendered.
|
|
668
|
+
const texts = message.content.filter((c: any) => c.type === "text" && c.text?.trim());
|
|
669
|
+
if (texts.length === 0) return;
|
|
592
670
|
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
this.contentContainer.addChild(new Text(label, this.outputPad, 0));
|
|
599
|
-
}
|
|
671
|
+
clearCurrentThought();
|
|
672
|
+
// Assistant text ends a tool burst. The live path also does this via
|
|
673
|
+
// message_update events; doing it here too keeps hydrated history from
|
|
674
|
+
// grouping tool rows across turn boundaries.
|
|
675
|
+
state.currentBurst = [];
|
|
600
676
|
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
if (hasVisibleAfter) this.contentContainer.addChild(new Spacer(1));
|
|
677
|
+
this.contentContainer.addChild(new Spacer(1));
|
|
678
|
+
for (const content of texts) {
|
|
679
|
+
this.contentContainer.addChild(new Markdown(content.text.trim(), this.outputPad, 0, this.markdownTheme));
|
|
605
680
|
}
|
|
606
681
|
};
|
|
607
682
|
|
|
@@ -613,20 +688,76 @@ function patchRenderers() {
|
|
|
613
688
|
patchAssistantMessageComponent();
|
|
614
689
|
}
|
|
615
690
|
|
|
616
|
-
|
|
617
|
-
|
|
691
|
+
// Re-render every transcript row we have touched so toggling applies to the
|
|
692
|
+
// visible transcript immediately instead of only to future rows.
|
|
693
|
+
function refreshTranscript() {
|
|
694
|
+
let ui: any;
|
|
695
|
+
for (const component of state.toolComponents) {
|
|
696
|
+
try {
|
|
697
|
+
// ToolExecutionComponent.invalidate() re-runs updateDisplay.
|
|
698
|
+
component.invalidate?.();
|
|
699
|
+
ui ??= component.ui;
|
|
700
|
+
} catch {
|
|
701
|
+
state.toolComponents.delete(component);
|
|
702
|
+
}
|
|
703
|
+
}
|
|
704
|
+
for (const component of state.assistantComponents) {
|
|
705
|
+
try {
|
|
706
|
+
if (component.lastMessage) component.updateContent?.(component.lastMessage);
|
|
707
|
+
component.invalidate?.();
|
|
708
|
+
} catch {
|
|
709
|
+
state.assistantComponents.delete(component);
|
|
710
|
+
}
|
|
711
|
+
}
|
|
712
|
+
ui?.requestRender?.();
|
|
713
|
+
}
|
|
714
|
+
|
|
715
|
+
function summaryLine(data: SummaryData): string {
|
|
716
|
+
const plural = (count: number) => (count === 1 ? "" : "s");
|
|
717
|
+
const parts: string[] = [];
|
|
718
|
+
if (data.reads) parts.push(`read ${data.reads} file${plural(data.reads)}`);
|
|
719
|
+
if (data.edits) parts.push(`edited ${data.edits} file${plural(data.edits)}`);
|
|
720
|
+
if (data.commands) parts.push(`ran ${data.commands} command${plural(data.commands)}`);
|
|
721
|
+
if (data.others) parts.push(`${data.others} other tool${plural(data.others)}`);
|
|
722
|
+
if (data.failed) parts.push(`${data.failed} failed`);
|
|
723
|
+
if (parts.length === 0) return "";
|
|
724
|
+
const text = parts.join(", ");
|
|
725
|
+
const capitalized = text[0].toUpperCase() + text.slice(1);
|
|
726
|
+
const duration = formatDuration(data.durationMs);
|
|
727
|
+
return duration ? `${capitalized} · ${duration}` : capitalized;
|
|
728
|
+
}
|
|
729
|
+
|
|
730
|
+
function normalizeSummary(input: unknown): SummaryData {
|
|
731
|
+
const source = (input && typeof input === "object" ? input : {}) as Record<string, unknown>;
|
|
732
|
+
const num = (value: unknown) => (typeof value === "number" && Number.isFinite(value) && value > 0 ? value : 0);
|
|
733
|
+
return {
|
|
734
|
+
reads: num(source.reads),
|
|
735
|
+
edits: num(source.edits),
|
|
736
|
+
commands: num(source.commands),
|
|
737
|
+
others: num(source.others),
|
|
738
|
+
failed: num(source.failed),
|
|
739
|
+
durationMs: num(source.durationMs),
|
|
740
|
+
};
|
|
618
741
|
}
|
|
619
742
|
|
|
620
|
-
function
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
743
|
+
function appendRunSummary(pi: ExtensionAPI) {
|
|
744
|
+
const stats = state.runStats;
|
|
745
|
+
// Single-tool runs are self-evident from the transcript; a summary line
|
|
746
|
+
// would just repeat the row above it.
|
|
747
|
+
if (!isEnabled() || stats.toolCount < 2) return;
|
|
748
|
+
const data: SummaryData = {
|
|
749
|
+
reads: stats.readFiles.size,
|
|
750
|
+
edits: stats.editFiles.size,
|
|
751
|
+
commands: stats.commandCount,
|
|
752
|
+
others: stats.otherCount,
|
|
753
|
+
failed: stats.failedCount,
|
|
754
|
+
durationMs: Date.now() - stats.startedAt,
|
|
755
|
+
};
|
|
756
|
+
pi.appendEntry(SUMMARY_ENTRY_TYPE, data);
|
|
626
757
|
}
|
|
627
758
|
|
|
628
759
|
function restoreConfigFromBranch(ctx: ExtensionContext) {
|
|
629
|
-
let nextConfig =
|
|
760
|
+
let nextConfig = { ...DEFAULT_CONFIG };
|
|
630
761
|
for (const entry of ctx.sessionManager.getBranch()) {
|
|
631
762
|
if (entry.type === "custom" && entry.customType === CONFIG_ENTRY_TYPE) {
|
|
632
763
|
nextConfig = normalizeConfig(entry.data);
|
|
@@ -635,8 +766,11 @@ function restoreConfigFromBranch(ctx: ExtensionContext) {
|
|
|
635
766
|
state.config = nextConfig;
|
|
636
767
|
}
|
|
637
768
|
|
|
638
|
-
function
|
|
639
|
-
|
|
769
|
+
function setEnabled(enabled: boolean, pi: ExtensionAPI, ctx: ExtensionContext) {
|
|
770
|
+
state.config.enabled = enabled;
|
|
771
|
+
pi.appendEntry(CONFIG_ENTRY_TYPE, { ...state.config });
|
|
772
|
+
refreshTranscript();
|
|
773
|
+
ctx.ui.notify(`Compact transcript: ${enabled ? "on" : "off"}`, "info");
|
|
640
774
|
}
|
|
641
775
|
|
|
642
776
|
function parseBoolean(value: string | undefined): boolean | undefined {
|
|
@@ -647,372 +781,33 @@ function parseBoolean(value: string | undefined): boolean | undefined {
|
|
|
647
781
|
return undefined;
|
|
648
782
|
}
|
|
649
783
|
|
|
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;
|
|
682
|
-
}
|
|
683
|
-
return entries;
|
|
684
|
-
}
|
|
685
|
-
|
|
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;
|
|
864
|
-
}
|
|
865
|
-
persistConfig(pi);
|
|
866
|
-
applyThinkingLabel(ctx);
|
|
867
|
-
}
|
|
868
|
-
|
|
869
|
-
async function showSettingsPanel(pi: ExtensionAPI, ctx: ExtensionContext): Promise<void> {
|
|
870
|
-
if (ctx.mode !== "tui") {
|
|
871
|
-
ctx.ui.notify(describeConfig(), "info");
|
|
872
|
-
return;
|
|
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
784
|
function registerCommand(pi: ExtensionAPI) {
|
|
920
785
|
pi.registerCommand("compact-transcript", {
|
|
921
|
-
description: "
|
|
786
|
+
description: "Toggle compact transcript rendering (on/off).",
|
|
922
787
|
getArgumentCompletions(prefix: string) {
|
|
923
|
-
|
|
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
|
|
788
|
+
return ["on", "off", "status"]
|
|
943
789
|
.filter((value) => value.startsWith(prefix.trimStart()))
|
|
944
790
|
.map((value) => ({ value, label: value }));
|
|
945
791
|
},
|
|
946
792
|
handler: async (args, ctx) => {
|
|
947
793
|
captureTheme(ctx);
|
|
948
|
-
const trimmed = args.trim();
|
|
949
|
-
if (trimmed === "
|
|
950
|
-
ctx.ui.notify(
|
|
794
|
+
const trimmed = args.trim().toLowerCase();
|
|
795
|
+
if (trimmed === "status") {
|
|
796
|
+
ctx.ui.notify(`Compact transcript: ${isEnabled() ? "on" : "off"}`, "info");
|
|
951
797
|
return;
|
|
952
798
|
}
|
|
953
|
-
if (!trimmed
|
|
954
|
-
|
|
799
|
+
if (!trimmed) {
|
|
800
|
+
setEnabled(!isEnabled(), pi, ctx);
|
|
955
801
|
return;
|
|
956
802
|
}
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
803
|
+
// on/off, plus pre-0.5 mode names as legacy aliases.
|
|
804
|
+
const legacyOn = ["balanced", "aggressive", "debug"].includes(trimmed);
|
|
805
|
+
const parsed = legacyOn ? true : parseBoolean(trimmed);
|
|
806
|
+
if (parsed !== undefined) {
|
|
807
|
+
setEnabled(parsed, pi, ctx);
|
|
962
808
|
return;
|
|
963
809
|
}
|
|
964
|
-
|
|
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");
|
|
810
|
+
ctx.ui.notify(`Unknown option "${trimmed}". Usage: /compact-transcript [on|off|status]`, "error");
|
|
1016
811
|
},
|
|
1017
812
|
});
|
|
1018
813
|
}
|
|
@@ -1021,17 +816,29 @@ export default function compactTranscript(pi: ExtensionAPI) {
|
|
|
1021
816
|
patchRenderers();
|
|
1022
817
|
registerCommand(pi);
|
|
1023
818
|
|
|
819
|
+
pi.registerEntryRenderer<SummaryData>(SUMMARY_ENTRY_TYPE, (entry, _options, theme) => {
|
|
820
|
+
const line = summaryLine(normalizeSummary(entry.data));
|
|
821
|
+
if (!line) return undefined;
|
|
822
|
+
return new Text(theme.fg("muted", line), 0, 0);
|
|
823
|
+
});
|
|
824
|
+
|
|
1024
825
|
pi.on("session_start", async (_event, ctx) => {
|
|
1025
826
|
restoreConfigFromBranch(ctx);
|
|
1026
827
|
captureTheme(ctx);
|
|
1027
|
-
|
|
828
|
+
// Drop all per-tool state and component registries from the previous
|
|
829
|
+
// session; stale burst counts otherwise corrupt the rebuilt transcript
|
|
830
|
+
// after resume or /reload.
|
|
831
|
+
resetToolRun();
|
|
832
|
+
state.runStats = newRunStats();
|
|
833
|
+
state.toolComponents = new Set();
|
|
834
|
+
state.assistantComponents = new Set();
|
|
1028
835
|
ctx.ui.setWorkingMessage();
|
|
1029
836
|
// Clear any footer status left behind by pre-0.4 versions of this extension.
|
|
1030
837
|
ctx.ui.setStatus(STATUS_KEY, undefined);
|
|
1031
838
|
});
|
|
1032
839
|
|
|
1033
840
|
pi.on("session_shutdown", async (_event, ctx) => {
|
|
1034
|
-
|
|
841
|
+
stopBlinkTimer();
|
|
1035
842
|
ctx.ui.setStatus(STATUS_KEY, undefined);
|
|
1036
843
|
ctx.ui.setWorkingMessage();
|
|
1037
844
|
});
|
|
@@ -1040,38 +847,34 @@ export default function compactTranscript(pi: ExtensionAPI) {
|
|
|
1040
847
|
state.agentActive = true;
|
|
1041
848
|
captureTheme(ctx);
|
|
1042
849
|
resetToolRun();
|
|
1043
|
-
|
|
1044
|
-
state.consecutiveThinking = 0;
|
|
1045
|
-
applyThinkingLabel(ctx);
|
|
850
|
+
state.runStats = newRunStats();
|
|
1046
851
|
});
|
|
1047
852
|
|
|
1048
853
|
pi.on("agent_end", (_event, _ctx) => {
|
|
1049
854
|
state.agentActive = false;
|
|
1050
|
-
state.
|
|
1051
|
-
|
|
855
|
+
state.currentBurst = [];
|
|
856
|
+
clearCurrentThought();
|
|
857
|
+
state.runningToolIds.clear();
|
|
858
|
+
stopBlinkTimer();
|
|
859
|
+
appendRunSummary(pi);
|
|
1052
860
|
});
|
|
1053
861
|
|
|
1054
862
|
pi.on("turn_start", (_event, ctx) => {
|
|
1055
|
-
state.consecutiveThinking = 0;
|
|
1056
863
|
captureTheme(ctx);
|
|
1057
|
-
applyThinkingLabel(ctx);
|
|
1058
864
|
});
|
|
1059
865
|
|
|
1060
866
|
pi.on("message_update", (event, ctx) => {
|
|
1061
867
|
captureTheme(ctx);
|
|
1062
868
|
const type = event.assistantMessageEvent?.type;
|
|
1063
|
-
if (type === "
|
|
1064
|
-
|
|
1065
|
-
applyThinkingLabel(ctx);
|
|
1066
|
-
return;
|
|
869
|
+
if (typeof type === "string" && type.startsWith("thinking_")) {
|
|
870
|
+
updateCurrentThoughtFromMessage(event.message);
|
|
1067
871
|
}
|
|
1068
|
-
if (
|
|
1069
|
-
|
|
1070
|
-
|
|
1071
|
-
|
|
1072
|
-
|
|
1073
|
-
|
|
1074
|
-
}
|
|
872
|
+
if (textSignalHasVisibleContent(event.assistantMessageEvent)) {
|
|
873
|
+
clearCurrentThought();
|
|
874
|
+
// Visible assistant text ends the current tool burst. Do not split on
|
|
875
|
+
// text_start alone: some providers create empty text blocks before a
|
|
876
|
+
// tool-only turn, and those blocks are hidden from the transcript.
|
|
877
|
+
state.currentBurst = [];
|
|
1075
878
|
}
|
|
1076
879
|
});
|
|
1077
880
|
|