@d3ara1n/pi-command-palette 0.1.1 → 0.2.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 +26 -2
- package/package.json +2 -1
- package/src/config.ts +67 -0
- package/src/index.ts +69 -3
package/README.md
CHANGED
|
@@ -24,7 +24,7 @@ Or add to `~/.pi/agent/settings.json`:
|
|
|
24
24
|
|
|
25
25
|
| Shortcut | Action |
|
|
26
26
|
|----------|--------|
|
|
27
|
-
| `Ctrl+Shift+P` | Open command palette |
|
|
27
|
+
| `Ctrl+Shift+P` _(default, configurable)_ | Open command palette |
|
|
28
28
|
|
|
29
29
|
The palette lists:
|
|
30
30
|
|
|
@@ -42,4 +42,28 @@ The "Model: Switch Model" action opens a secondary overlay listing all models wi
|
|
|
42
42
|
|
|
43
43
|
## Configuration
|
|
44
44
|
|
|
45
|
-
|
|
45
|
+
The default shortcut is `Ctrl+Shift+P`. If it conflicts with your terminal, override it via either of the following (evaluated in order, first match wins).
|
|
46
|
+
|
|
47
|
+
### 1. Environment variable
|
|
48
|
+
|
|
49
|
+
Useful for terminals that intercept `Ctrl+Shift+<key>` before it reaches the session (e.g. Termius on Windows/WSL2):
|
|
50
|
+
|
|
51
|
+
```bash
|
|
52
|
+
export PI_COMMAND_PALETTE_KEY=ctrl+alt+k
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
Add it to your shell profile to persist (`~/.zshrc` on macOS, `~/.bashrc` on bash).
|
|
56
|
+
|
|
57
|
+
### 2. settings.json
|
|
58
|
+
|
|
59
|
+
Set `commandPalette.shortcut` in `~/.pi/agent/settings.json` (global) or `.pi/settings.json` in your project (project overrides global):
|
|
60
|
+
|
|
61
|
+
```jsonc
|
|
62
|
+
{
|
|
63
|
+
"commandPalette": {
|
|
64
|
+
"shortcut": "ctrl+alt+k"
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
Any valid pi keybinding string works (e.g. `ctrl+shift+k`, `ctrl+alt+p`, `ctrl+k`). Restart pi (or run `/reload`) after changing the shortcut.
|
package/package.json
CHANGED
package/src/config.ts
ADDED
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Resolve the command-palette shortcut key from configuration sources.
|
|
3
|
+
*
|
|
4
|
+
* Priority (highest wins):
|
|
5
|
+
* 1. `PI_COMMAND_PALETTE_KEY` env var — works even when the terminal intercepts
|
|
6
|
+
* `Ctrl+Shift+P` before it reaches the session (e.g. Termius on Windows/WSL2).
|
|
7
|
+
* 2. `settings.json` `commandPalette.shortcut` (project `.pi/settings.json`
|
|
8
|
+
* overrides global `~/.pi/agent/settings.json`)
|
|
9
|
+
* 3. default `"ctrl+shift+p"`
|
|
10
|
+
*
|
|
11
|
+
* Why not a CLI flag? Flags are applied to the extension runtime AFTER extensions
|
|
12
|
+
* load, so `pi.getFlag()` only returns the registered default at `registerShortcut()`
|
|
13
|
+
* time. Env vars and settings files are both available immediately at process
|
|
14
|
+
* start, so they are the correct mechanisms here.
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
import * as fs from "node:fs";
|
|
18
|
+
import * as os from "node:os";
|
|
19
|
+
import * as path from "node:path";
|
|
20
|
+
import type { KeyId } from "@earendil-works/pi-tui";
|
|
21
|
+
|
|
22
|
+
export const DEFAULT_SHORTCUT = "ctrl+shift+p";
|
|
23
|
+
|
|
24
|
+
function getAgentDir(): string {
|
|
25
|
+
const envDir = process.env.PI_AGENT_DIR;
|
|
26
|
+
if (envDir) return envDir;
|
|
27
|
+
return path.join(os.homedir(), ".pi", "agent");
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function readSettings(filePath: string): Record<string, unknown> {
|
|
31
|
+
try {
|
|
32
|
+
if (!fs.existsSync(filePath)) return {};
|
|
33
|
+
return JSON.parse(fs.readFileSync(filePath, "utf-8"));
|
|
34
|
+
} catch {
|
|
35
|
+
return {};
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/** Lowercase + trim; returns undefined for empty/non-string input. */
|
|
40
|
+
function normalizeKey(raw: unknown): string | undefined {
|
|
41
|
+
if (typeof raw !== "string") return undefined;
|
|
42
|
+
const trimmed = raw.trim().toLowerCase();
|
|
43
|
+
return trimmed || undefined;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Resolve the command-palette shortcut key from env var → settings → default.
|
|
48
|
+
*
|
|
49
|
+
* @param cwd - Project working directory, for project-level settings override.
|
|
50
|
+
* Defaults to `process.cwd()` (accurate at pi startup when shortcuts register).
|
|
51
|
+
*/
|
|
52
|
+
export function resolveShortcutKey(cwd: string = process.cwd()): KeyId {
|
|
53
|
+
// 1. Env var — highest priority, for terminals that intercept the default combo.
|
|
54
|
+
const envKey = normalizeKey(process.env.PI_COMMAND_PALETTE_KEY);
|
|
55
|
+
if (envKey) return envKey as KeyId;
|
|
56
|
+
|
|
57
|
+
// 2. settings.json — global ~/.pi/agent/settings.json; project overrides global.
|
|
58
|
+
const globalSettings = readSettings(path.join(getAgentDir(), "settings.json"));
|
|
59
|
+
const projectSettings = readSettings(path.join(cwd, ".pi", "settings.json"));
|
|
60
|
+
const globalCfg = (globalSettings.commandPalette ?? {}) as { shortcut?: unknown };
|
|
61
|
+
const projectCfg = (projectSettings.commandPalette ?? {}) as { shortcut?: unknown };
|
|
62
|
+
const settingsKey = normalizeKey(projectCfg.shortcut ?? globalCfg.shortcut);
|
|
63
|
+
if (settingsKey) return settingsKey as KeyId;
|
|
64
|
+
|
|
65
|
+
// 3. default
|
|
66
|
+
return DEFAULT_SHORTCUT as KeyId;
|
|
67
|
+
}
|
package/src/index.ts
CHANGED
|
@@ -17,9 +17,13 @@ import { DynamicBorder } from "@earendil-works/pi-coding-agent";
|
|
|
17
17
|
import {
|
|
18
18
|
Container,
|
|
19
19
|
type SelectItem,
|
|
20
|
+
fuzzyFilter,
|
|
21
|
+
Key,
|
|
22
|
+
matchesKey,
|
|
20
23
|
SelectList,
|
|
21
24
|
Text,
|
|
22
25
|
} from "@earendil-works/pi-tui";
|
|
26
|
+
import { resolveShortcutKey } from "./config.ts";
|
|
23
27
|
|
|
24
28
|
// ── Types ──────────────────────────────────────────────────────────
|
|
25
29
|
|
|
@@ -188,9 +192,25 @@ async function showModelSelector(pi: ExtensionAPI, ctx: ExtensionContext): Promi
|
|
|
188
192
|
selectList.onSelect = (item) => done(item.value);
|
|
189
193
|
selectList.onCancel = () => done(null);
|
|
190
194
|
|
|
195
|
+
// Type-to-filter state
|
|
196
|
+
let query = "";
|
|
197
|
+
const queryText = new Text(theme.fg("accent", "> "), 1, 0);
|
|
198
|
+
|
|
199
|
+
function applyQuery() {
|
|
200
|
+
const filtered = query
|
|
201
|
+
? fuzzyFilter(items, query, (item: SelectItem) => `${item.label} ${item.description ?? ""}`)
|
|
202
|
+
: items;
|
|
203
|
+
(selectList as any).filteredItems = filtered;
|
|
204
|
+
selectList.setSelectedIndex(0);
|
|
205
|
+
queryText.setText(theme.fg("accent", `> ${query}▎`));
|
|
206
|
+
container.invalidate();
|
|
207
|
+
tui.requestRender();
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
container.addChild(queryText);
|
|
191
211
|
container.addChild(selectList);
|
|
192
212
|
container.addChild(
|
|
193
|
-
new Text(theme.fg("dim", "↑↓ navigate • enter select • esc cancel"), 1, 0),
|
|
213
|
+
new Text(theme.fg("dim", "type to filter • ↑↓ navigate • enter select • esc cancel"), 1, 0),
|
|
194
214
|
);
|
|
195
215
|
container.addChild(new DynamicBorder((s: string) => theme.fg("accent", s)));
|
|
196
216
|
|
|
@@ -202,6 +222,21 @@ async function showModelSelector(pi: ExtensionAPI, ctx: ExtensionContext): Promi
|
|
|
202
222
|
container.invalidate();
|
|
203
223
|
},
|
|
204
224
|
handleInput(data: string) {
|
|
225
|
+
// Backspace → trim query
|
|
226
|
+
if (matchesKey(data, Key.backspace)) {
|
|
227
|
+
if (query.length > 0) {
|
|
228
|
+
query = query.slice(0, -1);
|
|
229
|
+
applyQuery();
|
|
230
|
+
}
|
|
231
|
+
return;
|
|
232
|
+
}
|
|
233
|
+
// Printable character → append to query
|
|
234
|
+
if (data.length === 1 && data.charCodeAt(0) >= 32) {
|
|
235
|
+
query += data;
|
|
236
|
+
applyQuery();
|
|
237
|
+
return;
|
|
238
|
+
}
|
|
239
|
+
// Navigation / confirm / cancel → pass to SelectList
|
|
205
240
|
selectList.handleInput(data);
|
|
206
241
|
tui.requestRender();
|
|
207
242
|
},
|
|
@@ -225,7 +260,7 @@ async function showModelSelector(pi: ExtensionAPI, ctx: ExtensionContext): Promi
|
|
|
225
260
|
// ── Command palette overlay ────────────────────────────────────────
|
|
226
261
|
|
|
227
262
|
async function showCommandPalette(pi: ExtensionAPI, ctx: ExtensionContext): Promise<void> {
|
|
228
|
-
if (ctx.
|
|
263
|
+
if (!ctx.hasUI) return;
|
|
229
264
|
|
|
230
265
|
const paletteItems = buildPaletteItems(pi, ctx);
|
|
231
266
|
const selectItems: SelectItem[] = paletteItems.map((item) => ({
|
|
@@ -256,6 +291,22 @@ async function showCommandPalette(pi: ExtensionAPI, ctx: ExtensionContext): Prom
|
|
|
256
291
|
};
|
|
257
292
|
selectList.onCancel = () => done(null);
|
|
258
293
|
|
|
294
|
+
// Type-to-filter state
|
|
295
|
+
let query = "";
|
|
296
|
+
const queryText = new Text(theme.fg("accent", "> "), 1, 0);
|
|
297
|
+
|
|
298
|
+
function applyQuery() {
|
|
299
|
+
const filtered = query
|
|
300
|
+
? fuzzyFilter(selectItems, query, (item: SelectItem) => `${item.label} ${item.description ?? ""}`)
|
|
301
|
+
: selectItems;
|
|
302
|
+
(selectList as any).filteredItems = filtered;
|
|
303
|
+
selectList.setSelectedIndex(0);
|
|
304
|
+
queryText.setText(theme.fg("accent", `> ${query}▎`));
|
|
305
|
+
container.invalidate();
|
|
306
|
+
tui.requestRender();
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
container.addChild(queryText);
|
|
259
310
|
container.addChild(selectList);
|
|
260
311
|
container.addChild(
|
|
261
312
|
new Text(theme.fg("dim", "type to filter • ↑↓ navigate • enter select • esc cancel"), 1, 0),
|
|
@@ -270,6 +321,21 @@ async function showCommandPalette(pi: ExtensionAPI, ctx: ExtensionContext): Prom
|
|
|
270
321
|
container.invalidate();
|
|
271
322
|
},
|
|
272
323
|
handleInput(data: string) {
|
|
324
|
+
// Backspace → trim query
|
|
325
|
+
if (matchesKey(data, Key.backspace)) {
|
|
326
|
+
if (query.length > 0) {
|
|
327
|
+
query = query.slice(0, -1);
|
|
328
|
+
applyQuery();
|
|
329
|
+
}
|
|
330
|
+
return;
|
|
331
|
+
}
|
|
332
|
+
// Printable character → append to query
|
|
333
|
+
if (data.length === 1 && data.charCodeAt(0) >= 32) {
|
|
334
|
+
query += data;
|
|
335
|
+
applyQuery();
|
|
336
|
+
return;
|
|
337
|
+
}
|
|
338
|
+
// Navigation / confirm / cancel → pass to SelectList
|
|
273
339
|
selectList.handleInput(data);
|
|
274
340
|
tui.requestRender();
|
|
275
341
|
},
|
|
@@ -324,7 +390,7 @@ async function showCommandPalette(pi: ExtensionAPI, ctx: ExtensionContext): Prom
|
|
|
324
390
|
// ── Extension entry point ──────────────────────────────────────────
|
|
325
391
|
|
|
326
392
|
export default function commandPaletteExtension(pi: ExtensionAPI) {
|
|
327
|
-
pi.registerShortcut(
|
|
393
|
+
pi.registerShortcut(resolveShortcutKey(), {
|
|
328
394
|
description: "Open command palette",
|
|
329
395
|
handler: async (ctx) => {
|
|
330
396
|
await showCommandPalette(pi, ctx);
|