@mrclrchtr/supi-lsp 2.0.5 → 2.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/node_modules/@mrclrchtr/supi-code-runtime/node_modules/@mrclrchtr/supi-core/README.md +29 -21
- package/node_modules/@mrclrchtr/supi-code-runtime/node_modules/@mrclrchtr/supi-core/package.json +2 -1
- package/node_modules/@mrclrchtr/supi-code-runtime/node_modules/@mrclrchtr/supi-core/src/config/config-settings.ts +88 -69
- package/node_modules/@mrclrchtr/supi-code-runtime/node_modules/@mrclrchtr/supi-core/src/config/config.ts +22 -8
- package/node_modules/@mrclrchtr/supi-code-runtime/node_modules/@mrclrchtr/supi-core/src/config/prompt-surface.ts +357 -0
- package/node_modules/@mrclrchtr/supi-code-runtime/node_modules/@mrclrchtr/supi-core/src/prompt-surface.ts +4 -0
- package/node_modules/@mrclrchtr/supi-code-runtime/node_modules/@mrclrchtr/supi-core/src/settings/settings-command.ts +1 -1
- package/node_modules/@mrclrchtr/supi-code-runtime/node_modules/@mrclrchtr/supi-core/src/settings/settings-registry.ts +51 -29
- package/node_modules/@mrclrchtr/supi-code-runtime/node_modules/@mrclrchtr/supi-core/src/settings/settings-ui.ts +64 -18
- package/node_modules/@mrclrchtr/supi-code-runtime/node_modules/@mrclrchtr/supi-core/src/settings.ts +11 -5
- package/node_modules/@mrclrchtr/supi-code-runtime/node_modules/@mrclrchtr/supi-core/src/tool-framework.ts +10 -0
- package/node_modules/@mrclrchtr/supi-code-runtime/package.json +2 -2
- package/node_modules/@mrclrchtr/supi-core/README.md +29 -21
- package/node_modules/@mrclrchtr/supi-core/package.json +2 -1
- package/node_modules/@mrclrchtr/supi-core/src/config/config-settings.ts +88 -69
- package/node_modules/@mrclrchtr/supi-core/src/config/config.ts +22 -8
- package/node_modules/@mrclrchtr/supi-core/src/config/prompt-surface.ts +357 -0
- package/node_modules/@mrclrchtr/supi-core/src/prompt-surface.ts +4 -0
- package/node_modules/@mrclrchtr/supi-core/src/settings/settings-command.ts +1 -1
- package/node_modules/@mrclrchtr/supi-core/src/settings/settings-registry.ts +51 -29
- package/node_modules/@mrclrchtr/supi-core/src/settings/settings-ui.ts +64 -18
- package/node_modules/@mrclrchtr/supi-core/src/settings.ts +11 -5
- package/node_modules/@mrclrchtr/supi-core/src/tool-framework.ts +10 -0
- package/package.json +3 -3
|
@@ -0,0 +1,357 @@
|
|
|
1
|
+
// Configurable tool prompt-surface overrides.
|
|
2
|
+
//
|
|
3
|
+
// Resolution order: package defaults ← global SuPi config ← trusted project SuPi config.
|
|
4
|
+
// Project overrides are trust-gated; they require PI project trust and a PI-recognized
|
|
5
|
+
// trust-requiring resource (e.g. .pi/settings.json).
|
|
6
|
+
|
|
7
|
+
import {
|
|
8
|
+
type ExtensionContext,
|
|
9
|
+
hasTrustRequiringProjectResources,
|
|
10
|
+
} from "@earendil-works/pi-coding-agent";
|
|
11
|
+
import type { SuiPiToolPromptSurface } from "../tool-framework.ts";
|
|
12
|
+
import { loadSupiConfigSectionForScope, type SupiConfigOptions } from "./config.ts";
|
|
13
|
+
|
|
14
|
+
// ── Public types ───────────────────────────────────────────────────────────
|
|
15
|
+
|
|
16
|
+
export type ToolPromptSurfaceDiagnosticCode =
|
|
17
|
+
| "invalidPromptSurfaceConfig"
|
|
18
|
+
| "invalidPromptSurfaceField"
|
|
19
|
+
| "projectPromptSurfaceIgnored";
|
|
20
|
+
|
|
21
|
+
export interface ToolPromptSurfaceDiagnostic {
|
|
22
|
+
code: ToolPromptSurfaceDiagnosticCode;
|
|
23
|
+
scope: "global" | "project";
|
|
24
|
+
section: string;
|
|
25
|
+
toolName: string;
|
|
26
|
+
message: string;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export interface ResolveToolPromptSurfaceOptions extends SupiConfigOptions {
|
|
30
|
+
section: string;
|
|
31
|
+
toolName: string;
|
|
32
|
+
defaults: SuiPiToolPromptSurface;
|
|
33
|
+
ctx: Pick<ExtensionContext, "cwd" | "isProjectTrusted">;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export interface ResolveToolPromptSurfaceResult {
|
|
37
|
+
surface: SuiPiToolPromptSurface;
|
|
38
|
+
diagnostics: ToolPromptSurfaceDiagnostic[];
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// ── Private types ──────────────────────────────────────────────────────────
|
|
42
|
+
|
|
43
|
+
type PromptSurfaceField = keyof SuiPiToolPromptSurface;
|
|
44
|
+
type PromptSurfaceScope = ToolPromptSurfaceDiagnostic["scope"];
|
|
45
|
+
|
|
46
|
+
const PROMPT_SURFACE_FIELDS = new Set<string>(["description", "promptSnippet", "promptGuidelines"]);
|
|
47
|
+
|
|
48
|
+
const PROMPT_SURFACE_DIAGNOSTICS_KEY = Symbol.for(
|
|
49
|
+
"@mrclrchtr/supi-core/tool-prompt-surface/notified-diagnostics",
|
|
50
|
+
);
|
|
51
|
+
|
|
52
|
+
// ── Resolution ─────────────────────────────────────────────────────────────
|
|
53
|
+
|
|
54
|
+
/** Resolve a tool's model-facing prompt surface from defaults + SuPi config overrides. */
|
|
55
|
+
export function resolveToolPromptSurface(
|
|
56
|
+
options: ResolveToolPromptSurfaceOptions,
|
|
57
|
+
): ResolveToolPromptSurfaceResult {
|
|
58
|
+
const diagnostics: ToolPromptSurfaceDiagnostic[] = [];
|
|
59
|
+
let surface = clonePromptSurface(options.defaults);
|
|
60
|
+
|
|
61
|
+
const globalSection = loadSupiConfigSectionForScope(options.section, options.ctx.cwd, {
|
|
62
|
+
scope: "global",
|
|
63
|
+
homeDir: options.homeDir,
|
|
64
|
+
});
|
|
65
|
+
surface = applyPromptSurfaceScope(surface, options, "global", globalSection, diagnostics);
|
|
66
|
+
|
|
67
|
+
const projectSection = loadSupiConfigSectionForScope(options.section, options.ctx.cwd, {
|
|
68
|
+
scope: "project",
|
|
69
|
+
homeDir: options.homeDir,
|
|
70
|
+
});
|
|
71
|
+
const projectPromptSurface = getPromptSurfaceConfig(projectSection, options.toolName, {
|
|
72
|
+
diagnostics,
|
|
73
|
+
options,
|
|
74
|
+
scope: "project",
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
if (projectPromptSurface) {
|
|
78
|
+
const hasTrustMarker = hasTrustRequiringProjectResources(options.ctx.cwd);
|
|
79
|
+
const projectTrusted = options.ctx.isProjectTrusted();
|
|
80
|
+
if (hasTrustMarker && projectTrusted) {
|
|
81
|
+
surface = applyPromptSurfaceConfig(
|
|
82
|
+
surface,
|
|
83
|
+
options.defaults,
|
|
84
|
+
projectPromptSurface,
|
|
85
|
+
options,
|
|
86
|
+
"project",
|
|
87
|
+
diagnostics,
|
|
88
|
+
);
|
|
89
|
+
} else {
|
|
90
|
+
diagnostics.push({
|
|
91
|
+
code: "projectPromptSurfaceIgnored",
|
|
92
|
+
scope: "project",
|
|
93
|
+
section: options.section,
|
|
94
|
+
toolName: options.toolName,
|
|
95
|
+
message: hasTrustMarker
|
|
96
|
+
? `Project prompt-surface overrides for ${options.toolName} were ignored because the project is not trusted in PI.`
|
|
97
|
+
: `Project prompt-surface overrides for ${options.toolName} were ignored because ${options.ctx.cwd}/.pi/supi/config.json is not PI trust-gated. Add .pi/settings.json and trust the project to enable them.`,
|
|
98
|
+
});
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
return { surface, diagnostics };
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/** Notify prompt-surface diagnostics once per session/tool/diagnostic code. */
|
|
106
|
+
export function notifyToolPromptSurfaceDiagnostics(
|
|
107
|
+
ctx: Pick<ExtensionContext, "sessionManager" | "ui">,
|
|
108
|
+
diagnostics: readonly ToolPromptSurfaceDiagnostic[],
|
|
109
|
+
): void {
|
|
110
|
+
const globalRecord = globalThis as Record<symbol, Set<string> | undefined>;
|
|
111
|
+
const notified = globalRecord[PROMPT_SURFACE_DIAGNOSTICS_KEY] ?? new Set<string>();
|
|
112
|
+
globalRecord[PROMPT_SURFACE_DIAGNOSTICS_KEY] = notified;
|
|
113
|
+
const sessionId = ctx.sessionManager.getSessionId();
|
|
114
|
+
|
|
115
|
+
for (const diagnostic of diagnostics) {
|
|
116
|
+
const key = `${sessionId}:${diagnostic.section}:${diagnostic.toolName}:${diagnostic.code}`;
|
|
117
|
+
if (notified.has(key)) continue;
|
|
118
|
+
notified.add(key);
|
|
119
|
+
ctx.ui.notify(diagnostic.message, "warning");
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
// ── Scope helpers ──────────────────────────────────────────────────────────
|
|
124
|
+
|
|
125
|
+
// biome-ignore lint/complexity/useMaxParams: resolver dispatch with diagnostics
|
|
126
|
+
function applyPromptSurfaceScope(
|
|
127
|
+
current: SuiPiToolPromptSurface,
|
|
128
|
+
options: ResolveToolPromptSurfaceOptions,
|
|
129
|
+
scope: PromptSurfaceScope,
|
|
130
|
+
sectionConfig: Record<string, unknown> | null,
|
|
131
|
+
diagnostics: ToolPromptSurfaceDiagnostic[],
|
|
132
|
+
): SuiPiToolPromptSurface {
|
|
133
|
+
const promptSurface = getPromptSurfaceConfig(sectionConfig, options.toolName, {
|
|
134
|
+
diagnostics,
|
|
135
|
+
options,
|
|
136
|
+
scope,
|
|
137
|
+
});
|
|
138
|
+
if (!promptSurface) return current;
|
|
139
|
+
return applyPromptSurfaceConfig(
|
|
140
|
+
current,
|
|
141
|
+
options.defaults,
|
|
142
|
+
promptSurface,
|
|
143
|
+
options,
|
|
144
|
+
scope,
|
|
145
|
+
diagnostics,
|
|
146
|
+
);
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
// biome-ignore lint/complexity/useMaxParams: per-scope config merger with diagnostics
|
|
150
|
+
function applyPromptSurfaceConfig(
|
|
151
|
+
current: SuiPiToolPromptSurface,
|
|
152
|
+
defaults: SuiPiToolPromptSurface,
|
|
153
|
+
config: Record<string, unknown>,
|
|
154
|
+
options: ResolveToolPromptSurfaceOptions,
|
|
155
|
+
scope: PromptSurfaceScope,
|
|
156
|
+
diagnostics: ToolPromptSurfaceDiagnostic[],
|
|
157
|
+
): SuiPiToolPromptSurface {
|
|
158
|
+
let next = clonePromptSurface(current);
|
|
159
|
+
|
|
160
|
+
for (const field of getResetFields(config.$reset, options, scope, diagnostics)) {
|
|
161
|
+
next = { ...next, [field]: clonePromptSurfaceField(defaults[field]) };
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
const description = getOptionalNonEmptyString(
|
|
165
|
+
config.description,
|
|
166
|
+
"description",
|
|
167
|
+
options,
|
|
168
|
+
scope,
|
|
169
|
+
diagnostics,
|
|
170
|
+
);
|
|
171
|
+
if (description !== undefined) next.description = description;
|
|
172
|
+
|
|
173
|
+
const promptSnippet = getOptionalNonEmptyString(
|
|
174
|
+
config.promptSnippet,
|
|
175
|
+
"promptSnippet",
|
|
176
|
+
options,
|
|
177
|
+
scope,
|
|
178
|
+
diagnostics,
|
|
179
|
+
);
|
|
180
|
+
if (promptSnippet !== undefined) next.promptSnippet = promptSnippet;
|
|
181
|
+
|
|
182
|
+
const promptGuidelines = getOptionalStringArray(
|
|
183
|
+
config.promptGuidelines,
|
|
184
|
+
"promptGuidelines",
|
|
185
|
+
options,
|
|
186
|
+
scope,
|
|
187
|
+
diagnostics,
|
|
188
|
+
);
|
|
189
|
+
if (promptGuidelines !== undefined) next.promptGuidelines = promptGuidelines;
|
|
190
|
+
|
|
191
|
+
const prepend = getOptionalStringArray(
|
|
192
|
+
config.prependPromptGuidelines,
|
|
193
|
+
"prependPromptGuidelines",
|
|
194
|
+
options,
|
|
195
|
+
scope,
|
|
196
|
+
diagnostics,
|
|
197
|
+
);
|
|
198
|
+
if (prepend !== undefined) next.promptGuidelines = [...prepend, ...next.promptGuidelines];
|
|
199
|
+
|
|
200
|
+
const append = getOptionalStringArray(
|
|
201
|
+
config.appendPromptGuidelines,
|
|
202
|
+
"appendPromptGuidelines",
|
|
203
|
+
options,
|
|
204
|
+
scope,
|
|
205
|
+
diagnostics,
|
|
206
|
+
);
|
|
207
|
+
if (append !== undefined) next.promptGuidelines = [...next.promptGuidelines, ...append];
|
|
208
|
+
|
|
209
|
+
return next;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
// ── Config extraction ──────────────────────────────────────────────────────
|
|
213
|
+
|
|
214
|
+
function getPromptSurfaceConfig(
|
|
215
|
+
sectionConfig: Record<string, unknown> | null,
|
|
216
|
+
toolName: string,
|
|
217
|
+
deps: {
|
|
218
|
+
diagnostics: ToolPromptSurfaceDiagnostic[];
|
|
219
|
+
options: ResolveToolPromptSurfaceOptions;
|
|
220
|
+
scope: PromptSurfaceScope;
|
|
221
|
+
},
|
|
222
|
+
): Record<string, unknown> | null {
|
|
223
|
+
if (!sectionConfig) return null;
|
|
224
|
+
if (sectionConfig.tools === undefined) return null;
|
|
225
|
+
if (!isRecord(sectionConfig.tools)) {
|
|
226
|
+
pushInvalidConfig(deps, "tools must be an object.");
|
|
227
|
+
return null;
|
|
228
|
+
}
|
|
229
|
+
const toolConfig = sectionConfig.tools[toolName];
|
|
230
|
+
if (toolConfig === undefined) return null;
|
|
231
|
+
if (!isRecord(toolConfig)) {
|
|
232
|
+
pushInvalidConfig(deps, `tools.${toolName} must be an object.`);
|
|
233
|
+
return null;
|
|
234
|
+
}
|
|
235
|
+
if (toolConfig.promptSurface === undefined) return null;
|
|
236
|
+
if (!isRecord(toolConfig.promptSurface)) {
|
|
237
|
+
pushInvalidConfig(deps, `tools.${toolName}.promptSurface must be an object.`);
|
|
238
|
+
return null;
|
|
239
|
+
}
|
|
240
|
+
return toolConfig.promptSurface;
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
// ── Field validation ───────────────────────────────────────────────────────
|
|
244
|
+
|
|
245
|
+
function getResetFields(
|
|
246
|
+
value: unknown,
|
|
247
|
+
options: ResolveToolPromptSurfaceOptions,
|
|
248
|
+
scope: PromptSurfaceScope,
|
|
249
|
+
diagnostics: ToolPromptSurfaceDiagnostic[],
|
|
250
|
+
): PromptSurfaceField[] {
|
|
251
|
+
if (value === undefined) return [];
|
|
252
|
+
if (!Array.isArray(value)) {
|
|
253
|
+
pushInvalidField(options, scope, diagnostics, "$reset", "must be an array.");
|
|
254
|
+
return [];
|
|
255
|
+
}
|
|
256
|
+
const fields: PromptSurfaceField[] = [];
|
|
257
|
+
for (const item of value) {
|
|
258
|
+
if (typeof item === "string" && PROMPT_SURFACE_FIELDS.has(item)) {
|
|
259
|
+
fields.push(item as PromptSurfaceField);
|
|
260
|
+
} else {
|
|
261
|
+
pushInvalidField(
|
|
262
|
+
options,
|
|
263
|
+
scope,
|
|
264
|
+
diagnostics,
|
|
265
|
+
"$reset",
|
|
266
|
+
`contains unsupported field ${JSON.stringify(item)}.`,
|
|
267
|
+
);
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
return fields;
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
// biome-ignore lint/complexity/useMaxParams: validation helper with diagnostics
|
|
274
|
+
function getOptionalNonEmptyString(
|
|
275
|
+
value: unknown,
|
|
276
|
+
field: string,
|
|
277
|
+
options: ResolveToolPromptSurfaceOptions,
|
|
278
|
+
scope: PromptSurfaceScope,
|
|
279
|
+
diagnostics: ToolPromptSurfaceDiagnostic[],
|
|
280
|
+
): string | undefined {
|
|
281
|
+
if (value === undefined) return undefined;
|
|
282
|
+
if (typeof value === "string" && value.length > 0) return value;
|
|
283
|
+
pushInvalidField(options, scope, diagnostics, field, "must be a non-empty string.");
|
|
284
|
+
return undefined;
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
// biome-ignore lint/complexity/useMaxParams: validation helper with diagnostics
|
|
288
|
+
function getOptionalStringArray(
|
|
289
|
+
value: unknown,
|
|
290
|
+
field: string,
|
|
291
|
+
options: ResolveToolPromptSurfaceOptions,
|
|
292
|
+
scope: PromptSurfaceScope,
|
|
293
|
+
diagnostics: ToolPromptSurfaceDiagnostic[],
|
|
294
|
+
): string[] | undefined {
|
|
295
|
+
if (value === undefined) return undefined;
|
|
296
|
+
if (Array.isArray(value) && value.every((item) => typeof item === "string")) {
|
|
297
|
+
return [...value];
|
|
298
|
+
}
|
|
299
|
+
pushInvalidField(options, scope, diagnostics, field, "must be an array of strings.");
|
|
300
|
+
return undefined;
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
// ── Diagnostics ────────────────────────────────────────────────────────────
|
|
304
|
+
|
|
305
|
+
function pushInvalidConfig(
|
|
306
|
+
deps: {
|
|
307
|
+
diagnostics: ToolPromptSurfaceDiagnostic[];
|
|
308
|
+
options: ResolveToolPromptSurfaceOptions;
|
|
309
|
+
scope: PromptSurfaceScope;
|
|
310
|
+
},
|
|
311
|
+
detail: string,
|
|
312
|
+
): void {
|
|
313
|
+
deps.diagnostics.push({
|
|
314
|
+
// biome-ignore lint/security/noSecrets: false positive on string constant
|
|
315
|
+
code: "invalidPromptSurfaceConfig",
|
|
316
|
+
scope: deps.scope,
|
|
317
|
+
section: deps.options.section,
|
|
318
|
+
toolName: deps.options.toolName,
|
|
319
|
+
message: `Invalid prompt-surface config for ${deps.options.section}.${deps.options.toolName}: ${detail}`,
|
|
320
|
+
});
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
// biome-ignore lint/complexity/useMaxParams: diagnostics utility with many fields
|
|
324
|
+
function pushInvalidField(
|
|
325
|
+
options: ResolveToolPromptSurfaceOptions,
|
|
326
|
+
scope: PromptSurfaceScope,
|
|
327
|
+
diagnostics: ToolPromptSurfaceDiagnostic[],
|
|
328
|
+
field: string,
|
|
329
|
+
detail: string,
|
|
330
|
+
): void {
|
|
331
|
+
diagnostics.push({
|
|
332
|
+
// biome-ignore lint/security/noSecrets: false positive on string constant
|
|
333
|
+
code: "invalidPromptSurfaceField",
|
|
334
|
+
scope,
|
|
335
|
+
section: options.section,
|
|
336
|
+
toolName: options.toolName,
|
|
337
|
+
message: `Invalid prompt-surface field ${field} for ${options.section}.${options.toolName}: ${detail}`,
|
|
338
|
+
});
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
// ── Cloning ────────────────────────────────────────────────────────────────
|
|
342
|
+
|
|
343
|
+
function clonePromptSurface(surface: SuiPiToolPromptSurface): SuiPiToolPromptSurface {
|
|
344
|
+
return {
|
|
345
|
+
description: surface.description,
|
|
346
|
+
promptSnippet: surface.promptSnippet,
|
|
347
|
+
promptGuidelines: [...surface.promptGuidelines],
|
|
348
|
+
};
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
function clonePromptSurfaceField<T extends string | string[]>(value: T): T {
|
|
352
|
+
return (Array.isArray(value) ? [...value] : value) as T;
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
356
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
357
|
+
}
|
|
@@ -9,7 +9,7 @@ export function registerSettingsCommand(pi: ExtensionAPI): void {
|
|
|
9
9
|
pi.registerCommand("supi-settings", {
|
|
10
10
|
description: "Manage SuPi extension settings",
|
|
11
11
|
handler: async (_args, ctx) => {
|
|
12
|
-
openSettingsOverlay(ctx);
|
|
12
|
+
openSettingsOverlay(pi, ctx);
|
|
13
13
|
},
|
|
14
14
|
});
|
|
15
15
|
}
|
|
@@ -1,48 +1,70 @@
|
|
|
1
|
-
//
|
|
1
|
+
// Event-backed settings contribution types for SuPi extensions.
|
|
2
2
|
//
|
|
3
|
-
// Extensions
|
|
4
|
-
//
|
|
3
|
+
// Extensions contribute config-backed settings sections through PI's shared
|
|
4
|
+
// event bus. The public helper is registerConfigSettings(pi, ...); this module
|
|
5
|
+
// owns the internal collector protocol used by /supi-settings.
|
|
5
6
|
|
|
6
7
|
import type { ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
7
8
|
import type { SettingItem } from "@earendil-works/pi-tui";
|
|
8
|
-
|
|
9
|
+
|
|
10
|
+
export const SUPI_SETTINGS_COLLECT_EVENT = "supi:settings:collect";
|
|
9
11
|
|
|
10
12
|
export type SettingsScope = "project" | "global";
|
|
11
13
|
|
|
12
14
|
export interface SettingsSection {
|
|
13
|
-
/**
|
|
15
|
+
/** Stable contribution identifier — e.g. "lsp", "claude-md". */
|
|
14
16
|
id: string;
|
|
15
|
-
/** Human-readable label shown in the UI */
|
|
17
|
+
/** Human-readable label shown in the UI. */
|
|
16
18
|
label: string;
|
|
17
|
-
/** Load current SettingItem[] for the given scope */
|
|
19
|
+
/** Load current SettingItem[] for the given scope. */
|
|
18
20
|
loadValues: (scope: SettingsScope, cwd: string, ctx?: ExtensionContext) => SettingItem[];
|
|
19
|
-
/** Persist a
|
|
20
|
-
persistChange: (
|
|
21
|
-
scope: SettingsScope,
|
|
22
|
-
cwd: string,
|
|
23
|
-
settingId: string,
|
|
24
|
-
value: string,
|
|
25
|
-
ctx?: ExtensionContext,
|
|
26
|
-
) => void;
|
|
21
|
+
/** Persist a UI value back to SuPi config. */
|
|
22
|
+
persistChange: (scope: SettingsScope, cwd: string, settingId: string, value: string) => void;
|
|
27
23
|
}
|
|
28
24
|
|
|
29
|
-
|
|
25
|
+
export interface SettingsContributionCollector {
|
|
26
|
+
add(section: SettingsSection): void;
|
|
27
|
+
}
|
|
30
28
|
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
* Duplicate ids replace the previous registration.
|
|
35
|
-
*/
|
|
36
|
-
export function registerSettings(section: SettingsSection): void {
|
|
37
|
-
registry.register(section.id, section);
|
|
29
|
+
export interface SettingsCollectionDiagnostic {
|
|
30
|
+
kind: "warning";
|
|
31
|
+
message: string;
|
|
38
32
|
}
|
|
39
33
|
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
34
|
+
export interface SettingsCollectionResult {
|
|
35
|
+
sections: SettingsSection[];
|
|
36
|
+
diagnostics: SettingsCollectionDiagnostic[];
|
|
43
37
|
}
|
|
44
38
|
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
39
|
+
export function isSettingsContributionCollector(
|
|
40
|
+
value: unknown,
|
|
41
|
+
): value is SettingsContributionCollector {
|
|
42
|
+
return (
|
|
43
|
+
typeof value === "object" &&
|
|
44
|
+
value !== null &&
|
|
45
|
+
typeof (value as { add?: unknown }).add === "function"
|
|
46
|
+
);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/** Create a collector with last-wins duplicate handling and warning diagnostics. */
|
|
50
|
+
export function createSettingsContributionCollector(): SettingsContributionCollector & {
|
|
51
|
+
result(): SettingsCollectionResult;
|
|
52
|
+
} {
|
|
53
|
+
const sections = new Map<string, SettingsSection>();
|
|
54
|
+
const diagnostics: SettingsCollectionDiagnostic[] = [];
|
|
55
|
+
|
|
56
|
+
return {
|
|
57
|
+
add(section: SettingsSection): void {
|
|
58
|
+
if (sections.has(section.id)) {
|
|
59
|
+
diagnostics.push({
|
|
60
|
+
kind: "warning",
|
|
61
|
+
message: `Duplicate SuPi settings contribution "${section.id}"; using the last contribution.`,
|
|
62
|
+
});
|
|
63
|
+
}
|
|
64
|
+
sections.set(section.id, section);
|
|
65
|
+
},
|
|
66
|
+
result(): SettingsCollectionResult {
|
|
67
|
+
return { sections: Array.from(sections.values()), diagnostics: [...diagnostics] };
|
|
68
|
+
},
|
|
69
|
+
};
|
|
48
70
|
}
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
// Uses pi-tui's SettingsList with scope toggle (Tab), extension grouping,
|
|
4
4
|
// and search. Each extension declares its settings via registerSettings().
|
|
5
5
|
|
|
6
|
-
import type { ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
6
|
+
import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
7
7
|
import { getSettingsListTheme } from "@earendil-works/pi-coding-agent";
|
|
8
8
|
import {
|
|
9
9
|
Container,
|
|
@@ -19,9 +19,11 @@ import {
|
|
|
19
19
|
} from "@earendil-works/pi-tui";
|
|
20
20
|
import { getSelectableModels } from "../model-selection.ts";
|
|
21
21
|
import {
|
|
22
|
-
|
|
22
|
+
createSettingsContributionCollector,
|
|
23
|
+
type SettingsCollectionDiagnostic,
|
|
23
24
|
type SettingsScope,
|
|
24
25
|
type SettingsSection,
|
|
26
|
+
SUPI_SETTINGS_COLLECT_EVENT,
|
|
25
27
|
} from "./settings-registry.ts";
|
|
26
28
|
|
|
27
29
|
// ── Input submenu component ──────────────────────────────────
|
|
@@ -73,9 +75,15 @@ export function createInputSubmenu(
|
|
|
73
75
|
|
|
74
76
|
// ── Types ────────────────────────────────────────────────────
|
|
75
77
|
|
|
78
|
+
interface OverlayStatus {
|
|
79
|
+
kind: "warning" | "error";
|
|
80
|
+
message: string;
|
|
81
|
+
}
|
|
82
|
+
|
|
76
83
|
interface OverlayState {
|
|
77
84
|
scope: SettingsScope;
|
|
78
85
|
cwd: string;
|
|
86
|
+
status?: OverlayStatus;
|
|
79
87
|
}
|
|
80
88
|
|
|
81
89
|
// ── Pure helpers ─────────────────────────────────────────────
|
|
@@ -117,10 +125,27 @@ function findSectionAndId(
|
|
|
117
125
|
return { section, itemId };
|
|
118
126
|
}
|
|
119
127
|
|
|
128
|
+
function collectSettingsSections(pi: ExtensionAPI) {
|
|
129
|
+
const collector = createSettingsContributionCollector();
|
|
130
|
+
pi.events.emit(SUPI_SETTINGS_COLLECT_EVENT, collector);
|
|
131
|
+
return collector.result();
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
function latestStatus(diagnostics: SettingsCollectionDiagnostic[]): OverlayStatus | undefined {
|
|
135
|
+
const latest = diagnostics.at(-1);
|
|
136
|
+
return latest ? { kind: latest.kind, message: latest.message } : undefined;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
function formatSettingsError(settingId: string, error: unknown): string {
|
|
140
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
141
|
+
return `Could not save SuPi setting "${settingId}": ${message}`;
|
|
142
|
+
}
|
|
143
|
+
|
|
120
144
|
// ── Component ────────────────────────────────────────────────
|
|
121
145
|
|
|
122
146
|
interface SettingsOverlayDeps {
|
|
123
147
|
ctx: ExtensionContext;
|
|
148
|
+
sections: SettingsSection[];
|
|
124
149
|
state: OverlayState;
|
|
125
150
|
container: Container;
|
|
126
151
|
settingsList: SettingsList | null;
|
|
@@ -130,22 +155,23 @@ interface SettingsOverlayDeps {
|
|
|
130
155
|
}
|
|
131
156
|
|
|
132
157
|
function createSettingsList(deps: SettingsOverlayDeps): SettingsList {
|
|
133
|
-
const
|
|
134
|
-
const items = buildFlatItems(sections, deps.state.scope, deps.state.cwd, deps.ctx);
|
|
158
|
+
const items = buildFlatItems(deps.sections, deps.state.scope, deps.state.cwd, deps.ctx);
|
|
135
159
|
const onChange = (flatId: string, newValue: string) => {
|
|
136
|
-
const found = findSectionAndId(sections, flatId);
|
|
137
|
-
|
|
138
|
-
found
|
|
139
|
-
deps.state.scope,
|
|
140
|
-
deps.state.
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
160
|
+
const found = findSectionAndId(deps.sections, flatId);
|
|
161
|
+
try {
|
|
162
|
+
if (found) {
|
|
163
|
+
found.section.persistChange(deps.state.scope, deps.state.cwd, found.itemId, newValue);
|
|
164
|
+
deps.state.status = undefined;
|
|
165
|
+
}
|
|
166
|
+
} catch (error) {
|
|
167
|
+
deps.state.status = {
|
|
168
|
+
kind: "error",
|
|
169
|
+
message: formatSettingsError(found?.itemId ?? flatId, error),
|
|
170
|
+
};
|
|
145
171
|
}
|
|
146
172
|
// Re-read all values to reflect persisted changes, but keep the list
|
|
147
173
|
// instance (and its selectedIndex) intact.
|
|
148
|
-
const updatedItems = buildFlatItems(sections, deps.state.scope, deps.state.cwd, deps.ctx);
|
|
174
|
+
const updatedItems = buildFlatItems(deps.sections, deps.state.scope, deps.state.cwd, deps.ctx);
|
|
149
175
|
for (const updated of updatedItems) {
|
|
150
176
|
const existing = items.find((i) => i.id === updated.id);
|
|
151
177
|
if (existing && existing.currentValue !== updated.currentValue) {
|
|
@@ -171,6 +197,7 @@ function rebuildSettingsList(deps: SettingsOverlayDeps): SettingsList {
|
|
|
171
197
|
|
|
172
198
|
deps.container.clear();
|
|
173
199
|
deps.container.addChild(createHeaderComponent(deps));
|
|
200
|
+
deps.container.addChild(createStatusComponent(deps));
|
|
174
201
|
deps.container.addChild(settingsList);
|
|
175
202
|
|
|
176
203
|
return settingsList;
|
|
@@ -188,6 +215,20 @@ function createHeaderComponent(deps: SettingsOverlayDeps): Text {
|
|
|
188
215
|
return headerText;
|
|
189
216
|
}
|
|
190
217
|
|
|
218
|
+
function createStatusComponent(deps: SettingsOverlayDeps): {
|
|
219
|
+
render: () => string[];
|
|
220
|
+
invalidate: () => void;
|
|
221
|
+
} {
|
|
222
|
+
return {
|
|
223
|
+
render: () => {
|
|
224
|
+
const status = deps.state.status;
|
|
225
|
+
if (!status) return [];
|
|
226
|
+
return [deps.theme.fg(status.kind, status.message)];
|
|
227
|
+
},
|
|
228
|
+
invalidate: () => {},
|
|
229
|
+
};
|
|
230
|
+
}
|
|
231
|
+
|
|
191
232
|
function handleScopeToggle(deps: SettingsOverlayDeps): void {
|
|
192
233
|
deps.state.scope = deps.state.scope === "project" ? "global" : "project";
|
|
193
234
|
rebuildSettingsList(deps);
|
|
@@ -288,19 +329,24 @@ function buildModelItems(ctx?: ExtensionContext): SelectItem[] {
|
|
|
288
329
|
|
|
289
330
|
// ── Entry point ──────────────────────────────────────────────
|
|
290
331
|
|
|
291
|
-
export function openSettingsOverlay(ctx: ExtensionContext): void {
|
|
292
|
-
const
|
|
293
|
-
if (sections.length === 0) {
|
|
332
|
+
export function openSettingsOverlay(pi: ExtensionAPI, ctx: ExtensionContext): void {
|
|
333
|
+
const collection = collectSettingsSections(pi);
|
|
334
|
+
if (collection.sections.length === 0) {
|
|
294
335
|
ctx.ui.notify("No settings registered by SuPi extensions", "info");
|
|
295
336
|
return;
|
|
296
337
|
}
|
|
297
338
|
|
|
298
339
|
void ctx.ui.custom<void>((tui, theme, _kb, done) => {
|
|
299
|
-
const state: OverlayState = {
|
|
340
|
+
const state: OverlayState = {
|
|
341
|
+
scope: "project",
|
|
342
|
+
cwd: ctx.cwd,
|
|
343
|
+
status: latestStatus(collection.diagnostics),
|
|
344
|
+
};
|
|
300
345
|
const container = new Container();
|
|
301
346
|
|
|
302
347
|
const deps: SettingsOverlayDeps = {
|
|
303
348
|
ctx,
|
|
349
|
+
sections: collection.sections,
|
|
304
350
|
state,
|
|
305
351
|
container,
|
|
306
352
|
settingsList: null,
|
|
@@ -1,9 +1,15 @@
|
|
|
1
|
-
// supi-core settings domain — settings
|
|
1
|
+
// supi-core settings domain — event-backed settings contribution types and command wiring.
|
|
2
2
|
|
|
3
3
|
export { registerSettingsCommand } from "./settings/settings-command.ts";
|
|
4
|
-
export type {
|
|
4
|
+
export type {
|
|
5
|
+
SettingsCollectionDiagnostic,
|
|
6
|
+
SettingsCollectionResult,
|
|
7
|
+
SettingsContributionCollector,
|
|
8
|
+
SettingsScope,
|
|
9
|
+
SettingsSection,
|
|
10
|
+
} from "./settings/settings-registry.ts";
|
|
5
11
|
export {
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
12
|
+
createSettingsContributionCollector,
|
|
13
|
+
isSettingsContributionCollector,
|
|
14
|
+
SUPI_SETTINGS_COLLECT_EVENT,
|
|
9
15
|
} from "./settings/settings-registry.ts";
|
|
@@ -53,6 +53,16 @@ export function derivePromptSurface(spec: SuiPiToolSpec): SuiPiToolPromptSurface
|
|
|
53
53
|
};
|
|
54
54
|
}
|
|
55
55
|
|
|
56
|
+
// Re-export prompt-surface types (implemented in config/prompt-surface.ts)
|
|
57
|
+
export {
|
|
58
|
+
notifyToolPromptSurfaceDiagnostics,
|
|
59
|
+
type ResolveToolPromptSurfaceOptions,
|
|
60
|
+
type ResolveToolPromptSurfaceResult,
|
|
61
|
+
resolveToolPromptSurface,
|
|
62
|
+
type ToolPromptSurfaceDiagnostic,
|
|
63
|
+
type ToolPromptSurfaceDiagnosticCode,
|
|
64
|
+
} from "./config/prompt-surface.ts";
|
|
65
|
+
|
|
56
66
|
// ---------------------------------------------------------------------------
|
|
57
67
|
// Registration
|
|
58
68
|
// ---------------------------------------------------------------------------
|