Review Desk: a native panel and portable dialogs
Imani’s standard editor works in both terminal and RPC review flows. Now she wants a compact local panel for quickly accepting or rejecting a long note.
Her obstacle is portability. A terminal component is not automatically a remotely inspectable form.
She keeps the domain tool and standard dialogs, then adds the panel as an optional TUI presentation.
Milestone: open a focused native overlay
Human OMP slash command—TUI only:
/review-desk overlay
The supplied ReviewPanel supports:
- Up/Down to scroll;
ato accept locally;rto reject;- Escape to cancel.
The overlay does not edit the note. Accepting from it retains the existing text.
The body shows an eight-line scrolling window. Text rows are bounded to the smaller of the available width and 100 columns.
Exact excerpt—untrusted text handling in panel.ts:
export function safeText(text: string): string {
return replaceTabs(Bun.stripANSI(text)).replace(/[\u0000-\u0008\u000b-\u001f\u007f-\u009f\u202a-\u202e\u2066-\u2069]/g, "");
}
The note is data. It is not trusted ANSI styling, terminal control, clipboard control or bidirectional layout instruction.
Inspect progress
Observed:
- The loader-registered expanded custom renderer was checked at widths
0,1,4,24,80and160. - Every rendered row stayed within the applicable bound.
- Injected control sequences were removed.
- Real controller/TUI focus dispatch through a terminal emulator accepted the overlay.
- Cleanup hid and disposed it.
- The surrounding composer text remained unchanged.
- Aborting a custom UI signal rejected the promise and removed the overlay.
This was real controller and terminal-emulator behavior with a surrounding mode fixture. It was not a physical-terminal visual audit or a full Seed Desk composer smoke.
Exercise: open the overlay, scroll, then Escape.
Checkpoint: the panel closes; the domain records a cancelled review. An externally aborted presentation caused by revocation/navigation instead must not commit to another branch.
Choose semantic dialogs before custom controls
The portable baseline is:
| Method | Result | Cancellation distinction |
|---|---|---|
select(title, options, dialogOptions?) | Selected label, even for an option object | undefined on dismissal/cancellation |
confirm(title, message, dialogOptions?) | Boolean | Decline and cancellation both resolve false |
input(title, placeholder?, dialogOptions?) | Text | undefined means no answer; an empty string is a separate value |
editor(title, prefill?, dialogOptions?, editorOptions?) | Multiline text | undefined means cancelled |
Optional askDialog(questions, dialogOptions?) | A structured ask result | Can be cancelled, submitted, or redirected to chat |
input’s second argument is a placeholder, not an initial document. editor’s second argument is the prefill. Its fourth argument may set promptStyle.
Feature-detect askDialog; do not assume it exists because hasUI is true.
Rich ask data
A rich ask question contains:
id: stable question identity;question: full question text;- optional
header; options;- optional
multi; - optional zero-based
recommended.
Each option has label, optional description, and optional preview.
A submitted result has kind: "submit" and ordered results. Each result item contains:
id,question;optionsas labels;multi;selectedOptions;- optional
customInput,note,timedOut.
kind: "chat" means the user chose to discuss the question. It is not cancellation and it is not an answer.
ACP’s rich ask adapter can produce recommended fallback answers marked timedOut: true. A permission system must not interpret such a fallback as explicit consent.
Descriptions and previews are presentation capabilities, not a guarantee that every protocol adapter transmits them. For example, the supplied ACP basic select translates labels to an enum; its rich ask path carries richer descriptions.
Dialog options are not universally portable
ExtensionUIDialogOptions includes all of the following:
| Family | Members | Use |
|---|---|---|
| Cancellation and timing | signal, timeout, onTimeout, onTimeoutStart, onTimeoutReset | Cancel work and observe a host-managed timeout |
| Initial selector presentation | initialIndex, outline, helpText | Position and explain a TUI selector |
| Selector actions | onLeft, onRight, onExternalEditor | TUI-specific callbacks |
| Selection markers | selectionMarker, checkedIndices, markableCount | Radio/checkbox presentation for leading options |
Timeouts are milliseconds. timeoutStartsOnPresentation is an optional UI capability: the TUI sets it true so a queued selector’s timeout need not expire before it appears.
Do not infer that every option applies to every method. In particular, the supplied standard multiline editor path honors cancellation but does not implement the same dialog timeout plumbing as the selector/input paths.
RPC is semantic UI, not a terminal
The current wired RPC extension context reports hasUI: true and mode: "rpc". This outranks the stale type comment saying RPC has no UI.
The protocol uses requests and correlated responses.
RPC client input—request the human review command:
{"type":"prompt","id":"review-1","message":"/review-desk review"}
The client must continue reading output, present the emitted editor/selector requests, and reply using each request’s actual ID. It must not wait for the review to finish before processing the dialog requests needed to finish it.
Illustrative RPC response shapes—dialog-1 represents an ID received from the server, not a reusable workbook ID:
{"type":"extension_ui_response","id":"dialog-1","value":"Accept locally"}
{"type":"extension_ui_response","id":"dialog-1","confirmed":true}
{"type":"extension_ui_response","id":"dialog-1","cancelled":true}
Use the appropriate variant, not all three.
RPC specifics:
- Select sends string
options; optionaloptionDetailsalign descriptions by position. - Select returns a label, not an index.
- Select, confirm and input transmit timeout information.
- Editor transmits
title,prefilland optionalpromptStyle; it has no timeout field or local editor timeout scheduler in this adapter. - Aborting an active dialog emits
method: "cancel"withtargetIdequal to the original request ID. - Pre-aborted signals suppress presentation.
- Disconnect rejects pending and future requests.
- Local select/confirm/input timer expiry can settle without a cancel frame, so clients must honor the transmitted timeout too.
RPC supports fire-and-forget notifications, status, string-array widgets and editor-text requests. Title emission is opt-in through PI_RPC_EMIT_TITLE=1.
It does not serialize TUI component factories. custom() returns without invoking the factory. Component widgets, terminal listeners, custom editor replacement and autocomplete factories are unsupported there.
Passive presentation is not persistence
| UI method | Useful role | Current implementation boundary |
|---|---|---|
notify | Brief result or warning | Not a durable domain record |
setStatus(key, text) | Small named status indicator | Clear with undefined |
setWorkingMessage(message?) | Streaming activity wording | TUI support; RPC/ACP inert |
setWidget(key, content, options?) | Summary above/below editor | TUI supports strings or factories; RPC supports strings only |
setTitle | Terminal/window title | Not the persisted session name |
setFooter, setHeader | Advertised component replacement surfaces | No-op in the supplied TUI controller, as well as RPC/ACP |
custom(factory, options?) | Focused terminal component | Guard with ctx.mode === "tui" |
For string widgets, the TUI takes the first ten supplied strings and adds a truncation notice when necessary. This is not a promise that arbitrary long strings wrap into only ten terminal rows.
ExtensionCustomOptions contains:
overlay;- static or lazy
overlayOptions; onHandle, receiving an overlay handle;signal.
A component should implement render(width) and invalidate(), optionally input handling and dispose(). Cleanup must be idempotent. Review Desk’s panel becomes inert after disposal.
What changes next?
If Imani needs screen-reader-friendly or remotely operated review controls, she should extend the semantic domain interface or use host-supported forms. A custom terminal drawing does not become accessible merely because it is visible.
review_desk inspect/query/act is the agent-facing interface. The panel is one human-facing view of that interface.
Source, snapshot 2026-08-29: packages/coding-agent/src/extensibility/extensions/types.ts, UI interfaces; packages/coding-agent/src/modes/controllers/extension-ui-controller.ts, showHookCustom, #presentDialog; packages/coding-agent/src/modes/rpc/rpc-mode.ts; linked Review Desk panel.
Extensions inside those boundaries · Source chapter: extensions/review-desk-a-native-panel-and-portable-dialogs. Original evidence remains scoped to its recorded snapshot.