@d3ara1n/pi-command-palette 0.1.2 → 0.2.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 +31 -3
- package/package.json +1 -1
- package/src/config.ts +67 -0
- package/src/index.ts +352 -348
package/README.md
CHANGED
|
@@ -6,7 +6,11 @@ Global command palette for [Pi Coding Agent](https://pi.dev) — press **Ctrl+Sh
|
|
|
6
6
|
|
|
7
7
|
Pi's slash commands (`/model`, `/compact`, extension commands, etc.) only work when the editor is empty. If you've typed something and want to switch models or run a command, you're stuck. This extension opens a floating command palette via keyboard shortcut, regardless of editor state.
|
|
8
8
|
|
|
9
|
-
##
|
|
9
|
+
## Dependencies
|
|
10
|
+
|
|
11
|
+
None.
|
|
12
|
+
|
|
13
|
+
## Installation
|
|
10
14
|
|
|
11
15
|
```bash
|
|
12
16
|
pi install npm:@d3ara1n/pi-command-palette
|
|
@@ -24,7 +28,7 @@ Or add to `~/.pi/agent/settings.json`:
|
|
|
24
28
|
|
|
25
29
|
| Shortcut | Action |
|
|
26
30
|
|----------|--------|
|
|
27
|
-
| `Ctrl+Shift+P` | Open command palette |
|
|
31
|
+
| `Ctrl+Shift+P` _(default, configurable)_ | Open command palette |
|
|
28
32
|
|
|
29
33
|
The palette lists:
|
|
30
34
|
|
|
@@ -42,4 +46,28 @@ The "Model: Switch Model" action opens a secondary overlay listing all models wi
|
|
|
42
46
|
|
|
43
47
|
## Configuration
|
|
44
48
|
|
|
45
|
-
|
|
49
|
+
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).
|
|
50
|
+
|
|
51
|
+
### 1. Environment variable
|
|
52
|
+
|
|
53
|
+
Useful for terminals that intercept `Ctrl+Shift+<key>` before it reaches the session (e.g. Termius on Windows/WSL2):
|
|
54
|
+
|
|
55
|
+
```bash
|
|
56
|
+
export PI_COMMAND_PALETTE_KEY=ctrl+alt+k
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
Add it to your shell profile to persist (`~/.zshrc` on macOS, `~/.bashrc` on bash).
|
|
60
|
+
|
|
61
|
+
### 2. settings.json
|
|
62
|
+
|
|
63
|
+
Set `commandPalette.shortcut` in `~/.pi/agent/settings.json` (global) or `.pi/settings.json` in your project (project overrides global):
|
|
64
|
+
|
|
65
|
+
```jsonc
|
|
66
|
+
{
|
|
67
|
+
"commandPalette": {
|
|
68
|
+
"shortcut": "ctrl+alt+k"
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
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
|
@@ -15,30 +15,31 @@
|
|
|
15
15
|
import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
16
16
|
import { DynamicBorder } from "@earendil-works/pi-coding-agent";
|
|
17
17
|
import {
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
18
|
+
Container,
|
|
19
|
+
type SelectItem,
|
|
20
|
+
fuzzyFilter,
|
|
21
|
+
Key,
|
|
22
|
+
matchesKey,
|
|
23
|
+
SelectList,
|
|
24
|
+
Text,
|
|
25
25
|
} from "@earendil-works/pi-tui";
|
|
26
|
+
import { resolveShortcutKey } from "./config.ts";
|
|
26
27
|
|
|
27
28
|
// ── Types ──────────────────────────────────────────────────────────
|
|
28
29
|
|
|
29
30
|
type CommandAction =
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
31
|
+
| { type: "editor"; text: string }
|
|
32
|
+
| { type: "model-select" }
|
|
33
|
+
| { type: "compact" }
|
|
34
|
+
| { type: "reload" }
|
|
35
|
+
| { type: "restore" };
|
|
35
36
|
|
|
36
37
|
interface PaletteItem {
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
38
|
+
value: string;
|
|
39
|
+
label: string;
|
|
40
|
+
description: string;
|
|
41
|
+
category: string;
|
|
42
|
+
action: CommandAction;
|
|
42
43
|
}
|
|
43
44
|
|
|
44
45
|
// ── Module state ───────────────────────────────────────────────────
|
|
@@ -49,350 +50,353 @@ let savedEditorText: string | null = null;
|
|
|
49
50
|
// ── Helpers ────────────────────────────────────────────────────────
|
|
50
51
|
|
|
51
52
|
function buildPaletteItems(pi: ExtensionAPI, ctx: ExtensionContext): PaletteItem[] {
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
if (a.category === "Built-in" && b.category !== "Built-in") return -1;
|
|
149
|
-
if (a.category !== "Built-in" && b.category === "Built-in") return 1;
|
|
150
|
-
return a.label.localeCompare(b.label);
|
|
151
|
-
});
|
|
152
|
-
|
|
153
|
-
return items;
|
|
53
|
+
const items: PaletteItem[] = [];
|
|
54
|
+
|
|
55
|
+
// ── Restore option (if previous editor text was saved) ────────
|
|
56
|
+
if (savedEditorText) {
|
|
57
|
+
const preview =
|
|
58
|
+
savedEditorText.length > 40 ? `${savedEditorText.slice(0, 37)}...` : savedEditorText;
|
|
59
|
+
items.push({
|
|
60
|
+
value: "__restore",
|
|
61
|
+
label: "Restore: Previous Editor Text",
|
|
62
|
+
description: preview.replace(/\n/g, "⏎"),
|
|
63
|
+
category: "Built-in",
|
|
64
|
+
action: { type: "restore" },
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// ── Built-in actions ──────────────────────────────────────────
|
|
69
|
+
items.push({
|
|
70
|
+
value: "__model_select",
|
|
71
|
+
label: "Model: Switch Model",
|
|
72
|
+
description: "Select a model from the registry",
|
|
73
|
+
category: "Built-in",
|
|
74
|
+
action: { type: "model-select" },
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
items.push({
|
|
78
|
+
value: "__new_session",
|
|
79
|
+
label: "Session: New",
|
|
80
|
+
description: "Start a new session",
|
|
81
|
+
category: "Built-in",
|
|
82
|
+
action: { type: "editor", text: "/new" },
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
items.push({
|
|
86
|
+
value: "__compact",
|
|
87
|
+
label: "Session: Compact",
|
|
88
|
+
description: "Compact conversation to free context",
|
|
89
|
+
category: "Built-in",
|
|
90
|
+
action: { type: "compact" },
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
items.push({
|
|
94
|
+
value: "__reload",
|
|
95
|
+
label: "Session: Reload",
|
|
96
|
+
description: "Reload extensions, skills, and config",
|
|
97
|
+
category: "Built-in",
|
|
98
|
+
action: { type: "reload" },
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
items.push({
|
|
102
|
+
value: "__fork",
|
|
103
|
+
label: "Session: Fork",
|
|
104
|
+
description: "Fork from selected entry",
|
|
105
|
+
category: "Built-in",
|
|
106
|
+
action: { type: "editor", text: "/fork" },
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
items.push({
|
|
110
|
+
value: "__tree",
|
|
111
|
+
label: "Session: Tree",
|
|
112
|
+
description: "Navigate session tree",
|
|
113
|
+
category: "Built-in",
|
|
114
|
+
action: { type: "editor", text: "/tree" },
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
items.push({
|
|
118
|
+
value: "__resume",
|
|
119
|
+
label: "Session: Resume",
|
|
120
|
+
description: "Resume a previous session",
|
|
121
|
+
category: "Built-in",
|
|
122
|
+
action: { type: "editor", text: "/resume" },
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
// ── Extension commands, skills, templates ────────────────────
|
|
126
|
+
const commands = pi.getCommands();
|
|
127
|
+
for (const cmd of commands) {
|
|
128
|
+
const editorText = `/${cmd.name}`;
|
|
129
|
+
const sourceLabel =
|
|
130
|
+
cmd.source === "extension" ? "Command" : cmd.source === "skill" ? "Skill" : "Template";
|
|
131
|
+
|
|
132
|
+
items.push({
|
|
133
|
+
value: `cmd:${cmd.name}`,
|
|
134
|
+
label: `${sourceLabel}: /${cmd.name}`,
|
|
135
|
+
description: cmd.description ?? "",
|
|
136
|
+
category: sourceLabel,
|
|
137
|
+
action: { type: "editor", text: editorText },
|
|
138
|
+
});
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
// Sort: built-in first, then alphabetically within category
|
|
142
|
+
items.sort((a, b) => {
|
|
143
|
+
if (a.category === "Built-in" && b.category !== "Built-in") return -1;
|
|
144
|
+
if (a.category !== "Built-in" && b.category === "Built-in") return 1;
|
|
145
|
+
return a.label.localeCompare(b.label);
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
return items;
|
|
154
149
|
}
|
|
155
150
|
|
|
156
151
|
// ── Model selector ─────────────────────────────────────────────────
|
|
157
152
|
|
|
158
153
|
async function showModelSelector(pi: ExtensionAPI, ctx: ExtensionContext): Promise<void> {
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
154
|
+
let models: Awaited<ReturnType<typeof ctx.modelRegistry.getAvailable>>;
|
|
155
|
+
try {
|
|
156
|
+
models = await ctx.modelRegistry.getAvailable();
|
|
157
|
+
} catch {
|
|
158
|
+
ctx.ui.notify("Cannot enumerate models. Use Ctrl+L instead.", "warning");
|
|
159
|
+
return;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
if (models.length === 0) {
|
|
163
|
+
ctx.ui.notify("No models available.", "warning");
|
|
164
|
+
return;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
const items: SelectItem[] = models.map((m) => ({
|
|
168
|
+
value: `${m.provider}/${m.id}`,
|
|
169
|
+
label: m.name,
|
|
170
|
+
description: m.provider,
|
|
171
|
+
}));
|
|
172
|
+
|
|
173
|
+
const result = await ctx.ui.custom<string | null>(
|
|
174
|
+
(tui, theme, _kb, done) => {
|
|
175
|
+
const container = new Container();
|
|
176
|
+
container.addChild(new DynamicBorder((s: string) => theme.fg("accent", s)));
|
|
177
|
+
container.addChild(new Text(theme.fg("accent", theme.bold("Switch Model")), 1, 0));
|
|
178
|
+
|
|
179
|
+
const selectList = new SelectList(items, Math.min(items.length, 12), {
|
|
180
|
+
selectedPrefix: (t: string) => theme.fg("accent", t),
|
|
181
|
+
selectedText: (t: string) => theme.fg("accent", t),
|
|
182
|
+
description: (t: string) => theme.fg("muted", t),
|
|
183
|
+
scrollInfo: (t: string) => theme.fg("dim", t),
|
|
184
|
+
noMatch: (t: string) => theme.fg("warning", t),
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
selectList.onSelect = (item) => done(item.value);
|
|
188
|
+
selectList.onCancel = () => done(null);
|
|
189
|
+
|
|
190
|
+
// Type-to-filter state
|
|
191
|
+
let query = "";
|
|
192
|
+
const queryText = new Text(theme.fg("accent", "> "), 1, 0);
|
|
193
|
+
|
|
194
|
+
function applyQuery() {
|
|
195
|
+
const filtered = query
|
|
196
|
+
? fuzzyFilter(
|
|
197
|
+
items,
|
|
198
|
+
query,
|
|
199
|
+
(item: SelectItem) => `${item.label} ${item.description ?? ""}`,
|
|
200
|
+
)
|
|
201
|
+
: items;
|
|
202
|
+
(selectList as any).filteredItems = filtered;
|
|
203
|
+
selectList.setSelectedIndex(0);
|
|
204
|
+
queryText.setText(theme.fg("accent", `> ${query}▎`));
|
|
205
|
+
container.invalidate();
|
|
206
|
+
tui.requestRender();
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
container.addChild(queryText);
|
|
210
|
+
container.addChild(selectList);
|
|
211
|
+
container.addChild(
|
|
212
|
+
new Text(theme.fg("dim", "type to filter • ↑↓ navigate • enter select • esc cancel"), 1, 0),
|
|
213
|
+
);
|
|
214
|
+
container.addChild(new DynamicBorder((s: string) => theme.fg("accent", s)));
|
|
215
|
+
|
|
216
|
+
return {
|
|
217
|
+
render(w: number) {
|
|
218
|
+
return container.render(w);
|
|
219
|
+
},
|
|
220
|
+
invalidate() {
|
|
221
|
+
container.invalidate();
|
|
222
|
+
},
|
|
223
|
+
handleInput(data: string) {
|
|
224
|
+
// Backspace → trim query
|
|
225
|
+
if (matchesKey(data, Key.backspace)) {
|
|
226
|
+
if (query.length > 0) {
|
|
227
|
+
query = query.slice(0, -1);
|
|
228
|
+
applyQuery();
|
|
229
|
+
}
|
|
230
|
+
return;
|
|
231
|
+
}
|
|
232
|
+
// Printable character → append to query
|
|
233
|
+
if (data.length === 1 && data.charCodeAt(0) >= 32) {
|
|
234
|
+
query += data;
|
|
235
|
+
applyQuery();
|
|
236
|
+
return;
|
|
237
|
+
}
|
|
238
|
+
// Navigation / confirm / cancel → pass to SelectList
|
|
239
|
+
selectList.handleInput(data);
|
|
240
|
+
tui.requestRender();
|
|
241
|
+
},
|
|
242
|
+
};
|
|
243
|
+
},
|
|
244
|
+
{ overlay: true },
|
|
245
|
+
);
|
|
246
|
+
|
|
247
|
+
if (!result) return;
|
|
248
|
+
|
|
249
|
+
const [provider, modelId] = result.split("/");
|
|
250
|
+
const model = ctx.modelRegistry.find(provider, modelId);
|
|
251
|
+
if (model) {
|
|
252
|
+
const success = await pi.setModel(model);
|
|
253
|
+
if (success) {
|
|
254
|
+
ctx.ui.notify(`Model: ${provider}/${modelId}`, "info");
|
|
255
|
+
} else {
|
|
256
|
+
ctx.ui.notify(`No API key for ${provider}/${modelId}`, "error");
|
|
257
|
+
}
|
|
258
|
+
}
|
|
257
259
|
}
|
|
258
260
|
|
|
259
261
|
// ── Command palette overlay ────────────────────────────────────────
|
|
260
262
|
|
|
261
263
|
async function showCommandPalette(pi: ExtensionAPI, ctx: ExtensionContext): Promise<void> {
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
264
|
+
if (!ctx.hasUI) return;
|
|
265
|
+
|
|
266
|
+
const paletteItems = buildPaletteItems(pi, ctx);
|
|
267
|
+
const selectItems: SelectItem[] = paletteItems.map((item) => ({
|
|
268
|
+
value: item.value,
|
|
269
|
+
label: item.label,
|
|
270
|
+
description: item.description,
|
|
271
|
+
}));
|
|
272
|
+
|
|
273
|
+
const result = await ctx.ui.custom<PaletteItem | null>(
|
|
274
|
+
(tui, theme, _kb, done) => {
|
|
275
|
+
const container = new Container();
|
|
276
|
+
container.addChild(new DynamicBorder((s: string) => theme.fg("accent", s)));
|
|
277
|
+
container.addChild(new Text(theme.fg("accent", theme.bold("Command Palette")), 1, 0));
|
|
278
|
+
|
|
279
|
+
const selectList = new SelectList(selectItems, Math.min(selectItems.length, 15), {
|
|
280
|
+
selectedPrefix: (t: string) => theme.fg("accent", t),
|
|
281
|
+
selectedText: (t: string) => theme.fg("accent", t),
|
|
282
|
+
description: (t: string) => theme.fg("muted", t),
|
|
283
|
+
scrollInfo: (t: string) => theme.fg("dim", t),
|
|
284
|
+
noMatch: (t: string) => theme.fg("warning", t),
|
|
285
|
+
});
|
|
286
|
+
|
|
287
|
+
selectList.onSelect = (item) => {
|
|
288
|
+
const paletteItem = paletteItems.find((p) => p.value === item.value);
|
|
289
|
+
done(paletteItem ?? null);
|
|
290
|
+
};
|
|
291
|
+
selectList.onCancel = () => done(null);
|
|
292
|
+
|
|
293
|
+
// Type-to-filter state
|
|
294
|
+
let query = "";
|
|
295
|
+
const queryText = new Text(theme.fg("accent", "> "), 1, 0);
|
|
296
|
+
|
|
297
|
+
function applyQuery() {
|
|
298
|
+
const filtered = query
|
|
299
|
+
? fuzzyFilter(
|
|
300
|
+
selectItems,
|
|
301
|
+
query,
|
|
302
|
+
(item: SelectItem) => `${item.label} ${item.description ?? ""}`,
|
|
303
|
+
)
|
|
304
|
+
: selectItems;
|
|
305
|
+
(selectList as any).filteredItems = filtered;
|
|
306
|
+
selectList.setSelectedIndex(0);
|
|
307
|
+
queryText.setText(theme.fg("accent", `> ${query}▎`));
|
|
308
|
+
container.invalidate();
|
|
309
|
+
tui.requestRender();
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
container.addChild(queryText);
|
|
313
|
+
container.addChild(selectList);
|
|
314
|
+
container.addChild(
|
|
315
|
+
new Text(theme.fg("dim", "type to filter • ↑↓ navigate • enter select • esc cancel"), 1, 0),
|
|
316
|
+
);
|
|
317
|
+
container.addChild(new DynamicBorder((s: string) => theme.fg("accent", s)));
|
|
318
|
+
|
|
319
|
+
return {
|
|
320
|
+
render(w: number) {
|
|
321
|
+
return container.render(w);
|
|
322
|
+
},
|
|
323
|
+
invalidate() {
|
|
324
|
+
container.invalidate();
|
|
325
|
+
},
|
|
326
|
+
handleInput(data: string) {
|
|
327
|
+
// Backspace → trim query
|
|
328
|
+
if (matchesKey(data, Key.backspace)) {
|
|
329
|
+
if (query.length > 0) {
|
|
330
|
+
query = query.slice(0, -1);
|
|
331
|
+
applyQuery();
|
|
332
|
+
}
|
|
333
|
+
return;
|
|
334
|
+
}
|
|
335
|
+
// Printable character → append to query
|
|
336
|
+
if (data.length === 1 && data.charCodeAt(0) >= 32) {
|
|
337
|
+
query += data;
|
|
338
|
+
applyQuery();
|
|
339
|
+
return;
|
|
340
|
+
}
|
|
341
|
+
// Navigation / confirm / cancel → pass to SelectList
|
|
342
|
+
selectList.handleInput(data);
|
|
343
|
+
tui.requestRender();
|
|
344
|
+
},
|
|
345
|
+
};
|
|
346
|
+
},
|
|
347
|
+
{ overlay: true },
|
|
348
|
+
);
|
|
349
|
+
|
|
350
|
+
if (!result) return;
|
|
351
|
+
|
|
352
|
+
// Execute the selected action
|
|
353
|
+
const action = result.action;
|
|
354
|
+
switch (action.type) {
|
|
355
|
+
case "restore": {
|
|
356
|
+
if (savedEditorText !== null) {
|
|
357
|
+
ctx.ui.setEditorText(savedEditorText);
|
|
358
|
+
savedEditorText = null;
|
|
359
|
+
}
|
|
360
|
+
break;
|
|
361
|
+
}
|
|
362
|
+
case "editor": {
|
|
363
|
+
// Save current editor text before overwriting, so user can restore
|
|
364
|
+
const currentText = ctx.ui.getEditorText();
|
|
365
|
+
if (currentText && currentText.trim()) {
|
|
366
|
+
savedEditorText = currentText;
|
|
367
|
+
}
|
|
368
|
+
ctx.ui.setEditorText(action.text);
|
|
369
|
+
break;
|
|
370
|
+
}
|
|
371
|
+
case "model-select": {
|
|
372
|
+
await showModelSelector(pi, ctx);
|
|
373
|
+
break;
|
|
374
|
+
}
|
|
375
|
+
case "compact": {
|
|
376
|
+
ctx.compact({
|
|
377
|
+
onComplete: () => ctx.ui.notify("Compaction completed", "info"),
|
|
378
|
+
onError: (err) => ctx.ui.notify(`Compaction failed: ${err.message}`, "error"),
|
|
379
|
+
});
|
|
380
|
+
break;
|
|
381
|
+
}
|
|
382
|
+
case "reload": {
|
|
383
|
+
const currentText = ctx.ui.getEditorText();
|
|
384
|
+
if (currentText && currentText.trim()) {
|
|
385
|
+
savedEditorText = currentText;
|
|
386
|
+
}
|
|
387
|
+
ctx.ui.setEditorText("/reload");
|
|
388
|
+
break;
|
|
389
|
+
}
|
|
390
|
+
}
|
|
387
391
|
}
|
|
388
392
|
|
|
389
393
|
// ── Extension entry point ──────────────────────────────────────────
|
|
390
394
|
|
|
391
395
|
export default function commandPaletteExtension(pi: ExtensionAPI) {
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
396
|
+
pi.registerShortcut(resolveShortcutKey(), {
|
|
397
|
+
description: "Open command palette",
|
|
398
|
+
handler: async (ctx) => {
|
|
399
|
+
await showCommandPalette(pi, ctx);
|
|
400
|
+
},
|
|
401
|
+
});
|
|
398
402
|
}
|