@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 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
- No configuration needed. Works out of the box.
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
@@ -1,6 +1,7 @@
1
1
  {
2
2
  "name": "@d3ara1n/pi-command-palette",
3
- "version": "0.1.1",
3
+ "version": "0.2.0",
4
+ "type": "module",
4
5
  "description": "Global command palette for pi — press Ctrl+Shift+P to search and run commands from anywhere",
5
6
  "main": "src/index.ts",
6
7
  "keywords": [
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.mode !== "tui") return;
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("ctrl+shift+p", {
393
+ pi.registerShortcut(resolveShortcutKey(), {
328
394
  description: "Open command palette",
329
395
  handler: async (ctx) => {
330
396
  await showCommandPalette(pi, ctx);