pi-ui-extension 1.1.5 → 1.1.7
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
CHANGED
|
@@ -11,7 +11,7 @@ The package can be installed from npm or Git, and uses GitHub Actions for type c
|
|
|
11
11
|
|
|
12
12
|
## Preview
|
|
13
13
|
|
|
14
|
-
<img width="1377" height="671" alt="pi-ui-extension preview" src="https://github.com/
|
|
14
|
+
<img width="1377" height="671" alt="pi-ui-extension preview" src="https://github.com/1993yihuan/pi-ui-extension/releases/download/v1.1.6/preview.png" />
|
|
15
15
|
|
|
16
16
|
## Features
|
|
17
17
|
|
package/README.zh-CN.md
CHANGED
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
|
|
12
12
|
## 界面预览
|
|
13
13
|
|
|
14
|
-
<img width="1377" height="671" alt="pi-ui-extension 界面预览" src="https://github.com/
|
|
14
|
+
<img width="1377" height="671" alt="pi-ui-extension 界面预览" src="https://github.com/1993yihuan/pi-ui-extension/releases/download/v1.1.6/preview.png" />
|
|
15
15
|
|
|
16
16
|
## 功能概览
|
|
17
17
|
|
package/extensions/index.ts
CHANGED
|
@@ -13,7 +13,7 @@ import { installContextEstimates } from "./ctx/data.ts";
|
|
|
13
13
|
import { createActivityTracker, type ActivityTracker } from "./ctx/activity.ts";
|
|
14
14
|
import { installFooter, type FooterController } from "./footer/index.ts";
|
|
15
15
|
import { isFabricFullCodeMode } from "./fabric-detection.ts";
|
|
16
|
-
import { mcpResourceEntriesFromStatus } from "./mcp/resources.ts";
|
|
16
|
+
import { mcpResourceEntriesFromConfig, mcpResourceEntriesFromStatus, mergeMcpResourceEntries } from "./mcp/resources.ts";
|
|
17
17
|
import {
|
|
18
18
|
createDefaultSidebarPanels,
|
|
19
19
|
SESSION_PANEL_ID,
|
|
@@ -185,6 +185,9 @@ async function clearOtherSessions(
|
|
|
185
185
|
export default async function piUiExtension(pi: ExtensionAPI): Promise<void> {
|
|
186
186
|
const toolView = await installToolViewController(pi);
|
|
187
187
|
let mcpResources: readonly ResourceEntry[] = [];
|
|
188
|
+
let liveMcpResources: readonly ResourceEntry[] = [];
|
|
189
|
+
const currentMcpResources = (cwd: string, trusted: boolean): readonly ResourceEntry[] =>
|
|
190
|
+
mergeMcpResourceEntries(mcpResourceEntriesFromConfig(getAgentDir(), cwd, trusted), liveMcpResources);
|
|
188
191
|
let toolViewMode: ToolViewMode | undefined;
|
|
189
192
|
let footer: FooterController | undefined;
|
|
190
193
|
let sidebar: SidebarController | undefined;
|
|
@@ -219,7 +222,8 @@ export default async function piUiExtension(pi: ExtensionAPI): Promise<void> {
|
|
|
219
222
|
let switchRequested = false;
|
|
220
223
|
const getMcpServerNames = () => mcpResources.map((server) => server.name);
|
|
221
224
|
const unsubscribeMcpStatus = pi.events.on("pi-mcp-adapter/status/v1", (value) => {
|
|
222
|
-
|
|
225
|
+
liveMcpResources = mcpResourceEntriesFromStatus(value);
|
|
226
|
+
mcpResources = mergeMcpResourceEntries(mcpResources, liveMcpResources);
|
|
223
227
|
const branch = getCurrentBranch?.();
|
|
224
228
|
if (branch) toolUsage?.resetSession(branch, getMcpServerNames(), { preserveExchange: true });
|
|
225
229
|
sidebar?.refresh();
|
|
@@ -452,6 +456,7 @@ export default async function piUiExtension(pi: ExtensionAPI): Promise<void> {
|
|
|
452
456
|
// Pi resets extension UI before starting/reloading a session. Do not call
|
|
453
457
|
// stale controllers from the previous runtime after that reset.
|
|
454
458
|
clearControllers();
|
|
459
|
+
mcpResources = currentMcpResources(ctx.cwd, ctx.isProjectTrusted());
|
|
455
460
|
toolViewMode = toolView.getMode();
|
|
456
461
|
getCurrentBranch = () => ctx.sessionManager.getBranch();
|
|
457
462
|
const branch = ctx.sessionManager.getBranch();
|
|
@@ -529,7 +534,7 @@ export default async function piUiExtension(pi: ExtensionAPI): Promise<void> {
|
|
|
529
534
|
ctx.ui.notify("Wait for the current run to finish before opening resources", "warning");
|
|
530
535
|
return;
|
|
531
536
|
}
|
|
532
|
-
void loadResourceEntries(pi, ctx.cwd, mcpResources, ctx.isProjectTrusted(), button)
|
|
537
|
+
void loadResourceEntries(pi, ctx.cwd, button === "mcp" ? currentMcpResources(ctx.cwd, ctx.isProjectTrusted()) : mcpResources, ctx.isProjectTrusted(), button)
|
|
533
538
|
.then((rows) => {
|
|
534
539
|
if (!rows || rows.length === 0) {
|
|
535
540
|
ctx.ui.notify("No resources of this kind loaded", "info");
|
|
@@ -25,7 +25,7 @@ test("entry wires early/late MCP status into restored and live usage without dou
|
|
|
25
25
|
events: { on(name: string, fn: any) { bus.set(name, fn); return () => bus.delete(name); } },
|
|
26
26
|
};
|
|
27
27
|
const ctx: any = {
|
|
28
|
-
mode: "tui", cwd: directory, getSystemPrompt: () => "", getContextUsage: () => undefined,
|
|
28
|
+
mode: "tui", cwd: directory, isProjectTrusted: () => true, getSystemPrompt: () => "", getContextUsage: () => undefined,
|
|
29
29
|
sessionManager: { getBranch: () => branch, buildContextEntries: () => [], getSessionId: () => "test", getSessionDir: () => directory },
|
|
30
30
|
ui: { setStatus() {}, setFooter(factory: any) { footer?.dispose(); footer = factory?.(tui, theme, { onBranchChange: () => () => {}, getGitBranch: () => null }); } },
|
|
31
31
|
};
|
|
@@ -1,6 +1,29 @@
|
|
|
1
1
|
import assert from "node:assert/strict";
|
|
2
2
|
import test from "node:test";
|
|
3
|
-
import { mcpResourceEntriesFromStatus } from "./resources.ts";
|
|
3
|
+
import { mcpResourceEntriesFromConfig, mcpResourceEntriesFromStatus, mergeMcpResourceEntries } from "./resources.ts";
|
|
4
|
+
import { mkdtempSync, mkdirSync, writeFileSync, rmSync } from "node:fs";
|
|
5
|
+
import { tmpdir } from "node:os";
|
|
6
|
+
import { join } from "node:path";
|
|
7
|
+
|
|
8
|
+
test("discovers global and trusted project MCP configs without exposing secrets", () => {
|
|
9
|
+
const root = mkdtempSync(join(tmpdir(), "mcp-resource-"));
|
|
10
|
+
const agent = join(root, "agent"), cwd = join(root, "project");
|
|
11
|
+
mkdirSync(agent); mkdirSync(join(cwd, ".pi"), { recursive: true });
|
|
12
|
+
try {
|
|
13
|
+
writeFileSync(join(agent, "mcp.json"), JSON.stringify({ mcpServers: {
|
|
14
|
+
context7: { url: "https://secret.invalid", headers: { Authorization: "secret" } },
|
|
15
|
+
disabled: { disabled: true },
|
|
16
|
+
} }));
|
|
17
|
+
writeFileSync(join(cwd, ".pi", "mcp.json"), JSON.stringify({ mcpServers: { "chrome-devtools": { command: "secret" } } }));
|
|
18
|
+
const untrusted = mcpResourceEntriesFromConfig(agent, cwd, false);
|
|
19
|
+
assert.deepEqual(untrusted.map(x => x.name), ["context7"]);
|
|
20
|
+
const trusted = mcpResourceEntriesFromConfig(agent, cwd, true);
|
|
21
|
+
assert.deepEqual(trusted.map(x => x.name), ["context7", "chrome-devtools"]);
|
|
22
|
+
assert.doesNotMatch(JSON.stringify(trusted), /secret/);
|
|
23
|
+
assert.deepEqual(mergeMcpResourceEntries(trusted, mcpResourceEntriesFromStatus({ version: 1, servers: [{ name: "context7" }] }))
|
|
24
|
+
.map(x => x.description), ["MCP server loaded in the current Pi session", "Configured MCP server (connection not verified)"]);
|
|
25
|
+
} finally { rmSync(root, { recursive: true, force: true }); }
|
|
26
|
+
});
|
|
4
27
|
|
|
5
28
|
test("builds MCP resource rows from the adapter status snapshot", () => {
|
|
6
29
|
assert.deepEqual(
|
|
@@ -1,8 +1,40 @@
|
|
|
1
|
+
import { readFileSync } from "node:fs";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
|
|
1
4
|
export type McpResourceEntry = {
|
|
2
5
|
readonly name: string;
|
|
3
6
|
readonly description: string;
|
|
4
7
|
};
|
|
5
8
|
|
|
9
|
+
export function mcpResourceEntriesFromConfig(agentDir: string, cwd: string, trusted: boolean): readonly McpResourceEntry[] {
|
|
10
|
+
const paths = [join(agentDir, "mcp.json")];
|
|
11
|
+
if (trusted) paths.push(join(cwd, ".pi", "mcp.json"));
|
|
12
|
+
const rows = new Map<string, McpResourceEntry>();
|
|
13
|
+
for (const path of paths) {
|
|
14
|
+
try {
|
|
15
|
+
const config: unknown = JSON.parse(readFileSync(path, "utf8"));
|
|
16
|
+
if (!config || typeof config !== "object" || Array.isArray(config)) continue;
|
|
17
|
+
const servers = (config as { mcpServers?: unknown }).mcpServers;
|
|
18
|
+
if (!servers || typeof servers !== "object" || Array.isArray(servers)) continue;
|
|
19
|
+
for (const [name, value] of Object.entries(servers)) {
|
|
20
|
+
if (!name.trim() || !value || typeof value !== "object" || Array.isArray(value)) continue;
|
|
21
|
+
if ((value as { disabled?: unknown }).disabled === true) {
|
|
22
|
+
rows.delete(name);
|
|
23
|
+
continue;
|
|
24
|
+
}
|
|
25
|
+
rows.set(name, { name, description: "Configured MCP server (connection not verified)" });
|
|
26
|
+
}
|
|
27
|
+
} catch { /* Missing or invalid configuration is not fatal. */ }
|
|
28
|
+
}
|
|
29
|
+
return [...rows.values()];
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export function mergeMcpResourceEntries(configured: readonly McpResourceEntry[], live: readonly McpResourceEntry[]): readonly McpResourceEntry[] {
|
|
33
|
+
const rows = new Map(configured.map((entry) => [entry.name, entry]));
|
|
34
|
+
for (const entry of live) rows.set(entry.name, entry);
|
|
35
|
+
return [...rows.values()];
|
|
36
|
+
}
|
|
37
|
+
|
|
6
38
|
type McpStatusServer = {
|
|
7
39
|
readonly name?: unknown;
|
|
8
40
|
readonly disabled?: unknown;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pi-ui-extension",
|
|
3
|
-
"version": "1.1.
|
|
3
|
+
"version": "1.1.7",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Pi terminal UI extension with session management, context inspection, Tool/MCP usage tracking, sidebar panels, and a compact footer",
|
|
6
6
|
"keywords": [
|
|
@@ -17,7 +17,7 @@
|
|
|
17
17
|
"extensions": [
|
|
18
18
|
"./extensions/index.ts"
|
|
19
19
|
],
|
|
20
|
-
"image": "https://github.com/
|
|
20
|
+
"image": "https://github.com/1993yihuan/pi-ui-extension/releases/download/v1.1.6/preview.png"
|
|
21
21
|
},
|
|
22
22
|
"scripts": {
|
|
23
23
|
"typecheck": "tsc -p tsconfig.check.json",
|