feature-map/composer.md
Rama main · réplica de lectura del repositorio original.
# Composer
The composer is where a user writes a message and chooses how the agent runs
it. The main surfaces are the in-session composer at the bottom of a chat
and the new-session composer on
the landing page. Both show a model/effort pill with a hover tooltip, open a
configuration menu for harness, model, reasoning effort, and permission mode,
and accept attachments and slash commands. The in-session composer also queues
and steers messages while the agent is busy.
Side chats have their own composer. Supported generic side chats reuse the
model/effort picker before their first message and for later turns; changes
apply only to that side chat. See the [side-chat settings recipe](sessions.md).
## Sub-features
- `touch-targets`: native iOS/Android apps and browsers with a coarse primary
pointer and no hover use at least 44 CSS-pixel composer controls and menu rows.
The layout adapts at phone, tablet, and desktop widths; mouse browsers retain
compact controls regardless of viewport width.
- `prompt-link`: opening the new-session page with `?prompt=`
fills an editable draft without starting a session. The consumed prompt is
removed from the URL while other query parameters and the fragment remain.
A new link replaces a parked draft; subsequent edits survive navigation.
**Copy message to new session** opens it with a one-time `?handoff=` token
instead, naming an in-memory copy of the message's text and attachments. That
copy replaces the whole parked draft, even with no text or no attachments; the
token leaves the URL and is released once the composer takes it.
- `offline-hosts`: the new-session host picker keeps other offline hosts collapsed,
revealing five at a time in server recency order in a scrollable list. The selected
offline host and desktop reconnect action stay visible. Closing the menu resets it.
- `pill-tooltip`: hovering the pill shows one tooltip with Harness, Model, and
Connection rows (plus effort when the harness has it).
- `pill-tooltip-suppressed`: the tooltip stays hidden while the model picker is
open and does not reappear when the picker closes.
- `pill-opens-config`: clicking anywhere on the highlighted pill label opens the
configuration menu.
- `host-tooltip`: hovering the host chip shows a tooltip styled like the pill's
with Host and Status rows, hidden while the host menu is open. States:
connected, disconnected, asleep (resumable managed host), unknown, no host,
and a launching or failed sandbox with its stage; before launch also a
selected sandbox and connecting.
- `model-picker`: lists the host's live model catalog, highlights the current
model, and keeps the selection. Long catalogs scroll inside a compact menu
capped at 40% of viewport height, between 20rem and 36rem, and never taller
than the available space.
Ctrl+Shift+M opens it in both composers.
Hovering a row shows the model ID it launches, except when the label already
shows it and on the Fusion row.
A catalog of ten or more models adds a search box above the list that stays
visible while scrolling. When it spans several creators, models group under
collapsible headings with counts (Anthropic, OpenAI, Google, Moonshot AI,
Z.ai, ..., Other models), and the selected model's group opens. The heading
names who made the model, not who hosts it: Claude on Bedrock is Anthropic,
GLM on Databricks is Z.ai. Search matches names, IDs, and creators and shows
matches inside collapsed groups. Same-named rows from different hosts show
the host. Shorter or single-creator catalogs stay a flat list.
States: catalog loading, delayed catalog,
alias vs. full model ID, refused switch (the pill keeps the applied model),
a managed sandbox launching before its host is bound (its provider's default catalog),
long mixed catalog (grouped), search with no matches, unidentified models
(Other models).
Equivalent model-ID spellings use the catalog's display name consistently,
including case, gateway-prefix, and version-punctuation differences; context
variants keep their own labels and checked rows, including in side chats.
- `effort-picker`: reasoning effort for harnesses that support it (Codex, Claude,
Pi, Devin). States: available, unavailable placeholder, max/ultra levels, refused
change (the pill keeps the applied level).
- `side-chat-settings`: the side-chat model/effort pill uses the same catalog
and capability rules. Draft selections apply when the child is created;
live selections update that child's session. A live Claude picker follows
the runner's reported model if a requested change is refused. Native Codex
side chats show these controls after creation; selections apply to the next
turn without changing the parent;
older runners hide these controls.
- `project-session-defaults`: Project settings store a default agent and, for
Claude Code, Codex, and Pi, a default model and reasoning effort; the
new-session composer opened in that project (`/?project=<name>`) starts with
them. The Fork dialog and CLI, API, and scheduled creates don't apply model
or effort. States: no default, a Codex
effort the pinned model lacks (or, with no project model, the remembered
model lacks), an explicit composer pick, remembered Smart Routing, another
agent chosen, and a configured agent that picker curation
(`restrict_to_agent_names`, see `native-harnesses.md`) does not offer, which
New Chat reports as unavailable until the user picks an offered agent while
project settings keeps storing it.
- `config-cycle-hotkeys`: Ctrl+Alt+M cycles models and Ctrl+Alt+E cycles effort
in both composers (Ctrl on every platform). The relevant menu briefly opens
with the selected choice highlighted, preserving draft and input focus. In an
existing session, cycling stages choices locally and applies only the final
choice after two seconds without another press. Enter applies immediately
without sending the draft; Escape cancels. Wrapping back to the applied choice
sends no change. Before launch, choices update the local launch configuration.
Choices follow menu order and wrap, skipping unavailable options.
States: default, model-specific effort, staged/pending/refused changes, read-only,
unreachable, smart-routing effort disabled, terminal/editor focus untouched.
- `effort-terminal-mirror`: an effort change typed in the Codex terminal shows
in the composer, and a composer pick is not reverted by later terminal turns.
- `permission-mode`: the harness's native approval modes; the current mode is
marked and the choice persists. For Codex, New Chat offers Default (Codex's own
default or the user's Codex config), Ask for approval, Approve for me, Full
Access, Read Only, and Bypass approvals & sandbox; a running session offers
Ask for approval, Approve for me, and Full Access.
- `permissions-command`: in Codex native sessions, `/permissions` opens the
composer's approval-mode menu instead of Codex's own popup. Other sessions,
including Claude Code (whose `/permissions` edits tool rules) and Devin, don't
list it and send it as text. States: non-owner or change in flight (error, no
menu), arguments rejected.
- `permission-mode-hotkey`: Shift+Tab in the in-session composer for Claude,
Codex, and Devin native sessions steps through every mode in the chip's menu,
including Full Access and Dangerous, wrapping while the chip is clickable. States:
slash/mention menu acting on Tab, chip disabled, session in a mode the chip can't switch back to
(Codex read-only, Claude Don't ask; menu only), change in flight (Shift+Tab
holds focus without sending another request; Tab still moves focus forward), change
refused or failed (next press moves past it, or retries it when it is the only
other mode), no other mode (native focus move, no
`aria-keyshortcuts`).
- `plan-mode`: the + menu's Plan row turns on plan mode. For Codex it can be
armed on the new-session composer before the first prompt (a "Plan mode"
badge), and the session starts in Codex's own Plan mode. States: armed,
cleared by an agent switch, unavailable for Smart Routing and unsaved custom
agents, and a connected host too old to start Codex in Plan mode (create refused).
- `managed-mcp-picker`: choose administrator-configured services before launch
on a host or sandbox. States: loading, empty, error/retry, selected, unavailable,
remembered selection, explicitly none, restored connection needing sign-in.
- `managed-mcp-sign-in`: selecting an unconnected service opens inline sign-in;
selection and launch wait for success. States: connected, pending, cancelled,
failed, popup blocked, Databricks workspace prompt, bearer token needed.
- `slash-menu`: typing `/` opens commands and skills with keyboard navigation.
Rows show a one-line description. When the pointer or keyboard highlight rests
on a cut-off row for 400 ms, a scrollable tooltip shows the full text;
PageUp/PageDown page through a tall one while the composer keeps focus. The
tooltip fits within the viewport width, and on touch screens the highlight
doesn't open it.
The main composer lists its full built-in set and the session's skills; a live
side chat offers a narrower menu — `/help` plus the session's skills. A built-in
such as `/help` is handled locally; a recognised skill routes through
`slash_command` so it runs on an in-process harness. A native terminal side
chat shows no menu and sends slash text to the vendor TUI. Claude Code and
Codex native sessions offer `/clear` only when `/v1/info` lists `clear` in
`session_event_types`; claude-sdk sessions only when it reports
`session_claude_sdk_clear`. Otherwise a typed `/clear` goes to the harness
as text. A claude-sdk `/clear` continues in a new, empty session on the same
machine and the view follows it. States: loading, error/retry,
unavailable while disconnected, and an incomplete catalog.
- `session-mcp-status`: selecting `/mcp` in a started session replaces the slash
suggestions with the MCP servers enabled for that session and links to harness
Settings with the session’s host preselected. Host-native entries are limited
to the active harness; a claude-sdk session on a host lists its Claude servers,
only the organization-managed ones when its agent sets `skills: none` (or, with a
status saying why, while the session-agent request is loading or after it
fails). A host lookup failure leaves known agent entries visible. It is absent before a session starts and from side chat. When the
server has the MCP registry on, each registry service row also shows its
account state. States: connected; sign-in required, with an inline **Sign in**
that runs the popup sign-in and flips the row to connected, or a **Sign in
from Settings** link for a service that needs a typed token or workspace URL;
unavailable, for a service the account no longer sees; signing in; failed or
cancelled, reported on the row with Sign in available again. A host-native
server, including one pinned by an organization's managed Claude config,
shows connected or sign-in required when the host's sign-in manager
reports it (via `~/.omnigent/mcp-auth-status.json`, see
`docs/MCP_AUTH_STATUS.md`); its **Sign
in** opens the reported URL in a new tab and the panel re-reads the host
inventory every few seconds until the row is connected. In a claude-sdk
session, the next turn after the row turns connected reaches that server
without starting a new session. Custom agent servers carry no account state.
A claude-sdk session whose agent is running also shows each host server's
live connection in that session (connected, failed with its error, needs
sign-in, connecting, disabled, or not loaded when the CLI omits it),
including servers only the CLI loaded; a failed or needs-sign-in row whose
account needs no sign-in offers **Reconnect**, which shows reconnecting,
then the refreshed state or the reconnect error. Before the first turn the
panel says live status appears once the agent is running. Read-only viewers
see the state without Reconnect. **Go to Settings** keeps the session's host
selected even when it is a sandbox host the host picker otherwise hides.
While the host has an enterprise `managed-mcp.json`, Claude rows are only the
servers that file pins (no user or plugin rows), and Claude sessions reach
Omnigent's tools only through the file's `omnigent` relay entry
(`docs/CLAUDE_MANAGED_MCP.md`).
- `codex-plugin-skills`: typing `$` lists enabled Codex plugin skills in both
composers, including cached plugins whose marketplace inventory is unavailable.
Selecting a row inserts its namespaced identifier.
When neither `codex plugin list` nor Codex's loaded-skill catalog can be read,
the menu names the failure with Retry beside the skills it did read, instead
of "No skills available".
- `new-command`: `/new` opens a blank new-session composer in the same project;
working-directory selection follows the project's defaults, not the old session.
- `attachments`: attach button, paste, and drop onto the transcript; chips can
be removed. State: unsupported file type rejected without losing the message.
- `at-mentions`: a bare `@` in the in-session or new-session composer shows a
"Sessions" row. Opening it (click, Enter, Tab or →) shows a "Subagents" row
for the current session's subagents, then every other session the sidebar
has loaded; Back or Esc goes up one level. In native coding-agent sessions workspace files and folders sit
below the Sessions row. Typing lists the top matching sessions inline above
the files and, after a short pause, searches session titles and the
workspace. Picking a row puts a
pill-styled `@name` token in the sentence; Backspace deletes a token whole, and
deleting it untags the item. On send each token becomes a marker in place
(`[Attached: path]` or `[Session: "title" id=… subagent=yes|no]`), and a
tagged session adds one tool hint at the end, so native terminals show the
markers as typed text. The sent message shows the tokens as inline pills; a
session pill opens that session. The new-session composer lists the same
sessions (no subagents yet) with the same title search; its files come from
the host directory listing only, without the recursive workspace search.
States: file listing still loading (Enter holds; a pending session search
does not hold it), no match (Enter sends the literal text), late results
arriving (the highlighted row stays put), a file and a session with the same
name (the later one gets a suffix such as `@README.md (file)`), Attach to
agent from the file viewer (adds a token at the end).
- `dictation`: the microphone button beside the message box dictates speech
into the composer at the caret; ⌘/Ctrl+Alt+V toggles it unless focus is in a
terminal or code editor. While a browser or server take is listening, Enter
keeps the dictated text and Escape discards it. The button is absent when
neither the browser nor the server can transcribe; the desktop app's browser
engine cannot, so there the button uses the connected server's dictation.
Without it, the button starts macOS Dictation in the message box on a Mac,
which then uses its own Dictation controls, and is absent elsewhere.
- `send-shortcut`: Enter or Mod+Enter, chosen in settings; only one gesture sends.
Shift+Enter and Alt/Option+Enter insert a newline in both composers. The
Inbox reply box follows the same shortcut.
- `blocked-start`: hovering or focusing disabled Start explains the missing
setup. Clicking, tapping, or trying to submit pins that explanation until
dismissed or the missing setup is completed.
- `sandbox-profiles`: participating providers add a [profile picker](sandbox-profiles.md)
to New session; Start waits for an authorized, current selection.
- `list-formatting`: typing `-`, `*`, `1.` or `1)` then Space at the start of a
line indents it as a list item; the newline gesture continues the list
(numbers increment) and ends it after an empty item.
- `focus-hotkey`: Ctrl+Shift+L focuses the in-session message box from anywhere
in the session view (not on mobile).
- `new-session-hotkey`: ⌘+Shift+O on macOS or Ctrl+Shift+O on Windows/Linux
opens and focuses the new-session composer in browsers. Native shells use
⌘/Ctrl+N. Embedded apps require focus inside the app.
- `queue-and-steer`: messages sent while the agent is busy wait in a queue and
can be steered into the running turn. Side-chat composers queue and steer the
same way, with their own queue strip.
A message sent mid-turn to a native session says the agent will read it
after its current step in the timestamp row. It appears on desktop hover or
focus and stays visible with truncation on touch, without adding row height.
- `host-offline-hold`: a message sent to a native session whose host is offline
waits in the queue as "Waiting for host", ahead of other queued messages, and
sends once the host reconnects; no failed turn is recorded.
- `browser-pointer-queue`: desktop Design-mode instructions follow the same queue
and always-steer preference, keeping their element screenshots.
- `pi-compact`: `/compact` follows the normal queue/Always steer preference.
Send now interrupts the turn and compacts. During compaction, queued messages
wait in the web queue; immediate sends are retained by the Pi integration.
- `draft-persistence`: unsent text survives arriving messages and prompts.
- `prompt-history`: Up starts history recall only from empty text in an existing
session. Up/Down at the text boundaries browse recalled messages; editing one
returns to normal caret navigation. Whitespace and queued messages recalled
for editing remain drafts.
- `edit-sent-message`: Edit and resend (pencil) on your own sent message copies
its text and "@"-mentions into that pane's composer; attachments stay with the
original message, which the agent already has. The transcript is unchanged.
Sending is a new message, steered or queued like any mid-turn send, and Edit
never stops the agent. A non-empty draft asks "Replace your draft?" first.
Side chats, grid session panes, and writable sub-agent Workspace tabs edit
into their own composer, where "@"-mentions return as attachment markers. Edit
is hidden on messages the server hasn't accepted yet, messages with no text,
other contributors' messages (shared session or not), and read-only composers.
- `delivered-send-recovery`: a sent message stays out of the composer when its
HTTP acknowledgement is lost. Delivery during draft restoration clears the
untouched draft; user edits remain. The "Something went wrong" card from the
failed POST clears once the delivered message appears, live or on reconnect.
A reload while the server still holds a main-composer send (for example during
a sandbox wake) gives its text back unless the session already has that message.
- `managed-first-input`: submitting a new managed session saves its first input
and attachments before provisioning. Follow-ups sent during startup are saved
in order too. They run even if the creating browser closes; reopening shows
each message once. If the sandbox never comes up, each saved message stays in
the transcript marked **Not delivered**, live and after reload. **Copy message
to new session** opens New Chat with its text and attachments.
- `arclet-dbcert`: desktop Arclet launches check laptop credentials first. A
missing or expired dbcert preserves the draft and shows a yellow refresh
banner instead of provisioning. Skip continues the current launch without
waiting for the check. Refresh is explicit; launch needs another submit after
renewal. States: valid, invalid, checking, skipped, refreshing, failed, and
CLI not installed.
- `launch-action-required`: while a managed launch needs the user to do
something, such as a sandbox still waiting for its owner's credentials, the session's chat
transcript shows a yellow banner with the deployment's instructions. Provisioning
continues, and the banner leaves when the launch moves on. Dismissing hides
only that warning; a later one shows again.
- `custom-agent-placement`: New session and Add nested agent can launch an uploaded
custom agent on a fresh sandbox with repositories when `session_bundle_workspaces`
is advertised. Nested custom agents can also use new or existing worktrees on a
connected host. Older servers disable the unsupported worktree combination and
retain the registered-agent fallback on sandbox selection. Managed creates send
the first task and attachments with the bundle when durable input is supported.
- `import-bundle`: Create custom agent → Import bundle installs a `.tar.gz` agent
bundle and selects it. States: import in flight (Cancel and Create locked),
rejected bundle, imported agent missing from the refreshed list, and hidden on
a server without agent install.
- `mobile-labels`: on narrow screens labels collapse to icons without
overlapping the stop button.
## How to get to it (user POV)
**In-session composer** (open any session):
- Hover the model/effort pill to see the tooltip; click it to open configuration.
- Hover the host chip (status dot and machine icon) to see the host and whether
it is connected; click it to open the host menu. The status indicator remains
visible while connecting or without a selected host; the label describes
those states without claiming Online or Offline.
- In configuration, open the model picker, the effort picker, or the permission
mode menu.
- With focus in the prompt, press Shift+Tab to step the permission mode without
opening the menu; the draft and focus stay put.
- For a custom `claude-sdk` or `codex` agent session, open configuration and
pick an effort rung; the ladder is the harness's declared family (anthropic
or openai) and the pick persists on the session. Changing effort after a
completed turn applies on the next conversational turn and keeps the history.
Claude SDK `/compact` still operates on the existing client's context.
`/effort default` restores the harness's default. This is the in-session
dial, distinct from the new-session composer's effort choice.
- Press Ctrl+Shift+M (Ctrl on every platform) to open the model picker without
the gear; press Ctrl+Shift+L to focus the message box.
- In a new native session on a managed sandbox, open the model picker while it
shows **Provisioning sandbox…**: it lists the provider's default catalog, the
rows the new-session picker offered, until the session's host or its own
catalog can answer. A pick is saved for the launch.
- Press Ctrl+Alt+M or Ctrl+Alt+E to cycle models or effort; keep typing while
the temporary menu shows the selected choice.
- For a Codex session, open the Terminal view and change effort there, then
return to Chat.
- Type `/` in the message box; attach files with the button, by paste, or by
dropping them on the transcript.
- Select `/mcp` to replace the suggestions with the MCP servers enabled for the
session. A registry service that needs sign-in shows **Sign in**; click it
and finish the popup, or follow **Sign in from Settings** for a token or
workspace service. A host server that needs sign-in shows **Sign in** too,
opening the page its host reported in a new tab. An empty list points to Settings; the command is absent
on the new-session composer.
- For Codex plugin skills, type `$<skill-name>` in Chat and select the
namespaced suggestion. The vendor terminal owns its own completion menu.
A plugin catalog the host could not read is named in the Skills section with
a Retry link.
- Click the microphone button, or press ⌘/Ctrl+Alt+V, to dictate into the
message box. For browser or server dictation, press Enter to keep the text or
Escape to discard it. In the desktop app the button uses the connected
server's dictation; without it, a Mac opens macOS Dictation in the message
box (stop it with the macOS Dictation controls), and other platforms show no
button. A side chat's composer ("Ask a side question...") shows the same
button under the same rule; the shortcut belongs to the main composer.
- Type `@` mid-sentence and pick a session, a subagent of this session, or (in a
native coding-agent session) a file or folder; the pick becomes a pill in the
sentence. Send it, then click the session pill in the sent message to open
that session. From the file viewer, **Attach to agent** adds a file token to
the end of the draft.
- Send while the agent is working to queue a message, then steer it.
- Hover one of your sent messages and click Edit and resend (pencil) to copy
its text into the message box, revise, and send it as a new message.
Attachments stay on the original message.
- With the in-session message box empty, press Up to recall a previous prompt
(or the last queued message). While writing a draft, Up stops at the top.
- With a native session's host offline, send a message; it waits in the queue
as "Waiting for host" until the host reconnects.
- Type `/n`, press Tab to complete `/new`, then Enter to open the new-session
page in the same project. Selecting **/new** in Commands also opens it.
**Desktop browser pointer** (open a session in the desktop app):
- Choose **+ → Browser** to open a Browser soft tab, navigate to a page, enable
Design mode, and click an element. Type an instruction in the floating popup
and use Send or Enter.
- Change Settings → Always steer (on by default) to choose whether
busy-session follow-ups queue or send immediately. Existing queued messages
retain FIFO ordering.
**New-session composer** (landing page, or New session):
- **Add nested agent** opens this composer scoped to the source session, with
that session's sandbox preselected. See [sibling entry points](sessions.md#how-to-get-to-it-user-pov).
If creation is rejected, the same composer restores the prompt and attachments.
- Select **Codex → + → Plan** and confirm the **Plan mode** badge before sending.
- Write a prompt or attach a file before choosing a working directory. Hover
or focus disabled Start to see what is missing; click, tap, or press Enter to
keep the tooltip open. Click elsewhere or press Escape to dismiss it. Choose
a directory to clear the tooltip and enable Start.
- Open `/?prompt=Explain%20this%20code`. Check that the
draft is filled and editable, no session starts until Send, and an embedded
deployment stays under its mount path. Browser proof:
`tests/browser_ui/chat/test_prompt_url.py`; router and draft edge cases:
`web/src/shell/NewChatDialog.test.tsx` (`prompt URL handoff`).
- In the managed desktop app, choose **Arclet Sandbox** and submit a task with
missing or expired laptop credentials. Click **Refresh dbcert** in the yellow
banner, finish browser sign-in, then submit the preserved task again. Sending
alone must not open the browser, and renewal alone must not create a sandbox.
While **Checking laptop credentials…** is visible, **Skip** starts that launch
without waiting for verification or opening sign-in. Later launches check again.
- On a deployment whose launches can report that the user needs to act, start a
managed session whose launch needs you, e.g. a sandbox still waiting for its
owner's credentials. The session's chat
transcript keeps showing **Provisioning sandbox…** and adds a yellow banner
saying what to do. The terminal view doesn't show it.
- Open the host chip, expand **Offline hosts (N)**, then **Show more**. Check the
same menu on mobile and with a managed sandbox available. Offline rows remain
disabled; a remembered offline selection still requires an explicit switch.
- With the browser app focused, press ⌘+Shift+O (macOS) or
Ctrl+Shift+O (Windows/Linux) to open and focus a clean new-session
composer.
- Hover the model/effort pill to see the tooltip.
- Hover the host chip to see the selected host or sandbox and its connection.
- Pick a harness, then open its configuration for model, effort (Codex, Claude,
Pi, Devin), and permission mode before the session exists. Ctrl+Shift+M also
opens the agent/model picker here.
- Press Ctrl+Alt+M or Ctrl+Alt+E to cycle models or effort before launch.
The menu previews the choice briefly while focus stays in the message box.
- With a managed sandbox selected and no connected host, the model picker lists
the sandbox's default catalog when `/v1/info` advertises `default_models` (an
unbound gateway with `sandbox.model_discovery`, or a provider hook): rows under
the provider's label plus a "Default" row, for native and SDK agents.
A failed listing shows its error and keeps Default launchable; a
harness the catalog cannot serve keeps its ordinary list. When the provider
reports `unsupported` (no model can serve the harness), the composer shows
its reason and submit stays disabled; opening the picker (including by
Ctrl+Shift+M) or its "Other..." harness submenu prefetches the shown harness
rows' previews, and a harness row
whose preview is `unsupported` renders disabled with that reason. Agent rows
(bundle and your own) are never grayed out, since they can switch brains or
bring their own provider; selecting one with no models still blocks submit,
and a blocked brain harness is named in the message. A failed or timed-out
preview never disables anything. This is distinct from a connected host's
own catalog and from a bound (`inference_models`) target. Covered by
`tests/e2e_ui/start_session/test_sandbox_gateway_models_without_binding.py`,
`tests/e2e_ui/start_session/test_sandbox_model_selection.py::test_optional_gateway_preview_without_a_host`
and `tests/e2e_ui/start_session/test_sandbox_model_selection.py::test_sandbox_harness_without_reachable_models_blocks_launch`.
- Open a project's actions menu in the sidebar → **Project settings**, pick
Claude Code, Codex, or Pi as the agent, then set **Model** and **Effort**.
Click **New session in project** and check the pill shows both.
- Attach files or type `/` before the first send.
- For a managed sandbox, submit a prompt and close the browser once the session
URL appears, while provisioning is still in progress. Reopen that URL after
startup and check that the first turn completed without another send. Repeat
with an attachment and with the original tab left open.
- While the managed session is still starting, send two more messages from the
in-session composer, then close the browser. Reopen after startup and check
that all three reached the agent in order. Ordinary busy-session queueing
resumes after the first input is delivered.
- For a managed sandbox that fails to start (e.g. an Arclet whose host never
comes online), send a prompt with an attachment and wait for the failure. Check
the prompt stays marked **Not delivered**, also after reload. **Copy message to
new session** opens New Chat with the text and attachment.
- Select a Codex host and workspace, then type `$<skill-name>` and select the
namespaced plugin suggestion before starting the session; an unreadable
plugin catalog shows the same warning and Retry here.
- With an Arca connected, choose **Arclet Sandbox** with a native terminal agent
and type `/` for Claude or `$` for Codex before the first send. The menu previews that Arca's home-level
catalog; selecting a skill completes the prompt while the launch stays on Arclet.
Skill roots or plugins installed only on the connected host may be missing on
the sandbox. With no connected Arca, host skills become available after the sandbox starts.
SDK agents continue using their bundled skills before the sandbox starts.
Browser coverage: `tests/browser_ui/chat/test_arclet_skills.py`.
- Open **MCPs** in the workspace bar above the composer (the plug icon when space is tight), then turn
services on or off in **Tools for this session** before launching on a host or
sandbox. Turning a service off keeps its account connected. Explicit selections
are remembered for new sessions in this browser; an unsent draft takes precedence.
Reload to verify both remembered selections and choosing none.
If the catalog fails, retry or turn off saved services by ID to launch without
MCPs. The error itself leaves preferences intact.
- Turn on an unconnected OAuth service to open its sign-in popup. Complete sign-in
to select it; for Databricks, first enter the workspace URL and choose
**Connect workspace**. Bearer-token services require **Manage MCP accounts**
before selection. Use **Reconnect** if a selected service loses its connection.
- Dictate with the microphone button or ⌘/Ctrl+Alt+V before the first send,
under the same desktop rule as above.
- Open the agent picker's custom agents, then Create custom agent → Import
bundle: pick a `.tar.gz` agent bundle. It installs, closes the dialog, and
selects the agent, which stays listed after a reload. A rejected bundle (for
example a server agent's name) keeps the dialog open with the server's reason.
Under picker curation (`restrict_to_agent_names`, see
`native-harnesses.md`) this New Chat entry point is absent: a curated
deployment offers no bundle import from New Chat.
**Fork dialog** (Fork in the session header menu, or Fork from here on a
message, with a native target): the same model, effort, and permission mode
pickers, seeded from the source session when its harness family is kept.
**Mobile** (either composer on a phone-sized viewport): host and harness/model
controls live in the top-right glass group. The composer starts as one row with
Add, hint text, microphone, and Send; focusing the textarea expands it into a
stacked layout with permission controls. Add alone does not expand it. On the
landing page, choose the working directory below the headline; in a session,
find the current directory in the session-actions menu. Running background
tasks remain accessible from the session header.
**Side chat** (from an existing session you own): choose **+ → Start a new side
chat** in the composer, **Workspace → + → Side chat**, or **Ask in side chat** on
selected text. Editors and viewers of a shared session don't get these. For native Codex, send the first message before using its model/effort
pill; later selections apply on the next turn. For other supported harnesses,
open the side-chat model pill to choose model/effort before or after its
first message. Follow the **Side-chat settings** recipe in [Sessions](sessions.md)
for draft persistence and parent-isolation checks. The microphone beside
**Ask a side question...** starts dictation. Edit and resend (pencil) on a sent
side-chat message loads it into that side chat's composer, not the main one.
## Driving it with the repro environment
Preconditions: a running instance (`verify-env start`, then `verify-env
doctor`), the built web UI, and mock replies configured for any journey that
sends a message. Run e2e tests through the instance:
```sh
verify-env run -- python -m pytest <test> --ui-skip-build --video=on \
--output="$VERIFY_EVIDENCE/composer"
```
Tests under `tests/browser_ui/` stub every backend call and need no instance:
`uv run pytest <test> --browser-ui-skip-build --video=on --output="$VERIFY_EVIDENCE/composer"`.
For managed MCP journeys, the server must have an administrator-configured
registry enabled and a service visible to the current account; otherwise the
picker is hidden or the catalog is empty. Host and sandbox runners must support
managed MCP services. Use a fictional local OAuth provider for live sign-in
checks and allow popups; the browser tests below stub the catalog and callbacks.
Other connection providers alone do not enable the managed MCP picker.
- **`touch-targets`, new-session and in-session composers:**
`tests/browser_ui/chat/test_composer_geometry_contract.py::test_landing_and_live_cards_share_the_geometry_contract`
uses the built SPA with mocked APIs (no server required), comparing touch and
mouse input at phone, tablet, and desktop widths.
- **`blocked-start`:** on a fresh new-session page with no working directory,
enter a prompt or attach a file. Hover disabled Start, click it, then move
away: its tooltip should stay visible. Check dismissal by outside click,
Escape, or tabbing away from focused Start, plus Tab focus and Enter submission;
no session should be created.
Choose a working directory and check that the tooltip disappears and Start
enables. Repeat with a phone viewport and touch input. Component coverage:
`web/src/shell/NewChatDialog.test.tsx`.
- **`offline-hosts`:**
`tests/e2e_ui/start_session/test_start_session.py::test_start_session_keeps_offline_host_selected`
covers the connected-host and managed-sandbox variants with stubbed host responses.
Repeat at a phone viewport; desktop reconnect exceptions also have component
coverage in `web/src/shell/NewChatDialog.test.tsx`.
- **`pill-tooltip`, `pill-tooltip-suppressed`, in-session:**
`tests/browser_ui/chat/test_composer_tooltips.py::test_pill_uses_one_tooltip_and_suppresses_it_while_picker_is_open`
(runs with zero and one turn).
- **`pill-tooltip`, new-session composer:** no browser-level coverage; the web
unit tests for the new-session dialog cover it. Manually: open the landing
page, hover the pill, and expect the same styled rows as in a session, not a
plain browser title tooltip.
- **`host-tooltip`, in-session:**
`tests/browser_ui/chat/test_composer_tooltips.py::test_host_chip_tooltip_names_the_host_and_matches_the_pill`
(connected, menu suppression, same surface as the pill); disconnected and
asleep in `tests/e2e_ui/sessions/test_host_badge.py`; a provisioning sandbox
handing over to its connected host in
`tests/e2e_ui/start_session/test_managed_first_message_routing.py::test_managed_first_message_rebinds_live_stream_across_replicas`.
- **`host-tooltip`, new-session composer:** no browser-level coverage; the web
unit tests for the new-session dialog cover it. Manually: open the landing
page, hover the host chip, and expect the same card as the pill's tooltip
with the host and its status, not a plain browser title tooltip.
- **`pill-opens-config`:**
`tests/e2e_ui/chat/test_composer_pill_hover_matches_hit_area.py::test_composer_pill_highlighted_label_is_clickable`
- **`model-picker`, in-session:**
`tests/e2e_ui/chat/test_claude_model_picker.py::test_claude_native_picker_lists_only_live_databricks_models`,
`tests/e2e_ui/chat/test_claude_model_picker.py::test_claude_native_picker_updates_after_delayed_catalog`,
`tests/e2e_ui/chat/test_claude_model_picker.py::test_claude_native_picker_highlights_the_reported_model`,
`tests/e2e_ui/chat/test_claude_model_picker.py::test_claude_native_alias_selection_persists`.
These cases patch browser session/catalog responses; they do not drive a
native model change.
A launching sandbox's provider catalog (Claude, Codex, and Pi) and its
replacement by the host's or the session's own catalog:
`tests/browser_ui/chat/test_model_catalog.py::test_launching_sandbox_offers_its_provider_catalog`,
`tests/browser_ui/chat/test_model_catalog.py::test_bound_host_catalog_replaces_the_launch_preview`,
`tests/browser_ui/chat/test_model_catalog.py::test_session_catalog_replaces_the_launch_preview`
- **`model-picker`, in-session with a real Claude terminal (nightly):**
`tests/e2e_ui/chat/test_claude_model_picker.py::test_claude_native_picker_repeated_pins_reach_the_real_terminal`
selects pinned Opus through the gear, repeats it through the model pill,
then returns to Sonnet through Ctrl+Shift+M. Each uses the real browser,
server, owned isolated host, runner and Claude CLI. Subsequent mock requests
retain the wire model's exact generation; native status and `requestedModel`
retain the selected `[1m]` suffix, and the correlated API request includes
the `context-1m` beta. Chat and the persisted transcript each contain the
reply once, with no model-change error. Requires Claude Code and tmux. Use
a clean container when machine-managed Claude settings override the mock
endpoint; this does not establish live gateway compatibility.
- **`model-picker`, row hover (both composers):** no browser-level coverage;
`web/src/lib/composerModelLabel.test.ts`. Manually: hover a row labeled with
a family name and expect its full model ID; the Fusion row shows none.
- **`model-picker`, long mixed catalog:** in session,
`tests/browser_ui/chat/test_model_catalog.py::test_long_catalog_groups_by_creator_and_searches_by_keyboard`
opens on the selected creator, keeps the height cap and search while
scrolling, and picks a model in a collapsed group by keyboard. Creator
attribution is in `web/src/lib/modelCreators.test.ts`; grouping, search,
and keyboard cases for every composer are in
`web/src/components/composer/ComposerConfigSections.test.tsx`. The mock
environment has no long multi-creator catalog, so check the new-session
composer and side chat manually: pick Pi on a host whose Pi lists models
from several labs, open its configuration, expand a creator, search for
another creator's model, and pick it.
- **`model-picker`, new-session composer:**
`tests/e2e_ui/start_session/test_model_flows_prelaunch.py::test_claude_default_entry_names_the_true_default`,
`tests/e2e_ui/start_session/test_native_picker_cli_parity.py::test_codex_picker_offers_the_clis_catalog_and_default`
(real host and Codex CLI; custom, bundled, and hidden defaults on desktop and mobile),
`tests/e2e_ui/start_session/test_codex_catalog_availability.py::test_codex_prelaunch_label_reflects_catalog_availability`
(desktop and mobile; populated catalogs without a default, empty catalogs, and probe failures),
`tests/e2e_ui/start_session/test_composer_transition.py::test_selected_model_survives_delayed_create`
- **`effort-picker`, new-session composer:**
`tests/e2e_ui/start_session/test_codex_effort_prelaunch.py::test_new_codex_session_gear_offers_reasoning_effort`
- **`project-session-defaults`:**
`tests/e2e_ui/sessions/test_project_settings_model_default.py::test_project_effort_default_seeds_new_session`
(real host and Claude CLI against the mock model),
`tests/e2e_ui/sessions/test_project_settings_model_default.py::test_project_settings_model_and_effort_defaults_round_trip`;
ladder and precedence cases in `web/src/shell/ProjectSettingsDialog.test.tsx`
and `web/src/shell/NewChatDialog.projectPrefill.test.tsx`.
- **`effort-picker`, SDK in-session composer:**
`tests/e2e_ui/chat/test_sdk_effort_dial.py::test_claude_sdk_session_offers_the_effort_dial`
covers selection, persistence and reload. The stale-catalog cases in
`web/src/store/chatStore.test.ts` cover saving while catalog refresh stalls.
The effort-transition cases in `tests/inner/test_claude_sdk_executor.py` and
`tests/inner/test_codex_executor.py` cover subsequent turns and default reset.
Manually, complete a turn, change effort, then send a follow-up; inspect the
mock provider request to confirm the new effort and prior conversation.
- **`model-picker`, `effort-picker`, refused change:** the rollback cases in
`web/src/store/chatStore.test.ts` and the composer case in
`web/src/pages/ChatPage.composer.test.tsx` (`pnpm --dir web test`). A pick
during Codex startup waits for the terminal, then applies. If startup does
not become ready within the recovery budget, the pill keeps the previous
selection; server/runner coverage is in
`tests/server/integration/test_codex_model_change_after_idle_reap.py`.
Real Claude refusal coverage (nightly):
`tests/e2e_ui/chat/test_claude_model_picker.py::test_claude_native_refusal_preserves_a_newer_same_model_selection`
pauses only its owned CLI while a browser model change is pending, then
repeats that selection in a newer silent API write with a different effort.
The real request returns 503 and an actionable error card; the pending
indicator clears, the reported model stays unchanged, and the newer saved
model/effort survive. Cleanup kills the paused CLI, so this case does not
prove a retry after refusal.
- **`effort-terminal-mirror`:**
`tests/e2e_ui/chat/test_codex_effort_terminal_composer_mirror.py::test_codex_terminal_effort_change_reaches_composer`,
`tests/e2e_ui/chat/test_codex_effort_terminal_composer_mirror.py::test_composer_effort_pick_survives_terminal_turns`
- **`permission-mode`:**
`tests/e2e_ui/chat/test_claude_model_picker.py::test_claude_native_permission_mode_switch_persists`
- **`permission-mode-hotkey`:** in session with a real Claude CLI,
`tests/e2e_ui/chat/test_claude_native_permission_switch_from_dont_ask.py::test_shift_tab_cycles_permission_mode_from_composer`.
The E2E is nightly; Codex and Devin have component tests but no dedicated
shortcut E2E in the suite.
Component tests cover the in-flight, refused, downgraded, next-lap, menu-reset,
Codex/Devin full cycles, slash-menu, viewer, read-only
and unreachable states in `web/src/pages/ChatPage.composer.test.tsx`, mention-menu
priority in `web/src/pages/ChatPage.mention.test.tsx`; mode ordering and
wraparound are in
`web/src/lib/permissionModeCycle.test.ts`.
`web/src/shell/NewChatDialog.test.tsx` checks that Shift+Tab on the landing
page leaves the launch mode unchanged and does not prevent focus traversal.
- **`permissions-command`:** with a real Codex CLI (nightly),
`tests/e2e_ui/chat/test_codex_native_permissions_command.py::test_permissions_command_opens_codex_approval_picker`.
`tests/browser_ui/chat/test_slash_menu.py::test_permissions_command_opens_the_codex_approval_picker`
checks that the menu closes after a pick. Gating, the owner check, arguments,
and the plain-text fallback on Claude Code, Devin, and claude-sdk are in
`web/src/pages/ChatPage.composer.test.tsx`.
- **`plan-mode`:** with a real Codex CLI and a mock model, creation in Plan mode
and subsequent composer toggles preserve the preset and agent instructions in
`tests/e2e_ui/chat/test_codex_plan_mode_instructions.py::test_codex_plan_mode_creation_and_toggles_preserve_instructions`.
Landing-page arming is in `web/src/shell/NewChatDialog.test.tsx`; the older-host
refusal is in `tests/server/integration/test_multipart_create_host_launch.py`.
- **`model-picker`, `effort-picker`, `permission-mode`, Fork dialog:** the
permission-mode picker has component coverage in
`web/src/shell/ForkSessionDialog.test.tsx`; drive the model and effort
pickers by hand: fork a Codex session, pick another effort in the dialog, and
expect the new session's pill to show it. See [sessions](./sessions.md).
- **`managed-mcp-picker`, new-session composer:**
`tests/e2e_ui/sessions/test_mcp_registry_connections.py::test_managed_mcp_selection_is_sent_with_launch`
covers desktop sandbox and mobile local-host launches, including a long catalog
in a short viewport with a reachable last row and visible heading and footer.
After launch, the same cases use desktop agent info and mobile **Agent info** to select managed
services on a builtin agent and remove unavailable references after registry
disablement, while custom MCP controls remain read-only. The module has four
browser cases: these two launches, settings sign-in routing, and the
Databricks sign-in launch below. Selection persistence,
explicit opt-out during a persistent catalog failure, launch gating during
sign-in, and an absent picker without registry enablement
are covered in `web/src/shell/NewChatDialog.flow.test.tsx`.
- **`managed-mcp-sign-in`, new-session composer:**
`tests/e2e_ui/sessions/test_mcp_registry_connections.py::test_databricks_connects_from_launch_picker`
covers the workspace prompt and successful sign-in before a desktop host
launch. `web/src/components/McpRegistry.test.tsx` and
`web/src/lib/mcpRegistry.test.ts` cover selection after sign-in, cancellation,
retry, reconnect, and closed or blocked popups (`pnpm --dir web test`).
`tests/server/integration/test_mcp_gateway_http.py::test_real_http_oauth_refresh_through_gateway`
covers real OAuth, refresh, account reuse, and disconnect without a browser
or external credentials (`uv run --no-sync pytest tests/server/integration/test_mcp_gateway_http.py`).
Manually on desktop and mobile: select an unconnected OAuth service, confirm
launch is disabled while sign-in is pending, then approve in the popup and
expect it to close with the service checked and the selection count updated.
Repeat with popup cancellation: expect an error, an unchecked service, and a
usable retry. Launch on a host and a sandbox with the selected service and
confirm its tools are available on the first turn.
- **`new-command`:**
`tests/browser_ui/chat/test_slash_menu.py::test_new_command_opens_landing_in_current_project`
covers Enter, Tab then Enter, and clicking the command, with first-class,
legacy-label, off-sidebar, partial-sidebar, and no-project sessions.
`tests/browser_ui/chat/test_slash_menu.py::test_new_command_respects_unfiled_sidebar_row`
covers explicit removal from a project;
`tests/browser_ui/chat/test_slash_menu.py::test_new_command_project_lookup_error_is_visible_and_retryable`
covers failed lookup and retry without using a stale project name.
`tests/browser_ui/chat/test_slash_menu.py::test_new_command_deleted_project_opens_unfiled`
covers deleted projects with dangling session membership.
No running agent is needed.
- **`slash-menu`:**
`tests/browser_ui/chat/test_slash_menu.py::test_slash_menu_tracks_real_focus_and_wrapping_keyboard_navigation`,
`tests/browser_ui/chat/test_slash_menu.py::test_slash_menu_shows_full_skill_description`,
`tests/browser_ui/chat/test_slash_menu.py::test_slash_menu_shows_a_long_single_match_from_the_keyboard`,
`tests/browser_ui/chat/test_slash_menu.py::test_slash_menu_highlight_opens_no_tooltip_on_a_touch_screen`,
`tests/browser_ui/chat/test_slash_menu.py::test_slash_menu_bounds_a_very_long_skill_description`,
`tests/e2e_ui/chat/test_side_chat_slash_command.py::test_side_chat_slash_menu_and_skill_invocation`;
incomplete catalog:
`tests/e2e_ui/chat/test_codex_skills_menu_inventory_failure.py::test_failed_plugin_inventory_is_not_shown_as_an_empty_catalog`.
claude-sdk `/clear` from the menu, with a real Claude CLI on the mock model:
`tests/e2e_ui/chat/test_claude_sdk_clear.py::test_claude_sdk_clear_follows_a_new_empty_session`.
Manually, in a claude-sdk session with one finished turn, type `/cle` and
pick **/clear**, then repeat in a new session by typing `/clear` and pressing
Enter. Each time the view moves to an empty chat on the same machine, and the
original chat ends with a notice linking to it. Menu and typed entry, and the
capability gate, are in `web/src/pages/ChatPage.composer.test.tsx`.
- **`session-mcp-status`, in-session composer:** component coverage lives in
`web/src/pages/ChatPage.composer.test.tsx`. Manually open a started session,
select `/mcp`, and expect the effective MCP names and descriptions under
**Enabled for this session**. Repeat with no MCPs and expect the empty state
and Settings link; confirm `/mcp` is absent on the landing page and side chat.
Native host entries must match the active harness; a host error is shown
without hiding MCPs declared by the bound session agent. While the host loads,
its loading status stays visible alongside agent entries; a session without a
known host shows unavailable status rather than an empty inventory (a claude-sdk session
without a host lists only its agent's servers). In a
claude-sdk session on a host with Claude MCP servers, expect them listed, and
only the managed ones when the agent sets `skills: none`. Click
**Go to Settings** and confirm the session’s harness and host are selected
(Claude’s page for a claude-sdk session). For the post-sign-in journey, start
a claude-sdk session while a Claude server needs sign-in, finish the sign-in
outside Omnigent, then send another message and expect the agent to use that
server's tool. Covered by
`tests/e2e_ui/chat/test_claude_sdk_host_mcp_servers.py::test_claude_sdk_session_reaches_mcp_server_signed_in_mid_session`
and, for a reconnect that hangs, `tests/e2e/test_claude_sdk_mcp_reconnect_timeout_e2e.py`.
For live state, open `/mcp` in a new claude-sdk session on a host and expect
the not-running note; send a message while a Claude server rejects sign-in,
reopen `/mcp` and expect **Failed** or **Needs sign-in** with **Reconnect**;
finish the sign-in, click Reconnect and expect **Connected**. Covered by
`test_claude_sdk_host_mcp_servers.py::test_claude_sdk_mcp_panel_shows_live_state_and_reconnects`,
with the row and merge cases in `SlashCommandMenu.test.tsx` and
`ChatPage.composer.test.tsx`.
Under an enterprise `managed-mcp.json` carrying the `omnigent` relay entry,
`tests/e2e_ui/chat/test_claude_sdk_host_mcp_servers.py::test_claude_sdk_session_under_managed_mcp_config_keeps_omnigent_tools`
starts a claude-sdk session, expects only the managed row in `/mcp`, flips it
to **Connected** and expects the next turn to use the server. It writes the
platform's real managed file, so it runs only with
`OMNIGENT_TEST_CLAUDE_MANAGED_MCP=1` on a disposable machine or container
where that directory is writable. Its `bwrap` variant gives the agent a
`linux_bwrap` sandbox and also needs Bubblewrap on Linux; in a container, run
it privileged so bwrap can mount `/proc`. Without the entry, expect the
session to start without Omnigent tools and the runner log to name the
missing entry.
Registry states: `web/src/components/SlashCommandMenu.test.tsx` covers the
row labels, the Sign in / Signing in… / error renderings and the Settings
link; `ChatPage.composer.test.tsx` covers the catalog join (sign-in from the
panel through to Connected, cancellation with retry, the Settings link for a
token service, an unavailable service, a catalog read failure, and no
registry read when the server has it off or no row needs it);
`web/src/hooks/useMcpRegistry.test.tsx` covers the sign-in hook's abort and
confirmation paths, and a reopened panel shows no state from the previous
open. Host account state: `tests/host/test_mcp_inventory.py` (the status
file), `tests/host/test_frames.py` (what survives the tunnel),
`tests/server/routes/test_mcp_servers.py` (the route) and
`ChatPage.composer.test.tsx` (row state, the new-tab Sign in, polling).
Manually, on a host whose sign-in manager writes the status file, expect a
host row to read **Sign-in required** with a Sign in that opens the reported
page, and to flip to **Connected** within 5s of the file changing. With the
registry on, attach an OAuth service
that is not connected, select `/mcp`, expect **Sign-in required** and
**Sign in**, complete the popup and expect **Connected**; cancel it instead
and expect the message on the row with Sign in still offered.
- **`codex-plugin-skills`, both composers:**
`tests/e2e_ui/chat/test_codex_plugin_skills.py::test_plugin_skill_in_both_composers_after_inventory_failure`
installs an enabled local plugin using the real Codex CLI, moves away its
marketplace, asserts inventory failure, then opens and selects the cached
skill in an existing session and on the landing page. It restores remembered
host/harness/workspace choices; it does not test picker interaction or send
a model turn. Run through the instance with recording enabled as above.
Requires Codex; a missing CLI is a skip, not a verified journey.
Run the model-free process contract separately:
```sh
uv run --no-sync pytest tests/harnesses/codex_native/test_skill_discovery.py --reruns 0
```
Repeat with `OMNIGENT_CODEX_PATH` pointing to the CI-pinned CLI and the
installed CLI. This covers the loaded catalog, numeric prereleases, preferred
`local`, disable/re-enable, and uninstall with stale cache. Manually refresh
both composers after disabling the plugin and confirm its skill disappears.
Catalog/process checks do not prove browser rendering or live-model invocation.
- **`attachments`:**
`tests/browser_ui/chat/test_composer_attachments.py::test_attach_supported_files_and_remove`,
`tests/browser_ui/chat/test_composer_attachments.py::test_file_dropped_on_the_transcript_attaches`,
`tests/browser_ui/chat/test_composer_attachments.py::test_landing_rejects_unsupported_type_and_keeps_message`
- **`dictation`, in-session composer (own environment):**
`tests/e2e_ui/chat/test_dictation.py` streams a fake microphone through the
server's fake transcription engine, which only the test's own server enables;
run it with plain `uv run pytest`. It covers the button, the hotkey, Enter
keeping the text, Escape discarding it, and insertion at the caret.
- **`dictation`, both composers and side chat (own environment):**
`tests/e2e_ui/chat/test_dictation.py::test_speech_service_denial_falls_back_to_client_audio`
covers a browser speech service refusing access while the client microphone
remains available, through the buttons on both composers and in side chat.
It streams client audio through the fake server
engine and keeps the transcript on Enter.
Manually on the landing page: dictate with ⌘/Ctrl+Alt+V, then press Escape
to restore the previous draft.
- **`dictation`, desktop:**
`web/electron/e2e/desktop_dictation_availability.e2e.js` (new-session
composer only), run from `web/electron` after building the SPA
(`OMNIGENT_PW_NO_SANDBOX=1 OMNIGENT_PYTHON=../../.venv/bin/python xvfb-run -a
node --test e2e/desktop_dictation_availability.e2e.js`): no mic against a
server without dictation (on macOS, the macOS Dictation mic instead), then
the mic dictating through a fake-engine server. Manually, for the in-session,
new-session and side-chat composers: connect the desktop app to a default
`omnigent server` (`/v1/info` reports `dictation_available: false`) and
expect no mic and ⌘/Ctrl+Alt+V to do nothing on Windows or Linux. On a Mac
expect the mic in each composer; clicking it (or, in the in-session and
new-session composers, pressing ⌘⌥V; side chat has no shortcut) opens macOS
Dictation with that message box focused, and the spoken text stays in it.
Restart the server with
`OMNIGENT_DICTATION_ENGINE=fake`, reconnect, and expect the mic, the listening
state on click, and the fake transcript in the message box.
- **`send-shortcut`:**
`tests/e2e_ui/chat/test_composer_submit_shortcut.py::test_submit_with_mod_enter_persists_and_is_the_only_send_gesture`,
`tests/e2e_ui/chat/test_composer_submit_shortcut.py::test_alt_enter_inserts_newline_in_both_composers`
- **`list-formatting`, both composers:** no browser-level coverage; the list
cases in `web/src/components/composer/ChatComposer.test.tsx`
(`pnpm --dir web test`). Manually: type `1.` then Space and expect ` 1.`;
type an item, press Shift+Enter and expect ` 2.` on the next line; press
Shift+Enter on the empty item and expect the marker removed. With Enter as
the send gesture, plain Enter still sends a list; with Mod+Enter chosen,
plain Enter continues it.
- **`model-picker` and `focus-hotkey`, keyboard:** no browser-level coverage;
component tests `web/src/hooks/useModelPickerHotkey.test.tsx` and
`web/src/hooks/useFocusComposerHotkey.test.tsx`. Manually: in a session and on
the landing page press Ctrl+Shift+M and expect the model picker open; in a
session click the transcript, press Ctrl+Shift+L and expect the message box
focused. In a session whose gear is disabled (read-only or unreachable),
Ctrl+Shift+M does nothing.
- **`config-cycle-hotkeys`, both composers:** component coverage in
`web/src/hooks/useComposerConfigCycleHotkeys.test.tsx`,
`web/src/pages/ChatPage.composer.test.tsx`, and
`web/src/shell/NewChatDialog.test.tsx`. Manually: enter a draft, press
Ctrl+Alt+M several times and observe the model preview advance and wrap;
press Ctrl+Alt+E and observe effort change. In a session, press either shortcut
three times quickly: no change request should occur during the burst, then
only the final choice applies two seconds after the last press. Repeat with
Enter to apply immediately, Escape to cancel, and a full wrap to send no change.
Verify model then effort cycling applies both final choices. Wait for the
preview to close without moving focus or changing the draft. Repeat before
launch; reload the session to check persistence. The fork dialog has no composer cycle shortcuts; mobile
without a physical keyboard continues to use the pickers.
- **`queue-and-steer`:**
`tests/e2e_ui/chat/test_queue_steer.py::test_steer_sends_queued_message_while_busy`,
`tests/e2e_ui/chat/test_composer_bulk_steer.py::test_bulk_steer_retries_the_whole_queue`.
The native mid-turn notice has component coverage in
`web/src/pages/ChatPage.userBubble.test.tsx`.
`tests/browser_ui/chat/test_status_indicators.py::test_touch_screens_reveal_timestamps_and_pending_steering`
verifies phone and tablet touch visibility without hover.
- **`host-offline-hold`:** no browser-level coverage.
`tests/server/integration/test_session_host_launch.py::test_offline_host_send_held_by_client_records_nothing`
(plain `uv run pytest`) checks that a held send is refused with nothing
recorded; the queue and release have component coverage in
`web/src/store/chatStore.test.ts` and
`web/src/hooks/useReleaseHostHeldSends.test.tsx`. Manually: stop a native
session's host, send, and expect a "Waiting for host" queue row with no
failed turn; restart the host and expect the message to be sent once.
- **`browser-pointer-queue`, desktop:** manually hold an agent turn open, then
send a normal composer follow-up and two pointer instructions (Send and Enter).
With Always steer off, all three stay queued and the popup says "Queued for
agent." After the turn ends, verify FIFO delivery and each pointer screenshot.
Repeat in a fresh session with Always steer on: both entry points send while
busy and the pointer popup says "Sent to agent." If older messages are already
queued, new pointer instructions must join them even with Always steer on.
- **`pi-compact`:** in a Pi session, start a long turn, type `/comp`, press Tab,
then Enter. With Always steer off, confirm `/compact` queues; use Send now to
interrupt and compact. With Always steer on and an empty queue, Enter should
compact immediately. During compaction, send a follow-up in each mode: normal
queue mode keeps its queue row; Always steer hands it to Pi. Both should run
once after compaction. Repeat the queued flow while viewing another session.
- **`draft-persistence`:**
`tests/e2e_ui/chat/test_draft_survives_incoming_messages.py::test_mid_typing_answer_survives_arriving_prompt`
- **`prompt-history`, in-session composer with a keyboard:**
`tests/browser_ui/chat/test_composer_acceptance.py::test_prompt_history_preserves_drafts_during_real_arrow_navigation`
checks wrapped-text caret movement, empty-composer recall, and edited recall.
Multiline, whitespace, quotes, and queued drafts have component coverage in
`web/src/pages/ChatPage.composer.test.tsx`. The new-session and side-chat
composers have no prompt recall; the vendor terminal owns its own history.
- **`edit-sent-message`:**
`tests/e2e_ui/chat/test_edit_sent_message.py` covers editing a message with
an image (only its text is copied), editing mid-turn, and side chats. The
replace-draft dialog, pending messages, other authors, and read-only composers
have component coverage in `web/src/pages/ChatPage.userBubble.test.tsx`,
`web/src/pages/ChatPage.composer.test.tsx`, and
`web/src/components/chat/SideChatPane.test.tsx`. Grid session panes and
sub-agent Workspace tabs render the same `SideChatPane`, whose Edit logic has
component coverage; their own wiring is checked by hand: open
a grid pane or a writable Workspace tab, click Edit on a message you sent
there, and confirm that pane's composer fills while the main one stays empty.
- **`delivered-send-recovery`, in-session composer (desktop and mobile):**
`tests/e2e_ui/mobile/test_composer_after_unacked_send.py::test_composer_stays_empty_after_delivered_but_unacked_send`
runs with a phone viewport. Run the same lost-ack journey at a desktop viewport
for the desktop entry point. The restore/delivery ordering and preservation of
user edits are covered in `web/src/pages/ChatPage.composer.test.tsx`.
`tests/e2e_ui/mobile/test_offline_send_failure_card.py::test_failure_card_clears_once_reconnect_recovers_the_delivered_send`
cuts the browser's network while the server holds the send for a runner
relaunch, then checks the failure card clears when the reconnect shows the
delivered turn.
A send cut off by a reload is covered in `web/src/store/chatStore.test.ts`.
- **`managed-first-input`, new-session composer (own environments):**
`tests/e2e_ui/start_session/test_managed_first_input_tab_close.py::test_managed_first_input_runs_after_browser_closes`
closes the entire browser during provisioning, on both the creating replica
and another replica. It requires a persisted reply before opening a fresh
browser, then checks reload for duplicates.
`tests/e2e_ui/start_session/test_managed_first_message_routing.py::test_managed_first_message_rebinds_live_stream_across_replicas`
keeps the tab open and requires two live turns across the replica handoff.
`tests/e2e_ui/start_session/test_managed_initial_attachment.py::test_managed_initial_attachment_survives_browser_close`
covers attachments with an SDK agent and the real native Codex CLI (Codex and
tmux required).
`tests/e2e_ui/start_session/test_managed_startup_followups.py::test_startup_followups_survive_browser_close`
sends three messages during provisioning with both harness types, closes the
browser, and verifies ordered input delivery and a fresh-browser reload.
`tests/e2e_ui/start_session/test_managed_initial_attachment.py::test_failed_launch_hands_the_message_and_attachment_to_a_new_session`
fails the launch, checks **Not delivered** live and after reload, and copies the
message and attachment to New Chat.
These tests use real servers, host, runner and browser; only
cloud allocation and model output are local substitutes. Backend coverage in
`tests/server/integration/test_managed_startup_inputs.py` checks native
transcript ownership, bundle uploads, recovery, access revocation and deletion,
plus failed inputs in the snapshot.
- **`arclet-dbcert`, desktop new-session composer (own environment):**
`web/electron/e2e/desktop_dbcert_preflight.e2e.js` exercises the real desktop,
native bridge, CLI process, and local server with a fake dbcert executable and
machine policy. Follow `web/electron/e2e/README.md` for build and recording
prerequisites. It proves explicit refresh and launch, preserved input, skipping
a pending check, and skipped kubeconfig updates and host sync; macOS sign-in
needs a manual managed-desktop check. Component tests in
`web/src/shell/NewChatDialog.test.tsx` and
`web/src/components/DbcertBanner.test.tsx` cover leaving during a check, late
results after skipping, retry, dismissal, check errors, and older shells.
- **`launch-action-required`, session chat transcript:**
`tests/browser_ui/chat/test_status_indicators.py::test_action_banner_follows_the_launch`
shows the snapshot's instructions in the banner and clears it when the stream
moves on. Component tests in `web/src/pages/ChatIndicators.test.tsx` cover
dismissals, and
`tests/server/integration/test_host_session_binding.py::test_managed_launch_action_required_reaches_snapshot_and_stream`
covers the server. Nothing in this repo emits the stage, so a live check needs
a deployment that does.
- **`new-session-hotkey`:**
`tests/e2e_ui/sessions/test_new_session_hotkey.py::test_new_session_hotkey_from_focused_composer`
- **`import-bundle`:** no browser-level coverage; the web unit tests for the
Create custom agent and new-session dialogs cover it. Manually: open the agent
picker's custom agents, then Create custom agent → Import bundle, and pick a
`.tar.gz` of an agent directory. Expect the dialog to close with the agent
selected, and the agent still listed after a reload. A bundle named like a
server agent keeps the dialog open with the server's reason.
- **`mobile-labels`, new-session composer:**
`tests/e2e_ui/mobile/test_composer_model_label_stop_overlap.py::test_new_session_composer_collapses_labels_to_icons_on_mobile`
## Gotchas
- Test touch sizing with a real touch device or browser touch emulation; resizing
a mouse browser alone does not enable it.
- The Arclet dbcert preflight needs the updated desktop shell and web UI, with
managed features enabled on the server. Browsers, older shells, other sandbox
providers, and already-connected hosts skip this check. Refreshing a host's
credential from the session banner does not prove new-session gating works.
Skip bypasses only this launch's check; it does not renew credentials or
guarantee that sandbox authentication will succeed.
- The launch action banner appears only on deployments whose launch code
reports it; Omnigent's own sandbox providers never do. Its text comes from the
deployment, so check the wording there rather than in the web UI.
- Session choices and server-stored project defaults are separate from local
YAML and the shared-preference foundation; see
[Preferences and config defaults](./preferences.md) for those boundaries.
- Chat `/new` is local navigation, not a message to the agent. Native TUI
`/new` is unchanged.
- The in-session and new-session composers are different surfaces. A change to
the pill, tooltip, configuration menu, or effort picker in one does not reach
the other. Verify both, plus the mobile layout. The Fork dialog repeats the
model, effort, and permission-mode pickers for native targets; check it too.
- "New session" in a test name can mean a session with no turns yet, not the
landing page. Check which surface the test opens.
- Tooltip changes need a hover after the picker closes, not only on first load.
A tooltip that returns after clicking away is a regression.
- Effort exists only for some harnesses. Check at least one harness with effort
and one without, since the pill and tooltip rows change.
- Ctrl+Shift+M and Ctrl+Shift+L use Ctrl even on macOS, unlike the ⌘/Ctrl+Alt
chords. All of them yield to a focused terminal or code editor, so press them
with the transcript or composer focused.
- A refused model or effort change rolls the pill back to the applied setting.
There is no separate error control to inspect.
- Codex effort can change from the terminal as well as the composer. Confirm the
terminal command actually landed before blaming the mirror.
- The mock environment configures Claude and Codex only. Other harnesses'
catalogs need real CLIs or credentials.
- Creator groups appear only for ten or more models from at least two
creators, so Claude and Codex catalogs normally stay flat. A model whose IDs
don't name a known creator (a router alias such as Pi's `radius/balanced`)
lands in Other models; that is expected, not a missing catalog.
- The Claude and Codex pickers here list the native harnesses' catalogs. A
`claude-sdk` or `codex` SDK worker has no picker of its own, so don't verify
its models here: see the smart routing note in the [index](./README.md#not-yet-mapped).
- An enterprise `managed-mcp.json` affects every Claude Code process on the
machine, including other test runs. Don't deploy one on a shared developer
machine; the managed `/mcp` journey needs a disposable container or VM.
- The managed MCP picker uses only the administrator's registry; it does not
add arbitrary MCP URLs or enable itself from existing GitHub/Databricks
connections. Bearer-token accounts must be connected in settings first.
- Managed MCP browser tests stub launch and authentication. They prove composer
wiring; the HTTP integration test proves the real provider boundary.
- Dictation normally uses browser speech recognition, with server fallback
when unavailable or the browser speech service fails. Audio still comes from
the client device; choosing a remote host does not change the microphone.
Denying microphone permission must not start a fallback recording.
The desktop app uses the server, falls back to macOS Dictation on a Mac,
and hides the button elsewhere. A
verification instance without a transcription engine depends on browser
support; a missing microphone there does not prove that dictation is
unavailable on a configured server.