Keep sidebar layout and gestures inside the plugin while routing entity handles to their owning hosts. Examples cover empty projects, pinned workspaces, task sections, nested agents and device-local label folders.
7.4 KiB
Build a sidebar view
A sidebar view replaces the content between Paseo's header and footer. You receive theme tokens, active entity refs and navigation methods, then render your own React Native UI. Install the sidebar-views example to try projects, task-based agents, nested agents and label folders.
Register a view
import type {
PluginClientContext,
PluginSidebarViewProps,
WorkspaceRef,
} from "@getpaseo/plugin/client";
import { useWorkspaces, useWorkspace } from "@getpaseo/plugin/client";
import { Pressable, ScrollView, Text } from "react-native";
function WorkspaceRow({
reference,
navigation,
theme,
}: {
reference: WorkspaceRef;
navigation: PluginSidebarViewProps["navigation"];
theme: PluginSidebarViewProps["theme"];
}) {
const workspace = useWorkspace(reference);
if (!workspace) return null;
return (
<Pressable onPress={() => navigation.openWorkspace(reference)}>
<Text style={{ color: theme.colors.foreground }}>{workspace.title ?? workspace.name}</Text>
</Pressable>
);
}
function Workspaces({ navigation, theme }: PluginSidebarViewProps) {
const workspaces = useWorkspaces({ orderBy: "name" });
return (
<ScrollView>
{workspaces.map((reference) => (
<WorkspaceRow
key={reference.key}
reference={reference}
navigation={navigation}
theme={theme}
/>
))}
</ScrollView>
);
}
export default function contribute(client: PluginClientContext) {
return client.addSidebarView({ id: "workspaces", title: "My workspaces", Component: Workspaces });
}
Registration adds an option under Settings > Sidebar > Sidebar view. The user selects it; installing the plugin leaves the current view selected. Header navigation, plugin header items, the callout and footer stay visible. A missing or disabled view falls back to Default view. A view that throws shows a placeholder with Use default view.
The view renders once across connected hosts. active.workspace and active.agent are refs or
null. Use navigation.openWorkspace, openAgent, openTerminal or openProject with a ref;
openProject starts the new workspace flow. Navigation closes the compact sidebar. Call
dismiss() after navigation you implement yourself.
Query refs, read each row
useWorkspaces, useAgents, useProjects and useTerminals return immutable arrays of refs.
A ref has kind, hostId, id and a cross-host key. The same entity keeps the same frozen
ref for the app session. Pass it to useWorkspace, useAgent, useProject or useTerminal
inside the row. A deleted entity returns null.
const projects = useProjects({ orderBy: "displayName" }); // Includes empty projects.
const agents = useAgents({
hosts: [hostId],
where: (agent) => agent.workspace.key === workspaceRef.key,
orderBy: ["lastActivityAt", "desc"],
});
const folders = useWorkspaces({
groupBy: (workspace) => workspace.labels, // A workspace can appear in several folders.
});
Every query accepts hosts (server IDs), where, orderBy and groupBy. Omit hosts to query
every connected host. orderBy accepts a snapshot field, [field, "asc" | "desc"], or a
comparator. Null fields sort last. Keep comparators cheap; they run when data changes.
groupBy returns a string, { key, title?, color? }, or an array of these. Grouped results have
key, title, color and items. Groups follow first appearance after sorting; duplicate keys
use the first title. An empty array puts an item in no group.
An unchanged ref sequence keeps its array identity. Groups keep their identity when their metadata and item sequence stay the same. A workspace status change updates that row without rendering the list again. A grouping change updates the affected groups. Inline query objects and functions work without managing subscriptions yourself.
Entity hooks accept a selector and optional equality function:
const workspace = useWorkspace(ref); // Interned snapshot, compared by identity.
const name = useWorkspace(ref, (item) => ({ title: item.title })); // Shallow equality.
const selected = useWorkspace(ref, selector, equals);
Without a selector, custom equality receives snapshots. With a selector, it receives selections.
Existing workspace panels can continue passing a host-local string ID to useWorkspace and
useAgent. Prefer refs for a view spanning hosts. useHosts() retains its existing
serverId, label and connection status fields.
Workspace snapshots include project, pinnedAt, labels, background, branch,
lastPromptAt, parentAgent and createdAt (null on older hosts). Use indicator and the
localized indicatorLabel to display the status Paseo shows. status remains the raw daemon
status. Agent snapshots include their workspace and parent refs. Project snapshots provide
displayName, rootPath, kind and iconRevision.
Act on a ref
usePaseo() routes ref-based actions to the ref's host:
const paseo = usePaseo();
await paseo.workspaces.ref(workspaceRef).setPinned(true);
await paseo.workspaces.ref(workspaceRef).setLabel("Later", { assigned: true });
await paseo.workspaces.ref(workspaceRef).setLabel("Inbox", { assigned: false });
paseo.projects.ref(projectRef).open();
await paseo.projects.ref(projectRef).createWorkspace();
paseo.terminals.ref(terminalRef).focus();
await paseo.terminals.ref(terminalRef).kill();
Workspace and agent handles retain their existing methods. Assigning a new label creates it; assigning an existing label keeps its color. The label-folder example adds the destination before removing the source, so a failed assignment preserves the original folder. It owns the move gesture and error display.
Keep layout choices on the device
Define a settings document with scope: "device" for collapsed
folders or manual order. useSettings reads and writes it through the same revision-based API
as host settings. Device documents belong to the installation and settings ID, persist across
app launches, and are shared across that installation's client surfaces. They are not synced
to other devices. Use scope: "host" for data clients should share through the daemon.
Match the app's density
Render with React Native primitives. The SDK exports no sidebar-view rows, sections, lists, badges or drag helpers. The existing header/footer item kit is a separate surface.
For a dense row use a minimum height of 28, horizontal padding of 12, a 12-point primary label with 16-point line height and an 11-point secondary label. Use these supplied tokens:
| Row element | Theme token |
|---|---|
| Primary label | colors.foreground |
| Secondary label, section heading | colors.foregroundMuted |
| Selected row fill | colors.surface2 |
| Separator | colors.border |
| Action or focus accent | colors.accent |
| Success indicator | colors.statusSuccess |
| Needs input indicator | colors.statusWarning |
| Failure indicator | colors.statusDanger |
Give pressable rows an accessible label and show controls on touch layouts. The examples use only plugin-owned components and the tokens passed to the view.