@blackbelt-technology/pi-agent-dashboard 0.4.1 → 0.4.2
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/AGENTS.md +79 -32
- package/README.md +7 -3
- package/docs/architecture.md +361 -12
- package/package.json +7 -7
- package/packages/extension/package.json +7 -2
- package/packages/extension/src/__tests__/ask-user-schema-discriminator.test.ts +141 -0
- package/packages/extension/src/__tests__/ask-user-tool.test.ts +51 -7
- package/packages/extension/src/__tests__/multiselect-dashboard-routing.test.ts +203 -0
- package/packages/extension/src/__tests__/multiselect-polyfill.test.ts +92 -0
- package/packages/extension/src/__tests__/no-tui-multiselect-arm-regression.test.ts +81 -0
- package/packages/extension/src/__tests__/openspec-activity-detector.test.ts +37 -0
- package/packages/extension/src/__tests__/ui-decorators.test.ts +309 -0
- package/packages/extension/src/__tests__/ui-modules.test.ts +293 -0
- package/packages/extension/src/ask-user-tool.ts +165 -57
- package/packages/extension/src/bridge.ts +97 -4
- package/packages/extension/src/multiselect-decode.ts +40 -0
- package/packages/extension/src/multiselect-polyfill.ts +38 -8
- package/packages/extension/src/ui-modules.ts +272 -0
- package/packages/server/package.json +9 -3
- package/packages/server/src/__tests__/auto-attach.test.ts +61 -8
- package/packages/server/src/__tests__/browse-endpoint.test.ts +295 -19
- package/packages/server/src/__tests__/cli-bootstrap.test.ts +36 -0
- package/packages/server/src/__tests__/directory-service-refresh-force.test.ts +163 -0
- package/packages/server/src/__tests__/directory-service-specs-mtime.test.ts +315 -0
- package/packages/server/src/__tests__/directory-service-toctou.test.ts +303 -0
- package/packages/server/src/__tests__/directory-service.test.ts +174 -0
- package/packages/server/src/__tests__/installed-package-enricher.test.ts +225 -0
- package/packages/server/src/__tests__/package-manager-wrapper-move.test.ts +414 -0
- package/packages/server/src/__tests__/package-routes.test.ts +136 -3
- package/packages/server/src/__tests__/package-source-helpers.test.ts +101 -0
- package/packages/server/src/__tests__/pending-attach-registry.test.ts +123 -0
- package/packages/server/src/__tests__/pending-resume-intent-registry.test.ts +138 -0
- package/packages/server/src/__tests__/pi-core-checker.test.ts +73 -30
- package/packages/server/src/__tests__/pi-gateway-consume-pending-attach.test.ts +112 -0
- package/packages/server/src/__tests__/post-install-openspec-refresh.test.ts +180 -0
- package/packages/server/src/__tests__/post-install-rescan.test.ts +134 -0
- package/packages/server/src/__tests__/proposal-attach-naming.test.ts +79 -0
- package/packages/server/src/__tests__/session-action-handler-spawn-with-attach.test.ts +108 -0
- package/packages/server/src/__tests__/session-order-manager.test.ts +55 -0
- package/packages/server/src/__tests__/session-order-reboot.test.ts +242 -0
- package/packages/server/src/__tests__/session-scanner.test.ts +44 -0
- package/packages/server/src/__tests__/subscription-handler.test.ts +40 -0
- package/packages/server/src/__tests__/translate-path-source.test.ts +77 -0
- package/packages/server/src/__tests__/ui-decorators-replay.test.ts +209 -0
- package/packages/server/src/__tests__/ui-modules-replay.test.ts +221 -0
- package/packages/server/src/browse.ts +118 -13
- package/packages/server/src/browser-gateway.ts +19 -0
- package/packages/server/src/browser-handlers/__tests__/session-meta-handler.test.ts +183 -0
- package/packages/server/src/browser-handlers/directory-handler.ts +7 -1
- package/packages/server/src/browser-handlers/handler-context.ts +15 -0
- package/packages/server/src/browser-handlers/session-action-handler.ts +29 -3
- package/packages/server/src/browser-handlers/session-meta-handler.ts +46 -12
- package/packages/server/src/browser-handlers/subscription-handler.ts +46 -1
- package/packages/server/src/cli.ts +5 -6
- package/packages/server/src/directory-service.ts +156 -15
- package/packages/server/src/event-wiring.ts +111 -10
- package/packages/server/src/installed-package-enricher.ts +143 -0
- package/packages/server/src/package-manager-wrapper.ts +305 -8
- package/packages/server/src/package-source-helpers.ts +104 -0
- package/packages/server/src/pending-attach-registry.ts +112 -0
- package/packages/server/src/pending-resume-intent-registry.ts +107 -0
- package/packages/server/src/pi-core-checker.ts +9 -14
- package/packages/server/src/pi-gateway.ts +14 -0
- package/packages/server/src/proposal-attach-naming.ts +47 -0
- package/packages/server/src/routes/file-routes.ts +29 -3
- package/packages/server/src/routes/package-routes.ts +72 -3
- package/packages/server/src/routes/plugin-config-routes.ts +129 -0
- package/packages/server/src/routes/system-routes.ts +2 -0
- package/packages/server/src/server.ts +339 -10
- package/packages/server/src/session-api.ts +30 -5
- package/packages/server/src/session-order-manager.ts +22 -0
- package/packages/server/src/session-scanner.ts +10 -1
- package/packages/shared/package.json +9 -2
- package/packages/shared/src/__tests__/browser-protocol-types.test.ts +59 -0
- package/packages/shared/src/__tests__/config-plugins.test.ts +68 -0
- package/packages/shared/src/__tests__/extension-ui-module-shape.test.ts +265 -0
- package/packages/shared/src/__tests__/no-raw-openspec-status-in-skills.test.ts +81 -0
- package/packages/shared/src/__tests__/openspec-design-evidence.test.ts +288 -0
- package/packages/shared/src/__tests__/openspec-effective-status-script.test.ts +174 -0
- package/packages/shared/src/__tests__/openspec-poller-design-override.test.ts +225 -0
- package/packages/shared/src/__tests__/openspec-poller-specs-override.test.ts +284 -0
- package/packages/shared/src/__tests__/openspec-specs-evidence.test.ts +144 -0
- package/packages/shared/src/__tests__/platform/is-appimage-self-hit.test.ts +164 -0
- package/packages/shared/src/__tests__/plugin-bridge-register-extended.test.ts +72 -0
- package/packages/shared/src/__tests__/plugin-bridge-register.test.ts +113 -0
- package/packages/shared/src/__tests__/plugin-config-update-protocol.test.ts +41 -0
- package/packages/shared/src/__tests__/recommended-extensions.test.ts +5 -1
- package/packages/shared/src/__tests__/spawn-session-attach-proposal.test.ts +47 -0
- package/packages/shared/src/__tests__/tool-registry-strategies-appimage.test.ts +118 -0
- package/packages/shared/src/browser-protocol.ts +110 -4
- package/packages/shared/src/config.ts +45 -0
- package/packages/shared/src/dashboard-plugin/index.ts +11 -0
- package/packages/shared/src/dashboard-plugin/manifest-types.ts +58 -0
- package/packages/shared/src/dashboard-plugin/plugin-status.ts +26 -0
- package/packages/shared/src/dashboard-plugin/slot-props.ts +92 -0
- package/packages/shared/src/dashboard-plugin/slot-types.ts +151 -0
- package/packages/shared/src/openspec-activity-detector.ts +18 -22
- package/packages/shared/src/openspec-design-evidence.ts +109 -0
- package/packages/shared/src/openspec-poller.ts +117 -3
- package/packages/shared/src/openspec-specs-evidence.ts +79 -0
- package/packages/shared/src/platform/binary-lookup.ts +96 -1
- package/packages/shared/src/plugin-bridge-register.ts +139 -0
- package/packages/shared/src/protocol.ts +56 -2
- package/packages/shared/src/recommended-extensions.ts +7 -1
- package/packages/shared/src/rest-api.ts +68 -3
- package/packages/shared/src/state-replay.ts +11 -1
- package/packages/shared/src/tool-registry/strategies.ts +17 -3
- package/packages/shared/src/types.ts +160 -0
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
import { describe, it, expect, beforeEach, afterEach } from "vitest";
|
|
2
|
+
import fs from "node:fs";
|
|
3
|
+
import os from "node:os";
|
|
4
|
+
import path from "node:path";
|
|
5
|
+
import {
|
|
6
|
+
registerPluginBridge,
|
|
7
|
+
deregisterPluginBridge,
|
|
8
|
+
listManagedBridges,
|
|
9
|
+
} from "../plugin-bridge-register.js";
|
|
10
|
+
|
|
11
|
+
let tmpDir: string;
|
|
12
|
+
let homedir: string;
|
|
13
|
+
|
|
14
|
+
beforeEach(() => {
|
|
15
|
+
tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "plugin-bridge-test-"));
|
|
16
|
+
homedir = tmpDir;
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
afterEach(() => {
|
|
20
|
+
fs.rmSync(tmpDir, { recursive: true, force: true });
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
function settingsPath() {
|
|
24
|
+
return path.join(homedir, ".pi", "agent", "settings.json");
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function readSettings(): Record<string, unknown> {
|
|
28
|
+
return JSON.parse(fs.readFileSync(settingsPath(), "utf-8"));
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
describe("registerPluginBridge", () => {
|
|
32
|
+
it("writes dashboard-<id> entry under dashboardPluginBridges", () => {
|
|
33
|
+
const result = registerPluginBridge("demo", "/path/to/demo/bridge.js", { homedir });
|
|
34
|
+
expect(result.type).toBe("ok");
|
|
35
|
+
const s = readSettings();
|
|
36
|
+
const managed = s.dashboardPluginBridges as Record<string, string>;
|
|
37
|
+
expect(managed["dashboard-demo"]).toBe("/path/to/demo/bridge.js");
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
it("returns ok when entry already matches (idempotent)", () => {
|
|
41
|
+
registerPluginBridge("demo", "/path/to/bridge.js", { homedir });
|
|
42
|
+
const result = registerPluginBridge("demo", "/path/to/bridge.js", { homedir });
|
|
43
|
+
expect(result.type).toBe("ok");
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
it("returns conflict when entry exists with different path", () => {
|
|
47
|
+
registerPluginBridge("demo", "/old/bridge.js", { homedir });
|
|
48
|
+
const result = registerPluginBridge("demo", "/new/bridge.js", { homedir });
|
|
49
|
+
expect(result.type).toBe("conflict");
|
|
50
|
+
if (result.type === "conflict") {
|
|
51
|
+
expect(result.existingPath).toBe("/old/bridge.js");
|
|
52
|
+
expect(result.newPath).toBe("/new/bridge.js");
|
|
53
|
+
}
|
|
54
|
+
// Should not overwrite
|
|
55
|
+
const s = readSettings();
|
|
56
|
+
const managed = s.dashboardPluginBridges as Record<string, string>;
|
|
57
|
+
expect(managed["dashboard-demo"]).toBe("/old/bridge.js");
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
it("never touches user-owned packages array", () => {
|
|
61
|
+
// Pre-populate settings with user packages
|
|
62
|
+
fs.mkdirSync(path.join(homedir, ".pi", "agent"), { recursive: true });
|
|
63
|
+
fs.writeFileSync(
|
|
64
|
+
settingsPath(),
|
|
65
|
+
JSON.stringify({ packages: ["/user/extension1", "/user/extension2"] }),
|
|
66
|
+
);
|
|
67
|
+
|
|
68
|
+
registerPluginBridge("demo", "/demo/bridge.js", { homedir });
|
|
69
|
+
const s = readSettings();
|
|
70
|
+
const pkgs = s.packages as string[];
|
|
71
|
+
expect(pkgs).toEqual(["/user/extension1", "/user/extension2"]);
|
|
72
|
+
});
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
describe("deregisterPluginBridge", () => {
|
|
76
|
+
it("removes the managed entry", () => {
|
|
77
|
+
registerPluginBridge("demo", "/demo/bridge.js", { homedir });
|
|
78
|
+
deregisterPluginBridge("demo", { homedir });
|
|
79
|
+
const s = readSettings();
|
|
80
|
+
const managed = s.dashboardPluginBridges as Record<string, string>;
|
|
81
|
+
expect(managed["dashboard-demo"]).toBeUndefined();
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
it("is a no-op when entry does not exist", () => {
|
|
85
|
+
// Should not throw
|
|
86
|
+
expect(() => deregisterPluginBridge("nonexistent", { homedir })).not.toThrow();
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
it("does not remove other plugin entries", () => {
|
|
90
|
+
registerPluginBridge("a", "/a/bridge.js", { homedir });
|
|
91
|
+
registerPluginBridge("b", "/b/bridge.js", { homedir });
|
|
92
|
+
deregisterPluginBridge("a", { homedir });
|
|
93
|
+
const managed = listManagedBridges({ homedir });
|
|
94
|
+
expect(managed["dashboard-a"]).toBeUndefined();
|
|
95
|
+
expect(managed["dashboard-b"]).toBe("/b/bridge.js");
|
|
96
|
+
});
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
describe("listManagedBridges", () => {
|
|
100
|
+
it("returns all managed entries", () => {
|
|
101
|
+
registerPluginBridge("a", "/a/bridge.js", { homedir });
|
|
102
|
+
registerPluginBridge("b", "/b/bridge.js", { homedir });
|
|
103
|
+
const managed = listManagedBridges({ homedir });
|
|
104
|
+
expect(Object.keys(managed)).toHaveLength(2);
|
|
105
|
+
expect(managed["dashboard-a"]).toBe("/a/bridge.js");
|
|
106
|
+
expect(managed["dashboard-b"]).toBe("/b/bridge.js");
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
it("returns empty object when no plugins registered", () => {
|
|
110
|
+
const managed = listManagedBridges({ homedir });
|
|
111
|
+
expect(managed).toEqual({});
|
|
112
|
+
});
|
|
113
|
+
});
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Type-level test ensuring plugin_config_update is in ServerToBrowserMessage.
|
|
3
|
+
*
|
|
4
|
+
* Prevents the recurring esbuild-strips-as-any-cases regression where message
|
|
5
|
+
* types not in the union get dead-code eliminated in production builds.
|
|
6
|
+
*/
|
|
7
|
+
import { describe, it, expect } from "vitest";
|
|
8
|
+
import type { ServerToBrowserMessage, PluginConfigUpdateMessage } from "../browser-protocol.js";
|
|
9
|
+
|
|
10
|
+
// Type-level assertion: if PluginConfigUpdateMessage is NOT in the union, this fails to compile.
|
|
11
|
+
type AssertExtends<T, U> = T extends U ? true : never;
|
|
12
|
+
type _PluginConfigUpdateInUnion = AssertExtends<PluginConfigUpdateMessage, ServerToBrowserMessage>;
|
|
13
|
+
|
|
14
|
+
function extractPluginConfigId(msg: ServerToBrowserMessage): string | null {
|
|
15
|
+
switch (msg.type) {
|
|
16
|
+
case "plugin_config_update": return msg.id;
|
|
17
|
+
default: return null;
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
describe("ServerToBrowserMessage includes plugin_config_update", () => {
|
|
22
|
+
it("plugin_config_update is a valid discriminant", () => {
|
|
23
|
+
const msg: PluginConfigUpdateMessage = {
|
|
24
|
+
type: "plugin_config_update",
|
|
25
|
+
id: "demo",
|
|
26
|
+
config: { foo: 1 },
|
|
27
|
+
};
|
|
28
|
+
expect(extractPluginConfigId(msg)).toBe("demo");
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
it("config payload is only this plugin's namespace", () => {
|
|
32
|
+
const msg: PluginConfigUpdateMessage = {
|
|
33
|
+
type: "plugin_config_update",
|
|
34
|
+
id: "openspec",
|
|
35
|
+
config: { pollIntervalSeconds: 30 },
|
|
36
|
+
};
|
|
37
|
+
// The config is the plugin's namespace only — not the full config
|
|
38
|
+
expect((msg.config as any).pollIntervalSeconds).toBe(30);
|
|
39
|
+
expect((msg.config as any).plugins).toBeUndefined();
|
|
40
|
+
});
|
|
41
|
+
});
|
|
@@ -127,8 +127,12 @@ describe("RecommendedExtension type", () => {
|
|
|
127
127
|
|
|
128
128
|
describe("BUNDLED_EXTENSION_IDS manifest", () => {
|
|
129
129
|
it("contains exactly the v0.x initial bundled set", () => {
|
|
130
|
+
// pi-flows temporarily removed: upstream repo lacks SPDX license,
|
|
131
|
+
// blocking the bundle-recommended-extensions.sh license check.
|
|
132
|
+
// Re-add when https://github.com/BlackBeltTechnology/pi-flows has
|
|
133
|
+
// a license declared.
|
|
130
134
|
expect([...BUNDLED_EXTENSION_IDS].sort()).toEqual(
|
|
131
|
-
["pi-anthropic-messages"
|
|
135
|
+
["pi-anthropic-messages"].sort(),
|
|
132
136
|
);
|
|
133
137
|
});
|
|
134
138
|
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Type-level + structural tests for the `attachProposal` field on
|
|
3
|
+
* `SpawnSessionBrowserMessage`. See change:
|
|
4
|
+
* add-folder-task-checker-and-spawn-attach.
|
|
5
|
+
*/
|
|
6
|
+
import { describe, it, expect } from "vitest";
|
|
7
|
+
import type {
|
|
8
|
+
SpawnSessionBrowserMessage,
|
|
9
|
+
BrowserToServerMessage,
|
|
10
|
+
} from "../browser-protocol.js";
|
|
11
|
+
|
|
12
|
+
describe("SpawnSessionBrowserMessage.attachProposal", () => {
|
|
13
|
+
it("is optional — the bare-spawn payload still type-checks", () => {
|
|
14
|
+
// Compile-time: omitting attachProposal is allowed.
|
|
15
|
+
const bare: SpawnSessionBrowserMessage = { type: "spawn_session", cwd: "/x" };
|
|
16
|
+
expect(bare.attachProposal).toBeUndefined();
|
|
17
|
+
const _inUnion: BrowserToServerMessage = bare;
|
|
18
|
+
void _inUnion;
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
it("accepts a string attachProposal when set", () => {
|
|
22
|
+
const withAttach: SpawnSessionBrowserMessage = {
|
|
23
|
+
type: "spawn_session",
|
|
24
|
+
cwd: "/x",
|
|
25
|
+
attachProposal: "add-foo",
|
|
26
|
+
};
|
|
27
|
+
expect(withAttach.attachProposal).toBe("add-foo");
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
it("JSON round-trip preserves the field", () => {
|
|
31
|
+
const sent: SpawnSessionBrowserMessage = {
|
|
32
|
+
type: "spawn_session",
|
|
33
|
+
cwd: "/project/foo",
|
|
34
|
+
attachProposal: "add-auth",
|
|
35
|
+
};
|
|
36
|
+
const parsed = JSON.parse(JSON.stringify(sent)) as SpawnSessionBrowserMessage;
|
|
37
|
+
expect(parsed.type).toBe("spawn_session");
|
|
38
|
+
expect(parsed.cwd).toBe("/project/foo");
|
|
39
|
+
expect(parsed.attachProposal).toBe("add-auth");
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
it("JSON round-trip without the field omits it", () => {
|
|
43
|
+
const sent: SpawnSessionBrowserMessage = { type: "spawn_session", cwd: "/x" };
|
|
44
|
+
const parsed = JSON.parse(JSON.stringify(sent)) as SpawnSessionBrowserMessage;
|
|
45
|
+
expect("attachProposal" in parsed).toBe(false);
|
|
46
|
+
});
|
|
47
|
+
});
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tests for the AppImage self-hit guard inside `whereStrategy`. Per
|
|
3
|
+
* design D2 (change: fix-electron-appimage-cli-self-detection), the
|
|
4
|
+
* guard runs after `whichSync(name)` returns and demotes self-hits to
|
|
5
|
+
* `{ ok: false, reason: "appimage-self-hit: <path>" }` so the
|
|
6
|
+
* registry's diagnostic trail records the rejection.
|
|
7
|
+
*/
|
|
8
|
+
import { describe, it, expect } from "vitest";
|
|
9
|
+
import path from "node:path";
|
|
10
|
+
import os from "node:os";
|
|
11
|
+
|
|
12
|
+
import { whereStrategy } from "../tool-registry/strategies.js";
|
|
13
|
+
import {
|
|
14
|
+
ToolRegistry,
|
|
15
|
+
OverridesStore,
|
|
16
|
+
} from "../tool-registry/index.js";
|
|
17
|
+
|
|
18
|
+
function tmpStore(): OverridesStore {
|
|
19
|
+
return new OverridesStore({
|
|
20
|
+
filePath: path.join(
|
|
21
|
+
os.tmpdir(),
|
|
22
|
+
`where-strategy-appimage-test-${process.pid}-${Math.random().toString(36).slice(2)}.json`,
|
|
23
|
+
),
|
|
24
|
+
warn: () => {},
|
|
25
|
+
});
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
describe("whereStrategy AppImage self-hit guard", () => {
|
|
29
|
+
it("rejects an APPDIR-mount candidate", () => {
|
|
30
|
+
const savedAppDir = process.env.APPDIR;
|
|
31
|
+
const fakeAppDir = "/tmp/.mount_PI-DAS-TEST";
|
|
32
|
+
process.env.APPDIR = fakeAppDir;
|
|
33
|
+
try {
|
|
34
|
+
const strat = whereStrategy("pi-dashboard", {
|
|
35
|
+
which: () => fakeAppDir + "/pi-dashboard",
|
|
36
|
+
});
|
|
37
|
+
const r = strat.run({
|
|
38
|
+
overrides: {},
|
|
39
|
+
platform: "linux",
|
|
40
|
+
});
|
|
41
|
+
expect(r.ok).toBe(false);
|
|
42
|
+
if (!r.ok) {
|
|
43
|
+
expect(r.reason).toContain("appimage-self-hit");
|
|
44
|
+
expect(r.reason).toContain(fakeAppDir + "/pi-dashboard");
|
|
45
|
+
}
|
|
46
|
+
} finally {
|
|
47
|
+
if (savedAppDir === undefined) delete process.env.APPDIR;
|
|
48
|
+
else process.env.APPDIR = savedAppDir;
|
|
49
|
+
}
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
it("rejects a process.execPath self-hit", () => {
|
|
53
|
+
// Use the running process.execPath — realpath-equality matches.
|
|
54
|
+
const exec = process.execPath;
|
|
55
|
+
const strat = whereStrategy("pi-dashboard", {
|
|
56
|
+
which: () => exec,
|
|
57
|
+
});
|
|
58
|
+
const r = strat.run({
|
|
59
|
+
overrides: {},
|
|
60
|
+
platform: process.platform,
|
|
61
|
+
});
|
|
62
|
+
expect(r.ok).toBe(false);
|
|
63
|
+
if (!r.ok) {
|
|
64
|
+
expect(r.reason).toContain("appimage-self-hit");
|
|
65
|
+
}
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
it("returns ok for unrelated paths when no AppImage env vars are set", () => {
|
|
69
|
+
const savedAppDir = process.env.APPDIR;
|
|
70
|
+
const savedAppImage = process.env.APPIMAGE;
|
|
71
|
+
delete process.env.APPDIR;
|
|
72
|
+
delete process.env.APPIMAGE;
|
|
73
|
+
try {
|
|
74
|
+
const candidate = "/usr/local/bin/git";
|
|
75
|
+
const strat = whereStrategy("git", { which: () => candidate });
|
|
76
|
+
const r = strat.run({ overrides: {}, platform: "linux" });
|
|
77
|
+
expect(r.ok).toBe(true);
|
|
78
|
+
if (r.ok) expect(r.path).toBe(candidate);
|
|
79
|
+
} finally {
|
|
80
|
+
if (savedAppDir !== undefined) process.env.APPDIR = savedAppDir;
|
|
81
|
+
if (savedAppImage !== undefined) process.env.APPIMAGE = savedAppImage;
|
|
82
|
+
}
|
|
83
|
+
});
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
describe("ToolRegistry diagnostic trail records appimage-self-hit", () => {
|
|
87
|
+
it("Resolution.tried includes a 'where' entry whose reason contains 'appimage-self-hit'", () => {
|
|
88
|
+
const savedAppDir = process.env.APPDIR;
|
|
89
|
+
const fakeAppDir = "/tmp/.mount_PI-REG-TEST";
|
|
90
|
+
process.env.APPDIR = fakeAppDir;
|
|
91
|
+
try {
|
|
92
|
+
const registry = new ToolRegistry({
|
|
93
|
+
overrides: tmpStore(),
|
|
94
|
+
platform: "linux",
|
|
95
|
+
});
|
|
96
|
+
registry.register({
|
|
97
|
+
name: "synthetic-tool",
|
|
98
|
+
kind: "binary",
|
|
99
|
+
strategies: [
|
|
100
|
+
// Final strategy in the chain is `where`, fed by an injected
|
|
101
|
+
// `which` that returns an APPDIR-mount candidate.
|
|
102
|
+
whereStrategy("synthetic-tool", {
|
|
103
|
+
which: () => fakeAppDir + "/synthetic-tool",
|
|
104
|
+
}),
|
|
105
|
+
],
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
const res = registry.resolve("synthetic-tool");
|
|
109
|
+
expect(res.ok).toBe(false);
|
|
110
|
+
const whereEntry = res.tried.find((t) => t.strategy === "where");
|
|
111
|
+
expect(whereEntry).toBeDefined();
|
|
112
|
+
expect(String(whereEntry!.result)).toContain("appimage-self-hit");
|
|
113
|
+
} finally {
|
|
114
|
+
if (savedAppDir === undefined) delete process.env.APPDIR;
|
|
115
|
+
else process.env.APPDIR = savedAppDir;
|
|
116
|
+
}
|
|
117
|
+
});
|
|
118
|
+
});
|
|
@@ -11,6 +11,8 @@ import type {
|
|
|
11
11
|
OpenSpecData,
|
|
12
12
|
ModelInfo,
|
|
13
13
|
PiSessionInfo,
|
|
14
|
+
ExtensionUiModule,
|
|
15
|
+
DecoratorDescriptor,
|
|
14
16
|
} from "./types.js";
|
|
15
17
|
import type { TerminalSession } from "./terminal-types.js";
|
|
16
18
|
import type { EditorInstanceStatus } from "./editor-types.js";
|
|
@@ -214,13 +216,24 @@ export interface BrowserPromptCancelMessage {
|
|
|
214
216
|
promptId: string;
|
|
215
217
|
}
|
|
216
218
|
|
|
217
|
-
/** Progress event streamed during a package install/remove/update operation.
|
|
219
|
+
/** Progress event streamed during a package install/remove/update/move operation.
|
|
220
|
+
*
|
|
221
|
+
* `moveId` is set when this progress event is part of a move operation
|
|
222
|
+
* (which composes install + remove). Clients group events by `moveId`
|
|
223
|
+
* to display a single composite progress affordance instead of two
|
|
224
|
+
* separate operations. Consumers that ignore the field continue to
|
|
225
|
+
* render install + remove independently — graceful degradation.
|
|
226
|
+
*
|
|
227
|
+
* See change: unify-package-management-ui.
|
|
228
|
+
*/
|
|
218
229
|
export interface PackageProgressMessage {
|
|
219
230
|
type: "package_progress";
|
|
220
231
|
operationId: string;
|
|
232
|
+
/** Optional move grouping id when emitted as part of a move. */
|
|
233
|
+
moveId?: string;
|
|
221
234
|
event: {
|
|
222
235
|
type: "start" | "progress" | "complete" | "error";
|
|
223
|
-
action: "install" | "remove" | "update" | "clone" | "pull";
|
|
236
|
+
action: "install" | "remove" | "update" | "clone" | "pull" | "move";
|
|
224
237
|
source: string;
|
|
225
238
|
message?: string;
|
|
226
239
|
};
|
|
@@ -297,16 +310,72 @@ export interface BootstrapTicketCompleteMessage {
|
|
|
297
310
|
export interface PackageOperationCompleteMessage {
|
|
298
311
|
type: "package_operation_complete";
|
|
299
312
|
operationId: string;
|
|
300
|
-
|
|
313
|
+
/** Optional move grouping id; set on every event of a composite move op. */
|
|
314
|
+
moveId?: string;
|
|
315
|
+
action: "install" | "remove" | "update" | "move";
|
|
301
316
|
source: string;
|
|
302
317
|
scope: "global" | "local";
|
|
303
318
|
success: boolean;
|
|
304
319
|
error?: string;
|
|
305
320
|
/** Number of sessions reloaded (only on success). */
|
|
306
321
|
sessionsReloaded?: number;
|
|
322
|
+
/** Set on a move op when install succeeded but remove failed.
|
|
323
|
+
* Indicates the package now exists in BOTH scopes; UI should surface
|
|
324
|
+
* a recovery action (POST /api/packages/remove against fromScope). */
|
|
325
|
+
partialSuccess?: {
|
|
326
|
+
installed: boolean;
|
|
327
|
+
removed: boolean;
|
|
328
|
+
removeError?: string;
|
|
329
|
+
};
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
// ── Extension UI System (Phase 1: management-modal slot) ───────────
|
|
333
|
+
// See change: add-extension-ui-modal.
|
|
334
|
+
|
|
335
|
+
/** Server → browser: cached extension-declared UI modules for a session. */
|
|
336
|
+
export interface BrowserUiModulesListMessage {
|
|
337
|
+
type: "ui_modules_list";
|
|
338
|
+
sessionId: string;
|
|
339
|
+
modules: ExtensionUiModule[];
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
/** Server → browser: row data for a `view.dataEvent`. */
|
|
343
|
+
export interface BrowserUiDataListMessage {
|
|
344
|
+
type: "ui_data_list";
|
|
345
|
+
sessionId: string;
|
|
346
|
+
event: string;
|
|
347
|
+
items: unknown[];
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
// ── Extension UI System (Phase 2: live in-page decorations) ──
|
|
351
|
+
// See change: add-extension-ui-decorations.
|
|
352
|
+
|
|
353
|
+
/**
|
|
354
|
+
* Server → browser: a live decorator descriptor (forwarded verbatim from the
|
|
355
|
+
* extension). `removed: true` instructs the client to unmount the matching
|
|
356
|
+
* descriptor.
|
|
357
|
+
*/
|
|
358
|
+
export interface BrowserExtUiDecoratorMessage {
|
|
359
|
+
type: "ext_ui_decorator";
|
|
360
|
+
sessionId: string;
|
|
361
|
+
descriptor: DecoratorDescriptor;
|
|
362
|
+
removed?: boolean;
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
/** Sent when a plugin's config changes; carries only that plugin's namespace. */
|
|
366
|
+
export interface PluginConfigUpdateMessage {
|
|
367
|
+
type: "plugin_config_update";
|
|
368
|
+
/** Plugin id that was updated. */
|
|
369
|
+
id: string;
|
|
370
|
+
/**
|
|
371
|
+
* Only this plugin's namespace config (plugins.<id>.*).
|
|
372
|
+
* Never contains other plugins' configs.
|
|
373
|
+
*/
|
|
374
|
+
config: unknown;
|
|
307
375
|
}
|
|
308
376
|
|
|
309
377
|
export type ServerToBrowserMessage =
|
|
378
|
+
| PluginConfigUpdateMessage
|
|
310
379
|
| SessionAddedMessage
|
|
311
380
|
| SessionUpdatedMessage
|
|
312
381
|
| SessionRemovedMessage
|
|
@@ -344,7 +413,10 @@ export type ServerToBrowserMessage =
|
|
|
344
413
|
| BrowserPromptCancelMessage
|
|
345
414
|
| ModelsRefreshedMessage
|
|
346
415
|
| BootstrapStatusUpdateMessage
|
|
347
|
-
| BootstrapTicketCompleteMessage
|
|
416
|
+
| BootstrapTicketCompleteMessage
|
|
417
|
+
| BrowserUiModulesListMessage
|
|
418
|
+
| BrowserUiDataListMessage
|
|
419
|
+
| BrowserExtUiDecoratorMessage;
|
|
348
420
|
|
|
349
421
|
// ── Browser → Server ────────────────────────────────────────────────
|
|
350
422
|
|
|
@@ -483,6 +555,16 @@ export interface ResumeSessionBrowserMessage {
|
|
|
483
555
|
mode: "continue" | "fork";
|
|
484
556
|
/** When forking, optionally fork from a specific session entry instead of the latest */
|
|
485
557
|
entryId?: string;
|
|
558
|
+
/**
|
|
559
|
+
* Placement intent for the resumed session in the cwd's sessionOrder:
|
|
560
|
+
* - "front" (default): move to top of alive tier (Resume button, REST,
|
|
561
|
+
* prompt-auto-resume).
|
|
562
|
+
* - "keep": leave order alone (drag-to-resume — drop position was already
|
|
563
|
+
* persisted by an earlier `reorder_sessions` message).
|
|
564
|
+
* Server defaults to "front" when omitted, preserving prior behavior.
|
|
565
|
+
* See change: differentiate-resume-intent-by-trigger.
|
|
566
|
+
*/
|
|
567
|
+
placement?: "front" | "keep";
|
|
486
568
|
}
|
|
487
569
|
|
|
488
570
|
export interface HideSessionBrowserMessage {
|
|
@@ -498,6 +580,15 @@ export interface UnhideSessionBrowserMessage {
|
|
|
498
580
|
export interface SpawnSessionBrowserMessage {
|
|
499
581
|
type: "spawn_session";
|
|
500
582
|
cwd: string;
|
|
583
|
+
/**
|
|
584
|
+
* Optional kebab-case OpenSpec change name to attach to the spawned session
|
|
585
|
+
* once it registers. The server queues the intent in `pendingAttachByCwd`
|
|
586
|
+
* and consumes it on the next matching `session_register`.
|
|
587
|
+
* Old servers that ignore unknown fields produce a bare spawn (degraded but
|
|
588
|
+
* recoverable: the user attaches manually). See change:
|
|
589
|
+
* add-folder-task-checker-and-spawn-attach.
|
|
590
|
+
*/
|
|
591
|
+
attachProposal?: string;
|
|
501
592
|
}
|
|
502
593
|
|
|
503
594
|
export interface AttachProposalBrowserMessage {
|
|
@@ -629,6 +720,20 @@ export interface RequestRolesBrowserMessage {
|
|
|
629
720
|
sessionId: string;
|
|
630
721
|
}
|
|
631
722
|
|
|
723
|
+
/**
|
|
724
|
+
* Browser → server: the user invoked a Phase-1 module action / requested
|
|
725
|
+
* row data. Server forwards via `piGateway.sendToSession` to the bridge,
|
|
726
|
+
* which re-emits as `pi.events.emit(event, { ...params, action, _reply })`.
|
|
727
|
+
* See change: add-extension-ui-modal.
|
|
728
|
+
*/
|
|
729
|
+
export interface UiManagementBrowserMessage {
|
|
730
|
+
type: "ui_management";
|
|
731
|
+
sessionId: string;
|
|
732
|
+
action: string;
|
|
733
|
+
event: string;
|
|
734
|
+
params?: Record<string, unknown>;
|
|
735
|
+
}
|
|
736
|
+
|
|
632
737
|
export type BrowserToServerMessage =
|
|
633
738
|
| SubscribeMessage
|
|
634
739
|
| UnsubscribeMessage
|
|
@@ -669,4 +774,5 @@ export type BrowserToServerMessage =
|
|
|
669
774
|
| RolePresetSaveBrowserMessage
|
|
670
775
|
| RolePresetDeleteBrowserMessage
|
|
671
776
|
| RequestRolesBrowserMessage
|
|
777
|
+
| UiManagementBrowserMessage
|
|
672
778
|
| KillProcessBrowserMessage;
|
|
@@ -80,6 +80,12 @@ export interface KnownServer {
|
|
|
80
80
|
addedAt: string; // ISO timestamp
|
|
81
81
|
}
|
|
82
82
|
|
|
83
|
+
/**
|
|
84
|
+
* Plugin-specific config namespace.
|
|
85
|
+
* Lives at ~/.pi/dashboard/config.json#plugins.<id>.*
|
|
86
|
+
*/
|
|
87
|
+
export type PluginsConfig = Record<string, Record<string, unknown>>;
|
|
88
|
+
|
|
83
89
|
export interface DashboardConfig {
|
|
84
90
|
port: number;
|
|
85
91
|
piPort: number;
|
|
@@ -107,6 +113,13 @@ export interface DashboardConfig {
|
|
|
107
113
|
electronMode: boolean;
|
|
108
114
|
/** Persisted list of known remote servers */
|
|
109
115
|
knownServers: KnownServer[];
|
|
116
|
+
/**
|
|
117
|
+
* Per-plugin config namespaces. Reserved top-level key.
|
|
118
|
+
* Each plugin's config lives at plugins.<id>.*
|
|
119
|
+
* Plugin-shaped legacy top-level keys (e.g. openspec.*) stay at top-level
|
|
120
|
+
* until each extract-*-as-plugin change migrates them.
|
|
121
|
+
*/
|
|
122
|
+
plugins: PluginsConfig;
|
|
110
123
|
}
|
|
111
124
|
|
|
112
125
|
export interface CorsConfig {
|
|
@@ -117,6 +130,7 @@ export interface CorsConfig {
|
|
|
117
130
|
const VALID_SPAWN_STRATEGIES: SpawnStrategy[] = ["tmux", "headless"];
|
|
118
131
|
|
|
119
132
|
const DEFAULTS: DashboardConfig = {
|
|
133
|
+
plugins: {},
|
|
120
134
|
port: 8000,
|
|
121
135
|
piPort: 9999,
|
|
122
136
|
autoStart: true,
|
|
@@ -237,6 +251,36 @@ function parseMemoryLimits(raw: any): MemoryLimitsConfig {
|
|
|
237
251
|
};
|
|
238
252
|
}
|
|
239
253
|
|
|
254
|
+
function parsePluginsConfig(raw: unknown): PluginsConfig {
|
|
255
|
+
if (!raw || typeof raw !== "object" || Array.isArray(raw)) return {};
|
|
256
|
+
const result: PluginsConfig = {};
|
|
257
|
+
for (const [id, val] of Object.entries(raw as Record<string, unknown>)) {
|
|
258
|
+
if (val && typeof val === "object" && !Array.isArray(val)) {
|
|
259
|
+
result[id] = val as Record<string, unknown>;
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
return result;
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
/**
|
|
266
|
+
* Get the plugins config block from a loaded DashboardConfig.
|
|
267
|
+
* Provides typed access to plugins.<id>.* namespaces.
|
|
268
|
+
*/
|
|
269
|
+
export function getPluginsConfig(config: DashboardConfig): PluginsConfig {
|
|
270
|
+
return config.plugins ?? {};
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
/**
|
|
274
|
+
* Get a single plugin's config from a loaded DashboardConfig.
|
|
275
|
+
* Returns {} if the plugin has no stored config.
|
|
276
|
+
*/
|
|
277
|
+
export function getPluginConfig(
|
|
278
|
+
config: DashboardConfig,
|
|
279
|
+
pluginId: string,
|
|
280
|
+
): Record<string, unknown> {
|
|
281
|
+
return config.plugins?.[pluginId] ?? {};
|
|
282
|
+
}
|
|
283
|
+
|
|
240
284
|
function parseKnownServers(raw: any): KnownServer[] {
|
|
241
285
|
if (!Array.isArray(raw)) return [];
|
|
242
286
|
return raw
|
|
@@ -299,6 +343,7 @@ export function loadConfig(): DashboardConfig {
|
|
|
299
343
|
...(typeof parsed.lastServer === "string" ? { lastServer: parsed.lastServer } : {}),
|
|
300
344
|
electronMode: parsed.electronMode === true,
|
|
301
345
|
knownServers: parseKnownServers(parsed.knownServers),
|
|
346
|
+
plugins: parsePluginsConfig(parsed.plugins),
|
|
302
347
|
};
|
|
303
348
|
|
|
304
349
|
// Compute resolvedTrustedNetworks: merge trustedNetworks + auth.bypassHosts
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Barrel export for dashboard plugin system shared types.
|
|
3
|
+
* Import from:
|
|
4
|
+
* @blackbelt-technology/pi-dashboard-shared/dashboard-plugin/index.js
|
|
5
|
+
* @blackbelt-technology/pi-dashboard-shared/dashboard-plugin/slot-types.js
|
|
6
|
+
* etc.
|
|
7
|
+
*/
|
|
8
|
+
export * from "./slot-types.js";
|
|
9
|
+
export * from "./manifest-types.js";
|
|
10
|
+
export * from "./slot-props.js";
|
|
11
|
+
export * from "./plugin-status.js";
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import type { SlotId, SettingsTab } from "./slot-types.js";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* A single slot claim in a plugin manifest.
|
|
5
|
+
*/
|
|
6
|
+
export interface PluginClaim {
|
|
7
|
+
/** The slot this claim targets. */
|
|
8
|
+
slot: SlotId;
|
|
9
|
+
/** Exported component name from the plugin's client entry (for React slots). */
|
|
10
|
+
component?: string;
|
|
11
|
+
/** Route command for "command-route" slot (e.g. "/specs"). */
|
|
12
|
+
command?: string;
|
|
13
|
+
/** Trigger id for "anchored-popover" slot. */
|
|
14
|
+
trigger?: string;
|
|
15
|
+
/** toolName for "tool-renderer" slot. */
|
|
16
|
+
toolName?: string;
|
|
17
|
+
/**
|
|
18
|
+
* For "settings-section" slot: which SettingsPanel tab to render in.
|
|
19
|
+
* Defaults to "general" if omitted.
|
|
20
|
+
*/
|
|
21
|
+
tab?: SettingsTab;
|
|
22
|
+
/** Slot-specific extra config. */
|
|
23
|
+
config?: Record<string, unknown>;
|
|
24
|
+
/** Optional exported predicate function name for filtering contributions. */
|
|
25
|
+
predicate?: string;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* The pi-dashboard-plugin manifest.
|
|
30
|
+
* Declared as the `pi-dashboard-plugin` field in a package.json,
|
|
31
|
+
* or as a top-level `dashboard-plugin.json` adjacent to package.json.
|
|
32
|
+
*/
|
|
33
|
+
export interface PluginManifest {
|
|
34
|
+
/** Globally unique kebab-case plugin id. */
|
|
35
|
+
id: string;
|
|
36
|
+
/** Human-readable display name. */
|
|
37
|
+
displayName: string;
|
|
38
|
+
/**
|
|
39
|
+
* Lower number = rendered earlier in multi-contribution slots.
|
|
40
|
+
* Default 1000. First-party plugins use 100.
|
|
41
|
+
*/
|
|
42
|
+
priority?: number;
|
|
43
|
+
/** Relative path to the bundled client entry (from package root). */
|
|
44
|
+
client?: string;
|
|
45
|
+
/** Optional relative path to the server entry. */
|
|
46
|
+
server?: string;
|
|
47
|
+
/** Optional relative path to a pi-extension/bridge entry. */
|
|
48
|
+
bridge?: string;
|
|
49
|
+
/** Optional relative path to a JSON Schema 7 file for plugin config validation. */
|
|
50
|
+
configSchema?: string;
|
|
51
|
+
/** Slot claims. */
|
|
52
|
+
claims: PluginClaim[];
|
|
53
|
+
/**
|
|
54
|
+
* When true, the plugin is a test fixture and SHALL be excluded from
|
|
55
|
+
* production bundles (NODE_ENV=production).
|
|
56
|
+
*/
|
|
57
|
+
fixture?: boolean;
|
|
58
|
+
}
|