mirror of
https://github.com/getpaseo/paseo.git
synced 2026-11-08 09:24:41 +00:00
Merge remote-tracking branch 'origin/main' into diagnose/3589-contributor-6b87ee9f
This commit is contained in:
@@ -588,7 +588,7 @@ server.addHook("agent.permission.after", async ({ agent, requestId, resolution }
|
||||
});
|
||||
```
|
||||
|
||||
Agent hooks exclude the daemon's private disposable helpers. Public background workspaces and their ordinary agents use the same hooks.
|
||||
Agent hooks exclude the daemon's private disposable helpers. Background agents and the agents in background workspaces use the same hooks.
|
||||
|
||||
## Ordering and returned values
|
||||
|
||||
@@ -598,7 +598,7 @@ Creation request
|
||||
→ resolve defaults and validate provider configuration
|
||||
→ derive launch configuration with Paseo runtime tools and daemon prompt
|
||||
→ agent.open.before hooks (same ordering; env and plugin-local tools and MCP servers)
|
||||
→ set PASEO_AGENT_ID and PASEO_AGENT_CWD
|
||||
→ set PASEO_AGENT_ID, PASEO_AGENT_CWD, and PASEO_WORKSPACE_ID when the agent has a workspace
|
||||
→ open provider session and save agent configuration
|
||||
```
|
||||
|
||||
|
||||
@@ -571,7 +571,7 @@ server.addHook("agent.permission.after", async ({ agent, requestId, resolution }
|
||||
});
|
||||
```
|
||||
|
||||
Agent 钩子不包括 daemon 的私有临时辅助 Agent。公开的后台工作区及其普通 Agent 使用相同钩子。
|
||||
Agent 钩子不包括 daemon 的私有临时辅助 Agent。后台 Agent 和后台工作区中的 Agent 使用相同钩子。
|
||||
|
||||
## 顺序与返回值 {#ordering-and-returned-values}
|
||||
|
||||
@@ -581,7 +581,7 @@ Creation request
|
||||
→ resolve defaults and validate provider configuration
|
||||
→ derive launch configuration with Paseo runtime tools and daemon prompt
|
||||
→ agent.open.before hooks (same ordering; env and plugin-local tools and MCP servers)
|
||||
→ set PASEO_AGENT_ID and PASEO_AGENT_CWD
|
||||
→ set PASEO_AGENT_ID, PASEO_AGENT_CWD, and PASEO_WORKSPACE_ID when the agent has a workspace
|
||||
→ open provider session and save agent configuration
|
||||
```
|
||||
|
||||
|
||||
@@ -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` |
|
||||
@@ -144,8 +145,8 @@ pill.remove();
|
||||
```
|
||||
|
||||
Move dynamic text from the former component into updates from your model or SDK subscription.
|
||||
Custom icon components may still use hooks. Composer pills always show icon and label and never
|
||||
show chevrons. See [buttons](./reference.md#button-descriptor) for menus, popovers, and visibility.
|
||||
Custom icon components may still use hooks. Composer pills always show icon and label; menus and
|
||||
popovers add a chevron. See [buttons](./reference.md#button-descriptor) for menus, popovers, and visibility.
|
||||
|
||||
## 4. Separate imports
|
||||
|
||||
|
||||
@@ -287,6 +287,11 @@ Paseo refreshes an agent by closing its current provider session and opening it
|
||||
configuration and persistence. Re-read credentials, environment, global configuration, and MCP
|
||||
servers during `session.open`. There is no separate reload operation.
|
||||
|
||||
Conversation and combined rewind require a core-restored session with persistence. After a
|
||||
successful rewind, Paseo closes the session and reopens its latest persistence value with
|
||||
`history: "replay"`. Publish the retained native branch before `session.ready`, including its
|
||||
children. File-only rewind keeps the current session and conversation history.
|
||||
|
||||
Use `restoration: "core"` when Paseo can reopen a session from persistence. A provider-owned child
|
||||
uses `restoration: "parent"`; emit it as another `session.opened` event with `parentSessionId` and
|
||||
recreate it while restoring the parent.
|
||||
|
||||
@@ -73,16 +73,16 @@ The required root manifest is `paseo-plugin.json`:
|
||||
}
|
||||
```
|
||||
|
||||
| Field | Required | Behavior |
|
||||
| -------------- | -------- | -------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| `id` | Yes | Default installation ID. |
|
||||
| `name` | No | Non-empty display name for the registry and website; independent of the installation ID. |
|
||||
| `icon` | No | Relative path to a PNG inside the plugin package. |
|
||||
| `media` | No | Array of image or video paths inside the package, or HTTPS URLs. An empty array is valid. |
|
||||
| `description` | No | Non-empty summary shown below the plugin ID in **Settings → Plugins**. |
|
||||
| `translations` | No | Listing text in other languages, read by the registry and ignored by Paseo. See [other languages](/docs/plugins/publishing#other-languages). |
|
||||
| `requirements` | No | Supported Paseo versions, described below. |
|
||||
| `build` | No | Preparation commands, described in the CLI reference. |
|
||||
| Field | Required | Behavior |
|
||||
| -------------- | -------- | --------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| `id` | Yes | Default installation ID. |
|
||||
| `name` | No | Non-empty display name for the registry, the website, your row in **Settings → Plugins**, and your settings header. Without it, Paseo shows the ID. |
|
||||
| `icon` | No | Relative path to a PNG inside the plugin package. |
|
||||
| `media` | No | Array of image or video paths inside the package, or HTTPS URLs. An empty array is valid. |
|
||||
| `description` | No | Non-empty summary shown below the plugin name in **Settings → Plugins**. |
|
||||
| `translations` | No | Listing text in other languages, read by the registry and ignored by Paseo. See [other languages](/docs/plugins/publishing#other-languages). |
|
||||
| `requirements` | No | Supported Paseo versions, described below. |
|
||||
| `build` | No | Preparation commands, described in the CLI reference. |
|
||||
|
||||
Paths are relative to `paseo-plugin.json`, use forward slashes, and cannot contain `..`
|
||||
segments. Registry listings for visual plugins require an interface image even though `media` is
|
||||
@@ -641,6 +641,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
|
||||
@@ -1309,8 +1316,11 @@ Themes need a host that supports them. A client released before `addTheme` canno
|
||||
## Settings screens
|
||||
|
||||
Register a component with `client.addSettingsScreen({ id, title, icon, Component })` in
|
||||
`index.client.tsx`. It appears under **Settings → Plugins → your plugin** on that host.
|
||||
`id` is unique within the installation; `icon` is any [icon](#icons) form. Registration returns an
|
||||
`index.client.tsx`. Your plugin's row in **Settings → Plugins** on that host then shows a settings
|
||||
button (cog) that opens your first screen. When you register several screens, the settings page shows
|
||||
one tab per screen, labeled with its `title` and `icon`, in registration order. With one screen there
|
||||
are no tabs. The page header shows your manifest `name`, or your plugin ID when you leave `name`
|
||||
out; it never includes a screen title. `id` is unique within the installation; `icon` is any [icon](#icons) form. Registration returns an
|
||||
idempotent remover, and plugin teardown removes remaining screens.
|
||||
|
||||
Call `client.openSettings(id)` or a Command Center callback's `openSettings(id)` to open one
|
||||
@@ -1391,13 +1401,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:
|
||||
|
||||
@@ -1904,7 +1914,10 @@ decisions.
|
||||
`client.addComposerPill({ id, workspaceId, agentId, button })` uses the same [button descriptor](#button-descriptor)
|
||||
and returns the same registration. It targets one agent's composer track alongside Tasks and
|
||||
Subagents. Composer pills always show the icon and `label` (or `title` when `label` is omitted).
|
||||
They never show a chevron, including for menus and popovers.
|
||||
Menus and popovers add an upward chevron on every layout, because their surface opens above the
|
||||
pill; actions show none, so a pill that runs on tap reads differently from one that opens something.
|
||||
The track keeps its space above the composer whether or not it has pills, so adding and removing
|
||||
pills does not move the transcript.
|
||||
|
||||
```tsx
|
||||
const pill = client.addComposerPill({
|
||||
@@ -2085,6 +2098,8 @@ function PullRequestAction({ theme }: PluginSurfaceProps) {
|
||||
|
||||
The returned API covers projects, workspaces, agents, terminals, providers, and daemon config. See the [SDK API reference](/docs/sdk/reference) for its methods. Connection lifecycle methods are intentionally absent because Paseo owns the connection.
|
||||
|
||||
To run a helper the user shouldn't see, such as a classifier or summarizer, create it as a [background agent](/docs/sdk/agents#run-a-helper-as-a-background-agent) in an existing workspace.
|
||||
|
||||
### Discover hosts and target another host
|
||||
|
||||
Use `useHosts()` to display configured hosts and `getPaseoClient(serverId)` in an action callback
|
||||
@@ -2229,10 +2244,168 @@ export default function contribute(server: PluginServerContext) {
|
||||
}
|
||||
```
|
||||
|
||||
Inputs and outputs are validated on both sides. RPC names start with a lowercase letter and contain lowercase letters, numbers, dots, hyphens, or underscores. `useRpc()` returns a typed async function. Use TanStack Query for request state, caching, and mutations.
|
||||
Inputs are parsed once in the plugin process; outputs are parsed once on the client. Send schema inputs from the client and return output-schema inputs from the handler; schema transforms run at the receiving side. RPC names start with a lowercase letter and contain lowercase letters, numbers, dots, hyphens, or underscores. `useRpc()` returns a typed async function. Use TanStack Query for request state, caching, and mutations.
|
||||
|
||||
Backend handlers receive the same `PaseoApi` as `{ paseo }`. Their connection belongs to the subprocess and closes when the plugin stops. It does not subscribe to timelines or catalog events until plugin code subscribes. Follow the [SDK event contract](/docs/sdk/events) for cleanup and timeline replacements. Backend code can use Node APIs and dependencies installed in the plugin directory.
|
||||
|
||||
## Stream events from the daemon {#subscriptions}
|
||||
|
||||
A subscription runs a daemon handler for as long as one client listens and sends that client typed
|
||||
events. Use it for watchers, tails, and live media; use plugin RPC for a single answer.
|
||||
|
||||
Define the contract in `shared/`:
|
||||
|
||||
```ts
|
||||
// shared/diff.ts
|
||||
import { defineSubscription } from "@getpaseo/plugin";
|
||||
import { z } from "zod";
|
||||
|
||||
export const diffWatch = defineSubscription({
|
||||
name: "diff.watch",
|
||||
input: z.object({ cwd: z.string(), from: z.string() }),
|
||||
event: z.object({ type: z.literal("changes"), files: z.array(z.string()) }),
|
||||
});
|
||||
```
|
||||
|
||||
Handle it in `index.server.ts`. The handler receives `push` and an abort `signal` next to `paseo`:
|
||||
|
||||
```ts
|
||||
server.handle(diffWatch, ({ cwd, from }, { push, signal }) => {
|
||||
const watcher = watchChanges(cwd, from, (files) => void push({ type: "changes", files }));
|
||||
signal.addEventListener("abort", () => watcher.close(), { once: true });
|
||||
});
|
||||
```
|
||||
|
||||
Subscribe from a surface with `useSubscription(contract, input, onEvent)`:
|
||||
|
||||
```tsx
|
||||
const [files, setFiles] = useState<string[]>([]);
|
||||
const subscription = useSubscription(diffWatch, { cwd, from: "HEAD" }, (event) => {
|
||||
setFiles(event.files);
|
||||
});
|
||||
if (subscription.status === "error") return <Text>{subscription.error.message}</Text>;
|
||||
```
|
||||
|
||||
Outside React, `client.subscribe` takes the same arguments plus options and returns a remover:
|
||||
|
||||
```ts
|
||||
const stop = client.subscribe(diffWatch, { cwd, from: "HEAD" }, onChanges, { signal });
|
||||
await stop.ready; // the daemon accepted the subscription
|
||||
stop(); // or await stop.release()
|
||||
```
|
||||
|
||||
RPCs and subscriptions share one name space; registering the same name for both fails the plugin
|
||||
load.
|
||||
|
||||
### Handler lifetime
|
||||
|
||||
| The handler... | Result |
|
||||
| --------------------------------------------- | ------------------------------------------------------------ |
|
||||
| Throws synchronously, or the input is invalid | The subscribe call fails with the error. No events are sent. |
|
||||
| Returns nothing | The subscription stays open until the client releases it. |
|
||||
| Returns a promise that resolves | The subscription closes. |
|
||||
| Returns a promise that rejects | The subscription closes with the error. |
|
||||
|
||||
`signal` aborts when the client releases the subscription, unmounts the hook, changes its input,
|
||||
disconnects, or the plugin stops. Pushes after the abort are dropped. `push` validates the event
|
||||
against the contract and resolves once the event has left the plugin process; await it in a loop
|
||||
to pace a producer.
|
||||
|
||||
Each subscriber runs its own handler. Paseo does not share one handler between clients and does
|
||||
not replay missed events: after a reconnect or a plugin reload, mounted surfaces subscribe again and
|
||||
the handler starts from the beginning. Share an
|
||||
expensive watcher inside your plugin when several subscribers need it.
|
||||
|
||||
`useSubscription` returns a union on `status`:
|
||||
|
||||
| Status | Meaning |
|
||||
| ------------ | ---------------------------------------------------------------------------------------------- |
|
||||
| `connecting` | Sent; the daemon has not accepted it yet. |
|
||||
| `live` | The handler is running. Events arrive through `onEvent`. |
|
||||
| `closed` | The handler finished. |
|
||||
| `error` | The subscribe failed, the handler failed, an event failed the contract, or the plugin stopped. |
|
||||
|
||||
The hook subscribes again when `input` changes by shallow equality. `onEvent` is always the latest
|
||||
render's callback, so an inline function does not resubscribe. `client.subscribe` takes
|
||||
`{ signal, onClosed }`; `onClosed` receives `{ error? }`.
|
||||
|
||||
### Bytes
|
||||
|
||||
A top-level `Uint8Array` crosses the wire as binary, without JSON or base64. Everything else is
|
||||
JSON. The rule applies to subscription events, RPC outputs, and RPC inputs. A Node `Buffer` is a
|
||||
`Uint8Array`.
|
||||
|
||||
```ts
|
||||
export const thumbnail = defineRpc({
|
||||
name: "thumbnail.render",
|
||||
input: z.object({ path: z.string(), width: z.number() }),
|
||||
output: z.instanceof(Uint8Array),
|
||||
});
|
||||
|
||||
// index.server.ts
|
||||
server.handle(thumbnail, ({ path, width }) => renderPng(path, width));
|
||||
|
||||
// client
|
||||
const renderThumbnail = useRpc(thumbnail);
|
||||
const png = await renderThumbnail({ path, width }); // Uint8Array
|
||||
```
|
||||
|
||||
Declare bytes with `z.instanceof(Uint8Array)`. An event can be bytes or JSON per push: declare a
|
||||
union and branch with `event instanceof Uint8Array`. A `Uint8Array` nested in an object fails
|
||||
validation on the sending side with an error that names the rule; send the bytes as their own
|
||||
event or RPC value and the metadata as a separate JSON event.
|
||||
|
||||
One event or RPC value carries at most 100 MiB less 65 KiB of framing. Push large payloads in
|
||||
chunks of a few hundred KiB; each `push` waits for the previous one to leave the plugin process.
|
||||
Uploads from a phone have a lower limit: Android closes the connection when one send exceeds
|
||||
16 MiB, so split a larger upload into several RPCs.
|
||||
|
||||
### Several streams on one subscription
|
||||
|
||||
Put a header byte in front of each payload to multiplex streams. Keep the layout in `shared/`, so
|
||||
the handler and the surface call named functions:
|
||||
|
||||
```ts
|
||||
// shared/media-frames.ts
|
||||
export const MediaOpcode = { Video: 0x01, Audio: 0x02 } as const;
|
||||
|
||||
export function encodeMediaFrame(opcode: number, payload: Uint8Array): Uint8Array {
|
||||
const bytes = new Uint8Array(1 + payload.byteLength);
|
||||
bytes[0] = opcode;
|
||||
bytes.set(payload, 1);
|
||||
return bytes;
|
||||
}
|
||||
|
||||
export function decodeMediaFrame(bytes: Uint8Array) {
|
||||
return { opcode: bytes[0], payload: bytes.subarray(1) }; // a view, not a copy
|
||||
}
|
||||
|
||||
export const mediaStream = defineSubscription({
|
||||
name: "media.stream",
|
||||
input: z.object({ source: z.string() }),
|
||||
event: z.instanceof(Uint8Array),
|
||||
});
|
||||
```
|
||||
|
||||
```ts
|
||||
useSubscription(mediaStream, { source }, (bytes) => {
|
||||
const frame = decodeMediaFrame(bytes);
|
||||
if (frame.opcode === MediaOpcode.Audio) audio.enqueue(frame.payload);
|
||||
else video.draw(frame.payload);
|
||||
});
|
||||
```
|
||||
|
||||
Events from one subscription arrive in push order, JSON and binary alike.
|
||||
`plugin-examples/media-stream` streams video and audio this way and uploads a frame back through
|
||||
an RPC with a `Uint8Array` input; `plugin-examples/diff-watch` watches a working tree.
|
||||
|
||||
### Older hosts
|
||||
|
||||
Subscriptions and binary RPC values need a host that advertises them. Against an older host,
|
||||
`useSubscription` reports `error` asking to update the host, `client.subscribe` rejects `ready`
|
||||
with the same message, and an RPC with a `Uint8Array` input or output fails. Raise your plugin's
|
||||
`requirements.paseo` to the release that ships subscriptions.
|
||||
|
||||
## Debug backend output
|
||||
|
||||
Backend contributions can write to stdout and stderr with normal Node logging:
|
||||
|
||||
@@ -73,10 +73,10 @@ my-plugin/
|
||||
| 字段 | 是否必填 | 行为 |
|
||||
| -------------- | -------- | ---------------------------------------------------------------------------------------------------------- |
|
||||
| `id` | 是 | 默认的安装 ID。 |
|
||||
| `name` | 否 | 注册表和网站使用的非空显示名称;与安装 ID 无关。 |
|
||||
| `name` | 否 | 注册表、网站、**设置 → 插件** 中的插件行以及插件设置页标题使用的非空显示名称。没有 `name` 时显示插件 ID。 |
|
||||
| `icon` | 否 | 插件包内 PNG 文件的相对路径。 |
|
||||
| `media` | 否 | 包内图片或视频路径的数组,或 HTTPS URL。空数组是有效的。 |
|
||||
| `description` | 否 | 在 **设置 → 插件** 中显示在插件 ID 下方的非空摘要。 |
|
||||
| `description` | 否 | 在 **设置 → 插件** 中显示在插件名称下方的非空摘要。 |
|
||||
| `translations` | 否 | 其他语言的展示文本,由注册表读取,Paseo 会忽略它。见[其他语言](/docs/plugins/publishing#other-languages)。 |
|
||||
| `requirements` | 否 | 支持的 Paseo 版本,见下文。 |
|
||||
| `build` | 否 | 准备命令,见 CLI 参考。 |
|
||||
@@ -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 在侧边栏顶部或底部渲染的组件;按下它时做什么由它决定。先注册界面,再注册一个打开它的项:
|
||||
@@ -1087,7 +1091,7 @@ export default function contribute(client: PluginClientContext) {
|
||||
|
||||
## 设置界面 {#settings-screens}
|
||||
|
||||
在 `index.client.tsx` 中用 `client.addSettingsScreen({ id, title, icon, Component })` 注册一个组件。它会出现在该主机的 **设置 → 插件 → 你的插件** 下。`id` 在同一次安装中唯一;`icon` 可以是任意一种[图标](#icons)形式。注册返回一个幂等的移除函数,插件销毁时会移除剩余的界面。
|
||||
在 `index.client.tsx` 中用 `client.addSettingsScreen({ id, title, icon, Component })` 注册一个组件。之后,该主机 **设置 → 插件** 中你的插件行会显示一个设置按钮(齿轮),点击后打开你的第一个界面。注册多个界面时,设置页面会按注册顺序为每个界面显示一个标签页,使用其 `title` 和 `icon`;只有一个界面时不显示标签页。页面标题显示 manifest 中的 `name`;没有 `name` 时显示插件 ID,且从不包含界面标题。`id` 在同一次安装中唯一;`icon` 可以是任意一种[图标](#icons)形式。注册返回一个幂等的移除函数,插件销毁时会移除剩余的界面。
|
||||
|
||||
调用 `client.openSettings(id)` 或 Command Center 回调中的 `openSettings(id)` 可以打开你自己的某个界面。即使多台主机安装了同一个插件,每次安装也有各自的值和路由。
|
||||
|
||||
@@ -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)`。它返回一个可区分的状态:
|
||||
|
||||
@@ -1779,6 +1783,8 @@ function PullRequestAction({ theme }: PluginSurfaceProps) {
|
||||
|
||||
返回的 API 涵盖项目、工作区、Agent、终端、Provider 和 daemon 配置。其方法请参阅 [SDK API 参考](/docs/sdk/reference)。这里刻意没有连接生命周期方法,因为连接由 Paseo 负责。
|
||||
|
||||
如果要运行用户不需要看到的辅助任务,例如分类或摘要,请在已有工作区中把它创建为[后台 Agent](/docs/sdk/agents#run-a-helper-as-a-background-agent)。
|
||||
|
||||
### 发现主机并指定另一台主机 {#discover-hosts-and-target-another-host}
|
||||
|
||||
使用 `useHosts()` 显示已配置的主机,并在操作回调中使用 `getPaseoClient(serverId)` 对其中一台主机
|
||||
@@ -1924,6 +1930,140 @@ export default function contribute(server: PluginServerContext) {
|
||||
|
||||
后端处理函数以 `{ paseo }` 的形式收到同样的 `PaseoApi`。它们的连接属于子进程,并在插件停止时关闭。在插件代码订阅之前,它不会订阅时间线或目录事件。清理和时间线替换请遵循 [SDK 事件契约](/docs/sdk/events)。后端代码可以使用 Node API 以及安装在插件目录中的依赖。
|
||||
|
||||
## 从 daemon 推送事件 {#subscriptions}
|
||||
|
||||
订阅会在一个客户端监听期间运行 daemon 处理函数,并向该客户端发送带类型的事件。监听文件、追踪日志和实时媒体使用订阅;只需要一次应答时使用插件 RPC。
|
||||
|
||||
在 `shared/` 中定义契约:
|
||||
|
||||
```ts
|
||||
// shared/diff.ts
|
||||
import { defineSubscription } from "@getpaseo/plugin";
|
||||
import { z } from "zod";
|
||||
|
||||
export const diffWatch = defineSubscription({
|
||||
name: "diff.watch",
|
||||
input: z.object({ cwd: z.string(), from: z.string() }),
|
||||
event: z.object({ type: z.literal("changes"), files: z.array(z.string()) }),
|
||||
});
|
||||
```
|
||||
|
||||
在 `index.server.ts` 中处理它。处理函数除了 `paseo`,还会收到 `push` 和一个中止 `signal`:
|
||||
|
||||
```ts
|
||||
server.handle(diffWatch, ({ cwd, from }, { push, signal }) => {
|
||||
const watcher = watchChanges(cwd, from, (files) => void push({ type: "changes", files }));
|
||||
signal.addEventListener("abort", () => watcher.close(), { once: true });
|
||||
});
|
||||
```
|
||||
|
||||
在界面中用 `useSubscription(contract, input, onEvent)` 订阅:
|
||||
|
||||
```tsx
|
||||
const [files, setFiles] = useState<string[]>([]);
|
||||
const subscription = useSubscription(diffWatch, { cwd, from: "HEAD" }, (event) => {
|
||||
setFiles(event.files);
|
||||
});
|
||||
if (subscription.status === "error") return <Text>{subscription.error.message}</Text>;
|
||||
```
|
||||
|
||||
在 React 之外,`client.subscribe` 接受同样的参数和一个选项对象,并返回一个移除函数:
|
||||
|
||||
```ts
|
||||
const stop = client.subscribe(diffWatch, { cwd, from: "HEAD" }, onChanges, { signal });
|
||||
await stop.ready; // the daemon accepted the subscription
|
||||
stop(); // or await stop.release()
|
||||
```
|
||||
|
||||
RPC 和订阅共用一个名称空间;同一个名称同时注册为 RPC 和订阅会导致插件加载失败。
|
||||
|
||||
### 处理函数的生命周期 {#handler-lifetime}
|
||||
|
||||
| 处理函数…… | 结果 |
|
||||
| ------------------------ | -------------------------------------------- |
|
||||
| 同步抛出错误,或输入无效 | 订阅调用失败并返回该错误,不会发送任何事件。 |
|
||||
| 不返回任何值 | 订阅保持打开,直到客户端释放它。 |
|
||||
| 返回一个兑现的 Promise | 订阅关闭。 |
|
||||
| 返回一个拒绝的 Promise | 订阅关闭并带上该错误。 |
|
||||
|
||||
当客户端释放订阅、卸载 hook、更改输入、断开连接,或者插件停止时,`signal` 会中止。中止之后的推送会被丢弃。`push` 会按契约校验事件,并在事件离开插件进程后兑现;在循环中 await 它可以控制生产者的速度。
|
||||
|
||||
每个订阅者运行各自的处理函数。Paseo 不会在多个客户端之间共享同一个处理函数,也不会重放错过的事件:重新连接或重新加载插件之后,已挂载的界面会重新订阅,处理函数从头开始运行。多个订阅者需要同一个昂贵的监听器时,请在插件内部共享它。
|
||||
|
||||
`useSubscription` 返回一个以 `status` 区分的联合类型:
|
||||
|
||||
| 状态 | 含义 |
|
||||
| ------------ | -------------------------------------------------------- |
|
||||
| `connecting` | 已发送,daemon 尚未接受。 |
|
||||
| `live` | 处理函数正在运行,事件通过 `onEvent` 送达。 |
|
||||
| `closed` | 处理函数已结束。 |
|
||||
| `error` | 订阅失败、处理函数失败、事件不符合契约,或者插件已停止。 |
|
||||
|
||||
`input` 按浅比较发生变化时,hook 会重新订阅。`onEvent` 始终是最近一次渲染的回调,所以内联函数不会导致重新订阅。`client.subscribe` 接受 `{ signal, onClosed }`;`onClosed` 会收到 `{ error? }`。
|
||||
|
||||
### 字节 {#bytes}
|
||||
|
||||
顶层的 `Uint8Array` 以二进制形式传输,不经过 JSON 或 base64,其他值都是 JSON。这条规则适用于订阅事件、RPC 输出和 RPC 输入。Node 的 `Buffer` 也是 `Uint8Array`。
|
||||
|
||||
```ts
|
||||
export const thumbnail = defineRpc({
|
||||
name: "thumbnail.render",
|
||||
input: z.object({ path: z.string(), width: z.number() }),
|
||||
output: z.instanceof(Uint8Array),
|
||||
});
|
||||
|
||||
// index.server.ts
|
||||
server.handle(thumbnail, ({ path, width }) => renderPng(path, width));
|
||||
|
||||
// client
|
||||
const renderThumbnail = useRpc(thumbnail);
|
||||
const png = await renderThumbnail({ path, width }); // Uint8Array
|
||||
```
|
||||
|
||||
用 `z.instanceof(Uint8Array)` 声明字节。事件可以在每次推送时选择字节或 JSON:声明一个联合类型,并用 `event instanceof Uint8Array` 分支。嵌套在对象中的 `Uint8Array` 会在发送端校验失败,错误信息会说明这条规则;把字节作为单独的事件或 RPC 值发送,元数据作为另一个 JSON 事件发送。
|
||||
|
||||
一个事件或 RPC 值最多 100 MiB(减去 65 KiB 的帧头)。大载荷请按几百 KiB 分块推送;每次 `push` 都会等待上一次离开插件进程。手机上传的上限更低:一次发送超过 16 MiB 时 Android 会关闭连接,所以较大的上传请拆成多个 RPC。
|
||||
|
||||
### 在一个订阅中传输多条流 {#several-streams-on-one-subscription}
|
||||
|
||||
在每个载荷前加一个头字节即可复用多条流。把布局放在 `shared/` 中,让处理函数和界面调用有名字的函数:
|
||||
|
||||
```ts
|
||||
// shared/media-frames.ts
|
||||
export const MediaOpcode = { Video: 0x01, Audio: 0x02 } as const;
|
||||
|
||||
export function encodeMediaFrame(opcode: number, payload: Uint8Array): Uint8Array {
|
||||
const bytes = new Uint8Array(1 + payload.byteLength);
|
||||
bytes[0] = opcode;
|
||||
bytes.set(payload, 1);
|
||||
return bytes;
|
||||
}
|
||||
|
||||
export function decodeMediaFrame(bytes: Uint8Array) {
|
||||
return { opcode: bytes[0], payload: bytes.subarray(1) }; // a view, not a copy
|
||||
}
|
||||
|
||||
export const mediaStream = defineSubscription({
|
||||
name: "media.stream",
|
||||
input: z.object({ source: z.string() }),
|
||||
event: z.instanceof(Uint8Array),
|
||||
});
|
||||
```
|
||||
|
||||
```ts
|
||||
useSubscription(mediaStream, { source }, (bytes) => {
|
||||
const frame = decodeMediaFrame(bytes);
|
||||
if (frame.opcode === MediaOpcode.Audio) audio.enqueue(frame.payload);
|
||||
else video.draw(frame.payload);
|
||||
});
|
||||
```
|
||||
|
||||
同一个订阅的事件按推送顺序到达,JSON 和二进制都一样。`plugin-examples/media-stream` 用这种方式传输视频和音频,并通过一个输入为 `Uint8Array` 的 RPC 把一帧上传回去;`plugin-examples/diff-watch` 监听工作树。
|
||||
|
||||
### 旧版主机 {#older-hosts}
|
||||
|
||||
订阅和二进制 RPC 值需要主机声明支持。连接旧版主机时,`useSubscription` 会报告 `error` 并提示更新主机,`client.subscribe` 的 `ready` 会以同样的信息拒绝,带有 `Uint8Array` 输入或输出的 RPC 会失败。请把插件的 `requirements.paseo` 提高到包含订阅功能的版本。
|
||||
|
||||
## 调试后端输出 {#debug-backend-output}
|
||||
|
||||
后端贡献可以用普通的 Node 日志写入 stdout 和 stderr:
|
||||
|
||||
@@ -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