@d3ara1n/pi-command-palette 0.2.0 → 0.3.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 +9 -3
- package/package.json +3 -2
- package/src/config.ts +24 -24
- package/src/index.ts +355 -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
|
|
@@ -14,9 +18,11 @@ pi install npm:@d3ara1n/pi-command-palette
|
|
|
14
18
|
|
|
15
19
|
Or add to `~/.pi/agent/settings.json`:
|
|
16
20
|
|
|
17
|
-
```
|
|
21
|
+
```jsonc
|
|
18
22
|
{
|
|
19
|
-
"extensions": [
|
|
23
|
+
"extensions": [
|
|
24
|
+
"/absolute/path/to/pi-extensions/packages/pi-command-palette"
|
|
25
|
+
]
|
|
20
26
|
}
|
|
21
27
|
```
|
|
22
28
|
|
package/package.json
CHANGED
|
@@ -1,12 +1,13 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@d3ara1n/pi-command-palette",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.0",
|
|
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
7
|
"keywords": [
|
|
8
8
|
"pi-package",
|
|
9
|
-
"pi"
|
|
9
|
+
"pi",
|
|
10
|
+
"pi-extension"
|
|
10
11
|
],
|
|
11
12
|
"peerDependencies": {
|
|
12
13
|
"@earendil-works/pi-ai": "*",
|
package/src/config.ts
CHANGED
|
@@ -22,25 +22,25 @@ import type { KeyId } from "@earendil-works/pi-tui";
|
|
|
22
22
|
export const DEFAULT_SHORTCUT = "ctrl+shift+p";
|
|
23
23
|
|
|
24
24
|
function getAgentDir(): string {
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
25
|
+
const envDir = process.env.PI_AGENT_DIR;
|
|
26
|
+
if (envDir) return envDir;
|
|
27
|
+
return path.join(os.homedir(), ".pi", "agent");
|
|
28
28
|
}
|
|
29
29
|
|
|
30
30
|
function readSettings(filePath: string): Record<string, unknown> {
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
31
|
+
try {
|
|
32
|
+
if (!fs.existsSync(filePath)) return {};
|
|
33
|
+
return JSON.parse(fs.readFileSync(filePath, "utf-8"));
|
|
34
|
+
} catch {
|
|
35
|
+
return {};
|
|
36
|
+
}
|
|
37
37
|
}
|
|
38
38
|
|
|
39
39
|
/** Lowercase + trim; returns undefined for empty/non-string input. */
|
|
40
40
|
function normalizeKey(raw: unknown): string | undefined {
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
41
|
+
if (typeof raw !== "string") return undefined;
|
|
42
|
+
const trimmed = raw.trim().toLowerCase();
|
|
43
|
+
return trimmed || undefined;
|
|
44
44
|
}
|
|
45
45
|
|
|
46
46
|
/**
|
|
@@ -50,18 +50,18 @@ function normalizeKey(raw: unknown): string | undefined {
|
|
|
50
50
|
* Defaults to `process.cwd()` (accurate at pi startup when shortcuts register).
|
|
51
51
|
*/
|
|
52
52
|
export function resolveShortcutKey(cwd: string = process.cwd()): KeyId {
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
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
56
|
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
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
64
|
|
|
65
|
-
|
|
66
|
-
|
|
65
|
+
// 3. default
|
|
66
|
+
return DEFAULT_SHORTCUT as KeyId;
|
|
67
67
|
}
|
package/src/index.ts
CHANGED
|
@@ -15,31 +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
26
|
import { resolveShortcutKey } from "./config.ts";
|
|
27
27
|
|
|
28
28
|
// ── Types ──────────────────────────────────────────────────────────
|
|
29
29
|
|
|
30
30
|
type CommandAction =
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
31
|
+
| { type: "editor"; text: string }
|
|
32
|
+
| { type: "model-select" }
|
|
33
|
+
| { type: "compact" }
|
|
34
|
+
| { type: "reload" }
|
|
35
|
+
| { type: "restore" };
|
|
36
36
|
|
|
37
37
|
interface PaletteItem {
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
38
|
+
value: string;
|
|
39
|
+
label: string;
|
|
40
|
+
description: string;
|
|
41
|
+
category: string;
|
|
42
|
+
action: CommandAction;
|
|
43
43
|
}
|
|
44
44
|
|
|
45
45
|
// ── Module state ───────────────────────────────────────────────────
|
|
@@ -50,350 +50,357 @@ let savedEditorText: string | null = null;
|
|
|
50
50
|
// ── Helpers ────────────────────────────────────────────────────────
|
|
51
51
|
|
|
52
52
|
function buildPaletteItems(pi: ExtensionAPI, ctx: ExtensionContext): PaletteItem[] {
|
|
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
|
-
|
|
149
|
-
if (a.category === "Built-in" && b.category !== "Built-in") return -1;
|
|
150
|
-
if (a.category !== "Built-in" && b.category === "Built-in") return 1;
|
|
151
|
-
return a.label.localeCompare(b.label);
|
|
152
|
-
});
|
|
153
|
-
|
|
154
|
-
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;
|
|
155
149
|
}
|
|
156
150
|
|
|
157
151
|
// ── Model selector ─────────────────────────────────────────────────
|
|
158
152
|
|
|
159
153
|
async function showModelSelector(pi: ExtensionAPI, ctx: ExtensionContext): Promise<void> {
|
|
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
|
-
|
|
257
|
-
|
|
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
|
+
// FRAGILE: SelectList has no public filter/setItems API, so we poke its
|
|
203
|
+
// private filteredItems directly. If pi-tui renames it, filtering breaks
|
|
204
|
+
// silently with no compile error.
|
|
205
|
+
(selectList as any).filteredItems = filtered;
|
|
206
|
+
selectList.setSelectedIndex(0);
|
|
207
|
+
queryText.setText(theme.fg("accent", `> ${query}▎`));
|
|
208
|
+
container.invalidate();
|
|
209
|
+
tui.requestRender();
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
container.addChild(queryText);
|
|
213
|
+
container.addChild(selectList);
|
|
214
|
+
container.addChild(
|
|
215
|
+
new Text(theme.fg("dim", "type to filter • ↑↓ navigate • enter select • esc cancel"), 1, 0),
|
|
216
|
+
);
|
|
217
|
+
container.addChild(new DynamicBorder((s: string) => theme.fg("accent", s)));
|
|
218
|
+
|
|
219
|
+
return {
|
|
220
|
+
render(w: number) {
|
|
221
|
+
return container.render(w);
|
|
222
|
+
},
|
|
223
|
+
invalidate() {
|
|
224
|
+
container.invalidate();
|
|
225
|
+
},
|
|
226
|
+
handleInput(data: string) {
|
|
227
|
+
// Backspace → trim query
|
|
228
|
+
if (matchesKey(data, Key.backspace)) {
|
|
229
|
+
if (query.length > 0) {
|
|
230
|
+
query = query.slice(0, -1);
|
|
231
|
+
applyQuery();
|
|
232
|
+
}
|
|
233
|
+
return;
|
|
234
|
+
}
|
|
235
|
+
// Printable character → append to query
|
|
236
|
+
if (data.length === 1 && data.charCodeAt(0) >= 32) {
|
|
237
|
+
query += data;
|
|
238
|
+
applyQuery();
|
|
239
|
+
return;
|
|
240
|
+
}
|
|
241
|
+
// Navigation / confirm / cancel → pass to SelectList
|
|
242
|
+
selectList.handleInput(data);
|
|
243
|
+
tui.requestRender();
|
|
244
|
+
},
|
|
245
|
+
};
|
|
246
|
+
},
|
|
247
|
+
{ overlay: true },
|
|
248
|
+
);
|
|
249
|
+
|
|
250
|
+
if (!result) return;
|
|
251
|
+
|
|
252
|
+
const [provider, modelId] = result.split("/");
|
|
253
|
+
const model = ctx.modelRegistry.find(provider, modelId);
|
|
254
|
+
if (model) {
|
|
255
|
+
const success = await pi.setModel(model);
|
|
256
|
+
if (success) {
|
|
257
|
+
ctx.ui.notify(`Model: ${provider}/${modelId}`, "info");
|
|
258
|
+
} else {
|
|
259
|
+
ctx.ui.notify(`No API key for ${provider}/${modelId}`, "error");
|
|
260
|
+
}
|
|
261
|
+
}
|
|
258
262
|
}
|
|
259
263
|
|
|
260
264
|
// ── Command palette overlay ────────────────────────────────────────
|
|
261
265
|
|
|
262
266
|
async function showCommandPalette(pi: ExtensionAPI, ctx: ExtensionContext): Promise<void> {
|
|
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
|
-
|
|
387
|
-
|
|
267
|
+
if (!ctx.hasUI) return;
|
|
268
|
+
|
|
269
|
+
const paletteItems = buildPaletteItems(pi, ctx);
|
|
270
|
+
const selectItems: SelectItem[] = paletteItems.map((item) => ({
|
|
271
|
+
value: item.value,
|
|
272
|
+
label: item.label,
|
|
273
|
+
description: item.description,
|
|
274
|
+
}));
|
|
275
|
+
|
|
276
|
+
const result = await ctx.ui.custom<PaletteItem | null>(
|
|
277
|
+
(tui, theme, _kb, done) => {
|
|
278
|
+
const container = new Container();
|
|
279
|
+
container.addChild(new DynamicBorder((s: string) => theme.fg("accent", s)));
|
|
280
|
+
container.addChild(new Text(theme.fg("accent", theme.bold("Command Palette")), 1, 0));
|
|
281
|
+
|
|
282
|
+
const selectList = new SelectList(selectItems, Math.min(selectItems.length, 15), {
|
|
283
|
+
selectedPrefix: (t: string) => theme.fg("accent", t),
|
|
284
|
+
selectedText: (t: string) => theme.fg("accent", t),
|
|
285
|
+
description: (t: string) => theme.fg("muted", t),
|
|
286
|
+
scrollInfo: (t: string) => theme.fg("dim", t),
|
|
287
|
+
noMatch: (t: string) => theme.fg("warning", t),
|
|
288
|
+
});
|
|
289
|
+
|
|
290
|
+
selectList.onSelect = (item) => {
|
|
291
|
+
const paletteItem = paletteItems.find((p) => p.value === item.value);
|
|
292
|
+
done(paletteItem ?? null);
|
|
293
|
+
};
|
|
294
|
+
selectList.onCancel = () => done(null);
|
|
295
|
+
|
|
296
|
+
// Type-to-filter state
|
|
297
|
+
let query = "";
|
|
298
|
+
const queryText = new Text(theme.fg("accent", "> "), 1, 0);
|
|
299
|
+
|
|
300
|
+
function applyQuery() {
|
|
301
|
+
const filtered = query
|
|
302
|
+
? fuzzyFilter(
|
|
303
|
+
selectItems,
|
|
304
|
+
query,
|
|
305
|
+
(item: SelectItem) => `${item.label} ${item.description ?? ""}`,
|
|
306
|
+
)
|
|
307
|
+
: selectItems;
|
|
308
|
+
// FRAGILE: see model selector — depends on SelectList.filteredItems.
|
|
309
|
+
(selectList as any).filteredItems = filtered;
|
|
310
|
+
selectList.setSelectedIndex(0);
|
|
311
|
+
queryText.setText(theme.fg("accent", `> ${query}▎`));
|
|
312
|
+
container.invalidate();
|
|
313
|
+
tui.requestRender();
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
container.addChild(queryText);
|
|
317
|
+
container.addChild(selectList);
|
|
318
|
+
container.addChild(
|
|
319
|
+
new Text(theme.fg("dim", "type to filter • ↑↓ navigate • enter select • esc cancel"), 1, 0),
|
|
320
|
+
);
|
|
321
|
+
container.addChild(new DynamicBorder((s: string) => theme.fg("accent", s)));
|
|
322
|
+
|
|
323
|
+
return {
|
|
324
|
+
render(w: number) {
|
|
325
|
+
return container.render(w);
|
|
326
|
+
},
|
|
327
|
+
invalidate() {
|
|
328
|
+
container.invalidate();
|
|
329
|
+
},
|
|
330
|
+
handleInput(data: string) {
|
|
331
|
+
// Backspace → trim query
|
|
332
|
+
if (matchesKey(data, Key.backspace)) {
|
|
333
|
+
if (query.length > 0) {
|
|
334
|
+
query = query.slice(0, -1);
|
|
335
|
+
applyQuery();
|
|
336
|
+
}
|
|
337
|
+
return;
|
|
338
|
+
}
|
|
339
|
+
// Printable character → append to query
|
|
340
|
+
if (data.length === 1 && data.charCodeAt(0) >= 32) {
|
|
341
|
+
query += data;
|
|
342
|
+
applyQuery();
|
|
343
|
+
return;
|
|
344
|
+
}
|
|
345
|
+
// Navigation / confirm / cancel → pass to SelectList
|
|
346
|
+
selectList.handleInput(data);
|
|
347
|
+
tui.requestRender();
|
|
348
|
+
},
|
|
349
|
+
};
|
|
350
|
+
},
|
|
351
|
+
{ overlay: true },
|
|
352
|
+
);
|
|
353
|
+
|
|
354
|
+
if (!result) return;
|
|
355
|
+
|
|
356
|
+
// Execute the selected action
|
|
357
|
+
const action = result.action;
|
|
358
|
+
switch (action.type) {
|
|
359
|
+
case "restore": {
|
|
360
|
+
if (savedEditorText !== null) {
|
|
361
|
+
ctx.ui.setEditorText(savedEditorText);
|
|
362
|
+
savedEditorText = null;
|
|
363
|
+
}
|
|
364
|
+
break;
|
|
365
|
+
}
|
|
366
|
+
case "editor": {
|
|
367
|
+
// Save current editor text before overwriting, so user can restore
|
|
368
|
+
const currentText = ctx.ui.getEditorText();
|
|
369
|
+
if (currentText && currentText.trim()) {
|
|
370
|
+
savedEditorText = currentText;
|
|
371
|
+
}
|
|
372
|
+
ctx.ui.setEditorText(action.text);
|
|
373
|
+
break;
|
|
374
|
+
}
|
|
375
|
+
case "model-select": {
|
|
376
|
+
await showModelSelector(pi, ctx);
|
|
377
|
+
break;
|
|
378
|
+
}
|
|
379
|
+
case "compact": {
|
|
380
|
+
ctx.compact({
|
|
381
|
+
onComplete: () => ctx.ui.notify("Compaction completed", "info"),
|
|
382
|
+
onError: (err) => ctx.ui.notify(`Compaction failed: ${err.message}`, "error"),
|
|
383
|
+
});
|
|
384
|
+
break;
|
|
385
|
+
}
|
|
386
|
+
case "reload": {
|
|
387
|
+
const currentText = ctx.ui.getEditorText();
|
|
388
|
+
if (currentText && currentText.trim()) {
|
|
389
|
+
savedEditorText = currentText;
|
|
390
|
+
}
|
|
391
|
+
ctx.ui.setEditorText("/reload");
|
|
392
|
+
break;
|
|
393
|
+
}
|
|
394
|
+
}
|
|
388
395
|
}
|
|
389
396
|
|
|
390
397
|
// ── Extension entry point ──────────────────────────────────────────
|
|
391
398
|
|
|
392
399
|
export default function commandPaletteExtension(pi: ExtensionAPI) {
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
400
|
+
pi.registerShortcut(resolveShortcutKey(), {
|
|
401
|
+
description: "Open command palette",
|
|
402
|
+
handler: async (ctx) => {
|
|
403
|
+
await showCommandPalette(pi, ctx);
|
|
404
|
+
},
|
|
405
|
+
});
|
|
399
406
|
}
|