@d3ara1n/pi-command-palette 0.3.0 → 0.4.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 +11 -9
- package/package.json +1 -1
- package/src/config.ts +9 -8
- package/src/index.test.ts +25 -0
- package/src/index.ts +70 -7
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# pi-command-palette
|
|
2
2
|
|
|
3
|
-
Global command palette for [Pi Coding Agent](https://pi.dev) — press **Ctrl+
|
|
3
|
+
Global command palette for [Pi Coding Agent](https://pi.dev) — press **Ctrl+Alt+P** to search and run commands from anywhere.
|
|
4
4
|
|
|
5
5
|
## Why?
|
|
6
6
|
|
|
@@ -18,7 +18,7 @@ pi install npm:@d3ara1n/pi-command-palette
|
|
|
18
18
|
|
|
19
19
|
Or add to `~/.pi/agent/settings.json`:
|
|
20
20
|
|
|
21
|
-
```
|
|
21
|
+
```json
|
|
22
22
|
{
|
|
23
23
|
"extensions": [
|
|
24
24
|
"/absolute/path/to/pi-extensions/packages/pi-command-palette"
|
|
@@ -30,7 +30,7 @@ Or add to `~/.pi/agent/settings.json`:
|
|
|
30
30
|
|
|
31
31
|
| Shortcut | Action |
|
|
32
32
|
|----------|--------|
|
|
33
|
-
| `Ctrl+
|
|
33
|
+
| `Ctrl+Alt+P` _(default, configurable)_ | Open command palette |
|
|
34
34
|
|
|
35
35
|
The palette lists:
|
|
36
36
|
|
|
@@ -46,30 +46,32 @@ When a command replaces your current editor text, the original content is saved
|
|
|
46
46
|
|
|
47
47
|
The "Model: Switch Model" action opens a secondary overlay listing all models with configured API keys. Select one to switch instantly — no need to go through `/model` or `Ctrl+P`.
|
|
48
48
|
|
|
49
|
+
**Scoped models float to the top**, marked with a ★ (favorite) prefix. "Scoped" here means the same set pi uses for its built-in selector's scoped tab and `Ctrl+P` cycling — the `enabledModels` patterns in your `settings.json` (project `.pi/settings.json` overrides global `~/.pi/agent/settings.json`). Everything else follows alphabetically. If no scope is configured, the list is a plain alphabetical roster — nothing breaks.
|
|
50
|
+
|
|
49
51
|
## Configuration
|
|
50
52
|
|
|
51
|
-
The default shortcut is `Ctrl+
|
|
53
|
+
The default shortcut is `Ctrl+Alt+P`. This avoids pi's built-in `Ctrl+P` / `Ctrl+Shift+P` model cycling shortcuts while keeping the palette mnemonic. If it conflicts with your terminal, override it via either of the following (evaluated in order, first match wins).
|
|
52
54
|
|
|
53
55
|
### 1. Environment variable
|
|
54
56
|
|
|
55
57
|
Useful for terminals that intercept `Ctrl+Shift+<key>` before it reaches the session (e.g. Termius on Windows/WSL2):
|
|
56
58
|
|
|
57
59
|
```bash
|
|
58
|
-
export PI_COMMAND_PALETTE_KEY=ctrl+
|
|
60
|
+
export PI_COMMAND_PALETTE_KEY=ctrl+shift+p
|
|
59
61
|
```
|
|
60
62
|
|
|
61
63
|
Add it to your shell profile to persist (`~/.zshrc` on macOS, `~/.bashrc` on bash).
|
|
62
64
|
|
|
63
65
|
### 2. settings.json
|
|
64
66
|
|
|
65
|
-
Set `commandPalette.shortcut` in `~/.pi/agent/settings.json` (global) or `.pi/settings.json` in your project
|
|
67
|
+
Set `commandPalette.shortcut` in `~/.pi/agent/settings.json` (global) or `.pi/settings.json` in your project. A present project `commandPalette` block replaces the global block:
|
|
66
68
|
|
|
67
|
-
```
|
|
69
|
+
```json
|
|
68
70
|
{
|
|
69
71
|
"commandPalette": {
|
|
70
|
-
"shortcut": "ctrl+
|
|
72
|
+
"shortcut": "ctrl+shift+p"
|
|
71
73
|
}
|
|
72
74
|
}
|
|
73
75
|
```
|
|
74
76
|
|
|
75
|
-
Any valid pi keybinding string works (e.g. `ctrl+shift+k`, `ctrl+alt+
|
|
77
|
+
Any valid pi keybinding string works (e.g. `ctrl+shift+p`, `ctrl+shift+k`, `ctrl+alt+k`, `ctrl+k`). Restart pi (or run `/reload`) after changing the shortcut.
|
package/package.json
CHANGED
package/src/config.ts
CHANGED
|
@@ -4,9 +4,9 @@
|
|
|
4
4
|
* Priority (highest wins):
|
|
5
5
|
* 1. `PI_COMMAND_PALETTE_KEY` env var — works even when the terminal intercepts
|
|
6
6
|
* `Ctrl+Shift+P` before it reaches the session (e.g. Termius on Windows/WSL2).
|
|
7
|
-
* 2. `settings.json` `commandPalette.shortcut` (project
|
|
8
|
-
*
|
|
9
|
-
* 3. default `"ctrl+
|
|
7
|
+
* 2. `settings.json` `commandPalette.shortcut` (a present project
|
|
8
|
+
* `commandPalette` block replaces the global block)
|
|
9
|
+
* 3. default `"ctrl+alt+p"`
|
|
10
10
|
*
|
|
11
11
|
* Why not a CLI flag? Flags are applied to the extension runtime AFTER extensions
|
|
12
12
|
* load, so `pi.getFlag()` only returns the registered default at `registerShortcut()`
|
|
@@ -19,7 +19,7 @@ import * as os from "node:os";
|
|
|
19
19
|
import * as path from "node:path";
|
|
20
20
|
import type { KeyId } from "@earendil-works/pi-tui";
|
|
21
21
|
|
|
22
|
-
export const DEFAULT_SHORTCUT = "ctrl+
|
|
22
|
+
export const DEFAULT_SHORTCUT = "ctrl+alt+p";
|
|
23
23
|
|
|
24
24
|
function getAgentDir(): string {
|
|
25
25
|
const envDir = process.env.PI_AGENT_DIR;
|
|
@@ -54,12 +54,13 @@ export function resolveShortcutKey(cwd: string = process.cwd()): KeyId {
|
|
|
54
54
|
const envKey = normalizeKey(process.env.PI_COMMAND_PALETTE_KEY);
|
|
55
55
|
if (envKey) return envKey as KeyId;
|
|
56
56
|
|
|
57
|
-
// 2. settings.json —
|
|
57
|
+
// 2. settings.json — a present project block replaces the global block.
|
|
58
58
|
const globalSettings = readSettings(path.join(getAgentDir(), "settings.json"));
|
|
59
59
|
const projectSettings = readSettings(path.join(cwd, ".pi", "settings.json"));
|
|
60
|
-
const
|
|
61
|
-
const
|
|
62
|
-
const
|
|
60
|
+
const globalRaw = globalSettings.commandPalette;
|
|
61
|
+
const projectRaw = projectSettings.commandPalette;
|
|
62
|
+
const config = (projectRaw ?? globalRaw ?? {}) as { shortcut?: unknown };
|
|
63
|
+
const settingsKey = normalizeKey(config.shortcut);
|
|
63
64
|
if (settingsKey) return settingsKey as KeyId;
|
|
64
65
|
|
|
65
66
|
// 3. default
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Regression tests for model reference parsing.
|
|
3
|
+
* Run: node --test packages/pi-command-palette/src/index.test.ts
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import assert from "node:assert/strict";
|
|
7
|
+
import { test } from "node:test";
|
|
8
|
+
import { parseModelRef } from "./index.ts";
|
|
9
|
+
|
|
10
|
+
test("parseModelRef splits provider and model at the first slash", () => {
|
|
11
|
+
assert.deepEqual(parseModelRef("anthropic/claude-sonnet"), {
|
|
12
|
+
provider: "anthropic",
|
|
13
|
+
modelId: "claude-sonnet",
|
|
14
|
+
});
|
|
15
|
+
assert.deepEqual(parseModelRef("openrouter/vendor/model/with/slashes"), {
|
|
16
|
+
provider: "openrouter",
|
|
17
|
+
modelId: "vendor/model/with/slashes",
|
|
18
|
+
});
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
test("parseModelRef preserves empty provider or model segments", () => {
|
|
22
|
+
assert.equal(parseModelRef("model-without-provider"), undefined);
|
|
23
|
+
assert.deepEqual(parseModelRef("/model"), { provider: "", modelId: "model" });
|
|
24
|
+
assert.deepEqual(parseModelRef("provider/"), { provider: "provider", modelId: "" });
|
|
25
|
+
});
|
package/src/index.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* pi-command-palette — Global command palette for pi.
|
|
3
3
|
*
|
|
4
|
-
* Press Ctrl+
|
|
4
|
+
* Press Ctrl+Alt+P to open a searchable command palette overlay,
|
|
5
5
|
* regardless of whether the editor has content.
|
|
6
6
|
*
|
|
7
7
|
* Features:
|
|
@@ -13,7 +13,12 @@
|
|
|
13
13
|
*/
|
|
14
14
|
|
|
15
15
|
import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
16
|
-
import {
|
|
16
|
+
import {
|
|
17
|
+
DynamicBorder,
|
|
18
|
+
getAgentDir,
|
|
19
|
+
resolveModelScopeWithDiagnostics,
|
|
20
|
+
SettingsManager,
|
|
21
|
+
} from "@earendil-works/pi-coding-agent";
|
|
17
22
|
import {
|
|
18
23
|
Container,
|
|
19
24
|
type SelectItem,
|
|
@@ -150,6 +155,48 @@ function buildPaletteItems(pi: ExtensionAPI, ctx: ExtensionContext): PaletteItem
|
|
|
150
155
|
|
|
151
156
|
// ── Model selector ─────────────────────────────────────────────────
|
|
152
157
|
|
|
158
|
+
const STAR = "★ ";
|
|
159
|
+
|
|
160
|
+
/**
|
|
161
|
+
* @internal — exported for testing; parses the selector's `provider/model-id` values.
|
|
162
|
+
*/
|
|
163
|
+
export function parseModelRef(modelRef: string): { provider: string; modelId: string } | undefined {
|
|
164
|
+
const slash = modelRef.indexOf("/");
|
|
165
|
+
if (slash === -1) return undefined;
|
|
166
|
+
return {
|
|
167
|
+
provider: modelRef.slice(0, slash),
|
|
168
|
+
modelId: modelRef.slice(slash + 1),
|
|
169
|
+
};
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
/**
|
|
173
|
+
* Resolve the set of "scoped" model full-ids (`provider/id`) — the same models
|
|
174
|
+
* pi surfaces in its built-in selector's "scoped" tab and Ctrl+P cycling.
|
|
175
|
+
*
|
|
176
|
+
* Fully official-API driven, no manual settings parsing:
|
|
177
|
+
* - `SettingsManager.getEnabledModels()` reads the `enabledModels` scope
|
|
178
|
+
* patterns (global + project merge handled by pi).
|
|
179
|
+
* - `resolveModelScopeWithDiagnostics()` expands those patterns (globs, aliases,
|
|
180
|
+
* thinking-level suffixes) into concrete models — identical to pi's scope tab.
|
|
181
|
+
*
|
|
182
|
+
* Any failure degrades to an empty set: the selector still works, just without
|
|
183
|
+
* the scoped grouping.
|
|
184
|
+
*/
|
|
185
|
+
async function resolveScopedModelIds(
|
|
186
|
+
modelRegistry: ExtensionContext["modelRegistry"],
|
|
187
|
+
cwd: string,
|
|
188
|
+
): Promise<Set<string>> {
|
|
189
|
+
try {
|
|
190
|
+
const settings = SettingsManager.create(cwd, getAgentDir());
|
|
191
|
+
const patterns = settings.getEnabledModels();
|
|
192
|
+
if (!patterns || patterns.length === 0) return new Set();
|
|
193
|
+
const { scopedModels } = await resolveModelScopeWithDiagnostics(patterns, modelRegistry);
|
|
194
|
+
return new Set(scopedModels.map((s) => `${s.model.provider}/${s.model.id}`));
|
|
195
|
+
} catch {
|
|
196
|
+
return new Set();
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
|
|
153
200
|
async function showModelSelector(pi: ExtensionAPI, ctx: ExtensionContext): Promise<void> {
|
|
154
201
|
let models: Awaited<ReturnType<typeof ctx.modelRegistry.getAvailable>>;
|
|
155
202
|
try {
|
|
@@ -164,10 +211,24 @@ async function showModelSelector(pi: ExtensionAPI, ctx: ExtensionContext): Promi
|
|
|
164
211
|
return;
|
|
165
212
|
}
|
|
166
213
|
|
|
167
|
-
const
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
214
|
+
const scopedIds = await resolveScopedModelIds(ctx.modelRegistry, ctx.cwd);
|
|
215
|
+
|
|
216
|
+
// Scoped models float to the top with a ★ prefix (favorites); the rest follow
|
|
217
|
+
// alphabetically. Within the scoped group we also sort alphabetically so the
|
|
218
|
+
// ordering is stable and predictable regardless of registry order.
|
|
219
|
+
const decorated = models.map((m) => {
|
|
220
|
+
const value = `${m.provider}/${m.id}`;
|
|
221
|
+
return { model: m, value, scoped: scopedIds.has(value) };
|
|
222
|
+
});
|
|
223
|
+
decorated.sort((a, b) => {
|
|
224
|
+
if (a.scoped !== b.scoped) return a.scoped ? -1 : 1;
|
|
225
|
+
return a.model.name.localeCompare(b.model.name);
|
|
226
|
+
});
|
|
227
|
+
|
|
228
|
+
const items: SelectItem[] = decorated.map((d) => ({
|
|
229
|
+
value: d.value,
|
|
230
|
+
label: d.scoped ? `${STAR}${d.model.name}` : d.model.name,
|
|
231
|
+
description: d.model.provider,
|
|
171
232
|
}));
|
|
172
233
|
|
|
173
234
|
const result = await ctx.ui.custom<string | null>(
|
|
@@ -249,7 +310,9 @@ async function showModelSelector(pi: ExtensionAPI, ctx: ExtensionContext): Promi
|
|
|
249
310
|
|
|
250
311
|
if (!result) return;
|
|
251
312
|
|
|
252
|
-
const
|
|
313
|
+
const parsed = parseModelRef(result);
|
|
314
|
+
if (!parsed) return;
|
|
315
|
+
const { provider, modelId } = parsed;
|
|
253
316
|
const model = ctx.modelRegistry.find(provider, modelId);
|
|
254
317
|
if (model) {
|
|
255
318
|
const success = await pi.setModel(model);
|