@mrclrchtr/supi-lsp 2.0.6 → 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.
Files changed (24) hide show
  1. package/node_modules/@mrclrchtr/supi-code-runtime/node_modules/@mrclrchtr/supi-core/README.md +29 -21
  2. package/node_modules/@mrclrchtr/supi-code-runtime/node_modules/@mrclrchtr/supi-core/package.json +2 -1
  3. package/node_modules/@mrclrchtr/supi-code-runtime/node_modules/@mrclrchtr/supi-core/src/config/config-settings.ts +88 -69
  4. package/node_modules/@mrclrchtr/supi-code-runtime/node_modules/@mrclrchtr/supi-core/src/config/config.ts +22 -8
  5. package/node_modules/@mrclrchtr/supi-code-runtime/node_modules/@mrclrchtr/supi-core/src/config/prompt-surface.ts +357 -0
  6. package/node_modules/@mrclrchtr/supi-code-runtime/node_modules/@mrclrchtr/supi-core/src/prompt-surface.ts +4 -0
  7. package/node_modules/@mrclrchtr/supi-code-runtime/node_modules/@mrclrchtr/supi-core/src/settings/settings-command.ts +1 -1
  8. package/node_modules/@mrclrchtr/supi-code-runtime/node_modules/@mrclrchtr/supi-core/src/settings/settings-registry.ts +51 -29
  9. package/node_modules/@mrclrchtr/supi-code-runtime/node_modules/@mrclrchtr/supi-core/src/settings/settings-ui.ts +64 -18
  10. package/node_modules/@mrclrchtr/supi-code-runtime/node_modules/@mrclrchtr/supi-core/src/settings.ts +11 -5
  11. package/node_modules/@mrclrchtr/supi-code-runtime/node_modules/@mrclrchtr/supi-core/src/tool-framework.ts +10 -0
  12. package/node_modules/@mrclrchtr/supi-code-runtime/package.json +2 -2
  13. package/node_modules/@mrclrchtr/supi-core/README.md +29 -21
  14. package/node_modules/@mrclrchtr/supi-core/package.json +2 -1
  15. package/node_modules/@mrclrchtr/supi-core/src/config/config-settings.ts +88 -69
  16. package/node_modules/@mrclrchtr/supi-core/src/config/config.ts +22 -8
  17. package/node_modules/@mrclrchtr/supi-core/src/config/prompt-surface.ts +357 -0
  18. package/node_modules/@mrclrchtr/supi-core/src/prompt-surface.ts +4 -0
  19. package/node_modules/@mrclrchtr/supi-core/src/settings/settings-command.ts +1 -1
  20. package/node_modules/@mrclrchtr/supi-core/src/settings/settings-registry.ts +51 -29
  21. package/node_modules/@mrclrchtr/supi-core/src/settings/settings-ui.ts +64 -18
  22. package/node_modules/@mrclrchtr/supi-core/src/settings.ts +11 -5
  23. package/node_modules/@mrclrchtr/supi-core/src/tool-framework.ts +10 -0
  24. 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
+ }
@@ -0,0 +1,4 @@
1
+ // supi-core prompt-surface — configurable tool prompt-surface resolver (package boundary for @mrclrchtr/supi-core/prompt-surface).
2
+
3
+ // biome-ignore lint/performance/noReExportAll: intentional barrel
4
+ export * from "./config/prompt-surface.ts";
@@ -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
- // Settings registry for SuPi extensions.
1
+ // Event-backed settings contribution types for SuPi extensions.
2
2
  //
3
- // Extensions declare their settings via `registerSettings()` during their
4
- // factory function. The generic settings UI reads them via `getRegisteredSettings()`.
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
- import { createRegistry } from "../registry-utils.ts";
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
- /** Extension identifier — e.g. "lsp", "claude-md" */
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 change back to config */
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
- const registry = createRegistry<SettingsSection>("settings-registry");
25
+ export interface SettingsContributionCollector {
26
+ add(section: SettingsSection): void;
27
+ }
30
28
 
31
- /**
32
- * Register a settings section for an extension.
33
- * Call during the extension factory function (not async handlers).
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
- /** Get all registered settings sections in registration order. */
41
- export function getRegisteredSettings(): SettingsSection[] {
42
- return registry.getAll();
34
+ export interface SettingsCollectionResult {
35
+ sections: SettingsSection[];
36
+ diagnostics: SettingsCollectionDiagnostic[];
43
37
  }
44
38
 
45
- /** Clear the registry — used by tests. */
46
- export function clearRegisteredSettings(): void {
47
- registry.clear();
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
- getRegisteredSettings,
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 sections = getRegisteredSettings();
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
- if (found) {
138
- found.section.persistChange(
139
- deps.state.scope,
140
- deps.state.cwd,
141
- found.itemId,
142
- newValue,
143
- deps.ctx,
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 sections = getRegisteredSettings();
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 = { scope: "project", cwd: ctx.cwd };
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 registry (lightweight, type-only pi-tui import).
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 { SettingsScope, SettingsSection } from "./settings/settings-registry.ts";
4
+ export type {
5
+ SettingsCollectionDiagnostic,
6
+ SettingsCollectionResult,
7
+ SettingsContributionCollector,
8
+ SettingsScope,
9
+ SettingsSection,
10
+ } from "./settings/settings-registry.ts";
5
11
  export {
6
- clearRegisteredSettings,
7
- getRegisteredSettings,
8
- registerSettings,
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
  // ---------------------------------------------------------------------------
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mrclrchtr/supi-code-runtime",
3
- "version": "2.0.6",
3
+ "version": "2.1.0",
4
4
  "description": "SuPi code-runtime — shared workspace context, capability contracts, and canonical types for the code-understanding stack",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -29,7 +29,7 @@
29
29
  "!__tests__"
30
30
  ],
31
31
  "dependencies": {
32
- "@mrclrchtr/supi-core": "2.0.6"
32
+ "@mrclrchtr/supi-core": "2.1.0"
33
33
  },
34
34
  "bundledDependencies": [
35
35
  "@mrclrchtr/supi-core"