# 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. pi2dsh also ships a DSH client module that redraws
portable presentation intent — status, widgets, headers, footers, editor text
and side conversations — in native Web slots. A Pi terminal component itself
is not mounted in the browser; terminal-only factories and raw key handling
remain headless or become an explicit Web-native projection.
**24 upstream-shaped Pi rule rows** — 5 same semantics · 19 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.*` | Same semantics | Pi's keyed status entries render in the active DSH front door: dsh-TUI's native status line, dsh-pi-tui's public footer-status slot, or package-keyed pills in the bridge's browser half. setStatus(key, undefined) removes exactly one entry. |
| [`setWidget`](#setwidget-ctxui) | `ctx.ui.*` | Mapped, difference stated | Widgets float in DSH web's lower-right overlay stack, session-scoped: a single-line widget renders as an ambient status pill, taller content opens as a floating card on arrival and collapses to a labelled pill (diagnostics-shaped content gets product rows — file chips, severity dots, locations). setWidget(key, undefined) removes one widget. A component factory runs with a LIVE tui handle: requestRender re-renders the retained component into the same slot, and replacing or clearing the widget disposes it. |
| [`select`](#select-ctxui) | `ctx.ui.*` | Same semantics | Mapped to one native DSH userQuestions single-select request. A terminal-oriented multi-line title becomes DSH's plain heading plus native detail; links render as Markdown on Web and OSC 8 in dsh-TUI without visible escape bytes. |
| [`confirm`](#confirm-ctxui) | `ctx.ui.*` | Same semantics | Mapped to one native DSH userQuestions Yes/No request. A terminal-oriented multi-line title becomes DSH's plain heading plus native detail; links render as Markdown on Web and OSC 8 in dsh-TUI without visible escape bytes. |
| [`input`](#input-ctxui) | `ctx.ui.*` | Same semantics | Mapped to one native DSH userQuestions free-text request. A terminal-oriented multi-line title becomes DSH's plain heading plus native detail; links render as Markdown on Web and OSC 8 in dsh-TUI without visible escape bytes. |
| [`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. Terminal-oriented title formatting uses the same surface-native DSH heading/detail projection. |
| [`custom`](#custom-ctxui) | `ctx.ui.*` | Mapped, difference stated | Runs the real Pi component on the terminal front doors: a dsh-TUI full-screen scene or dsh-pi-tui's raw component mount, with width, ANSI frames, raw input, render invalidation, result completion and disposal preserved. Headless/rpc compositions resolve undefined. A declared browser product surface also resolves undefined because it owns presentation. A surface with native dialogs but neither a terminal renderer nor declared browser presentation throws a catchable PiCapabilityError so a package can run its non-terminal fallback instead of silently succeeding. The web deliberately projects no TUI (2026-08-29 product decision): the content a scene would carry is served by product surfaces instead (a package's side conversation by the side-chat window, MCP management by the MCP tab), and product-UI command invocations take the same degradation so a component never doubles the window presenting it. |
| [`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 as Pi entry lines in the per-session Pi-format archive (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.*` · Same semantics
The selected terminal backend owns a package/key-scoped handle: tuiStatus on dsh-TUI, or a replaceable chrome.footer.status contribution on dsh-pi-tui. Browser mode writes BrowserSurfaces by (session, package, key); the client half draws the entries from the bridge route.
### `setWidget`
`ctx.ui.*` · Mapped, difference stated
BrowserSurfaces.setWidget keeps the rendered text per widget key; the client half presents it from the shell overlay (pill or floating card by shape), never as an in-column strip — the strip form could not be dismissed and wore the previous session's content on the new-session screen. Factories get a real driver because packages RETAIN the handle: pi-lens closes over tui and calls requestRender from later tool executions — with an undefined handle that call crashed the tool, far outside any factory-time catch.
### `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; the shared dialog projection separates title/body and selects the link encoding the active public renderer consumes.
### `confirm`
`ctx.ui.*` · Same semantics
One native DSH UserQuestionService request with two choices, using the shared dialog projection for title/body and surface-native links.
### `input`
`ctx.ui.*` · Same semantics
One native DSH UserQuestionService free-text request. The shared dialog projection preserves visible copy and link destinations, selects the active renderer's link encoding, and removes duplicate raw-link lines.
### `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; link encoding uses the active public renderer.
### `custom`
`ctx.ui.*` · Mapped, difference stated
The Pi component stays Host-owned behind a transport-neutral relay whose messages are width/lines/input/close. The current local dsh-pi-tui transport wraps that relay with the public UNSTABLE_API_LEVEL=1 mountComponent facade; dsh-TUI keeps its public scene seat. No callback or component object is part of the relay contract, so a future DSH Server/Client split changes only the transport. One scene at a time, opening a new one finishes the previous run.
### `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).