# Ephemlet authoring guidance Build one small, self-contained Pi UI component, not an extension. Persisted source is loaded by jiti `evalModule` with a virtual filename in the owning extension; do not assume a project-root-relative import path. Export a default `factory(context)` that returns a native Pi `Component`, synchronously or asynchronously. Keep module initialization side-effect-free: validation can evaluate the module before mounting it. The component may expose `focused` and `dispose()`; dispose every timer, listener, subscription, or other resource it starts. The host owns Close and Save chrome, a right/top overlay, and the one-visible-ephemlet rule; do not recreate host chrome. Mount context exposes `pi`, a live `ctx` getter, `tui`, `theme`, `keybindings`, `state`, `setState`, `viewport()`, and `signal`. Use `context.pi` and native Pi TUI APIs; read installed Pi SDK documentation rather than inventing APIs. Choose local behavior for self-contained UI, agent flow for user intent requiring Pi conversation, and external flow only when request facts provide a concrete integration. Actions must expose progress/status/errors and support cancellation through `context.signal` or explicit local controls. Submit user intents with `pi.sendUserMessage(text, { deliverAs: "followUp", expandPromptTemplates: false })` when they should continue after the current run. Keep state JSON-serializable; guard asynchronous updates after cancellation or disposal. Use only request facts and supplied existing source as authority; preserve useful behavior and avoid invented claims or unrelated features. Components run as trusted native code with Pi's permissions, not in a browser or sandbox. Shell, filesystem, API, and web side effects may use native Node/Pi APIs when the interaction brief calls for them; never embed credentials in session source or state. Expose operation progress, results, errors, and cancellation; report task-relevant side effects to the main agent through normal message flow. Source must be self-contained and at most 128 KiB UTF-8, with state at most 64 KiB JSON. The builder returns complete source through `submit_ephemlet`; the main agent can also supply source directly through the `ephemlet` tool. ## Native component contract ```ts import { Text } from "@earendil-works/pi-tui"; export default function mount(context) { // context: pi, live ctx getter, tui, theme, keybindings, // live state getter, setState(JSON), viewport(), signal. const component = new Text("Hello", 0, 0); return component; } ``` Return a native component with `render(width): string[]` and `invalidate(): void`. Optional `handleInput(data)` and `handleMouse(event)` receive native Pi input events in the content area. Implement `focused` and propagate it to nested editors for cursor/IME support. Host reserves Escape, Alt+Left/Right, Alt+Tab, Ctrl+S, Ctrl+W, and Shift+PageUp/PageDown; ordinary Tab belongs to your component. Use `viewport()` to size/scroll content; fit every line to supplied width using Pi's helpers. Return at most 8192 string lines per render and never write diagnostics to stdout/stderr; use `context.ctx.ui.notify()` for actionable errors. Keep rendering side-effect-free and route nested keyboard/mouse interactions within the returned root component. Host provides scrolling for overlong reports, plus chrome/navigation and responsive right/top positioning. All components may remain mounted while hidden, but only one is visible; avoid unnecessary background work. Read `context.state` for current durable state, including main-agent updates; call `context.setState(next)` after meaningful interactions. Uncheckpointed local variables do not survive session reopen. Read `context.ctx` when performing operations rather than retaining the context of the originating tool call. Do not register permanent tools/commands in a component: Pi has no general unregistration API. Track `pi.on()` unsubscribe callbacks and release them in `dispose()`. Guard all asynchronous results with `context.signal`; host aborts it before disposal. Source/state updates use stable ID and expected revision; query `ephemlet list/get` before adapting existing work. `get` returns bounded metadata by default; request `include: ["source"]`, `["state"]`, or both only when those fields are needed. The main agent cannot see the panel and receives no automatic notification of local state changes; it reads current state through `get`. Submit user intents that need conversation work through the explicit follow-up flow above rather than treating local state updates as agent messages. Background builds return an ID/job ID immediately; ready/failed notices do not trigger a new turn, so query `get` before relying on completion. Do not confuse build completion with human interaction completion.