Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions docs/architecture.md
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,8 @@ Extensions never see `PocketApp`. They get `PocketHost` (`host.ts`), whose funct

Subagents leave Agent Teams out of every subagent (`extensions/subagents.ts`), even while it is off, so turning it on later gives no subagent a way to start sessions.

Enabled extensions may additionally register validated, data-only `uiLocales`; authenticated `hello` messages carry those packs to the browser. The client translates explicit UI strings, falling back to English for unknown keys. This does not translate user content or change server behavior.

## PiP's conversation

PiP (`pip.ts`) is a conversation with a `pip` mark in its session entry, which `sessions()` leaves out of every list. The harness settings select every installed extension but PiP's (`pocket-pip`) by default; PiP's conversation stores its own selection (`PIP_EXTENSIONS`, without bash, write, or edit), which `ensure` sets again each time it opens, so PiP's tools reach no other conversation. Its tools act through `Commands` and `Schedules` as PiP's person, with that person's checks, so `canSee` keeps every PiP from everyone but its person: anyone who could write to it would act as them. Its messages to sessions are once per tool call (a request id hashed from the call's key), and each follow leaves a mark in its list (`FollowsDoc`) when it reports or is stopped, so a replayed call neither sends nor follows twice. A tab's PiP panel is a second client in PiP's room (`setPip`), whose `view` events reach the tab as `pipview`; `watching` counts it, so PiP's answers push nothing while it shows.
Expand Down
99 changes: 99 additions & 0 deletions docs/examples/zh-cn-ui.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
/** A small data-only Simplified Chinese UI locale pack. It translates the fixed Pocket UI text that its own tests exercise; a complete dictionary belongs in a separate extension. */
import { defineExtension } from "@earendil-works/pi-durable";

// Keys are the exact English source strings used by t() at the UI call sites. A key that
// is absent here, and every user-written or model-written string, stays English.

const strings: Record<string, string> = {
Menu: "菜单",
Files: "文件",
Extensions: "扩展",
Appearance: "外观",
Discover: "发现",
"Installed extensions": "已安装扩展",
"Install theme": "安装主题",
"Install (stays off)": "安装(保持关闭)",
"Index settings and status": "索引设置与状态",
"Technical details": "技术详情",
Installed: "已安装",
"Installed at": "安装时间",
"Where these come from": "这些内容的来源",
"Extensions install turned off. Nothing runs until you turn it on.":
"安装后保持关闭,启用前不会运行。",
On: "开启",
Revoked: "已撤销",
"View source": "查看源码",
Update: "更新",
Updates: "更新",
"Reply speed": "回复速度",
"Status line": "状态栏",
"Mark unread": "标记为未读",
"Mark read": "标记为已读",
Select: "选择",
Sessions: "会话",
"New session": "新会话",
People: "成员",
Memory: "记忆",
Providers: "模型提供方",
Model: "模型",
Save: "保存",
Cancel: "取消",
Close: "关闭",
Search: "搜索",
Rename: "重命名",
Send: "发送",
Stop: "停止",
Copy: "复制",
Theme: "主题",
Edit: "编辑",
Done: "完成",
Loading: "加载中",
"No matches.": "没有匹配项。",
"Try again": "重试",
"Turn off": "关闭",
"Turn on": "开启",
"Sign out": "退出登录",
New: "新建",
Open: "打开",
Remove: "移除",
Add: "添加",
Back: "返回",
Name: "名称",
Details: "详情",
Install: "安装",
Uninstall: "卸载",
Running: "运行中",
Context: "上下文",
};

const templates: Record<string, string> = {
"{{count}} entries need a newer Pi Pocket.": "{{count}} 个条目需要更新版本的 Pi Pocket。",
"{{count}} entry needs a newer Pi Pocket.": "{{count}} 个条目需要更新版本的 Pi Pocket。",
"Update {{title}}": "更新 {{title}}",
"Source: {{title}}": "源码:{{title}}",
"{{rate}} tokens a second": "每秒 {{rate}} 个 token",
"its first token after {{seconds}}s": "首个 token 耗时 {{seconds}} 秒",
"in {{seconds}}s": "{{seconds}} 秒后",
"How fast the last answer came: {{detail}}": "上一条回复的生成速度:{{detail}}",
"Could not attach {{name}}: {{error}}": "无法附加 {{name}}:{{error}}",
"Back to {{tab}}": "返回{{tab}}",
"{{model}} (not signed in)": "{{model}}(未登录)",
"{{verb}} {{what}}. {{action}} to undo.": "{{verb}} {{what}}。{{action}}可撤销。",
"{{count}} allowed: {{models}}": "已允许 {{count}} 个:{{models}}",
"Back to sessions, {{count}} waiting for you": "返回会话列表,{{count}} 项待你处理",
};

export default function createZhCnUiLocale() {
return {
...defineExtension({ name: "zh-cn-ui" }),
uiLocales: [
{
locale: "zh-CN",
label: "简体中文",
default: true,
strings,
templates,
},
],
};
}
35 changes: 35 additions & 0 deletions docs/extensions.md
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,41 @@ These examples work. `npm test` runs them (`test/docs-examples.test.ts`):
- [examples/no-force-push.ts](examples/no-force-push.ts): a hook that stops some bash calls, and a prompt section that tells Pi so.
- [examples/read-page.ts](examples/read-page.ts): a tool that reads a public web page with `host.fetchPublic`, and fences its text with `host.text`.
- [examples/server-status.ts](examples/server-status.ts) with [examples/server-status.js](examples/server-status.js): a page on the rail, with a route that reads the machine's load and memory, and events that keep it live.
- [examples/zh-cn-ui.ts](examples/zh-cn-ui.ts): a data-only Simplified Chinese UI locale pack.

## UI locale packs (data only)

A module may return optional `uiLocales` data with its ordinary Pi Durable extensions. Locale packs translate fixed Pocket UI text; they do not translate user-authored text, model output, provider names, or arbitrary HTML. The module itself is still ordinary server code: it runs with the server's rights, while its locale data is validated and sent as data, not as browser JavaScript.

```typescript
import { defineExtension } from "@earendil-works/pi-durable";

export default function createUiLocale() {
return {
...defineExtension({ name: "example-ui-locale" }),
uiLocales: [
{
locale: "zh-CN",
label: "简体中文",
default: true,
strings: { Files: "文件" },
templates: {
"Back to sessions, {{count}} waiting for you":
"返回会话列表,{{count}} 项待你处理",
},
},
],
};
}
```

- Use canonical BCP 47 tags and exact English source strings as keys. `strings` is for text without parameters; `templates` is for source text with named `{{placeholders}}`.
- The browser translates only where its code explicitly calls `t("Files")` or `t("Back to sessions, {{count}} waiting for you", { count })`. A missing key stays English. A translated template must have the same placeholder names as its source; values are inserted as-is and are not translated again. This is not a page-wide translation pass.
- The loader validates the locale data and rejects an invalid pack or a locale tag already registered by another enabled module. Limits may change; use canonical tags and do not rely on undocumented size or entry limits.
- A pack marked `default: true` is preferred among packs that are already enabled; it does not turn a module on. Without an enabled pack, the UI uses English. There is no per-person language setting or saved locale choice.
- Packs reach signed-in clients in `hello.server.uiLocales` and the authenticated `/api/me` response. There is no public dictionary endpoint. The locale data itself adds no route or external fetch.

A module may also call `host.notice(level, message, conversationId, { key, params })` to give a notice an optional UI translation key and parameters. The original `message` remains in the server log and in the notice payload; the metadata is only for presentation.

## Rules that are easy to break

Expand Down
4 changes: 2 additions & 2 deletions docs/extensions/reference/host.md
Original file line number Diff line number Diff line change
Expand Up @@ -197,12 +197,12 @@ Calls `listener` each time the owner changes a spend limit. It returns a functio
### `notice`

```ts
notice(level: NoticeLevel, message: string, conversationId?: ConversationId): void;
notice(level: NoticeLevel, message: string, conversationId?: ConversationId, ui?: { key: string; params?: Record<string, string | number> }): void;
```

Since 0.1.0 · Stable

Reports something to the server's log and to the app. `level` is `"info"`, `"warning"`, or `"error"`. With `conversationId`, only the tabs of that session show the notice. Pi Pocket 0.13.1 added `conversationId`.
Reports something to the server's log and to the app. `level` is `"info"`, `"warning"`, or `"error"`. With `conversationId`, only the tabs of that session show the notice. The optional fourth argument adds a data-only UI translation key and parameters; the original message remains in logs and payloads. Pi Pocket 0.13.1 added `conversationId`; UI locale metadata is experimental.

### `text`

Expand Down
32 changes: 31 additions & 1 deletion docs/extensions/reference/types.md
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ The types file imports types from Pi Durable, Pi AI, and Preact by package name.
- [`Web`](web.md) and the types that it uses: `WebAccess`, `WebMethod`, `WebHandler`, and `WebRequest`.
- [`Command`, `CommandContext`, and `CommandResult`](command-context.md).
- [`Pocket`](pocket.md) and the types that it uses: `BrowserModule`, `Action`, the objects that you give (`RailSpec`, `MenuSpec`, `LauncherSpec`, `PageSpec`, `SheetSpec`, `ToolCallSpec`, `CallSummary`), `Added`, `PageProps`, `SheetProps`, and the props of the pieces.
- `Module`, `Person`, `Role`, and `NoticeLevel`, on this page.
- `Module`, `PocketExtension`, `PocketUiLocale`, `Person`, `Role`, and `NoticeLevel`, on this page.

The types file does not have the internal members of the host. It does not have Pi Durable's own API either: import `defineExtension`, `defineTool`, and the rest from `@earendil-works/pi-durable`.

Expand Down Expand Up @@ -78,6 +78,36 @@ Since 0.1.0 · Stable

Sets up the module. It gets the [host](host.md) and [`web`](web.md). It returns the module's extensions: one, a list, or nothing when the module only adds to the app. It can be async. Pi Pocket 0.14.0 added `web`, async setup, and modules that return nothing.

The returned extension may also include `uiLocales`, a list of validated, data-only UI translation packs. They translate only explicit `t()` calls; missing entries remain English. Packs do not run browser code, add routes, or enable the module. This API is experimental until released; see [UI locale packs](../../extensions.md#ui-locale-packs-data-only).

### `PocketExtension`

```ts
type PocketExtension = Extension & {
readonly uiLocales?: readonly PocketUiLocale[];
};
```

Experimental · Not yet released

Pi Pocket's extension shape, with optional locale data. It wraps an ordinary Pi Durable `Extension`.

### `PocketUiLocale`

```ts
interface PocketUiLocale {
readonly locale: string;
readonly label: string;
readonly default?: boolean;
readonly strings: Readonly<Record<string, string>>;
readonly templates?: Readonly<Record<string, string>>;
}
```

Experimental · Not yet released

A canonical language tag and data-only dictionaries. `strings` holds exact source strings without parameters; `templates` holds exact source strings with named `{{placeholders}}`. The loader validates their shapes and enforces placeholder parity. Dynamic values are inserted without translation. See [UI locale packs](../../extensions.md#ui-locale-packs-data-only) for full semantics and limits.

### `pocketApi`

```ts
Expand Down
2 changes: 1 addition & 1 deletion docs/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ A drop-in extension stays when Pi Pocket updates, and the owner can turn it off
## The documents

- [customizing.md](customizing.md): how to change how Pi works, without code.
- [extensions.md](extensions.md): how to write extensions: tools, prompt sections, hooks, and pages, buttons, and routes in the app. It is an index: read it, then the guides in [extensions/](extensions/) that your task needs.
- [extensions.md](extensions.md): how to write extensions: tools, prompt sections, hooks, UI locale packs, and pages, buttons, and routes in the app. It is an index: read it, then the guides in [extensions/](extensions/) that your task needs.
- The members of `host`, `web`, and `pocket` are in [extensions/reference/](extensions/reference/host.md).
- The exact types are in [types/pocket.d.ts](../types/pocket.d.ts).
- Working examples are in [examples/](examples/).
Expand Down
1 change: 1 addition & 0 deletions docs/map.md
Original file line number Diff line number Diff line change
Expand Up @@ -151,6 +151,7 @@ Plain ES modules, loaded as they are. `store.js` holds the shared state, and not

| File | Owns |
| ---------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `i18n.js` | Explicit fixed-UI translation through `t()`, validated locale packs, and live English fallback |
| `store.js` | State, the event stream (SSE, or long polling when a tunnel holds it back), `api()`, `actions`, `workspaceOrder` (the numbers, by `session-tree.js`) |
| `live.js` | The message Pi is writing, put together from view updates that carry what changed (`liveMessage`); it imports nothing, so the server's tests run it |
| `back.js` | The back stack: routes and the layers over them (sheets, the drawer, panels that cover the screen, a file over the tree) as history entries, so back closes what opened last; `useBack`, `pushRoute`, `goHome`, the list put under a conversation opened straight away, taps let go that land on what just slid in |
Expand Down
Loading