pi-ui-extension 1.1.4 → 1.1.6
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
|
@@ -6,6 +6,9 @@
|
|
|
6
6
|
|
|
7
7
|
The package can be installed from npm or Git, and uses GitHub Actions for type checking, automated tests, and npm Trusted Publishing via OIDC.
|
|
8
8
|
|
|
9
|
+
> [!IMPORTANT]
|
|
10
|
+
> `pi-ui-extension` is designed for **Pi fullscreen mode**. Please run Pi in fullscreen mode when using this extension. In non-fullscreen mode, sidebar panels, layout sizing, and related UI behavior may be unavailable or displayed incorrectly.
|
|
11
|
+
|
|
9
12
|
## Preview
|
|
10
13
|
|
|
11
14
|
<img width="1377" height="671" alt="pi-ui-extension preview" src="https://github.com/user-attachments/assets/ddea5dc9-8796-433a-abfc-c7ea2adec089" />
|
|
@@ -22,7 +25,7 @@ The extension mounts a right-side sidebar in Pi's fullscreen TUI. It contains th
|
|
|
22
25
|
|
|
23
26
|
The sidebar supports mouse interaction, wheel scrolling, and terminal resize handling. Panels shrink or become scrollable when vertical space is limited.
|
|
24
27
|
|
|
25
|
-
> The sidebar depends on Pi fullscreen
|
|
28
|
+
> The sidebar depends on **Pi fullscreen mode**. Non-fullscreen mode is not a supported display mode for this extension; sidebar panels, layout sizing, and related UI behavior may be unavailable or displayed incorrectly.
|
|
26
29
|
|
|
27
30
|
### 2. Session management
|
|
28
31
|
|
package/README.zh-CN.md
CHANGED
|
@@ -6,6 +6,9 @@
|
|
|
6
6
|
|
|
7
7
|
当前包同时支持通过 npm 或 Git 安装,并使用 GitHub Actions 完成类型检查、自动化测试和 npm Trusted Publishing(OIDC)发布。
|
|
8
8
|
|
|
9
|
+
> [!IMPORTANT]
|
|
10
|
+
> `pi-ui-extension` **需要在 Pi 的 fullscreen 模式下使用**。请在 fullscreen 模式下运行 Pi;非 fullscreen 模式不属于本扩展的支持范围,Sidebar、面板尺寸计算及相关布局可能无法正常显示。
|
|
11
|
+
|
|
9
12
|
## 界面预览
|
|
10
13
|
|
|
11
14
|
<img width="1377" height="671" alt="pi-ui-extension 界面预览" src="https://github.com/user-attachments/assets/ddea5dc9-8796-433a-abfc-c7ea2adec089" />
|
|
@@ -22,7 +25,7 @@
|
|
|
22
25
|
|
|
23
26
|
Sidebar 支持鼠标交互、滚轮滚动和终端尺寸变化。终端高度不足时,各区域会根据可用空间自动收缩或出现滚动区域。
|
|
24
27
|
|
|
25
|
-
> 右侧 Sidebar 依赖 Pi fullscreen
|
|
28
|
+
> 右侧 Sidebar 依赖 **Pi fullscreen 模式**。非 fullscreen 模式不属于本扩展的支持范围,Sidebar、面板尺寸计算及相关布局可能无法正常显示。
|
|
26
29
|
|
|
27
30
|
### 2. Sessions 会话管理
|
|
28
31
|
|
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