mirror of
https://github.com/getpaseo/paseo.git
synced 2026-11-07 17:04:42 +00:00
Let plugins provide custom sidebar views across hosts (#8906)
* feat(plugin): expose stable sidebar entity data across hosts Collections return interned references and preserve their arrays when membership and order stay unchanged. Entity selectors share one hook with identity defaults for full snapshots and shallow defaults for selections. The app caches host projections and shares underlying store and host subscriptions. Browser render counts exercise the real session store and plugin provider without memoized rows. Terminal hooks consume the collection source port; daemon-wide terminal discovery is the separate terminal phase. * 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. * feat(plugin): add sidebar view examples and ref actions 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. * feat(plugin): observe terminals across connected hosts Share host-wide terminal observations across plugin surfaces and gate them on terminalCollection. Keep directory observations compatible, and carry creation time in worker v6 state so v5 terminals can drain with an unknown timestamp.
This commit is contained in:
@@ -73,6 +73,7 @@ Use this table as the complete registration checklist.
|
||||
| `plugin.addSurface(id, Component)` in the old root entry | `client.addScreen({ id, title, Component })` in `index.client.tsx` |
|
||||
| `plugin.addSidebarItem({ id, title, icon, surface })` in the old root entry | `client.addSidebarHeaderItem({ id, title, Component })` in `index.client.tsx`; see [sidebar items](reference#sidebar-items) |
|
||||
| New sidebar footer contribution | `client.addSidebarFooterItem({ id, title, Component })` in `index.client.tsx` |
|
||||
| New sidebar replacement view | `client.addSidebarView({ id, title, Component })` in `index.client.tsx`; see [sidebar views](sidebar-views) |
|
||||
| New file panel contribution | `client.addFilePanel(panel)` in `index.client.tsx` |
|
||||
| `plugin.addWorkspacePanel(panel)` in the old root entry | `client.addWorkspacePanel(panel)` in `index.client.tsx` |
|
||||
| Agent panel with `context: "agent"` | `client.addAgentPanel(panel)` in `index.client.tsx`; omit `context` |
|
||||
|
||||
@@ -634,6 +634,13 @@ Cleanup can be async. Release timers, watchers, sockets, and other resources cre
|
||||
|
||||
Register async handlers with `server.addHook(name, handler)`. See [Hooks](/docs/plugins/hooks) for names, payloads, ordering, errors, and complete examples.
|
||||
|
||||
## Sidebar views
|
||||
|
||||
`addSidebarView({ id, title, description?, Component })` registers an optional replacement for the
|
||||
sidebar content region. It returns cleanup. The user selects one view in Settings > Sidebar;
|
||||
registration does not activate it. See [Build a sidebar view](sidebar-views.md) for the props,
|
||||
ref-based queries and actions, device state and theme tokens.
|
||||
|
||||
## Screens and sidebar items
|
||||
|
||||
A screen is a full page of plugin UI. A sidebar item is a component Paseo renders in the sidebar
|
||||
@@ -1387,13 +1394,13 @@ export default function contribute(server: PluginServerContext) {
|
||||
}
|
||||
```
|
||||
|
||||
| Definition field | Contract |
|
||||
| ------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------ |
|
||||
| `id` | Lowercase identifier matching `[a-z][a-z0-9_-]*`; unique within the installation. |
|
||||
| `scope` | Required `"host"`. All authorized clients of that host share the document. No per-user, device-local, or cross-host synchronization. |
|
||||
| `version` | Required positive integer describing the schema, independent of the write revision. |
|
||||
| `schema` | Zod schema for JSON values. Supply defaults so parsing `{}` produces a complete document. |
|
||||
| `migrate(values, fromVersion)` | Optional synchronous or asynchronous conversion from an older stored version. Its output must pass the current schema. |
|
||||
| Definition field | Contract |
|
||||
| ------------------------------ | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| `id` | Lowercase identifier matching `[a-z][a-z0-9_-]*`; unique within the installation. |
|
||||
| `scope` | Required `"host"` or `"device"`. Host documents are shared by authorized clients. Device documents stay on this device, keyed by installation and settings ID; use them for layout preferences. |
|
||||
| `version` | Required positive integer describing the schema, independent of the write revision. |
|
||||
| `schema` | Zod schema for JSON values. Supply defaults so parsing `{}` produces a complete document. |
|
||||
| `migrate(values, fromVersion)` | Optional synchronous or asynchronous conversion from an older stored version. Its output must pass the current schema. |
|
||||
|
||||
Call `useSettings(preferences)` in any contributed component. It returns a discriminated state:
|
||||
|
||||
|
||||
@@ -510,6 +510,10 @@ export default function contribute(client: PluginClientContext) {
|
||||
|
||||
使用 `server.addHook(name, handler)` 注册异步处理函数。请参阅[钩子](/docs/plugins/hooks),了解命名、载荷、顺序、错误处理和完整示例。
|
||||
|
||||
## 侧边栏视图 {#sidebar-views}
|
||||
|
||||
`addSidebarView({ id, title, description?, Component })` 注册可替换侧边栏内容区域的视图,并返回清理函数。用户在设置的侧边栏页面中选择视图;注册不会激活它。视图属性、基于引用的查询与操作、设备状态和主题令牌见[构建侧边栏视图](sidebar-views.md)。
|
||||
|
||||
## 界面和侧边栏项 {#screens-and-sidebar-items}
|
||||
|
||||
界面是一整页的插件 UI。侧边栏项是 Paseo 在侧边栏顶部或底部渲染的组件;按下它时做什么由它决定。先注册界面,再注册一个打开它的项:
|
||||
@@ -1157,13 +1161,13 @@ export default function contribute(server: PluginServerContext) {
|
||||
}
|
||||
```
|
||||
|
||||
| 定义字段 | 契约 |
|
||||
| ------------------------------ | --------------------------------------------------------------------------------------------- |
|
||||
| `id` | 匹配 `[a-z][a-z0-9_-]*` 的小写标识符;在同一次安装中唯一。 |
|
||||
| `scope` | 必须为 `"host"`。该主机所有获得授权的客户端共享这份文档。没有按用户、设备本地或跨主机的同步。 |
|
||||
| `version` | 必填的正整数,描述 schema 的版本,与写入修订号无关。 |
|
||||
| `schema` | JSON 值的 Zod schema。请提供默认值,使解析 `{}` 能得到一份完整的文档。 |
|
||||
| `migrate(values, fromVersion)` | 可选的同步或异步函数,从较旧的存储版本转换。它的输出必须通过当前的 schema。 |
|
||||
| 定义字段 | 契约 |
|
||||
| ------------------------------ | ----------------------------------------------------------------------------------------------------------------------- |
|
||||
| `id` | 匹配 `[a-z][a-z0-9_-]*` 的小写标识符;在同一次安装中唯一。 |
|
||||
| `scope` | 必填 `"host"` 或 `"device"`。主机文档由授权客户端共享。设备文档以插件安装和设置 ID 为键保存在当前设备上,用于布局偏好。 |
|
||||
| `version` | 必填的正整数,描述 schema 的版本,与写入修订号无关。 |
|
||||
| `schema` | JSON 值的 Zod schema。请提供默认值,使解析 `{}` 能得到一份完整的文档。 |
|
||||
| `migrate(values, fromVersion)` | 可选的同步或异步函数,从较旧的存储版本转换。它的输出必须通过当前的 schema。 |
|
||||
|
||||
在任何贡献的组件中调用 `useSettings(preferences)`。它返回一个可区分的状态:
|
||||
|
||||
|
||||
@@ -0,0 +1,177 @@
|
||||
# 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](https://github.com/getpaseo/paseo/tree/main/plugin-examples/sidebar-views)
|
||||
to try projects, task-based agents, nested agents and label folders.
|
||||
|
||||
## Register a view
|
||||
|
||||
```tsx
|
||||
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`.
|
||||
|
||||
```tsx
|
||||
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:
|
||||
|
||||
```tsx
|
||||
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`.
|
||||
|
||||
## List terminals
|
||||
|
||||
The terminals example selects a host, lists its terminals, focuses a terminal tab and closes a
|
||||
terminal through its handle. `useTerminals({ hosts: [hostId] })` requires a host advertising
|
||||
`terminalCollection`. A terminal reader throws an update-host error for an unsupported queried
|
||||
host; other entity hooks continue working. Keep host selection outside an error boundary so the
|
||||
user can select another host. Omit `hosts` to require the capability on every connected host.
|
||||
|
||||
Terminal snapshots contain `workspace`, `title`, `isScript` and `createdAt`. A terminal kept alive
|
||||
by a worker from before timestamp tracking has `createdAt: null`. Script ownership follows
|
||||
workspace script runtime entries. The host observation follows terminal creation, closure, title
|
||||
changes and reconnects, and releases when its last plugin subscriber leaves.
|
||||
|
||||
## Act on a ref
|
||||
|
||||
`usePaseo()` routes ref-based actions to the ref's host:
|
||||
|
||||
```tsx
|
||||
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](reference.md#settings-screens) 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.
|
||||
@@ -0,0 +1,135 @@
|
||||
# 构建侧边栏视图 {#build-a-sidebar-view}
|
||||
|
||||
侧边栏视图替换 Paseo 页眉与页脚之间的内容。插件接收主题令牌、当前实体引用和导航方法,使用 React Native 绘制自己的界面。安装[侧边栏视图示例](https://github.com/getpaseo/paseo/tree/main/plugin-examples/sidebar-views),体验项目列表、按任务分组的 Agent、嵌套 Agent 和标签文件夹。
|
||||
|
||||
## 注册视图 {#register-a-view}
|
||||
|
||||
```tsx
|
||||
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 });
|
||||
}
|
||||
```
|
||||
|
||||
注册后,用户可以在**设置 → 侧边栏 → 侧边栏视图**中选择该视图。安装插件不会切换当前视图。页眉导航、插件页眉项、提示区域和页脚保持可见。所选视图缺失或被禁用时,显示默认视图。视图抛出异常时,显示带有**使用默认视图**按钮的占位界面。
|
||||
|
||||
视图同时展示所有已连接主机的数据。`active.workspace` 和 `active.agent` 是引用或 `null`。将引用传给 `navigation.openWorkspace`、`openAgent`、`openTerminal` 或 `openProject`;`openProject` 打开新建工作区流程。导航会关闭紧凑布局的侧边栏。如果插件自行执行导航,完成后调用 `dismiss()`。
|
||||
|
||||
## 查询引用,逐行读取 {#query-refs-read-each-row}
|
||||
|
||||
`useWorkspaces`、`useAgents`、`useProjects` 和 `useTerminals` 返回不可变的引用数组。引用包含 `kind`、`hostId`、`id` 和跨主机唯一的 `key`。在一次应用会话中,同一实体使用同一个冻结引用。在行组件内将它传给 `useWorkspace`、`useAgent`、`useProject` 或 `useTerminal`。实体删除后返回 `null`。
|
||||
|
||||
```tsx
|
||||
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.
|
||||
});
|
||||
```
|
||||
|
||||
每个查询都支持 `hosts`(服务器 ID)、`where`、`orderBy` 和 `groupBy`。省略 `hosts` 时查询所有已连接主机。`orderBy` 接受快照字段、`[field, "asc" | "desc"]` 或比较函数。空值排在最后。比较函数会在数据变化时执行,应保持开销较小。
|
||||
|
||||
`groupBy` 返回字符串、`{ key, title?, color? }` 或这些值组成的数组。分组结果包含 `key`、`title`、`color` 和 `items`。分组按排序后首次出现的顺序排列;重复的键使用第一个标题。返回空数组表示该项不属于任何分组。
|
||||
|
||||
引用序列不变时,数组对象保持不变。分组的元数据和项序列不变时,分组对象保持不变。工作区状态变化只更新对应行,不会重新渲染整个列表。分组变化会更新受影响的分组。查询对象和函数可以内联编写,无需自行管理订阅。
|
||||
|
||||
实体钩子支持选择器和可选的相等比较函数:
|
||||
|
||||
```tsx
|
||||
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);
|
||||
```
|
||||
|
||||
没有选择器时,自定义相等比较函数接收快照;有选择器时接收选择结果。现有工作区面板仍可向 `useWorkspace` 和 `useAgent` 传入主机本地的字符串 ID。跨主机视图应使用引用。`useHosts()` 保留现有的 `serverId`、`label` 和连接 `status` 字段。
|
||||
|
||||
工作区快照包含 `project`、`pinnedAt`、`labels`、`background`、`branch`、`lastPromptAt`、`parentAgent` 和 `createdAt`(旧主机上为 null)。使用 `indicator` 和本地化的 `indicatorLabel` 显示 Paseo 的状态。`status` 保持 daemon 原始状态。Agent 快照包含工作区和父 Agent 引用。项目快照提供 `displayName`、`rootPath`、`kind` 和 `iconRevision`。
|
||||
|
||||
## 列出终端 {#list-terminals}
|
||||
|
||||
终端示例支持选择主机、列出终端、聚焦终端标签页,以及通过句柄关闭终端。`useTerminals({ hosts: [hostId] })` 要求主机声明 `terminalCollection` 能力。查询不支持该能力的主机时,终端读取会抛出要求更新主机的错误;其他实体钩子继续工作。将主机选择器放在错误边界之外,方便用户切换主机。省略 `hosts` 时,所有已连接主机都必须支持该能力。
|
||||
|
||||
终端快照包含 `workspace`、`title`、`isScript` 和 `createdAt`。在时间戳跟踪引入前启动的 worker 所保留的终端,其 `createdAt` 为 null。脚本归属来自工作区脚本运行时条目。主机观察跟踪终端创建、关闭、标题变化和重新连接,并在最后一个终端读取订阅者离开时释放。
|
||||
|
||||
## 通过引用执行操作 {#act-on-a-ref}
|
||||
|
||||
`usePaseo()` 将基于引用的操作发送到引用所属的主机:
|
||||
|
||||
```tsx
|
||||
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();
|
||||
```
|
||||
|
||||
工作区和 Agent 句柄保留现有方法。分配新标签会创建该标签;分配已有标签会保留其颜色。标签文件夹示例先添加目标标签,再移除源标签,因此分配失败时仍保留原文件夹。插件负责移动手势和错误显示。
|
||||
|
||||
## 在设备上保存布局选择 {#keep-layout-choices-on-the-device}
|
||||
|
||||
定义 `scope: "device"` 的[设置文档](reference.md#settings-screens),保存折叠文件夹或手动排序。`useSettings` 通过与主机设置相同的修订版本 API 读取和写入。设备文档属于插件安装和设置 ID,在应用重启后保留,并由该安装的客户端界面共享。它们不会同步到其他设备。需要由客户端通过 daemon 共享的数据使用 `scope: "host"`。
|
||||
|
||||
## 匹配应用的信息密度 {#match-the-apps-density}
|
||||
|
||||
使用 React Native 原语绘制界面。SDK 不导出侧边栏视图的行、分区、列表、徽标或拖拽辅助组件。现有页眉和页脚组件包属于独立界面。
|
||||
|
||||
紧凑行的最小高度为 28,水平内边距为 12;主标签字号为 12、行高为 16,次标签字号为 11。使用以下主题令牌:
|
||||
|
||||
| 行元素 | 主题令牌 |
|
||||
| ---------------- | ------------------------ |
|
||||
| 主标签 | `colors.foreground` |
|
||||
| 次标签、分区标题 | `colors.foregroundMuted` |
|
||||
| 选中行背景 | `colors.surface2` |
|
||||
| 分隔线 | `colors.border` |
|
||||
| 操作或焦点强调色 | `colors.accent` |
|
||||
| 成功指示 | `colors.statusSuccess` |
|
||||
| 需要输入指示 | `colors.statusWarning` |
|
||||
| 失败指示 | `colors.statusDanger` |
|
||||
|
||||
为可点击行提供无障碍标签,并在触摸布局中显示操作控件。示例仅使用插件自己的组件和传给视图的主题令牌。
|
||||
Reference in New Issue
Block a user