decorated-pi 0.4.0 → 0.5.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 +74 -57
- package/extensions/guidance.ts +20 -13
- package/extensions/index.ts +72 -6
- package/extensions/io.ts +4 -3
- package/extensions/lsp/servers.ts +63 -3
- package/extensions/mcp/builtin.ts +348 -42
- package/extensions/mcp/client.ts +28 -19
- package/extensions/mcp/index.ts +407 -80
- package/extensions/model-integration.ts +19 -8
- package/extensions/patch.ts +232 -52
- package/extensions/rtk.ts +244 -0
- package/extensions/settings.ts +63 -1
- package/extensions/slash.ts +199 -67
- package/extensions/smart-at.ts +27 -1
- package/extensions/wakatime.ts +403 -0
- package/package.json +4 -5
package/extensions/mcp/index.ts
CHANGED
|
@@ -1,102 +1,332 @@
|
|
|
1
|
-
import type
|
|
2
|
-
import {
|
|
1
|
+
import { keyHint, type ExtensionAPI, type ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
2
|
+
import { Text } from "@earendil-works/pi-tui";
|
|
3
3
|
import { McpConnection } from "./client.js";
|
|
4
|
-
import {
|
|
4
|
+
import {
|
|
5
|
+
resolveMcpConfigs, saveProjectMcpDescription,
|
|
6
|
+
loadMcpCache, updateServerCache, cleanupStaleCache,
|
|
7
|
+
type McpServerConfig, type McpToolCache,
|
|
8
|
+
} from "./builtin.js";
|
|
9
|
+
import {
|
|
10
|
+
getMcpBrokerModelKey, getCompactModelKey,
|
|
11
|
+
getMcpDescription, setMcpDescription,
|
|
12
|
+
parseModelKey,
|
|
13
|
+
} from "../settings.js";
|
|
5
14
|
|
|
6
15
|
export interface McpServerStatus {
|
|
7
16
|
name: string;
|
|
8
17
|
url: string;
|
|
9
18
|
source: string;
|
|
10
|
-
|
|
19
|
+
description?: string;
|
|
20
|
+
state: "connecting" | "connected" | "failed" | "disabled";
|
|
11
21
|
toolCount: number;
|
|
12
|
-
tools: Array<{ name: string; description
|
|
22
|
+
tools: Array<{ name: string; description?: string; inputSchema?: Record<string, unknown> }>;
|
|
13
23
|
error?: string;
|
|
14
24
|
}
|
|
15
25
|
|
|
16
26
|
let activeConnections: McpConnection[] = [];
|
|
17
27
|
let allServers = new Map<string, McpServerStatus>();
|
|
28
|
+
let cachedConfigs: McpServerConfig[] = [];
|
|
18
29
|
let connectPromise: Promise<void> | null = null;
|
|
30
|
+
let cachedCwd = "";
|
|
19
31
|
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
32
|
+
const MCP_RESULT_FOLD_LINES = 45;
|
|
33
|
+
|
|
34
|
+
function trimTrailingEmptyLines(lines: string[]): string[] {
|
|
35
|
+
let end = lines.length;
|
|
36
|
+
while (end > 0 && lines[end - 1] === "") end -= 1;
|
|
37
|
+
return lines.slice(0, end);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function collapseMcpText(text: string, maxLines = MCP_RESULT_FOLD_LINES) {
|
|
41
|
+
const lines = trimTrailingEmptyLines(text.split("\n"));
|
|
42
|
+
const totalLines = lines.length;
|
|
43
|
+
const displayLines = lines.slice(0, maxLines);
|
|
44
|
+
const remainingLines = Math.max(0, totalLines - maxLines);
|
|
45
|
+
return { totalLines, displayLines, remainingLines };
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function getTextContent(result: { content?: Array<{ type: string; text?: string }> }): string {
|
|
49
|
+
return (result.content ?? [])
|
|
50
|
+
.filter((c): c is { type: "text"; text?: string } => c.type === "text")
|
|
51
|
+
.map((c) => c.text ?? "")
|
|
52
|
+
.join("\n");
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function formatMcpResultText(text: string, expanded: boolean, theme: any): string {
|
|
56
|
+
const { totalLines, displayLines, remainingLines } = collapseMcpText(
|
|
57
|
+
text,
|
|
58
|
+
expanded ? Number.MAX_SAFE_INTEGER : MCP_RESULT_FOLD_LINES,
|
|
59
|
+
);
|
|
60
|
+
let rendered = displayLines.join("\n") ? theme.fg("toolOutput", displayLines.join("\n")) : "";
|
|
61
|
+
if (!expanded && remainingLines > 0) {
|
|
62
|
+
rendered += `${theme.fg("muted", `\n... (${remainingLines} more lines, ${totalLines} total,`)} ${keyHint("app.tools.expand", "to expand")})`;
|
|
63
|
+
}
|
|
64
|
+
return rendered;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function renderMcpResult(result: any, options: { expanded: boolean }, theme: any, context: any) {
|
|
68
|
+
const component = context.lastComponent ?? new Text("", 0, 0);
|
|
69
|
+
component.setText(formatMcpResultText(getTextContent(result), options.expanded, theme));
|
|
70
|
+
return component;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// ── config helpers ────────────────────────────────────────────────────────
|
|
74
|
+
|
|
75
|
+
export function updateConfigEnabled(serverName: string, enabled: boolean): void {
|
|
76
|
+
const config = cachedConfigs.find(c => c.name === serverName);
|
|
77
|
+
if (config) config.enabled = enabled;
|
|
78
|
+
const server = allServers.get(serverName);
|
|
79
|
+
if (server) {
|
|
80
|
+
if (!enabled) {
|
|
81
|
+
// Stash the real connection state, set to disabled
|
|
82
|
+
server.state = "disabled";
|
|
83
|
+
} else {
|
|
84
|
+
// Re-enable: if there's still an active connection, restore it
|
|
85
|
+
const conn = activeConnections.find(c => c.serverName === serverName);
|
|
86
|
+
server.state = conn ? "connected" : "connecting";
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// ── helpers ───────────────────────────────────────────────────────────────
|
|
92
|
+
|
|
93
|
+
function serverDescription(s: McpServerConfig): string | undefined {
|
|
94
|
+
return s.description || getMcpDescription(s.name, cachedCwd);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function makeToolName(serverName: string, toolName: string): string {
|
|
98
|
+
return `${serverName}_${toolName}`;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
function makeToolLabel(serverName: string, toolName: string, desc?: string): string {
|
|
102
|
+
return `MCP ${serverName}: ${toolName}${desc ? ` (${desc.slice(0, 20)})` : ""}`;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// ── cache helpers ─────────────────────────────────────────────────────────
|
|
106
|
+
|
|
107
|
+
function cacheScopeForSource(source: string): "global" | "project" {
|
|
108
|
+
return source === "project" ? "project" : "global";
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// ── auto-summary ──────────────────────────────────────────────────────────
|
|
112
|
+
|
|
113
|
+
async function autoDescribeServer(
|
|
114
|
+
conn: McpConnection,
|
|
115
|
+
serverName: string,
|
|
116
|
+
registry: any,
|
|
117
|
+
): Promise<string> {
|
|
118
|
+
const descs = conn.tools.map(t => `- ${t.name}: ${t.description || "(no description)"}`).join("\n");
|
|
119
|
+
|
|
120
|
+
const prompt = `Describe what this MCP server is and what it does, based on the tools it exposes. Start with action verbs directly, like a capability summary.
|
|
121
|
+
|
|
122
|
+
Server: "${serverName}"
|
|
123
|
+
Tools:
|
|
124
|
+
${descs}
|
|
125
|
+
|
|
126
|
+
Respond with ONLY one short sentence. No quotes.`;
|
|
127
|
+
|
|
128
|
+
return await summarizeWithBroker(registry, prompt) || `${serverName} MCP server (${conn.tools.length} tools)`;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
async function summarizeWithBroker(registry: any, prompt: string): Promise<string | undefined> {
|
|
132
|
+
if (!registry) return undefined;
|
|
133
|
+
|
|
134
|
+
const brokerKey = getMcpBrokerModelKey() || getCompactModelKey();
|
|
135
|
+
const model = brokerKey
|
|
136
|
+
? (() => {
|
|
137
|
+
const parsed = parseModelKey(brokerKey);
|
|
138
|
+
return parsed ? registry.find(parsed.provider, parsed.modelId) : undefined;
|
|
139
|
+
})()
|
|
140
|
+
: undefined;
|
|
141
|
+
|
|
142
|
+
if (!model) return undefined;
|
|
143
|
+
|
|
144
|
+
try {
|
|
145
|
+
const auth = await registry.getApiKeyAndHeaders(model);
|
|
146
|
+
if (!auth.ok) return undefined;
|
|
147
|
+
|
|
148
|
+
const { complete } = await import("@earendil-works/pi-ai");
|
|
149
|
+
const resp = await complete(model, {
|
|
150
|
+
systemPrompt: "You are a concise MCP server description generator.",
|
|
151
|
+
messages: [{
|
|
152
|
+
role: "user" as const,
|
|
153
|
+
content: [{ type: "text" as const, text: prompt }],
|
|
154
|
+
timestamp: Date.now(),
|
|
155
|
+
}],
|
|
156
|
+
}, {
|
|
157
|
+
maxTokens: 128,
|
|
158
|
+
apiKey: auth.apiKey ?? "",
|
|
159
|
+
headers: auth.headers,
|
|
160
|
+
signal: AbortSignal.timeout(15_000),
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
if (resp.stopReason === "error") return undefined;
|
|
164
|
+
return resp.content
|
|
165
|
+
.filter((c: any): c is { type: "text"; text: string } => c.type === "text")
|
|
166
|
+
.map((c: any) => c.text).join(" ").trim() || undefined;
|
|
167
|
+
} catch {
|
|
168
|
+
return undefined;
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
// ── register cached tools ─────────────────────────────────────────────────
|
|
47
173
|
|
|
174
|
+
function registerCachedTools(pi: ExtensionAPI, configs: McpServerConfig[]): void {
|
|
175
|
+
const cache = loadMcpCache(cachedCwd);
|
|
176
|
+
if (!cache) return;
|
|
177
|
+
|
|
178
|
+
for (const config of configs) {
|
|
179
|
+
if (!config.enabled) continue;
|
|
180
|
+
const entry = cache.servers[config.name];
|
|
181
|
+
if (!entry || entry.tools.length === 0) continue;
|
|
182
|
+
|
|
183
|
+
for (const t of entry.tools) {
|
|
184
|
+
const toolName = makeToolName(config.name, t.name);
|
|
185
|
+
const desc = t.description || `${t.name} (MCP tool)`;
|
|
186
|
+
pi.registerTool({
|
|
187
|
+
name: toolName,
|
|
188
|
+
label: makeToolLabel(config.name, t.name, t.description),
|
|
189
|
+
description: desc,
|
|
190
|
+
promptSnippet: desc || `MCP tool ${config.name}/${t.name}`,
|
|
191
|
+
renderResult: renderMcpResult,
|
|
192
|
+
parameters: t.inputSchema,
|
|
193
|
+
execute: async (_id, params, _signal, _update, _ctx) => {
|
|
194
|
+
const conn = activeConnections.find(c => c.serverName === config.name);
|
|
195
|
+
if (!conn) {
|
|
196
|
+
return {
|
|
197
|
+
content: [{ type: "text", text: `MCP server "${config.name}" is not connected.` }],
|
|
198
|
+
isError: true,
|
|
199
|
+
details: {},
|
|
200
|
+
};
|
|
201
|
+
}
|
|
48
202
|
try {
|
|
49
|
-
await conn.
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
for (const tool of conn.tools) {
|
|
53
|
-
const prefixedName = `${server.name}_${tool.name}`;
|
|
54
|
-
pi.registerTool({
|
|
55
|
-
name: prefixedName,
|
|
56
|
-
label: `MCP: ${tool.name}`,
|
|
57
|
-
description: tool.description,
|
|
58
|
-
promptSnippet: tool.description.slice(0, 120),
|
|
59
|
-
parameters: Type.Unsafe(tool.inputSchema as never),
|
|
60
|
-
execute: async (_toolCallId, params, _signal, _onUpdate, _ctx2) => {
|
|
61
|
-
const text = await conn.callTool(
|
|
62
|
-
tool.name,
|
|
63
|
-
params as Record<string, unknown>,
|
|
64
|
-
);
|
|
65
|
-
return {
|
|
66
|
-
content: [{ type: "text" as const, text }],
|
|
67
|
-
isError: false,
|
|
68
|
-
details: { server: server.name, tool: tool.name },
|
|
69
|
-
};
|
|
70
|
-
},
|
|
71
|
-
});
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
allServers.set(server.name, {
|
|
75
|
-
name: server.name,
|
|
76
|
-
url: server.url,
|
|
77
|
-
source: server.source,
|
|
78
|
-
state: "connected",
|
|
79
|
-
toolCount: conn.tools.length,
|
|
80
|
-
tools: conn.tools.map((t) => ({ name: t.name, description: t.description })),
|
|
81
|
-
});
|
|
203
|
+
const text = await conn.callTool(t.name, params as Record<string, unknown>);
|
|
204
|
+
return { content: [{ type: "text", text }], isError: false, details: {} };
|
|
82
205
|
} catch (err) {
|
|
83
206
|
const msg = err instanceof Error ? err.message : String(err);
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
207
|
+
return {
|
|
208
|
+
content: [{ type: "text", text: `MCP call failed on "${config.name}/${t.name}": ${msg}` }],
|
|
209
|
+
isError: true,
|
|
210
|
+
details: {},
|
|
211
|
+
};
|
|
212
|
+
}
|
|
213
|
+
},
|
|
214
|
+
});
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
// ── connect ───────────────────────────────────────────────────────────────
|
|
220
|
+
|
|
221
|
+
async function connectAll(configs: McpServerConfig[], registry: any): Promise<void> {
|
|
222
|
+
allServers = new Map(
|
|
223
|
+
configs.map((s) => [
|
|
224
|
+
s.name,
|
|
225
|
+
{
|
|
226
|
+
name: s.name,
|
|
227
|
+
url: s.url ?? s.command ?? "(unknown)",
|
|
228
|
+
source: s.source,
|
|
229
|
+
description: serverDescription(s),
|
|
230
|
+
state: "connecting" as const,
|
|
231
|
+
toolCount: 0,
|
|
232
|
+
tools: [],
|
|
233
|
+
},
|
|
234
|
+
]),
|
|
235
|
+
);
|
|
236
|
+
|
|
237
|
+
connectPromise = Promise.all(
|
|
238
|
+
configs.map(async (server) => {
|
|
239
|
+
const conn = new McpConnection(server.name, server);
|
|
240
|
+
conn.source = server.source;
|
|
241
|
+
|
|
242
|
+
try {
|
|
243
|
+
await conn.connect(30_000);
|
|
244
|
+
activeConnections.push(conn);
|
|
245
|
+
|
|
246
|
+
let desc = serverDescription(server);
|
|
247
|
+
if (!desc) {
|
|
248
|
+
desc = await autoDescribeServer(conn, server.name, registry);
|
|
249
|
+
if (desc) {
|
|
250
|
+
if (server.source === "project") saveProjectMcpDescription(cachedCwd, server.name, desc);
|
|
251
|
+
else setMcpDescription(server.name, desc);
|
|
93
252
|
}
|
|
94
|
-
}
|
|
95
|
-
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
allServers.set(server.name, {
|
|
256
|
+
name: server.name,
|
|
257
|
+
url: server.url ?? server.command ?? "(unknown)",
|
|
258
|
+
source: server.source,
|
|
259
|
+
description: desc,
|
|
260
|
+
state: "connected",
|
|
261
|
+
toolCount: conn.tools.length,
|
|
262
|
+
tools: conn.tools.map((t) => ({
|
|
263
|
+
name: t.name,
|
|
264
|
+
description: t.description,
|
|
265
|
+
inputSchema: t.inputSchema,
|
|
266
|
+
})),
|
|
267
|
+
});
|
|
268
|
+
|
|
269
|
+
// Update cache with this server's tools
|
|
270
|
+
const tools: McpToolCache[] = conn.tools.map(t => ({
|
|
271
|
+
name: t.name,
|
|
272
|
+
description: t.description,
|
|
273
|
+
inputSchema: t.inputSchema,
|
|
274
|
+
}));
|
|
275
|
+
updateServerCache(
|
|
276
|
+
server.name,
|
|
277
|
+
{ description: desc, tools, cachedAt: Date.now() },
|
|
278
|
+
cacheScopeForSource(server.source),
|
|
279
|
+
cachedCwd || undefined,
|
|
280
|
+
);
|
|
281
|
+
} catch (err) {
|
|
282
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
283
|
+
allServers.set(server.name, {
|
|
284
|
+
name: server.name,
|
|
285
|
+
url: server.url ?? server.command ?? "(unknown)",
|
|
286
|
+
source: server.source,
|
|
287
|
+
description: serverDescription(server),
|
|
288
|
+
state: "failed",
|
|
289
|
+
toolCount: 0,
|
|
290
|
+
tools: [],
|
|
291
|
+
error: msg,
|
|
292
|
+
});
|
|
293
|
+
}
|
|
294
|
+
}),
|
|
295
|
+
).then(() => undefined);
|
|
296
|
+
|
|
297
|
+
await connectPromise;
|
|
298
|
+
connectPromise = null;
|
|
299
|
+
}
|
|
96
300
|
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
301
|
+
// ── setup ─────────────────────────────────────────────────────────────────
|
|
302
|
+
|
|
303
|
+
export function setupMcp(pi: ExtensionAPI) {
|
|
304
|
+
pi.on("session_start", async (_event, ctx: ExtensionContext) => {
|
|
305
|
+
await teardownMcp();
|
|
306
|
+
cachedCwd = ctx.cwd;
|
|
307
|
+
|
|
308
|
+
const configs = resolveMcpConfigs(ctx.cwd).sort((a, b) => a.name.localeCompare(b.name));
|
|
309
|
+
cachedConfigs = configs;
|
|
310
|
+
if (configs.length === 0) return;
|
|
311
|
+
|
|
312
|
+
// Clean stale cache entries for removed servers
|
|
313
|
+
cleanupStaleCache(configs, cachedCwd);
|
|
314
|
+
|
|
315
|
+
const enabledConfigs = configs.filter(s => s.enabled);
|
|
316
|
+
|
|
317
|
+
// Register tools from cache — prompt-stable, works even if MCP is down
|
|
318
|
+
registerCachedTools(pi, enabledConfigs);
|
|
319
|
+
|
|
320
|
+
const needSummary = enabledConfigs.filter(s => !serverDescription(s));
|
|
321
|
+
|
|
322
|
+
if (needSummary.length === 0) {
|
|
323
|
+
// All servers have descriptions — connect in background, update cache
|
|
324
|
+
void connectAll(enabledConfigs, ctx.modelRegistry);
|
|
325
|
+
return;
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
// Some servers lack description — connect and auto-summarize synchronously
|
|
329
|
+
await connectAll(enabledConfigs, ctx.modelRegistry);
|
|
100
330
|
});
|
|
101
331
|
|
|
102
332
|
pi.on("session_shutdown", () => {
|
|
@@ -105,7 +335,103 @@ export function setupMcp(pi: ExtensionAPI) {
|
|
|
105
335
|
}
|
|
106
336
|
|
|
107
337
|
export function getMcpStatus(): McpServerStatus[] {
|
|
108
|
-
|
|
338
|
+
const cache = loadMcpCache(cachedCwd);
|
|
339
|
+
const result: McpServerStatus[] = [];
|
|
340
|
+
for (const config of cachedConfigs) {
|
|
341
|
+
const connected = allServers.get(config.name);
|
|
342
|
+
if (connected) {
|
|
343
|
+
result.push(connected);
|
|
344
|
+
} else {
|
|
345
|
+
const cachedEntry = cache?.servers[config.name];
|
|
346
|
+
result.push({
|
|
347
|
+
name: config.name,
|
|
348
|
+
url: config.url ?? config.command ?? "(unknown)",
|
|
349
|
+
source: config.source,
|
|
350
|
+
description: serverDescription(config),
|
|
351
|
+
state: config.enabled ? "connecting" : "disabled",
|
|
352
|
+
toolCount: cachedEntry?.tools.length ?? 0,
|
|
353
|
+
tools: cachedEntry?.tools ?? [],
|
|
354
|
+
});
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
return result;
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
// ── refresh single server cache ───────────────────────────────────────────
|
|
361
|
+
|
|
362
|
+
export const __mcpIndexTest = { collapseMcpText };
|
|
363
|
+
|
|
364
|
+
export async function refreshServerCache(
|
|
365
|
+
serverName: string,
|
|
366
|
+
registry: any,
|
|
367
|
+
): Promise<{ ok: boolean; error?: string }> {
|
|
368
|
+
const config = resolveMcpConfigs(cachedCwd).find(s => s.name === serverName);
|
|
369
|
+
if (!config) return { ok: false, error: `Server "${serverName}" not found in config.` };
|
|
370
|
+
|
|
371
|
+
// Disconnect existing connection for this server
|
|
372
|
+
const existing = activeConnections.find(c => c.serverName === serverName);
|
|
373
|
+
if (existing) {
|
|
374
|
+
try { await existing.disconnect(); } catch { /* ignore */ }
|
|
375
|
+
activeConnections = activeConnections.filter(c => c.serverName !== serverName);
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
const conn = new McpConnection(config.name, config);
|
|
379
|
+
conn.source = config.source;
|
|
380
|
+
|
|
381
|
+
try {
|
|
382
|
+
await conn.connect(30_000);
|
|
383
|
+
activeConnections.push(conn);
|
|
384
|
+
|
|
385
|
+
let desc = serverDescription(config);
|
|
386
|
+
if (!desc) {
|
|
387
|
+
desc = await autoDescribeServer(conn, config.name, registry);
|
|
388
|
+
if (desc) {
|
|
389
|
+
if (config.source === "project") saveProjectMcpDescription(cachedCwd, config.name, desc);
|
|
390
|
+
else setMcpDescription(config.name, desc);
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
allServers.set(config.name, {
|
|
395
|
+
name: config.name,
|
|
396
|
+
url: config.url ?? config.command ?? "(unknown)",
|
|
397
|
+
source: config.source,
|
|
398
|
+
description: desc,
|
|
399
|
+
state: "connected",
|
|
400
|
+
toolCount: conn.tools.length,
|
|
401
|
+
tools: conn.tools.map(t => ({
|
|
402
|
+
name: t.name,
|
|
403
|
+
description: t.description,
|
|
404
|
+
inputSchema: t.inputSchema,
|
|
405
|
+
})),
|
|
406
|
+
});
|
|
407
|
+
|
|
408
|
+
const tools: McpToolCache[] = conn.tools.map(t => ({
|
|
409
|
+
name: t.name,
|
|
410
|
+
description: t.description,
|
|
411
|
+
inputSchema: t.inputSchema,
|
|
412
|
+
}));
|
|
413
|
+
updateServerCache(
|
|
414
|
+
config.name,
|
|
415
|
+
{ description: desc, tools, cachedAt: Date.now() },
|
|
416
|
+
cacheScopeForSource(config.source),
|
|
417
|
+
cachedCwd || undefined,
|
|
418
|
+
);
|
|
419
|
+
|
|
420
|
+
return { ok: true };
|
|
421
|
+
} catch (err) {
|
|
422
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
423
|
+
allServers.set(config.name, {
|
|
424
|
+
name: config.name,
|
|
425
|
+
url: config.url ?? config.command ?? "(unknown)",
|
|
426
|
+
source: config.source,
|
|
427
|
+
description: serverDescription(config),
|
|
428
|
+
state: "failed",
|
|
429
|
+
toolCount: 0,
|
|
430
|
+
tools: [],
|
|
431
|
+
error: msg,
|
|
432
|
+
});
|
|
433
|
+
return { ok: false, error: msg };
|
|
434
|
+
}
|
|
109
435
|
}
|
|
110
436
|
|
|
111
437
|
async function teardownMcp(): Promise<void> {
|
|
@@ -120,4 +446,5 @@ async function teardownMcp(): Promise<void> {
|
|
|
120
446
|
);
|
|
121
447
|
activeConnections = [];
|
|
122
448
|
allServers = new Map();
|
|
449
|
+
cachedConfigs = [];
|
|
123
450
|
}
|
|
@@ -25,8 +25,8 @@ import { fileTypeFromFile } from "file-type";
|
|
|
25
25
|
import { isContextOverflow, type Model } from "@earendil-works/pi-ai";
|
|
26
26
|
import {
|
|
27
27
|
loadConfig, saveConfig, parseModelKey, formatModelKey,
|
|
28
|
-
getImageModelKey, getCompactModelKey,
|
|
29
|
-
setImageModelKey, setCompactModelKey,
|
|
28
|
+
getImageModelKey, getCompactModelKey, getMcpBrokerModelKey,
|
|
29
|
+
setImageModelKey, setCompactModelKey, setMcpBrokerModelKey,
|
|
30
30
|
} from "./settings.js";
|
|
31
31
|
import * as fs from "node:fs";
|
|
32
32
|
import * as os from "node:os";
|
|
@@ -172,6 +172,7 @@ function setupImageReadFallback(pi: ExtensionAPI) {
|
|
|
172
172
|
|
|
173
173
|
const TAB_IMAGE = 0;
|
|
174
174
|
const TAB_COMPACT = 1;
|
|
175
|
+
const TAB_BROKER = 2;
|
|
175
176
|
|
|
176
177
|
export class ModelPickerComponent extends Container {
|
|
177
178
|
private searchInput: Input;
|
|
@@ -182,6 +183,7 @@ export class ModelPickerComponent extends Container {
|
|
|
182
183
|
private activeTab = TAB_IMAGE;
|
|
183
184
|
private imageKey: string | null;
|
|
184
185
|
private compactKey: string | null;
|
|
186
|
+
private brokerKey: string | null;
|
|
185
187
|
private allItems: { label: string; desc: string; model: Model<any> | null; modelName?: string }[] = [];
|
|
186
188
|
private filtered: typeof this.allItems = [];
|
|
187
189
|
private selectedIndex = 0;
|
|
@@ -197,6 +199,7 @@ export class ModelPickerComponent extends Container {
|
|
|
197
199
|
this.onDone = onDone;
|
|
198
200
|
this.imageKey = getImageModelKey();
|
|
199
201
|
this.compactKey = getCompactModelKey();
|
|
202
|
+
this.brokerKey = getMcpBrokerModelKey();
|
|
200
203
|
|
|
201
204
|
this.addChild(new DynamicBorder());
|
|
202
205
|
this.addChild(new Spacer(1));
|
|
@@ -232,8 +235,8 @@ export class ModelPickerComponent extends Container {
|
|
|
232
235
|
}
|
|
233
236
|
}
|
|
234
237
|
|
|
235
|
-
private currentKey() { return this.activeTab === TAB_IMAGE ? this.imageKey : this.compactKey; }
|
|
236
|
-
private currentKind() { return this.activeTab === TAB_IMAGE ? "image" : "compact"; }
|
|
238
|
+
private currentKey() { return this.activeTab === TAB_IMAGE ? this.imageKey : this.activeTab === TAB_BROKER ? this.brokerKey : this.compactKey; }
|
|
239
|
+
private currentKind() { return this.activeTab === TAB_IMAGE ? "image" : this.activeTab === TAB_BROKER ? "broker" : "compact"; }
|
|
237
240
|
|
|
238
241
|
private switchTab(tab: number) {
|
|
239
242
|
this.activeTab = tab;
|
|
@@ -260,16 +263,21 @@ export class ModelPickerComponent extends Container {
|
|
|
260
263
|
const t = this.theme;
|
|
261
264
|
const im = this.activeTab === TAB_IMAGE ? t.fg("accent", "●") : "○";
|
|
262
265
|
const cm = this.activeTab === TAB_COMPACT ? t.fg("accent", "●") : "○";
|
|
263
|
-
const
|
|
264
|
-
const
|
|
265
|
-
this.
|
|
266
|
+
const bm = this.activeTab === TAB_BROKER ? t.fg("accent", "●") : "○";
|
|
267
|
+
const il = this.activeTab === TAB_IMAGE ? t.bold("Image") : t.fg("dim", "Image");
|
|
268
|
+
const cl = this.activeTab === TAB_COMPACT ? t.bold("Compact") : t.fg("dim", "Compact");
|
|
269
|
+
const bl = this.activeTab === TAB_BROKER ? t.bold("Broker") : t.fg("dim", "Broker");
|
|
270
|
+
this.tabTitle.setText(`${im} ${il} | ${cm} ${cl} | ${bm} ${bl}`);
|
|
266
271
|
const key = this.currentKey();
|
|
267
272
|
this.subtitleText.setText(key ? t.fg("warning", `Current ${this.currentKind()} model: ${key}`) : t.fg("warning", `No ${this.currentKind()} model set`));
|
|
268
273
|
}
|
|
269
274
|
|
|
270
275
|
handleInput(keyData: string) {
|
|
271
276
|
const kb = getKeybindings();
|
|
272
|
-
if (kb.matches(keyData, "tui.input.tab")) {
|
|
277
|
+
if (kb.matches(keyData, "tui.input.tab")) {
|
|
278
|
+
const next = this.activeTab === TAB_IMAGE ? TAB_COMPACT : this.activeTab === TAB_COMPACT ? TAB_BROKER : TAB_IMAGE;
|
|
279
|
+
this.switchTab(next); this.tui.requestRender(); return;
|
|
280
|
+
}
|
|
273
281
|
if (kb.matches(keyData, "tui.select.up")) { this.selectedIndex = this.selectedIndex === 0 ? this.filtered.length - 1 : this.selectedIndex - 1; this.updateList(); return; }
|
|
274
282
|
if (kb.matches(keyData, "tui.select.down")) { this.selectedIndex = this.selectedIndex === this.filtered.length - 1 ? 0 : this.selectedIndex + 1; this.updateList(); return; }
|
|
275
283
|
if (kb.matches(keyData, "tui.select.confirm")) { const s = this.filtered[this.selectedIndex]; if (s) this.selectModel(s.model); return; }
|
|
@@ -289,12 +297,15 @@ export class ModelPickerComponent extends Container {
|
|
|
289
297
|
const kind = this.currentKind();
|
|
290
298
|
if (model) {
|
|
291
299
|
if (kind === "image") setImageModelKey(formatModelKey(model));
|
|
300
|
+
else if (kind === "broker") setMcpBrokerModelKey(formatModelKey(model));
|
|
292
301
|
else setCompactModelKey(formatModelKey(model));
|
|
293
302
|
} else {
|
|
294
303
|
if (kind === "image") setImageModelKey(null);
|
|
304
|
+
else if (kind === "broker") setMcpBrokerModelKey(null);
|
|
295
305
|
else setCompactModelKey(null);
|
|
296
306
|
}
|
|
297
307
|
if (kind === "image") this.imageKey = model ? formatModelKey(model) : null;
|
|
308
|
+
else if (kind === "broker") this.brokerKey = model ? formatModelKey(model) : null;
|
|
298
309
|
else this.compactKey = model ? formatModelKey(model) : null;
|
|
299
310
|
this.switchTab(this.activeTab); this.tui.requestRender();
|
|
300
311
|
}
|