@mrclrchtr/supi-prompt-suggestions 1.16.1
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 +62 -0
- package/node_modules/@mrclrchtr/supi-core/README.md +113 -0
- package/node_modules/@mrclrchtr/supi-core/package.json +79 -0
- package/node_modules/@mrclrchtr/supi-core/src/abort-utils.ts +31 -0
- package/node_modules/@mrclrchtr/supi-core/src/api.ts +44 -0
- package/node_modules/@mrclrchtr/supi-core/src/config/config-settings.ts +188 -0
- package/node_modules/@mrclrchtr/supi-core/src/config/config.ts +207 -0
- package/node_modules/@mrclrchtr/supi-core/src/config.ts +12 -0
- package/node_modules/@mrclrchtr/supi-core/src/context/context-messages.ts +119 -0
- package/node_modules/@mrclrchtr/supi-core/src/context/context-provider-registry.ts +36 -0
- package/node_modules/@mrclrchtr/supi-core/src/context/context-tag.ts +31 -0
- package/node_modules/@mrclrchtr/supi-core/src/context.ts +16 -0
- package/node_modules/@mrclrchtr/supi-core/src/debug-registry.ts +255 -0
- package/node_modules/@mrclrchtr/supi-core/src/footer-registry.ts +57 -0
- package/node_modules/@mrclrchtr/supi-core/src/index.ts +40 -0
- package/node_modules/@mrclrchtr/supi-core/src/llm.ts +211 -0
- package/node_modules/@mrclrchtr/supi-core/src/model-selection.ts +134 -0
- package/node_modules/@mrclrchtr/supi-core/src/path-utils.ts +40 -0
- package/node_modules/@mrclrchtr/supi-core/src/path.ts +2 -0
- package/node_modules/@mrclrchtr/supi-core/src/progress-widget.ts +189 -0
- package/node_modules/@mrclrchtr/supi-core/src/project-roots.ts +170 -0
- package/node_modules/@mrclrchtr/supi-core/src/project.ts +15 -0
- package/node_modules/@mrclrchtr/supi-core/src/registry-utils.ts +93 -0
- package/node_modules/@mrclrchtr/supi-core/src/report.ts +121 -0
- package/node_modules/@mrclrchtr/supi-core/src/session-utils.ts +29 -0
- package/node_modules/@mrclrchtr/supi-core/src/session.ts +4 -0
- package/node_modules/@mrclrchtr/supi-core/src/settings/settings-command.ts +15 -0
- package/node_modules/@mrclrchtr/supi-core/src/settings/settings-registry.ts +48 -0
- package/node_modules/@mrclrchtr/supi-core/src/settings/settings-ui.ts +331 -0
- package/node_modules/@mrclrchtr/supi-core/src/settings-ui.ts +6 -0
- package/node_modules/@mrclrchtr/supi-core/src/settings.ts +9 -0
- package/node_modules/@mrclrchtr/supi-core/src/spinner-frames.ts +11 -0
- package/node_modules/@mrclrchtr/supi-core/src/status-spinner.ts +68 -0
- package/node_modules/@mrclrchtr/supi-core/src/substrate-types.ts +11 -0
- package/node_modules/@mrclrchtr/supi-core/src/terminal.ts +60 -0
- package/node_modules/@mrclrchtr/supi-core/src/tool-framework.ts +182 -0
- package/node_modules/@mrclrchtr/supi-core/src/types.ts +2 -0
- package/package.json +39 -0
- package/src/api.ts +8 -0
- package/src/config/config.ts +16 -0
- package/src/config/settings.ts +44 -0
- package/src/editor/editor.ts +138 -0
- package/src/extension.ts +26 -0
- package/src/generation/client.ts +122 -0
- package/src/generation/generator.ts +225 -0
- package/src/generation/model-resolution.ts +72 -0
- package/src/generation/normalize.ts +50 -0
- package/src/session.ts +168 -0
package/README.md
ADDED
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
<div align="center">
|
|
2
|
+
<h1>@mrclrchtr/supi-prompt-suggestions</h1>
|
|
3
|
+
</div>
|
|
4
|
+
|
|
5
|
+
# @mrclrchtr/supi-prompt-suggestions
|
|
6
|
+
|
|
7
|
+
Advisory ghost-text prompt suggestions for the [pi coding agent](https://github.com/earendil-works/pi).
|
|
8
|
+
|
|
9
|
+
**Experimental.** Disabled by default — no surprise background model calls.
|
|
10
|
+
|
|
11
|
+
## Install
|
|
12
|
+
|
|
13
|
+
```bash
|
|
14
|
+
pi install npm:@mrclrchtr/supi-prompt-suggestions
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
For local development:
|
|
18
|
+
|
|
19
|
+
```bash
|
|
20
|
+
pi install ./packages/supi-prompt-suggestions
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
## What you get
|
|
24
|
+
|
|
25
|
+
After install and configuration, suggested next user prompts appear as dim **ghost text** in the editor after each assistant response.
|
|
26
|
+
|
|
27
|
+
- **Right Arrow** — accept the suggestion without submitting
|
|
28
|
+
- **Escape** or **any keystroke** — dismiss the suggestion
|
|
29
|
+
|
|
30
|
+
Suggestions are generated by a cheap, fast model you choose. Only the last assistant text is sent — no transcript, files, or project data.
|
|
31
|
+
|
|
32
|
+
## Configuration
|
|
33
|
+
|
|
34
|
+
Settings live under `/supi-settings` → **Prompt suggestions**:
|
|
35
|
+
|
|
36
|
+
- **Suggestion model** — `disabled` (default) or a model from PI's scoped `enabledModels`
|
|
37
|
+
- Uses SuPi's normal scoped config: set a global default, override per project
|
|
38
|
+
|
|
39
|
+
## Privacy
|
|
40
|
+
|
|
41
|
+
- Only the last 8,000 characters of the final assistant message are sent
|
|
42
|
+
- The system prompt does not mention PI, SuPi, the project, or the conversation
|
|
43
|
+
- No tool outputs, file contents, or session metadata are included
|
|
44
|
+
|
|
45
|
+
## Package surfaces
|
|
46
|
+
|
|
47
|
+
- `@mrclrchtr/supi-prompt-suggestions/extension` — pi extension entrypoint
|
|
48
|
+
- `@mrclrchtr/supi-prompt-suggestions/api` — public config type: `PromptSuggestionsConfig`
|
|
49
|
+
|
|
50
|
+
## Source layout
|
|
51
|
+
|
|
52
|
+
- `src/extension.ts` — pi extension entrypoint: registers settings, wires events to `SessionLifecycle`
|
|
53
|
+
- `src/session.ts` — `SessionLifecycle` class: owns ghost editor, status spinner, and generation orchestration
|
|
54
|
+
- `src/api.ts` — package API surface (`./api` export)
|
|
55
|
+
- `src/index.ts` — package root re-export
|
|
56
|
+
- `src/config/config.ts` — config types and defaults
|
|
57
|
+
- `src/config/settings.ts` — settings registration + model picker submenu
|
|
58
|
+
- `src/editor/editor.ts` — ghost-text editor wrapper component
|
|
59
|
+
- `src/generation/generator.ts` — `SuggestionGenerator` class: async orchestration with concurrency control
|
|
60
|
+
- `src/generation/client.ts` — low-level model API client (pure functions)
|
|
61
|
+
- `src/generation/model-resolution.ts` — model + API key resolution from scoped config
|
|
62
|
+
- `src/generation/normalize.ts` — suggestion text normalization
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
<div align="center">
|
|
2
|
+
<a href="https://github.com/mrclrchtr/supi/tree/main/packages/supi-core">
|
|
3
|
+
<picture>
|
|
4
|
+
<img src="https://raw.githubusercontent.com/mrclrchtr/supi/main/packages/supi-core/assets/logo.png" alt="SuPi" width="50%">
|
|
5
|
+
</picture>
|
|
6
|
+
</a>
|
|
7
|
+
</div>
|
|
8
|
+
|
|
9
|
+
# @mrclrchtr/supi-core
|
|
10
|
+
|
|
11
|
+
Shared infrastructure for SuPi extensions.
|
|
12
|
+
|
|
13
|
+
This is a **pure library** — it does not register any pi commands or tools. The `/supi-settings` command is now available through `@mrclrchtr/supi-settings`.
|
|
14
|
+
|
|
15
|
+
## Install
|
|
16
|
+
|
|
17
|
+
```bash
|
|
18
|
+
pnpm add @mrclrchtr/supi-core
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
## Package surfaces
|
|
22
|
+
|
|
23
|
+
- `@mrclrchtr/supi-core/api` — reusable helpers for other packages and extensions
|
|
24
|
+
- `@mrclrchtr/supi-core/report` — shared text/report rendering helpers for TUI and plain-text summaries
|
|
25
|
+
|
|
26
|
+
## What you get from the API
|
|
27
|
+
|
|
28
|
+
### Config helpers
|
|
29
|
+
|
|
30
|
+
- `loadSupiConfig()` — merged config with resolution order `defaults <- global <- project`
|
|
31
|
+
- `loadSupiConfigForScope()` — load one scope at a time for settings UIs
|
|
32
|
+
- `writeSupiConfig()` — persist values
|
|
33
|
+
- `removeSupiConfigKey()` — remove a key or override
|
|
34
|
+
|
|
35
|
+
Config file locations:
|
|
36
|
+
|
|
37
|
+
- global: `~/.pi/agent/supi/config.json`
|
|
38
|
+
- project: `.pi/supi/config.json`
|
|
39
|
+
|
|
40
|
+
### Settings helpers
|
|
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
|
|
46
|
+
- `createInputSubmenu()` — helper for simple text-entry submenus
|
|
47
|
+
|
|
48
|
+
The built-in settings UI supports:
|
|
49
|
+
|
|
50
|
+
- project/global scope toggle
|
|
51
|
+
- grouped extension sections
|
|
52
|
+
- searchable setting lists
|
|
53
|
+
|
|
54
|
+
### Context helpers
|
|
55
|
+
|
|
56
|
+
- `wrapExtensionContext()` — wrap injected text in SuPi's `<extension-context>` tag
|
|
57
|
+
- `findLastUserMessageIndex()`
|
|
58
|
+
- `getContextToken()`
|
|
59
|
+
- `getPromptContent()`
|
|
60
|
+
- `pruneAndReorderContextMessages()`
|
|
61
|
+
- `restorePromptContent()`
|
|
62
|
+
|
|
63
|
+
### Shared registries
|
|
64
|
+
|
|
65
|
+
- context-provider registry for `/supi-context`
|
|
66
|
+
- debug-event registry for producers that want shared debug capture
|
|
67
|
+
- settings registry used by `/supi-settings`
|
|
68
|
+
|
|
69
|
+
### Project and session helpers
|
|
70
|
+
|
|
71
|
+
- project-root detection and directory walking helpers such as `findProjectRoot()` and `walkProject()`
|
|
72
|
+
- active-branch session helper: `getActiveBranchEntries()`
|
|
73
|
+
- terminal helpers such as `formatTitle()`, `signalWaiting()`, and `signalDone()`
|
|
74
|
+
|
|
75
|
+
### Report helpers
|
|
76
|
+
|
|
77
|
+
- `clampReportWidth()` — enforce a minimum readable report width
|
|
78
|
+
- `formatReportTitle()` / `formatSectionHeader()` — shared themed headers
|
|
79
|
+
- `formatDimLine()` / `formatKeyValueLine()` — common summary rows
|
|
80
|
+
- `formatOverflowHint()` — consistent preview-overflow hints
|
|
81
|
+
- `wrapReportText()` — ANSI-aware wrapped report blocks with optional indentation
|
|
82
|
+
|
|
83
|
+
## Example
|
|
84
|
+
|
|
85
|
+
```ts
|
|
86
|
+
import { loadSupiConfig, registerConfigSettings, wrapExtensionContext } from "@mrclrchtr/supi-core/api";
|
|
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
|
+
});
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
## Source
|
|
108
|
+
|
|
109
|
+
- `src/api.ts` — exported library surface
|
|
110
|
+
- `src/config.ts` — shared config loading and writing
|
|
111
|
+
- `src/config-settings.ts` — config-backed settings registration helper
|
|
112
|
+
- `src/settings-ui.ts` — shared settings overlay
|
|
113
|
+
- `src/report.ts` — shared text/report rendering helpers
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@mrclrchtr/supi-core",
|
|
3
|
+
"version": "1.16.1",
|
|
4
|
+
"description": "SuPi core — shared infrastructure for SuPi extensions (XML context tags, config system)",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"repository": {
|
|
7
|
+
"type": "git",
|
|
8
|
+
"url": "git+https://github.com/mrclrchtr/supi.git",
|
|
9
|
+
"directory": "packages/supi-core"
|
|
10
|
+
},
|
|
11
|
+
"homepage": "https://github.com/mrclrchtr/supi/tree/main/packages/supi-core#readme",
|
|
12
|
+
"bugs": {
|
|
13
|
+
"url": "https://github.com/mrclrchtr/supi/issues"
|
|
14
|
+
},
|
|
15
|
+
"publishConfig": {
|
|
16
|
+
"access": "public"
|
|
17
|
+
},
|
|
18
|
+
"keywords": [
|
|
19
|
+
"pi",
|
|
20
|
+
"pi-coding-agent",
|
|
21
|
+
"supi",
|
|
22
|
+
"extension-utils",
|
|
23
|
+
"settings",
|
|
24
|
+
"configuration"
|
|
25
|
+
],
|
|
26
|
+
"type": "module",
|
|
27
|
+
"files": [
|
|
28
|
+
"src/**/*.ts",
|
|
29
|
+
"!__tests__"
|
|
30
|
+
],
|
|
31
|
+
"peerDependencies": {
|
|
32
|
+
"@earendil-works/pi-ai": "*",
|
|
33
|
+
"@earendil-works/pi-coding-agent": "*",
|
|
34
|
+
"@earendil-works/pi-tui": "*",
|
|
35
|
+
"typebox": "*"
|
|
36
|
+
},
|
|
37
|
+
"peerDependenciesMeta": {
|
|
38
|
+
"@earendil-works/pi-ai": {
|
|
39
|
+
"optional": true
|
|
40
|
+
},
|
|
41
|
+
"@earendil-works/pi-coding-agent": {
|
|
42
|
+
"optional": true
|
|
43
|
+
},
|
|
44
|
+
"@earendil-works/pi-tui": {
|
|
45
|
+
"optional": true
|
|
46
|
+
},
|
|
47
|
+
"typebox": {
|
|
48
|
+
"optional": true
|
|
49
|
+
}
|
|
50
|
+
},
|
|
51
|
+
"devDependencies": {
|
|
52
|
+
"@types/node": "25.9.4",
|
|
53
|
+
"vitest": "4.1.9"
|
|
54
|
+
},
|
|
55
|
+
"main": "src/api.ts",
|
|
56
|
+
"exports": {
|
|
57
|
+
"./abort-utils": "./src/abort-utils.ts",
|
|
58
|
+
"./api": "./src/api.ts",
|
|
59
|
+
"./config": "./src/config.ts",
|
|
60
|
+
"./context": "./src/context.ts",
|
|
61
|
+
"./debug": "./src/debug-registry.ts",
|
|
62
|
+
"./footer-registry": "./src/footer-registry.ts",
|
|
63
|
+
"./llm": "./src/llm.ts",
|
|
64
|
+
"./model-selection": "./src/model-selection.ts",
|
|
65
|
+
"./package.json": "./package.json",
|
|
66
|
+
"./path": "./src/path.ts",
|
|
67
|
+
"./report": "./src/report.ts",
|
|
68
|
+
"./progress-widget": "./src/progress-widget.ts",
|
|
69
|
+
"./project": "./src/project.ts",
|
|
70
|
+
"./session": "./src/session.ts",
|
|
71
|
+
"./settings": "./src/settings.ts",
|
|
72
|
+
"./settings-ui": "./src/settings-ui.ts",
|
|
73
|
+
"./spinner-frames": "./src/spinner-frames.ts",
|
|
74
|
+
"./status-spinner": "./src/status-spinner.ts",
|
|
75
|
+
"./terminal": "./src/terminal.ts",
|
|
76
|
+
"./tool-framework": "./src/tool-framework.ts",
|
|
77
|
+
"./types": "./src/types.ts"
|
|
78
|
+
}
|
|
79
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Abort controller/signal utility helpers.
|
|
3
|
+
*
|
|
4
|
+
* @module
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Combine a caller-provided abort controller with a generation timeout signal.
|
|
9
|
+
*
|
|
10
|
+
* Returns the combined signal and a cleanup function that removes both
|
|
11
|
+
* event listeners. Callers must invoke cleanup in a `finally` block
|
|
12
|
+
* to avoid listener leaks.
|
|
13
|
+
*/
|
|
14
|
+
export function combineAbortSignals(
|
|
15
|
+
abort: AbortController,
|
|
16
|
+
timeoutMs: number,
|
|
17
|
+
): { signal: AbortSignal; cleanup: () => void } {
|
|
18
|
+
const timeoutSignal = AbortSignal.timeout(timeoutMs);
|
|
19
|
+
const combinedController = new AbortController();
|
|
20
|
+
const onAbort = () => combinedController.abort();
|
|
21
|
+
abort.signal.addEventListener("abort", onAbort, { once: true });
|
|
22
|
+
timeoutSignal.addEventListener("abort", onAbort, { once: true });
|
|
23
|
+
|
|
24
|
+
return {
|
|
25
|
+
signal: combinedController.signal,
|
|
26
|
+
cleanup: () => {
|
|
27
|
+
abort.signal.removeEventListener("abort", onAbort);
|
|
28
|
+
timeoutSignal.removeEventListener("abort", onAbort);
|
|
29
|
+
},
|
|
30
|
+
};
|
|
31
|
+
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
// supi-core — shared infrastructure for SuPi extensions.
|
|
2
|
+
// Provides XML context tag wrapping, unified config system, context-message utilities,
|
|
3
|
+
// settings registry for supi-wide TUI settings, and a shared tool-spec/registration framework.
|
|
4
|
+
//
|
|
5
|
+
// Convenience barrel — re-exports all domain entry points.
|
|
6
|
+
// For lighter imports, use one of the domain subpaths directly
|
|
7
|
+
// (e.g. @mrclrchtr/supi-core/config, @mrclrchtr/supi-core/context).
|
|
8
|
+
|
|
9
|
+
// biome-ignore lint/performance/noReExportAll: intentional convenience barrel
|
|
10
|
+
export * from "./abort-utils.ts";
|
|
11
|
+
// biome-ignore lint/performance/noReExportAll: intentional convenience barrel
|
|
12
|
+
export * from "./config.ts";
|
|
13
|
+
// biome-ignore lint/performance/noReExportAll: intentional convenience barrel
|
|
14
|
+
export * from "./context.ts";
|
|
15
|
+
// biome-ignore lint/performance/noReExportAll: intentional convenience barrel
|
|
16
|
+
export * from "./debug-registry.ts";
|
|
17
|
+
// biome-ignore lint/performance/noReExportAll: intentional convenience barrel
|
|
18
|
+
export * from "./footer-registry.ts";
|
|
19
|
+
// biome-ignore lint/performance/noReExportAll: intentional convenience barrel
|
|
20
|
+
export * from "./llm.ts";
|
|
21
|
+
// biome-ignore lint/performance/noReExportAll: intentional convenience barrel
|
|
22
|
+
export * from "./model-selection.ts";
|
|
23
|
+
// biome-ignore lint/performance/noReExportAll: intentional convenience barrel
|
|
24
|
+
export * from "./path.ts";
|
|
25
|
+
// biome-ignore lint/performance/noReExportAll: intentional convenience barrel
|
|
26
|
+
export * from "./project.ts";
|
|
27
|
+
// biome-ignore lint/performance/noReExportAll: intentional convenience barrel
|
|
28
|
+
export * from "./report.ts";
|
|
29
|
+
// biome-ignore lint/performance/noReExportAll: intentional convenience barrel
|
|
30
|
+
export * from "./session.ts";
|
|
31
|
+
// biome-ignore lint/performance/noReExportAll: intentional convenience barrel
|
|
32
|
+
export * from "./settings.ts";
|
|
33
|
+
// biome-ignore lint/performance/noReExportAll: intentional convenience barrel
|
|
34
|
+
export * from "./settings-ui.ts";
|
|
35
|
+
// biome-ignore lint/performance/noReExportAll: intentional convenience barrel
|
|
36
|
+
export * from "./spinner-frames.ts";
|
|
37
|
+
// biome-ignore lint/performance/noReExportAll: intentional convenience barrel
|
|
38
|
+
export * from "./status-spinner.ts";
|
|
39
|
+
// biome-ignore lint/performance/noReExportAll: intentional convenience barrel
|
|
40
|
+
export * from "./terminal.ts";
|
|
41
|
+
// biome-ignore lint/performance/noReExportAll: intentional convenience barrel
|
|
42
|
+
export * from "./tool-framework.ts";
|
|
43
|
+
// biome-ignore lint/performance/noReExportAll: intentional convenience barrel
|
|
44
|
+
export * from "./types.ts";
|
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
// Config-aware settings helper for SuPi config-backed settings sections.
|
|
2
|
+
// Wraps registerSettings() and centralizes selected-scope loading + scoped persistence.
|
|
3
|
+
//
|
|
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.
|
|
7
|
+
|
|
8
|
+
import type { ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
9
|
+
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";
|
|
12
|
+
import { loadSupiConfigForScope, removeSupiConfigKey, writeSupiConfig } from "./config.ts";
|
|
13
|
+
|
|
14
|
+
// ── Types ──────────────────────────────────────────────────────────────────
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Supported config value types for declarative persistChange.
|
|
18
|
+
*
|
|
19
|
+
* - `"boolean"`: maps "on" → true, "off" → false
|
|
20
|
+
* - `"number"`: parses integer via Number.parseInt, falls back to unset on invalid
|
|
21
|
+
* - `"stringList"`: splits on comma, trims whitespace, unsets on empty
|
|
22
|
+
*/
|
|
23
|
+
export type ConfigSettingType = "boolean" | "number" | "stringList";
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Extended setting item that can declare its config type for auto-generated
|
|
27
|
+
* persistence handling.
|
|
28
|
+
*/
|
|
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
|
+
*/
|
|
34
|
+
configType?: ConfigSettingType;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Helpers provided to the persistChange callback for writing or removing
|
|
39
|
+
* scoped config values.
|
|
40
|
+
*/
|
|
41
|
+
export interface ConfigSettingsHelpers {
|
|
42
|
+
/** Write a key to the selected scope's config section. */
|
|
43
|
+
set(key: string, value: unknown): void;
|
|
44
|
+
/** Remove a key from the selected scope's config section. */
|
|
45
|
+
unset(key: string): void;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export interface ConfigSettingsOptions<T> {
|
|
49
|
+
/** Extension identifier — e.g. "lsp", "claude-md" */
|
|
50
|
+
id: string;
|
|
51
|
+
/** Human-readable label shown in the UI */
|
|
52
|
+
label: string;
|
|
53
|
+
/** SuPi config section name — e.g. "lsp", "claude-md" */
|
|
54
|
+
section: string;
|
|
55
|
+
/** Default config values */
|
|
56
|
+
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
|
+
*/
|
|
64
|
+
buildItems: (
|
|
65
|
+
settings: T,
|
|
66
|
+
scope: SettingsScope,
|
|
67
|
+
cwd: string,
|
|
68
|
+
ctx?: ExtensionContext,
|
|
69
|
+
) => ConfigSettingItem[];
|
|
70
|
+
/**
|
|
71
|
+
* Handle a settings change with scoped persistence helpers.
|
|
72
|
+
*
|
|
73
|
+
* Optional when all items returned by `buildItems` declare a `configType`.
|
|
74
|
+
* Required when any item lacks a `configType`.
|
|
75
|
+
*/
|
|
76
|
+
persistChange?: (
|
|
77
|
+
scope: SettingsScope,
|
|
78
|
+
cwd: string,
|
|
79
|
+
settingId: string,
|
|
80
|
+
value: string,
|
|
81
|
+
helpers: ConfigSettingsHelpers,
|
|
82
|
+
) => void;
|
|
83
|
+
/** Optional home directory for config resolution (testing). */
|
|
84
|
+
homeDir?: string;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// ── Auto-generated persistChange ───────────────────────────────────────────
|
|
88
|
+
|
|
89
|
+
function autoPersistChange(
|
|
90
|
+
settingId: string,
|
|
91
|
+
value: string,
|
|
92
|
+
helpers: ConfigSettingsHelpers,
|
|
93
|
+
items: ConfigSettingItem[],
|
|
94
|
+
): void {
|
|
95
|
+
const item = items.find((i) => i.id === settingId);
|
|
96
|
+
if (!item?.configType) return;
|
|
97
|
+
|
|
98
|
+
switch (item.configType) {
|
|
99
|
+
case "boolean": {
|
|
100
|
+
helpers.set(settingId, value === "on");
|
|
101
|
+
break;
|
|
102
|
+
}
|
|
103
|
+
case "number": {
|
|
104
|
+
const num = Number.parseInt(value, 10);
|
|
105
|
+
if (Number.isFinite(num) && num > 0) {
|
|
106
|
+
helpers.set(settingId, num);
|
|
107
|
+
} else {
|
|
108
|
+
helpers.unset(settingId);
|
|
109
|
+
}
|
|
110
|
+
break;
|
|
111
|
+
}
|
|
112
|
+
case "stringList": {
|
|
113
|
+
const names = value
|
|
114
|
+
.split(",")
|
|
115
|
+
.map((s) => s.trim())
|
|
116
|
+
.filter((s) => s.length > 0);
|
|
117
|
+
if (names.length > 0) {
|
|
118
|
+
helpers.set(settingId, names);
|
|
119
|
+
} else {
|
|
120
|
+
helpers.unset(settingId);
|
|
121
|
+
}
|
|
122
|
+
break;
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
function areAllItemsDeclarative(items: ConfigSettingItem[]): boolean {
|
|
128
|
+
return items.length > 0 && items.every((i) => i.configType !== undefined);
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
// ── Registration ───────────────────────────────────────────────────────────
|
|
132
|
+
|
|
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
|
+
let cachedItems: ConfigSettingItem[] | undefined;
|
|
146
|
+
|
|
147
|
+
registerSettings({
|
|
148
|
+
id: options.id,
|
|
149
|
+
label: options.label,
|
|
150
|
+
loadValues: (scope, cwd, ctx) => {
|
|
151
|
+
const settings = loadSupiConfigForScope(options.section, cwd, options.defaults, {
|
|
152
|
+
scope,
|
|
153
|
+
homeDir: options.homeDir,
|
|
154
|
+
});
|
|
155
|
+
const items = options.buildItems(settings, scope, cwd, ctx);
|
|
156
|
+
cachedItems = items;
|
|
157
|
+
return items;
|
|
158
|
+
},
|
|
159
|
+
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
|
|
176
|
+
if (options.persistChange) {
|
|
177
|
+
options.persistChange(scope, cwd, settingId, value, helpers);
|
|
178
|
+
return;
|
|
179
|
+
}
|
|
180
|
+
|
|
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);
|
|
185
|
+
}
|
|
186
|
+
},
|
|
187
|
+
});
|
|
188
|
+
}
|