@mrclrchtr/supi-extras 8.0.1 → 9.0.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 +35 -4
- package/node_modules/@mrclrchtr/supi-core/package.json +1 -1
- package/node_modules/@mrclrchtr/supi-core/src/debug-registry.ts +9 -4
- package/node_modules/@mrclrchtr/supi-core/src/session-capabilities.ts +55 -1
- package/node_modules/@mrclrchtr/supi-core/src/session-capability-profiles.ts +240 -0
- package/node_modules/@mrclrchtr/supi-core/src/session.ts +22 -0
- package/package.json +2 -2
- package/src/session-capabilities-selector.ts +67 -0
- package/src/session-capabilities-tools.ts +0 -1
- package/src/session-capabilities-ui-rows.ts +16 -7
- package/src/session-capabilities-ui.ts +56 -22
- package/src/session-capabilities.ts +97 -48
- package/src/session-capability-profile-store.ts +176 -0
- package/src/session-capability-profile-ui.ts +284 -0
package/README.md
CHANGED
|
@@ -32,16 +32,47 @@ pi install npm:@mrclrchtr/supi-extras
|
|
|
32
32
|
`/supi-capabilities` opens a searchable selector. Use it to change extension tool and skill visibility for the current session.
|
|
33
33
|
|
|
34
34
|
- The selector lists only extension tools that were active at startup. It does not list PI built-in tools or tools supplied by integrations.
|
|
35
|
-
- Tool and skill overrides are stored in the session. Resume restores them. Fork and `/clone-session` copy them.
|
|
35
|
+
- Tool and skill overrides are stored in the session. Resume restores them. Fork and `/clone-session` copy them. New sessions use the optional default profile; without a default, they start without overrides.
|
|
36
36
|
- Tree navigation keeps explicit capability tool choices and does not change other tool choices.
|
|
37
|
-
-
|
|
38
|
-
- Saved tool
|
|
37
|
+
- The selector waits until the agent is idle before it opens. It does not interrupt a running tool call.
|
|
38
|
+
- Saved tool and skill exclusions remain stored when an item is absent. They apply if the item becomes eligible later.
|
|
39
39
|
- Skill changes affect the model skill catalog only. Explicit `/skill:name` commands and `$name` shortcuts stay available.
|
|
40
40
|
- Use **Reset Tools**, **Reset Skills**, or **Reset All** to remove overrides.
|
|
41
|
-
- The footer shows
|
|
41
|
+
- The footer shows an override count and the applied profile name. `Modified` means session exclusions differ from the copy captured when the profile was applied.
|
|
42
42
|
|
|
43
43
|
Install [`@mrclrchtr/supi-skills`](https://github.com/mrclrchtr/supi/tree/main/packages/supi-skills) to manage skills. Tool controls work without that package.
|
|
44
44
|
|
|
45
|
+
### Session Capability Profiles
|
|
46
|
+
|
|
47
|
+
Use Tab to open **Profiles** in `/supi-capabilities`. Apply a saved profile, save current exclusions under a new name, replace or delete a saved profile, or choose a default. Saving requires an explicit global or project scope. Replacement and deletion require confirmation.
|
|
48
|
+
|
|
49
|
+
Profiles contain **exclusions**, not a fixed list of available capabilities. New eligible tools and skills remain available unless their exact names are excluded. Package rows select current tools only; they do not create rules for future tools. Profiles do not change the model, instructions, child Agent Profiles, or security permissions.
|
|
50
|
+
|
|
51
|
+
Store definitions in `~/.pi/agent/supi/config.json` or a trusted project's `.pi/supi/config.json`:
|
|
52
|
+
|
|
53
|
+
```json
|
|
54
|
+
{
|
|
55
|
+
"sessionCapabilities": {
|
|
56
|
+
"profiles": {
|
|
57
|
+
"research": {
|
|
58
|
+
"excludedTools": ["agent_run"],
|
|
59
|
+
"hiddenSkills": ["implement"]
|
|
60
|
+
}
|
|
61
|
+
},
|
|
62
|
+
"defaultProfile": "research"
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
- Profile IDs contain lowercase words separated by optional hyphens. Omitted exclusion lists are empty. Unknown fields and invalid values produce errors.
|
|
68
|
+
- Project profiles and defaults require Pi project trust. A project definition replaces the complete global definition with the same ID. An invalid project definition blocks that ID; it does not use the global definition instead.
|
|
69
|
+
- An absent project `defaultProfile` inherits the global default. Set it to `null` to use no default in that project. Missing or invalid defaults produce a warning and normal startup without profile exclusions.
|
|
70
|
+
- Applying a profile replaces both session selections and stores a separate session copy. Later configuration edits, deletion, and reload do not change that copy. Resume and fork do not apply a new default.
|
|
71
|
+
- Saving a profile does not apply it. Apply it explicitly to use its current saved definition.
|
|
72
|
+
- Deletion leaves default references unchanged. Deleting a project definition can expose a same-ID global definition; the confirmation explains this effect.
|
|
73
|
+
- **Reset All** clears session exclusions and profile identity. It does not change saved profiles or defaults, and it does not reapply the default in the current session.
|
|
74
|
+
- Manual profile management is TUI-only. Configured startup defaults also work in non-interactive modes.
|
|
75
|
+
|
|
45
76
|
## Shortcuts
|
|
46
77
|
|
|
47
78
|
- `Alt+S` — stash the current editor text
|
|
@@ -6,6 +6,9 @@
|
|
|
6
6
|
|
|
7
7
|
export type DebugLevel = "debug" | "info" | "warning" | "error";
|
|
8
8
|
export type DebugAgentAccess = "off" | "sanitized" | "raw";
|
|
9
|
+
|
|
10
|
+
/** Custom session-entry type used to persist sanitized debug events. */
|
|
11
|
+
export const DEBUG_EVENT_ENTRY_TYPE = "supi-debug-event";
|
|
9
12
|
export interface DebugRegistryConfig {
|
|
10
13
|
/** Whether producers should retain debug events. */
|
|
11
14
|
enabled: boolean;
|
|
@@ -212,7 +215,7 @@ function toSanitizedView(event: DebugEvent): DebugEventView {
|
|
|
212
215
|
category: event.category,
|
|
213
216
|
message: event.message,
|
|
214
217
|
cwd: event.cwd,
|
|
215
|
-
data: event.data,
|
|
218
|
+
data: event.data === undefined ? undefined : redactDebugData(event.data),
|
|
216
219
|
};
|
|
217
220
|
}
|
|
218
221
|
|
|
@@ -241,15 +244,17 @@ export function recordDebugEvent(input: DebugEventInput): DebugEvent | null {
|
|
|
241
244
|
};
|
|
242
245
|
state.events.push(event);
|
|
243
246
|
trimToMaxEvents(state);
|
|
244
|
-
const view = toSanitizedView(event);
|
|
245
247
|
for (const listener of state.listeners) {
|
|
246
248
|
try {
|
|
247
|
-
listener(
|
|
249
|
+
listener(toSanitizedView(event));
|
|
248
250
|
} catch {
|
|
249
251
|
// Debug-event consumers must not alter producer behavior.
|
|
250
252
|
}
|
|
251
253
|
}
|
|
252
|
-
return {
|
|
254
|
+
return {
|
|
255
|
+
...event,
|
|
256
|
+
data: event.data === undefined ? undefined : redactDebugData(event.data),
|
|
257
|
+
};
|
|
253
258
|
}
|
|
254
259
|
|
|
255
260
|
/** Query retained debug events newest-first. Results are sanitized unless raw access is requested and allowed. */
|
|
@@ -1,3 +1,9 @@
|
|
|
1
|
+
import {
|
|
2
|
+
decodeSessionCapabilityProfileSnapshot,
|
|
3
|
+
type SessionCapabilityProfile,
|
|
4
|
+
type SessionCapabilityProfileSnapshot,
|
|
5
|
+
} from "./session-capability-profiles.ts";
|
|
6
|
+
|
|
1
7
|
/** Session-only tool and skill visibility state. */
|
|
2
8
|
export interface SessionCapabilityState {
|
|
3
9
|
version: 1;
|
|
@@ -7,6 +13,8 @@ export interface SessionCapabilityState {
|
|
|
7
13
|
/** Tools that the user explicitly enabled for this session. */
|
|
8
14
|
toolEnabledNames?: string[];
|
|
9
15
|
hiddenSkillNames: string[];
|
|
16
|
+
/** Original profile identity and exclusions. Later configuration changes do not affect this copy. */
|
|
17
|
+
profileSnapshot?: SessionCapabilityProfileSnapshot;
|
|
10
18
|
}
|
|
11
19
|
|
|
12
20
|
/** A model-visible skill that a provider can offer to the selector. */
|
|
@@ -94,8 +102,9 @@ export function decodeSessionCapabilities(value: unknown): SessionCapabilityStat
|
|
|
94
102
|
(name) => !initiallyInactive.has(name),
|
|
95
103
|
);
|
|
96
104
|
const eligible = new Set(eligibleToolNames);
|
|
97
|
-
const toolDenylist = stringList(record.toolDenylist)
|
|
105
|
+
const toolDenylist = stringList(record.toolDenylist);
|
|
98
106
|
const denied = new Set(toolDenylist);
|
|
107
|
+
const profileSnapshot = decodeSessionCapabilityProfileSnapshot(record.profileSnapshot);
|
|
99
108
|
|
|
100
109
|
return {
|
|
101
110
|
version: 1,
|
|
@@ -106,9 +115,54 @@ export function decodeSessionCapabilities(value: unknown): SessionCapabilityStat
|
|
|
106
115
|
(name) => eligible.has(name) && !denied.has(name),
|
|
107
116
|
),
|
|
108
117
|
hiddenSkillNames: stringList(record.hiddenSkillNames),
|
|
118
|
+
...(profileSnapshot ? { profileSnapshot } : {}),
|
|
119
|
+
};
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/** Copy current exclusion sets for saving. Do not use inventory to remove names. */
|
|
123
|
+
export function captureSessionCapabilityExclusions(
|
|
124
|
+
state: SessionCapabilityState,
|
|
125
|
+
): SessionCapabilityProfile {
|
|
126
|
+
return {
|
|
127
|
+
excludedTools: [...new Set(state.toolDenylist)],
|
|
128
|
+
hiddenSkills: [...new Set(state.hiddenSkillNames)],
|
|
109
129
|
};
|
|
110
130
|
}
|
|
111
131
|
|
|
132
|
+
/**
|
|
133
|
+
* Replace exclusions and capture a profile copy. Return undefined for an invalid profile.
|
|
134
|
+
* Do not mutate the input state or activate tools. The caller owns runtime restoration.
|
|
135
|
+
*/
|
|
136
|
+
export function applySessionCapabilityProfile(
|
|
137
|
+
state: SessionCapabilityState,
|
|
138
|
+
profile: unknown,
|
|
139
|
+
): SessionCapabilityState | undefined {
|
|
140
|
+
const profileSnapshot = decodeSessionCapabilityProfileSnapshot(profile);
|
|
141
|
+
if (!profileSnapshot) return undefined;
|
|
142
|
+
return decodeSessionCapabilities({
|
|
143
|
+
...state,
|
|
144
|
+
toolDenylist: profileSnapshot.excludedTools,
|
|
145
|
+
hiddenSkillNames: profileSnapshot.hiddenSkills,
|
|
146
|
+
profileSnapshot,
|
|
147
|
+
});
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
function equalNameSets(left: readonly string[], right: readonly string[]): boolean {
|
|
151
|
+
const leftSet = new Set(left);
|
|
152
|
+
const rightSet = new Set(right);
|
|
153
|
+
return leftSet.size === rightSet.size && [...leftSet].every((name) => rightSet.has(name));
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/** Compare current exclusions with the applied copy. No profile means no Modified marker. */
|
|
157
|
+
export function isSessionCapabilityProfileModified(state: SessionCapabilityState): boolean {
|
|
158
|
+
const snapshot = state.profileSnapshot;
|
|
159
|
+
return (
|
|
160
|
+
snapshot !== undefined &&
|
|
161
|
+
(!equalNameSets(state.toolDenylist, snapshot.excludedTools) ||
|
|
162
|
+
!equalNameSets(state.hiddenSkillNames, snapshot.hiddenSkills))
|
|
163
|
+
);
|
|
164
|
+
}
|
|
165
|
+
|
|
112
166
|
/** Return a safe copy of the latest valid snapshot in all session entries. */
|
|
113
167
|
export function findSessionCapabilities(
|
|
114
168
|
entries: readonly unknown[],
|
|
@@ -0,0 +1,240 @@
|
|
|
1
|
+
/** Exact names to exclude. Omitted configuration lists mean empty sets. */
|
|
2
|
+
export interface SessionCapabilityProfileDefinition {
|
|
3
|
+
excludedTools?: string[];
|
|
4
|
+
hiddenSkills?: string[];
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
/** Validated exclusion sets. Names need not match the current inventory. */
|
|
8
|
+
export interface SessionCapabilityProfile {
|
|
9
|
+
excludedTools: string[];
|
|
10
|
+
hiddenSkills: string[];
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/** A profile is available only when validation has no errors. */
|
|
14
|
+
export interface SessionCapabilityProfileValidation {
|
|
15
|
+
profile?: SessionCapabilityProfile;
|
|
16
|
+
errors: string[];
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
20
|
+
if (typeof value !== "object" || value === null || Array.isArray(value)) return false;
|
|
21
|
+
const prototype = Object.getPrototypeOf(value);
|
|
22
|
+
return prototype === Object.prototype || prototype === null;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function readNames(value: unknown): string[] | undefined {
|
|
26
|
+
if (!Array.isArray(value)) return undefined;
|
|
27
|
+
const names: unknown[] = [...new Set(value)];
|
|
28
|
+
return names.every((name): name is string => typeof name === "string") ? names : undefined;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/** Validate and copy a complete definition. Do not check inventory or expand patterns. */
|
|
32
|
+
export function validateSessionCapabilityProfile(
|
|
33
|
+
value: unknown,
|
|
34
|
+
): SessionCapabilityProfileValidation {
|
|
35
|
+
if (!isRecord(value)) return { errors: ["Profile must be an object."] };
|
|
36
|
+
const excludedTools = Object.hasOwn(value, "excludedTools") ? readNames(value.excludedTools) : [];
|
|
37
|
+
const hiddenSkills = Object.hasOwn(value, "hiddenSkills") ? readNames(value.hiddenSkills) : [];
|
|
38
|
+
const errors = Object.keys(value)
|
|
39
|
+
.filter((key) => key !== "excludedTools" && key !== "hiddenSkills")
|
|
40
|
+
.map((key) => `Unknown profile field: ${key}.`);
|
|
41
|
+
if (!excludedTools) errors.push("excludedTools must be an array of strings.");
|
|
42
|
+
if (!hiddenSkills) errors.push("hiddenSkills must be an array of strings.");
|
|
43
|
+
if (errors.length || !excludedTools || !hiddenSkills) return { errors };
|
|
44
|
+
return { profile: { excludedTools, hiddenSkills }, errors };
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/** Configuration scope that supplied a profile or default. */
|
|
48
|
+
export type SessionCapabilityProfileScope = "global" | "project";
|
|
49
|
+
|
|
50
|
+
/** Configuration values for the sessionCapabilities section. */
|
|
51
|
+
export interface SessionCapabilityProfilesSection {
|
|
52
|
+
profiles?: Record<string, SessionCapabilityProfileDefinition>;
|
|
53
|
+
defaultProfile?: string | null;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/** Session-owned identity and original exclusion sets, independent of configuration. */
|
|
57
|
+
export interface SessionCapabilityProfileSnapshot extends SessionCapabilityProfile {
|
|
58
|
+
id: string;
|
|
59
|
+
source: SessionCapabilityProfileScope;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/** A configuration error with a scope and field path for display. */
|
|
63
|
+
export interface SessionCapabilityProfileDiagnostic {
|
|
64
|
+
scope: SessionCapabilityProfileScope;
|
|
65
|
+
path: string;
|
|
66
|
+
message: string;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/** A scoped definition, including invalid definitions and hidden global definitions. */
|
|
70
|
+
export interface SessionCapabilityProfileEntry {
|
|
71
|
+
id: string;
|
|
72
|
+
source: SessionCapabilityProfileScope;
|
|
73
|
+
shadowed: boolean;
|
|
74
|
+
profile?: SessionCapabilityProfile;
|
|
75
|
+
errors: SessionCapabilityProfileDiagnostic[];
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/** Default selection source can differ from the effective profile source. */
|
|
79
|
+
export type SessionCapabilityProfileDefault =
|
|
80
|
+
| { status: "none"; source?: SessionCapabilityProfileScope }
|
|
81
|
+
| {
|
|
82
|
+
status: "resolved";
|
|
83
|
+
source: SessionCapabilityProfileScope;
|
|
84
|
+
id: string;
|
|
85
|
+
profile: SessionCapabilityProfileSnapshot;
|
|
86
|
+
}
|
|
87
|
+
| { status: "invalid"; source: SessionCapabilityProfileScope; id?: string };
|
|
88
|
+
|
|
89
|
+
/** Raw sections, not merged configuration. Callers must check trust before project reads. */
|
|
90
|
+
export interface ResolveSessionCapabilityProfilesOptions {
|
|
91
|
+
global?: unknown;
|
|
92
|
+
project?: unknown;
|
|
93
|
+
projectTrusted: boolean;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/** Data for profile rows, scoped actions, and default warnings. No I/O is performed. */
|
|
97
|
+
export interface SessionCapabilityProfilesResolution {
|
|
98
|
+
entries: SessionCapabilityProfileEntry[];
|
|
99
|
+
effectiveProfiles: SessionCapabilityProfileSnapshot[];
|
|
100
|
+
default: SessionCapabilityProfileDefault;
|
|
101
|
+
errors: SessionCapabilityProfileDiagnostic[];
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/** IDs contain lowercase ASCII words separated by single hyphens. */
|
|
105
|
+
export function isSessionCapabilityProfileId(value: unknown): value is string {
|
|
106
|
+
return typeof value === "string" && /^[a-z]+(?:-[a-z]+)*$/.test(value);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/** Validate and copy stored identity and original lists. Invalid metadata has no identity. */
|
|
110
|
+
export function decodeSessionCapabilityProfileSnapshot(
|
|
111
|
+
value: unknown,
|
|
112
|
+
): SessionCapabilityProfileSnapshot | undefined {
|
|
113
|
+
if (!isRecord(value) || !isSessionCapabilityProfileId(value.id)) return undefined;
|
|
114
|
+
if (value.source !== "global" && value.source !== "project") return undefined;
|
|
115
|
+
const { id, source, ...definition } = value;
|
|
116
|
+
if (!Object.hasOwn(definition, "excludedTools") || !Object.hasOwn(definition, "hiddenSkills"))
|
|
117
|
+
return undefined;
|
|
118
|
+
const result = validateSessionCapabilityProfile(definition);
|
|
119
|
+
return result.profile ? { id, source, ...result.profile } : undefined;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
function readEntries(
|
|
123
|
+
value: unknown,
|
|
124
|
+
source: SessionCapabilityProfileScope,
|
|
125
|
+
): SessionCapabilityProfileEntry[] {
|
|
126
|
+
if (!isRecord(value) || !isRecord(value.profiles)) return [];
|
|
127
|
+
return Object.entries(value.profiles).map(([id, definition]) => {
|
|
128
|
+
const result = validateSessionCapabilityProfile(definition);
|
|
129
|
+
if (!isSessionCapabilityProfileId(id))
|
|
130
|
+
result.errors.push("Invalid profile ID: use lowercase words with single hyphens.");
|
|
131
|
+
return {
|
|
132
|
+
id,
|
|
133
|
+
source,
|
|
134
|
+
shadowed: false,
|
|
135
|
+
profile: result.errors.length ? undefined : result.profile,
|
|
136
|
+
errors: result.errors.map((message) => ({ scope: source, path: `profiles.${id}`, message })),
|
|
137
|
+
};
|
|
138
|
+
});
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
type DefaultReference =
|
|
142
|
+
| SessionCapabilityProfileDefault
|
|
143
|
+
| { status: "pending"; source: SessionCapabilityProfileScope; id: string };
|
|
144
|
+
|
|
145
|
+
function readDefault(
|
|
146
|
+
value: unknown,
|
|
147
|
+
source: SessionCapabilityProfileScope,
|
|
148
|
+
errors: SessionCapabilityProfileDiagnostic[],
|
|
149
|
+
): DefaultReference | undefined {
|
|
150
|
+
if (!isRecord(value) || !Object.hasOwn(value, "defaultProfile")) return undefined;
|
|
151
|
+
if (value.defaultProfile === null) return { status: "none", source };
|
|
152
|
+
if (isSessionCapabilityProfileId(value.defaultProfile)) {
|
|
153
|
+
return { status: "pending", source, id: value.defaultProfile };
|
|
154
|
+
}
|
|
155
|
+
errors.push({
|
|
156
|
+
scope: source,
|
|
157
|
+
path: "defaultProfile",
|
|
158
|
+
message: "defaultProfile must be null or a valid profile ID.",
|
|
159
|
+
});
|
|
160
|
+
return { status: "invalid", source };
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
function resolveDefault(
|
|
164
|
+
selected: DefaultReference | undefined,
|
|
165
|
+
profiles: SessionCapabilityProfileSnapshot[],
|
|
166
|
+
errors: SessionCapabilityProfileDiagnostic[],
|
|
167
|
+
): SessionCapabilityProfileDefault {
|
|
168
|
+
if (!selected) return { status: "none" };
|
|
169
|
+
if (selected.status !== "pending") return selected;
|
|
170
|
+
const { source, id } = selected;
|
|
171
|
+
const profile = profiles.find((entry) => entry.id === id);
|
|
172
|
+
if (profile) return { status: "resolved", source, id, profile };
|
|
173
|
+
errors.push({
|
|
174
|
+
scope: source,
|
|
175
|
+
path: "defaultProfile",
|
|
176
|
+
message: `Default profile ${id} is missing or invalid. No profile will be applied.`,
|
|
177
|
+
});
|
|
178
|
+
return { status: "invalid", source, id };
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
interface ScopedProfiles {
|
|
182
|
+
entries: SessionCapabilityProfileEntry[];
|
|
183
|
+
errors: SessionCapabilityProfileDiagnostic[];
|
|
184
|
+
default?: DefaultReference;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
/** Reject malformed sections, but retain known IDs to prevent global fallback. */
|
|
188
|
+
function readSection(value: unknown, scope: SessionCapabilityProfileScope): ScopedProfiles {
|
|
189
|
+
if (value === undefined) return { entries: [], errors: [] };
|
|
190
|
+
if (!isRecord(value)) {
|
|
191
|
+
return {
|
|
192
|
+
entries: [],
|
|
193
|
+
default: { status: "invalid", source: scope },
|
|
194
|
+
errors: [
|
|
195
|
+
{ scope, path: "sessionCapabilities", message: "sessionCapabilities must be an object." },
|
|
196
|
+
],
|
|
197
|
+
};
|
|
198
|
+
}
|
|
199
|
+
const sectionErrors = Object.keys(value)
|
|
200
|
+
.filter((key) => key !== "profiles" && key !== "defaultProfile")
|
|
201
|
+
.map((key) => ({ scope, path: key, message: `Unknown section field: ${key}.` }));
|
|
202
|
+
if (Object.hasOwn(value, "profiles") && !isRecord(value.profiles)) {
|
|
203
|
+
sectionErrors.push({ scope, path: "profiles", message: "profiles must be an object." });
|
|
204
|
+
}
|
|
205
|
+
const entries = readEntries(value, scope);
|
|
206
|
+
const errors = [...sectionErrors, ...entries.flatMap((entry) => entry.errors)];
|
|
207
|
+
const selection = readDefault(value, scope, errors);
|
|
208
|
+
if (sectionErrors.length) {
|
|
209
|
+
for (const entry of entries) {
|
|
210
|
+
entry.profile = undefined;
|
|
211
|
+
entry.errors.push(...sectionErrors);
|
|
212
|
+
}
|
|
213
|
+
return { entries, errors, default: { status: "invalid", source: scope } };
|
|
214
|
+
}
|
|
215
|
+
return { entries, errors, default: selection };
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
/**
|
|
219
|
+
* Resolve whole profiles by ID. Ignore project data unless projectTrusted is true.
|
|
220
|
+
* An absent section is undefined. Malformed sections cannot supply profiles or a default.
|
|
221
|
+
*/
|
|
222
|
+
export function resolveSessionCapabilityProfiles(
|
|
223
|
+
options: ResolveSessionCapabilityProfilesOptions,
|
|
224
|
+
): SessionCapabilityProfilesResolution {
|
|
225
|
+
const global = readSection(options.global, "global");
|
|
226
|
+
const project = readSection(options.projectTrusted ? options.project : undefined, "project");
|
|
227
|
+
const entries = [...global.entries, ...project.entries];
|
|
228
|
+
const errors = [...global.errors, ...project.errors];
|
|
229
|
+
const selected = new Map(entries.map((entry) => [entry.id, entry]));
|
|
230
|
+
for (const entry of entries) entry.shadowed = selected.get(entry.id) !== entry;
|
|
231
|
+
const effectiveProfiles = [...selected.values()].flatMap((entry) =>
|
|
232
|
+
entry.profile ? [{ id: entry.id, source: entry.source, ...entry.profile }] : [],
|
|
233
|
+
);
|
|
234
|
+
return {
|
|
235
|
+
entries,
|
|
236
|
+
effectiveProfiles,
|
|
237
|
+
default: resolveDefault(project.default ?? global.default, effectiveProfiles, errors),
|
|
238
|
+
errors,
|
|
239
|
+
};
|
|
240
|
+
}
|
|
@@ -7,17 +7,39 @@ export type {
|
|
|
7
7
|
SessionCapabilityState,
|
|
8
8
|
} from "./session-capabilities.ts";
|
|
9
9
|
export {
|
|
10
|
+
applySessionCapabilityProfile,
|
|
11
|
+
captureSessionCapabilityExclusions,
|
|
10
12
|
clearPendingSessionCapabilityForks,
|
|
11
13
|
createEmptySessionCapabilityState,
|
|
12
14
|
decodeSessionCapabilities,
|
|
13
15
|
findSessionCapabilities,
|
|
14
16
|
getSessionCapabilitySkillProvider,
|
|
17
|
+
isSessionCapabilityProfileModified,
|
|
15
18
|
readPendingSessionCapabilityFork,
|
|
16
19
|
registerSessionCapabilitySkillProvider,
|
|
17
20
|
SESSION_CAPABILITIES_ENTRY,
|
|
18
21
|
sessionCapabilityState,
|
|
19
22
|
setPendingSessionCapabilityFork,
|
|
20
23
|
} from "./session-capabilities.ts";
|
|
24
|
+
export type {
|
|
25
|
+
ResolveSessionCapabilityProfilesOptions,
|
|
26
|
+
SessionCapabilityProfile,
|
|
27
|
+
SessionCapabilityProfileDefault,
|
|
28
|
+
SessionCapabilityProfileDefinition,
|
|
29
|
+
SessionCapabilityProfileDiagnostic,
|
|
30
|
+
SessionCapabilityProfileEntry,
|
|
31
|
+
SessionCapabilityProfileScope,
|
|
32
|
+
SessionCapabilityProfileSnapshot,
|
|
33
|
+
SessionCapabilityProfilesResolution,
|
|
34
|
+
SessionCapabilityProfilesSection,
|
|
35
|
+
SessionCapabilityProfileValidation,
|
|
36
|
+
} from "./session-capability-profiles.ts";
|
|
37
|
+
export {
|
|
38
|
+
decodeSessionCapabilityProfileSnapshot,
|
|
39
|
+
isSessionCapabilityProfileId,
|
|
40
|
+
resolveSessionCapabilityProfiles,
|
|
41
|
+
validateSessionCapabilityProfile,
|
|
42
|
+
} from "./session-capability-profiles.ts";
|
|
21
43
|
export type { SessionNameTrackerHost } from "./session-utils.ts";
|
|
22
44
|
export {
|
|
23
45
|
createSessionNameTracker,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mrclrchtr/supi-extras",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "9.0.0",
|
|
4
4
|
"description": "Session capability controls, prompt stash, shortcuts, and activity indicators",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|
|
@@ -33,7 +33,7 @@
|
|
|
33
33
|
],
|
|
34
34
|
"dependencies": {
|
|
35
35
|
"clipboardy": "^5.3.1",
|
|
36
|
-
"@mrclrchtr/supi-core": "
|
|
36
|
+
"@mrclrchtr/supi-core": "9.0.0"
|
|
37
37
|
},
|
|
38
38
|
"bundledDependencies": [
|
|
39
39
|
"@mrclrchtr/supi-core"
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import type { ExtensionCommandContext } from "@earendil-works/pi-coding-agent";
|
|
2
|
+
import type {
|
|
3
|
+
SessionCapabilityProfileSnapshot,
|
|
4
|
+
SessionCapabilitySkill,
|
|
5
|
+
SessionCapabilityState,
|
|
6
|
+
} from "@mrclrchtr/supi-core/session";
|
|
7
|
+
import {
|
|
8
|
+
type CapabilityProfileAction,
|
|
9
|
+
type CapabilityToolItem,
|
|
10
|
+
createCapabilitySelector,
|
|
11
|
+
} from "./session-capabilities-ui.ts";
|
|
12
|
+
import type { CapabilitySection } from "./session-capabilities-ui-rows.ts";
|
|
13
|
+
import { createCapabilityProfileStore } from "./session-capability-profile-store.ts";
|
|
14
|
+
import {
|
|
15
|
+
buildCapabilityProfileRows,
|
|
16
|
+
runCapabilityProfileAction,
|
|
17
|
+
} from "./session-capability-profile-ui.ts";
|
|
18
|
+
|
|
19
|
+
interface SelectorOptions {
|
|
20
|
+
ctx: ExtensionCommandContext;
|
|
21
|
+
homeDir?: string;
|
|
22
|
+
state: SessionCapabilityState;
|
|
23
|
+
tools: CapabilityToolItem[];
|
|
24
|
+
skills: SessionCapabilitySkill[] | undefined;
|
|
25
|
+
onChange(id: string, value: string): void;
|
|
26
|
+
onApply(profile: SessionCapabilityProfileSnapshot): void;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/** Close custom UI before built-in dialogs, then reopen Profiles with current configuration. */
|
|
30
|
+
export async function openCapabilitySelector(options: SelectorOptions): Promise<void> {
|
|
31
|
+
const { ctx, state, tools, skills } = options;
|
|
32
|
+
const store = createCapabilityProfileStore({
|
|
33
|
+
cwd: ctx.cwd,
|
|
34
|
+
homeDir: options.homeDir,
|
|
35
|
+
isProjectTrusted: () => ctx.isProjectTrusted(),
|
|
36
|
+
});
|
|
37
|
+
let initialSection: CapabilitySection = "tools";
|
|
38
|
+
while (true) {
|
|
39
|
+
const profileRows = buildCapabilityProfileRows({
|
|
40
|
+
catalog: store.read(),
|
|
41
|
+
tools,
|
|
42
|
+
skills,
|
|
43
|
+
projectTrusted: ctx.isProjectTrusted(),
|
|
44
|
+
});
|
|
45
|
+
const action = await ctx.ui.custom<CapabilityProfileAction | undefined>(
|
|
46
|
+
(tui, theme, _keys, done) =>
|
|
47
|
+
createCapabilitySelector({
|
|
48
|
+
state,
|
|
49
|
+
tools,
|
|
50
|
+
skills,
|
|
51
|
+
profileRows,
|
|
52
|
+
initialSection,
|
|
53
|
+
theme,
|
|
54
|
+
tui,
|
|
55
|
+
done,
|
|
56
|
+
onChange: options.onChange,
|
|
57
|
+
}),
|
|
58
|
+
);
|
|
59
|
+
if (!action) return;
|
|
60
|
+
try {
|
|
61
|
+
await runCapabilityProfileAction(action, { ctx, store, state, apply: options.onApply });
|
|
62
|
+
} catch (error) {
|
|
63
|
+
ctx.ui.notify(error instanceof Error ? error.message : String(error), "error");
|
|
64
|
+
}
|
|
65
|
+
initialSection = "profiles";
|
|
66
|
+
}
|
|
67
|
+
}
|
|
@@ -83,7 +83,6 @@ export function reconcileToolInventory(
|
|
|
83
83
|
|
|
84
84
|
state.initiallyInactiveToolNames = Array.from(initiallyInactive);
|
|
85
85
|
state.eligibleToolNames = Array.from(eligible);
|
|
86
|
-
state.toolDenylist = state.toolDenylist.filter((name) => eligible.has(name));
|
|
87
86
|
state.toolEnabledNames = (state.toolEnabledNames ?? []).filter(
|
|
88
87
|
(name) => eligible.has(name) && !state.toolDenylist.includes(name),
|
|
89
88
|
);
|
|
@@ -2,7 +2,13 @@ import type { SelectItem } from "@earendil-works/pi-tui";
|
|
|
2
2
|
import type { SessionCapabilitySkill, SessionCapabilityState } from "@mrclrchtr/supi-core/session";
|
|
3
3
|
|
|
4
4
|
/** A section shown by the session capability selector. */
|
|
5
|
-
export type CapabilitySection = "tools" | "skills";
|
|
5
|
+
export type CapabilitySection = "tools" | "skills" | "profiles";
|
|
6
|
+
|
|
7
|
+
/** A profile action returned after the selector closes, before any dialog opens. */
|
|
8
|
+
export interface CapabilityProfileAction {
|
|
9
|
+
id: string;
|
|
10
|
+
value: string;
|
|
11
|
+
}
|
|
6
12
|
|
|
7
13
|
/** A tool row that the session selector can display. */
|
|
8
14
|
export interface CapabilityToolItem {
|
|
@@ -182,11 +188,14 @@ function skillRows(
|
|
|
182
188
|
}
|
|
183
189
|
|
|
184
190
|
/** Build the rows for one session capability section. */
|
|
185
|
-
export function buildCapabilityRows(
|
|
186
|
-
section: CapabilitySection
|
|
187
|
-
tools: CapabilityToolItem[]
|
|
188
|
-
skills: SessionCapabilitySkill[] | undefined
|
|
189
|
-
state: SessionCapabilityState
|
|
190
|
-
|
|
191
|
+
export function buildCapabilityRows(options: {
|
|
192
|
+
section: CapabilitySection;
|
|
193
|
+
tools: CapabilityToolItem[];
|
|
194
|
+
skills: SessionCapabilitySkill[] | undefined;
|
|
195
|
+
state: SessionCapabilityState;
|
|
196
|
+
profileRows?: CapabilityRow[];
|
|
197
|
+
}): CapabilityRow[] {
|
|
198
|
+
const { section, tools, skills, state, profileRows = [] } = options;
|
|
199
|
+
if (section === "profiles") return profileRows;
|
|
191
200
|
return section === "tools" ? toolRows(tools, state) : skillRows(skills, state);
|
|
192
201
|
}
|