mirror of
https://github.com/getpaseo/paseo.git
synced 2026-11-07 00:44:49 +00:00
feat(plugins): unify panel props and persist tab state (#7685)
* feat(plugins): unify panel props and persist tab state * refactor(plugins): make agent panel registration canonical * docs(plugins): update Chinese panel API reference and migration
This commit is contained in:
@@ -70,6 +70,26 @@ function boundaryViolations(entry: string, runtime: "shared" | "server" | "clien
|
||||
return violations;
|
||||
}
|
||||
|
||||
function typeErrors(code: string): string[] {
|
||||
const configPath = path.resolve(sourceDirectory, "../tsconfig.examples.json");
|
||||
const { config } = ts.readConfigFile(configPath, ts.sys.readFile);
|
||||
const { options } = ts.parseJsonConfigFileContent(config, ts.sys, path.dirname(configPath));
|
||||
const file = path.resolve(
|
||||
sourceDirectory,
|
||||
"../../../plugin-examples/skill-example/client/example.tsx",
|
||||
);
|
||||
const host = ts.createCompilerHost(options);
|
||||
const getSourceFile = host.getSourceFile;
|
||||
host.getSourceFile = (name, language, ...rest) =>
|
||||
path.resolve(name) === file
|
||||
? ts.createSourceFile(name, code, language)
|
||||
: getSourceFile.call(host, name, language, ...rest);
|
||||
host.fileExists = (name) => path.resolve(name) === file || ts.sys.fileExists(name);
|
||||
const program = ts.createProgram([file], options, host);
|
||||
return ts
|
||||
.getPreEmitDiagnostics(program)
|
||||
.map((diagnostic) => ts.flattenDiagnosticMessageText(diagnostic.messageText, "\n"));
|
||||
}
|
||||
describe("plugin SDK import boundaries", () => {
|
||||
it("publishes process capabilities without their Windows implementation helpers", () => {
|
||||
expect(Object.keys(server).sort()).toEqual(["execCommand", "spawnProcess", "terminateProcess"]);
|
||||
@@ -142,3 +162,33 @@ describe("plugin example import boundaries", () => {
|
||||
expect(violations).toEqual([]);
|
||||
});
|
||||
});
|
||||
|
||||
describe("plugin panel SDK boundary", () => {
|
||||
it("accepts uniform params and persisted tab state alongside 0.12 panels", () => {
|
||||
expect(
|
||||
typeErrors(`
|
||||
import { type PluginClientContext, type PluginFilePanelProps, type PluginWorkspacePanelProps, type PluginAgentPanelProps, type PluginTab, useTabState } from "@getpaseo/plugin/client";
|
||||
import { z } from "zod";
|
||||
function FilePanel({ params, revision, tab, file }: PluginFilePanelProps) {
|
||||
const [state, setState] = useTabState(tab, z.object({ view: z.enum(["preview", "source"]) }), { view: "preview" });
|
||||
setState({ view: "source" });
|
||||
tab.setState({ nested: [true, null, 3] });
|
||||
// @ts-expect-error only JSON state crosses the host boundary
|
||||
tab.setState(() => {});
|
||||
// @ts-expect-error params belong to the opener
|
||||
params.path = "other";
|
||||
return null;
|
||||
}
|
||||
function OldWorkspace({ context, workspaceId }: PluginWorkspacePanelProps) { return null; }
|
||||
function OldAgent({ context, workspaceId, agentId }: PluginAgentPanelProps) { return null; }
|
||||
function Agent({ params }: PluginAgentPanelProps) { params.agentId.toUpperCase(); return null; }
|
||||
function contribute(client: PluginClientContext) {
|
||||
client.addWorkspacePanel({ id: "old-workspace", title: "Old", icon: "File", context: "workspace", Component: OldWorkspace });
|
||||
client.addWorkspacePanel({ id: "old-agent", title: "Old", icon: "File", context: "agent", Component: OldAgent });
|
||||
client.addWorkspacePanel({ id: "workspace", title: "Workspace", icon: "File", Component: OldWorkspace });
|
||||
client.addAgentPanel({ id: "agent", title: "Agent", icon: "File", Component: Agent });
|
||||
}
|
||||
`),
|
||||
).toEqual([]);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import type { PluginText } from "../text.js";
|
||||
import type { ComponentType } from "react";
|
||||
import type { PaseoApi } from "@getpaseo/client";
|
||||
import type { AgentTimelineItem } from "@getpaseo/protocol/agent-types";
|
||||
import type { AgentTimelineItem, JsonValue } from "@getpaseo/protocol/agent-types";
|
||||
import type { ZodType, input as ZodInput, output as ZodOutput } from "zod";
|
||||
import type { PluginFile, PluginFileLocation, PluginFileTab } from "./file.js";
|
||||
import type { PluginFile } from "./file.js";
|
||||
import type { PluginRpcContract } from "../rpc.js";
|
||||
import type {
|
||||
PluginButtonRegistration,
|
||||
@@ -111,23 +111,44 @@ interface PluginWorkspacePanelBase<Text extends PluginText = string> {
|
||||
locations?: readonly PluginPanelLocation[];
|
||||
}
|
||||
|
||||
export interface PluginWorkspacePanelProps extends PluginNavigableHostProps {
|
||||
context: "workspace";
|
||||
workspaceId: string;
|
||||
export interface PluginTab {
|
||||
/** Shows the tab's unsaved dot and asks before closing. */
|
||||
setModified(modified: boolean): void;
|
||||
/** Opaque persisted JSON. Undefined until written; the panel validates it. */
|
||||
readonly state: unknown;
|
||||
setState(state: JsonValue): void;
|
||||
}
|
||||
|
||||
export interface PluginAgentPanelProps extends PluginNavigableHostProps {
|
||||
context: "agent";
|
||||
export interface PluginPanelProps<Params> extends PluginNavigableHostProps {
|
||||
workspaceId: string;
|
||||
readonly params: Readonly<Params>;
|
||||
revision: number;
|
||||
tab: PluginTab;
|
||||
}
|
||||
export interface PluginWorkspacePanelParams {}
|
||||
export interface PluginAgentPanelParams {
|
||||
readonly agentId: string;
|
||||
}
|
||||
export interface PluginFilePanelParams {
|
||||
readonly path: string;
|
||||
readonly lineStart?: number;
|
||||
readonly lineEnd?: number;
|
||||
}
|
||||
export interface PluginWorkspacePanelProps extends PluginPanelProps<PluginWorkspacePanelParams> {
|
||||
// COMPAT(pluginPanelFlatProps): added in v0.13.0, remove after 2027-04-23
|
||||
/** @deprecated Use the panel registration to select the context. */
|
||||
context: "workspace";
|
||||
}
|
||||
export interface PluginAgentPanelProps extends PluginPanelProps<PluginAgentPanelParams> {
|
||||
// COMPAT(pluginPanelFlatProps): added in v0.13.0, remove after 2027-04-23
|
||||
/** @deprecated Use the panel registration to select the context. */
|
||||
context: "agent";
|
||||
// COMPAT(pluginPanelFlatProps): added in v0.13.0, remove after 2027-04-23
|
||||
/** @deprecated Read `params.agentId`. */
|
||||
agentId: string;
|
||||
}
|
||||
|
||||
export interface PluginFilePanelProps extends PluginNavigableHostProps {
|
||||
workspaceId: string;
|
||||
/** The file itself. The panel decides how to read it, show it, and save it. */
|
||||
export interface PluginFilePanelProps extends PluginPanelProps<PluginFilePanelParams> {
|
||||
file: PluginFile;
|
||||
tab: PluginFileTab;
|
||||
location: PluginFileLocation;
|
||||
}
|
||||
|
||||
export interface PluginFilePanelContribution<Text extends PluginText = string> {
|
||||
@@ -160,6 +181,7 @@ export interface PluginClientContext extends PluginCommandCapabilities, PluginCl
|
||||
addScreen(contribution: PluginScreenContribution<PluginText>): PluginCleanup;
|
||||
addSidebarHeaderItem(contribution: PluginSidebarItemContribution<PluginText>): PluginCleanup;
|
||||
addSidebarFooterItem(contribution: PluginSidebarItemContribution<PluginText>): PluginCleanup;
|
||||
addAgentPanel(contribution: PluginAgentPanelContribution<PluginText>): PluginCleanup;
|
||||
addWorkspacePanel(contribution: PluginWorkspacePanelContribution<PluginText>): PluginCleanup;
|
||||
addCommandCenterItem(
|
||||
contribution: PluginCommandCenterItemContribution<PluginText>,
|
||||
@@ -187,14 +209,24 @@ export type PluginClientContribution = (client: PluginClientContext) => PluginCl
|
||||
|
||||
export type PluginWorkspacePanelContribution<Text extends PluginText = string> =
|
||||
| (PluginWorkspacePanelBase<Text> & {
|
||||
context: "workspace";
|
||||
// COMPAT(pluginPanelFlatProps): added in v0.13.0, remove after 2027-04-23
|
||||
/** @deprecated Omit context. */
|
||||
context?: "workspace";
|
||||
Component: ComponentType<PluginWorkspacePanelProps>;
|
||||
})
|
||||
| (PluginWorkspacePanelBase<Text> & {
|
||||
// COMPAT(pluginAgentPanelAlias): added in v0.13.0, remove after 2027-04-23
|
||||
/** @deprecated Register with `addAgentPanel`. */
|
||||
context: "agent";
|
||||
Component: ComponentType<PluginAgentPanelProps>;
|
||||
});
|
||||
|
||||
export interface PluginAgentPanelContribution<
|
||||
Text extends PluginText = string,
|
||||
> extends PluginWorkspacePanelBase<Text> {
|
||||
Component: ComponentType<PluginAgentPanelProps>;
|
||||
}
|
||||
|
||||
export interface PluginSettingsScreenContribution<Text extends PluginText = string> {
|
||||
id: string;
|
||||
title: Text;
|
||||
|
||||
@@ -44,19 +44,6 @@ export interface PluginFileWriteOptions {
|
||||
/** What happened to the file on disk. Switch on `type`; later versions may add fields. */
|
||||
export type PluginFileEvent = { type: "changed" } | { type: "deleted" };
|
||||
|
||||
/** What the panel reports back to its tab. */
|
||||
export interface PluginFileTab {
|
||||
/** Shows the tab's unsaved dot and asks before the tab closes. */
|
||||
setModified(modified: boolean): void;
|
||||
}
|
||||
|
||||
/** The line a link or search result opened the file at. `revision` bumps on every new target. */
|
||||
export interface PluginFileLocation {
|
||||
lineStart?: number;
|
||||
lineEnd?: number;
|
||||
revision: number;
|
||||
}
|
||||
|
||||
/** The file changed on disk since the handle last read or wrote it. */
|
||||
export class PluginFileConflictError extends Error {
|
||||
constructor(message = "The file changed on disk.") {
|
||||
|
||||
@@ -1,5 +1,11 @@
|
||||
export type {
|
||||
PluginHostProps,
|
||||
PluginTab,
|
||||
PluginPanelProps,
|
||||
PluginWorkspacePanelParams,
|
||||
PluginAgentPanelParams,
|
||||
PluginFilePanelParams,
|
||||
PluginAgentPanelContribution,
|
||||
PluginFilePanelContribution,
|
||||
PluginFilePanelProps,
|
||||
PluginSurfaceProps,
|
||||
@@ -52,8 +58,6 @@ export type {
|
||||
PluginFile,
|
||||
PluginFileBytesState,
|
||||
PluginFileEvent,
|
||||
PluginFileLocation,
|
||||
PluginFileTab,
|
||||
PluginFileTextState,
|
||||
PluginFileUrlState,
|
||||
PluginFileWriteOptions,
|
||||
@@ -91,3 +95,5 @@ export declare function openExternalUrl(url: string): Promise<void>;
|
||||
export { defineTranslations, useTranslation } from "./translations.js";
|
||||
export type { PluginI18n } from "./translations.js";
|
||||
export type { PluginText, PluginMessage, PluginTranslations } from "../text.js";
|
||||
|
||||
export { useTabState } from "./tab.js";
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
import { useCallback, useMemo } from "react";
|
||||
import type { JsonValue } from "@getpaseo/protocol/agent-types";
|
||||
import type { ZodType } from "zod";
|
||||
import type { PluginTab } from "./contracts.js";
|
||||
|
||||
/** Validates the panel's persisted state, using the default for missing or invalid values. */
|
||||
export function useTabState<T extends JsonValue>(
|
||||
tab: PluginTab,
|
||||
schema: ZodType<T>,
|
||||
defaultState: T,
|
||||
): [T, (next: T) => void] {
|
||||
const parsed = useMemo(() => schema.safeParse(tab.state), [schema, tab.state]);
|
||||
const writeState = tab.setState;
|
||||
const setState = useCallback((next: T) => writeState(next), [writeState]);
|
||||
return [parsed.success ? parsed.data : defaultState, setState];
|
||||
}
|
||||
Reference in New Issue
Block a user