@mrclrchtr/supi-debug 4.9.0 → 5.0.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 +10 -2
- package/node_modules/@mrclrchtr/supi-core/README.md +2 -0
- package/node_modules/@mrclrchtr/supi-core/package.json +1 -1
- package/node_modules/@mrclrchtr/supi-core/src/config/config.ts +31 -0
- package/node_modules/@mrclrchtr/supi-core/src/config.ts +2 -0
- package/node_modules/@mrclrchtr/supi-core/src/debug-registry.ts +19 -3
- package/node_modules/@mrclrchtr/supi-core/src/debug-timing.ts +34 -21
- package/node_modules/@mrclrchtr/supi-core/src/settings/settings-registry.ts +3 -0
- package/package.json +3 -2
- package/prompts/supi-tooling-retro.md +62 -0
- package/src/command.ts +132 -0
- package/src/debug.ts +92 -158
- package/src/output.ts +101 -0
- package/src/query.ts +45 -0
- package/src/render-details.ts +336 -0
- package/src/renderer.ts +232 -57
- package/src/session-events.ts +88 -14
- package/src/tool/guidance.ts +1 -1
|
@@ -0,0 +1,336 @@
|
|
|
1
|
+
import type { TruncationResult } from "@earendil-works/pi-coding-agent";
|
|
2
|
+
import {
|
|
3
|
+
type DebugAgentAccess,
|
|
4
|
+
type DebugEventView,
|
|
5
|
+
isDebugLevel,
|
|
6
|
+
} from "@mrclrchtr/supi-core/debug";
|
|
7
|
+
|
|
8
|
+
const MAX_RENDER_EVENTS = 12;
|
|
9
|
+
const MAX_EVENT_VALUE_DEPTH = 6;
|
|
10
|
+
const MAX_EVENT_VALUE_CHARS = 1_200;
|
|
11
|
+
const MAX_EVENT_VALUE_ITEMS = 256;
|
|
12
|
+
const MAX_EVENT_STRING_CHARS = 300;
|
|
13
|
+
const MAX_EVENT_TEXT_CHARS = 1_000;
|
|
14
|
+
|
|
15
|
+
/** Small truncation facts used by transcript renderers. */
|
|
16
|
+
export interface DebugOutputTruncation {
|
|
17
|
+
truncated: true;
|
|
18
|
+
outputLines: number;
|
|
19
|
+
totalLines: number;
|
|
20
|
+
outputBytes: number;
|
|
21
|
+
totalBytes: number;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/** Stable, bounded facts used by the debug tool and its transcript renderers. */
|
|
25
|
+
export type DebugRenderEvent = Omit<DebugEventView, "rawData">;
|
|
26
|
+
|
|
27
|
+
export interface DebugRenderDetails {
|
|
28
|
+
enabled?: boolean;
|
|
29
|
+
agentAccess?: DebugAgentAccess;
|
|
30
|
+
sessionFile?: string;
|
|
31
|
+
rawAccessDenied: boolean;
|
|
32
|
+
rawDataUnavailable: boolean;
|
|
33
|
+
events: DebugRenderEvent[];
|
|
34
|
+
eventCount: number;
|
|
35
|
+
omittedEventCount: number;
|
|
36
|
+
eventDataTruncated: boolean;
|
|
37
|
+
persistedEventCount?: number;
|
|
38
|
+
truncation?: DebugOutputTruncation;
|
|
39
|
+
emptyReason?: "no-persisted-events" | "no-matches";
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
interface DetailValueState {
|
|
43
|
+
remainingChars: number;
|
|
44
|
+
remainingItems: number;
|
|
45
|
+
truncated: boolean;
|
|
46
|
+
seen: WeakSet<object>;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
interface CreateDebugRenderDetailsOptions {
|
|
50
|
+
enabled?: boolean;
|
|
51
|
+
agentAccess?: DebugAgentAccess;
|
|
52
|
+
sessionFile?: string;
|
|
53
|
+
rawAccessDenied?: boolean;
|
|
54
|
+
rawDataUnavailable?: boolean;
|
|
55
|
+
persistedEventCount?: number;
|
|
56
|
+
truncation?: TruncationResult;
|
|
57
|
+
eventCount?: number;
|
|
58
|
+
emptyReason?: "no-persisted-events" | "no-matches";
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function boundedText(value: string, maxChars: number): { value: string; truncated: boolean } {
|
|
62
|
+
if (value.length <= maxChars) return { value, truncated: false };
|
|
63
|
+
return { value: `${value.slice(0, Math.max(0, maxChars - 1))}…`, truncated: true };
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function boundedCount(value: unknown, fallback: number): number {
|
|
67
|
+
return typeof value === "number" && Number.isFinite(value) && value >= 0
|
|
68
|
+
? Math.floor(value)
|
|
69
|
+
: fallback;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function takeItem(state: DetailValueState): boolean {
|
|
73
|
+
if (state.remainingItems <= 0) {
|
|
74
|
+
state.truncated = true;
|
|
75
|
+
return false;
|
|
76
|
+
}
|
|
77
|
+
state.remainingItems--;
|
|
78
|
+
return true;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function takeChars(state: DetailValueState, value: string): string {
|
|
82
|
+
if (state.remainingChars <= 0) {
|
|
83
|
+
state.truncated = true;
|
|
84
|
+
return "[Truncated]";
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
const maxChars = Math.min(MAX_EVENT_STRING_CHARS, state.remainingChars);
|
|
88
|
+
state.remainingChars -= maxChars;
|
|
89
|
+
if (value.length <= maxChars) return value;
|
|
90
|
+
|
|
91
|
+
state.truncated = true;
|
|
92
|
+
return `${value.slice(0, Math.max(0, maxChars - 1))}…`;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function boundArray(value: unknown[], state: DetailValueState, depth: number): unknown[] {
|
|
96
|
+
const items: unknown[] = [];
|
|
97
|
+
for (const item of value) {
|
|
98
|
+
if (state.remainingChars <= 0 || state.remainingItems <= 0) {
|
|
99
|
+
state.truncated = true;
|
|
100
|
+
break;
|
|
101
|
+
}
|
|
102
|
+
items.push(boundValue(item, state, depth + 1));
|
|
103
|
+
}
|
|
104
|
+
if (items.length < value.length) {
|
|
105
|
+
state.truncated = true;
|
|
106
|
+
items.push("[Truncated]");
|
|
107
|
+
}
|
|
108
|
+
return items;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
function boundObject(
|
|
112
|
+
value: Record<string, unknown>,
|
|
113
|
+
state: DetailValueState,
|
|
114
|
+
depth: number,
|
|
115
|
+
): Record<string, unknown> | string {
|
|
116
|
+
const objectValue: Record<string, unknown> = {};
|
|
117
|
+
let omittedEntries = false;
|
|
118
|
+
|
|
119
|
+
try {
|
|
120
|
+
for (const key in value) {
|
|
121
|
+
if (!Object.hasOwn(value, key)) continue;
|
|
122
|
+
if (state.remainingChars <= 0 || state.remainingItems <= 0) {
|
|
123
|
+
state.truncated = true;
|
|
124
|
+
omittedEntries = true;
|
|
125
|
+
break;
|
|
126
|
+
}
|
|
127
|
+
const boundedKey = takeChars(state, key);
|
|
128
|
+
objectValue[boundedKey] = boundValue(value[key], state, depth + 1);
|
|
129
|
+
}
|
|
130
|
+
} catch {
|
|
131
|
+
state.truncated = true;
|
|
132
|
+
return "[Unserializable]";
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
if (omittedEntries) {
|
|
136
|
+
state.truncated = true;
|
|
137
|
+
objectValue["[Truncated]"] = true;
|
|
138
|
+
}
|
|
139
|
+
return objectValue;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
function boundReference(value: object, state: DetailValueState, depth: number): unknown {
|
|
143
|
+
if (depth >= MAX_EVENT_VALUE_DEPTH) {
|
|
144
|
+
state.truncated = true;
|
|
145
|
+
return "[MaxDepth]";
|
|
146
|
+
}
|
|
147
|
+
if (state.seen.has(value)) {
|
|
148
|
+
state.truncated = true;
|
|
149
|
+
return "[Circular]";
|
|
150
|
+
}
|
|
151
|
+
state.seen.add(value);
|
|
152
|
+
return Array.isArray(value)
|
|
153
|
+
? boundArray(value, state, depth)
|
|
154
|
+
: boundObject(value as Record<string, unknown>, state, depth);
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
function boundValue(value: unknown, state: DetailValueState, depth: number): unknown {
|
|
158
|
+
if (!takeItem(state)) return "[Truncated]";
|
|
159
|
+
if (value === undefined || value === null) return value;
|
|
160
|
+
if (typeof value === "string") return takeChars(state, value);
|
|
161
|
+
if (typeof value === "boolean") return value;
|
|
162
|
+
if (typeof value === "number")
|
|
163
|
+
return Number.isFinite(value) ? value : takeChars(state, String(value));
|
|
164
|
+
if (typeof value === "bigint") return takeChars(state, `${value}n`);
|
|
165
|
+
if (typeof value === "function" || typeof value === "symbol") {
|
|
166
|
+
return takeChars(state, String(value));
|
|
167
|
+
}
|
|
168
|
+
return boundReference(value, state, depth);
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
function boundEvent(event: DebugEventView): { event: DebugRenderEvent; truncated: boolean } {
|
|
172
|
+
const state: DetailValueState = {
|
|
173
|
+
remainingChars: MAX_EVENT_VALUE_CHARS,
|
|
174
|
+
remainingItems: MAX_EVENT_VALUE_ITEMS,
|
|
175
|
+
truncated: false,
|
|
176
|
+
seen: new WeakSet<object>(),
|
|
177
|
+
};
|
|
178
|
+
const source = boundedText(String(event.source), MAX_EVENT_STRING_CHARS);
|
|
179
|
+
const category = boundedText(String(event.category), MAX_EVENT_STRING_CHARS);
|
|
180
|
+
const message = boundedText(String(event.message), MAX_EVENT_TEXT_CHARS);
|
|
181
|
+
const cwd =
|
|
182
|
+
event.cwd === undefined ? undefined : boundedText(String(event.cwd), MAX_EVENT_STRING_CHARS);
|
|
183
|
+
const operationId =
|
|
184
|
+
event.operationId === undefined
|
|
185
|
+
? undefined
|
|
186
|
+
: boundedText(String(event.operationId), MAX_EVENT_STRING_CHARS);
|
|
187
|
+
|
|
188
|
+
const boundedEvent: DebugRenderEvent = {
|
|
189
|
+
id: Number.isFinite(event.id) ? event.id : 0,
|
|
190
|
+
timestamp: Number.isFinite(event.timestamp) ? event.timestamp : 0,
|
|
191
|
+
...(operationId ? { operationId: operationId.value } : {}),
|
|
192
|
+
source: source.value,
|
|
193
|
+
level: event.level,
|
|
194
|
+
category: category.value,
|
|
195
|
+
message: message.value,
|
|
196
|
+
...(cwd ? { cwd: cwd.value } : {}),
|
|
197
|
+
...(event.data === undefined ? {} : { data: boundValue(event.data, state, 0) }),
|
|
198
|
+
};
|
|
199
|
+
|
|
200
|
+
return {
|
|
201
|
+
event: boundedEvent,
|
|
202
|
+
truncated:
|
|
203
|
+
source.truncated ||
|
|
204
|
+
category.truncated ||
|
|
205
|
+
message.truncated ||
|
|
206
|
+
Boolean(cwd?.truncated) ||
|
|
207
|
+
Boolean(operationId?.truncated) ||
|
|
208
|
+
state.truncated,
|
|
209
|
+
};
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
function summarizeTruncation(
|
|
213
|
+
truncation: TruncationResult | undefined,
|
|
214
|
+
): DebugOutputTruncation | undefined {
|
|
215
|
+
if (!truncation?.truncated) return undefined;
|
|
216
|
+
return {
|
|
217
|
+
truncated: true,
|
|
218
|
+
outputLines: truncation.outputLines,
|
|
219
|
+
totalLines: truncation.totalLines,
|
|
220
|
+
outputBytes: truncation.outputBytes,
|
|
221
|
+
totalBytes: truncation.totalBytes,
|
|
222
|
+
};
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
/** Build bounded, JSON-safe details for tool and message transcript surfaces. */
|
|
226
|
+
export function createDebugRenderDetails(
|
|
227
|
+
events: readonly DebugEventView[],
|
|
228
|
+
options: CreateDebugRenderDetailsOptions = {},
|
|
229
|
+
): DebugRenderDetails {
|
|
230
|
+
const boundedEvents = events.slice(0, MAX_RENDER_EVENTS).map(boundEvent);
|
|
231
|
+
const eventDataTruncated = boundedEvents.some((entry) => entry.truncated);
|
|
232
|
+
const eventCount = Math.max(events.length, boundedCount(options.eventCount, events.length));
|
|
233
|
+
|
|
234
|
+
return {
|
|
235
|
+
...(options.enabled === undefined ? {} : { enabled: options.enabled }),
|
|
236
|
+
...(options.agentAccess === undefined ? {} : { agentAccess: options.agentAccess }),
|
|
237
|
+
...(options.sessionFile === undefined
|
|
238
|
+
? {}
|
|
239
|
+
: { sessionFile: boundedText(options.sessionFile, MAX_EVENT_STRING_CHARS).value }),
|
|
240
|
+
rawAccessDenied: options.rawAccessDenied === true,
|
|
241
|
+
rawDataUnavailable: options.rawDataUnavailable === true,
|
|
242
|
+
events: boundedEvents.map((entry) => entry.event),
|
|
243
|
+
eventCount,
|
|
244
|
+
omittedEventCount: Math.max(0, eventCount - boundedEvents.length),
|
|
245
|
+
eventDataTruncated,
|
|
246
|
+
...(options.persistedEventCount === undefined
|
|
247
|
+
? {}
|
|
248
|
+
: { persistedEventCount: boundedCount(options.persistedEventCount, 0) }),
|
|
249
|
+
...(options.emptyReason === undefined ? {} : { emptyReason: options.emptyReason }),
|
|
250
|
+
...(summarizeTruncation(options.truncation)
|
|
251
|
+
? { truncation: summarizeTruncation(options.truncation) }
|
|
252
|
+
: {}),
|
|
253
|
+
};
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
function parseEvent(value: unknown): DebugEventView | undefined {
|
|
257
|
+
if (typeof value !== "object" || value === null) return undefined;
|
|
258
|
+
const record = value as Record<string, unknown>;
|
|
259
|
+
if (
|
|
260
|
+
typeof record.id !== "number" ||
|
|
261
|
+
!Number.isFinite(record.id) ||
|
|
262
|
+
typeof record.timestamp !== "number" ||
|
|
263
|
+
!Number.isFinite(record.timestamp) ||
|
|
264
|
+
typeof record.source !== "string" ||
|
|
265
|
+
!isDebugLevel(record.level) ||
|
|
266
|
+
typeof record.category !== "string" ||
|
|
267
|
+
typeof record.message !== "string"
|
|
268
|
+
) {
|
|
269
|
+
return undefined;
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
return {
|
|
273
|
+
id: record.id,
|
|
274
|
+
timestamp: record.timestamp,
|
|
275
|
+
source: record.source,
|
|
276
|
+
level: record.level,
|
|
277
|
+
category: record.category,
|
|
278
|
+
message: record.message,
|
|
279
|
+
...(typeof record.operationId === "string" ? { operationId: record.operationId } : {}),
|
|
280
|
+
...(typeof record.cwd === "string" ? { cwd: record.cwd } : {}),
|
|
281
|
+
...(record.data === undefined ? {} : { data: record.data }),
|
|
282
|
+
};
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
function parseTruncation(value: unknown): DebugOutputTruncation | undefined {
|
|
286
|
+
if (typeof value !== "object" || value === null) return undefined;
|
|
287
|
+
const record = value as Record<string, unknown>;
|
|
288
|
+
if (record.truncated !== true) return undefined;
|
|
289
|
+
return {
|
|
290
|
+
truncated: true,
|
|
291
|
+
outputLines: boundedCount(record.outputLines, 0),
|
|
292
|
+
totalLines: boundedCount(record.totalLines, 0),
|
|
293
|
+
outputBytes: boundedCount(record.outputBytes, 0),
|
|
294
|
+
totalBytes: boundedCount(record.totalBytes, 0),
|
|
295
|
+
};
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
/** Read old or malformed message details without allowing the renderer to throw. */
|
|
299
|
+
export function readDebugRenderDetails(value: unknown): DebugRenderDetails {
|
|
300
|
+
const record =
|
|
301
|
+
typeof value === "object" && value !== null ? (value as Record<string, unknown>) : {};
|
|
302
|
+
const events = Array.isArray(record.events)
|
|
303
|
+
? record.events.flatMap((event) => {
|
|
304
|
+
const parsed = parseEvent(event);
|
|
305
|
+
return parsed ? [parsed] : [];
|
|
306
|
+
})
|
|
307
|
+
: [];
|
|
308
|
+
const details = createDebugRenderDetails(events, {
|
|
309
|
+
enabled: typeof record.enabled === "boolean" ? record.enabled : undefined,
|
|
310
|
+
agentAccess:
|
|
311
|
+
record.agentAccess === "off" ||
|
|
312
|
+
record.agentAccess === "sanitized" ||
|
|
313
|
+
record.agentAccess === "raw"
|
|
314
|
+
? record.agentAccess
|
|
315
|
+
: undefined,
|
|
316
|
+
sessionFile: typeof record.sessionFile === "string" ? record.sessionFile : undefined,
|
|
317
|
+
rawAccessDenied: record.rawAccessDenied === true,
|
|
318
|
+
rawDataUnavailable: record.rawDataUnavailable === true,
|
|
319
|
+
emptyReason:
|
|
320
|
+
record.emptyReason === "no-persisted-events" || record.emptyReason === "no-matches"
|
|
321
|
+
? record.emptyReason
|
|
322
|
+
: undefined,
|
|
323
|
+
persistedEventCount:
|
|
324
|
+
typeof record.persistedEventCount === "number" ? record.persistedEventCount : undefined,
|
|
325
|
+
eventCount: boundedCount(record.eventCount, events.length),
|
|
326
|
+
});
|
|
327
|
+
|
|
328
|
+
const parsedTruncation = parseTruncation(record.truncation);
|
|
329
|
+
if (parsedTruncation) details.truncation = parsedTruncation;
|
|
330
|
+
details.eventDataTruncated = details.eventDataTruncated || record.eventDataTruncated === true;
|
|
331
|
+
details.omittedEventCount = Math.max(
|
|
332
|
+
details.omittedEventCount,
|
|
333
|
+
boundedCount(record.omittedEventCount, details.omittedEventCount),
|
|
334
|
+
);
|
|
335
|
+
return details;
|
|
336
|
+
}
|
package/src/renderer.ts
CHANGED
|
@@ -1,16 +1,73 @@
|
|
|
1
|
-
import
|
|
1
|
+
import {
|
|
2
|
+
type ExtensionAPI,
|
|
3
|
+
formatSize,
|
|
4
|
+
keyHint,
|
|
5
|
+
type Theme,
|
|
6
|
+
type ToolRenderResultOptions,
|
|
7
|
+
truncateHead,
|
|
8
|
+
} from "@earendil-works/pi-coding-agent";
|
|
2
9
|
import { Text } from "@earendil-works/pi-tui";
|
|
3
10
|
import type { DebugEventView } from "@mrclrchtr/supi-core/debug";
|
|
4
11
|
import { formatDataLines } from "./format.ts";
|
|
12
|
+
import {
|
|
13
|
+
createDebugRenderDetails,
|
|
14
|
+
type DebugRenderDetails,
|
|
15
|
+
type DebugRenderEvent,
|
|
16
|
+
readDebugRenderDetails,
|
|
17
|
+
} from "./render-details.ts";
|
|
5
18
|
|
|
6
19
|
const DEBUG_REPORT_TYPE = "supi-debug-report";
|
|
20
|
+
const MAX_RENDER_LINES = 240;
|
|
21
|
+
const MAX_RENDER_BYTES = 16 * 1024;
|
|
7
22
|
|
|
8
|
-
interface
|
|
9
|
-
|
|
10
|
-
|
|
23
|
+
interface DebugToolResult {
|
|
24
|
+
content?: Array<{ type: string; text?: string }>;
|
|
25
|
+
details?: unknown;
|
|
11
26
|
}
|
|
12
27
|
|
|
13
|
-
|
|
28
|
+
interface DebugCallArgs {
|
|
29
|
+
operationId?: unknown;
|
|
30
|
+
source?: unknown;
|
|
31
|
+
level?: unknown;
|
|
32
|
+
category?: unknown;
|
|
33
|
+
limit?: unknown;
|
|
34
|
+
sessionFile?: unknown;
|
|
35
|
+
includeRaw?: unknown;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function boundedCallValue(value: unknown, maxLength = 80): string | undefined {
|
|
39
|
+
if (typeof value !== "string" && typeof value !== "number") return undefined;
|
|
40
|
+
const text = String(value);
|
|
41
|
+
return text.length > maxLength ? `${text.slice(0, maxLength - 1)}…` : text;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function formatCallArgs(args: unknown): string {
|
|
45
|
+
const input = (args ?? {}) as DebugCallArgs;
|
|
46
|
+
const filters: string[] = [];
|
|
47
|
+
const operationId = boundedCallValue(input.operationId);
|
|
48
|
+
const source = boundedCallValue(input.source);
|
|
49
|
+
const level = boundedCallValue(input.level);
|
|
50
|
+
const category = boundedCallValue(input.category);
|
|
51
|
+
const limit = boundedCallValue(input.limit);
|
|
52
|
+
const sessionFile = boundedCallValue(input.sessionFile);
|
|
53
|
+
|
|
54
|
+
if (operationId) filters.push(`operationId=${operationId}`);
|
|
55
|
+
if (source) filters.push(`source=${source}`);
|
|
56
|
+
if (level) filters.push(`level=${level}`);
|
|
57
|
+
if (category) filters.push(`category=${category}`);
|
|
58
|
+
if (limit) filters.push(`limit=${limit}`);
|
|
59
|
+
if (sessionFile) filters.push(`sessionFile=${sessionFile}`);
|
|
60
|
+
if (input.includeRaw === true) filters.push("raw");
|
|
61
|
+
return filters.join(" ");
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/** Render the compact human-facing call header for `supi_debug`. */
|
|
65
|
+
export function renderDebugToolCall(args: unknown, theme: Theme): Text {
|
|
66
|
+
const callArgs = formatCallArgs(args);
|
|
67
|
+
let content = theme.fg("toolTitle", "supi_debug");
|
|
68
|
+
if (callArgs) content += ` ${theme.fg("dim", callArgs)}`;
|
|
69
|
+
return new Text(content, 0, 0);
|
|
70
|
+
}
|
|
14
71
|
|
|
15
72
|
function formatLevel(theme: Theme, level: string): string {
|
|
16
73
|
const color =
|
|
@@ -24,81 +81,199 @@ function formatLevel(theme: Theme, level: string): string {
|
|
|
24
81
|
return theme.fg(color, level.toUpperCase());
|
|
25
82
|
}
|
|
26
83
|
|
|
27
|
-
function
|
|
28
|
-
const
|
|
84
|
+
function formatTimestamp(timestamp: number): string {
|
|
85
|
+
const date = new Date(timestamp);
|
|
86
|
+
return Number.isNaN(date.getTime()) ? String(timestamp) : date.toISOString();
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function pushEventLines(lines: string[], event: DebugRenderEvent, theme: Theme): void {
|
|
90
|
+
const timestamp = theme.fg("dim", `[${formatTimestamp(event.timestamp)}]`);
|
|
29
91
|
const level = formatLevel(theme, event.level);
|
|
30
92
|
const source = theme.fg("toolTitle", `${event.source}/${event.category}`);
|
|
31
93
|
|
|
32
94
|
lines.push(`${timestamp} ${level} ${source}: ${event.message}`);
|
|
95
|
+
if (event.operationId) lines.push(theme.fg("dim", ` operationId: ${event.operationId}`));
|
|
96
|
+
if (event.cwd) lines.push(theme.fg("dim", ` cwd: ${event.cwd}`));
|
|
97
|
+
pushDataLines(lines, "data", event.data, theme);
|
|
98
|
+
}
|
|
33
99
|
|
|
34
|
-
|
|
35
|
-
|
|
100
|
+
function pushDataLines(lines: string[], label: string, value: unknown, theme: Theme): void {
|
|
101
|
+
const dataLines = formatDataLines(value);
|
|
102
|
+
if (dataLines.length === 0) return;
|
|
103
|
+
if (dataLines.length === 1) {
|
|
104
|
+
lines.push(theme.fg("dim", ` ${label}: ${dataLines[0]}`));
|
|
105
|
+
return;
|
|
36
106
|
}
|
|
107
|
+
lines.push(theme.fg("dim", ` ${label}:`));
|
|
108
|
+
for (const line of dataLines) lines.push(theme.fg("dim", ` ${line}`));
|
|
109
|
+
}
|
|
37
110
|
|
|
38
|
-
|
|
39
|
-
if (
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
for (const dl of dataLines) {
|
|
45
|
-
lines.push(theme.fg("dim", ` ${dl}`));
|
|
46
|
-
}
|
|
47
|
-
}
|
|
48
|
-
}
|
|
111
|
+
function formatTruncation(truncation: DebugRenderDetails["truncation"]): string | undefined {
|
|
112
|
+
if (!truncation?.truncated) return undefined;
|
|
113
|
+
const omittedLines = truncation.totalLines - truncation.outputLines;
|
|
114
|
+
const omittedBytes = truncation.totalBytes - truncation.outputBytes;
|
|
115
|
+
return `Agent output truncated: showing ${truncation.outputLines} of ${truncation.totalLines} lines (${formatSize(truncation.outputBytes)} of ${formatSize(truncation.totalBytes)}); ${omittedLines} lines (${formatSize(omittedBytes)}) omitted.`;
|
|
116
|
+
}
|
|
49
117
|
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
118
|
+
function buildDetailNotes(details: DebugRenderDetails, theme: Theme): string[] {
|
|
119
|
+
const notes: string[] = [];
|
|
120
|
+
if (details.omittedEventCount > 0) {
|
|
121
|
+
notes.push(
|
|
122
|
+
theme.fg(
|
|
123
|
+
"warning",
|
|
124
|
+
`${details.omittedEventCount} event${details.omittedEventCount === 1 ? "" : "s"} omitted from the transcript view.`,
|
|
125
|
+
),
|
|
126
|
+
);
|
|
127
|
+
}
|
|
128
|
+
if (details.eventDataTruncated) {
|
|
129
|
+
notes.push(theme.fg("warning", "Some event data was bounded for the transcript view."));
|
|
130
|
+
}
|
|
131
|
+
const truncation = formatTruncation(details.truncation);
|
|
132
|
+
if (truncation) notes.push(theme.fg("warning", truncation));
|
|
133
|
+
if (details.rawDataUnavailable) {
|
|
134
|
+
notes.push(theme.fg("warning", "Raw debug data is not persisted for historical sessions."));
|
|
135
|
+
} else if (details.rawAccessDenied) {
|
|
136
|
+
notes.push(
|
|
137
|
+
theme.fg(
|
|
138
|
+
"warning",
|
|
139
|
+
"Raw debug data was requested but is not enabled in SuPi Debug settings.",
|
|
140
|
+
),
|
|
141
|
+
);
|
|
60
142
|
}
|
|
143
|
+
return notes;
|
|
61
144
|
}
|
|
62
145
|
|
|
63
|
-
function
|
|
146
|
+
function buildEventLines(details: DebugRenderDetails, theme: Theme): string[] {
|
|
64
147
|
const lines: string[] = [];
|
|
65
|
-
for (const event of details.events
|
|
148
|
+
for (const event of details.events) {
|
|
66
149
|
if (lines.length > 0) lines.push("");
|
|
67
150
|
pushEventLines(lines, event, theme);
|
|
68
151
|
}
|
|
69
|
-
|
|
70
|
-
if (details.rawAccessDenied) {
|
|
71
|
-
lines.push("");
|
|
152
|
+
if (lines.length === 0) {
|
|
72
153
|
lines.push(
|
|
73
154
|
theme.fg(
|
|
74
|
-
"
|
|
75
|
-
|
|
155
|
+
"muted",
|
|
156
|
+
details.emptyReason === "no-persisted-events"
|
|
157
|
+
? "This session has no persisted debug events."
|
|
158
|
+
: "No matching debug events available.",
|
|
76
159
|
),
|
|
77
160
|
);
|
|
78
161
|
}
|
|
162
|
+
return lines;
|
|
163
|
+
}
|
|
79
164
|
|
|
80
|
-
|
|
165
|
+
function renderExpandedReport(details: DebugRenderDetails, theme: Theme): string {
|
|
166
|
+
const notes = buildDetailNotes(details, theme);
|
|
167
|
+
const noteText = notes.join("\n");
|
|
168
|
+
const noteBytes = new TextEncoder().encode(noteText).byteLength;
|
|
169
|
+
const marker = "[Transcript view truncated: more output omitted.]";
|
|
170
|
+
const markerBytes = new TextEncoder().encode(marker).byteLength;
|
|
171
|
+
const result = truncateHead(buildEventLines(details, theme).join("\n"), {
|
|
172
|
+
maxLines: Math.max(1, MAX_RENDER_LINES - notes.length - 1),
|
|
173
|
+
maxBytes: Math.max(1, MAX_RENDER_BYTES - noteBytes - markerBytes - 2),
|
|
174
|
+
});
|
|
175
|
+
const bodyLines = result.content ? result.content.split("\n") : [];
|
|
176
|
+
if (result.truncated) bodyLines.push(marker);
|
|
177
|
+
return [...bodyLines, ...notes].join("\n");
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
function renderToolSummary(details: DebugRenderDetails, theme: Theme): string {
|
|
181
|
+
if (details.eventCount === 0) {
|
|
182
|
+
return details.emptyReason === "no-persisted-events"
|
|
183
|
+
? theme.fg("muted", "No persisted debug events")
|
|
184
|
+
: theme.fg("muted", "No matching debug events");
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
let summary = `${details.eventCount} event${details.eventCount === 1 ? "" : "s"}`;
|
|
188
|
+
if (details.omittedEventCount > 0) summary += ` · ${details.omittedEventCount} omitted`;
|
|
189
|
+
if (details.eventDataTruncated) summary += " · data bounded";
|
|
190
|
+
if (details.truncation?.truncated) summary += " · output truncated";
|
|
191
|
+
if (details.rawDataUnavailable || details.rawAccessDenied) summary += " · raw unavailable";
|
|
192
|
+
return theme.fg("muted", summary);
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
function renderProgress(details: unknown, expanded: boolean, theme: Theme): Text {
|
|
196
|
+
const progress =
|
|
197
|
+
typeof details === "object" && details !== null ? (details as Record<string, unknown>) : {};
|
|
198
|
+
const scannedLines =
|
|
199
|
+
typeof progress.scannedLines === "number" ? progress.scannedLines.toLocaleString("en-US") : "?";
|
|
200
|
+
const matchedEvents =
|
|
201
|
+
typeof progress.matchedEvents === "number"
|
|
202
|
+
? progress.matchedEvents.toLocaleString("en-US")
|
|
203
|
+
: "?";
|
|
204
|
+
const line = `Reading persisted debug events… ${scannedLines} lines scanned · ${matchedEvents} matches`;
|
|
205
|
+
return new Text(theme.fg("warning", expanded ? line : "Reading persisted debug events…"), 0, 0);
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
/** Render the compact or expanded result for the `supi_debug` tool. */
|
|
209
|
+
export function renderDebugToolResult(
|
|
210
|
+
result: DebugToolResult,
|
|
211
|
+
options: ToolRenderResultOptions,
|
|
212
|
+
theme: Theme,
|
|
213
|
+
context: { isError: boolean },
|
|
214
|
+
): Text {
|
|
215
|
+
if (options.isPartial) return renderProgress(result.details, options.expanded, theme);
|
|
216
|
+
if (context.isError) return new Text(theme.fg("error", "supi_debug failed"), 0, 0);
|
|
217
|
+
|
|
218
|
+
const details = readDebugRenderDetails(result.details);
|
|
219
|
+
if (!options.expanded) {
|
|
220
|
+
let summary = renderToolSummary(details, theme);
|
|
221
|
+
if (details.eventCount > 0)
|
|
222
|
+
summary += theme.fg("dim", ` ${keyHint("app.tools.expand", "to expand")}`);
|
|
223
|
+
return new Text(summary, 0, 0);
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
return new Text(renderExpandedReport(details, theme), 0, 0);
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
function renderEmptyMessage(content: unknown, details: DebugRenderDetails, theme: Theme): Text {
|
|
230
|
+
const text =
|
|
231
|
+
typeof content === "string"
|
|
232
|
+
? content
|
|
233
|
+
: details.emptyReason === "no-persisted-events"
|
|
234
|
+
? "This session has no persisted debug events."
|
|
235
|
+
: "No debug events.";
|
|
236
|
+
return new Text(theme.fg("muted", text), 0, 0);
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
function renderCollapsedMessage(details: DebugRenderDetails, theme: Theme): Text {
|
|
240
|
+
const first = details.events[0];
|
|
241
|
+
let summary = `${details.eventCount} event${details.eventCount === 1 ? "" : "s"} — ${first?.source ?? "debug"}/${first?.category ?? "event"}`;
|
|
242
|
+
if (details.eventCount > 1) summary += ` +${details.eventCount - 1} more`;
|
|
243
|
+
if (details.omittedEventCount > 0) summary += ` · ${details.omittedEventCount} omitted`;
|
|
244
|
+
if (details.eventDataTruncated) summary += " · data bounded";
|
|
245
|
+
if (details.truncation?.truncated) summary += " · output truncated";
|
|
246
|
+
if (details.rawDataUnavailable || details.rawAccessDenied) summary += " · raw unavailable";
|
|
247
|
+
return new Text(theme.fg("muted", summary), 0, 0);
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
function renderDebugMessage(
|
|
251
|
+
content: unknown,
|
|
252
|
+
details: DebugRenderDetails,
|
|
253
|
+
expanded: boolean,
|
|
254
|
+
theme: Theme,
|
|
255
|
+
): Text {
|
|
256
|
+
if (details.eventCount === 0) return renderEmptyMessage(content, details, theme);
|
|
257
|
+
if (!expanded) return renderCollapsedMessage(details, theme);
|
|
258
|
+
return new Text(renderExpandedReport(details, theme), 0, 0);
|
|
81
259
|
}
|
|
82
260
|
|
|
83
261
|
/** Register the TUI message renderer for supi-debug-report custom messages. */
|
|
84
262
|
export function registerDebugMessageRenderer(pi: ExtensionAPI): void {
|
|
85
|
-
pi.registerMessageRenderer(DEBUG_REPORT_TYPE, (message, options, theme) =>
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
return new Text(renderExpandedReport(details, theme), 0, 0);
|
|
103
|
-
});
|
|
263
|
+
pi.registerMessageRenderer(DEBUG_REPORT_TYPE, (message, options, theme) =>
|
|
264
|
+
renderDebugMessage(
|
|
265
|
+
message.content,
|
|
266
|
+
readDebugRenderDetails(message.details),
|
|
267
|
+
options.expanded,
|
|
268
|
+
theme,
|
|
269
|
+
),
|
|
270
|
+
);
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
/** Create bounded details for a debug report message. */
|
|
274
|
+
export function createDebugMessageDetails(
|
|
275
|
+
events: readonly DebugEventView[],
|
|
276
|
+
options: Parameters<typeof createDebugRenderDetails>[1] = {},
|
|
277
|
+
): DebugRenderDetails {
|
|
278
|
+
return createDebugRenderDetails(events, options);
|
|
104
279
|
}
|