@mrclrchtr/supi-settings 4.8.0 → 4.10.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +64 -24
- package/node_modules/@mrclrchtr/supi-core/README.md +3 -1
- package/node_modules/@mrclrchtr/supi-core/package.json +1 -1
- package/node_modules/@mrclrchtr/supi-core/src/debug-registry.ts +8 -0
- package/node_modules/@mrclrchtr/supi-core/src/debug-timing.ts +120 -0
- package/package.json +2 -2
- package/src/ui/scoped-settings-list.ts +15 -13
- package/src/ui/settings-list-layout.ts +19 -0
- package/src/ui/settings-row-model.ts +16 -0
- package/src/ui/settings-ui.ts +12 -3
package/README.md
CHANGED
|
@@ -6,56 +6,96 @@
|
|
|
6
6
|
|
|
7
7
|
# @mrclrchtr/supi-settings
|
|
8
8
|
|
|
9
|
-
SuPi Settings
|
|
9
|
+
SuPi Settings is a Pi extension and a small public UI library. It adds one interactive screen for the project and global settings that loaded SuPi extensions contribute.
|
|
10
|
+
|
|
11
|
+
This package is part of the SuPi release stack.
|
|
10
12
|
|
|
11
13
|
## Install
|
|
12
14
|
|
|
15
|
+
Install the Pi extension:
|
|
16
|
+
|
|
13
17
|
```bash
|
|
14
18
|
pi install npm:@mrclrchtr/supi-settings
|
|
15
19
|
```
|
|
16
20
|
|
|
21
|
+
If Pi is open, run `/reload` after installation.
|
|
22
|
+
|
|
17
23
|
For local development:
|
|
18
24
|
|
|
19
25
|
```bash
|
|
20
26
|
pi install ./packages/supi-settings
|
|
21
27
|
```
|
|
22
28
|
|
|
23
|
-
## What
|
|
29
|
+
## What it adds
|
|
30
|
+
|
|
31
|
+
The extension registers one slash command:
|
|
32
|
+
|
|
33
|
+
- **`/supi-settings`** — open a searchable settings screen
|
|
34
|
+
|
|
35
|
+
The package does not register a tool, a shortcut, or its own setting. The screen contains settings from other loaded SuPi extensions. If no extension contributes settings, Pi shows `No settings registered by SuPi extensions`.
|
|
36
|
+
|
|
37
|
+
The screen is available in Pi interactive TUI mode. Print, JSON, and RPC modes do not provide this custom screen.
|
|
38
|
+
|
|
39
|
+
## Use the settings screen
|
|
40
|
+
|
|
41
|
+
The screen starts in project scope.
|
|
24
42
|
|
|
25
|
-
|
|
43
|
+
| Input | Action |
|
|
44
|
+
|---|---|
|
|
45
|
+
| Type text | Filter by section, setting, key, or displayed value |
|
|
46
|
+
| Up or Down | Move through the settings |
|
|
47
|
+
| Enter | Open the actions for the selected setting |
|
|
48
|
+
| Space | Cycle a boolean, enum, or fixed numeric choice |
|
|
49
|
+
| Tab | Switch between project and global scope |
|
|
50
|
+
| Escape | Close the current menu or the settings screen |
|
|
26
51
|
|
|
27
|
-
|
|
52
|
+
Each value can have a `(project)`, `(global)`, or `(default)` source badge.
|
|
28
53
|
|
|
29
|
-
|
|
54
|
+
In project scope, a project value overrides a global value or the package default. **Inherit from global** and **Use default** delete the project value. In global scope, **Reset to default** deletes the global value.
|
|
30
55
|
|
|
31
|
-
|
|
56
|
+
Free-text, list, model, and custom settings open a matching editor or picker when the contributing module provides one.
|
|
32
57
|
|
|
33
|
-
|
|
58
|
+
## Storage and boundaries
|
|
34
59
|
|
|
35
|
-
|
|
60
|
+
Each contributing settings module owns its reads, writes, validation, refresh behavior, and notices. The screen waits for a write to finish and then reads a new snapshot. A failure in one module does not hide settings from modules that loaded successfully.
|
|
36
61
|
|
|
37
|
-
|
|
62
|
+
Modules that use the standard `@mrclrchtr/supi-core` config adapter store values in:
|
|
38
63
|
|
|
39
|
-
|
|
64
|
+
- global: `~/.pi/agent/supi/config.json`
|
|
65
|
+
- project: `<cwd>/.pi/supi/config.json`
|
|
66
|
+
|
|
67
|
+
A custom module can use a different store. SuPi Settings does not call a model or a remote service itself, but a contributed module controls its own operations. Pi extensions run with the permissions of the Pi process.
|
|
68
|
+
|
|
69
|
+
## Public API
|
|
70
|
+
|
|
71
|
+
Add the package as a dependency when another extension needs the reusable UI helpers:
|
|
72
|
+
|
|
73
|
+
```bash
|
|
74
|
+
pnpm add @mrclrchtr/supi-settings
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
Import only from the explicit API subpath:
|
|
78
|
+
|
|
79
|
+
```ts
|
|
80
|
+
import {
|
|
81
|
+
createInputSubmenu,
|
|
82
|
+
createModelPickerSubmenu,
|
|
83
|
+
openSettingsOverlay,
|
|
84
|
+
} from "@mrclrchtr/supi-settings/api";
|
|
85
|
+
```
|
|
40
86
|
|
|
41
|
-
|
|
87
|
+
The API exports:
|
|
42
88
|
|
|
43
|
-
- `
|
|
44
|
-
- `
|
|
45
|
-
- `
|
|
46
|
-
- `supi-cache` — prompt-cache monitoring and history collection
|
|
47
|
-
- `supi-debug` — debug event capture and retention
|
|
48
|
-
- `supi-insights` — report-generation options
|
|
49
|
-
- `supi-skills` — skill load and model-invocation controls
|
|
89
|
+
- `openSettingsOverlay(pi, ctx)` — collect the current settings modules and open the screen.
|
|
90
|
+
- `createInputSubmenu(currentValue, label, done)` — create a free-text submenu with confirm and cancel handling.
|
|
91
|
+
- `createModelPickerSubmenu(currentValue, done, ctx?, options?)` — create a picker from static choices and models that match Pi's configured `enabledModels` patterns. The picker includes `disabled` by default.
|
|
50
92
|
|
|
51
|
-
|
|
93
|
+
The `@mrclrchtr/supi-settings/extension` subpath exports the Pi extension factory that registers `/supi-settings`.
|
|
52
94
|
|
|
53
|
-
|
|
54
|
-
- `@mrclrchtr/supi-settings/api` — settings UI and submenu helpers
|
|
95
|
+
Settings modules use `registerSettings()` and the `SettingsModule` types from `@mrclrchtr/supi-core/settings`. Their `read()` and `apply()` methods are asynchronous. The screen collects the modules when the command opens.
|
|
55
96
|
|
|
56
97
|
## Source layout
|
|
57
98
|
|
|
58
|
-
- `src/extension.ts` —
|
|
59
|
-
- `src/api.ts` —
|
|
99
|
+
- `src/extension.ts` — Pi extension entrypoint
|
|
100
|
+
- `src/api.ts` — public UI exports
|
|
60
101
|
- `src/ui/` — settings screen, scoped list, action menu, and submenus
|
|
61
|
-
- `@mrclrchtr/supi-core/settings` owns the registry, schema, scope resolution, and persistence
|
|
@@ -48,7 +48,7 @@ Config file locations:
|
|
|
48
48
|
### Shared registries
|
|
49
49
|
|
|
50
50
|
- context-provider registry for `/supi-context`
|
|
51
|
-
- debug-event registry for producers that want shared debug capture
|
|
51
|
+
- debug-event registry and monotonic phase timers for producers that want shared debug capture
|
|
52
52
|
- settings registry used by `/supi-settings`
|
|
53
53
|
|
|
54
54
|
### Project and session helpers
|
|
@@ -108,5 +108,7 @@ export default function myExtension(pi: ExtensionAPI) {
|
|
|
108
108
|
|
|
109
109
|
- `src/api.ts` — exported library surface
|
|
110
110
|
- `src/config.ts` — shared config loading and writing
|
|
111
|
+
- `src/debug-registry.ts` — Debug domain surface, event state, retention, redaction, listeners, and queries
|
|
112
|
+
- `src/debug-timing.ts` — monotonic total and phase timers for Debug Event Producers
|
|
111
113
|
- `src/settings/` — settings registry, schema, scope resolution, and persistence
|
|
112
114
|
- `src/report.ts` — shared text/report rendering helpers
|
|
@@ -4,6 +4,9 @@
|
|
|
4
4
|
// supi-debug extension owns policy/configuration and exposes events through a
|
|
5
5
|
// command/tool while this module stays dependency-free for producers.
|
|
6
6
|
|
|
7
|
+
// biome-ignore lint/performance/noReExportAll: preserve the stable debug domain entry point
|
|
8
|
+
export * from "./debug-timing.ts";
|
|
9
|
+
|
|
7
10
|
export type DebugLevel = "debug" | "info" | "warning" | "error";
|
|
8
11
|
export type DebugAgentAccess = "off" | "sanitized" | "raw";
|
|
9
12
|
export interface DebugRegistryConfig {
|
|
@@ -180,6 +183,11 @@ export function getDebugRegistryConfig(): DebugRegistryConfig {
|
|
|
180
183
|
return cloneConfig(getState().config);
|
|
181
184
|
}
|
|
182
185
|
|
|
186
|
+
/** Return whether the Debug Registry currently retains producer events. */
|
|
187
|
+
export function isDebugRegistryEnabled(): boolean {
|
|
188
|
+
return getState().config.enabled;
|
|
189
|
+
}
|
|
190
|
+
|
|
183
191
|
/** Best-effort redaction helper for data exposed through sanitized debug views. */
|
|
184
192
|
export function redactDebugData<T>(value: T): T {
|
|
185
193
|
return redactValue(value, 8) as T;
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
import { performance } from "node:perf_hooks";
|
|
2
|
+
import {
|
|
3
|
+
type DebugEvent,
|
|
4
|
+
type DebugEventInput,
|
|
5
|
+
isDebugRegistryEnabled,
|
|
6
|
+
recordDebugEvent,
|
|
7
|
+
} from "./debug-registry.ts";
|
|
8
|
+
|
|
9
|
+
/** Monotonic duration data added to a timed debug event. */
|
|
10
|
+
export interface DebugTiming {
|
|
11
|
+
readonly durationMs: number;
|
|
12
|
+
readonly phasesMs: Readonly<Record<string, number>>;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/** Debug event input whose data can receive the reserved `timing` field. */
|
|
16
|
+
export interface TimedDebugEventInput extends Omit<DebugEventInput, "data"> {
|
|
17
|
+
readonly data?: Readonly<Record<string, unknown>>;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/** Test seam for the monotonic clock used by a debug timer. */
|
|
21
|
+
export interface DebugTimerOptions {
|
|
22
|
+
readonly now?: () => number;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/** Lazy timed-event input that is not evaluated when Debug is disabled. */
|
|
26
|
+
export type TimedDebugEventFactory = () => TimedDebugEventInput;
|
|
27
|
+
|
|
28
|
+
/** One-shot debug timer with optional sequential phase measurements. */
|
|
29
|
+
export interface DebugTimer {
|
|
30
|
+
/** Whether this timer sampled Debug as enabled when it started. */
|
|
31
|
+
readonly enabled: boolean;
|
|
32
|
+
/** Finish the current phase and start the next unnamed interval. */
|
|
33
|
+
mark(phase: string): void;
|
|
34
|
+
/** Record one event. A second call returns `null` without recording another event. */
|
|
35
|
+
finish(
|
|
36
|
+
input: TimedDebugEventInput | TimedDebugEventFactory,
|
|
37
|
+
finalPhase?: string,
|
|
38
|
+
): DebugEvent | null;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Start a monotonic timer for one debug event.
|
|
43
|
+
*
|
|
44
|
+
* Each `mark(name)` stores the interval since the previous mark. `finish()`
|
|
45
|
+
* stores the total duration and can name the final interval. Repeated phase
|
|
46
|
+
* names are accumulated. Event data reserves the `timing` field. When Debug is
|
|
47
|
+
* disabled at start, this returns a no-op timer and does not read the clock.
|
|
48
|
+
* Pass a factory to `finish()` to avoid event-data construction when disabled.
|
|
49
|
+
* Clock, event-construction, and registry failures are isolated from the
|
|
50
|
+
* measured operation and make the timer a no-op.
|
|
51
|
+
*/
|
|
52
|
+
export function startDebugTimer(options: DebugTimerOptions = {}): DebugTimer {
|
|
53
|
+
if (!isDebugRegistryEnabled()) return DISABLED_DEBUG_TIMER;
|
|
54
|
+
const now = options.now ?? performance.now.bind(performance);
|
|
55
|
+
let startedAt: number;
|
|
56
|
+
try {
|
|
57
|
+
startedAt = now();
|
|
58
|
+
} catch {
|
|
59
|
+
return DISABLED_DEBUG_TIMER;
|
|
60
|
+
}
|
|
61
|
+
let previousAt = startedAt;
|
|
62
|
+
let finished = false;
|
|
63
|
+
let failed = false;
|
|
64
|
+
const phases = new Map<string, number>();
|
|
65
|
+
|
|
66
|
+
const markAt = (phase: string, current: number): void => {
|
|
67
|
+
const name = phase.trim();
|
|
68
|
+
if (!name) return;
|
|
69
|
+
phases.set(name, (phases.get(name) ?? 0) + Math.max(0, current - previousAt));
|
|
70
|
+
previousAt = current;
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
return {
|
|
74
|
+
enabled: true,
|
|
75
|
+
mark(phase) {
|
|
76
|
+
if (finished || failed) return;
|
|
77
|
+
try {
|
|
78
|
+
markAt(phase, now());
|
|
79
|
+
} catch {
|
|
80
|
+
failed = true;
|
|
81
|
+
}
|
|
82
|
+
},
|
|
83
|
+
finish(input, finalPhase) {
|
|
84
|
+
if (finished || failed) return null;
|
|
85
|
+
finished = true;
|
|
86
|
+
try {
|
|
87
|
+
if (!isDebugRegistryEnabled()) return null;
|
|
88
|
+
const completedAt = now();
|
|
89
|
+
if (finalPhase) markAt(finalPhase, completedAt);
|
|
90
|
+
const phasesMs = Object.fromEntries(
|
|
91
|
+
[...phases.entries()].map(([name, value]) => [name, duration(value)]),
|
|
92
|
+
);
|
|
93
|
+
const timing: DebugTiming = {
|
|
94
|
+
durationMs: duration(completedAt - startedAt),
|
|
95
|
+
phasesMs,
|
|
96
|
+
};
|
|
97
|
+
const eventInput = typeof input === "function" ? input() : input;
|
|
98
|
+
return recordDebugEvent({
|
|
99
|
+
...eventInput,
|
|
100
|
+
data: { ...eventInput.data, timing },
|
|
101
|
+
});
|
|
102
|
+
} catch {
|
|
103
|
+
return null;
|
|
104
|
+
}
|
|
105
|
+
},
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
const DISABLED_DEBUG_TIMER: DebugTimer = Object.freeze({
|
|
110
|
+
enabled: false,
|
|
111
|
+
mark() {},
|
|
112
|
+
finish() {
|
|
113
|
+
return null;
|
|
114
|
+
},
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
function duration(value: number): number {
|
|
118
|
+
if (!Number.isFinite(value)) return 0;
|
|
119
|
+
return Math.round(Math.max(0, value) * 10) / 10;
|
|
120
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mrclrchtr/supi-settings",
|
|
3
|
-
"version": "4.
|
|
3
|
+
"version": "4.10.0",
|
|
4
4
|
"description": "One project/global settings UI for SuPi packages",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|
|
@@ -30,7 +30,7 @@
|
|
|
30
30
|
"!__tests__"
|
|
31
31
|
],
|
|
32
32
|
"dependencies": {
|
|
33
|
-
"@mrclrchtr/supi-core": "4.
|
|
33
|
+
"@mrclrchtr/supi-core": "4.10.0"
|
|
34
34
|
},
|
|
35
35
|
"bundledDependencies": [
|
|
36
36
|
"@mrclrchtr/supi-core"
|
|
@@ -1,14 +1,7 @@
|
|
|
1
1
|
// Source-aware settings list with scope actions and custom submenus.
|
|
2
2
|
|
|
3
3
|
import type { ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
4
|
-
import {
|
|
5
|
-
Input,
|
|
6
|
-
Key,
|
|
7
|
-
matchesKey,
|
|
8
|
-
truncateToWidth,
|
|
9
|
-
visibleWidth,
|
|
10
|
-
wrapTextWithAnsi,
|
|
11
|
-
} from "@earendil-works/pi-tui";
|
|
4
|
+
import { Input, Key, matchesKey, truncateToWidth, visibleWidth } from "@earendil-works/pi-tui";
|
|
12
5
|
import type { SettingsAction, SettingsModule, SettingsScope } from "@mrclrchtr/supi-core/settings";
|
|
13
6
|
import type { ThemeAccessor } from "./settings-action-menu.ts";
|
|
14
7
|
import {
|
|
@@ -16,8 +9,14 @@ import {
|
|
|
16
9
|
createActionMenuComponent,
|
|
17
10
|
getConcreteChoices,
|
|
18
11
|
} from "./settings-action-menu.ts";
|
|
12
|
+
import { renderDescriptionViewport } from "./settings-list-layout.ts";
|
|
19
13
|
import { type LoadedSettingsModule, readSettingsModules } from "./settings-module-reader.ts";
|
|
20
|
-
import {
|
|
14
|
+
import {
|
|
15
|
+
filterSettingsRows,
|
|
16
|
+
maxVisibleSectionCount,
|
|
17
|
+
rowsFromModules,
|
|
18
|
+
type ScopedRow,
|
|
19
|
+
} from "./settings-row-model.ts";
|
|
21
20
|
import { createInputSubmenu, createModelPickerSubmenu } from "./settings-submenus.ts";
|
|
22
21
|
|
|
23
22
|
interface SubmenuState {
|
|
@@ -27,7 +26,6 @@ interface SubmenuState {
|
|
|
27
26
|
handleInput?: (data: string) => void;
|
|
28
27
|
};
|
|
29
28
|
}
|
|
30
|
-
|
|
31
29
|
export class ScopedSettingsList {
|
|
32
30
|
private modules: SettingsModule[];
|
|
33
31
|
private scope: SettingsScope;
|
|
@@ -139,6 +137,7 @@ export class ScopedSettingsList {
|
|
|
139
137
|
Math.max(...displayRows.map((row) => visibleWidth(row.field.field.label))),
|
|
140
138
|
);
|
|
141
139
|
let previousSection: string | undefined;
|
|
140
|
+
let visibleSectionCount = 0;
|
|
142
141
|
for (let i = start; i < end; i++) {
|
|
143
142
|
const row = displayRows[i];
|
|
144
143
|
if (!row) continue;
|
|
@@ -146,6 +145,7 @@ export class ScopedSettingsList {
|
|
|
146
145
|
lines.push(
|
|
147
146
|
truncateToWidth(` ${this.theme.fg("muted", this.theme.bold(row.moduleLabel))}`, width),
|
|
148
147
|
);
|
|
148
|
+
visibleSectionCount++;
|
|
149
149
|
previousSection = row.moduleLabel;
|
|
150
150
|
}
|
|
151
151
|
const isSelected = i === this.selectedIndex;
|
|
@@ -159,14 +159,16 @@ export class ScopedSettingsList {
|
|
|
159
159
|
const valueText = this.theme.fg(isSelected ? "accent" : "muted", value);
|
|
160
160
|
lines.push(truncateToWidth(`${prefix}${labelText} ${valueText}`, width));
|
|
161
161
|
}
|
|
162
|
+
const reservedSectionCount = maxVisibleSectionCount(displayRows, maxVisible);
|
|
163
|
+
for (let i = visibleSectionCount; i < reservedSectionCount; i++) lines.push("");
|
|
162
164
|
if (start > 0 || end < displayRows.length) {
|
|
163
165
|
lines.push(this.theme.fg("dim", ` (${this.selectedIndex + 1}/${displayRows.length})`));
|
|
164
166
|
}
|
|
165
167
|
const description = displayRows[this.selectedIndex]?.field.field.description;
|
|
166
|
-
if (description) {
|
|
168
|
+
if (displayRows.some((row) => row.field.field.description)) {
|
|
167
169
|
lines.push("");
|
|
168
|
-
for (const line of
|
|
169
|
-
lines.push(this.theme.fg("dim", ` ${line}`));
|
|
170
|
+
for (const line of renderDescriptionViewport(description, width)) {
|
|
171
|
+
lines.push(line ? this.theme.fg("dim", ` ${line}`) : "");
|
|
170
172
|
}
|
|
171
173
|
}
|
|
172
174
|
lines.push("");
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { truncateToWidth, wrapTextWithAnsi } from "@earendil-works/pi-tui";
|
|
2
|
+
|
|
3
|
+
const DESCRIPTION_VIEWPORT_HEIGHT = 4;
|
|
4
|
+
|
|
5
|
+
/** Render a fixed-height description preview so selection changes do not resize the menu. */
|
|
6
|
+
export function renderDescriptionViewport(
|
|
7
|
+
description: string | undefined,
|
|
8
|
+
width: number,
|
|
9
|
+
): string[] {
|
|
10
|
+
const contentWidth = Math.max(1, width - 4);
|
|
11
|
+
const wrapped = description ? wrapTextWithAnsi(description, contentWidth) : [];
|
|
12
|
+
const visible = wrapped.slice(0, DESCRIPTION_VIEWPORT_HEIGHT);
|
|
13
|
+
if (wrapped.length > DESCRIPTION_VIEWPORT_HEIGHT) {
|
|
14
|
+
const lastIndex = DESCRIPTION_VIEWPORT_HEIGHT - 1;
|
|
15
|
+
visible[lastIndex] = truncateToWidth(`${visible[lastIndex] ?? ""}…`, contentWidth, "…");
|
|
16
|
+
}
|
|
17
|
+
while (visible.length < DESCRIPTION_VIEWPORT_HEIGHT) visible.push("");
|
|
18
|
+
return visible;
|
|
19
|
+
}
|
|
@@ -17,6 +17,22 @@ export function rowsFromModules(loaded: LoadedSettingsModule[]): ScopedRow[] {
|
|
|
17
17
|
);
|
|
18
18
|
}
|
|
19
19
|
|
|
20
|
+
/** Return the largest section-header count for any row window of the given size. */
|
|
21
|
+
export function maxVisibleSectionCount(rows: ScopedRow[], windowSize: number): number {
|
|
22
|
+
let maximum = 0;
|
|
23
|
+
for (let start = 0; start <= rows.length - windowSize; start++) {
|
|
24
|
+
let count = 0;
|
|
25
|
+
let previousSection: string | undefined;
|
|
26
|
+
for (const row of rows.slice(start, start + windowSize)) {
|
|
27
|
+
if (row.moduleLabel === previousSection) continue;
|
|
28
|
+
count++;
|
|
29
|
+
previousSection = row.moduleLabel;
|
|
30
|
+
}
|
|
31
|
+
maximum = Math.max(maximum, count);
|
|
32
|
+
}
|
|
33
|
+
return maximum;
|
|
34
|
+
}
|
|
35
|
+
|
|
20
36
|
export function filterSettingsRows(rows: ScopedRow[], query: string): ScopedRow[] {
|
|
21
37
|
if (!query) return rows;
|
|
22
38
|
const normalized = query.toLowerCase();
|
package/src/ui/settings-ui.ts
CHANGED
|
@@ -9,6 +9,7 @@ import { Container, Key, matchesKey, Text } from "@earendil-works/pi-tui";
|
|
|
9
9
|
import {
|
|
10
10
|
createSettingsContributionCollector,
|
|
11
11
|
type SettingsCollectionDiagnostic,
|
|
12
|
+
type SettingsModule,
|
|
12
13
|
type SettingsScope,
|
|
13
14
|
SUPI_SETTINGS_COLLECT_EVENT,
|
|
14
15
|
} from "@mrclrchtr/supi-core/settings";
|
|
@@ -37,14 +38,22 @@ function latestStatus(diagnostics: SettingsCollectionDiagnostic[]): OverlayStatu
|
|
|
37
38
|
return latest ? { kind: latest.kind, message: latest.message } : undefined;
|
|
38
39
|
}
|
|
39
40
|
|
|
41
|
+
/** Keep the skill catalog after the smaller fixed settings sections. */
|
|
42
|
+
function orderSettingsModules(modules: SettingsModule[]): SettingsModule[] {
|
|
43
|
+
const skills = modules.find((module) => module.id === "skills");
|
|
44
|
+
if (!skills) return modules;
|
|
45
|
+
return [...modules.filter((module) => module.id !== skills.id), skills];
|
|
46
|
+
}
|
|
47
|
+
|
|
40
48
|
export async function openSettingsOverlay(pi: ExtensionAPI, ctx: ExtensionContext): Promise<void> {
|
|
41
49
|
const collection = collectSettingsModules(pi);
|
|
42
|
-
|
|
50
|
+
const modules = orderSettingsModules(collection.modules);
|
|
51
|
+
if (modules.length === 0) {
|
|
43
52
|
ctx.ui.notify("No settings registered by SuPi extensions", "info");
|
|
44
53
|
return;
|
|
45
54
|
}
|
|
46
55
|
|
|
47
|
-
const initial = await readSettingsModules(
|
|
56
|
+
const initial = await readSettingsModules(modules, {
|
|
48
57
|
scope: "project",
|
|
49
58
|
cwd: ctx.cwd,
|
|
50
59
|
ctx,
|
|
@@ -61,7 +70,7 @@ export async function openSettingsOverlay(pi: ExtensionAPI, ctx: ExtensionContex
|
|
|
61
70
|
|
|
62
71
|
const container = new Container();
|
|
63
72
|
const scopedList = new ScopedSettingsList(
|
|
64
|
-
|
|
73
|
+
modules,
|
|
65
74
|
initial.loaded,
|
|
66
75
|
state.scope,
|
|
67
76
|
state.cwd,
|