# Translator for Pi

English | [简体中文](README.zh-CN.md)

[![npm](https://img.shields.io/npm/v/@orbit-labs-ai/translator?logo=npm&label=npm)](https://www.npmjs.com/package/@orbit-labs-ai/translator)

This directory is the Pi adapter of [Translator](../README.md): a Pi extension whose entry point is **`index.ts`**. The [main guide](../README.md) covers installation, every setting and privacy. This page explains how the extension behaves inside Pi and how it is organized.

All Pi-specific code lives here; the shared translation rules live in `../shared/`. This directory handles only Pi registration, event handling, branch history, session journals and the optional Pi subprocess backend.

## Features

- **Shared preflight.** Before either direction calls a model, the shared scanner identifies certain literals, including bare Chinese filenames, paths, identifiers/settings and mentions, and checks the remaining prose. Literal-only input and output skip without a translation call or log; quoted Chinese prose remains translatable, and plain `OK` output still translates.
- **Input hook.** The `input` handler translates Chinese prompts and returns the translation together with your images. Any pre-submission failure, including a storage or UI error, blocks the prompt instead of sending the original. Post-delivery display errors cannot undo submission.
- **Answer translation.** With Output on, the `turn_end` handler translates each complete final answer and adds the Chinese as a separate entry after it. The answer itself is never replaced or resubmitted. Pi ends the turn only after this handler, so a long answer is translated in parts at the same time.
- **Translation cards.** Each translation is a session entry drawn as a card with only what the transcript lacks: your original after the delivered translated user row, the Chinese translation after an answer. Successful originals are never shown at translation completion or while input waits in a queue; failures show immediately. A card renders Markdown with Pi's own renderer and theme; expanding it (Ctrl+O) adds the translator model, history reuse and trace. A blocked prompt's card shows the reason and the original. The model never reads these entries.
- **Settings panel.** `/translator` opens a compact native panel with Input, Instruction, Output, Model (with search) and Thinking; `?` explains the selected row.
- **Toggle bar and shortcuts.** A bar under the editor shows Input, Instruction and Output; click them in fullscreen mode, or press Ctrl+Alt+T, L or Y. Ctrl+Alt+C cancels pending translations in both directions.
- **Branch settings.** Settings are saved per conversation branch and follow tree navigation, reload and resume.
- **Isolated requests.** Each fresh input translation or output part needing translation gets one `streamSimple` completion with no history, tools, provider retries, AI audit or repair call, on the session model or a pinned one, at the translator's own thinking level.
- **Journals.** A translation that proceeds past preflight is journaled beside the session file before any model call, so even a failed first prompt leaves a record. Preflight skips create no translation record.

## Installing and loading

From the `translator` directory:

```sh
./install.sh --agent pi
```

The installer adds the extension to the `extensions` list in your Pi `settings.json` and keeps everything else:

```json
{
  "extensions": ["/absolute/HOME/.agents/hooks/translator/pi/index.ts"]
}
```

This shows the entry it adds, not a file to copy over your settings. `--pi-dir` or `PI_CODING_AGENT_DIR` selects a non-default agent directory. The installer backs up the settings it changes, keeps your other extensions, and migrates known registrations of the old `pi.ts`. It does not edit project-level registrations.

After changing the source or the registration, run `/reload` or restart Pi; `/translator status` confirms that the extension loaded. Until then, a running Pi keeps the version it loaded, whole: an update on disk never mixes with it. Pi lets only commands reload extensions, so the extension cannot apply an update by itself. Changes to the configuration file alone are picked up at the next request.

To load only this entry during development, run `pi -e /path/to/orbit-tools/Hooks/translator/pi/index.ts`, and do not register it anywhere else at the same time. The package manifest likewise exposes only `./pi/index.ts`. Pi supplies the SDK and TUI dependencies, so there is nothing to install with npm.

The entry loads its local dependency graph through Pi's synchronous jiti loader,
so `/reload` refreshes nested `.mjs` code as well as the entry point instead of
reusing Node's native ESM cache. Host SDK/TUI imports keep their shared identity.

## Using it in Pi

**`/translator` is the only translation slash command.** With no arguments in the TUI, it opens a compact native panel: **Input**, **Instruction**, **Output**, **Model** and **Thinking**. The default view is nine lines: a short heading, five controls and one keyboard hint. ON/OFF badges distinguish the switches; the model resolves inline, and thinking shows an arrow only when the effective level differs (for example, `max → high` or `medium → off`). Locks and unavailable models remain visible without expanding help.

Use **↑/↓** to move, **Enter/Space** to change, and **Esc** to close; changes apply immediately. Press **?** to show or hide the selected setting's explanation, including the reason for a thinking difference. Full details also remain available through `/translator status`. The panel uses Pi's `SettingsList`, with a searchable `SelectList`/`Input` model picker offering **Follow session** and available registry models. Inside the picker, `?` is ordinary search text and **Esc** goes back. Closing the panel restores editor focus without changing the draft.

```text
/translator
/translator status
/translator help
/translator input [on|off|toggle|status]
/translator instruction [on|off|toggle|status]
/translator output [on|off|toggle|status]
/translator model [follow|provider/model]
/translator thinking [low|medium|high|xhigh|max]
```

The three switch subcommands toggle when no value is given; `model` and `thinking` without a value report settings only. Invalid arguments and unknown model IDs do not change saved settings. Configuration makes no model call and never changes the main model or its thinking level. A model pin applies to both translation directions; `model follow` overrides a file pin and follows future main-model switches. Thinking is independent (default `medium`), not inherited from the main conversation. Changing model or thinking still permits reuse of valid cached input translations; disable the cache to force a fresh comparison.

- **Existing controls.** Click **Input**, **Instruction** or **Output** below the editor in fullscreen mode. **Ctrl+Alt+T/L/Y** toggle **Input**, **Instruction** and **Output**, including inside the settings panel. The panel, commands, shortcuts and bar share live state. The bar wraps without stealing your draft or focus; regular mode uses keyboard controls. RPC/JSON/print create no terminal widget or panel; the no-argument command shows status/help instead. Global defaults refresh at input/agent boundaries; model selection refreshes translator feedback. Forced-disabled input is visibly locked; unreadable config disables stale controls.
- **Scope.** All five settings are branch-relative overrides over file defaults, restored on tree navigation, reload and resume; a new session starts with file defaults. `--no-session` keeps them in memory. Neither the shared config nor main-model settings are edited. Branch/session changes close the panel and invalidate its old callbacks. Changes apply to subsequent translations, not an in-flight request's snapshot.
- **Cancel input and output translation.** **Ctrl+Alt+C** cancels both directions, including simultaneous requests and while the panel has focus. Cancelled input submits neither original nor translation; cancelled output keeps the completed original answer. The shortcut does not change settings or disable later translations. There is no cancellation slash command or subcommand.
- **Your prompts.** After a translated prompt is actually delivered as a user message, its original appears once in a display-only card after that row. The user row already shows the English, so the card does not repeat it. Steering and follow-up translations stay journal/cache-only while queued, even during long tool calls. RPC notifications and headless stderr previews follow the same delivery timing. Failed input still shows its original and error immediately.
- **Answers.** With answer translation on, the status line shows `Translator · Output: translating to Chinese…` while it works, and the Chinese then appears as a separate entry after the answer. A warning that names a part (`part 2 of 4`) means that one part of a long answer failed; the answer stays as it is.
- **Failures and other modes.** Failures appear as warnings. In RPC mode, records arrive as notifications; without a UI, previews go to standard error, never to the protocol's standard output.

The handlers combine their own cancellation signals with `ctx.signal` when the
host supplies one. Ordinary SDK/RPC `abort()` during idle input preprocessing is
not a reliable substitute for the shortcut; the regression suite retains this
limitation separately from the successful cancellation checks. Tree navigation,
reload and shutdown invalidate both input and output work and block late writes.
Bare SDK disposal blocks late results but may leave a request running until
completion or timeout. Checks on Pi 1.0.4 and 1.1.0 invoke the registered
shortcut against real SDK requests; physical Escape/key delivery is not claimed.

## Gateway and unattended processes

A launcher can set `TRANSLATOR_PI_BYPASS=1` in its Pi child's environment to keep translation out of automated task execution. The entry returns before registering hooks, controls or renderers, without reading translation configuration, history or saved branch overrides. Both input and answer translation are skipped; the input and answer stay unchanged, with no language suffix, translation calls or new translation journals. Broken translation config and an unavailable translation model cannot block a task through Translator. Other extensions and Pi's tools/trust/authentication remain active.

The switch is process-local and requires an updated Translator as well as a launcher that sets it. Do not export it globally: normal Pi sessions without it still translate, including JSON/print/RPC input, with their existing fail-closed behavior. Saved settings remain intact for later interactive use. `TRANSLATOR_DISABLE=1` retains its separate, input-only meaning. This bypass disables `/translator` and its UI for the opted-out process; it is not a session setting or prompt-text heuristic.

## How it works

| Module | Responsibility |
|---|---|
| `index.ts` | Entry point: renderers, controls, input/output hooks and public message/delivery lifecycle handlers |
| `card.mjs` | The card for each record: only the Chinese, as Markdown, with details when expanded |
| `input.mjs` | Translates or reuses input, journals it before any model call, keeps images, and replaces or blocks the prompt |
| `output.mjs` | Translates only the complete, public final answer; shows and records it, never replaces or resubmits it |
| `controls.mjs` | Unified command, button/shortcut actions, validated branch settings and panel/widget lifecycle |
| `settings.mjs` | Native five-option settings panel, searchable registry model picker and effective-thinking feedback |
| `widget.mjs` | Below-editor toggle layout, theme, wrapping and native mouse hit testing |
| `history.mjs` | Older records on the active branch, plus the shared, versioned exact-match cache |
| `trace.mjs` | Journals kept beside the session; never writes to Pi's native session file |
| `model.mjs` | Chooses the translator: the session model, at `thinkingLevel` clamped to what it supports |
| `backend.mjs` | Isolated Pi subprocess used by the command-line tool and by Claude Code's optional Pi backend |

**Input.** Pre-submission failures, including UI, storage or runtime errors, return `action: "handled"`, blocking the prompt; Pi's ordinary extension-error handling could otherwise let the original through. Success writes the journal and enabled cache before returning `action: "transform"` with the English and original images, but appends no successful display entry yet. Public `message_start` / `message_end` identify delivery. Because Pi persists the row after `message_end`, the next ordered message/provider boundary confirms that exact user object on the active branch and appends a custom entry with `userMessageId` after it, before a later row. No timer, message injection, context mutation or extra turn is used. A post-delivery display error warns that the prompt was sent; it cannot fail submission retroactively.

**Delivery limits.** Pi exposes no input-to-queue ID or extension queue-removal event. Exact submitted text and image bytes are matched within the live session/generation; repeated identical originals consume separate occurrences, and repeat message events cannot duplicate cards. Different originals with indistinguishable pending translations, conflicting bypassed inputs (including `source: "extension"` or Input off), and normalized/rewritten images or text are deliberately not guessed. Their display may be omitted while persistent journals and enabled caches remain. Cleared queues are forgotten when no pending work is observed at input/settlement; tree/session navigation, reload and shutdown discard pending display associations. Nothing rebuilds them from historical text. Existing delivered cards survive resume in their original order; a legacy card from an older version is not moved.

**Translation requests.** Each translation is a separate `ctx.modelRegistry.streamSimple` request with no conversation history, no tools, no cache session and no provider retries. It uses the effective branch/file model pin, or the session's current model read for each request when following. Translator `thinkingLevel` (default `medium`, overridden by `/translator thinking`) is independent of the main session's level: without an override, a session at `max` translates at requested `medium`. Pi clamps it to model support, and a model without reasoning gets no reasoning option. A missing pinned model fails rather than silently switching to the main model. Neither the model pin nor thinking changes call Pi's main-model setters. The language instruction and answer translation remain independent switches.

**Preflight language decisions.** Both directions decide only before translation, before config/history/model work. The shared scanner identifies certain literal spans for both detection and request masking: code, math, links, filenames (including bare `报告.md`), paths and literal-like identifiers/settings/mentions. Literal-only text skips in both directions. Input translates remaining Chinese prose, including quoted Chinese; ambiguous prose stays visible to the model. Output translates remaining English prose even when Chinese is also present. Acronyms, code-like identifiers and the exact, case-sensitive technical names/labels `Markdown`, `Mermaid`, `Downloads`, `Token` and `Tokens` do not make an otherwise Chinese answer English. This output-classification exception is not a blanket exemption for capitalized words; real English sentences and plain `OK` output still translate. Skipping creates no translation request, record, status, preview or language suffix. See [local language detection](../README.md#local-language-detection) and [what is protected locally](../README.md#what-is-protected-locally).

**Response safety, not semantic validation.** After one completion per input or output part, only completed/nonempty response and literal-restoration safety checks run: each placeholder exactly once, and protected input code blocks in order. A failure blocks input or leaves the original answer unchanged; there is no AI audit or repair call. Neither a draft, restored text nor a joined long answer is rechecked for language. Literal content cannot trigger language rejection, and accepted results have no semantic quality guarantee.

**Cache integrity.** Reuse is keyed by exact source plus the scanner/prompt contract. The 0.6.10 contract bump isolates older versioned entries. Nonempty cache candidates, including unversioned legacy records, are checked only for source-literal bytes and occurrence counts, not order, language or semantics. Changing model/thinking does not bypass a valid entry; turn the cache off to force a fresh translation.

**Backend isolation.** The optional CLI subprocess explicitly passes `--append-system-prompt ""` to replace APPEND_SYSTEM discovery, in addition to disabling context files, tools, extensions, MCP and persistence.

**Answers.** Answer translation ignores tool calls, hidden thinking, unfinished or aborted answers, and anything that is not an assistant message. Message IDs prevent duplicate translations on the active branch. A failure keeps the original answer, and switching sessions discards a pending translation. An answer longer than 2,000 characters once literals are masked is cut into parts of about equal length, between paragraphs or lines and never inside masked code, and up to four parts are translated at once, each with its own deadline; see [Long answers](../README.md#long-answers).

**Storage.** With `--no-session`, everything stays in memory. Settings use non-context `translation-mode` entries as field patches, not snapshots of file defaults: old boolean entries remain valid, model changes save an atomic `provider`/`model` pair, and `follow` saves `null`/`null`. Malformed patches are ignored atomically. Persistent sessions get write-ahead translation journals even before Pi has written its first native conversation row. The extension never edits the input editor or its history, and never changes messages the model can see.

## Verifying

From the `translator` directory:

```sh
node --test test/pi-*.test.mjs test/output-parts.test.mjs test/backend.test.mjs
```

The integration test uses the real Pi SDK, a settings entry generated by the installer, an isolated agent directory and a local HTTP model fixture. It checks the requests actually sent, the renderers, cache reuse, resuming from disk, restoring branch settings, translator-only model/thinking overrides in both directions, the isolation of answers, and the journal written when the first input fails. If the SDK is missing, the test is skipped and says so, which is not a pass. Set `PI_SDK_PATH` when Pi is not installed in its usual managed layout.

`pi-gateway.test.mjs` runs the real JSON/print CLI with the process-local bypass, a localhost provider that rejects translation, enabled resumed branch settings, an unavailable translation pin and malformed config. It checks unchanged task/cron/peer input, unchanged English answers, zero translation requests/journals, continued execution of another extension and fail-closed ordinary input when the bypass is absent. It does not use the live gateway, WeChat or personal configuration.

Additional regressions exercise the actual fullscreen renderer with raw mouse
sequences, draft focus, narrow layouts, branch changes and live source upgrades.
The native settings component is tested with the compact layout, on-demand help,
model search, keyboard/focus and mouse routing, all five thinking choices, inline
requested/effective feedback, shortcut synchronization, environment locks, Unicode,
theme changes, CSI-u question-mark input, search selection stability and stale
callbacks. The real TUI fixture mounts the panel in the editor slot and checks
mouse routing after resize. Real SDK tests inspect pinned/follow-model
requests and thinking clamping without changing the main session, and suspend
local translator responses to verify Ctrl+Alt+C cancellation of input, output and
both simultaneously. Reload/dispose/session replacement reject late results; a
separate regression documents the ordinary idle-input abort limitation. Real CLI
wire tests exclude synthetic global/ancestor instructions. These are not
physical-terminal tests. The [0.6.10 verification record](../docs/testing.md#verification-record-for-0610)
reports the complete passing suite on Pi 1.1.0, including both-direction literal
masking/skips, single-completion response safety, long parts, cache integrity and
real steer/follow-up delivery ordering. Pi 1.0.4 was tested for earlier releases,
not retested for this patch.

See the [main guide](../README.md), the [architecture](../docs/architecture.md) and the [test matrix](../docs/testing.md) for configuration, privacy and limitations.

## License

[MIT](../../../LICENSE) © 2026 Orbit Labs contributors.
