feat(plugin): let users select a sidebar view

An exclusive view replaces the sidebar content while the host retains navigation, callouts, and footer. The device remembers the selection, unavailable views fall back to the default, and throwing views provide recovery. Device-scoped plugin settings persist locally with schema validation and revision conflicts. Web E2E and Android tablet QA cover selection, preserved chrome, disabled-view fallback, and error recovery.
This commit is contained in:
Mohamed Boudra
2026-11-06 09:22:19 +01:00
parent e4106b4e2d
commit dc309586f0
45 changed files with 1059 additions and 79 deletions
+10 -1
View File
@@ -393,7 +393,7 @@ describe("sidebar data SDK boundary", () => {
it("accepts references and preserves string-id panel consumers", () => {
expect(
typeErrors(`
import { useWorkspace, useAgent, type WorkspaceRef, type AgentRef, useProject, useTerminal, useWorkspaces, useAgents, useProjects, useTerminals, type ProjectRef, type TerminalRef, type Query, type Group } from "@getpaseo/plugin/client";
import { useWorkspace, useAgent, type PluginClientContext, type PluginSidebarViewProps, type WorkspaceRef, type AgentRef, useProject, useTerminal, useWorkspaces, useAgents, useProjects, useTerminals, type ProjectRef, type TerminalRef, type Query, type Group } from "@getpaseo/plugin/client";
import type { PluginWorkspaceSnapshot } from "@getpaseo/plugin";
function Panel({ workspace, agent }: { workspace: WorkspaceRef; agent: AgentRef }) {
const snapshot: PluginWorkspaceSnapshot | null = useWorkspace(workspace);
@@ -407,6 +407,15 @@ describe("sidebar data SDK boundary", () => {
const terminals: readonly TerminalRef[] = useTerminals();
if (projects[0]) useProject(projects[0], item => item.displayName);
if (terminals[0]) useTerminal(terminals[0]);
function View(props: PluginSidebarViewProps) {
props.navigation.openWorkspace(workspace);
props.navigation.openAgent(agent);
props.dismiss();
return null;
}
function contribute(client: PluginClientContext) {
client.addSidebarView({ id: "workspaces", title: "Workspaces", Component: View });
}
const query: Query<PluginWorkspaceSnapshot> = { hosts: [workspace.hostId], orderBy: ["name", "desc"] };
return null;
}
+20
View File
@@ -1,3 +1,4 @@
import type { WorkspaceRef, AgentRef, ProjectRef, TerminalRef } from "../refs.js";
import type { PluginText } from "../text.js";
import type { ComponentType } from "react";
import type { PaseoApi } from "@getpaseo/client";
@@ -96,6 +97,24 @@ export interface PluginSidebarItemContribution<Text extends PluginText = string>
Component: ComponentType<PluginSidebarItemProps>;
}
export interface PluginSidebarViewProps extends Pick<PluginHostProps, "theme" | "layout"> {
active: { workspace: WorkspaceRef | null; agent: AgentRef | null };
navigation: {
openWorkspace(ref: WorkspaceRef): void;
openAgent(ref: AgentRef): void;
openTerminal(ref: TerminalRef): void;
openProject(ref: ProjectRef): void;
};
dismiss(): void;
}
export interface PluginSidebarViewContribution<Text extends PluginText = string> {
id: string;
title: Text;
description?: Text;
Component: ComponentType<PluginSidebarViewProps>;
}
/**
* A Lucide icon name, inline SVG markup, or a component. Components receive the surface's size
* and color; buttons widen the props with their host and target context.
@@ -185,6 +204,7 @@ export interface PluginClientContext extends PluginCommandCapabilities, PluginCl
playAudio(source: { base64: string; mimeType: string }): Promise<void>;
addSettingsScreen(contribution: PluginSettingsScreenContribution<PluginText>): PluginCleanup;
addScreen(contribution: PluginScreenContribution<PluginText>): PluginCleanup;
addSidebarView(contribution: PluginSidebarViewContribution<PluginText>): PluginCleanup;
addSidebarHeaderItem(contribution: PluginSidebarItemContribution<PluginText>): PluginCleanup;
addSidebarFooterItem(contribution: PluginSidebarItemContribution<PluginText>): PluginCleanup;
addAgentPanel(contribution: PluginAgentPanelContribution<PluginText>): PluginCleanup;
+2
View File
@@ -15,6 +15,8 @@ export type {
PluginOpenScreenInput,
PluginScreenLocation,
PluginSidebarItemProps,
PluginSidebarViewProps,
PluginSidebarViewContribution,
PluginSidebarItemContribution,
PluginIcon,
PluginPanelLocation,
+4 -3
View File
@@ -4,10 +4,10 @@ import { defineRpc } from "./rpc.js";
export interface SettingsDefinition<Schema extends ZodType = ZodType> {
id: string;
scope: "host";
scope: "host" | "device";
version: number;
schema: Schema;
/** Convert a previous stored version to the current schema. Runs on the host. */
/** Convert a previous stored version to the current schema. Runs in the selected scope. */
migrate?: (values: unknown, fromVersion: number) => unknown | Promise<unknown>;
}
@@ -15,7 +15,8 @@ export function defineSettings<Schema extends ZodType>(
definition: SettingsDefinition<Schema>,
): SettingsDefinition<Schema> {
if (!/^[a-z][a-z0-9_-]*$/.test(definition.id)) throw new Error("Invalid settings ID");
if (definition.scope !== "host") throw new Error("Only host-scoped settings are supported");
if (definition.scope !== "host" && definition.scope !== "device")
throw new Error("Invalid settings scope");
if (!Number.isSafeInteger(definition.version) || definition.version < 1)
throw new Error("Invalid settings version");
return definition;