mirror of
https://github.com/getpaseo/paseo.git
synced 2026-10-24 10:12:44 +00:00
* feat(plugins): add workspace panels and command center contributions Give local plugins contextual UI entry points without making the daemon interpret client placement or callbacks. Panels receive stable IDs and read cached client state through required selectors with shallow equality, avoiding unrelated guest rerenders and context-discovery RPCs. * fix(plugins): align shared client state ownership Workspace panels and Command Center callbacks consume the same frozen client snapshots, so their adapter belongs to the plugin boundary rather than either contribution surface. Refresh the Nix dependency hash for the selector-store dependency added by this feature.
177 lines
5.3 KiB
TypeScript
177 lines
5.3 KiB
TypeScript
import { describe, expect, it } from "vitest";
|
|
import { evaluatePluginClientBundle } from "./evaluate";
|
|
|
|
function bundle(body: string): string {
|
|
return `(function() {
|
|
const module = { exports: {} };
|
|
module.exports.default = function(plugin) { ${body}; return function() {}; };
|
|
return module.exports;
|
|
})`;
|
|
}
|
|
|
|
describe("evaluatePluginClientBundle", () => {
|
|
it("collects a surface and its sidebar placement", () => {
|
|
const plugin = evaluatePluginClientBundle(
|
|
"example",
|
|
bundle(`
|
|
function Surface() { return null; }
|
|
plugin.addSurface("main", Surface);
|
|
plugin.addSidebarItem({ id: "main", title: "Example", icon: "Blocks", surface: "main" });
|
|
`),
|
|
);
|
|
|
|
expect(plugin.id).toBe("example");
|
|
expect(plugin.surfaces.map((surface) => surface.id)).toEqual(["main"]);
|
|
expect(plugin.sidebarItems).toEqual([
|
|
{ id: "main", title: "Example", icon: "Blocks", surface: "main" },
|
|
]);
|
|
});
|
|
|
|
it("collects a declarative attachment source", () => {
|
|
const plugin = evaluatePluginClientBundle(
|
|
"linear",
|
|
bundle(`
|
|
plugin.addAttachmentSource({
|
|
id: "issues",
|
|
title: "Linear issue",
|
|
icon: "CircleDot",
|
|
pickerTitle: "Attach Linear issue",
|
|
searchPlaceholder: "Search by identifier or title",
|
|
search: { name: "issues.search", input: {}, output: {} },
|
|
});
|
|
`),
|
|
);
|
|
|
|
expect(plugin.attachmentSources).toEqual([
|
|
{
|
|
id: "issues",
|
|
title: "Linear issue",
|
|
icon: "CircleDot",
|
|
pickerTitle: "Attach Linear issue",
|
|
searchPlaceholder: "Search by identifier or title",
|
|
search: { name: "issues.search", input: {}, output: {} },
|
|
},
|
|
]);
|
|
});
|
|
|
|
it("collects contextual workspace panels and Command Center items", () => {
|
|
const plugin = evaluatePluginClientBundle(
|
|
"review",
|
|
bundle(`
|
|
function ReviewPanel() { return null; }
|
|
plugin.addWorkspacePanel({
|
|
id: "review",
|
|
title: "Review",
|
|
icon: "Scan",
|
|
context: "agent",
|
|
Component: ReviewPanel,
|
|
});
|
|
plugin.addCommandCenterItem({
|
|
id: "open-review",
|
|
title: "Open review",
|
|
icon: "Scan",
|
|
context: "agent",
|
|
onSelect() {},
|
|
});
|
|
`),
|
|
);
|
|
|
|
expect(
|
|
plugin.workspacePanels.map(({ id, title, icon, context }) => ({
|
|
id,
|
|
title,
|
|
icon,
|
|
context,
|
|
})),
|
|
).toEqual([{ id: "review", title: "Review", icon: "Scan", context: "agent" }]);
|
|
expect(
|
|
plugin.commandCenterItems.map(({ id, title, icon, context }) => ({
|
|
id,
|
|
title,
|
|
icon,
|
|
context,
|
|
})),
|
|
).toEqual([{ id: "open-review", title: "Open review", icon: "Scan", context: "agent" }]);
|
|
});
|
|
|
|
it("rejects duplicate workspace panel and Command Center ids", () => {
|
|
expect(() =>
|
|
evaluatePluginClientBundle(
|
|
"review",
|
|
bundle(`
|
|
function Panel() { return null; }
|
|
const panel = { id: "review", title: "Review", icon: "Scan", context: "workspace", Component: Panel };
|
|
plugin.addWorkspacePanel(panel);
|
|
plugin.addWorkspacePanel(panel);
|
|
`),
|
|
),
|
|
).toThrow("Duplicate workspace panel: review");
|
|
|
|
expect(() =>
|
|
evaluatePluginClientBundle(
|
|
"review",
|
|
bundle(`
|
|
const item = { id: "review", title: "Review", icon: "Scan", context: "global", onSelect() {} };
|
|
plugin.addCommandCenterItem(item);
|
|
plugin.addCommandCenterItem(item);
|
|
`),
|
|
),
|
|
).toThrow("Duplicate Command Center item: review");
|
|
});
|
|
|
|
it("rejects duplicate attachment source ids", () => {
|
|
expect(() =>
|
|
evaluatePluginClientBundle(
|
|
"linear",
|
|
bundle(`
|
|
const source = {
|
|
id: "issues",
|
|
title: "Linear issue",
|
|
icon: "CircleDot",
|
|
pickerTitle: "Attach Linear issue",
|
|
searchPlaceholder: "Search",
|
|
search: { name: "issues.search", input: {}, output: {} },
|
|
};
|
|
plugin.addAttachmentSource(source);
|
|
plugin.addAttachmentSource(source);
|
|
`),
|
|
),
|
|
).toThrow("Duplicate attachment source: issues");
|
|
});
|
|
|
|
it("rejects a sidebar placement whose surface does not exist", () => {
|
|
expect(() =>
|
|
evaluatePluginClientBundle(
|
|
"example",
|
|
bundle(`
|
|
plugin.addSidebarItem({ id: "main", title: "Example", icon: "Blocks", surface: "missing" });
|
|
`),
|
|
),
|
|
).toThrow("references missing surface missing");
|
|
});
|
|
|
|
it("rejects a bundle without a default contribution function", () => {
|
|
expect(() => evaluatePluginClientBundle("example", `(function() { return {}; })`)).toThrow(
|
|
"must default export a function",
|
|
);
|
|
});
|
|
|
|
it("requires a cleanup function", () => {
|
|
expect(() =>
|
|
evaluatePluginClientBundle("example", `(function() { return { default: function() {} }; })`),
|
|
).toThrow("must return a cleanup function");
|
|
});
|
|
|
|
it("does not publish partial contributions when setup fails", () => {
|
|
expect(() =>
|
|
evaluatePluginClientBundle(
|
|
"example",
|
|
`(function() { return { default: function(plugin) {
|
|
plugin.addSurface("main", function() { return null; });
|
|
throw new Error("setup exploded");
|
|
} }; })`,
|
|
),
|
|
).toThrow("setup exploded");
|
|
});
|
|
});
|