@mrclrchtr/supi-debug 4.10.0 → 6.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.
@@ -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,22 @@
1
- import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
1
+ import {
2
+ type ExtensionAPI,
3
+ formatSize,
4
+ type Theme,
5
+ truncateHead,
6
+ } from "@earendil-works/pi-coding-agent";
2
7
  import { Text } from "@earendil-works/pi-tui";
3
8
  import type { DebugEventView } from "@mrclrchtr/supi-core/debug";
4
9
  import { formatDataLines } from "./format.ts";
10
+ import {
11
+ createDebugRenderDetails,
12
+ type DebugRenderDetails,
13
+ type DebugRenderEvent,
14
+ readDebugRenderDetails,
15
+ } from "./render-details.ts";
5
16
 
6
17
  const DEBUG_REPORT_TYPE = "supi-debug-report";
7
-
8
- interface DebugReportDetails {
9
- events?: DebugEventView[];
10
- rawAccessDenied?: boolean;
11
- }
12
-
13
- type Theme = Parameters<Parameters<ExtensionAPI["registerMessageRenderer"]>[1]>[2];
18
+ const MAX_RENDER_LINES = 240;
19
+ const MAX_RENDER_BYTES = 16 * 1024;
14
20
 
15
21
  function formatLevel(theme: Theme, level: string): string {
16
22
  const color =
@@ -24,81 +30,150 @@ function formatLevel(theme: Theme, level: string): string {
24
30
  return theme.fg(color, level.toUpperCase());
25
31
  }
26
32
 
27
- function pushEventLines(lines: string[], event: DebugEventView, theme: Theme): void {
28
- const timestamp = theme.fg("dim", `[${new Date(event.timestamp).toISOString()}]`);
33
+ function formatTimestamp(timestamp: number): string {
34
+ const date = new Date(timestamp);
35
+ return Number.isNaN(date.getTime()) ? String(timestamp) : date.toISOString();
36
+ }
37
+
38
+ function pushEventLines(lines: string[], event: DebugRenderEvent, theme: Theme): void {
39
+ const timestamp = theme.fg("dim", `[${formatTimestamp(event.timestamp)}]`);
29
40
  const level = formatLevel(theme, event.level);
30
41
  const source = theme.fg("toolTitle", `${event.source}/${event.category}`);
31
42
 
32
43
  lines.push(`${timestamp} ${level} ${source}: ${event.message}`);
44
+ if (event.operationId) lines.push(theme.fg("dim", ` operationId: ${event.operationId}`));
45
+ if (event.cwd) lines.push(theme.fg("dim", ` cwd: ${event.cwd}`));
46
+ pushDataLines(lines, "data", event.data, theme);
47
+ }
33
48
 
34
- if (event.cwd) {
35
- lines.push(theme.fg("dim", ` cwd: ${event.cwd}`));
49
+ function pushDataLines(lines: string[], label: string, value: unknown, theme: Theme): void {
50
+ const dataLines = formatDataLines(value);
51
+ if (dataLines.length === 0) return;
52
+ if (dataLines.length === 1) {
53
+ lines.push(theme.fg("dim", ` ${label}: ${dataLines[0]}`));
54
+ return;
36
55
  }
56
+ lines.push(theme.fg("dim", ` ${label}:`));
57
+ for (const line of dataLines) lines.push(theme.fg("dim", ` ${line}`));
58
+ }
37
59
 
38
- const dataLines = formatDataLines(event.data);
39
- if (dataLines.length > 0) {
40
- if (dataLines.length === 1) {
41
- lines.push(theme.fg("dim", ` data: ${dataLines[0]}`));
42
- } else {
43
- lines.push(theme.fg("dim", " data:"));
44
- for (const dl of dataLines) {
45
- lines.push(theme.fg("dim", ` ${dl}`));
46
- }
47
- }
48
- }
60
+ function formatTruncation(truncation: DebugRenderDetails["truncation"]): string | undefined {
61
+ if (!truncation?.truncated) return undefined;
62
+ const omittedLines = truncation.totalLines - truncation.outputLines;
63
+ const omittedBytes = truncation.totalBytes - truncation.outputBytes;
64
+ return `Agent output truncated: showing ${truncation.outputLines} of ${truncation.totalLines} lines (${formatSize(truncation.outputBytes)} of ${formatSize(truncation.totalBytes)}); ${omittedLines} lines (${formatSize(omittedBytes)}) omitted.`;
65
+ }
49
66
 
50
- const rawLines = formatDataLines(event.rawData);
51
- if (rawLines.length > 0) {
52
- if (rawLines.length === 1) {
53
- lines.push(theme.fg("dim", ` rawData: ${rawLines[0]}`));
54
- } else {
55
- lines.push(theme.fg("dim", " rawData:"));
56
- for (const rl of rawLines) {
57
- lines.push(theme.fg("dim", ` ${rl}`));
58
- }
59
- }
67
+ function buildDetailNotes(details: DebugRenderDetails, theme: Theme): string[] {
68
+ const notes: string[] = [];
69
+ if (details.omittedEventCount > 0) {
70
+ notes.push(
71
+ theme.fg(
72
+ "warning",
73
+ `${details.omittedEventCount} event${details.omittedEventCount === 1 ? "" : "s"} omitted from the transcript view.`,
74
+ ),
75
+ );
76
+ }
77
+ if (details.eventDataTruncated) {
78
+ notes.push(theme.fg("warning", "Some event data was bounded for the transcript view."));
60
79
  }
80
+ const truncation = formatTruncation(details.truncation);
81
+ if (truncation) notes.push(theme.fg("warning", truncation));
82
+ if (details.rawDataUnavailable) {
83
+ notes.push(theme.fg("warning", "Raw debug data is not persisted for historical sessions."));
84
+ } else if (details.rawAccessDenied) {
85
+ notes.push(
86
+ theme.fg(
87
+ "warning",
88
+ "Raw debug data was requested but is not enabled in SuPi Debug settings.",
89
+ ),
90
+ );
91
+ }
92
+ return notes;
61
93
  }
62
94
 
63
- function renderExpandedReport(details: DebugReportDetails, theme: Theme): string {
95
+ function buildEventLines(details: DebugRenderDetails, theme: Theme): string[] {
64
96
  const lines: string[] = [];
65
- for (const event of details.events ?? []) {
97
+ for (const event of details.events) {
66
98
  if (lines.length > 0) lines.push("");
67
99
  pushEventLines(lines, event, theme);
68
100
  }
69
-
70
- if (details.rawAccessDenied) {
71
- lines.push("");
101
+ if (lines.length === 0) {
72
102
  lines.push(
73
103
  theme.fg(
74
- "warning",
75
- "Raw debug data was requested but is not enabled in SuPi Debug settings.",
104
+ "muted",
105
+ details.emptyReason === "no-persisted-events"
106
+ ? "This session has no persisted debug events."
107
+ : "No matching debug events available.",
76
108
  ),
77
109
  );
78
110
  }
111
+ return lines;
112
+ }
79
113
 
80
- return lines.join("\n");
114
+ export function renderExpandedReport(details: DebugRenderDetails, theme: Theme): string {
115
+ const notes = buildDetailNotes(details, theme);
116
+ const noteText = notes.join("\n");
117
+ const noteBytes = new TextEncoder().encode(noteText).byteLength;
118
+ const marker = "[Transcript view truncated: more output omitted.]";
119
+ const markerBytes = new TextEncoder().encode(marker).byteLength;
120
+ const result = truncateHead(buildEventLines(details, theme).join("\n"), {
121
+ maxLines: Math.max(1, MAX_RENDER_LINES - notes.length - 1),
122
+ maxBytes: Math.max(1, MAX_RENDER_BYTES - noteBytes - markerBytes - 2),
123
+ });
124
+ const bodyLines = result.content ? result.content.split("\n") : [];
125
+ if (result.truncated) bodyLines.push(marker);
126
+ return [...bodyLines, ...notes].join("\n");
81
127
  }
82
128
 
83
- /** Register the TUI message renderer for supi-debug-report custom messages. */
84
- export function registerDebugMessageRenderer(pi: ExtensionAPI): void {
85
- pi.registerMessageRenderer(DEBUG_REPORT_TYPE, (message, options, theme) => {
86
- const { expanded } = options;
87
- const details = (message.details ?? {}) as DebugReportDetails;
88
- const events = details.events ?? [];
129
+ function renderEmptyMessage(content: unknown, details: DebugRenderDetails, theme: Theme): Text {
130
+ const text =
131
+ typeof content === "string"
132
+ ? content
133
+ : details.emptyReason === "no-persisted-events"
134
+ ? "This session has no persisted debug events."
135
+ : "No debug events.";
136
+ return new Text(theme.fg("muted", text), 0, 0);
137
+ }
89
138
 
90
- if (events.length === 0) {
91
- const text = typeof message.content === "string" ? message.content : "No debug events.";
92
- return new Text(theme.fg("muted", text), 0, 0);
93
- }
139
+ function renderCollapsedMessage(details: DebugRenderDetails, theme: Theme): Text {
140
+ const first = details.events[0];
141
+ let summary = `${details.eventCount} event${details.eventCount === 1 ? "" : "s"} — ${first?.source ?? "debug"}/${first?.category ?? "event"}`;
142
+ if (details.eventCount > 1) summary += ` +${details.eventCount - 1} more`;
143
+ if (details.omittedEventCount > 0) summary += ` · ${details.omittedEventCount} omitted`;
144
+ if (details.eventDataTruncated) summary += " · data bounded";
145
+ if (details.truncation?.truncated) summary += " · output truncated";
146
+ if (details.rawDataUnavailable || details.rawAccessDenied) summary += " · raw unavailable";
147
+ return new Text(theme.fg("muted", summary), 0, 0);
148
+ }
94
149
 
95
- if (!expanded) {
96
- const first = events[0];
97
- const more = events.length > 1 ? ` +${events.length - 1} more` : "";
98
- const summary = `${events.length} event${events.length === 1 ? "" : "s"} — ${first.source}/${first.category}${more}`;
99
- return new Text(theme.fg("muted", summary), 0, 0);
100
- }
150
+ function renderDebugMessage(
151
+ content: unknown,
152
+ details: DebugRenderDetails,
153
+ expanded: boolean,
154
+ theme: Theme,
155
+ ): Text {
156
+ if (details.eventCount === 0) return renderEmptyMessage(content, details, theme);
157
+ if (!expanded) return renderCollapsedMessage(details, theme);
158
+ return new Text(renderExpandedReport(details, theme), 0, 0);
159
+ }
101
160
 
102
- return new Text(renderExpandedReport(details, theme), 0, 0);
103
- });
161
+ /** Register the TUI message renderer for supi-debug-report custom messages. */
162
+ export function registerDebugMessageRenderer(pi: ExtensionAPI): void {
163
+ pi.registerMessageRenderer(DEBUG_REPORT_TYPE, (message, options, theme) =>
164
+ renderDebugMessage(
165
+ message.content,
166
+ readDebugRenderDetails(message.details),
167
+ options.expanded,
168
+ theme,
169
+ ),
170
+ );
171
+ }
172
+
173
+ /** Create bounded details for a debug report message. */
174
+ export function createDebugMessageDetails(
175
+ events: readonly DebugEventView[],
176
+ options: Parameters<typeof createDebugRenderDetails>[1] = {},
177
+ ): DebugRenderDetails {
178
+ return createDebugRenderDetails(events, options);
104
179
  }