@ikuma.cloud/pix-mcp 0.0.7 → 0.0.9
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 +121 -39
- package/mcp.schema.json +1 -1
- package/package.json +6 -2
- package/src/catalog.ts +1 -1
- package/src/client.ts +335 -9
- package/src/command.ts +69 -0
- package/src/config.ts +1 -1
- package/src/index.ts +416 -23
- package/src/output.ts +85 -5
- package/src/prompt-picker.ts +120 -0
- package/src/prompts.ts +41 -77
- package/src/resource-picker.ts +157 -0
- package/src/resources.ts +769 -0
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
import {
|
|
2
|
+
DynamicBorder,
|
|
3
|
+
type ExtensionCommandContext,
|
|
4
|
+
type Theme,
|
|
5
|
+
} from "@earendil-works/pi-coding-agent";
|
|
6
|
+
import {
|
|
7
|
+
Container,
|
|
8
|
+
SelectList,
|
|
9
|
+
Text,
|
|
10
|
+
type Component,
|
|
11
|
+
type SelectItem,
|
|
12
|
+
type TUI,
|
|
13
|
+
type TuiMouseEvent,
|
|
14
|
+
} from "@earendil-works/pi-tui";
|
|
15
|
+
import { compact } from "./catalog.ts";
|
|
16
|
+
import { promptKey, type PromptEntry } from "./prompts.ts";
|
|
17
|
+
|
|
18
|
+
function details(entry: PromptEntry, theme: Theme): string {
|
|
19
|
+
const title = compact(entry.prompt.title ?? "", 120);
|
|
20
|
+
const description = compact(entry.prompt.description ?? "", 320);
|
|
21
|
+
return [
|
|
22
|
+
...(title ? [theme.fg("accent", theme.bold(title))] : []),
|
|
23
|
+
theme.fg(
|
|
24
|
+
description ? "muted" : "dim",
|
|
25
|
+
description || "No description provided.",
|
|
26
|
+
),
|
|
27
|
+
].join("\n");
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export function createPromptPicker(
|
|
31
|
+
entries: PromptEntry[],
|
|
32
|
+
tui: TUI,
|
|
33
|
+
theme: Theme,
|
|
34
|
+
done: (result: string | undefined) => void,
|
|
35
|
+
signal?: AbortSignal,
|
|
36
|
+
): Component & { dispose(): void } {
|
|
37
|
+
const items: SelectItem[] = entries.map((entry) => ({
|
|
38
|
+
value: promptKey(entry.server, entry.prompt.name),
|
|
39
|
+
label: `${entry.server} / ${entry.prompt.name}`,
|
|
40
|
+
}));
|
|
41
|
+
const entriesByKey = new Map(
|
|
42
|
+
entries.map((entry) => [promptKey(entry.server, entry.prompt.name), entry]),
|
|
43
|
+
);
|
|
44
|
+
const container = new Container();
|
|
45
|
+
container.addChild(new DynamicBorder((text) => theme.fg("accent", text)));
|
|
46
|
+
container.addChild(
|
|
47
|
+
new Text(theme.fg("accent", theme.bold("Select MCP prompt"))),
|
|
48
|
+
);
|
|
49
|
+
|
|
50
|
+
const list = new SelectList(items, Math.min(items.length, 10), {
|
|
51
|
+
selectedPrefix: (text) => theme.fg("accent", text),
|
|
52
|
+
selectedText: (text) => theme.fg("accent", text),
|
|
53
|
+
description: (text) => theme.fg("muted", text),
|
|
54
|
+
scrollInfo: (text) => theme.fg("dim", text),
|
|
55
|
+
noMatch: (text) => theme.fg("warning", text),
|
|
56
|
+
});
|
|
57
|
+
container.addChild(list);
|
|
58
|
+
|
|
59
|
+
const focusedDescription = new Text("", 1, 1);
|
|
60
|
+
const updateDescription = (item: SelectItem) => {
|
|
61
|
+
const entry = entriesByKey.get(item.value);
|
|
62
|
+
if (entry) focusedDescription.setText(details(entry, theme));
|
|
63
|
+
};
|
|
64
|
+
const initialItem = items[0];
|
|
65
|
+
if (initialItem) updateDescription(initialItem);
|
|
66
|
+
list.onSelectionChange = updateDescription;
|
|
67
|
+
container.addChild(focusedDescription);
|
|
68
|
+
container.addChild(
|
|
69
|
+
new Text(theme.fg("dim", "↑↓ navigate • enter select • esc cancel")),
|
|
70
|
+
);
|
|
71
|
+
container.addChild(new DynamicBorder((text) => theme.fg("accent", text)));
|
|
72
|
+
|
|
73
|
+
let finished = false;
|
|
74
|
+
const finish = (result: string | undefined) => {
|
|
75
|
+
if (finished) return;
|
|
76
|
+
finished = true;
|
|
77
|
+
done(result);
|
|
78
|
+
};
|
|
79
|
+
list.onSelect = (item) => finish(item.value);
|
|
80
|
+
list.onCancel = () => finish(undefined);
|
|
81
|
+
const onAbort = () => finish(undefined);
|
|
82
|
+
signal?.addEventListener("abort", onAbort, { once: true });
|
|
83
|
+
if (signal?.aborted) onAbort();
|
|
84
|
+
|
|
85
|
+
return {
|
|
86
|
+
render(width: number) {
|
|
87
|
+
return container.render(width);
|
|
88
|
+
},
|
|
89
|
+
invalidate() {
|
|
90
|
+
container.invalidate();
|
|
91
|
+
},
|
|
92
|
+
handleInput(data: string) {
|
|
93
|
+
list.handleInput(data);
|
|
94
|
+
tui.requestRender();
|
|
95
|
+
},
|
|
96
|
+
handleMouse(event: TuiMouseEvent) {
|
|
97
|
+
const result = container.handleMouse(event);
|
|
98
|
+
if (result?.render) tui.requestRender();
|
|
99
|
+
return result;
|
|
100
|
+
},
|
|
101
|
+
dispose() {
|
|
102
|
+
signal?.removeEventListener("abort", onAbort);
|
|
103
|
+
},
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
export async function pickPrompt(
|
|
108
|
+
entries: PromptEntry[],
|
|
109
|
+
ctx: ExtensionCommandContext,
|
|
110
|
+
): Promise<PromptEntry | undefined> {
|
|
111
|
+
if (ctx.signal?.aborted) return;
|
|
112
|
+
const selected = await ctx.ui.custom<string | undefined>(
|
|
113
|
+
(tui, theme, _keybindings, done) =>
|
|
114
|
+
createPromptPicker(entries, tui, theme, done, ctx.signal),
|
|
115
|
+
);
|
|
116
|
+
if (selected === undefined) return;
|
|
117
|
+
return entries.find(
|
|
118
|
+
(entry) => promptKey(entry.server, entry.prompt.name) === selected,
|
|
119
|
+
);
|
|
120
|
+
}
|
package/src/prompts.ts
CHANGED
|
@@ -4,6 +4,12 @@ import type {
|
|
|
4
4
|
Prompt,
|
|
5
5
|
} from "@modelcontextprotocol/sdk/types.js";
|
|
6
6
|
import { compact } from "./catalog.ts";
|
|
7
|
+
import {
|
|
8
|
+
quoteCommandArgument,
|
|
9
|
+
renderCommandToken,
|
|
10
|
+
tokenizeCommand,
|
|
11
|
+
type CommandToken,
|
|
12
|
+
} from "./command.ts";
|
|
7
13
|
import { formatContent } from "./output.ts";
|
|
8
14
|
|
|
9
15
|
export interface PromptEntry {
|
|
@@ -11,10 +17,7 @@ export interface PromptEntry {
|
|
|
11
17
|
prompt: Prompt;
|
|
12
18
|
}
|
|
13
19
|
|
|
14
|
-
export
|
|
15
|
-
value: string;
|
|
16
|
-
separator?: number;
|
|
17
|
-
}
|
|
20
|
+
export type PromptArgumentToken = CommandToken;
|
|
18
21
|
|
|
19
22
|
export type PromptCommand =
|
|
20
23
|
| { action: "list"; server?: string }
|
|
@@ -30,7 +33,7 @@ export function promptKey(server: string, name: string): string {
|
|
|
30
33
|
}
|
|
31
34
|
|
|
32
35
|
export function parsePromptCommand(input: string): PromptCommand {
|
|
33
|
-
const tokens =
|
|
36
|
+
const tokens = tokenizeCommand(input, "prompt");
|
|
34
37
|
const action = tokens.shift()?.value;
|
|
35
38
|
if (action === "list") {
|
|
36
39
|
if (tokens.length > 1) throw new Error("Usage: /mcp-prompt list [server]");
|
|
@@ -120,15 +123,15 @@ export function formatPromptList(
|
|
|
120
123
|
const argumentsHint = (entry.prompt.arguments ?? [])
|
|
121
124
|
.map((argument) =>
|
|
122
125
|
argument.required
|
|
123
|
-
? `<${
|
|
124
|
-
: `[${
|
|
126
|
+
? `<${quoteCommandArgument(argument.name)}>`
|
|
127
|
+
: `[${quoteCommandArgument(argument.name)}]`,
|
|
125
128
|
)
|
|
126
129
|
.join(" ");
|
|
127
130
|
const description = compact(
|
|
128
131
|
entry.prompt.description ?? entry.prompt.title ?? "",
|
|
129
132
|
160,
|
|
130
133
|
);
|
|
131
|
-
const line = `${entry.server} ${
|
|
134
|
+
const line = `${entry.server} ${quoteCommandArgument(entry.prompt.name)}${argumentsHint ? ` ${argumentsHint}` : ""}${description ? ` — ${description}` : ""}`;
|
|
132
135
|
if (shown >= 100 || lines.join("\n").length + line.length > 15_000) {
|
|
133
136
|
lines.push(`${selected.length - shown} additional prompts omitted.`);
|
|
134
137
|
break;
|
|
@@ -149,7 +152,7 @@ export function promptCompletions(
|
|
|
149
152
|
const trailingSpace = /\s$/.test(prefix);
|
|
150
153
|
let tokens: PromptArgumentToken[];
|
|
151
154
|
try {
|
|
152
|
-
tokens =
|
|
155
|
+
tokens = tokenizeCommand(prefix);
|
|
153
156
|
} catch {
|
|
154
157
|
return null;
|
|
155
158
|
}
|
|
@@ -191,7 +194,7 @@ export function promptCompletions(
|
|
|
191
194
|
)
|
|
192
195
|
.sort((a, b) => a.prompt.name.localeCompare(b.prompt.name, "en"));
|
|
193
196
|
return prompts.map((entry) => ({
|
|
194
|
-
value: `run ${
|
|
197
|
+
value: `run ${quoteCommandArgument(server)} ${quoteCommandArgument(entry.prompt.name)}`,
|
|
195
198
|
label: entry.prompt.name,
|
|
196
199
|
...(entry.prompt.description || entry.prompt.title
|
|
197
200
|
? {
|
|
@@ -237,9 +240,9 @@ export function promptCompletions(
|
|
|
237
240
|
}
|
|
238
241
|
const base = [
|
|
239
242
|
"run",
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
...supplied.map(
|
|
243
|
+
quoteCommandArgument(server),
|
|
244
|
+
quoteCommandArgument(selected.prompt.name),
|
|
245
|
+
...supplied.map(renderCommandToken),
|
|
243
246
|
].join(" ");
|
|
244
247
|
const arguments_ = definitions.filter(
|
|
245
248
|
(argument) =>
|
|
@@ -247,7 +250,7 @@ export function promptCompletions(
|
|
|
247
250
|
);
|
|
248
251
|
return arguments_.length > 0
|
|
249
252
|
? arguments_.map((argument) => ({
|
|
250
|
-
value: `${base} ${
|
|
253
|
+
value: `${base} ${quoteCommandArgument(argument.name)}=`,
|
|
251
254
|
label: `${argument.name}=`,
|
|
252
255
|
...(argument.description
|
|
253
256
|
? { description: compact(argument.description, 100) }
|
|
@@ -262,81 +265,34 @@ function completionItems(
|
|
|
262
265
|
label: (value: string) => string,
|
|
263
266
|
) {
|
|
264
267
|
return values.map((value) => ({
|
|
265
|
-
value: `${prefix}${
|
|
268
|
+
value: `${prefix}${quoteCommandArgument(value)}`,
|
|
266
269
|
label: label(value),
|
|
267
270
|
}));
|
|
268
271
|
}
|
|
269
272
|
|
|
270
|
-
function
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
273
|
+
function hasUsablePromptContent(
|
|
274
|
+
content: GetPromptResult["messages"][number]["content"],
|
|
275
|
+
): boolean {
|
|
276
|
+
if (content.type === "text") return content.text.trim().length > 0;
|
|
277
|
+
if (content.type === "resource")
|
|
278
|
+
return "text" in content.resource
|
|
279
|
+
? content.resource.text.trim().length > 0
|
|
280
|
+
: content.resource.blob.length > 0;
|
|
281
|
+
if (content.type === "resource_link") return content.uri.trim().length > 0;
|
|
282
|
+
return content.data.length > 0;
|
|
274
283
|
}
|
|
275
284
|
|
|
276
|
-
function
|
|
277
|
-
const tokens: PromptArgumentToken[] = [];
|
|
278
|
-
let token = "";
|
|
279
|
-
let separator: number | undefined;
|
|
280
|
-
let quote: '"' | "'" | undefined;
|
|
281
|
-
let escaped = false;
|
|
282
|
-
let started = false;
|
|
283
|
-
for (const character of input) {
|
|
284
|
-
if (escaped) {
|
|
285
|
-
token += character;
|
|
286
|
-
escaped = false;
|
|
287
|
-
started = true;
|
|
288
|
-
} else if (character === "\\" && quote !== "'") {
|
|
289
|
-
escaped = true;
|
|
290
|
-
started = true;
|
|
291
|
-
} else if (quote) {
|
|
292
|
-
if (character === quote) quote = undefined;
|
|
293
|
-
else token += character;
|
|
294
|
-
started = true;
|
|
295
|
-
} else if (character === '"' || character === "'") {
|
|
296
|
-
quote = character;
|
|
297
|
-
started = true;
|
|
298
|
-
} else if (/\s/u.test(character)) {
|
|
299
|
-
if (started) {
|
|
300
|
-
tokens.push({
|
|
301
|
-
value: token,
|
|
302
|
-
...(separator !== undefined ? { separator } : {}),
|
|
303
|
-
});
|
|
304
|
-
token = "";
|
|
305
|
-
separator = undefined;
|
|
306
|
-
started = false;
|
|
307
|
-
}
|
|
308
|
-
} else {
|
|
309
|
-
if (character === "=" && separator === undefined)
|
|
310
|
-
separator = token.length;
|
|
311
|
-
token += character;
|
|
312
|
-
started = true;
|
|
313
|
-
}
|
|
314
|
-
}
|
|
315
|
-
if (quote) throw new Error("Unterminated quote in prompt arguments.");
|
|
316
|
-
if (escaped) throw new Error("Trailing escape in prompt arguments.");
|
|
317
|
-
if (started)
|
|
318
|
-
tokens.push({
|
|
319
|
-
value: token,
|
|
320
|
-
...(separator !== undefined ? { separator } : {}),
|
|
321
|
-
});
|
|
322
|
-
return tokens;
|
|
323
|
-
}
|
|
324
|
-
|
|
325
|
-
export async function formatPromptResult(
|
|
326
|
-
result: GetPromptResult,
|
|
327
|
-
server: string,
|
|
328
|
-
name: string,
|
|
329
|
-
) {
|
|
285
|
+
export async function formatPromptResult(result: GetPromptResult) {
|
|
330
286
|
if (result.messages.length === 0)
|
|
331
287
|
throw new Error("MCP prompt returned no messages.");
|
|
332
|
-
const blocks: ContentBlock[] = [
|
|
333
|
-
|
|
334
|
-
];
|
|
288
|
+
const blocks: ContentBlock[] = [];
|
|
289
|
+
let hasSourceContent = false;
|
|
335
290
|
const showRoles =
|
|
336
291
|
result.messages.length > 1 || result.messages[0]?.role !== "user";
|
|
337
292
|
for (const message of result.messages) {
|
|
338
293
|
if (showRoles) blocks.push({ type: "text", text: `[${message.role}]` });
|
|
339
294
|
const content = message.content;
|
|
295
|
+
if (hasUsablePromptContent(content)) hasSourceContent = true;
|
|
340
296
|
if (content.type === "resource") {
|
|
341
297
|
if ("text" in content.resource) {
|
|
342
298
|
blocks.push({
|
|
@@ -362,5 +318,13 @@ export async function formatPromptResult(
|
|
|
362
318
|
});
|
|
363
319
|
} else blocks.push(content);
|
|
364
320
|
}
|
|
365
|
-
|
|
321
|
+
if (!hasSourceContent)
|
|
322
|
+
throw new Error("MCP prompt returned no usable content.");
|
|
323
|
+
const formatted = await formatContent(blocks, {
|
|
324
|
+
artifact: result,
|
|
325
|
+
preserveOrder: true,
|
|
326
|
+
});
|
|
327
|
+
if (formatted.content.length === 0)
|
|
328
|
+
throw new Error("MCP prompt returned no usable content.");
|
|
329
|
+
return formatted;
|
|
366
330
|
}
|
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
import {
|
|
2
|
+
DynamicBorder,
|
|
3
|
+
type ExtensionCommandContext,
|
|
4
|
+
type Theme,
|
|
5
|
+
} from "@earendil-works/pi-coding-agent";
|
|
6
|
+
import {
|
|
7
|
+
Container,
|
|
8
|
+
SelectList,
|
|
9
|
+
Text,
|
|
10
|
+
type Component,
|
|
11
|
+
type SelectItem,
|
|
12
|
+
type TUI,
|
|
13
|
+
type TuiMouseEvent,
|
|
14
|
+
} from "@earendil-works/pi-tui";
|
|
15
|
+
import { compact } from "./catalog.ts";
|
|
16
|
+
import { resourceEntryKey, type ResourceEntry } from "./resources.ts";
|
|
17
|
+
|
|
18
|
+
function metadata(entry: ResourceEntry) {
|
|
19
|
+
return entry.kind === "resource" ? entry.resource : entry.template;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function identifier(entry: ResourceEntry) {
|
|
23
|
+
return entry.kind === "resource"
|
|
24
|
+
? entry.resource.uri
|
|
25
|
+
: entry.template.uriTemplate;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function details(entry: ResourceEntry, theme: Theme): string {
|
|
29
|
+
const item = metadata(entry);
|
|
30
|
+
const title = compact(item.title ?? item.name, 120);
|
|
31
|
+
const description = compact(item.description ?? "", 320);
|
|
32
|
+
const uri = compact(identifier(entry), 500);
|
|
33
|
+
const annotations = item.annotations;
|
|
34
|
+
const hints = [
|
|
35
|
+
...(item.mimeType ? [`Type: ${compact(item.mimeType, 120)}`] : []),
|
|
36
|
+
...(entry.kind === "resource" && entry.resource.size !== undefined
|
|
37
|
+
? [`Size: ${entry.resource.size.toLocaleString("en-US")} bytes`]
|
|
38
|
+
: []),
|
|
39
|
+
...(entry.kind === "template" && entry.variables.length > 0
|
|
40
|
+
? [
|
|
41
|
+
`Variables: ${entry.variables.map((value) => compact(value, 80)).join(", ")}`,
|
|
42
|
+
]
|
|
43
|
+
: []),
|
|
44
|
+
...(annotations?.audience?.length
|
|
45
|
+
? [`Audience: ${annotations.audience.slice(0, 2).join(", ")}`]
|
|
46
|
+
: []),
|
|
47
|
+
...(annotations?.priority !== undefined
|
|
48
|
+
? [`Priority: ${annotations.priority}`]
|
|
49
|
+
: []),
|
|
50
|
+
...(annotations?.lastModified
|
|
51
|
+
? [`Modified: ${compact(annotations.lastModified, 80)}`]
|
|
52
|
+
: []),
|
|
53
|
+
];
|
|
54
|
+
return [
|
|
55
|
+
theme.fg("accent", theme.bold(title)),
|
|
56
|
+
theme.fg(
|
|
57
|
+
"muted",
|
|
58
|
+
`${entry.kind === "template" ? "Template" : "Resource"}: ${uri}`,
|
|
59
|
+
),
|
|
60
|
+
...(description ? [theme.fg("muted", description)] : []),
|
|
61
|
+
...(hints.length > 0 ? [theme.fg("dim", hints.join(" • "))] : []),
|
|
62
|
+
].join("\n");
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export function createResourcePicker(
|
|
66
|
+
entries: ResourceEntry[],
|
|
67
|
+
tui: TUI,
|
|
68
|
+
theme: Theme,
|
|
69
|
+
done: (result: string | undefined) => void,
|
|
70
|
+
signal?: AbortSignal,
|
|
71
|
+
): Component & { dispose(): void } {
|
|
72
|
+
const items: SelectItem[] = entries.map((entry) => {
|
|
73
|
+
const item = metadata(entry);
|
|
74
|
+
return {
|
|
75
|
+
value: resourceEntryKey(entry),
|
|
76
|
+
label: `${entry.server} / ${compact(item.title ?? item.name, 120)}`,
|
|
77
|
+
description: compact(identifier(entry), 180),
|
|
78
|
+
};
|
|
79
|
+
});
|
|
80
|
+
const entriesByKey = new Map(
|
|
81
|
+
entries.map((entry) => [resourceEntryKey(entry), entry]),
|
|
82
|
+
);
|
|
83
|
+
const container = new Container();
|
|
84
|
+
container.addChild(new DynamicBorder((text) => theme.fg("accent", text)));
|
|
85
|
+
container.addChild(
|
|
86
|
+
new Text(theme.fg("accent", theme.bold("Select MCP resource"))),
|
|
87
|
+
);
|
|
88
|
+
|
|
89
|
+
const list = new SelectList(items, Math.min(items.length, 10), {
|
|
90
|
+
selectedPrefix: (text) => theme.fg("accent", text),
|
|
91
|
+
selectedText: (text) => theme.fg("accent", text),
|
|
92
|
+
description: (text) => theme.fg("muted", text),
|
|
93
|
+
scrollInfo: (text) => theme.fg("dim", text),
|
|
94
|
+
noMatch: (text) => theme.fg("warning", text),
|
|
95
|
+
});
|
|
96
|
+
container.addChild(list);
|
|
97
|
+
|
|
98
|
+
const focusedDescription = new Text("", 1, 1);
|
|
99
|
+
const updateDescription = (item: SelectItem) => {
|
|
100
|
+
const entry = entriesByKey.get(item.value);
|
|
101
|
+
if (entry) focusedDescription.setText(details(entry, theme));
|
|
102
|
+
};
|
|
103
|
+
const initialItem = items[0];
|
|
104
|
+
if (initialItem) updateDescription(initialItem);
|
|
105
|
+
list.onSelectionChange = updateDescription;
|
|
106
|
+
container.addChild(focusedDescription);
|
|
107
|
+
container.addChild(
|
|
108
|
+
new Text(theme.fg("dim", "↑↓ navigate • enter select • esc cancel")),
|
|
109
|
+
);
|
|
110
|
+
container.addChild(new DynamicBorder((text) => theme.fg("accent", text)));
|
|
111
|
+
|
|
112
|
+
let finished = false;
|
|
113
|
+
const finish = (result: string | undefined) => {
|
|
114
|
+
if (finished) return;
|
|
115
|
+
finished = true;
|
|
116
|
+
done(result);
|
|
117
|
+
};
|
|
118
|
+
list.onSelect = (item) => finish(item.value);
|
|
119
|
+
list.onCancel = () => finish(undefined);
|
|
120
|
+
const onAbort = () => finish(undefined);
|
|
121
|
+
signal?.addEventListener("abort", onAbort, { once: true });
|
|
122
|
+
if (signal?.aborted) onAbort();
|
|
123
|
+
|
|
124
|
+
return {
|
|
125
|
+
render(width: number) {
|
|
126
|
+
return container.render(width);
|
|
127
|
+
},
|
|
128
|
+
invalidate() {
|
|
129
|
+
container.invalidate();
|
|
130
|
+
},
|
|
131
|
+
handleInput(data: string) {
|
|
132
|
+
list.handleInput(data);
|
|
133
|
+
tui.requestRender();
|
|
134
|
+
},
|
|
135
|
+
handleMouse(event: TuiMouseEvent) {
|
|
136
|
+
const result = container.handleMouse(event);
|
|
137
|
+
if (result?.render) tui.requestRender();
|
|
138
|
+
return result;
|
|
139
|
+
},
|
|
140
|
+
dispose() {
|
|
141
|
+
signal?.removeEventListener("abort", onAbort);
|
|
142
|
+
},
|
|
143
|
+
};
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
export async function pickResource(
|
|
147
|
+
entries: ResourceEntry[],
|
|
148
|
+
ctx: ExtensionCommandContext,
|
|
149
|
+
): Promise<ResourceEntry | undefined> {
|
|
150
|
+
if (ctx.signal?.aborted) return;
|
|
151
|
+
const selected = await ctx.ui.custom<string | undefined>(
|
|
152
|
+
(tui, theme, _keybindings, done) =>
|
|
153
|
+
createResourcePicker(entries, tui, theme, done, ctx.signal),
|
|
154
|
+
);
|
|
155
|
+
if (selected === undefined) return;
|
|
156
|
+
return entries.find((entry) => resourceEntryKey(entry) === selected);
|
|
157
|
+
}
|