@ikuma.cloud/pix-mcp 0.0.8 → 0.0.10

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.
@@ -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
+ }