Red host API version 0.15.0 is defined by
src/plugin/host_api.json. That file is the canonical,
machine-readable list of execute actions, request actions, signatures, and introduction
versions. Runtime dispatch and the bundled-plugin corpus are checked against it in tests.
Plugin packages may declare a semver range in red_api_version. Red checks that range
before activation. A malformed or incompatible range quarantines that plugin and reports
the source path, stage, current API version, and this migration guide; editor startup and
unrelated plugins continue. While Red is pre-1.0:
- patch API releases fix behavior without intentionally changing signatures;
- minor API releases may add calls and fields, and may deprecate calls;
- removing or incompatibly changing a call requires a host-API minor bump, a change manifest entry, and a migration note.
Load runs parse, name resolution, and type checking against Red's host declarations
before activation. Diagnostics retain source spans and use stable families:
HUSK-P0001 for parsing, HUSK-T0001 for semantic/type errors, and HUSK-A0001 for a
literal host call absent from the canonical schema. Literal host calls also check
required/optional arity (HUSK-A0002) and obvious literal argument types
(HUSK-A0003) against the machine-readable signature. Invalid static plugin
annotations use HUSK-A0004. --no-typecheck is an unsupported development
escape hatch; compatibility guarantees do not apply while it is enabled.
Red 0.15.0 retains the complete 0.4.0, 0.6.0, 0.7.0, 0.8.0, 0.9.0,
0.10.0, 0.11.0, 0.12.0, and 0.14.0 contracts, so existing packages that declare those
minors continue to load. New packages should target "red_api_version": "^0.15.0".
GetEditorInfo includes a stable process-local id on every buffer summary.
Use OpenBufferById(id: i32) to select that exact live buffer, including multiple
unnamed buffers that all have the display name [No Name]. Buffer IDs survive
buffer-list compaction but are not persistent across editor processes. The older
OpenBuffer(name: String) call remains available for unambiguous buffer names.
Host API 0.13.0 adds document_id to GetWindows entries and successful
DocumentSymbols results, and to file:saved events. This identifies one live
buffer and survives buffer-index changes. Cache symbols by this ID and revision, not by the display or navigation
path. The optional argument to DocumentSymbols remains a buffer index. Responses
for closed, renamed, or changed documents return an error.
GetWindows.breadcrumb_components contains display-only path labels: relative to
the working directory when possible, otherwise home-abbreviated with ~, otherwise
absolute. Native roots, drive letters, and UNC shares are preserved. Never use these
labels for file access or LSP requests.
red::document_symbol_chain(symbols, position, file) returns the containing symbol
ancestry from normalized document symbols. position uses zero-based UTF-16 LSP
coordinates; file is the response's navigation path. The native lookup preserves
the original records and works beyond the bundled plugin's former 512-symbol limit.
Host API 0.12.0 adds languages.<id>.grammar.indents, an ordered list of
package-relative query files. Packs using it must require ^0.12.0 or later.
See the indentation query contract for captures,
fallback behavior, and portable fixtures. Older packs remain supported.
Host API 0.11.0 adds opt-in arguments to plugin commands:
#[red::command(
name = "Service",
arguments = true,
completions = [["enable", "disable", "status"], ["local", "workspace"]],
)]
fn service(command: CommandInvocation) {
red::execute("Print", command.raw_args);
}
CommandInvocation contains the exact registered name, whitespace-separated
args: [String], and unexpanded raw_args: String. A callback may also accept
Json when it does not need the typed record. A palette or keymap invocation
without arguments receives an empty argument list. Existing commands continue to
take no parameters unless they opt in. The same arguments and completions
fields are accepted by red::add_command metadata.
Each inner completion array describes one argument position. Choices must be
nonempty strings without whitespace; an empty array leaves that position without
suggestions. Choices are hints, not validation: the callback must validate its
arguments. Tab and Shift-Tab cycle matching choices without invoking the
callback. Hidden commands stay out of completion, and built-in colon commands
retain precedence. There is no shell quoting, expansion, or completion callback.
Packages using these fields must target ^0.11.0 or later.
Host API 0.10.0 adds an optional formatter table to each language definition:
[languages.python.formatter]
name = "Black"
command = "black"
args = ["--quiet", "--stdin-filename", "{file}", "-"]
root_markers = ["pyproject.toml", ".git"]Red launches the command directly, sends the document on standard input, and replaces
the document with UTF-8 standard output. {file} and {workspace} placeholders are
expanded in arguments and environment values. Project-local executables under
node_modules/.bin, .venv/bin, venv/bin, and vendor/bin take precedence over
PATH.
The global [formatting] table supports on_save (default true),
trim_trailing_whitespace (default true), and a provider of auto, external,
or lsp. Save-time formatting removes trailing spaces and tabs before invoking the
formatter, except for language ids in trim_trailing_whitespace_exclude (default
["gitcommit", "markdown"]). auto prefers an installed language-pack formatter and
falls back to LSP when the formatter is absent; a formatter that starts and fails does
not silently switch engines. Set formatting.on_save = false to disable automatic
formatting and its whitespace cleanup without disabling the explicit Format Document
action. The legacy
lsp.format_on_save flag remains accepted as an alias for either boolean value.
The modern key wins when both appear in the same config layer; later command-line
overrides still take precedence.
OpenScratchBuffer(callback, name, text, commands?) accepts an optional syntax
language name plus optional submit and cancel plugin command names. The syntax
selection is local to the scratch buffer and falls back to automatic filename
detection when omitted or unknown. In a managed scratch buffer, :w and :wq invoke
the submit command without writing the display name to disk, while :q and :q!
invoke the cancel command without quitting Red. Save, Quit, and configured key
bindings follow the same routing. The options were added in host API 0.8.0; calls
using the original three required arguments remain compatible.
CompanionCall(callback, method, params, timeout_ms?) lazily starts the calling
package's declared native companion and exchanges bounded JSON-lines RPC
messages. Red owns the process, matches responses to requests, enforces the
timeout, and stops the process when the package or editor shuts down.
DocumentSnapshot(callback, path?) returns the current text and revision for an
open document. DocumentApply(callback, options) atomically applies non-overlapping
edits after checking the expected revision and optional preimages. The result
contains an attributed transaction ID. DocumentUndo(callback, options) reverts
that exact transaction only while it is still the most recent transaction from
the same plugin. Red remains responsible for buffer state, dirty tracking, LSP
notifications, and undo history.
Plugin-owned storage is captured as an opaque namespaced extension in Red session snapshots. Unknown extensions survive load/save cycles, allowing a package to restore its workflow without Red understanding the payload.
These calls were introduced in host API 0.6.0.
FileOperation(callback: fn(Json), operation: Json) applies a structured filesystem
operation inside the active workspace. Supported kind values are create,
create_file, create_directory, rename, move, copy, delete, trash,
restore, undo_trash, and stat. Mutation paths must be workspace-relative; the host
rejects absolute paths, parent traversal, workspace-root mutation, symlink escapes,
self/descendant copies, and implicit overwrites.
Create requests accept path; create treats a trailing slash as a directory and
supports bounded Bash-style list and range brace expansion. Rename, move, and copy
accept source and destination. Delete, trash, restore, and undo accept paths: [String]. Results contain ok, an optional error, and operation-specific path data.
Trash restoration is available only on platforms whose system trash API exposes stable
item identities.
FileOperation was introduced in host API 0.4.0.
OpenInput(title: String, initial: String, handlers: ComposerHandlers) opens the same
compact, single-line input used by LSP rename. It submits through
ComposerHandlers.submitted and cancels through ComposerHandlers.cancelled.
OpenConfirm(title: String, message: String, handlers: PickerHandlers, options?: Json)
opens a compact Accept/Cancel dialog. Cancel is selected by default; Left or y selects
Accept, Right or n selects Cancel, Enter confirms the selection, and Escape cancels.
Accept invokes PickerHandlers.selected with an item whose id is accept;
cancellation invokes PickerHandlers.cancelled.
Both calls were introduced in host API 0.4.0, and their callback handles remain owned
and released by the calling plugin.
The optional OpenConfirm options object accepts accept_label, cancel_label, and
rows. Each row is an array of { text, style } segments, allowing a plugin to present
bounded, theme-aware details while retaining the host dialog's safe default and input
behavior. Calls without options keep the original compact presentation.
UpdateOverlayBusy(id: String, busy: bool) adds host-driven spinner animation before
the first visible line of an existing overlay. The host owns frame timing and redraws;
plugins only toggle busy state and replace the overlay content when work completes.
These additions were introduced in host API 0.9.0.
Top-level functions can declare commands, event listeners, state, configuration, and lifecycle hooks directly:
struct SymbolsState {
request: i32,
enabled: bool,
}
#[red::state]
fn initial_state() -> SymbolsState {
return SymbolsState { request: -1, enabled: true };
}
#[red::config("plugin_config")]
fn configuration_loaded(event: Json) {
red::state_patch(SymbolsState {
enabled: event.value.symbols.enabled,
});
}
#[red::command(
name = "LspDocumentSymbols",
title = "Show document symbols",
category = "LSP",
description = "Find symbols in the current document",
aliases = ["outline", "symbols"],
scope = "global",
)]
fn document_symbols() {
red::request("DocumentSymbols", show_document_symbols);
}
#[red::on("timeout:callback")]
fn symbol_batch_timeout(event: TimerEvent) {
// Handle a host-owned timer notification.
}
#[red::lifecycle("deactivate")]
fn stop_background_work() {
// Cancel timers or release plugin-owned resources.
}
#[red::command] requires a nonempty name, a zero-argument function by default, and
optional string title, category, and description fields plus an optional
string-array aliases field. visible = false hides a command from the command
palette and colon completion without disabling direct invocation or keymaps.
The optional scope is "editor" by default. Set it to "global" only for
commands that are safe and meaningful when a plugin panel owns focus, such as
workspace pickers or pane toggles. Packages using command scope must declare
"red_api_version": "^0.7.0".
#[red::on] takes exactly one nonempty event-name string and requires a
one-argument function. A function may subscribe to multiple distinct events by
repeating #[red::on(...)].
#[red::state] marks one zero-argument initializer with an explicit named record
return type. Red runs it after static registration and before activation, stores
its concrete record privately for that plugin, and makes it available through
red::state(). Update individual named fields with a sparse record literal:
red::state_patch(SymbolsState { enabled: false });
Patches retain every omitted field and reject unknown fields or records that do
not belong to the current plugin. They avoid rebuilding unrelated collections,
which makes them preferable for state containing picker results, transcript
blocks, or other larger values. Use red::state_set(state) only when replacing
the complete record intentionally. Existing red::state("key") and
red::state_set("key", value) calls remain supported and independent of the
typed record; reserve them for explicit compatibility or high-volume payload
boundaries that cannot safely live inside the typed state snapshot.
#[red::config("key")] binds a one-argument callback to an initial GetConfig
request. #[red::config] requests the complete configuration. Configuration
requests are staged and delivered only after state initialization and activation
succeed. Each key may be bound once per plugin.
#[red::lifecycle("hook")] binds a function to activate, deactivate,
before_exit, state_export, or state_import; activation, deactivation, and
export callbacks take no arguments, while exit and import callbacks take one.
An explicitly annotated hook takes precedence over a conventionally named
function, which remains supported for compatibility.
Red validates these annotations before activation, retains source diagnostics, and registers their generation-safe callbacks in source order. State initialization, activation, configuration requests, and lifecycle registration participate in the same transactional reload as imperative commands and listeners: failed validation, ownership conflicts, initializer failures, or activation failures cannot replace or leak the previous plugin's wiring. Annotations are supported on top-level functions only, including functions in package source modules. Other attribute namespaces are ignored by Red.
red::add_command(name, callback[, metadata]) accepts an optional Json object
with title, category, description, aliases: [String], visible: bool,
and scope. Red uses visible commands to populate the command palette; aliases
are search terms and do not create alternate colon commands. The palette shows
the exact, case-sensitive :Name invocation when it is available and resolves
keymaps from the user's effective configuration.
The optional scope is "editor" by default. Set it to "global" only for
commands that are safe and meaningful when a plugin panel owns focus, such as
workspace pickers or pane toggles. Global commands use the configured
normal-mode binding from focused panels, subject to keys reserved by that
panel's own input handling. Existing two-argument registrations and metadata
without a scope continue to work. Imperative red::add_command and red::on
remain supported for dynamic or conditional registrations, including
process-specific and filesystem-watch-specific event names.
New pickers should use
OpenPicker(title: String, items: [PickerItem], options: PickerOptions, handlers: PickerHandlers).
The host returns an opaque integer handle that may be passed to UpdatePickerItems,
UpdatePickerQuery, UpdatePickerSelection, UpdatePickerStatus, and ClosePicker. Plugins
must not assign or interpret this handle.
red::execute("OpenPicker", "Themes", items, PickerOptions {
placeholder: "Filter themes",
}, PickerHandlers {
changed: theme_changed,
cancelled: theme_cancelled,
selected: theme_selected,
});
PickerHandlers accepts selected, cancelled, changed, query, and action
callbacks; unused handlers may be omitted. changed, query, and action can run
repeatedly. Selection and cancellation are terminal: the host consumes every handler for
that picker before invoking the terminal callback. Closing or replacing the dialog,
reloading its plugin, or unloading its plugin also releases the handlers. Stale handles
are ignored.
Callbacks are retained by the runtime and delivered only to the plugin that opened the
picker. They do not use global picker:*:<id> subscriptions. Picker items and callback
payloads use the declared PickerItem, PickerCancelled, and PickerActionEvent records;
the PickerItem.data field remains Json so a plugin can attach its own payload.
OpenPicker was added in host API 0.3.0. New plugins targeting this Red release should
declare "red_api_version": "^0.7.0". The numeric-ID OpenDynamicPicker API remains
available for compatibility, but new plugins should not use it.
Plugins that collect a multiline request should call OpenComposer(title: String, query: String, history: [String], handlers: ComposerHandlers). submitted receives the complete prompt as a String; cancelled receives a ComposerCancelled record. Both are terminal one-shot callbacks scoped to the plugin that opened the composer, so plugins do not allocate IDs or register synthetic event names.
red::execute("OpenComposer", "Agent prompt", draft, history, ComposerHandlers {
submitted: prompt_submitted,
cancelled: prompt_cancelled,
});
The host owns multiline editing, wrapping, cursor movement, and history navigation; it does not send a callback for each keystroke. Input is limited to 128 KiB so an escaping-heavy prompt remains within the Codex app-server frame limit; an oversized paste leaves the current draft intact and shows a validation message. The prompt starts in Insert mode. Enter or Ctrl-Enter submits; Alt-Enter, Shift-Enter, or Ctrl-j inserts a newline; Escape enters Normal mode. Normal mode uses the same Unicode-aware word motions, character searches, text objects, and transactional edits as file-backed editor buffers: counts, d/c/y operators, f/t/F/T, ;/,, Visual selections, p/P, u/Ctrl-r, dot-repeat, and local macros operate on the prompt itself. / and ? search only the prompt; : and application commands are not exposed. In idle Normal mode, Enter submits and Escape cancels. Enter finishes an active search without submitting, and unfinished operators or visual selections never accidentally send the prompt. Escape leaves Visual or Search mode without closing the composer. Ctrl-c cancels from either mode. Ctrl-p / Ctrl-n moves through the supplied history while preserving the current draft. See keyboard compatibility for terminal-specific limitations and diagnostics.
OpenComposer was introduced in host API 0.3.0. The numeric-ID OpenAgentComposer API and its composer:submitted:<id> / composer:cancelled:<id> events remain available for compatibility with 0.2.0 plugins.
AgentArchiveSession(session_id: String) was also introduced in host API 0.2.0. Use it when Codex app-server has already stopped and the host must not send an interrupt to a replacement process that may reuse the same session ID. Use AgentCloseSession(session_id: String) for a live session that should be closed normally.
Host API 0.9.0 adds AgentResumeSession(cwd, session_id) for rejoining a
persisted Codex thread and AgentForgetSession(session_id) for an explicit new
conversation. The bundled Agent plugin renders
agent:conversation_restore_pending with its composer disabled, then replaces
the cached projection from agent:session_restored; a
agent:session_restore_failed event converts the cached transcript to clearly
archived context.
AgentPrompt automatically attaches bounded editor context containing the active visual selection or a roughly 80-line cursor excerpt, unsaved-state metadata, cursor/range, and intersecting diagnostics. Files outside the workspace, ignored paths, common credential/secret filenames, and binary buffers are omitted. Plugins that need to inspect or explicitly override this context can call GetAgentContext(callback) and AgentPromptWithContext(session_id: String, text: String, context: Json); the context object accepts uri and text fields and is included in the direct Codex turn.
CreateTextPanel, UpdateTextPanel, and AppendTextPanel provide a source-backed conversation surface. TextPanelBlock accepts an id, kind (user, agent, error, or text), format (plain or markdown), and text; the host preserves the source while wrapping and rendering it for the current panel width. These calls were introduced in host API 0.2.0.
Both row and text panels accept side: "left", "right", "top", or
"bottom" in PanelConfig. width is measured in columns for left/right panes
and rows for top/bottom panes. Users can move a focused pane using
Ctrl-w H/J/K/L, resize it using Ctrl-w < / > or Ctrl-w - / +, or drag
its divider. The host remembers widths and heights independently and preserves
the pane's stable ID, source blocks, scroll state, focus, and composer draft.
While dragging, the host highlights only the captured divider. It prefers the
theme's sash.hoverBorder, panelTitle.activeBorder, or focusBorder, then
derives a readable accent when those colors are absent or insufficiently
contrasted. Plugins do not need to configure a border to make resizing visible.
PanelConfig may include composer: Json { placeholder: String, rows: i32 } for a persistent footer composer and header_actions: [Json { id: String, label: String, compact_label?: String }] for clickable, right-aligned header controls. Row panels can also set surface: ThemeStyleSpec and border: ThemeStyleSpec to resolve theme-aware panel foreground, background, and separator colors without affecting other panels. Header actions emit panel:event:<id> using their configured id; compact labels are selected automatically on narrow panels, with the rightmost actions retained when space is especially limited. Focus the footer with FocusTextPanelComposer(id), update its enabled/status state with SetTextPanelComposerState(id, enabled, status?), replace its bounded prompt history with SetTextPanelComposerHistory(id, history), and clear its draft with ClearTextPanelComposer(id). SetTextPanelComposerHistory was introduced in host API 0.15.0; it normalizes, deduplicates, and limits supplied entries without changing the current draft.
A focused footer is a host-owned modal text area, not an editor file buffer. It supports Unicode-safe editing, wrapping, click-to-position cursor movement, counts, Vim word and character motions, d/c/y operators, inner/around text objects, Visual/Visual-line/Visual-block selections, transactional undo/redo, local yank/paste registers, dot-repeat, local macro recording, and prompt-local / or ? searches. Its mode, pending operator, selection, undo tree, register, and search never alter the main editor. File writes, Ex commands, window operations, LSP requests, and plugin callbacks cannot be invoked through textarea keys.
The footer uses the same Enter policy as OpenComposer: Enter or Ctrl-Enter submits; Alt-Enter, Shift-Enter, or Ctrl-j inserts a newline. While the footer is focused, Ctrl-j edits the prompt rather than scrolling the conversation. Escape enters Normal mode. In idle Normal mode, Enter submits and Escape emits composer_blur; Escape from Visual or Search mode first returns to Normal. Ctrl-p/Ctrl-n navigate local prompt history. In Insert mode, Up moves within a multiline draft until the first visual row, then recalls older prompts; once history navigation starts, Up/Down browse entries and Down past the newest entry restores the exact draft and cursor. Ctrl-r opens an empty, editable Search prompts: field with matching history entries previewed separately; repeated Ctrl-r selects older matches, Enter loads the selected match without submitting it, and Escape or Ctrl-g cancels. Normal-mode Ctrl-r remains redo, and / or ? remains prompt-local text search. Ctrl-c remains a pane-level shortcut that emits interrupt without changing the composer draft or focus. Successful submission emits panel:event:<id> with action: "submit" and the complete text; other footer actions include composer_focus, composer_blur, interrupt, clear, new, history, and close. SetPanelVisible(id, visible) hides or restores a panel without discarding its blocks, scroll position, or draft. Replacing text-panel blocks with an empty list resets scrolling and restores tail-following. Footer panels shrink on narrow terminals while preserving an editor viewport.
Codex app-server updates other than assistant text chunks are forwarded to plugins as agent:activity with the normalized update payload. Core editor-tool calls also emit this event with session_update: "editor_tool", status: "in_progress", and a concise title such as Opening src/main.rs or Editing src/main.rs (2 changes). This allows status/tool/plan progress to be displayed without treating it as transcript text. Agent recovery uses agent:conversation_restore_pending, AgentResumeSession, agent:session_restored, and agent:session_restore_failed; plugins must not enable follow-up input while restoration is pending.
Plugins load independently. Source, version, dependency, compile, activation, and runtime
failures quarantine only their owner. red --self-check prints every bundled plugin's
status. Required plugin dependencies must be active or the dependent plugin is
quarantined with the dependency chain.
Plugin subprocesses inherit only the standard execution, locale, temporary-directory, platform, and SSH-agent environment keys. Explicit environment overrides remain allowlisted. Process stdin is limited to 16 MiB, raw output to 2 MiB, individual streaming lines to 256 KiB, and pending process events to 16 (at most roughly 32 MiB of payload); oversized output is reported without letting an untrusted process grow editor memory indefinitely.
buffer:changed, cursor:moved, and viewport:changed describe the latest
editor state. Dot-repeat, macros, counted edits, and visual-block replay may
coalesce these notifications within a bounded group of source-local actions.
Do not use their delivery count as an edit log; use the buffer revision and
read the current document instead. Mode transitions remain observable in order.
Explicit plugin commands, LSP requests, saves, and document switches flush
pending changes before they execute. A long replay publishes intermediate
states and can be interrupted with Ctrl-C without dropping other queued keys.
Event listeners and request callbacks decode host payloads according to their declared
parameter types. Named records, arrays, tuples, and nested Option<T> values are
decoded recursively. Both JSON null and omitted optional fields become None; a
present value becomes Some(value). Existing callbacks declared as Json retain their
dynamic behavior, and additional object fields are preserved for compatibility.
Tagged JSON objects also decode into nominal Husk enum variants. The host recognizes
type, session_update, kind, and $case discriminators and maps snake-case wire tags to
PascalCase variants. Declare an Unknown(Json) case to preserve forward-compatible
payloads when the host adds a new variant. Plugin process events share this host enum:
fn process_finished(event: ProcessEvent) {
match event {
ProcessEvent::Exit { process_id, code, plugin_name } => {
if let Some(exit_code) = code {
red::execute("Print", process_id + " exited with " + exit_code);
}
}
ProcessEvent::Stdout { line, process_id, plugin_name } => {}
ProcessEvent::Stderr { line, process_id, plugin_name } => {}
ProcessEvent::Error { message, process_id, plugin_name } => {}
}
}
When typed records cross back into host actions or plugin storage, None serializes to
JSON null and Some(value) serializes to the value itself, including inside arrays and
nested records. Json remains intentional for persisted user-defined state, arbitrary
configuration, genuinely open-ended process output, and plugin-defined payloads such as
PickerItem.data. Prefer nominal records and enums whenever the host owns the shape.
User plugin files are polled with a 250 ms debounce. A replacement VM is parsed,
typechecked, statically registered, activated, and migrated before it replaces
the live program. A bad save
leaves the previous callbacks and program active and records an active_with_reload_error
status. Host requests, editor actions, logs, and timers produced while staging are
published only after a successful swap. Starting or killing a process from reload-time
activate, state_import, or deactivate is rejected so a failed reload cannot leak
or terminate a subprocess; manage processes from an event or command callback instead.
State is intentionally explicit. A plugin that wants state carried across a successful reload implements:
fn state_export() -> Json { /* return versioned state */ }
fn state_import(saved: Json) { /* validate or migrate saved state */ }
If either hook fails, the replacement is discarded. Successful replacement removes old
commands, event callbacks, pending requests, and VM state before the new registry becomes
authoritative. Plugins should clean up host-owned panels, timers, watchers, and processes
from deactivate.
Shared UiAction records can supply group and description for contextual
keyboard help. Use priority: "reference" for a supported binding that belongs
in the complete reference but not in the compact action strip. Disabled actions
are omitted. The strip and its clickable F1 shortcuts affordance use the same
records, including the current mode and enabled state. F1 opens help above the
active surface; closing help does not replace that surface or its draft.
Host API 0.14.0 adds AgentReadDefaultModel(callback), which returns
{ model_info, error } for the current workspace without creating a thread.
It reads Codex's effective configuration, falling back to the catalog default
when no model is configured. This is a preview; thread settings remain authoritative.
AgentListModels(callback) and
AgentSetModel(callback, session_id, selection). The catalog callback returns
{ models, error }; model entries use Codex's model/list shape. Selection is
{ model: String, effort?: String }. An empty session ID stores the choice for
the next conversation; an existing ID updates only that conversation's next-turn
settings. The callback reports { accepted, error }. No global configuration is
written. agent:model_changed carries { session_id, model_info }, where
model_info contains the effective model, optional provider, and optional
effort. Treat that event as authoritative, and ignore foreign session IDs.
agent:model_rerouted reports { session_id, model } for the running turn only.
For name-oriented lists, PickerOptions.item_layout: "label_first" reserves the
longest filtered label, then aligns annotations and descriptions in shared columns.
Secondary fields disappear before labels are shortened; the default layout is unchanged.
UpdatePickerSelection(handle, item_id) selects a currently visible item without
resetting the query. This is useful after populating a loading picker in place.
SetTextPanelHeaderDetail(id, detail?) updates header metadata in place. Detail
is { text: String, secondary?: String, compact_text?: String, action?: String, shortcut?: String }.
The header compacts its buttons before dropping secondary text. An optional
compact_text preserves essential state in the shortened label. Clicking the visible
detail emits the configured panel action; the shortcut works while the panel is focused and is included in contextual
help. Omitting detail clears it. Draft, focus, scroll, and panel layout survive.