@d3ara1n/pi-command-palette 0.5.2 → 0.5.3
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 +21 -1
- package/package.json +4 -1
- package/src/index.test.ts +62 -6
- package/src/index.ts +55 -16
package/README.md
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
# pi-command-palette
|
|
2
2
|
|
|
3
|
+
[](https://www.npmjs.com/package/@d3ara1n/pi-command-palette) [](https://www.npmjs.com/package/@d3ara1n/pi-command-palette) [](https://www.npmjs.com/package/@d3ara1n/pi-command-palette)
|
|
4
|
+
|
|
3
5
|
Global command palette for [Pi Coding Agent](https://pi.dev) — press **Ctrl+Shift+P** to search and run commands from anywhere.
|
|
4
6
|
|
|
5
7
|
## Why?
|
|
@@ -8,7 +10,7 @@ Pi's slash commands (`/model`, `/compact`, extension commands, etc.) only work w
|
|
|
8
10
|
|
|
9
11
|
## Dependencies
|
|
10
12
|
|
|
11
|
-
|
|
13
|
+
- [`@d3ara1n/pi-command-palette-core`](../pi-command-palette-core) — shared registry for native palette commands (pure npm library, installed automatically)
|
|
12
14
|
|
|
13
15
|
## Installation
|
|
14
16
|
|
|
@@ -35,6 +37,7 @@ Or add to `~/.pi/agent/settings.json`:
|
|
|
35
37
|
The palette lists:
|
|
36
38
|
|
|
37
39
|
- **Built-in actions** — curated shortcuts for common operations (detailed below)
|
|
40
|
+
- **Native commands** — entries registered by other extensions that run a callback directly (see below)
|
|
38
41
|
- **Extension commands** — All registered `/command` entries
|
|
39
42
|
- **Skills & Templates** — Skill commands and prompt templates
|
|
40
43
|
|
|
@@ -64,6 +67,23 @@ Built-in actions are grouped by how they run:
|
|
|
64
67
|
|
|
65
68
|
> Pi ships with more built-in slash commands (e.g. `/export`, `/share`, `/name`, `/settings`). This palette only surfaces a curated subset above — for the rest, type them directly into the editor.
|
|
66
69
|
|
|
70
|
+
### Native commands from other extensions
|
|
71
|
+
|
|
72
|
+
Extensions built on [`@d3ara1n/pi-command-palette-core`](../pi-command-palette-core) can register palette entries backed by a **direct callback** instead of a `/command` editor fill. They appear above the extension-command entries, and selecting one runs the callback in place — your editor text is never touched, saved, or restored:
|
|
73
|
+
|
|
74
|
+
```ts
|
|
75
|
+
import { paletteCommandRegistry } from "@d3ara1n/pi-command-palette-core";
|
|
76
|
+
|
|
77
|
+
paletteCommandRegistry.register({
|
|
78
|
+
id: "my-plugin:do-thing",
|
|
79
|
+
label: "My Plugin: Do the Thing",
|
|
80
|
+
description: "Runs immediately, without touching the editor",
|
|
81
|
+
run: (pi, ctx) => { /* ... */ },
|
|
82
|
+
});
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
The registry is read every time the palette opens, so commands can be registered and unregistered at any time. Failures inside `run` are caught and surfaced as an error notification. See the [core package](../pi-command-palette-core) for the full API.
|
|
86
|
+
|
|
67
87
|
### Editor text preservation
|
|
68
88
|
|
|
69
89
|
When a command replaces your editor text, or you run **Editor: Clear Content**, the original content is saved and a **Restore: Previous Editor Text** entry appears at the top of the palette. Select it to get your text back.
|
package/package.json
CHANGED
|
@@ -1,9 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@d3ara1n/pi-command-palette",
|
|
3
|
-
"version": "0.5.
|
|
3
|
+
"version": "0.5.3",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Global command palette for pi — press Ctrl+Shift+P to search and run commands from anywhere",
|
|
6
6
|
"main": "src/index.ts",
|
|
7
|
+
"dependencies": {
|
|
8
|
+
"@d3ara1n/pi-command-palette-core": "*"
|
|
9
|
+
},
|
|
7
10
|
"keywords": [
|
|
8
11
|
"pi-package",
|
|
9
12
|
"pi",
|
package/src/index.test.ts
CHANGED
|
@@ -1,11 +1,19 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Regression tests for model reference parsing
|
|
3
|
-
* filter that keeps scoped models on top while searching
|
|
2
|
+
* Regression tests for model reference parsing, the partitioned fuzzy
|
|
3
|
+
* filter that keeps scoped models on top while searching, and the palette
|
|
4
|
+
* item ordering that keeps built-ins → native commands → editor-fill entries.
|
|
4
5
|
*/
|
|
5
6
|
|
|
6
7
|
import assert from "node:assert/strict";
|
|
7
|
-
import { test } from "node:test";
|
|
8
|
-
import {
|
|
8
|
+
import { after, test } from "node:test";
|
|
9
|
+
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
10
|
+
import { paletteCommandRegistry } from "@d3ara1n/pi-command-palette-core";
|
|
11
|
+
import { buildPaletteItems, parseModelRef, partitionedFuzzyFilter } from "./index.ts";
|
|
12
|
+
|
|
13
|
+
/** Minimal fake of the pi API surface buildPaletteItems uses. */
|
|
14
|
+
function fakePi(commands: { name: string; description?: string }[]): ExtensionAPI {
|
|
15
|
+
return { getCommands: () => commands } as unknown as ExtensionAPI;
|
|
16
|
+
}
|
|
9
17
|
|
|
10
18
|
test("parseModelRef splits provider and model at the first slash", () => {
|
|
11
19
|
assert.deepEqual(parseModelRef("anthropic/claude-sonnet"), {
|
|
@@ -64,7 +72,10 @@ test("partitionedFuzzyFilter drops non-matches independently per partition", ()
|
|
|
64
72
|
|
|
65
73
|
const result = partitionedFuzzyFilter(primary, secondary, "keep", getText);
|
|
66
74
|
|
|
67
|
-
assert.deepEqual(
|
|
75
|
+
assert.deepEqual(
|
|
76
|
+
result.map((m) => m.label),
|
|
77
|
+
["keep-scoped", "keep-other"],
|
|
78
|
+
);
|
|
68
79
|
});
|
|
69
80
|
|
|
70
81
|
test("partitionedFuzzyFilter returns only primary matches when secondary has none", () => {
|
|
@@ -74,5 +85,50 @@ test("partitionedFuzzyFilter returns only primary matches when secondary has non
|
|
|
74
85
|
|
|
75
86
|
const result = partitionedFuzzyFilter(primary, secondary, "son", getText);
|
|
76
87
|
|
|
77
|
-
assert.deepEqual(
|
|
88
|
+
assert.deepEqual(
|
|
89
|
+
result.map((m) => m.label),
|
|
90
|
+
["sonnet"],
|
|
91
|
+
);
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
// ── buildPaletteItems ordering ─────────────────────────────────────
|
|
95
|
+
|
|
96
|
+
const idsBefore = new Set(paletteCommandRegistry.getAll().map((c) => c.id));
|
|
97
|
+
after(() => {
|
|
98
|
+
for (const c of paletteCommandRegistry.getAll()) {
|
|
99
|
+
if (!idsBefore.has(c.id)) paletteCommandRegistry.unregister(c.id);
|
|
100
|
+
}
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
test("buildPaletteItems orders built-ins above native commands above editor fills", () => {
|
|
104
|
+
paletteCommandRegistry.register({
|
|
105
|
+
id: "test:peek",
|
|
106
|
+
label: "Peek: Ask This Session",
|
|
107
|
+
run: () => {},
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
const items = buildPaletteItems(
|
|
111
|
+
fakePi([{ name: "some-command", description: "extension command" }]),
|
|
112
|
+
);
|
|
113
|
+
|
|
114
|
+
const ranks = items.map((item) =>
|
|
115
|
+
item.category === "Built-in" ? 0 : item.action.type === "native" ? 1 : 2,
|
|
116
|
+
);
|
|
117
|
+
// Monotonically non-decreasing → no editor-fill entry sits above a native
|
|
118
|
+
// entry, and no native entry sits above a built-in.
|
|
119
|
+
assert.ok(ranks.every((r, i) => i === 0 || ranks[i - 1] <= r));
|
|
120
|
+
|
|
121
|
+
const native = items.find((item) => item.value === "native:test:peek");
|
|
122
|
+
assert.ok(native);
|
|
123
|
+
assert.equal(native.label, "Peek: Ask This Session");
|
|
124
|
+
assert.equal(native.action.type, "native");
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
test("buildPaletteItems picks up native commands registered after load", () => {
|
|
128
|
+
// The registry is read at palette-open time, so a late registration must
|
|
129
|
+
// show up on the next build without any re-init.
|
|
130
|
+
paletteCommandRegistry.register({ id: "test:late", label: "Registered Late", run: () => {} });
|
|
131
|
+
|
|
132
|
+
const items = buildPaletteItems(fakePi([]));
|
|
133
|
+
assert.ok(items.some((item) => item.value === "native:test:late"));
|
|
78
134
|
});
|
package/src/index.ts
CHANGED
|
@@ -14,10 +14,8 @@
|
|
|
14
14
|
*/
|
|
15
15
|
|
|
16
16
|
import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
17
|
-
import {
|
|
18
|
-
|
|
19
|
-
DynamicBorder,
|
|
20
|
-
} from "@earendil-works/pi-coding-agent";
|
|
17
|
+
import { copyToClipboard, DynamicBorder } from "@earendil-works/pi-coding-agent";
|
|
18
|
+
import { paletteCommandRegistry } from "@d3ara1n/pi-command-palette-core";
|
|
21
19
|
import {
|
|
22
20
|
Container,
|
|
23
21
|
type SelectItem,
|
|
@@ -33,6 +31,7 @@ import { resolveShortcutKey } from "./config.ts";
|
|
|
33
31
|
|
|
34
32
|
type CommandAction =
|
|
35
33
|
| { type: "editor"; text: string }
|
|
34
|
+
| { type: "native"; id: string }
|
|
36
35
|
| { type: "model-select" }
|
|
37
36
|
| { type: "compact" }
|
|
38
37
|
| { type: "reload" }
|
|
@@ -67,7 +66,22 @@ const BUILTIN_ORDER: Record<string, number> = {
|
|
|
67
66
|
|
|
68
67
|
// ── Helpers ────────────────────────────────────────────────────────
|
|
69
68
|
|
|
70
|
-
|
|
69
|
+
/**
|
|
70
|
+
* Sort ranks: built-in actions first, then native commands registered by other
|
|
71
|
+
* extensions (direct callbacks), then everything that fills the editor with a
|
|
72
|
+
* `/command`. Lower rank = higher up in the palette.
|
|
73
|
+
*/
|
|
74
|
+
function paletteSortRank(item: PaletteItem): number {
|
|
75
|
+
if (item.category === "Built-in") return 0;
|
|
76
|
+
if (item.action.type === "native") return 1;
|
|
77
|
+
return 2;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* @internal — exported for testing; builds the palette item list from
|
|
82
|
+
* built-ins, the native-command registry, and pi's command registry.
|
|
83
|
+
*/
|
|
84
|
+
export function buildPaletteItems(pi: ExtensionAPI): PaletteItem[] {
|
|
71
85
|
const items: PaletteItem[] = [];
|
|
72
86
|
|
|
73
87
|
// ── Restore option (if previous editor text was saved) ────────
|
|
@@ -156,6 +170,20 @@ function buildPaletteItems(pi: ExtensionAPI, ctx: ExtensionContext): PaletteItem
|
|
|
156
170
|
action: { type: "clear-editor" },
|
|
157
171
|
});
|
|
158
172
|
|
|
173
|
+
// ── Native commands from other extensions ────────────────────
|
|
174
|
+
// Direct callbacks registered via @d3ara1n/pi-command-palette-core —
|
|
175
|
+
// executed in place, never touching the editor. Read at palette-open time,
|
|
176
|
+
// so late registrations are visible the next time the palette opens.
|
|
177
|
+
for (const cmd of paletteCommandRegistry.getAll()) {
|
|
178
|
+
items.push({
|
|
179
|
+
value: `native:${cmd.id}`,
|
|
180
|
+
label: cmd.label,
|
|
181
|
+
description: cmd.description ?? "",
|
|
182
|
+
category: "Native",
|
|
183
|
+
action: { type: "native", id: cmd.id },
|
|
184
|
+
});
|
|
185
|
+
}
|
|
186
|
+
|
|
159
187
|
// ── Extension commands, skills, templates ────────────────────
|
|
160
188
|
const commands = pi.getCommands();
|
|
161
189
|
for (const cmd of commands) {
|
|
@@ -172,13 +200,13 @@ function buildPaletteItems(pi: ExtensionAPI, ctx: ExtensionContext): PaletteItem
|
|
|
172
200
|
});
|
|
173
201
|
}
|
|
174
202
|
|
|
175
|
-
// Sort: built-in actions first
|
|
176
|
-
//
|
|
203
|
+
// Sort: built-in actions first (ordered by BUILTIN_ORDER, then alphabetical),
|
|
204
|
+
// then native commands, then editor-fill entries — each group alphabetical.
|
|
177
205
|
items.sort((a, b) => {
|
|
178
|
-
const
|
|
179
|
-
const
|
|
180
|
-
if (
|
|
181
|
-
if (
|
|
206
|
+
const ar = paletteSortRank(a);
|
|
207
|
+
const br = paletteSortRank(b);
|
|
208
|
+
if (ar !== br) return ar - br;
|
|
209
|
+
if (ar === 0) {
|
|
182
210
|
const ai = BUILTIN_ORDER[a.value] ?? Number.MAX_SAFE_INTEGER;
|
|
183
211
|
const bi = BUILTIN_ORDER[b.value] ?? Number.MAX_SAFE_INTEGER;
|
|
184
212
|
if (ai !== bi) return ai - bi;
|
|
@@ -226,10 +254,7 @@ export function partitionedFuzzyFilter<T>(
|
|
|
226
254
|
getText: (item: T) => string,
|
|
227
255
|
): T[] {
|
|
228
256
|
if (!query.trim()) return [...primary, ...secondary];
|
|
229
|
-
return [
|
|
230
|
-
...fuzzyFilter(primary, query, getText),
|
|
231
|
-
...fuzzyFilter(secondary, query, getText),
|
|
232
|
-
];
|
|
257
|
+
return [...fuzzyFilter(primary, query, getText), ...fuzzyFilter(secondary, query, getText)];
|
|
233
258
|
}
|
|
234
259
|
|
|
235
260
|
async function showModelSelector(pi: ExtensionAPI, ctx: ExtensionContext): Promise<void> {
|
|
@@ -374,7 +399,7 @@ async function showModelSelector(pi: ExtensionAPI, ctx: ExtensionContext): Promi
|
|
|
374
399
|
async function showCommandPalette(pi: ExtensionAPI, ctx: ExtensionContext): Promise<void> {
|
|
375
400
|
if (!ctx.hasUI) return;
|
|
376
401
|
|
|
377
|
-
const paletteItems = buildPaletteItems(pi
|
|
402
|
+
const paletteItems = buildPaletteItems(pi);
|
|
378
403
|
const selectItems: SelectItem[] = paletteItems.map((item) => ({
|
|
379
404
|
value: item.value,
|
|
380
405
|
label: item.label,
|
|
@@ -464,6 +489,20 @@ async function showCommandPalette(pi: ExtensionAPI, ctx: ExtensionContext): Prom
|
|
|
464
489
|
// Execute the selected action
|
|
465
490
|
const action = result.action;
|
|
466
491
|
switch (action.type) {
|
|
492
|
+
case "native": {
|
|
493
|
+
const cmd = paletteCommandRegistry.get(action.id);
|
|
494
|
+
if (!cmd) {
|
|
495
|
+
ctx.ui.notify(`Palette command not found: ${action.id}`, "warning");
|
|
496
|
+
break;
|
|
497
|
+
}
|
|
498
|
+
try {
|
|
499
|
+
await cmd.run(pi, ctx);
|
|
500
|
+
} catch (err) {
|
|
501
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
502
|
+
ctx.ui.notify(`Palette command "${cmd.label}" failed: ${message}`, "error");
|
|
503
|
+
}
|
|
504
|
+
break;
|
|
505
|
+
}
|
|
467
506
|
case "restore": {
|
|
468
507
|
if (savedEditorText !== null) {
|
|
469
508
|
ctx.ui.setEditorText(savedEditorText);
|