@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
@@ -39,10 +39,9 @@ Config file locations:
39
39
 
40
40
  ### Settings helpers
41
41
 
42
- - `registerSettings()` — register an arbitrary settings section
43
- - `registerConfigSettings()` — register a config-backed settings section with scoped persistence helpers
44
- - `registerSettingsCommand()` — register `/supi-settings`
45
- - `openSettingsOverlay()` — open the shared settings UI directly
42
+ - `registerConfigSettings(pi, options)` — contribute a config-backed settings section with scoped persistence helpers
43
+ - `registerSettingsCommand(pi)` — register `/supi-settings` (used by `@mrclrchtr/supi-settings`)
44
+ - `openSettingsOverlay(pi, ctx)` — open the shared settings UI directly
46
45
  - `createInputSubmenu()` — helper for simple text-entry submenus
47
46
 
48
47
  The built-in settings UI supports:
@@ -83,25 +82,34 @@ The built-in settings UI supports:
83
82
  ## Example
84
83
 
85
84
  ```ts
85
+ import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
86
86
  import { loadSupiConfig, registerConfigSettings, wrapExtensionContext } from "@mrclrchtr/supi-core/api";
87
87
 
88
- const config = loadSupiConfig("my-extension", process.cwd(), {
89
- enabled: true,
90
- });
91
-
92
- registerConfigSettings({
93
- id: "my-extension",
94
- label: "My Extension",
95
- section: "my-extension",
96
- defaults: { enabled: true },
97
- buildItems: () => [],
98
- persistChange: () => {},
99
- });
100
-
101
- const message = wrapExtensionContext("my-extension", "hello", {
102
- file: "CLAUDE.md",
103
- turn: 1,
104
- });
88
+ export default function myExtension(pi: ExtensionAPI) {
89
+ const defaults = { enabled: true };
90
+ const config = loadSupiConfig("my-extension", process.cwd(), defaults);
91
+
92
+ registerConfigSettings(pi, {
93
+ id: "my-extension",
94
+ label: "My Extension",
95
+ section: "my-extension",
96
+ defaults,
97
+ buildItems: (settings) => [
98
+ {
99
+ id: "enabled",
100
+ label: "Enabled",
101
+ currentValue: settings.enabled ? "on" : "off",
102
+ values: ["on", "off"],
103
+ configType: "boolean" as const,
104
+ },
105
+ ],
106
+ });
107
+
108
+ const message = wrapExtensionContext("my-extension", "hello", {
109
+ enabled: config.enabled,
110
+ });
111
+ void message;
112
+ }
105
113
  ```
106
114
 
107
115
  ## Source
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mrclrchtr/supi-core",
3
- "version": "2.0.6",
3
+ "version": "2.1.0",
4
4
  "description": "SuPi core — shared infrastructure for SuPi extensions (XML context tags, config system)",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -70,6 +70,7 @@
70
70
  "./status-spinner": "./src/status-spinner.ts",
71
71
  "./terminal": "./src/terminal.ts",
72
72
  "./tool-framework": "./src/tool-framework.ts",
73
+ "./prompt-surface": "./src/prompt-surface.ts",
73
74
  "./types": "./src/types.ts"
74
75
  }
75
76
  }
@@ -1,14 +1,17 @@
1
- // Config-aware settings helper for SuPi config-backed settings sections.
2
- // Wraps registerSettings() and centralizes selected-scope loading + scoped persistence.
1
+ // Config-aware settings contribution helper for SuPi packages.
3
2
  //
4
- // Setting items can declare a `configType` ("boolean" | "number" | "stringList")
5
- // to enable auto-generated persistChange. When all items have a configType,
6
- // the persistChange callback can be omitted.
3
+ // Registers config-backed settings sections through PI's shared event bus so
4
+ // /supi-settings can collect contributions from all loaded extensions without
5
+ // relying on a shared supi-core module instance.
7
6
 
8
- import type { ExtensionContext } from "@earendil-works/pi-coding-agent";
7
+ import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
9
8
  import type { SettingItem } from "@earendil-works/pi-tui";
10
- import type { SettingsScope } from "../settings/settings-registry.ts";
11
- import { registerSettings } from "../settings/settings-registry.ts";
9
+ import {
10
+ isSettingsContributionCollector,
11
+ type SettingsScope,
12
+ type SettingsSection,
13
+ SUPI_SETTINGS_COLLECT_EVENT,
14
+ } from "../settings/settings-registry.ts";
12
15
  import { loadSupiConfigForScope, removeSupiConfigKey, writeSupiConfig } from "./config.ts";
13
16
 
14
17
  // ── Types ──────────────────────────────────────────────────────────────────
@@ -22,22 +25,13 @@ import { loadSupiConfigForScope, removeSupiConfigKey, writeSupiConfig } from "./
22
25
  */
23
26
  export type ConfigSettingType = "boolean" | "number" | "stringList";
24
27
 
25
- /**
26
- * Extended setting item that can declare its config type for auto-generated
27
- * persistence handling.
28
- */
28
+ /** Extended setting item that can declare its config type for persistence. */
29
29
  export interface ConfigSettingItem extends SettingItem {
30
- /**
31
- * When set, persistChange for this item is auto-generated.
32
- * All items must declare a configType for auto-generation to activate.
33
- */
30
+ /** Config value type used for auto-generated persistence. */
34
31
  configType?: ConfigSettingType;
35
32
  }
36
33
 
37
- /**
38
- * Helpers provided to the persistChange callback for writing or removing
39
- * scoped config values.
40
- */
34
+ /** Helpers provided to persistChange for scoped SuPi config writes. */
41
35
  export interface ConfigSettingsHelpers {
42
36
  /** Write a key to the selected scope's config section. */
43
37
  set(key: string, value: unknown): void;
@@ -45,22 +39,23 @@ export interface ConfigSettingsHelpers {
45
39
  unset(key: string): void;
46
40
  }
47
41
 
42
+ export interface ConfigSettingsPersistedChange {
43
+ scope: SettingsScope;
44
+ cwd: string;
45
+ settingId: string;
46
+ value: string;
47
+ }
48
+
48
49
  export interface ConfigSettingsOptions<T> {
49
- /** Extension identifier — e.g. "lsp", "claude-md" */
50
+ /** Settings contribution identifier — e.g. "lsp", "claude-md". */
50
51
  id: string;
51
- /** Human-readable label shown in the UI */
52
+ /** Human-readable label shown in the UI. */
52
53
  label: string;
53
- /** SuPi config section name — e.g. "lsp", "claude-md" */
54
+ /** SuPi config section name — e.g. "lsp", "claude-md". */
54
55
  section: string;
55
- /** Default config values */
56
+ /** Default config values. */
56
57
  defaults: T;
57
- /**
58
- * Build SettingItem[] from scoped config. Called by loadValues.
59
- *
60
- * Items can include a `configType` property for auto-generated
61
- * persistChange handling. When ALL items declare a configType,
62
- * the `persistChange` callback can be omitted.
63
- */
58
+ /** Build SettingItem[] from scoped config. */
64
59
  buildItems: (
65
60
  settings: T,
66
61
  scope: SettingsScope,
@@ -68,10 +63,10 @@ export interface ConfigSettingsOptions<T> {
68
63
  ctx?: ExtensionContext,
69
64
  ) => ConfigSettingItem[];
70
65
  /**
71
- * Handle a settings change with scoped persistence helpers.
66
+ * Convert a UI value into scoped SuPi config writes.
72
67
  *
73
- * Optional when all items returned by `buildItems` declare a `configType`.
74
- * Required when any item lacks a `configType`.
68
+ * Optional when every item returned by `buildItems` declares `configType`.
69
+ * Required when any item lacks `configType`.
75
70
  */
76
71
  persistChange?: (
77
72
  scope: SettingsScope,
@@ -80,6 +75,8 @@ export interface ConfigSettingsOptions<T> {
80
75
  value: string,
81
76
  helpers: ConfigSettingsHelpers,
82
77
  ) => void;
78
+ /** Optional live runtime sync after successful persistence. */
79
+ afterPersist?: (change: ConfigSettingsPersistedChange) => void;
83
80
  /** Optional home directory for config resolution (testing). */
84
81
  homeDir?: string;
85
82
  }
@@ -128,23 +125,27 @@ function areAllItemsDeclarative(items: ConfigSettingItem[]): boolean {
128
125
  return items.length > 0 && items.every((i) => i.configType !== undefined);
129
126
  }
130
127
 
131
- // ── Registration ───────────────────────────────────────────────────────────
128
+ function createHelpers<T>(options: ConfigSettingsOptions<T>, scope: SettingsScope, cwd: string) {
129
+ return {
130
+ set: (key: string, val: unknown) => {
131
+ writeSupiConfig(
132
+ { section: options.section, scope, cwd },
133
+ { [key]: val },
134
+ { homeDir: options.homeDir },
135
+ );
136
+ },
137
+ unset: (key: string) => {
138
+ removeSupiConfigKey({ section: options.section, scope, cwd }, key, {
139
+ homeDir: options.homeDir,
140
+ });
141
+ },
142
+ } satisfies ConfigSettingsHelpers;
143
+ }
132
144
 
133
- /**
134
- * Register a config-backed settings section.
135
- *
136
- * Loads display values from the selected scope only (`defaults <- selected scope`)
137
- * instead of merged effective runtime config. Provides scoped `set` / `unset`
138
- * persistence helpers so extensions don't need to wire `writeSupiConfig` /
139
- * `removeSupiConfigKey` by hand.
140
- *
141
- * When every item returned by `buildItems` declares a `configType`, the
142
- * `persistChange` callback is optional and will be auto-generated.
143
- */
144
- export function registerConfigSettings<T>(options: ConfigSettingsOptions<T>): void {
145
+ function toSettingsSection<T>(options: ConfigSettingsOptions<T>): SettingsSection {
145
146
  let cachedItems: ConfigSettingItem[] | undefined;
146
147
 
147
- registerSettings({
148
+ return {
148
149
  id: options.id,
149
150
  label: options.label,
150
151
  loadValues: (scope, cwd, ctx) => {
@@ -157,32 +158,50 @@ export function registerConfigSettings<T>(options: ConfigSettingsOptions<T>): vo
157
158
  return items;
158
159
  },
159
160
  persistChange: (scope, cwd, settingId, value) => {
160
- const helpers: ConfigSettingsHelpers = {
161
- set: (key, val) => {
162
- writeSupiConfig(
163
- { section: options.section, scope, cwd },
164
- { [key]: val },
165
- { homeDir: options.homeDir },
166
- );
167
- },
168
- unset: (key) => {
169
- removeSupiConfigKey({ section: options.section, scope, cwd }, key, {
170
- homeDir: options.homeDir,
171
- });
172
- },
173
- };
174
-
175
- // Use manual persistChange when provided
161
+ const helpers = createHelpers(options, scope, cwd);
162
+
176
163
  if (options.persistChange) {
177
164
  options.persistChange(scope, cwd, settingId, value, helpers);
178
- return;
165
+ } else {
166
+ const items = cachedItems ?? options.buildItems(options.defaults, scope, cwd, undefined);
167
+ if (!areAllItemsDeclarative(items)) {
168
+ throw new Error(
169
+ `Settings contribution "${options.id}" needs persistChange or configType on every item.`,
170
+ );
171
+ }
172
+ autoPersistChange(settingId, value, helpers, items);
179
173
  }
180
174
 
181
- // Auto-generate when all items are declarative
182
- const items = cachedItems ?? options.buildItems(options.defaults, scope, cwd, undefined);
183
- if (areAllItemsDeclarative(items)) {
184
- autoPersistChange(settingId, value, helpers, items);
175
+ try {
176
+ options.afterPersist?.({ scope, cwd, settingId, value });
177
+ } catch (error) {
178
+ const message = error instanceof Error ? error.message : String(error);
179
+ throw new Error(`Saved setting, but live sync failed: ${message}`, { cause: error });
185
180
  }
186
181
  },
182
+ };
183
+ }
184
+
185
+ // ── Registration ───────────────────────────────────────────────────────────
186
+
187
+ /**
188
+ * Register a config-backed settings contribution for `/supi-settings`.
189
+ *
190
+ * Contributions are collected through PI's process-local event bus. Call this
191
+ * during the extension factory function, not in async session handlers.
192
+ */
193
+ export function registerConfigSettings<T>(
194
+ pi: ExtensionAPI,
195
+ options: ConfigSettingsOptions<T>,
196
+ ): void {
197
+ const section = toSettingsSection(options);
198
+ const dispose = pi.events.on(SUPI_SETTINGS_COLLECT_EVENT, (collector) => {
199
+ if (isSettingsContributionCollector(collector)) {
200
+ collector.add(section);
201
+ }
202
+ });
203
+
204
+ pi.on("session_shutdown", () => {
205
+ dispose();
187
206
  });
188
207
  }
@@ -21,6 +21,15 @@ function getProjectConfigPath(cwd: string): string {
21
21
  return path.join(cwd, PROJECT_CONFIG_DIR, CONFIG_FILE);
22
22
  }
23
23
 
24
+ /** Return the SuPi config file path for one scope. */
25
+ export function getSupiConfigPath(
26
+ scope: "global" | "project",
27
+ cwd: string,
28
+ options?: SupiConfigOptions,
29
+ ): string {
30
+ return scope === "global" ? getGlobalConfigPath(options?.homeDir) : getProjectConfigPath(cwd);
31
+ }
32
+
24
33
  export function readJsonFile(filePath: string): Record<string, unknown> | null {
25
34
  let content: string;
26
35
  try {
@@ -95,15 +104,22 @@ export function loadSupiConfigForScope<T>(
95
104
  defaults: T,
96
105
  options: { scope: "global" | "project" } & SupiConfigOptions,
97
106
  ): T {
98
- const config =
99
- options.scope === "global"
100
- ? readJsonFile(getGlobalConfigPath(options.homeDir))
101
- : readJsonFile(getProjectConfigPath(cwd));
107
+ const config = readJsonFile(getSupiConfigPath(options.scope, cwd, { homeDir: options.homeDir }));
102
108
 
103
109
  const scopedSection = extractSection(config, section);
104
110
  return shallowMerge(defaults, scopedSection);
105
111
  }
106
112
 
113
+ /** Load the raw object for one config section and one scope. */
114
+ export function loadSupiConfigSectionForScope(
115
+ section: string,
116
+ cwd: string,
117
+ options: { scope: "global" | "project" } & SupiConfigOptions,
118
+ ): Record<string, unknown> | null {
119
+ const config = readJsonFile(getSupiConfigPath(options.scope, cwd, { homeDir: options.homeDir }));
120
+ return extractSection(config, section);
121
+ }
122
+
107
123
  export interface SupiConfigLocation {
108
124
  section: string;
109
125
  scope: "global" | "project";
@@ -118,8 +134,7 @@ export function writeSupiConfig(
118
134
  value: Record<string, unknown>,
119
135
  options?: SupiConfigOptions,
120
136
  ): void {
121
- const configPath =
122
- loc.scope === "global" ? getGlobalConfigPath(options?.homeDir) : getProjectConfigPath(loc.cwd);
137
+ const configPath = getSupiConfigPath(loc.scope, loc.cwd, options);
123
138
 
124
139
  const dir = path.dirname(configPath);
125
140
  fs.mkdirSync(dir, { recursive: true });
@@ -142,8 +157,7 @@ export function removeSupiConfigKey(
142
157
  key: string,
143
158
  options?: SupiConfigOptions,
144
159
  ): void {
145
- const configPath =
146
- loc.scope === "global" ? getGlobalConfigPath(options?.homeDir) : getProjectConfigPath(loc.cwd);
160
+ const configPath = getSupiConfigPath(loc.scope, loc.cwd, options);
147
161
 
148
162
  const existing = readJsonFile(configPath);
149
163
  if (!existing) return;