# Asking the user, and rendering
Asking the user something, and drawing. Questions
(`select` / `confirm` / `input`) become real DSH user questions that
genuinely block the turn. Terminal decoration (footer, statusline, themes,
shortcuts) registers and never fires — exactly as in Pi's own non-terminal
modes. Plugin-drawn cards are the one Pi UI surface pi2dsh does not draw yet.
**24 Pi surfaces** — 4 same semantics · 20 mapped, difference stated.
| Pi surface | Kind | Status | What it does on DSH |
|---|---|---|---|
| [`registerMessageRenderer`](#registermessagerenderer-pi) | `pi.*` | Mapped, difference stated | The renderer really runs: a custom message the package sent is drawn by the package's own code and shown in the DSH web conversation. What reaches the browser is the component's rendered text, not a mounted Pi component. |
| [`registerEntryRenderer`](#registerentryrenderer-pi) | `pi.*` | Mapped, difference stated | The renderer really runs: entries the package appended are drawn by the package's own code and shown in the DSH web conversation. What reaches the browser is the component's rendered text, not a mounted Pi component. |
| [`registerMarkdownTransformer`](#registermarkdowntransformer-pi) | `pi.*` | Mapped, difference stated | Registration is accepted; DSH owns presentation, so the transformer is never invoked — matching Pi's non-TUI surfaces. |
| [`notify`](#notify-ctxui) | `ctx.ui.*` | Same semantics | Captured as a command result when applicable and emitted through DSH logging at the severity the caller passed (warning and error log as warnings). |
| [`setStatus`](#setstatus-ctxui) | `ctx.ui.*` | Mapped, difference stated | Pi's keyed status entries render as pills in the bridge's own browser half (Pi's status bar is a terminal-footer surface; DSH's equivalent is the frame-wide pill stack). setStatus(key, undefined) removes exactly one entry, Pi's clear shape. |
| [`setWidget`](#setwidget-ctxui) | `ctx.ui.*` | Mapped, difference stated | String-array widgets render as a strip in DSH's conversation.input.dock seat (a full-width row of its own above the composer card). setWidget(key, undefined) removes one widget. Component factories are ignored, exactly like Pi's own rpc mode, where widgets travel to a host as lines. |
| [`select`](#select-ctxui) | `ctx.ui.*` | Same semantics | Mapped to one native DSH userQuestions single-select request. |
| [`confirm`](#confirm-ctxui) | `ctx.ui.*` | Same semantics | Mapped to one native DSH userQuestions Yes/No request. |
| [`input`](#input-ctxui) | `ctx.ui.*` | Same semantics | Mapped to one native DSH userQuestions free-text request. |
| [`editor`](#editor-ctxui) | `ctx.ui.*` | Mapped, difference stated | Mapped to one DSH userQuestions free-text request. The prefill is shown as context but is NOT editable text: the caller receives what the user typed fresh, not an edit of the prefill. |
| [`custom`](#custom-ctxui) | `ctx.ui.*` | Mapped, difference stated | Resolves undefined, exactly like Pi's own rpc mode; guarded fallbacks keep working. A Pi component cannot be forwarded to a browser — but the SHAPE packages use this for (a focused side panel over the conversation) is drawn natively by the bridge's own browser half. |
| [`setWorkingMessage`](#setworkingmessage-ctxui) | `ctx.ui.*` | Mapped, difference stated | A live working message, drawn in DSH's conversation.composer.dock band (under the composer card — the host's ambient-readout seat, where its own stats line sits). Calling with no argument restores the default, i.e. clears it. |
| [`setWorkingVisible`](#setworkingvisible-ctxui) | `ctx.ui.*` | Mapped, difference stated | Hides or shows the working chrome (message, indicator, hidden-thinking label) without clearing it — Pi's exact semantics. |
| [`setWorkingIndicator`](#setworkingindicator-ctxui) | `ctx.ui.*` | Mapped, difference stated | WorkingIndicatorOptions ({frames?: string[]}) project to the frames' text in the same composer-dock working chrome. An empty array hides the indicator and frames: undefined restores the default (clears it); animated frames render as their static concatenation — the honest still of the package's own frames. |
| [`setHiddenThinkingLabel`](#sethiddenthinkinglabel-ctxui) | `ctx.ui.*` | Mapped, difference stated | The label shows in the same working chrome; calling with no argument restores the default, i.e. clears it. DSH owns the thinking block's own presentation, so the label is informational chrome, not a re-render of the block. |
| [`setFooter`](#setfooter-ctxui) | `ctx.ui.*` | Mapped, difference stated | A custom footer factory renders when it can build its component without Pi's TUI, into the conversation.composer.dock band; otherwise the surface stays empty — the same shape as Pi's rpc mode, where no footer factory runs at all. The factory receives the bridge's headless theme. |
| [`setHeader`](#setheader-ctxui) | `ctx.ui.*` | Mapped, difference stated | A custom header factory renders when it can build its component without Pi's TUI, into DSH's conversation.session.header.utilities seat; otherwise the surface stays empty — the same shape as Pi's rpc mode. |
| [`setTitle`](#settitle-ctxui) | `ctx.ui.*` | Mapped, difference stated | Pi's transient window title shows as a frame-wide pill in the bridge's browser half. It deliberately does NOT rename the DSH session: a session title is durable, user-owned and shown in the session list, and quietly rewriting it would outlive the turn that asked. |
| [`theme`](#theme-ctxui) | `ctx.ui.*` | Mapped, difference stated | A headless theme whose styling calls return unstyled text. |
| [`getAllThemes`](#getallthemes-ctxui) | `ctx.ui.*` | Mapped, difference stated | Lists the single headless theme. |
| [`getTheme`](#gettheme-ctxui) | `ctx.ui.*` | Mapped, difference stated | Resolves only the headless theme. |
| [`setTheme`](#settheme-ctxui) | `ctx.ui.*` | Mapped, difference stated | Accepts the headless theme; other names report an explicit error result. |
| [`getToolsExpanded`](#gettoolsexpanded-ctxui) | `ctx.ui.*` | Mapped, difference stated | A bridge-local presentation flag. |
| [`setToolsExpanded`](#settoolsexpanded-ctxui) | `ctx.ui.*` | Mapped, difference stated | A bridge-local presentation flag. |
## How each one is built
Every surface below names the DSH mechanism that carries it — the seam, service
or waterfall — so the mapping can be checked against the harness rather than
taken on trust.
### `registerMessageRenderer`
`pi.*` · Mapped, difference stated
A custom message is a durable DSH log entry carrying Pi's role:"custom" marker (source.piCustomType), so the projection reads it back from the session through ctx.sessions.get(id) rather than from a cache — the renderer keeps working after a restart. The returned component is projected through its own render(width), and the text takes a seat in the conversation flow.
### `registerEntryRenderer`
`pi.*` · Mapped, difference stated
Custom entries live in the pi2dsh sidecar (DSH's durable log has no channel for event types declared outside the harness, so the host's own conversation view can never show them). The registered EntryRenderer runs per entry, its component is projected through render(width), and the text is seated in the conversation flow. A renderer that throws contributes nothing and leaves every other package's entries — and the request — intact.
### `registerMarkdownTransformer`
`pi.*` · Mapped, difference stated
Kept in bridge state and readable back; no DSH surface consumes it.
### `notify`
`ctx.ui.*` · Same semantics
Written to the DSH logger at the severity the caller passed, and returned as the command result when the call happens inside one.
### `setStatus`
`ctx.ui.*` · Mapped, difference stated
BrowserSurfaces.setStatus keys entries by (session, package, status key); the client half polls the bridge's own /pi2dsh/browser-state route and draws them in its shell.overlay seat. Pi's rpc mode transmits setStatus to a host too — this is one of the surfaces rpc genuinely wires.
### `setWidget`
`ctx.ui.*` · Mapped, difference stated
BrowserSurfaces.setWidget keeps the lines per widget key; the client half draws them in the conversation.input.dock slot. The factory branch is dropped at the ui seam, mirroring rpc-mode.ts's "Only support string arrays in RPC mode".
### `select`
`ctx.ui.*` · Same semantics
One native DSH UserQuestionService request carrying Pi's options as the choices. The turn really blocks until a human answers.
### `confirm`
`ctx.ui.*` · Same semantics
One native DSH UserQuestionService request with two choices.
### `input`
`ctx.ui.*` · Same semantics
One native DSH UserQuestionService free-text request.
### `editor`
`ctx.ui.*` · Mapped, difference stated
One native DSH UserQuestionService free-text request. DSH has no editable-prefill question type, so the prefill is shown in the question body and the answer comes back as fresh text.
### `custom`
`ctx.ui.*` · Mapped, difference stated
Two halves. The call itself resolves undefined, matching Pi's rpc mode, because a Pi TUI component has no meaning in a browser. Separately, pi2dsh ships a client half (`dsh.client` + `exports["./client"]`) that takes DSH's frame-wide `shell.overlay` seat and renders a side conversation as a floating panel plus the presentation surfaces, fed by this package's own route (`/pi2dsh/browser-state`). So the capability lands as a native DSH surface rather than as a relayed Pi component.
### `setWorkingMessage`
`ctx.ui.*` · Mapped, difference stated
Recorded per session per package and drawn by the client half's composer-dock seat. Pi's rpc mode is a no-op here (it has no TUI loader); a browser host genuinely can show the text, so the bridge supersedes it.
### `setWorkingVisible`
`ctx.ui.*` · Mapped, difference stated
A flag on the per-package surface view; the client half skips working keys while it is false. Supersedes Pi's rpc-mode no-op with a real visibility switch.
### `setWorkingIndicator`
`ctx.ui.*` · Mapped, difference stated
surfaceText projects the frames object to text, recorded like the other working surfaces. Pi's rpc mode ignores the call; the browser host draws the static projection instead.
### `setHiddenThinkingLabel`
`ctx.ui.*` · Mapped, difference stated
Recorded per session per package like the other working surfaces. Pi's rpc mode is a no-op here; the browser host shows the text.
### `setFooter`
`ctx.ui.*` · Mapped, difference stated
surfaceText calls the factory with the headless theme (and no TUI) and renders the returned component's render(80) output; a factory that needs the TUI throws and degrades to an empty surface.
### `setHeader`
`ctx.ui.*` · Mapped, difference stated
surfaceText calls the factory with the headless theme (and no TUI) and renders the component; failures degrade to empty. Drawn by the client half's header-utilities seat.
### `setTitle`
`ctx.ui.*` · Mapped, difference stated
Recorded per session per package and drawn as a pill in the shell.overlay seat, next to the status pills.
### `theme`
`ctx.ui.*` · Mapped, difference stated
A single headless Theme object whose styling functions return their input unchanged; DSH does the rendering.
### `getAllThemes`
`ctx.ui.*` · Mapped, difference stated
Lists the one headless theme the bridge owns.
### `getTheme`
`ctx.ui.*` · Mapped, difference stated
Resolves the one headless theme by name.
### `setTheme`
`ctx.ui.*` · Mapped, difference stated
Accepts the headless theme and returns Pi's explicit error result for any other name, rather than pretending a switch happened.
### `getToolsExpanded`
`ctx.ui.*` · Mapped, difference stated
A bridge-local flag; nothing renders from it.
### `setToolsExpanded`
`ctx.ui.*` · Mapped, difference stated
A bridge-local flag; nothing renders from it.
---
Back to the [capability index](README.md) · the whole verdict in
[pi-abi-coverage.md](../pi-abi-coverage.md).