@herbertgao/pi-extensions 2026.8.15 → 2026.9.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 +4 -1
- package/THIRD_PARTY_NOTICES.md +26 -0
- package/node_modules/@herbertgao/pi-cc-extensions/extensions/config/config.ts +20 -0
- package/node_modules/@herbertgao/pi-cc-extensions/extensions/config/panel.ts +46 -3
- package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/mouse/interaction.ts +55 -17
- package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/mouse/packets.ts +6 -0
- package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/mouse/scroll.ts +5 -0
- package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/tool/result.ts +34 -30
- package/node_modules/@herbertgao/pi-cc-extensions/package.json +3 -3
- package/node_modules/@juicesharp/rpiv-ask-user-question/package.json +2 -2
- package/node_modules/@narumitw/pi-btw/README.md +5 -1
- package/node_modules/@narumitw/pi-btw/dist/index.ts +143 -14
- package/node_modules/@narumitw/pi-btw/dist/index.ts.map +3 -3
- package/node_modules/@narumitw/pi-btw/package.json +4 -4
- package/node_modules/@narumitw/pi-btw/src/fullscreen-ui.ts +158 -7
- package/node_modules/@narumitw/pi-btw/src/text.ts +18 -0
- package/node_modules/@narumitw/pi-btw/src/transcript-pager.ts +5 -16
- package/node_modules/@narumitw/pi-caffeinate/LICENSE +21 -0
- package/node_modules/@narumitw/pi-caffeinate/README.md +224 -0
- package/node_modules/@narumitw/pi-caffeinate/dist/index.ts +1112 -0
- package/node_modules/@narumitw/pi-caffeinate/dist/index.ts.map +7 -0
- package/node_modules/@narumitw/pi-caffeinate/package.json +53 -0
- package/node_modules/@narumitw/pi-caffeinate/src/caffeinate.ts +811 -0
- package/node_modules/@narumitw/pi-caffeinate/src/dbus-inhibit.ts +153 -0
- package/node_modules/@narumitw/pi-caffeinate/src/index.ts +1 -0
- package/node_modules/@narumitw/pi-caffeinate/src/inhibitor-process.ts +43 -0
- package/node_modules/@narumitw/pi-caffeinate/src/inhibitors.ts +167 -0
- package/node_modules/@narumitw/pi-caffeinate/src/settings.ts +202 -0
- package/node_modules/@tifan/pi-handoff/README.md +6 -3
- package/node_modules/@tifan/pi-handoff/package.json +1 -1
- package/node_modules/@tifan/pi-handoff/src/index.ts +8 -3
- package/node_modules/pi-mcp-adapter/CHANGELOG.md +27 -0
- package/node_modules/pi-mcp-adapter/README.md +13 -13
- package/node_modules/pi-mcp-adapter/commands.ts +58 -21
- package/node_modules/pi-mcp-adapter/config.ts +18 -4
- package/node_modules/pi-mcp-adapter/direct-tools.ts +26 -6
- package/node_modules/pi-mcp-adapter/dist/config.d.ts +4 -0
- package/node_modules/pi-mcp-adapter/dist/config.js +13 -4
- package/node_modules/pi-mcp-adapter/dist/config.js.map +1 -1
- package/node_modules/pi-mcp-adapter/dist/types.d.ts +11 -3
- package/node_modules/pi-mcp-adapter/dist/types.js.map +1 -1
- package/node_modules/pi-mcp-adapter/error-signal.ts +15 -4
- package/node_modules/pi-mcp-adapter/errors.ts +141 -0
- package/node_modules/pi-mcp-adapter/host-html-template.ts +58 -5
- package/node_modules/pi-mcp-adapter/index.ts +2 -3
- package/node_modules/pi-mcp-adapter/init.ts +5 -0
- package/node_modules/pi-mcp-adapter/mcp-panel.ts +30 -9
- package/node_modules/pi-mcp-adapter/mcp-setup-panel.ts +66 -28
- package/node_modules/pi-mcp-adapter/mcp-status.ts +2 -0
- package/node_modules/pi-mcp-adapter/package.json +2 -1
- package/node_modules/pi-mcp-adapter/proxy-modes.ts +66 -13
- package/node_modules/pi-mcp-adapter/sandbox-proxy-template.ts +217 -0
- package/node_modules/pi-mcp-adapter/server-manager.ts +337 -6
- package/node_modules/pi-mcp-adapter/skills/mcp-scripting/SKILL.md +1 -0
- package/node_modules/pi-mcp-adapter/types.ts +18 -3
- package/node_modules/pi-mcp-adapter/ui-resource-handler.ts +18 -2
- package/node_modules/pi-mcp-adapter/ui-server.ts +179 -54
- package/node_modules/pi-mcp-adapter/ui-session.ts +28 -8
- package/package.json +11 -7
|
@@ -3,6 +3,8 @@ import type { UiHostContext, UiResourceContent, UiResourceCsp } from "./types.ts
|
|
|
3
3
|
// Use locally bundled AppBridge to avoid CDN Zod bundling issues
|
|
4
4
|
const DEFAULT_APP_BRIDGE_MODULE_URL = "/app-bridge.bundle.js";
|
|
5
5
|
const APP_SANDBOX = "allow-scripts allow-forms allow-modals allow-popups allow-downloads";
|
|
6
|
+
const APP_PROXY_SANDBOX = `${APP_SANDBOX} allow-same-origin`;
|
|
7
|
+
const APP_INNER_SANDBOX = APP_PROXY_SANDBOX;
|
|
6
8
|
|
|
7
9
|
export interface HostHtmlTemplateInput {
|
|
8
10
|
sessionToken: string;
|
|
@@ -16,6 +18,7 @@ export interface HostHtmlTemplateInput {
|
|
|
16
18
|
cacheToolConsent: boolean;
|
|
17
19
|
hostContext?: UiHostContext;
|
|
18
20
|
appBridgeModuleUrl?: string;
|
|
21
|
+
sandboxProxyUrl: string;
|
|
19
22
|
}
|
|
20
23
|
|
|
21
24
|
export function buildHostHtmlTemplate(input: HostHtmlTemplateInput): string {
|
|
@@ -31,6 +34,9 @@ export function buildHostHtmlTemplate(input: HostHtmlTemplateInput): string {
|
|
|
31
34
|
const requireToolConsent = safeInlineJSON(input.requireToolConsent);
|
|
32
35
|
const cacheToolConsent = safeInlineJSON(input.cacheToolConsent);
|
|
33
36
|
const moduleUrl = safeInlineJSON(input.appBridgeModuleUrl ?? DEFAULT_APP_BRIDGE_MODULE_URL);
|
|
37
|
+
const sandboxProxyUrl = safeInlineJSON(input.sandboxProxyUrl);
|
|
38
|
+
const resourceCsp = safeInlineJSON(input.resource.meta.csp);
|
|
39
|
+
const resourcePermissions = safeInlineJSON(input.resource.meta.permissions);
|
|
34
40
|
|
|
35
41
|
return `<!doctype html>
|
|
36
42
|
<html lang="en">
|
|
@@ -112,7 +118,7 @@ export function buildHostHtmlTemplate(input: HostHtmlTemplateInput): string {
|
|
|
112
118
|
</div>
|
|
113
119
|
</header>
|
|
114
120
|
<main>
|
|
115
|
-
<iframe id="mcp-app" sandbox="${
|
|
121
|
+
<iframe id="mcp-app" sandbox="${APP_PROXY_SANDBOX}" referrerpolicy="no-referrer"></iframe>
|
|
116
122
|
</main>
|
|
117
123
|
<div class="overlay" id="error-overlay">
|
|
118
124
|
<div class="panel">
|
|
@@ -138,6 +144,10 @@ export function buildHostHtmlTemplate(input: HostHtmlTemplateInput): string {
|
|
|
138
144
|
const ALLOW_ATTRIBUTE = ${allowAttribute};
|
|
139
145
|
const REQUIRE_TOOL_CONSENT = ${requireToolConsent};
|
|
140
146
|
const CACHE_TOOL_CONSENT = ${cacheToolConsent};
|
|
147
|
+
const SANDBOX_PROXY_URL = ${sandboxProxyUrl};
|
|
148
|
+
const RESOURCE_CSP = ${resourceCsp};
|
|
149
|
+
const RESOURCE_PERMISSIONS = ${resourcePermissions};
|
|
150
|
+
const INNER_SANDBOX = ${safeInlineJSON(APP_INNER_SANDBOX)};
|
|
141
151
|
const STREAM_CONTEXT_KEY = "pi-mcp-adapter/stream";
|
|
142
152
|
const STREAM_PATCH_METHOD = "notifications/pi-mcp-adapter/ui-result-patch";
|
|
143
153
|
|
|
@@ -148,6 +158,7 @@ export function buildHostHtmlTemplate(input: HostHtmlTemplateInput): string {
|
|
|
148
158
|
const errorOverlay = document.getElementById("error-overlay");
|
|
149
159
|
const completionOverlay = document.getElementById("completion-overlay");
|
|
150
160
|
const errorMessage = document.getElementById("error-message");
|
|
161
|
+
const sandboxProxyOrigin = new URL(SANDBOX_PROXY_URL).origin;
|
|
151
162
|
|
|
152
163
|
document.getElementById("server-name").textContent = SERVER_NAME;
|
|
153
164
|
document.getElementById("tool-name").textContent = TOOL_NAME;
|
|
@@ -205,10 +216,41 @@ export function buildHostHtmlTemplate(input: HostHtmlTemplateInput): string {
|
|
|
205
216
|
const bridge = new AppBridge(
|
|
206
217
|
null,
|
|
207
218
|
{ name: "pi", version: "1.0.0" },
|
|
208
|
-
{
|
|
219
|
+
{
|
|
220
|
+
serverTools: {},
|
|
221
|
+
openLinks: {},
|
|
222
|
+
logging: {},
|
|
223
|
+
updateModelContext: {},
|
|
224
|
+
message: {},
|
|
225
|
+
sandbox: {
|
|
226
|
+
...(RESOURCE_CSP ? { csp: RESOURCE_CSP } : {}),
|
|
227
|
+
...(RESOURCE_PERMISSIONS ? { permissions: RESOURCE_PERMISSIONS } : {}),
|
|
228
|
+
},
|
|
229
|
+
},
|
|
209
230
|
{ hostContext: HOST_CONTEXT }
|
|
210
231
|
);
|
|
211
232
|
|
|
233
|
+
let sandboxResourceSent = false;
|
|
234
|
+
bridge.onsandboxready = async () => {
|
|
235
|
+
if (sandboxResourceSent) return;
|
|
236
|
+
sandboxResourceSent = true;
|
|
237
|
+
try {
|
|
238
|
+
const response = await fetch("/ui-app?resource=" + encodeURIComponent(UI_RESOURCE_TOKEN), {
|
|
239
|
+
headers: { Accept: "text/html" },
|
|
240
|
+
});
|
|
241
|
+
if (!response.ok) throw new Error("UI resource request failed: HTTP " + response.status);
|
|
242
|
+
const html = await response.text();
|
|
243
|
+
await bridge.sendSandboxResourceReady({
|
|
244
|
+
html,
|
|
245
|
+
sandbox: INNER_SANDBOX,
|
|
246
|
+
...(RESOURCE_CSP ? { csp: RESOURCE_CSP } : {}),
|
|
247
|
+
...(RESOURCE_PERMISSIONS ? { permissions: RESOURCE_PERMISSIONS } : {}),
|
|
248
|
+
});
|
|
249
|
+
} catch (error) {
|
|
250
|
+
showError("Failed to load MCP App resource: " + String(error));
|
|
251
|
+
}
|
|
252
|
+
};
|
|
253
|
+
|
|
212
254
|
bridge.oncalltool = async (params) => {
|
|
213
255
|
if (!consentGranted) {
|
|
214
256
|
const accepted = window.confirm("Allow this UI to call server tools for this session?");
|
|
@@ -240,7 +282,7 @@ export function buildHostHtmlTemplate(input: HostHtmlTemplateInput): string {
|
|
|
240
282
|
// Also listen for raw postMessage events with custom types (notify, prompt, intent, etc.)
|
|
241
283
|
// These bypass the AppBridge protocol but are used by some MCP UI implementations
|
|
242
284
|
window.addEventListener("message", async (event) => {
|
|
243
|
-
if (event.source !== iframe.contentWindow) return;
|
|
285
|
+
if (event.source !== iframe.contentWindow || event.origin !== sandboxProxyOrigin) return;
|
|
244
286
|
const data = event.data;
|
|
245
287
|
if (!data || typeof data !== "object") return;
|
|
246
288
|
|
|
@@ -304,6 +346,12 @@ export function buildHostHtmlTemplate(input: HostHtmlTemplateInput): string {
|
|
|
304
346
|
}
|
|
305
347
|
|
|
306
348
|
// Connect bridge BEFORE loading iframe to ensure we're listening when the app sends ui/initialize
|
|
349
|
+
const sandboxMessageGuard = (event) => {
|
|
350
|
+
if (event.source === iframe.contentWindow && event.origin !== sandboxProxyOrigin) {
|
|
351
|
+
event.stopImmediatePropagation();
|
|
352
|
+
}
|
|
353
|
+
};
|
|
354
|
+
window.addEventListener("message", sandboxMessageGuard, true);
|
|
307
355
|
try {
|
|
308
356
|
const transport = new PostMessageTransport(iframe.contentWindow, iframe.contentWindow);
|
|
309
357
|
await bridge.connect(transport);
|
|
@@ -315,7 +363,7 @@ export function buildHostHtmlTemplate(input: HostHtmlTemplateInput): string {
|
|
|
315
363
|
const iframeLoaded = new Promise((resolve) => {
|
|
316
364
|
iframe.onload = resolve;
|
|
317
365
|
});
|
|
318
|
-
iframe.src =
|
|
366
|
+
iframe.src = SANDBOX_PROXY_URL;
|
|
319
367
|
await iframeLoaded;
|
|
320
368
|
|
|
321
369
|
const eventSource = new EventSource("/events?session=" + encodeURIComponent(SESSION_TOKEN));
|
|
@@ -340,6 +388,9 @@ export function buildHostHtmlTemplate(input: HostHtmlTemplateInput): string {
|
|
|
340
388
|
showError("Failed to forward cancellation: " + String(error));
|
|
341
389
|
}
|
|
342
390
|
});
|
|
391
|
+
eventSource.addEventListener("resource-updated", () => {
|
|
392
|
+
setStatus("Resource updated on the server. Reopen this UI to load the latest version.");
|
|
393
|
+
});
|
|
343
394
|
eventSource.addEventListener("result-patch", async (event) => {
|
|
344
395
|
try {
|
|
345
396
|
await bridge.notification({
|
|
@@ -443,7 +494,9 @@ function sanitizeCspDomains(domains: unknown): string[] {
|
|
|
443
494
|
}
|
|
444
495
|
|
|
445
496
|
function safeInlineJSON(value: unknown): string {
|
|
446
|
-
|
|
497
|
+
const json = JSON.stringify(value);
|
|
498
|
+
if (json === undefined) return "undefined";
|
|
499
|
+
return json
|
|
447
500
|
.replace(/</g, "\\u003c")
|
|
448
501
|
.replace(/>/g, "\\u003e")
|
|
449
502
|
.replace(/&/g, "\\u0026")
|
|
@@ -590,7 +590,7 @@ function installMcpAdapter(pi: ExtensionAPI, options: McpAdapterOptions) {
|
|
|
590
590
|
initPromise = null;
|
|
591
591
|
if (earlyConfig.settings?.freezeDirectTools === true) {
|
|
592
592
|
directToolsFrozen = true;
|
|
593
|
-
logger.info("MCP: direct tools frozen after initial sync —
|
|
593
|
+
logger.info("MCP: direct tools frozen after initial sync — metadata can refresh without rebuilding the active tool surface");
|
|
594
594
|
}
|
|
595
595
|
}).catch(async err => {
|
|
596
596
|
if (!owner.isActive() || generation !== lifecycleGeneration) {
|
|
@@ -817,7 +817,6 @@ function installMcpAdapter(pi: ExtensionAPI, options: McpAdapterOptions) {
|
|
|
817
817
|
case "reconnect":
|
|
818
818
|
commandOwner?.throwIfInactive();
|
|
819
819
|
await reconnectServers(state, commandCtx, targetServer);
|
|
820
|
-
if (directToolsFrozen) syncToolSurface(commandCtx);
|
|
821
820
|
break;
|
|
822
821
|
case "tools":
|
|
823
822
|
await showTools(state, commandCtx);
|
|
@@ -1168,7 +1167,7 @@ function installMcpAdapter(pi: ExtensionAPI, options: McpAdapterOptions) {
|
|
|
1168
1167
|
}
|
|
1169
1168
|
if (params.connect) {
|
|
1170
1169
|
const result = await executeConnect(state, params.connect, signal);
|
|
1171
|
-
syncToolSurface(_ctx as ExtensionContext);
|
|
1170
|
+
if (!directToolsFrozen) syncToolSurface(_ctx as ExtensionContext);
|
|
1172
1171
|
return result;
|
|
1173
1172
|
}
|
|
1174
1173
|
if (params.describe) {
|
|
@@ -215,6 +215,10 @@ export async function initializeMcp(
|
|
|
215
215
|
notifyToolMetadataUpdated(state, serverName, reason);
|
|
216
216
|
updateStatusBar(state);
|
|
217
217
|
});
|
|
218
|
+
manager.setListenStateChangedListener?.(() => {
|
|
219
|
+
if (!owner.isActive()) return;
|
|
220
|
+
updateStatusBar(state);
|
|
221
|
+
});
|
|
218
222
|
owner.addCleanup(() => lifecycle.gracefulShutdown());
|
|
219
223
|
owner.addCleanup(() => {
|
|
220
224
|
if (state.uiServer) {
|
|
@@ -641,6 +645,7 @@ export async function lazyConnect(state: McpExtensionState, serverName: string,
|
|
|
641
645
|
return false;
|
|
642
646
|
}
|
|
643
647
|
if (connection?.status === "connected") {
|
|
648
|
+
await state.manager.ensureListen?.(serverName, connection);
|
|
644
649
|
updateServerMetadata(state, serverName);
|
|
645
650
|
markKeepAliveAfterConnect(state, serverName);
|
|
646
651
|
return true;
|
|
@@ -134,6 +134,8 @@ interface ServerState {
|
|
|
134
134
|
includeTools?: string[];
|
|
135
135
|
excludeTools?: string[];
|
|
136
136
|
exposeResources: boolean;
|
|
137
|
+
disabled: boolean;
|
|
138
|
+
wasDisabled: boolean;
|
|
137
139
|
connectionStatus: ConnectionStatus;
|
|
138
140
|
failureMessage?: string | null;
|
|
139
141
|
tools: ToolState[];
|
|
@@ -243,6 +245,7 @@ class McpPanel {
|
|
|
243
245
|
|
|
244
246
|
const status = callbacks.getConnectionStatus(serverName);
|
|
245
247
|
const failureMessage = callbacks.getFailureMessage?.(serverName) ?? null;
|
|
248
|
+
const serverDisabled = isServerDisabled(definition);
|
|
246
249
|
let directCount = 0;
|
|
247
250
|
let directTokens = 0;
|
|
248
251
|
for (const tool of tools) {
|
|
@@ -258,6 +261,8 @@ class McpPanel {
|
|
|
258
261
|
...(definition.includeTools !== undefined ? { includeTools: definition.includeTools } : {}),
|
|
259
262
|
...(definition.excludeTools !== undefined ? { excludeTools: definition.excludeTools } : {}),
|
|
260
263
|
exposeResources: definition.exposeResources !== false,
|
|
264
|
+
disabled: serverDisabled,
|
|
265
|
+
wasDisabled: serverDisabled,
|
|
261
266
|
connectionStatus: status,
|
|
262
267
|
failureMessage,
|
|
263
268
|
tools,
|
|
@@ -275,7 +280,7 @@ class McpPanel {
|
|
|
275
280
|
if (this.inactivityTimeout) clearTimeout(this.inactivityTimeout);
|
|
276
281
|
this.inactivityTimeout = setTimeout(() => {
|
|
277
282
|
this.cleanup();
|
|
278
|
-
this.done({ cancelled: true, changes: new Map() });
|
|
283
|
+
this.done({ cancelled: true, changes: new Map(), disabledChanges: new Map() });
|
|
279
284
|
}, McpPanel.INACTIVITY_MS);
|
|
280
285
|
}
|
|
281
286
|
|
|
@@ -334,12 +339,16 @@ class McpPanel {
|
|
|
334
339
|
}
|
|
335
340
|
|
|
336
341
|
private updateDirty(): void {
|
|
337
|
-
this.dirty = this.servers.some((s) => s.tools.some((t) => t.isDirect !== t.wasDirect));
|
|
342
|
+
this.dirty = this.servers.some((s) => s.disabled !== s.wasDisabled || s.tools.some((t) => t.isDirect !== t.wasDirect));
|
|
338
343
|
}
|
|
339
344
|
|
|
340
345
|
private buildResult(): McpPanelResult {
|
|
341
346
|
const changes = new Map<string, true | string[] | false>();
|
|
347
|
+
const disabledChanges = new Map<string, boolean>();
|
|
342
348
|
for (const server of this.servers) {
|
|
349
|
+
if (server.disabled !== server.wasDisabled) {
|
|
350
|
+
disabledChanges.set(server.name, server.disabled);
|
|
351
|
+
}
|
|
343
352
|
const changed = server.tools.some((t) => t.isDirect !== t.wasDirect);
|
|
344
353
|
if (!changed) continue;
|
|
345
354
|
const directTools = server.tools.filter((t) => t.isDirect);
|
|
@@ -351,7 +360,7 @@ class McpPanel {
|
|
|
351
360
|
changes.set(server.name, directTools.map((t) => t.name));
|
|
352
361
|
}
|
|
353
362
|
}
|
|
354
|
-
return { changes, cancelled: false };
|
|
363
|
+
return { changes, disabledChanges, cancelled: false };
|
|
355
364
|
}
|
|
356
365
|
|
|
357
366
|
handleInput(data: string): void {
|
|
@@ -367,7 +376,7 @@ class McpPanel {
|
|
|
367
376
|
// Global shortcuts — always work, even during desc search
|
|
368
377
|
if (matchesKey(data, "ctrl+c")) {
|
|
369
378
|
this.cleanup();
|
|
370
|
-
this.done({ cancelled: true, changes: new Map() });
|
|
379
|
+
this.done({ cancelled: true, changes: new Map(), disabledChanges: new Map() });
|
|
371
380
|
return;
|
|
372
381
|
}
|
|
373
382
|
|
|
@@ -424,7 +433,7 @@ class McpPanel {
|
|
|
424
433
|
return;
|
|
425
434
|
}
|
|
426
435
|
this.cleanup();
|
|
427
|
-
this.done({ cancelled: true, changes: new Map() });
|
|
436
|
+
this.done({ cancelled: true, changes: new Map(), disabledChanges: new Map() });
|
|
428
437
|
return;
|
|
429
438
|
}
|
|
430
439
|
|
|
@@ -477,6 +486,17 @@ class McpPanel {
|
|
|
477
486
|
return;
|
|
478
487
|
}
|
|
479
488
|
|
|
489
|
+
if (matchesKey(data, "ctrl+d")) {
|
|
490
|
+
const item = this.visibleItems[this.cursorIndex];
|
|
491
|
+
if (!item || item.type !== "server" || this.authOnly) return;
|
|
492
|
+
const server = this.servers[item.serverIndex];
|
|
493
|
+
if (!server) return;
|
|
494
|
+
server.disabled = !server.disabled;
|
|
495
|
+
this.updateDirty();
|
|
496
|
+
this.tui.requestRender();
|
|
497
|
+
return;
|
|
498
|
+
}
|
|
499
|
+
|
|
480
500
|
if (matchesKey(data, "ctrl+y")) {
|
|
481
501
|
const item = this.visibleItems[this.cursorIndex];
|
|
482
502
|
if (!item) return;
|
|
@@ -632,7 +652,7 @@ class McpPanel {
|
|
|
632
652
|
private handleDiscardInput(data: string): void {
|
|
633
653
|
if (matchesKey(data, "ctrl+c")) {
|
|
634
654
|
this.cleanup();
|
|
635
|
-
this.done({ cancelled: true, changes: new Map() });
|
|
655
|
+
this.done({ cancelled: true, changes: new Map(), disabledChanges: new Map() });
|
|
636
656
|
return;
|
|
637
657
|
}
|
|
638
658
|
if (matchesKey(data, "escape") || data === "n" || data === "N") {
|
|
@@ -642,7 +662,7 @@ class McpPanel {
|
|
|
642
662
|
if (this.keys.selectConfirm(data)) {
|
|
643
663
|
this.cleanup();
|
|
644
664
|
if (this.discardSelected === 0) {
|
|
645
|
-
this.done({ cancelled: true, changes: new Map() });
|
|
665
|
+
this.done({ cancelled: true, changes: new Map(), disabledChanges: new Map() });
|
|
646
666
|
} else {
|
|
647
667
|
this.done(this.buildResult());
|
|
648
668
|
}
|
|
@@ -650,7 +670,7 @@ class McpPanel {
|
|
|
650
670
|
}
|
|
651
671
|
if (data === "y" || data === "Y") {
|
|
652
672
|
this.cleanup();
|
|
653
|
-
this.done({ cancelled: true, changes: new Map() });
|
|
673
|
+
this.done({ cancelled: true, changes: new Map(), disabledChanges: new Map() });
|
|
654
674
|
return;
|
|
655
675
|
}
|
|
656
676
|
if (matchesKey(data, "left") || matchesKey(data, "right") || matchesKey(data, "tab")) {
|
|
@@ -883,6 +903,7 @@ class McpPanel {
|
|
|
883
903
|
italic("⏎") + " expand/auth",
|
|
884
904
|
italic("ctrl+a") + " auth",
|
|
885
905
|
italic("ctrl+r") + " reconnect",
|
|
906
|
+
italic("ctrl+d") + " disable/enable",
|
|
886
907
|
...(this.selectedServerHasFailureMessage() ? [italic("ctrl+y") + " copy error"] : []),
|
|
887
908
|
italic("?") + " desc search",
|
|
888
909
|
...(saveLabel ? [italic(saveLabel) + " save"] : []),
|
|
@@ -995,7 +1016,7 @@ class McpPanel {
|
|
|
995
1016
|
private renderConnectionStatus(server: ServerState): string {
|
|
996
1017
|
const t = this.t;
|
|
997
1018
|
if (this.authInFlight === server.name) return ` ${fg(t.needsAuth, "authenticating")}`;
|
|
998
|
-
if (server.
|
|
1019
|
+
if (server.disabled) return ` ${fg(t.description, "disabled")}`;
|
|
999
1020
|
if (server.connectionStatus === "needs-auth") return ` ${fg(t.needsAuth, "needs auth")}`;
|
|
1000
1021
|
if (server.connectionStatus === "connecting") return ` ${fg(t.needsAuth, "connecting")}`;
|
|
1001
1022
|
if (server.connectionStatus === "failed") return ` ${fg(t.cancel, "failed")}`;
|
|
@@ -2,7 +2,7 @@ import { matchesKey, truncateToWidth, visibleWidth } from "@earendil-works/pi-tu
|
|
|
2
2
|
import { createPanelKeys, type PanelKeybindings, type PanelKeys } from "./panel-keys.ts";
|
|
3
3
|
import type { ImportKind } from "./types.ts";
|
|
4
4
|
import { getConfigDirName } from "./agent-dir.ts";
|
|
5
|
-
import { KNOWN_SERVER_PRESETS, type ConfigWritePreview, type KnownServerPreset, type McpDiscoverySummary } from "./config.ts";
|
|
5
|
+
import { KNOWN_SERVER_PRESETS, type ConfigWritePreview, type KnownServerPreset, type McpDiscoverySummary, type SharedConfigTarget } from "./config.ts";
|
|
6
6
|
import type { McpOnboardingState } from "./onboarding-state.ts";
|
|
7
7
|
|
|
8
8
|
interface SetupTheme {
|
|
@@ -54,13 +54,13 @@ function wrapText(text: string, width: number): string[] {
|
|
|
54
54
|
|
|
55
55
|
export interface SetupPanelCallbacks {
|
|
56
56
|
previewImports: (imports: ImportKind[]) => ConfigWritePreview;
|
|
57
|
-
|
|
58
|
-
previewRepoPrompt: () => ConfigWritePreview | null;
|
|
59
|
-
previewKnownServer: (preset: KnownServerPreset) => ConfigWritePreview;
|
|
57
|
+
previewStarterConfig: (target: SharedConfigTarget) => ConfigWritePreview;
|
|
58
|
+
previewRepoPrompt: (target: SharedConfigTarget) => ConfigWritePreview | null;
|
|
59
|
+
previewKnownServer: (preset: KnownServerPreset, target: SharedConfigTarget) => ConfigWritePreview;
|
|
60
60
|
adoptImports: (imports: ImportKind[]) => Promise<{ added: ImportKind[]; path: string }>;
|
|
61
|
-
|
|
62
|
-
addRepoPrompt: () => Promise<{ path: string; serverName: string }>;
|
|
63
|
-
addKnownServer: (preset: KnownServerPreset) => Promise<{ path: string; serverName: string }>;
|
|
61
|
+
scaffoldConfig: (target: SharedConfigTarget) => Promise<{ path: string }>;
|
|
62
|
+
addRepoPrompt: (target: SharedConfigTarget) => Promise<{ path: string; serverName: string }>;
|
|
63
|
+
addKnownServer: (preset: KnownServerPreset, target: SharedConfigTarget) => Promise<{ path: string; serverName: string }>;
|
|
64
64
|
openPath: (path: string) => Promise<void>;
|
|
65
65
|
markSetupCompleted: () => void;
|
|
66
66
|
}
|
|
@@ -75,13 +75,14 @@ type Screen = "empty" | "setup" | "imports" | "paths";
|
|
|
75
75
|
|
|
76
76
|
type ActionId =
|
|
77
77
|
| "run-setup"
|
|
78
|
+
| "select-shared-target"
|
|
78
79
|
| "adopt-imports"
|
|
79
80
|
| "view-example"
|
|
80
81
|
| "show-precedence"
|
|
81
82
|
| "open-paths"
|
|
82
83
|
| "add-repoprompt"
|
|
83
84
|
| "add-known-server"
|
|
84
|
-
| "scaffold-
|
|
85
|
+
| "scaffold-shared-config"
|
|
85
86
|
| "close";
|
|
86
87
|
|
|
87
88
|
interface Action {
|
|
@@ -89,6 +90,7 @@ interface Action {
|
|
|
89
90
|
label: string;
|
|
90
91
|
description: string;
|
|
91
92
|
preset?: KnownServerPreset;
|
|
93
|
+
target?: SharedConfigTarget;
|
|
92
94
|
}
|
|
93
95
|
|
|
94
96
|
export class McpSetupPanel {
|
|
@@ -96,6 +98,7 @@ export class McpSetupPanel {
|
|
|
96
98
|
private actionCursor = 0;
|
|
97
99
|
private importCursor = 0;
|
|
98
100
|
private pathCursor = 0;
|
|
101
|
+
private sharedConfigTarget: SharedConfigTarget = "project";
|
|
99
102
|
private selectedImports = new Set<ImportKind>();
|
|
100
103
|
private busy = false;
|
|
101
104
|
private notice: { text: string; tone: "success" | "warning" | "muted" } | null = null;
|
|
@@ -144,9 +147,13 @@ export class McpSetupPanel {
|
|
|
144
147
|
if (this.discovery.imports.length > 0) {
|
|
145
148
|
actions.push({ id: "adopt-imports", label: "Adopt detected compatibility imports", description: `Choose which host-specific MCP configs Pi should import into its own override file. ${this.discovery.imports.length} source${this.discovery.imports.length === 1 ? "" : "s"} found.` });
|
|
146
149
|
}
|
|
147
|
-
actions.push(
|
|
148
|
-
|
|
149
|
-
|
|
150
|
+
actions.push(
|
|
151
|
+
{ id: "select-shared-target", label: `${this.sharedConfigTarget === "project" ? "●" : "○"} Add to this project (.mcp.json)`, description: "Write new shared MCP servers to the project/team config.", target: "project" },
|
|
152
|
+
{ id: "select-shared-target", label: `${this.sharedConfigTarget === "global" ? "●" : "○"} Add globally (~/.config/mcp/mcp.json)`, description: "Write new shared MCP servers to your all-projects config.", target: "global" },
|
|
153
|
+
);
|
|
154
|
+
actions.push({ id: "view-example", label: "View example shared config", description: "Preview a working shared MCP config you can paste or adapt." });
|
|
155
|
+
if (!this.selectedSharedConfigExists()) {
|
|
156
|
+
actions.push({ id: "scaffold-shared-config", label: `Scaffold ${this.sharedTargetLabel()}`, description: "Write a minimal config at the selected normal MCP setup path, then reload Pi." });
|
|
150
157
|
}
|
|
151
158
|
actions.push({ id: "show-precedence", label: "Explain config precedence", description: "Show the read order and where Pi writes compatibility settings." });
|
|
152
159
|
if (this.getDetectedPaths().length > 0) {
|
|
@@ -156,7 +163,7 @@ export class McpSetupPanel {
|
|
|
156
163
|
actions.push({ id: "add-known-server", label: preset.name, description: preset.summary, preset });
|
|
157
164
|
}
|
|
158
165
|
if (!this.discovery.repoPrompt.configured && this.discovery.repoPrompt.executablePath && this.discovery.repoPrompt.targetPath && this.discovery.repoPrompt.entry && this.discovery.repoPrompt.serverName) {
|
|
159
|
-
actions.push({ id: "add-repoprompt", label: "Add RepoPrompt to shared
|
|
166
|
+
actions.push({ id: "add-repoprompt", label: "Add RepoPrompt to selected shared config", description: "Write a standard MCP entry for RepoPrompt to the selected normal setup path, then reload MCP in-session." });
|
|
160
167
|
}
|
|
161
168
|
actions.push({ id: "close", label: "Close", description: "Exit the onboarding flow." });
|
|
162
169
|
return actions;
|
|
@@ -170,6 +177,15 @@ export class McpSetupPanel {
|
|
|
170
177
|
return [...new Set(paths)];
|
|
171
178
|
}
|
|
172
179
|
|
|
180
|
+
private sharedTargetLabel(): string {
|
|
181
|
+
return this.sharedConfigTarget === "project" ? "project .mcp.json" : "global ~/.config/mcp/mcp.json";
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
private selectedSharedConfigExists(): boolean {
|
|
185
|
+
const sourceId = this.sharedConfigTarget === "project" ? "shared-project" : "shared-global";
|
|
186
|
+
return this.discovery.sources.some((source) => source.id === sourceId && source.exists);
|
|
187
|
+
}
|
|
188
|
+
|
|
173
189
|
private getSelectedAction(): Action | undefined {
|
|
174
190
|
const actions = this.getActions();
|
|
175
191
|
return actions[this.actionCursor];
|
|
@@ -293,9 +309,15 @@ export class McpSetupPanel {
|
|
|
293
309
|
this.tui.requestRender();
|
|
294
310
|
return;
|
|
295
311
|
}
|
|
296
|
-
if (action.id === "
|
|
312
|
+
if (action.id === "select-shared-target" && action.target) {
|
|
313
|
+
this.sharedConfigTarget = action.target;
|
|
314
|
+
this.notice = { text: `New shared servers will be written to ${this.sharedTargetLabel()}.`, tone: "muted" };
|
|
315
|
+
this.tui.requestRender();
|
|
316
|
+
return;
|
|
317
|
+
}
|
|
318
|
+
if (action.id === "scaffold-shared-config") {
|
|
297
319
|
await this.runBusy(async () => {
|
|
298
|
-
const result = await this.callbacks.
|
|
320
|
+
const result = await this.callbacks.scaffoldConfig(this.sharedConfigTarget);
|
|
299
321
|
this.callbacks.markSetupCompleted();
|
|
300
322
|
this.notice = { text: `Wrote starter config to ${result.path}. Pi will reload after this panel closes.`, tone: "success" };
|
|
301
323
|
});
|
|
@@ -303,7 +325,7 @@ export class McpSetupPanel {
|
|
|
303
325
|
}
|
|
304
326
|
if (action.id === "add-repoprompt") {
|
|
305
327
|
await this.runBusy(async () => {
|
|
306
|
-
const result = await this.callbacks.addRepoPrompt();
|
|
328
|
+
const result = await this.callbacks.addRepoPrompt(this.sharedConfigTarget);
|
|
307
329
|
this.callbacks.markSetupCompleted();
|
|
308
330
|
this.notice = { text: `Added ${result.serverName} to ${result.path}. Pi will reload after this panel closes.`, tone: "success" };
|
|
309
331
|
});
|
|
@@ -312,7 +334,7 @@ export class McpSetupPanel {
|
|
|
312
334
|
if (action.id === "add-known-server" && action.preset) {
|
|
313
335
|
const preset = action.preset;
|
|
314
336
|
await this.runBusy(async () => {
|
|
315
|
-
const result = await this.callbacks.addKnownServer(preset);
|
|
337
|
+
const result = await this.callbacks.addKnownServer(preset, this.sharedConfigTarget);
|
|
316
338
|
this.callbacks.markSetupCompleted();
|
|
317
339
|
this.notice = { text: `Added ${result.serverName} to ${result.path}. Pi will reload after this panel closes.`, tone: "success" };
|
|
318
340
|
});
|
|
@@ -416,8 +438,11 @@ export class McpSetupPanel {
|
|
|
416
438
|
for (let index = start; index < end; index++) {
|
|
417
439
|
const action = actions[index];
|
|
418
440
|
if (!action) continue;
|
|
441
|
+
if (action.id === "select-shared-target" && (index === start || actions[index - 1]?.id !== "select-shared-target")) {
|
|
442
|
+
lines.push(this.padLine(fg(this.t.title, "Choose where new shared servers go"), innerW));
|
|
443
|
+
}
|
|
419
444
|
if (action.id === "add-known-server" && (index === start || actions[index - 1]?.id !== "add-known-server")) {
|
|
420
|
-
lines.push(this.padLine(fg(this.t.title,
|
|
445
|
+
lines.push(this.padLine(fg(this.t.title, `Add a known server to ${this.sharedTargetLabel()}`), innerW));
|
|
421
446
|
}
|
|
422
447
|
const selected = index === this.actionCursor;
|
|
423
448
|
const cursor = selected ? fg(this.t.selected, "›") : " ";
|
|
@@ -495,12 +520,12 @@ export class McpSetupPanel {
|
|
|
495
520
|
? ` ${this.discovery.conflicts.length} same-name conflict${this.discovery.conflicts.length === 1 ? "" : "s"} reported.`
|
|
496
521
|
: "";
|
|
497
522
|
if (!this.discovery.hasAnyConfig) {
|
|
498
|
-
return `
|
|
523
|
+
return `Add shared servers to .mcp.json for this project/team or ~/.config/mcp/mcp.json for all projects. Adopt host imports or quick-add RepoPrompt from this screen.${hostNote}${conflictNote}`;
|
|
499
524
|
}
|
|
500
525
|
if (this.discovery.totalServerCount === 0 && this.discovery.imports.length > 0) {
|
|
501
526
|
return `Detected ${this.discovery.imports.length} compatibility import source${this.discovery.imports.length === 1 ? "" : "s"}. Adopt them into Pi or inspect the underlying files.${hostNote}${conflictNote}`;
|
|
502
527
|
}
|
|
503
|
-
return `
|
|
528
|
+
return `Use .mcp.json for project/team servers or ~/.config/mcp/mcp.json for all projects. Pi-owned files are for compatibility imports and adapter-specific overrides, not another normal setup path.${hostNote}${conflictNote}`;
|
|
504
529
|
}
|
|
505
530
|
|
|
506
531
|
private visibleActionRange(total: number): { start: number; end: number } {
|
|
@@ -534,6 +559,12 @@ export class McpSetupPanel {
|
|
|
534
559
|
],
|
|
535
560
|
previewW,
|
|
536
561
|
);
|
|
562
|
+
case "select-shared-target":
|
|
563
|
+
return this.formatPreview([
|
|
564
|
+
action.target === "project" ? "Project target: .mcp.json" : "Global target: ~/.config/mcp/mcp.json",
|
|
565
|
+
"Known server presets and starter configs will be written to the selected normal MCP setup path.",
|
|
566
|
+
"Pi-owned mcp.json files remain compatibility and adapter-only override state.",
|
|
567
|
+
], previewW);
|
|
537
568
|
case "view-example":
|
|
538
569
|
return this.formatPreview([
|
|
539
570
|
"Example shared `.mcp.json`:",
|
|
@@ -546,10 +577,17 @@ export class McpSetupPanel {
|
|
|
546
577
|
" }",
|
|
547
578
|
"}",
|
|
548
579
|
"",
|
|
549
|
-
"Use Scaffold
|
|
580
|
+
"Use Scaffold selected config when you want a safe empty shell instead of a live example server.",
|
|
550
581
|
], previewW);
|
|
551
582
|
case "show-precedence":
|
|
552
583
|
return this.formatPreview([
|
|
584
|
+
"Recommended shared config:",
|
|
585
|
+
" project/team: .mcp.json",
|
|
586
|
+
" all projects: ~/.config/mcp/mcp.json",
|
|
587
|
+
"",
|
|
588
|
+
"Advanced compatibility and Pi-owned layers:",
|
|
589
|
+
" host imports, .agents files, package MCP manifests, and Pi overrides",
|
|
590
|
+
"",
|
|
553
591
|
"Read order (later entries win):",
|
|
554
592
|
"0. detected host configs (opt-in lowest-precedence fallback)",
|
|
555
593
|
"1. ~/.config/mcp/mcp.json",
|
|
@@ -570,7 +608,7 @@ export class McpSetupPanel {
|
|
|
570
608
|
: ["No config paths were detected."], previewW);
|
|
571
609
|
case "add-repoprompt": {
|
|
572
610
|
const repoPrompt = this.discovery.repoPrompt;
|
|
573
|
-
const preview = this.callbacks.previewRepoPrompt();
|
|
611
|
+
const preview = this.callbacks.previewRepoPrompt(this.sharedConfigTarget);
|
|
574
612
|
if (!preview) {
|
|
575
613
|
return this.formatPreview(["RepoPrompt is not available to add from this setup screen."], previewW);
|
|
576
614
|
}
|
|
@@ -579,7 +617,7 @@ export class McpSetupPanel {
|
|
|
579
617
|
preview,
|
|
580
618
|
[
|
|
581
619
|
`Executable: ${repoPrompt.executablePath ?? "not found"}`,
|
|
582
|
-
`Target: ${
|
|
620
|
+
`Target: ${this.sharedTargetLabel()}`,
|
|
583
621
|
`Server name: ${repoPrompt.serverName ?? "repoprompt"}`,
|
|
584
622
|
],
|
|
585
623
|
previewW,
|
|
@@ -590,17 +628,17 @@ export class McpSetupPanel {
|
|
|
590
628
|
if (!preset) return this.formatPreview(["Known server preset is unavailable."], previewW);
|
|
591
629
|
return this.formatWritePreview(
|
|
592
630
|
`${preset.name} write preview`,
|
|
593
|
-
this.callbacks.previewKnownServer(preset),
|
|
594
|
-
[preset.summary],
|
|
631
|
+
this.callbacks.previewKnownServer(preset, this.sharedConfigTarget),
|
|
632
|
+
[preset.summary, `Target: ${this.sharedTargetLabel()}`],
|
|
595
633
|
previewW,
|
|
596
634
|
);
|
|
597
635
|
}
|
|
598
|
-
case "scaffold-
|
|
636
|
+
case "scaffold-shared-config":
|
|
599
637
|
return this.formatWritePreview(
|
|
600
|
-
|
|
601
|
-
this.callbacks.
|
|
638
|
+
`${this.sharedTargetLabel()} starter write preview`,
|
|
639
|
+
this.callbacks.previewStarterConfig(this.sharedConfigTarget),
|
|
602
640
|
[
|
|
603
|
-
"This writes a minimal
|
|
641
|
+
"This writes a minimal config at the selected normal MCP setup path.",
|
|
604
642
|
"It intentionally avoids adding a fake placeholder server that would fail on first reload.",
|
|
605
643
|
],
|
|
606
644
|
previewW,
|
|
@@ -48,7 +48,9 @@ export function createMcpStatusSnapshot(state: McpExtensionState): McpStatusSnap
|
|
|
48
48
|
servers.push({
|
|
49
49
|
name,
|
|
50
50
|
status,
|
|
51
|
+
listenState: connection?.status === "connected" ? connection.listenState : "disconnected",
|
|
51
52
|
toolCount,
|
|
53
|
+
...(connection?.status === "connected" && connection.listenCatalogStale ? { catalogStale: true } : {}),
|
|
52
54
|
...(resourceCount !== undefined ? { resourceCount } : {}),
|
|
53
55
|
...(status === "failed" && failedAgoSeconds !== null ? { failedAgoSeconds } : {}),
|
|
54
56
|
disabled,
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pi-mcp-adapter",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.32.1",
|
|
4
4
|
"description": "MCP (Model Context Protocol) adapter extension for Pi coding agent",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"types": "./index.ts",
|
|
@@ -125,6 +125,7 @@
|
|
|
125
125
|
"mcp-status.ts",
|
|
126
126
|
"metadata-cache.ts",
|
|
127
127
|
"host-html-template.ts",
|
|
128
|
+
"sandbox-proxy-template.ts",
|
|
128
129
|
"ui-resource-handler.ts",
|
|
129
130
|
"consent-manager.ts",
|
|
130
131
|
"ui-server.ts",
|