@sherif-fanous/pi-presets-plus 0.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.
- package/CHANGELOG.md +9 -0
- package/LICENSE +21 -0
- package/README.md +67 -0
- package/package.json +74 -0
- package/src/activation/active-state.ts +25 -0
- package/src/activation/apply.ts +236 -0
- package/src/activation/baseline.ts +32 -0
- package/src/activation/clear.ts +434 -0
- package/src/activation/dirty.ts +69 -0
- package/src/activation/drift-handlers.ts +71 -0
- package/src/activation/drift.ts +77 -0
- package/src/activation/same-set.ts +32 -0
- package/src/activation/state-matches.ts +29 -0
- package/src/activation/thinking.ts +54 -0
- package/src/commands/presets/clear.ts +18 -0
- package/src/commands/presets/index.ts +9 -0
- package/src/commands/presets/notify.ts +22 -0
- package/src/commands/presets/reload.ts +28 -0
- package/src/commands/presets/router.ts +139 -0
- package/src/commands/presets/status.ts +262 -0
- package/src/flag.ts +88 -0
- package/src/hotkey-conflicts.ts +136 -0
- package/src/hotkey-reload-baseline.ts +112 -0
- package/src/hotkeys.ts +104 -0
- package/src/index.ts +171 -0
- package/src/messages.ts +34 -0
- package/src/store/api.ts +262 -0
- package/src/store/load.ts +175 -0
- package/src/store/merge.ts +69 -0
- package/src/store/paths.ts +38 -0
- package/src/store/save.ts +75 -0
- package/src/store/validate.ts +195 -0
- package/src/types.ts +169 -0
- package/src/ui/confirm.ts +126 -0
- package/src/ui/editor.ts +1617 -0
- package/src/ui/filter.ts +79 -0
- package/src/ui/frame.ts +109 -0
- package/src/ui/hotkey-input.ts +242 -0
- package/src/ui/info-dialog.ts +118 -0
- package/src/ui/labels.ts +51 -0
- package/src/ui/picker-state.ts +151 -0
- package/src/ui/picker.ts +982 -0
- package/src/ui/reload-prompt.ts +59 -0
- package/src/ui/status.ts +55 -0
- package/src/ui/widgets.ts +274 -0
package/CHANGELOG.md
ADDED
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Sherif Fanous
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
# pi-presets-plus
|
|
2
|
+
|
|
3
|
+
A [Pi](https://github.com/badlogic/pi) extension that lets you bundle a model, thinking level, tools, and system prompt into a named preset, then switch between presets with one hotkey.
|
|
4
|
+
|
|
5
|
+
## Why
|
|
6
|
+
|
|
7
|
+
Pi already lets you change the model you talk to, how hard it thinks, which tools it can use, and what system prompt it follows. Each of those is its own setting, and tweaking them one at a time is fine for ad-hoc work but it gets tedious when you have a handful of working modes you keep coming back to: a fast cheap model for boilerplate, a heavy reasoning model for tricky design, a "review only" setup with no write tools and a strict prompt, a planning mode, an implementation mode.
|
|
8
|
+
|
|
9
|
+
`pi-presets-plus` lets you save a complete setup as a named preset and switch to it with one keystroke.
|
|
10
|
+
|
|
11
|
+
## Install
|
|
12
|
+
|
|
13
|
+
```shell
|
|
14
|
+
pi install npm:@sherif-fanous/pi-presets-plus
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
Or try it without installing:
|
|
18
|
+
|
|
19
|
+
```shell
|
|
20
|
+
pi -e npm:@sherif-fanous/pi-presets-plus
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
To uninstall:
|
|
24
|
+
|
|
25
|
+
```shell
|
|
26
|
+
pi remove npm:@sherif-fanous/pi-presets-plus
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
## Quick start
|
|
30
|
+
|
|
31
|
+
1. Run `/presets` in any Pi session to open the preset picker.
|
|
32
|
+
2. Press `n` to create a new preset, or `e` to edit an existing one.
|
|
33
|
+
3. Press `F1` on any row in the editor to get help for that row.
|
|
34
|
+
4. Save your preset and, optionally, give it a hotkey. From then on, pressing the hotkey switches to the preset. Run `/presets clear` to go back to Pi's defaults.
|
|
35
|
+
|
|
36
|
+
The picker also lets you filter by name, switch the scope filter, reorder, duplicate, and delete presets. The footer always shows the keys you can press.
|
|
37
|
+
|
|
38
|
+
## What's in a preset
|
|
39
|
+
|
|
40
|
+
| Field | What it does |
|
|
41
|
+
| -------- | ------------------------------------------------------------------------------------------------------------------ |
|
|
42
|
+
| Name | A short, memorable label for the preset. Names are unique within their scope. |
|
|
43
|
+
| Scope | _User_ presets follow you across every project. _Project_ presets stay tied to one repo and can be shared. |
|
|
44
|
+
| Provider | The service that hosts the model (OpenAI, Anthropic, etc.). Only providers Pi knows about appear here. |
|
|
45
|
+
| Model | The specific model Pi will use when this preset is active. |
|
|
46
|
+
| Thinking | How much extra reasoning effort to ask for. Some models don't support every level; unavailable ones appear dimmed. |
|
|
47
|
+
| Tools | Either keep whatever tools are active, or pin an exact tool list to the preset. |
|
|
48
|
+
| Prompt | Extra instructions added to Pi's system prompt while the preset is active. Pi's defaults are kept either way. |
|
|
49
|
+
| Hotkey | Optional. A single key combination (like `ctrl+shift+1`) that switches to this preset. |
|
|
50
|
+
|
|
51
|
+
## Where presets live
|
|
52
|
+
|
|
53
|
+
| Scope | Path |
|
|
54
|
+
| ------- | ------------------------------------------------------------------------------------------- |
|
|
55
|
+
| User | `<agent-dir>/presets-plus/presets.json` (typically `~/.pi/agent/presets-plus/presets.json`) |
|
|
56
|
+
| Project | `<repo>/.pi/presets-plus/presets.json` |
|
|
57
|
+
|
|
58
|
+
If a project preset and a user preset share a name, the project preset wins while you're working in that project.
|
|
59
|
+
|
|
60
|
+
## Commands
|
|
61
|
+
|
|
62
|
+
| Command | What it does |
|
|
63
|
+
| ----------------- | ------------------------------------------------------------- |
|
|
64
|
+
| `/presets` | Opens the picker. |
|
|
65
|
+
| `/presets clear` | Clears the active preset and returns to Pi's defaults. |
|
|
66
|
+
| `/presets reload` | Re-reads your preset files (use after editing them by hand). |
|
|
67
|
+
| `/presets status` | Shows the active preset's settings compared to Pi's defaults. |
|
package/package.json
ADDED
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@sherif-fanous/pi-presets-plus",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Pi extension: model + thinking + tools + system-prompt presets, with a UI on top.",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"pi",
|
|
7
|
+
"pi-coding-agent",
|
|
8
|
+
"pi-package",
|
|
9
|
+
"presets",
|
|
10
|
+
"favorites",
|
|
11
|
+
"model-presets",
|
|
12
|
+
"thinking-presets",
|
|
13
|
+
"tools-presets",
|
|
14
|
+
"system-prompt-presets"
|
|
15
|
+
],
|
|
16
|
+
"license": "MIT",
|
|
17
|
+
"author": "Sherif Fanous",
|
|
18
|
+
"type": "module",
|
|
19
|
+
"files": [
|
|
20
|
+
"src",
|
|
21
|
+
"README.md",
|
|
22
|
+
"LICENSE",
|
|
23
|
+
"CHANGELOG.md",
|
|
24
|
+
"package.json"
|
|
25
|
+
],
|
|
26
|
+
"prettier": {
|
|
27
|
+
"importOrder": [
|
|
28
|
+
"<BUILTIN_MODULES>",
|
|
29
|
+
"",
|
|
30
|
+
"<THIRD_PARTY_MODULES>"
|
|
31
|
+
],
|
|
32
|
+
"importOrderTypeScriptVersion": "6.0.0",
|
|
33
|
+
"plugins": [
|
|
34
|
+
"@ianvs/prettier-plugin-sort-imports"
|
|
35
|
+
]
|
|
36
|
+
},
|
|
37
|
+
"devDependencies": {
|
|
38
|
+
"@biomejs/biome": "^2.4.13",
|
|
39
|
+
"@eslint/js": "^10.0.1",
|
|
40
|
+
"@ianvs/prettier-plugin-sort-imports": "^4.7.1",
|
|
41
|
+
"@mariozechner/pi-ai": "^0.73.0",
|
|
42
|
+
"@mariozechner/pi-coding-agent": "^0.73.0",
|
|
43
|
+
"@mariozechner/pi-tui": "^0.73.0",
|
|
44
|
+
"@stylistic/eslint-plugin": "^5.10.0",
|
|
45
|
+
"eslint": "^10.2.1",
|
|
46
|
+
"eslint-plugin-perfectionist": "^5.9.0",
|
|
47
|
+
"eslint-plugin-unicorn": "^64.0.0",
|
|
48
|
+
"fallow": "^2.55.0",
|
|
49
|
+
"prettier": "^3.8.3",
|
|
50
|
+
"sort-package-json": "^3.6.1",
|
|
51
|
+
"typescript": "^6.0.3",
|
|
52
|
+
"typescript-eslint": "^8.59.1",
|
|
53
|
+
"vitest": "^4.1.5"
|
|
54
|
+
},
|
|
55
|
+
"peerDependencies": {
|
|
56
|
+
"@mariozechner/pi-ai": "*",
|
|
57
|
+
"@mariozechner/pi-coding-agent": "*",
|
|
58
|
+
"@mariozechner/pi-tui": "*",
|
|
59
|
+
"@sinclair/typebox": "*"
|
|
60
|
+
},
|
|
61
|
+
"pi": {
|
|
62
|
+
"extensions": [
|
|
63
|
+
"./src/index.ts"
|
|
64
|
+
]
|
|
65
|
+
},
|
|
66
|
+
"scripts": {
|
|
67
|
+
"format": "prettier --write eslint.config.mjs package.json src/. tests/. tsconfig.json vitest.config.ts",
|
|
68
|
+
"format-check": "prettier --check eslint.config.mjs package.json src/. tests/. tsconfig.json vitest.config.ts",
|
|
69
|
+
"lint": "biome lint src/. tests/. && eslint src/. tests/.",
|
|
70
|
+
"sort-package-json": "npx sort-package-json",
|
|
71
|
+
"test": "vitest",
|
|
72
|
+
"type-check": "tsc --noEmit"
|
|
73
|
+
}
|
|
74
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* In-memory active-preset reference for the activation runtime.
|
|
3
|
+
*
|
|
4
|
+
* Owns the module-scoped state cell tracking which preset is currently
|
|
5
|
+
* active; it does NOT persist session entries, update status, or mutate
|
|
6
|
+
* pi model/tool state.
|
|
7
|
+
*/
|
|
8
|
+
import type { ActivePresetState } from "../types.js";
|
|
9
|
+
|
|
10
|
+
let active: ActivePresetState | undefined;
|
|
11
|
+
|
|
12
|
+
/** Remove the current active-preset attachment. */
|
|
13
|
+
export function clearActive(): void {
|
|
14
|
+
active = undefined;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/** Return the current active-preset attachment, if any. */
|
|
18
|
+
export function getActive(): ActivePresetState | undefined {
|
|
19
|
+
return active;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/** Replace the current active-preset attachment. */
|
|
23
|
+
export function setActive(next: ActivePresetState): void {
|
|
24
|
+
active = next;
|
|
25
|
+
}
|
|
@@ -0,0 +1,236 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Preset apply flow.
|
|
3
|
+
*
|
|
4
|
+
* Owns the end-to-end activation of a preset: writing model, thinking, and
|
|
5
|
+
* tool state, recording the baseline overlay, persisting activation, and
|
|
6
|
+
* refreshing status. It does NOT own command lookup, session restore, or
|
|
7
|
+
* picker UI.
|
|
8
|
+
*/
|
|
9
|
+
import { ACTIVATED_MESSAGE_TYPE } from "../messages.js";
|
|
10
|
+
import type { LoadedPreset } from "../types.js";
|
|
11
|
+
import { updateStatus } from "../ui/status.js";
|
|
12
|
+
import { getActive, setActive } from "./active-state.js";
|
|
13
|
+
import { captureBaseline } from "./baseline.js";
|
|
14
|
+
import { markClean } from "./dirty.js";
|
|
15
|
+
import { snapshotPresetForDrift } from "./drift.js";
|
|
16
|
+
import { stateMatches } from "./state-matches.js";
|
|
17
|
+
import { effectiveThinkingLevel } from "./thinking.js";
|
|
18
|
+
import type {
|
|
19
|
+
ExtensionAPI,
|
|
20
|
+
ExtensionContext,
|
|
21
|
+
} from "@mariozechner/pi-coding-agent";
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Counter (not boolean) so nested `withSelfTriggeredModelSet` calls cannot
|
|
25
|
+
* silently drop the guard when the inner call's `finally` runs while the
|
|
26
|
+
* outer call is still in flight. Today there is only one caller, but the
|
|
27
|
+
* counter shape costs nothing and removes a footgun for future callers.
|
|
28
|
+
*/
|
|
29
|
+
let selfTriggeredModelSetDepth = 0;
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* In-memory result from applying a preset.
|
|
33
|
+
*
|
|
34
|
+
* Refusal kinds:
|
|
35
|
+
* - `no-key`: the preset is unavailable because its provider key is missing.
|
|
36
|
+
* - `no-model`: the preset is unavailable because its model is not installed.
|
|
37
|
+
* - `unknown-model`: the preset references a provider/model not in the registry.
|
|
38
|
+
* - `key-revoked`: the model resolved, but `setModel` refused it at apply time.
|
|
39
|
+
*/
|
|
40
|
+
export type ApplyResult =
|
|
41
|
+
| { ok: true }
|
|
42
|
+
| {
|
|
43
|
+
ok: false;
|
|
44
|
+
kind: "key-revoked" | "no-key" | "no-model" | "unknown-model";
|
|
45
|
+
reason: string;
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Apply `preset` to Pi state and return a structured refusal on expected
|
|
50
|
+
* activation failures. Callers surface refusal `reason` through the channel
|
|
51
|
+
* appropriate to their context; this function still emits non-refusal warning
|
|
52
|
+
* or informational accompaniments inline for successful activation.
|
|
53
|
+
*/
|
|
54
|
+
export async function apply(
|
|
55
|
+
preset: LoadedPreset,
|
|
56
|
+
ctx: ExtensionContext,
|
|
57
|
+
pi: ExtensionAPI,
|
|
58
|
+
): Promise<ApplyResult> {
|
|
59
|
+
if (preset.unavailable) {
|
|
60
|
+
const kind = preset.unavailable;
|
|
61
|
+
|
|
62
|
+
return { ok: false, kind, reason: failureReason(kind, preset) };
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
const current = getActive();
|
|
66
|
+
|
|
67
|
+
if (
|
|
68
|
+
current?.name === preset.name &&
|
|
69
|
+
current.scope === preset.scope &&
|
|
70
|
+
current.restore.kind === "baseline" &&
|
|
71
|
+
stateMatches(preset, pi, ctx)
|
|
72
|
+
) {
|
|
73
|
+
if (current.dirty) await markClean(ctx);
|
|
74
|
+
|
|
75
|
+
return { ok: true };
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
const model = ctx.modelRegistry.find(preset.provider, preset.model);
|
|
79
|
+
|
|
80
|
+
if (!model) {
|
|
81
|
+
return {
|
|
82
|
+
ok: false,
|
|
83
|
+
kind: "unknown-model",
|
|
84
|
+
reason: failureReason("unknown-model", preset),
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
const previousBaseline =
|
|
89
|
+
current?.restore.kind === "baseline" ? current.restore : undefined;
|
|
90
|
+
const baseline = previousBaseline?.baseline ?? captureBaseline(pi, ctx);
|
|
91
|
+
const applyCount = (previousBaseline?.applyCount ?? 0) + 1;
|
|
92
|
+
const previousAppliedTools = previousBaseline?.lastApplied.tools;
|
|
93
|
+
const previousOwnedTools = previousBaseline?.owned.tools ?? false;
|
|
94
|
+
|
|
95
|
+
if (!(await setModelGuarded(pi, model))) {
|
|
96
|
+
return {
|
|
97
|
+
ok: false,
|
|
98
|
+
kind: "key-revoked",
|
|
99
|
+
reason: failureReason("key-revoked", preset),
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
const effective = effectiveThinkingLevel(preset, model);
|
|
104
|
+
const declared = preset.thinkingLevel ?? "off";
|
|
105
|
+
|
|
106
|
+
pi.setThinkingLevel(effective);
|
|
107
|
+
|
|
108
|
+
if (effective !== declared) {
|
|
109
|
+
ctx.ui.notify(
|
|
110
|
+
`Preset "${preset.name}" requested thinking level "${declared}" for ${preset.provider}/${preset.model}. Applied "${effective}" instead.`,
|
|
111
|
+
"info",
|
|
112
|
+
);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
let appliedTools = previousAppliedTools;
|
|
116
|
+
let ownedTools = previousOwnedTools;
|
|
117
|
+
|
|
118
|
+
if (preset.tools && preset.tools.length > 0) {
|
|
119
|
+
const validTools = filterValidTools(preset.tools, pi.getAllTools());
|
|
120
|
+
const dropped = preset.tools.filter(
|
|
121
|
+
(toolName) => !validTools.includes(toolName),
|
|
122
|
+
);
|
|
123
|
+
|
|
124
|
+
if (dropped.length > 0) {
|
|
125
|
+
ctx.ui.notify(
|
|
126
|
+
`Preset "${preset.name}" references unknown tools: ${dropped.join(", ")}. They were ignored.`,
|
|
127
|
+
"warning",
|
|
128
|
+
);
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
pi.setActiveTools(validTools);
|
|
132
|
+
appliedTools = validTools;
|
|
133
|
+
ownedTools = true;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
setActive({
|
|
137
|
+
declared: snapshotPresetForDrift({ ...preset, tools: appliedTools }),
|
|
138
|
+
dirty: false,
|
|
139
|
+
name: preset.name,
|
|
140
|
+
restore: {
|
|
141
|
+
applyCount,
|
|
142
|
+
baseline,
|
|
143
|
+
kind: "baseline",
|
|
144
|
+
lastApplied: {
|
|
145
|
+
...(appliedTools !== undefined ? { tools: appliedTools } : {}),
|
|
146
|
+
model: { id: preset.model, provider: preset.provider },
|
|
147
|
+
thinkingLevel: effective,
|
|
148
|
+
},
|
|
149
|
+
owned: { model: true, thinkingLevel: true, tools: ownedTools },
|
|
150
|
+
},
|
|
151
|
+
scope: preset.scope,
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
pi.appendEntry("presets-plus:active", {
|
|
155
|
+
name: preset.name,
|
|
156
|
+
scope: preset.scope,
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
pi.sendMessage({
|
|
160
|
+
customType: ACTIVATED_MESSAGE_TYPE,
|
|
161
|
+
content: `Preset ${preset.name} applied`,
|
|
162
|
+
display: true,
|
|
163
|
+
details: {
|
|
164
|
+
name: preset.name,
|
|
165
|
+
model: `${preset.provider}/${preset.model}`,
|
|
166
|
+
thinkingLevel: effective,
|
|
167
|
+
},
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
updateStatus(ctx, getActive(), (name, scope) =>
|
|
171
|
+
name === preset.name && scope === preset.scope ? preset : undefined,
|
|
172
|
+
);
|
|
173
|
+
|
|
174
|
+
return { ok: true };
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
export function isSelfTriggeredModelSet(): boolean {
|
|
178
|
+
return selfTriggeredModelSetDepth > 0;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
/**
|
|
182
|
+
* Run `fn` with the self-call counter raised so a `model_select` event
|
|
183
|
+
* triggered by `pi.setModel` inside `fn` is recognized as our own write
|
|
184
|
+
* and ignored by the drift-detection handler. The counter shape (vs. a
|
|
185
|
+
* boolean) keeps nested or concurrent calls from accidentally lowering the
|
|
186
|
+
* guard while an outer call is still in flight.
|
|
187
|
+
*/
|
|
188
|
+
export async function withSelfTriggeredModelSet<T>(
|
|
189
|
+
fn: () => Promise<T>,
|
|
190
|
+
): Promise<T> {
|
|
191
|
+
selfTriggeredModelSetDepth++;
|
|
192
|
+
|
|
193
|
+
try {
|
|
194
|
+
return await fn();
|
|
195
|
+
} finally {
|
|
196
|
+
selfTriggeredModelSetDepth--;
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
function failureReason(
|
|
201
|
+
kind: Exclude<ApplyResult, { ok: true }>["kind"],
|
|
202
|
+
preset: Pick<LoadedPreset, "model" | "name" | "provider">,
|
|
203
|
+
): string {
|
|
204
|
+
switch (kind) {
|
|
205
|
+
case "no-key":
|
|
206
|
+
return `Preset "${preset.name}" is unavailable: missing API key. Activation skipped.`;
|
|
207
|
+
case "no-model":
|
|
208
|
+
return `Preset "${preset.name}" is unavailable: model not installed. Activation skipped.`;
|
|
209
|
+
case "unknown-model":
|
|
210
|
+
return `Preset "${preset.name}" references unknown model ${preset.provider}/${preset.model}.`;
|
|
211
|
+
case "key-revoked":
|
|
212
|
+
return `No API key configured for ${preset.provider}/${preset.model}.`;
|
|
213
|
+
|
|
214
|
+
default: {
|
|
215
|
+
const exhaustive: never = kind;
|
|
216
|
+
|
|
217
|
+
return exhaustive;
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
function filterValidTools(
|
|
223
|
+
desired: readonly string[],
|
|
224
|
+
allTools: readonly { name: string }[],
|
|
225
|
+
): string[] {
|
|
226
|
+
const available = new Set(allTools.map((tool) => tool.name));
|
|
227
|
+
|
|
228
|
+
return desired.filter((toolName) => available.has(toolName));
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
async function setModelGuarded(
|
|
232
|
+
pi: Pick<ExtensionAPI, "setModel">,
|
|
233
|
+
model: NonNullable<ReturnType<ExtensionContext["modelRegistry"]["find"]>>,
|
|
234
|
+
): Promise<boolean> {
|
|
235
|
+
return withSelfTriggeredModelSet(() => pi.setModel(model));
|
|
236
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Baseline capture for preset activation overlays.
|
|
3
|
+
*
|
|
4
|
+
* Owns reading the current pi state so a preset can later be cleared back
|
|
5
|
+
* to its pre-activation values; it does NOT apply presets or restore
|
|
6
|
+
* state.
|
|
7
|
+
*/
|
|
8
|
+
import type { PresetOverlayBaseline } from "../types.js";
|
|
9
|
+
import type {
|
|
10
|
+
ExtensionAPI,
|
|
11
|
+
ExtensionContext,
|
|
12
|
+
} from "@mariozechner/pi-coding-agent";
|
|
13
|
+
|
|
14
|
+
/** Minimal context surface needed to capture the current model. */
|
|
15
|
+
type BaselineContext = Pick<ExtensionContext, "model">;
|
|
16
|
+
|
|
17
|
+
/** Minimal pi surface needed to capture restorable state. */
|
|
18
|
+
type BaselinePi = Pick<ExtensionAPI, "getActiveTools" | "getThinkingLevel">;
|
|
19
|
+
|
|
20
|
+
/** Capture the current Pi state as an overlay restore baseline. */
|
|
21
|
+
export function captureBaseline(
|
|
22
|
+
pi: BaselinePi,
|
|
23
|
+
ctx: BaselineContext,
|
|
24
|
+
): PresetOverlayBaseline {
|
|
25
|
+
return {
|
|
26
|
+
model: ctx.model
|
|
27
|
+
? { provider: ctx.model.provider, id: ctx.model.id }
|
|
28
|
+
: null,
|
|
29
|
+
thinkingLevel: pi.getThinkingLevel(),
|
|
30
|
+
tools: pi.getActiveTools(),
|
|
31
|
+
};
|
|
32
|
+
}
|