@d3ara1n/pi-command-palette 0.6.0 → 0.7.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 +8 -10
- package/package.json +1 -1
- package/src/index.test.ts +16 -23
- package/src/index.ts +102 -110
package/README.md
CHANGED
|
@@ -34,17 +34,16 @@ Or add to `~/.pi/agent/settings.json`:
|
|
|
34
34
|
|----------|--------|
|
|
35
35
|
| `Ctrl+Shift+P` _(default, configurable)_ | Open command palette |
|
|
36
36
|
|
|
37
|
-
The palette opens as a single macOS-launcher-style overlay with nested pages.
|
|
37
|
+
The palette opens as a single macOS-launcher-style overlay with nested pages. The root page mixes leaves and sub-pages: built-in actions and the model selector sit directly on the root, while the remaining categories open as sub-pages — selecting one with **Enter** replaces the current list in the same overlay instead of opening a second overlay. Press **Backspace** with an empty search field to return to the parent page; press **Esc** to close the palette immediately.
|
|
38
38
|
|
|
39
|
-
The
|
|
39
|
+
The root page lists:
|
|
40
40
|
|
|
41
|
-
- **Built-in
|
|
42
|
-
- **
|
|
43
|
-
- **
|
|
44
|
-
- **Skills** — installed
|
|
45
|
-
- **Templates** — prompt templates
|
|
41
|
+
- **Built-in actions** — curated shortcuts for common operations, shown directly on the root page so urgent entries like Restore never hide behind a sub-page (detailed below)
|
|
42
|
+
- **Models** — a sub-page listing every model with a configured API key (see below)
|
|
43
|
+
- **Extension Actions** — a sub-page of entries registered by other extensions that run a callback directly (see below)
|
|
44
|
+
- **Commands** / **Skills** / **Templates** — sub-pages for all registered `/command` entries, installed skills, and prompt templates; entries are labeled with their bare `/name` since the breadcrumb already names the category
|
|
46
45
|
|
|
47
|
-
Use **↑/↓** to move through entries and **←/→** to edit the search cursor. Search is fuzzy within the current page and updates as you type; Backspace uses the normal text-editing behavior while the query is non-empty.
|
|
46
|
+
Use **↑/↓** to move through entries and **←/→** to edit the search cursor. Search is fuzzy within the current page and updates as you type; searching the root page also matches entries from every sub-page, with each match's category shown next to its description. Backspace uses the normal text-editing behavior while the query is non-empty.
|
|
48
47
|
|
|
49
48
|
### Built-in actions
|
|
50
49
|
|
|
@@ -54,7 +53,6 @@ Built-in actions are grouped by how they run:
|
|
|
54
53
|
|
|
55
54
|
| Action | What it does |
|
|
56
55
|
|--------|--------------|
|
|
57
|
-
| Model: Switch Model | Open a model selector overlay; switch instantly |
|
|
58
56
|
| Session: Compact | Compact the conversation right away |
|
|
59
57
|
| Editor: Copy Content | Copy current editor text to the clipboard |
|
|
60
58
|
| Editor: Clear Content | Clear the editor, saving the current text to the restore buffer |
|
|
@@ -95,7 +93,7 @@ When a command replaces your editor text, or you run **Editor: Clear Content**,
|
|
|
95
93
|
|
|
96
94
|
### Model selector
|
|
97
95
|
|
|
98
|
-
The "
|
|
96
|
+
The "Models" entry opens a model page inside the same overlay. Models are loaded when the page is first entered, then can be searched and selected without stacking another overlay.
|
|
99
97
|
|
|
100
98
|
**Scoped models float to the top**, marked with a ★ (favorite) prefix. "Scoped" here means the same set pi uses for its built-in selector's scoped tab and `Ctrl+P` cycling — the `enabledModels` patterns in your `settings.json` (project `.pi/settings.json` overrides global `~/.pi/agent/settings.json`). Everything else follows alphabetically. Filtering preserves that boundary too — scoped matches stay above the rest while you type, rather than collapsing into one score-ordered list. If no scope is configured, the list is a plain alphabetical roster — nothing breaks.
|
|
101
99
|
|
package/package.json
CHANGED
package/src/index.test.ts
CHANGED
|
@@ -1,37 +1,22 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Regression tests for
|
|
3
|
-
*
|
|
4
|
-
*
|
|
2
|
+
* Regression tests for the partitioned fuzzy filter that keeps scoped
|
|
3
|
+
* models on top while searching, and the palette item ordering that keeps
|
|
4
|
+
* built-ins → native commands → editor-fill entries.
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
7
|
import assert from "node:assert/strict";
|
|
8
8
|
import { after, test } from "node:test";
|
|
9
9
|
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
10
10
|
import { paletteCommandRegistry } from "@d3ara1n/pi-command-palette-core";
|
|
11
|
-
import { buildPaletteItems,
|
|
11
|
+
import { buildPaletteItems, partitionedFuzzyFilter } from "./index.ts";
|
|
12
12
|
|
|
13
13
|
/** Minimal fake of the pi API surface buildPaletteItems uses. */
|
|
14
|
-
function fakePi(
|
|
14
|
+
function fakePi(
|
|
15
|
+
commands: { name: string; description?: string; source?: "extension" | "skill" | "template" }[],
|
|
16
|
+
): ExtensionAPI {
|
|
15
17
|
return { getCommands: () => commands } as unknown as ExtensionAPI;
|
|
16
18
|
}
|
|
17
19
|
|
|
18
|
-
test("parseModelRef splits provider and model at the first slash", () => {
|
|
19
|
-
assert.deepEqual(parseModelRef("anthropic/claude-sonnet"), {
|
|
20
|
-
provider: "anthropic",
|
|
21
|
-
modelId: "claude-sonnet",
|
|
22
|
-
});
|
|
23
|
-
assert.deepEqual(parseModelRef("openrouter/vendor/model/with/slashes"), {
|
|
24
|
-
provider: "openrouter",
|
|
25
|
-
modelId: "vendor/model/with/slashes",
|
|
26
|
-
});
|
|
27
|
-
});
|
|
28
|
-
|
|
29
|
-
test("parseModelRef preserves empty provider or model segments", () => {
|
|
30
|
-
assert.equal(parseModelRef("model-without-provider"), undefined);
|
|
31
|
-
assert.deepEqual(parseModelRef("/model"), { provider: "", modelId: "model" });
|
|
32
|
-
assert.deepEqual(parseModelRef("provider/"), { provider: "provider", modelId: "" });
|
|
33
|
-
});
|
|
34
|
-
|
|
35
20
|
// ── partitionedFuzzyFilter ─────────────────────────────────────────
|
|
36
21
|
|
|
37
22
|
test("partitionedFuzzyFilter concatenates partitions unchanged for empty query", () => {
|
|
@@ -108,7 +93,7 @@ test("buildPaletteItems orders built-ins above native commands above editor fill
|
|
|
108
93
|
});
|
|
109
94
|
|
|
110
95
|
const items = buildPaletteItems(
|
|
111
|
-
fakePi([{ name: "some-command", description: "extension command" }]),
|
|
96
|
+
fakePi([{ name: "some-command", description: "extension command", source: "extension" }]),
|
|
112
97
|
);
|
|
113
98
|
|
|
114
99
|
const ranks = items.map((item) =>
|
|
@@ -122,6 +107,14 @@ test("buildPaletteItems orders built-ins above native commands above editor fill
|
|
|
122
107
|
assert.ok(native);
|
|
123
108
|
assert.equal(native.label, "Peek: Ask This Session");
|
|
124
109
|
assert.equal(native.action.type, "native");
|
|
110
|
+
|
|
111
|
+
// Command/skill/template labels carry no category prefix — the page
|
|
112
|
+
// breadcrumb already names the category, and root search shows it via
|
|
113
|
+
// the description decoration.
|
|
114
|
+
const cmd = items.find((item) => item.value === "cmd:some-command");
|
|
115
|
+
assert.ok(cmd);
|
|
116
|
+
assert.equal(cmd.label, "/some-command");
|
|
117
|
+
assert.equal(cmd.category, "Command");
|
|
125
118
|
});
|
|
126
119
|
|
|
127
120
|
test("buildPaletteItems picks up native commands registered after load", () => {
|
package/src/index.ts
CHANGED
|
@@ -5,9 +5,11 @@
|
|
|
5
5
|
* regardless of whether the editor has content.
|
|
6
6
|
*
|
|
7
7
|
* Features:
|
|
8
|
-
* -
|
|
9
|
-
*
|
|
10
|
-
*
|
|
8
|
+
* - Single overlay with nested pages: built-in actions on the root page,
|
|
9
|
+
* extension actions / commands / skills / templates as sub-pages, models
|
|
10
|
+
* loaded on first visit
|
|
11
|
+
* - Fuzzy search within the current page; searching the root page matches
|
|
12
|
+
* every leaf across sub-pages
|
|
11
13
|
* - Floating overlay on top of existing content
|
|
12
14
|
* - Saves editor text before overwriting; offers "Restore" in palette
|
|
13
15
|
* - Clear editor into the restore buffer
|
|
@@ -25,6 +27,7 @@ import {
|
|
|
25
27
|
matchesKey,
|
|
26
28
|
SelectList,
|
|
27
29
|
Text,
|
|
30
|
+
type TUI,
|
|
28
31
|
} from "@earendil-works/pi-tui";
|
|
29
32
|
import { resolveShortcutKey } from "./config.ts";
|
|
30
33
|
|
|
@@ -33,7 +36,6 @@ import { resolveShortcutKey } from "./config.ts";
|
|
|
33
36
|
type CommandAction =
|
|
34
37
|
| { type: "editor"; text: string }
|
|
35
38
|
| { type: "native"; id: string }
|
|
36
|
-
| { type: "model-select" }
|
|
37
39
|
| { type: "model"; provider: string; modelId: string }
|
|
38
40
|
| { type: "compact" }
|
|
39
41
|
| { type: "reload" }
|
|
@@ -60,10 +62,9 @@ let savedEditorText: string | null = null;
|
|
|
60
62
|
* non-built-in entries always sort after built-ins.
|
|
61
63
|
*/
|
|
62
64
|
const BUILTIN_ORDER: Record<string, number> = {
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
__clear_editor: 3,
|
|
65
|
+
__restore: 0,
|
|
66
|
+
__copy_editor: 1,
|
|
67
|
+
__clear_editor: 2,
|
|
67
68
|
};
|
|
68
69
|
|
|
69
70
|
// ── Helpers ────────────────────────────────────────────────────────
|
|
@@ -100,14 +101,6 @@ export function buildPaletteItems(pi: ExtensionAPI): PaletteItem[] {
|
|
|
100
101
|
}
|
|
101
102
|
|
|
102
103
|
// ── Built-in actions ──────────────────────────────────────────
|
|
103
|
-
items.push({
|
|
104
|
-
value: "__model_select",
|
|
105
|
-
label: "Model: Switch Model",
|
|
106
|
-
description: "Select a model from the registry",
|
|
107
|
-
category: "Built-in",
|
|
108
|
-
action: { type: "model-select" },
|
|
109
|
-
});
|
|
110
|
-
|
|
111
104
|
items.push({
|
|
112
105
|
value: "__new_session",
|
|
113
106
|
label: "Session: New",
|
|
@@ -195,7 +188,7 @@ export function buildPaletteItems(pi: ExtensionAPI): PaletteItem[] {
|
|
|
195
188
|
|
|
196
189
|
items.push({
|
|
197
190
|
value: `cmd:${cmd.name}`,
|
|
198
|
-
label:
|
|
191
|
+
label: `/${cmd.name}`,
|
|
199
192
|
description: cmd.description ?? "",
|
|
200
193
|
category: sourceLabel,
|
|
201
194
|
action: { type: "editor", text: editorText },
|
|
@@ -219,22 +212,6 @@ export function buildPaletteItems(pi: ExtensionAPI): PaletteItem[] {
|
|
|
219
212
|
return items;
|
|
220
213
|
}
|
|
221
214
|
|
|
222
|
-
// ── Model selector ─────────────────────────────────────────────────
|
|
223
|
-
|
|
224
|
-
const STAR = "★ ";
|
|
225
|
-
|
|
226
|
-
/**
|
|
227
|
-
* @internal — exported for testing; parses the selector's `provider/model-id` values.
|
|
228
|
-
*/
|
|
229
|
-
export function parseModelRef(modelRef: string): { provider: string; modelId: string } | undefined {
|
|
230
|
-
const slash = modelRef.indexOf("/");
|
|
231
|
-
if (slash === -1) return undefined;
|
|
232
|
-
return {
|
|
233
|
-
provider: modelRef.slice(0, slash),
|
|
234
|
-
modelId: modelRef.slice(slash + 1),
|
|
235
|
-
};
|
|
236
|
-
}
|
|
237
|
-
|
|
238
215
|
// ── Partitioned fuzzy filter ───────────────────────────────────────
|
|
239
216
|
|
|
240
217
|
/**
|
|
@@ -261,17 +238,20 @@ export function partitionedFuzzyFilter<T>(
|
|
|
261
238
|
|
|
262
239
|
// ── Command palette overlay ────────────────────────────────────────
|
|
263
240
|
|
|
241
|
+
interface PageEntry {
|
|
242
|
+
type: "page";
|
|
243
|
+
value: string;
|
|
244
|
+
label: string;
|
|
245
|
+
description: string;
|
|
246
|
+
page: PalettePage;
|
|
247
|
+
}
|
|
248
|
+
|
|
264
249
|
interface PalettePage {
|
|
265
250
|
title: string;
|
|
266
|
-
items: Array<PaletteItem |
|
|
251
|
+
items: Array<PaletteItem | PageEntry>;
|
|
267
252
|
}
|
|
268
253
|
|
|
269
|
-
function pageItem(
|
|
270
|
-
value: string,
|
|
271
|
-
label: string,
|
|
272
|
-
description: string,
|
|
273
|
-
page: PalettePage,
|
|
274
|
-
): PalettePage["items"][number] {
|
|
254
|
+
function pageItem(value: string, label: string, description: string, page: PalettePage): PageEntry {
|
|
275
255
|
return { type: "page", value, label, description, page };
|
|
276
256
|
}
|
|
277
257
|
|
|
@@ -283,15 +263,20 @@ async function showCommandPalette(pi: ExtensionAPI, ctx: ExtensionContext): Prom
|
|
|
283
263
|
paletteItems.filter((item) => item.category === category);
|
|
284
264
|
const leafPage = (title: string, items: PaletteItem[]): PalettePage => ({ title, items });
|
|
285
265
|
|
|
286
|
-
const builtins = leaves("Built-in")
|
|
266
|
+
const builtins = leaves("Built-in");
|
|
287
267
|
const native = leaves("Native");
|
|
288
268
|
const commands = leaves("Command");
|
|
289
269
|
const skills = leaves("Skill");
|
|
290
270
|
const templates = leaves("Template");
|
|
271
|
+
// Leaves reachable only through sub-pages — merged into the root list
|
|
272
|
+
// while searching there. Built-in leaves live on the root page itself.
|
|
273
|
+
const subLeaves = paletteItems.filter((item) => item.category !== "Built-in");
|
|
291
274
|
const modelPage: PalettePage = { title: "Models", items: [] };
|
|
292
275
|
const rootItems: PalettePage["items"] = [
|
|
293
|
-
pageItem("models", "
|
|
294
|
-
|
|
276
|
+
pageItem("models", "Models", "Switch the active model", modelPage),
|
|
277
|
+
// Built-in actions sit directly on the root page so urgent entries like
|
|
278
|
+
// Restore are visible without descending into a sub-page.
|
|
279
|
+
...builtins,
|
|
295
280
|
...(native.length ? [pageItem("native", "Extension Actions", "Actions provided by extensions", leafPage("Extension Actions", native))] : []),
|
|
296
281
|
...(commands.length ? [pageItem("commands", "Commands", "Extension slash commands", leafPage("Commands", commands))] : []),
|
|
297
282
|
...(skills.length ? [pageItem("skills", "Skills", "Installed skills", leafPage("Skills", skills))] : []),
|
|
@@ -299,8 +284,43 @@ async function showCommandPalette(pi: ExtensionAPI, ctx: ExtensionContext): Prom
|
|
|
299
284
|
];
|
|
300
285
|
const root: PalettePage = { title: "Command Palette", items: rootItems };
|
|
301
286
|
|
|
287
|
+
// Captured from the overlay factory so palette actions can request a
|
|
288
|
+
// render after mutating editor state (see the action switch below).
|
|
289
|
+
let tuiRef: TUI | undefined;
|
|
290
|
+
|
|
291
|
+
/** Scoped-model marker prefix (★). */
|
|
292
|
+
const STAR = "★ ";
|
|
293
|
+
|
|
294
|
+
/**
|
|
295
|
+
* Build the model list for the Models page: scoped models (★) first, then
|
|
296
|
+
* the rest — alphabetical within each group. Reads the registry
|
|
297
|
+
* synchronously; throws if enumeration fails.
|
|
298
|
+
*/
|
|
299
|
+
function buildModelItems(): PaletteItem[] {
|
|
300
|
+
const scopedIds = new Set(ctx.scopedModels.map((s) => `${s.model.provider}/${s.model.id}`));
|
|
301
|
+
return ctx.modelRegistry
|
|
302
|
+
.getAvailable()
|
|
303
|
+
.map((m): PaletteItem => {
|
|
304
|
+
const scoped = scopedIds.has(`${m.provider}/${m.id}`);
|
|
305
|
+
return {
|
|
306
|
+
value: `${m.provider}/${m.id}`,
|
|
307
|
+
label: scoped ? `${STAR}${m.name}` : m.name,
|
|
308
|
+
description: m.provider,
|
|
309
|
+
category: "Models",
|
|
310
|
+
action: { type: "model", provider: m.provider, modelId: m.id },
|
|
311
|
+
};
|
|
312
|
+
})
|
|
313
|
+
.sort((a, b) => {
|
|
314
|
+
const aScoped = scopedIds.has(a.value);
|
|
315
|
+
const bScoped = scopedIds.has(b.value);
|
|
316
|
+
if (aScoped !== bScoped) return aScoped ? -1 : 1;
|
|
317
|
+
return a.label.localeCompare(b.label);
|
|
318
|
+
});
|
|
319
|
+
}
|
|
320
|
+
|
|
302
321
|
const result = await ctx.ui.custom<PaletteItem | null>(
|
|
303
322
|
(tui, theme, _kb, done) => {
|
|
323
|
+
tuiRef = tui;
|
|
304
324
|
const container = new Container();
|
|
305
325
|
const listHost = new Container();
|
|
306
326
|
const queryInput = new Input();
|
|
@@ -309,8 +329,7 @@ async function showCommandPalette(pi: ExtensionAPI, ctx: ExtensionContext): Prom
|
|
|
309
329
|
{ page: root, input: "" },
|
|
310
330
|
];
|
|
311
331
|
let selectList!: SelectList;
|
|
312
|
-
let visibleItems:
|
|
313
|
-
let modelLoading = false;
|
|
332
|
+
let visibleItems: Array<PaletteItem | PageEntry> = [];
|
|
314
333
|
|
|
315
334
|
const listTheme = {
|
|
316
335
|
selectedPrefix: (t: string) => theme.fg("accent", t),
|
|
@@ -328,12 +347,7 @@ async function showCommandPalette(pi: ExtensionAPI, ctx: ExtensionContext): Prom
|
|
|
328
347
|
const { page } = current();
|
|
329
348
|
const query = queryInput.getValue();
|
|
330
349
|
visibleItems =
|
|
331
|
-
page === root && query.trim()
|
|
332
|
-
? [
|
|
333
|
-
...root.items,
|
|
334
|
-
...paletteItems.filter((item) => item.action.type !== "model-select"),
|
|
335
|
-
]
|
|
336
|
-
: page.items;
|
|
350
|
+
page === root && query.trim() ? [...root.items, ...subLeaves] : page.items;
|
|
337
351
|
const items = visibleItems.map((item) => ({
|
|
338
352
|
value: item.value,
|
|
339
353
|
label: item.label,
|
|
@@ -364,68 +378,45 @@ async function showCommandPalette(pi: ExtensionAPI, ctx: ExtensionContext): Prom
|
|
|
364
378
|
current().selectedValue = selected.value;
|
|
365
379
|
const item = visibleItems.find((candidate) => candidate.value === selected.value);
|
|
366
380
|
if (!item) return;
|
|
367
|
-
if ("page" in item) {
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
value,
|
|
383
|
-
label: scoped ? `${STAR}${m.name}` : m.name,
|
|
384
|
-
description: m.provider,
|
|
385
|
-
category: "Built-in",
|
|
386
|
-
action: { type: "model", provider: m.provider, modelId: m.id } as CommandAction,
|
|
387
|
-
},
|
|
388
|
-
};
|
|
389
|
-
})
|
|
390
|
-
.sort((a, b) =>
|
|
391
|
-
a.scoped === b.scoped
|
|
392
|
-
? a.item.label.localeCompare(b.item.label)
|
|
393
|
-
: a.scoped
|
|
394
|
-
? -1
|
|
395
|
-
: 1,
|
|
396
|
-
);
|
|
397
|
-
item.page.items = decorated.map((d) => d.item);
|
|
398
|
-
modelLoading = false;
|
|
399
|
-
if (item.page.items.length === 0) {
|
|
400
|
-
ctx.ui.notify("No models available.", "warning");
|
|
401
|
-
return;
|
|
402
|
-
}
|
|
403
|
-
pushPage(item.page);
|
|
404
|
-
} catch {
|
|
405
|
-
modelLoading = false;
|
|
406
|
-
ctx.ui.notify("Cannot enumerate models.", "warning");
|
|
407
|
-
}
|
|
381
|
+
if (!("page" in item)) {
|
|
382
|
+
done(item);
|
|
383
|
+
return;
|
|
384
|
+
}
|
|
385
|
+
// Entering the Models page loads the registry once per palette
|
|
386
|
+
// session; later visits reuse the cached list.
|
|
387
|
+
if (item.page === modelPage && item.page.items.length === 0) {
|
|
388
|
+
try {
|
|
389
|
+
item.page.items = buildModelItems();
|
|
390
|
+
} catch {
|
|
391
|
+
ctx.ui.notify("Cannot enumerate models. Use Ctrl+L instead.", "warning");
|
|
392
|
+
return;
|
|
393
|
+
}
|
|
394
|
+
if (item.page.items.length === 0) {
|
|
395
|
+
ctx.ui.notify("No models available.", "warning");
|
|
408
396
|
return;
|
|
409
397
|
}
|
|
410
|
-
pushPage(item.page);
|
|
411
|
-
} else {
|
|
412
|
-
done(item);
|
|
413
398
|
}
|
|
399
|
+
pushPage(item.page);
|
|
414
400
|
};
|
|
415
401
|
selectList.onSelectionChange = (selected) => {
|
|
416
402
|
current().selectedValue = selected.value;
|
|
417
403
|
};
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
// Breadcrumb title: updated in place whenever the page stack changes.
|
|
407
|
+
const titleText = new Text(theme.fg("accent", theme.bold(root.title)), 1, 0);
|
|
408
|
+
|
|
409
|
+
function updateTitle() {
|
|
410
|
+
titleText.setText(
|
|
411
|
+
theme.fg("accent", theme.bold(stack.map((frame) => frame.page.title).join(" › "))),
|
|
412
|
+
);
|
|
423
413
|
}
|
|
424
414
|
|
|
425
415
|
function pushPage(page: PalettePage) {
|
|
426
416
|
current().input = queryInput.getValue();
|
|
427
417
|
stack.push({ page, input: "" });
|
|
428
418
|
queryInput.setValue("");
|
|
419
|
+
updateTitle();
|
|
429
420
|
rebuild();
|
|
430
421
|
tui.requestRender();
|
|
431
422
|
}
|
|
@@ -434,16 +425,19 @@ async function showCommandPalette(pi: ExtensionAPI, ctx: ExtensionContext): Prom
|
|
|
434
425
|
if (stack.length <= 1) return;
|
|
435
426
|
stack.pop();
|
|
436
427
|
queryInput.setValue(current().input);
|
|
428
|
+
updateTitle();
|
|
437
429
|
rebuild();
|
|
438
430
|
tui.requestRender();
|
|
439
431
|
}
|
|
440
432
|
|
|
441
433
|
rebuild();
|
|
442
434
|
container.addChild(new DynamicBorder((s: string) => theme.fg("accent", s)));
|
|
443
|
-
container.addChild(
|
|
435
|
+
container.addChild(titleText);
|
|
444
436
|
container.addChild(queryInput);
|
|
445
437
|
container.addChild(listHost);
|
|
446
|
-
container.addChild(
|
|
438
|
+
container.addChild(
|
|
439
|
+
new Text(theme.fg("dim", "↑↓ navigate • enter open/select • backspace go back • esc close"), 1, 0),
|
|
440
|
+
);
|
|
447
441
|
container.addChild(new DynamicBorder((s: string) => theme.fg("accent", s)));
|
|
448
442
|
|
|
449
443
|
return {
|
|
@@ -455,10 +449,7 @@ async function showCommandPalette(pi: ExtensionAPI, ctx: ExtensionContext): Prom
|
|
|
455
449
|
queryInput.focused = value;
|
|
456
450
|
},
|
|
457
451
|
render(w: number) {
|
|
458
|
-
|
|
459
|
-
const title = stack.map((frame) => frame.page.title).join(" › ");
|
|
460
|
-
lines[1] = theme.fg("accent", theme.bold(title));
|
|
461
|
-
return lines;
|
|
452
|
+
return container.render(w);
|
|
462
453
|
},
|
|
463
454
|
invalidate() {
|
|
464
455
|
container.invalidate();
|
|
@@ -514,10 +505,6 @@ async function showCommandPalette(pi: ExtensionAPI, ctx: ExtensionContext): Prom
|
|
|
514
505
|
}
|
|
515
506
|
break;
|
|
516
507
|
}
|
|
517
|
-
case "model-select": {
|
|
518
|
-
// Kept for compatibility with callers that may construct this action.
|
|
519
|
-
break;
|
|
520
|
-
}
|
|
521
508
|
case "restore": {
|
|
522
509
|
if (savedEditorText !== null) {
|
|
523
510
|
ctx.ui.setEditorText(savedEditorText);
|
|
@@ -584,6 +571,11 @@ async function showCommandPalette(pi: ExtensionAPI, ctx: ExtensionContext): Prom
|
|
|
584
571
|
break;
|
|
585
572
|
}
|
|
586
573
|
}
|
|
574
|
+
|
|
575
|
+
// The overlay close renders the underlying UI before this promise
|
|
576
|
+
// resolves, and setEditorText mutates state without requesting a render —
|
|
577
|
+
// without this, the editor shows stale text until the next keypress.
|
|
578
|
+
tuiRef?.requestRender();
|
|
587
579
|
}
|
|
588
580
|
|
|
589
581
|
// ── Extension entry point ──────────────────────────────────────────
|