@mrclrchtr/supi-debug 0.1.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 +57 -0
- package/node_modules/@mrclrchtr/supi-core/README.md +90 -0
- package/node_modules/@mrclrchtr/supi-core/package.json +30 -0
- package/node_modules/@mrclrchtr/supi-core/src/config-settings.ts +76 -0
- package/node_modules/@mrclrchtr/supi-core/src/config.ts +186 -0
- package/node_modules/@mrclrchtr/supi-core/src/context-messages.ts +119 -0
- package/node_modules/@mrclrchtr/supi-core/src/context-provider-registry.ts +36 -0
- package/node_modules/@mrclrchtr/supi-core/src/context-tag.ts +31 -0
- package/node_modules/@mrclrchtr/supi-core/src/debug-registry.ts +255 -0
- package/node_modules/@mrclrchtr/supi-core/src/index.ts +83 -0
- package/node_modules/@mrclrchtr/supi-core/src/project-roots.ts +170 -0
- package/node_modules/@mrclrchtr/supi-core/src/registry-utils.ts +54 -0
- package/node_modules/@mrclrchtr/supi-core/src/session-utils.ts +29 -0
- package/node_modules/@mrclrchtr/supi-core/src/settings-command.ts +15 -0
- package/node_modules/@mrclrchtr/supi-core/src/settings-registry.ts +41 -0
- package/node_modules/@mrclrchtr/supi-core/src/settings-ui.ts +226 -0
- package/node_modules/@mrclrchtr/supi-core/src/terminal.ts +60 -0
- package/package.json +43 -0
- package/src/debug.ts +351 -0
- package/src/format.ts +89 -0
- package/src/index.ts +1 -0
- package/src/renderer.ts +104 -0
|
@@ -0,0 +1,226 @@
|
|
|
1
|
+
// Generic settings overlay for SuPi extensions.
|
|
2
|
+
//
|
|
3
|
+
// Uses pi-tui's SettingsList with scope toggle (Tab), extension grouping,
|
|
4
|
+
// and search. Each extension declares its settings via registerSettings().
|
|
5
|
+
|
|
6
|
+
import type { ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
7
|
+
import { getSettingsListTheme } from "@earendil-works/pi-coding-agent";
|
|
8
|
+
import {
|
|
9
|
+
Container,
|
|
10
|
+
Input,
|
|
11
|
+
Key,
|
|
12
|
+
matchesKey,
|
|
13
|
+
type SettingItem,
|
|
14
|
+
SettingsList,
|
|
15
|
+
Text,
|
|
16
|
+
} from "@earendil-works/pi-tui";
|
|
17
|
+
import {
|
|
18
|
+
getRegisteredSettings,
|
|
19
|
+
type SettingsScope,
|
|
20
|
+
type SettingsSection,
|
|
21
|
+
} from "./settings-registry.ts";
|
|
22
|
+
|
|
23
|
+
// ── Input submenu component ──────────────────────────────────
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Creates a pi-tui Input-backed submenu component with enter-to-confirm
|
|
27
|
+
* and escape-to-cancel handling.
|
|
28
|
+
*
|
|
29
|
+
* @param currentValue - Initial value for the text input.
|
|
30
|
+
* @param label - Label text displayed above the input.
|
|
31
|
+
* @param done - Callback invoked with the confirmed value, or undefined on cancel.
|
|
32
|
+
*/
|
|
33
|
+
export function createInputSubmenu(
|
|
34
|
+
currentValue: string,
|
|
35
|
+
label: string,
|
|
36
|
+
done: (selectedValue?: string) => void,
|
|
37
|
+
): {
|
|
38
|
+
render: (width: number) => string[];
|
|
39
|
+
invalidate: () => void;
|
|
40
|
+
handleInput: (data: string) => boolean;
|
|
41
|
+
} {
|
|
42
|
+
const input = new Input();
|
|
43
|
+
input.setValue(currentValue);
|
|
44
|
+
|
|
45
|
+
return {
|
|
46
|
+
render: (_width: number) => {
|
|
47
|
+
const lines = [` ${label}`];
|
|
48
|
+
lines.push(...input.render(_width));
|
|
49
|
+
lines.push(" enter confirm • esc cancel");
|
|
50
|
+
return lines;
|
|
51
|
+
},
|
|
52
|
+
invalidate: () => {
|
|
53
|
+
input.invalidate();
|
|
54
|
+
},
|
|
55
|
+
handleInput: (data: string) => {
|
|
56
|
+
if (matchesKey(data, Key.escape)) {
|
|
57
|
+
done();
|
|
58
|
+
return true;
|
|
59
|
+
}
|
|
60
|
+
if (matchesKey(data, Key.enter)) {
|
|
61
|
+
done(input.getValue());
|
|
62
|
+
return true;
|
|
63
|
+
}
|
|
64
|
+
input.handleInput(data);
|
|
65
|
+
return true;
|
|
66
|
+
},
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// ── Types ────────────────────────────────────────────────────
|
|
71
|
+
|
|
72
|
+
interface OverlayState {
|
|
73
|
+
scope: SettingsScope;
|
|
74
|
+
cwd: string;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// ── Pure helpers ─────────────────────────────────────────────
|
|
78
|
+
|
|
79
|
+
function getScopeLabel(scope: SettingsScope): string {
|
|
80
|
+
return scope === "project" ? "Project" : "Global";
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function buildFlatItems(
|
|
84
|
+
sections: SettingsSection[],
|
|
85
|
+
scope: SettingsScope,
|
|
86
|
+
cwd: string,
|
|
87
|
+
): SettingItem[] {
|
|
88
|
+
const items: SettingItem[] = [];
|
|
89
|
+
for (const section of sections) {
|
|
90
|
+
const sectionItems = section.loadValues(scope, cwd);
|
|
91
|
+
for (const item of sectionItems) {
|
|
92
|
+
items.push({
|
|
93
|
+
...item,
|
|
94
|
+
id: `${section.id}.${item.id}`,
|
|
95
|
+
label: `${section.label}: ${item.label}`,
|
|
96
|
+
});
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
return items;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function findSectionAndId(
|
|
103
|
+
sections: SettingsSection[],
|
|
104
|
+
flatId: string,
|
|
105
|
+
): { section: SettingsSection; itemId: string } | null {
|
|
106
|
+
const dotIndex = flatId.indexOf(".");
|
|
107
|
+
if (dotIndex === -1) return null;
|
|
108
|
+
const sectionId = flatId.slice(0, dotIndex);
|
|
109
|
+
const itemId = flatId.slice(dotIndex + 1);
|
|
110
|
+
const section = sections.find((s) => s.id === sectionId);
|
|
111
|
+
if (!section) return null;
|
|
112
|
+
return { section, itemId };
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// ── Component ────────────────────────────────────────────────
|
|
116
|
+
|
|
117
|
+
interface SettingsOverlayDeps {
|
|
118
|
+
state: OverlayState;
|
|
119
|
+
container: Container;
|
|
120
|
+
settingsList: SettingsList | null;
|
|
121
|
+
tui: Parameters<Parameters<ExtensionContext["ui"]["custom"]>[0]>[0];
|
|
122
|
+
theme: Parameters<Parameters<ExtensionContext["ui"]["custom"]>[0]>[1];
|
|
123
|
+
done: () => void;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
function createSettingsList(deps: SettingsOverlayDeps): SettingsList {
|
|
127
|
+
const sections = getRegisteredSettings();
|
|
128
|
+
const items = buildFlatItems(sections, deps.state.scope, deps.state.cwd);
|
|
129
|
+
const onChange = (flatId: string, newValue: string) => {
|
|
130
|
+
const found = findSectionAndId(sections, flatId);
|
|
131
|
+
if (found) {
|
|
132
|
+
found.section.persistChange(deps.state.scope, deps.state.cwd, found.itemId, newValue);
|
|
133
|
+
}
|
|
134
|
+
// Re-read all values to reflect persisted changes, but keep the list
|
|
135
|
+
// instance (and its selectedIndex) intact.
|
|
136
|
+
const updatedItems = buildFlatItems(sections, deps.state.scope, deps.state.cwd);
|
|
137
|
+
for (const updated of updatedItems) {
|
|
138
|
+
const existing = items.find((i) => i.id === updated.id);
|
|
139
|
+
if (existing && existing.currentValue !== updated.currentValue) {
|
|
140
|
+
settingsList.updateValue(updated.id, updated.currentValue);
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
deps.tui.requestRender();
|
|
144
|
+
};
|
|
145
|
+
const settingsList = new SettingsList(
|
|
146
|
+
items,
|
|
147
|
+
Math.min(items.length + 4, 20),
|
|
148
|
+
getSettingsListTheme(),
|
|
149
|
+
onChange,
|
|
150
|
+
() => deps.done(),
|
|
151
|
+
{ enableSearch: true },
|
|
152
|
+
);
|
|
153
|
+
return settingsList;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
function rebuildSettingsList(deps: SettingsOverlayDeps): SettingsList {
|
|
157
|
+
const settingsList = createSettingsList(deps);
|
|
158
|
+
deps.settingsList = settingsList;
|
|
159
|
+
|
|
160
|
+
deps.container.clear();
|
|
161
|
+
deps.container.addChild(createHeaderComponent(deps));
|
|
162
|
+
deps.container.addChild(settingsList);
|
|
163
|
+
|
|
164
|
+
return settingsList;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
function createHeaderComponent(deps: SettingsOverlayDeps): Text {
|
|
168
|
+
const { theme, state } = deps;
|
|
169
|
+
const scopeLabel = getScopeLabel(state.scope);
|
|
170
|
+
const otherScope = state.scope === "project" ? "Global" : "Project";
|
|
171
|
+
const headerText = new Text(
|
|
172
|
+
`${theme.fg("accent", theme.bold("SuPi Settings"))} ${theme.fg("text", `Scope: ${scopeLabel}`)} ${theme.fg("dim", `(tab → ${otherScope})`)}`,
|
|
173
|
+
0,
|
|
174
|
+
0,
|
|
175
|
+
);
|
|
176
|
+
return headerText;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
function handleScopeToggle(deps: SettingsOverlayDeps): void {
|
|
180
|
+
deps.state.scope = deps.state.scope === "project" ? "global" : "project";
|
|
181
|
+
rebuildSettingsList(deps);
|
|
182
|
+
deps.tui.requestRender();
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
// ── Entry point ──────────────────────────────────────────────
|
|
186
|
+
|
|
187
|
+
export function openSettingsOverlay(ctx: ExtensionContext): void {
|
|
188
|
+
const sections = getRegisteredSettings();
|
|
189
|
+
if (sections.length === 0) {
|
|
190
|
+
ctx.ui.notify("No settings registered by SuPi extensions", "info");
|
|
191
|
+
return;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
void ctx.ui.custom<void>((tui, theme, _kb, done) => {
|
|
195
|
+
const state: OverlayState = { scope: "project", cwd: ctx.cwd };
|
|
196
|
+
const container = new Container();
|
|
197
|
+
|
|
198
|
+
const deps: SettingsOverlayDeps = {
|
|
199
|
+
state,
|
|
200
|
+
container,
|
|
201
|
+
settingsList: null,
|
|
202
|
+
tui,
|
|
203
|
+
theme,
|
|
204
|
+
done,
|
|
205
|
+
};
|
|
206
|
+
|
|
207
|
+
rebuildSettingsList(deps);
|
|
208
|
+
|
|
209
|
+
const component = {
|
|
210
|
+
render: (width: number) => container.render(width),
|
|
211
|
+
invalidate: () => container.invalidate(),
|
|
212
|
+
handleInput: (data: string) => {
|
|
213
|
+
if (matchesKey(data, Key.tab)) {
|
|
214
|
+
handleScopeToggle(deps);
|
|
215
|
+
return true;
|
|
216
|
+
}
|
|
217
|
+
// Delegate input to the settings list (always set after rebuildSettingsList)
|
|
218
|
+
deps.settingsList?.handleInput?.(data);
|
|
219
|
+
deps.tui.requestRender();
|
|
220
|
+
return true;
|
|
221
|
+
},
|
|
222
|
+
};
|
|
223
|
+
|
|
224
|
+
return component;
|
|
225
|
+
});
|
|
226
|
+
}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared terminal title formatting and signaling utilities.
|
|
3
|
+
*
|
|
4
|
+
* Centralized place for pi title convention (π prefix), completion (✓)
|
|
5
|
+
* and waiting (●) indicators, and the audible terminal bell.
|
|
6
|
+
*/
|
|
7
|
+
import path from "node:path";
|
|
8
|
+
|
|
9
|
+
/** Unicode checkmark shown when the agent finishes a turn. */
|
|
10
|
+
export const DONE_SYMBOL = "\u2713";
|
|
11
|
+
/** Unicode dot shown when waiting for user input. */
|
|
12
|
+
export const WAITING_SYMBOL = "\u25CF";
|
|
13
|
+
|
|
14
|
+
/** Minimal UI surface needed for title operations. */
|
|
15
|
+
export interface TitleTarget {
|
|
16
|
+
ui: {
|
|
17
|
+
setTitle?(title: string): void;
|
|
18
|
+
};
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Format pi's canonical terminal title from session name and cwd.
|
|
23
|
+
* Falls back gracefully when either is missing.
|
|
24
|
+
*
|
|
25
|
+
* @example
|
|
26
|
+
* formatTitle("my-session", "/home/projects/foo") // "π - my-session - foo"
|
|
27
|
+
* formatTitle(undefined, "/home/projects/foo") // "π - foo"
|
|
28
|
+
* formatTitle("my-session") // "π - my-session"
|
|
29
|
+
* formatTitle() // "π"
|
|
30
|
+
*/
|
|
31
|
+
export function formatTitle(sessionName?: string, cwd?: string): string {
|
|
32
|
+
const base = cwd ? path.basename(cwd) : undefined;
|
|
33
|
+
if (sessionName && base) return `π - ${sessionName} - ${base}`;
|
|
34
|
+
if (sessionName) return `π - ${sessionName}`;
|
|
35
|
+
if (base) return `π - ${base}`;
|
|
36
|
+
return "π";
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/** Sound the audible terminal bell (ASCII BEL). */
|
|
40
|
+
export function signalBell(): void {
|
|
41
|
+
process.stdout.write("\x07");
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Set the terminal title to indicate the agent is waiting for user input.
|
|
46
|
+
* Prefixes with ● and sounds the terminal bell.
|
|
47
|
+
*/
|
|
48
|
+
export function signalWaiting(ctx: TitleTarget, title: string): void {
|
|
49
|
+
ctx.ui.setTitle?.(`${WAITING_SYMBOL} ${title}`);
|
|
50
|
+
signalBell();
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Set the terminal title to indicate the agent turn has completed.
|
|
55
|
+
* Prefixes with ✓ and sounds the terminal bell.
|
|
56
|
+
*/
|
|
57
|
+
export function signalDone(ctx: TitleTarget, title: string): void {
|
|
58
|
+
ctx.ui.setTitle?.(`${DONE_SYMBOL} ${title}`);
|
|
59
|
+
signalBell();
|
|
60
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@mrclrchtr/supi-debug",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "SuPi Debug extension — shared debug event inspection for SuPi extensions",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"repository": {
|
|
7
|
+
"type": "git",
|
|
8
|
+
"url": "git+https://github.com/mrclrchtr/supi.git"
|
|
9
|
+
},
|
|
10
|
+
"publishConfig": {
|
|
11
|
+
"access": "public"
|
|
12
|
+
},
|
|
13
|
+
"keywords": [
|
|
14
|
+
"pi-package",
|
|
15
|
+
"pi",
|
|
16
|
+
"pi-coding-agent"
|
|
17
|
+
],
|
|
18
|
+
"files": [
|
|
19
|
+
"src/**/*.ts",
|
|
20
|
+
"README.md"
|
|
21
|
+
],
|
|
22
|
+
"dependencies": {
|
|
23
|
+
"@mrclrchtr/supi-core": "workspace:*"
|
|
24
|
+
},
|
|
25
|
+
"bundledDependencies": [
|
|
26
|
+
"@mrclrchtr/supi-core"
|
|
27
|
+
],
|
|
28
|
+
"peerDependencies": {
|
|
29
|
+
"@earendil-works/pi-coding-agent": "*",
|
|
30
|
+
"@earendil-works/pi-tui": "*",
|
|
31
|
+
"typebox": "*"
|
|
32
|
+
},
|
|
33
|
+
"devDependencies": {
|
|
34
|
+
"vitest": "^4.1.5",
|
|
35
|
+
"@mrclrchtr/supi-test-utils": "workspace:*"
|
|
36
|
+
},
|
|
37
|
+
"pi": {
|
|
38
|
+
"extensions": [
|
|
39
|
+
"./src/debug.ts"
|
|
40
|
+
]
|
|
41
|
+
},
|
|
42
|
+
"main": "src/index.ts"
|
|
43
|
+
}
|
package/src/debug.ts
ADDED
|
@@ -0,0 +1,351 @@
|
|
|
1
|
+
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
2
|
+
import {
|
|
3
|
+
clearDebugEvents,
|
|
4
|
+
configureDebugRegistry,
|
|
5
|
+
DEBUG_REGISTRY_DEFAULTS,
|
|
6
|
+
type DebugAgentAccess,
|
|
7
|
+
type DebugEventQuery,
|
|
8
|
+
type DebugEventView,
|
|
9
|
+
type DebugLevel,
|
|
10
|
+
type DebugNotifyLevel,
|
|
11
|
+
getDebugEvents,
|
|
12
|
+
getDebugSummary,
|
|
13
|
+
loadSupiConfig,
|
|
14
|
+
registerConfigSettings,
|
|
15
|
+
registerContextProvider,
|
|
16
|
+
} from "@mrclrchtr/supi-core";
|
|
17
|
+
import { Type } from "typebox";
|
|
18
|
+
import { maybeLogLoadStatus } from "../status-log.ts";
|
|
19
|
+
import { formatDataLines } from "./format.ts";
|
|
20
|
+
import { registerDebugMessageRenderer } from "./renderer.ts";
|
|
21
|
+
|
|
22
|
+
const DEBUG_SECTION = "debug";
|
|
23
|
+
const DEBUG_REPORT_TYPE = "supi-debug-report";
|
|
24
|
+
|
|
25
|
+
interface DebugConfig {
|
|
26
|
+
enabled: boolean;
|
|
27
|
+
agentAccess: DebugAgentAccess;
|
|
28
|
+
maxEvents: number;
|
|
29
|
+
notifyLevel: DebugNotifyLevel;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const DEBUG_DEFAULTS: DebugConfig = { ...DEBUG_REGISTRY_DEFAULTS };
|
|
33
|
+
|
|
34
|
+
type DebugToolParams = DebugEventQuery;
|
|
35
|
+
|
|
36
|
+
function normalizeAgentAccess(value: string): DebugAgentAccess {
|
|
37
|
+
return value === "off" || value === "raw" ? value : "sanitized";
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function normalizeNotifyLevel(value: string): DebugNotifyLevel {
|
|
41
|
+
return value === "warning" || value === "error" ? value : "off";
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function normalizeMaxEvents(value: string | number): number {
|
|
45
|
+
const parsed = typeof value === "number" ? value : Number.parseInt(value, 10);
|
|
46
|
+
return Number.isFinite(parsed) && parsed > 0 ? Math.floor(parsed) : DEBUG_DEFAULTS.maxEvents;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function normalizeEnabled(value: unknown): boolean {
|
|
50
|
+
if (typeof value === "boolean") {
|
|
51
|
+
return value;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
if (typeof value === "string") {
|
|
55
|
+
const normalized = value.trim().toLowerCase();
|
|
56
|
+
if (
|
|
57
|
+
normalized === "true" ||
|
|
58
|
+
normalized === "on" ||
|
|
59
|
+
normalized === "1" ||
|
|
60
|
+
normalized === "yes"
|
|
61
|
+
) {
|
|
62
|
+
return true;
|
|
63
|
+
}
|
|
64
|
+
if (
|
|
65
|
+
normalized === "false" ||
|
|
66
|
+
normalized === "off" ||
|
|
67
|
+
normalized === "0" ||
|
|
68
|
+
normalized === "no" ||
|
|
69
|
+
normalized === ""
|
|
70
|
+
) {
|
|
71
|
+
return false;
|
|
72
|
+
}
|
|
73
|
+
return DEBUG_DEFAULTS.enabled;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
if (value === 1) return true;
|
|
77
|
+
if (value === 0) return false;
|
|
78
|
+
return DEBUG_DEFAULTS.enabled;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function loadDebugConfig(cwd: string): DebugConfig {
|
|
82
|
+
const config = loadSupiConfig(DEBUG_SECTION, cwd, DEBUG_DEFAULTS);
|
|
83
|
+
return {
|
|
84
|
+
enabled: normalizeEnabled(config.enabled),
|
|
85
|
+
agentAccess: normalizeAgentAccess(String(config.agentAccess)),
|
|
86
|
+
maxEvents: normalizeMaxEvents(config.maxEvents),
|
|
87
|
+
notifyLevel: normalizeNotifyLevel(String(config.notifyLevel)),
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function applyDebugConfig(cwd: string): DebugConfig {
|
|
92
|
+
const config = loadDebugConfig(cwd);
|
|
93
|
+
configureDebugRegistry(config);
|
|
94
|
+
return config;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function syncLiveDebugRegistry(cwd: string): DebugConfig {
|
|
98
|
+
const config = applyDebugConfig(cwd);
|
|
99
|
+
if (!config.enabled) {
|
|
100
|
+
clearDebugEvents();
|
|
101
|
+
}
|
|
102
|
+
return config;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
function registerDebugSettings(): void {
|
|
106
|
+
registerConfigSettings({
|
|
107
|
+
id: "debug",
|
|
108
|
+
label: "Debug",
|
|
109
|
+
section: DEBUG_SECTION,
|
|
110
|
+
defaults: DEBUG_DEFAULTS,
|
|
111
|
+
buildItems: (settings) => [
|
|
112
|
+
{
|
|
113
|
+
id: "enabled",
|
|
114
|
+
label: "Enabled",
|
|
115
|
+
description: "Enable/disable session-local SuPi debug event capture",
|
|
116
|
+
currentValue: settings.enabled ? "on" : "off",
|
|
117
|
+
values: ["on", "off"],
|
|
118
|
+
},
|
|
119
|
+
{
|
|
120
|
+
id: "agentAccess",
|
|
121
|
+
label: "Agent Access",
|
|
122
|
+
description: "Control whether the agent can fetch sanitized or raw debug events",
|
|
123
|
+
currentValue: normalizeAgentAccess(String(settings.agentAccess)),
|
|
124
|
+
values: ["off", "sanitized", "raw"],
|
|
125
|
+
},
|
|
126
|
+
{
|
|
127
|
+
id: "maxEvents",
|
|
128
|
+
label: "Max Events",
|
|
129
|
+
description: "Maximum session-local debug events retained in memory",
|
|
130
|
+
currentValue: String(normalizeMaxEvents(settings.maxEvents)),
|
|
131
|
+
values: ["50", "100", "250", "500"],
|
|
132
|
+
},
|
|
133
|
+
{
|
|
134
|
+
id: "notifyLevel",
|
|
135
|
+
label: "Notify Level",
|
|
136
|
+
description: "Minimum debug event severity that may notify the user",
|
|
137
|
+
currentValue: normalizeNotifyLevel(String(settings.notifyLevel)),
|
|
138
|
+
values: ["off", "warning", "error"],
|
|
139
|
+
},
|
|
140
|
+
],
|
|
141
|
+
// biome-ignore lint/complexity/useMaxParams: ConfigSettingsOptions interface callback
|
|
142
|
+
persistChange: (_scope, cwd, settingId, value, helpers) => {
|
|
143
|
+
if (settingId === "enabled") {
|
|
144
|
+
helpers.set("enabled", value === "on");
|
|
145
|
+
} else if (settingId === "agentAccess") {
|
|
146
|
+
helpers.set("agentAccess", normalizeAgentAccess(value));
|
|
147
|
+
} else if (settingId === "maxEvents") {
|
|
148
|
+
helpers.set("maxEvents", normalizeMaxEvents(value));
|
|
149
|
+
} else if (settingId === "notifyLevel") {
|
|
150
|
+
helpers.set("notifyLevel", normalizeNotifyLevel(value));
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
syncLiveDebugRegistry(cwd);
|
|
154
|
+
},
|
|
155
|
+
});
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
function parseCommandArgs(args: string): DebugEventQuery {
|
|
159
|
+
const query: DebugEventQuery = {};
|
|
160
|
+
const parts = args.trim().split(/\s+/).filter(Boolean);
|
|
161
|
+
for (const part of parts) {
|
|
162
|
+
const [key, value] = part.split("=", 2);
|
|
163
|
+
if (!value) continue;
|
|
164
|
+
if (key === "source") query.source = value;
|
|
165
|
+
if (key === "category") query.category = value;
|
|
166
|
+
if (key === "level" && isDebugLevel(value)) query.level = value;
|
|
167
|
+
if (key === "limit") query.limit = normalizeMaxEvents(value);
|
|
168
|
+
}
|
|
169
|
+
return query;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
function isDebugLevel(value: string): value is DebugLevel {
|
|
173
|
+
return value === "debug" || value === "info" || value === "warning" || value === "error";
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
function pushFormattedData(lines: string[], label: string, value: unknown): void {
|
|
177
|
+
const dataLines = formatDataLines(value);
|
|
178
|
+
if (dataLines.length === 0) return;
|
|
179
|
+
if (dataLines.length === 1) {
|
|
180
|
+
lines.push(` ${label}: ${dataLines[0]}`);
|
|
181
|
+
} else {
|
|
182
|
+
lines.push(` ${label}:`);
|
|
183
|
+
for (const dl of dataLines) {
|
|
184
|
+
lines.push(` ${dl}`);
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
function formatEvents(events: DebugEventView[], rawAccessDenied: boolean): string[] {
|
|
190
|
+
if (events.length === 0) {
|
|
191
|
+
return ["No matching debug events available."];
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
const lines: string[] = [];
|
|
195
|
+
for (const event of events) {
|
|
196
|
+
lines.push(
|
|
197
|
+
`[${new Date(event.timestamp).toISOString()}] ${event.level.toUpperCase()} ${event.source}/${event.category}: ${event.message}`,
|
|
198
|
+
);
|
|
199
|
+
if (event.cwd) lines.push(` cwd: ${event.cwd}`);
|
|
200
|
+
pushFormattedData(lines, "data", event.data);
|
|
201
|
+
pushFormattedData(lines, "rawData", event.rawData);
|
|
202
|
+
}
|
|
203
|
+
if (rawAccessDenied) {
|
|
204
|
+
lines.push("");
|
|
205
|
+
lines.push("Raw debug data was requested but is not enabled in SuPi Debug settings.");
|
|
206
|
+
}
|
|
207
|
+
return lines;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
function formatEventLines(query: DebugEventQuery): string[] {
|
|
211
|
+
const { events, rawAccessDenied } = getDebugEvents(query);
|
|
212
|
+
return formatEvents(events, rawAccessDenied);
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
function buildSummaryData(): Record<string, string | number> | null {
|
|
216
|
+
const summary = getDebugSummary();
|
|
217
|
+
if (!summary) return null;
|
|
218
|
+
|
|
219
|
+
const data: Record<string, string | number> = { total: summary.total };
|
|
220
|
+
for (const [level, count] of Object.entries(summary.byLevel)) {
|
|
221
|
+
if (count !== undefined) data[`level:${level}`] = count;
|
|
222
|
+
}
|
|
223
|
+
for (const [source, count] of Object.entries(summary.bySource)) {
|
|
224
|
+
data[`source:${source}`] = count;
|
|
225
|
+
}
|
|
226
|
+
return data;
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
function toolAccessAllowed(config: DebugConfig): boolean {
|
|
230
|
+
return config.enabled && config.agentAccess !== "off";
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
function buildToolResult(params: DebugToolParams, config: DebugConfig) {
|
|
234
|
+
if (!config.enabled) {
|
|
235
|
+
return {
|
|
236
|
+
content: [
|
|
237
|
+
{
|
|
238
|
+
type: "text" as const,
|
|
239
|
+
text: "SuPi debug event capture is disabled. Enable Debug in /supi-settings to retain events.",
|
|
240
|
+
},
|
|
241
|
+
],
|
|
242
|
+
isError: true,
|
|
243
|
+
details: { enabled: false },
|
|
244
|
+
};
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
if (!toolAccessAllowed(config)) {
|
|
248
|
+
return {
|
|
249
|
+
content: [{ type: "text" as const, text: "Agent access to SuPi debug events is disabled." }],
|
|
250
|
+
isError: true,
|
|
251
|
+
details: { enabled: true, agentAccess: config.agentAccess },
|
|
252
|
+
};
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
const query: DebugEventQuery = {
|
|
256
|
+
source: params.source,
|
|
257
|
+
level: params.level,
|
|
258
|
+
category: params.category,
|
|
259
|
+
limit: params.limit,
|
|
260
|
+
includeRaw: params.includeRaw,
|
|
261
|
+
allowRaw: config.agentAccess === "raw",
|
|
262
|
+
};
|
|
263
|
+
const result = getDebugEvents(query);
|
|
264
|
+
const lines = formatEventLines(query);
|
|
265
|
+
return {
|
|
266
|
+
content: [{ type: "text" as const, text: lines.join("\n") }],
|
|
267
|
+
details: {
|
|
268
|
+
enabled: true,
|
|
269
|
+
agentAccess: config.agentAccess,
|
|
270
|
+
rawAccessDenied: result.rawAccessDenied,
|
|
271
|
+
events: result.events,
|
|
272
|
+
},
|
|
273
|
+
};
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
/** Register the shared SuPi debug command, settings, context summary, and agent tool. */
|
|
277
|
+
export default function debugExtension(pi: ExtensionAPI) {
|
|
278
|
+
applyDebugConfig(process.cwd());
|
|
279
|
+
registerDebugSettings();
|
|
280
|
+
registerDebugMessageRenderer(pi);
|
|
281
|
+
|
|
282
|
+
registerContextProvider({
|
|
283
|
+
id: "debug",
|
|
284
|
+
label: "Debug",
|
|
285
|
+
getData: buildSummaryData,
|
|
286
|
+
});
|
|
287
|
+
|
|
288
|
+
pi.on("session_start", async (_event, ctx) => {
|
|
289
|
+
clearDebugEvents();
|
|
290
|
+
applyDebugConfig(ctx.cwd);
|
|
291
|
+
maybeLogLoadStatus(pi, ctx.cwd);
|
|
292
|
+
});
|
|
293
|
+
|
|
294
|
+
pi.registerCommand("supi-debug", {
|
|
295
|
+
description: "Show recent SuPi debug events",
|
|
296
|
+
handler: async (args, ctx) => {
|
|
297
|
+
const config = applyDebugConfig(ctx.cwd);
|
|
298
|
+
if (!config.enabled) {
|
|
299
|
+
pi.sendMessage({
|
|
300
|
+
customType: DEBUG_REPORT_TYPE,
|
|
301
|
+
content: "SuPi debug event capture is disabled. Enable Debug in /supi-settings.",
|
|
302
|
+
display: true,
|
|
303
|
+
});
|
|
304
|
+
return;
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
const query = parseCommandArgs(args);
|
|
308
|
+
const { events, rawAccessDenied } = getDebugEvents(query);
|
|
309
|
+
const lines = formatEvents(events, rawAccessDenied);
|
|
310
|
+
pi.sendMessage({
|
|
311
|
+
customType: DEBUG_REPORT_TYPE,
|
|
312
|
+
content: lines.join("\n"),
|
|
313
|
+
display: true,
|
|
314
|
+
details: { events, rawAccessDenied },
|
|
315
|
+
});
|
|
316
|
+
},
|
|
317
|
+
});
|
|
318
|
+
|
|
319
|
+
pi.registerTool({
|
|
320
|
+
name: "supi_debug",
|
|
321
|
+
label: "SuPi Debug",
|
|
322
|
+
description: "Fetch recent session-local SuPi extension debug events for troubleshooting.",
|
|
323
|
+
promptSnippet:
|
|
324
|
+
"Fetch recent SuPi extension debug events when troubleshooting extension behavior.",
|
|
325
|
+
promptGuidelines: [
|
|
326
|
+
"Use supi_debug when the user asks to inspect SuPi extension failures, fallback reasons, or recent debug events.",
|
|
327
|
+
"supi_debug returns sanitized events by default; request raw data only when the user explicitly wants raw diagnostics and settings allow it.",
|
|
328
|
+
],
|
|
329
|
+
parameters: Type.Object({
|
|
330
|
+
source: Type.Optional(Type.String({ description: "Filter by extension source, e.g. rtk" })),
|
|
331
|
+
level: Type.Optional(
|
|
332
|
+
Type.Union([
|
|
333
|
+
Type.Literal("debug"),
|
|
334
|
+
Type.Literal("info"),
|
|
335
|
+
Type.Literal("warning"),
|
|
336
|
+
Type.Literal("error"),
|
|
337
|
+
]),
|
|
338
|
+
),
|
|
339
|
+
category: Type.Optional(Type.String({ description: "Filter by event category" })),
|
|
340
|
+
limit: Type.Optional(Type.Number({ description: "Maximum number of events to return" })),
|
|
341
|
+
includeRaw: Type.Optional(
|
|
342
|
+
Type.Boolean({ description: "Request raw event data when settings permit it" }),
|
|
343
|
+
),
|
|
344
|
+
}),
|
|
345
|
+
// biome-ignore lint/complexity/useMaxParams: pi ToolDefinition.execute signature
|
|
346
|
+
async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
|
|
347
|
+
const config = applyDebugConfig(ctx.cwd);
|
|
348
|
+
return buildToolResult(params as DebugToolParams, config);
|
|
349
|
+
},
|
|
350
|
+
});
|
|
351
|
+
}
|