---
title: Question Widget (inline answer pills with stable position)
---

# Question Widget (inline answer pills with stable position)

## What it is

When an agent asks you a multiple-choice or yes/no question, Omniscio parses the question out of the agent's message and renders it as an interactive **Question Widget** inline — instead of forcing you to type the letter "A" back. The widget shows the question text in bold, then a row of clickable **pills** for each option (A, B, C, …). You click pills to pick answers, optionally type a custom answer in a textarea, then submit with **Ctrl+Enter** to send the formatted reply back to the agent.

If the agent asked **multiple** questions in one turn (e.g., a 5-question clarification dump), the widget shows them one at a time with a "1/5" step counter and **Previous / Next** arrow buttons. As of the stable-position update, navigating between questions of different lengths no longer makes the question text jump up and down on your screen — the question line stays at a stable Y while pills extend below the locked area, so you can read the next question without re-tracking.

## Where to find it

There is nothing to open. The Question Widget is not a window, a panel or a page — it is drawn **inside the agent's own message**, in whichever session the question was asked in, so it has no menu entry and no keyboard shortcut of its own. It appears on the newest agent message you can actually see whenever that message contains a question, and it stops being interactive once you answer it or a newer agent message replaces it. The message carrying a waiting question is flagged with its own header chip, described first under **How it behaves** below, and a session with a question waiting shows the blue "Question" state on its row.

What you *can* open is the set of toggles that govern how the widget renders. They are listed at the end of **How it behaves** below; turn everything off and the agent's question simply renders as ordinary text.

## How it behaves

What follows is the widget's behaviour in the order you meet it: the header cue that flags a waiting question, which message is allowed to be interactive, how throwaway "Other" options are cleaned up, how to answer and submit, the layout that stops the question jumping between steps, the undo stack in the custom-answer box, and the settings behind all of it.

### Header cue — the "Question" chip

So a pending question is never scrolled past, the agent message that's asking one carries a small blue **"Question"** chip in its **header** (beside the "Agent" label and timestamp), with a tooltip reading "This message has a question waiting for your answer." It appears on **exactly the message that hosts a live, unanswered question** — the same scope as the interactive widget: it shows only while the question-widget feature is on and you haven't replied yet, and it clears the instant you answer or a newer message supersedes the question (the same **option-B rule** described under "Render scope" below). Because it lives in the header, it stays visible even when the message is long (the question sits below the fold) or the turn is collapsed. It's a label, not a button — the interactive pills are still the widget itself.

For agents working on this code: the chip renders in [`MessageBubble.tsx`](/src/renderer/src/components/ui/MessageBubble/MessageBubble.tsx), gated on the memoized `qwLikelyHostsWidget` (`!isOperator && questionWidgetEnabled && isLikelyQuestionWidget(message.content)`) and reusing the shared [`Pill`](/src/renderer/src/components/ui/Pill.tsx) (`tone="info"`, the themeable `--status-question` blue). It follows the same `questionWidgetEnabled` gate as everything else here, so turning **Question widget — replace plain text** off hides the chip too. Locked by [`message-bubble-question-indicator.test.tsx`](/tests/unit/components/ui/message-bubble-question-indicator.test.tsx); strings `messageBubble.questionWaiting` / `messageBubble.questionWaitingTooltip`.

### Render scope — only the last VISIBLE agent message is interactive

The clickable widget appears on **exactly one** message: the **last agent message you can actually see**. When a **newer agent reply arrives**, the older question stops being a widget and renders as plain markdown instead (the bold question line followed by the lettered options as an ordinary list). Its pills, textarea, and Send button are gone; the text stays in the transcript so you can still read what was asked.

This is deliberate. A widget that lingers on a superseded question invites you to click pills and submit an answer to something the conversation has already moved past — the answer would arrive out of context, attached to a question the agent stopped waiting on.

**Answering retires a question, however you answer it** (owner rule, 2026-09-20: _"a Question Widget should disappear as soon as the user responds, not just when the agent emits another message"_). Send from the widget, type a reply, or tap a Quick Reply — the moment your response lands, the question closes and renders as plain text, without waiting for the agent to say something back. Before this rule a reply sent through the chat left the widget live for the whole of the agent's next turn, still inviting a second answer to a question you had already answered.

What does **not** count is Omniscio talking to itself: a mechanical nudge, an auto-resume, or another agent's hand-off is not you answering, so the question stays live.

"Visible" is the key word, and it means exactly what the transcript shows. A reply that Omniscio folds away — a background check-in, an auto-wait notice, a no-op wake-up — never counts as the agent saying something newer, so it cannot retire the question above it.

Practically, this means:

- Scroll back through a long session and every **past** question is plain text. Only the newest one the agent actually asked is live.
- Replying without picking an option closes the widget too — your reply IS the answer. The question text stays in the transcript, so you can still read what was asked.
- If the agent asks a question and you dismiss it (the **X** on a desktop window, **⋯ → Dismiss** on a phone — both revert it to plain text), then it's plain text by your choice; the last-visible rule is separate and applies even to questions you never touched.
- A direct, standalone widget that Omniscio mounts on its own (not parsed out of a chat message — e.g. a triage card) is unaffected by this rule; the scope gate only governs questions rendered _through_ a chat message's markdown.

For agents working on this code: the gate is the row-level `isFinalMessage` prop threaded into `renderSegments` inside [`AgentMarkdown.tsx`](/src/renderer/src/components/ui/AgentMarkdown.tsx); a question segment mounts the interactive widget only when `isFinalMessage === true`, otherwise it falls back to raw question markdown. The full invariant, threading path, and locked-by tests live in the feature contract at [`.claude/memory/contracts/qw-parser-contract.md`](/.claude/memory/contracts/qw-parser-contract.md) under "Render scope (consumer side)".

### Redundant "Other" options are hidden automatically

The widget **always** renders its own **Custom answer** free-text field (the last lettered slot), so you can always type a reply that no pill covers. Because that escape hatch is built in, an agent that _also_ writes its own throwaway choice — "Other (I'll explain)", "Something else", "None of the above", "Different — I'll describe it" — is just stacking a duplicate on top of it. Omniscio detects those and drops them before the widget renders, then re-letters what's left so the options stay a clean A, B, C, … The built-in Custom answer field is what covers "none of these."

Guardrails, so a real choice is never lost:

- An option the agent explicitly marked **(recommended)** is never hidden, even if its wording looks like a throwaway.
- If _every_ option is a throwaway, none are removed — a question is never emptied.
- It's display-only: the message still counts as a question (it still lights the amber "needs you" dot), and nothing about parsing or your saved answers changes.

Omniscio also quietly asks the agent not to add such an option in the first place (see **Format hint injection** on the [second page](question-widget-part-2.md)), so with a cooperative model you'll rarely see one at all — the filter is the belt-and-suspenders backstop for when the agent adds one anyway. Set the `AMC_DISABLE_QW_OTHER_FILTER=1` environment variable to turn the filter off and show the agent's options verbatim. The matcher was calibrated against the live message history (924 real question rounds, zero false positives); the exact rules, guards, and the one deliberately-broad edge live in the feature contract at [`.claude/memory/contracts/qw-parser-contract.md`](/.claude/memory/contracts/qw-parser-contract.md) under "Render-side redundant-option filter (consumer side)".

### How to use it

1. **Pick options.** Click a letter pill or press the matching letter key (A/B/C/…). Pills can be **single-select** (tap one to lock it; tapping a different one swaps) or **multi-select** (toggle as many as you want) — the widget infers which based on the agent's wording, and you can flip the mode with the small mode segmented control above the pills. **One-key shortcut:** when the widget itself is focused (press **Q** or click the card), **Space** picks the agent's **recommended** option — the one with the soft green outline — or the **first** option if none is recommended, exactly like pressing that option's letter. (Space still types a normal space while your cursor is in the Custom-answer field.) **On the last question, Space again sends:** once you've picked an answer, pressing **Space** one more time submits — so a single-question widget is two taps (Space to pick, Space to send). With a **recommended** option (green-outlined), the first Space accepts it and the second sends; with **no** recommendation, the first Space selects the first option and the second still sends once you've answered — you no longer need to reach for **Ctrl+Enter**. (Turning the recommendation highlight off means nothing reads as recommended, so the same "second Space sends once answered" path applies.) The first Space alone never sends. Holding **Shift** while pressing Space never sends — it stays the "add this option" toggle.
2. **Type a custom answer.** Click "Custom answer" (small button next to the pills) — or press the letter past the last option — to reveal a textarea on the active question. In **single-select** mode, opening Custom or typing in it replaces any prior pill pick (and picking a pill closes Custom and discards the typed text), so the visual state always matches what gets sent. In **multi-select** mode, Custom answer text is sent **in addition** to the pill picks. Because it counts as one of your answers, the Custom-answer box's letter badge takes the **same selected look as a picked pill** the moment you type something non-blank — accent-coloured letter, plus a ticked checkbox in multi-select — so a typed answer never reads as ignored sitting next to your ticked pills. An empty or whitespace-only box stays neutral, matching the fact that blank text contributes nothing to the reply. To get the multi-select "compose" behavior in single-select mode for a single action, hold **Shift** while clicking the pill or pressing the letter — Shift always means "add, don't replace". Each question keeps its own custom-answer text, even when you navigate away and back.
3. **Move between questions.** Use the **◀ Previous** and **Next ▶** buttons (or Left/Right arrow keys when focus is in the widget). The "1/5" counter shows where you are. If you skip a question without answering, the Previous arrow's tooltip notes how many questions are skipped behind you (e.g. "Previous question (2 skipped)").
4. **Send your answer.** Press **Ctrl+Enter** to submit. If you have unanswered questions and the **Confirm before submitting with unanswered questions** setting is on (default), a confirmation dialog appears that **names exactly which questions are still unanswered** (e.g. "Questions 2 and 4 are still unanswered", or "Question 3 is still unanswered" for one) — click **Submit** to send the partial set, or **Cancel** (also Esc, or clicking outside) to jump straight to the **first unanswered question** so you can fill it in instead of being left on the last one. The **Send** button also warns you on hover when a confirm step is coming ("2 unanswered questions — you'll be asked to confirm"), so a partial submit is never a surprise.
   - **Default (Original view).** The agent receives a **verbose** multi-line reply so the original question text is preserved alongside your answer in the conversation transcript (easier to scan when scrolling back). Each numbered question renders on its own line as `<n>. **<full question text>**`; your answer renders on the **next line as an indented sub-bullet** beneath it (a nested Markdown list item — it visually sits indented under the question instead of running on the same line) — e.g. `1. **Which color do you want?**\n   - Red\n2. **Which size?**\n   - Large`. When you pick **multiple** options — or add a custom answer alongside a pick — each one renders as its **own** indented bullet, never joined on one line, so two choices can't blur into a single run-on (option texts routinely contain their own commas). Picking A and C gives `1. **What next?**\n   - Refresh the VM, then re-verify\n   - Get it merge-ready`; a pick plus a custom note gives `1. **Should we ship?**\n   - Yes, with caveats\n   - run the regression suite first`. Skipped questions are kept in the list as `<n>. **<full question text>**\n   - _skipped_` rows so the agent can see which questions you intentionally didn't answer; the original `<n>` index is preserved. Free-form questions with no options have **no pills at all** — you answer them by typing in the Custom-answer field, and your text is sent verbatim (an earlier build stuck synthetic Yes/No pills on open-ended questions; that was removed).
   - **Plain Speak view.** When you submit while looking at the **Plain Speak** rewrite of the agent's message (the QW that rendered out of the rewrite, not the original), the same verbose body is prefixed with a one-paragraph **provenance header** explaining the questions came from the Plain Speak overlay and were paraphrased. The agent — which never saw the rewrite verbatim — needs the header to know the question text it sees was paraphrased; the regular path skips the header because the agent itself authored the question text. **All-skipped** falls back to the terse `1. skipped  2. skipped` format with no header (sending only a header would be meaningless). A single answered question still gets the header for predictable formatting.

5. **Dismiss without answering.** Click the X to close the widget — the agent message reverts to plain text and you can type your own free-form reply.

#### Send button availability across session statuses

The widget's Send button mirrors the main composer's Send button: it is **never disabled because of the session's status** — the only thing that stops it is having nothing to send. Paused, ended, error, and archived sessions all let you click Send — the backend's `sendResponse` runs the auto-resume path (`--resume <cliSessionId>` plus auto-unarchive for `archived`) so the agent picks up the conversation. If you fill in answers on a paused session and click Send, the row flips back to `running` and your answers go through. A session that is still **`starting`** works too: the answers are folded into the launching turn, or they expedite a session still waiting in the restore queue. See [pause-or-stop-a-session.md](pause-or-stop-a-session.md) and [send-a-message.md](send-a-message.md) for the matching composer behavior.

**Optimistic send.** The moment you click Send (or press Ctrl+Enter), the widget collapses to its read-only submitted summary and — if your post-send-navigation setting is set to move you to the next session — Omniscio moves you there **immediately**, before the agent actually receives the answers. This matches how the regular composer behaves on every other send: the textarea clears the instant you press Enter, you don't wait for the round-trip. The IPC continues in the background; on success nothing else happens (you're already on the next session). On **failure** (the session ended, paused refused to resume, IPC error) the widget reappears with your answers intact, Omniscio navigates you back to the original session, and an error toast explains what happened so you can retry. A second click or Ctrl+Enter fired in the same instant is ignored so you can't accidentally double-submit. The one exception: if the session is in the `starting` state (CLI launch handshake) the click is blocked entirely — the widget does not collapse and no send is dispatched.

**An answered question stays answered — even if you scroll away and come back.** Once your answer
goes out, that question reads as answered from then on. It used to come back as a live, clickable
widget the moment its message scrolled off screen and back — with your answer already sent — because
the only record that you had replied lived in the on-screen widget itself, and Omniscio unloads
messages that scroll out of view to keep long conversations fast. That was most visible when the
agent's next reply is one Omniscio folds away (a background check-in, an auto-wait notice, a no-op
wake-up): the question stayed the newest thing you could see, so it never closed on its own either.
Omniscio now records the fact that you answered — just the fact, never the answer text — so the
question stays closed across scrolling, leaving the session and coming back, and a full app reload.
Answers you have picked but **not** sent are untouched: a question you never submitted is still live
and clickable, which is what keeps a nudge or a Quick Reply from stranding a question you still mean
to answer. The same applies to a **card deck** — once the batch ships, every card closes together
and cannot be sent a second time.

### Stable position — what changed

In multi-question widgets, all questions render simultaneously as **stacked grid slots** in the same cell. Only the **active** slot is visible and interactive; the others are present in the DOM but hidden (`aria-hidden="true"`, `pointer-events: none`, `opacity: 0`). The widget's body sizes itself to the **tallest** slot, so:

- Going Q1 (long pill labels, 3 lines wrapped) → Q2 (short pill labels, 1 line) → Q3 (one-liner) keeps the question text at the **same vertical position**.
- Pills can extend **below** the locked question position; the question itself never moves.
- Forward and backward navigation still slide softly (forward = right-to-left, back = left-to-right). With OS reduced-motion enabled (Windows: Settings → Accessibility → Visual effects → Animation effects = Off; macOS: System Settings → Accessibility → Display → Reduce motion), the slide is instant.
- The **Custom answer** pill is rendered on _every_ slot (active and inactive) so the body's max-height calc stays symmetric across navigation. Inactive slots' Custom Answer buttons are present for layout only — clicking one (e.g., via assistive tech bypass) does not open a textarea or change state.

#### Pinned control bar — stays at the top while you scroll

The widget's top **control bar** — the ◀ ▶ navigation arrows, the single/multi mode
toggle, and the help (?) and dismiss (×) buttons — stays glued to the top of the chat
viewport while you scroll down past a tall widget, so those controls are always reachable
even after the question's pills have scrolled off-screen. As soon as the whole widget
scrolls out of view, the bar unpins and scrolls away with it.

On a phone the bar always stays on **one line** — it never wraps, and it fits by carrying
fewer controls rather than by squeezing them. Three things give way there, in this order:

1. **The trailing actions collapse into a ⋯ menu.** Ask the Council and the dismiss (×)
   are not drawn in the row at all on a phone — one three-dot button replaces both and
   opens them as menu items. This is the structural cap on the row: the next control
   somebody adds to this surface costs a menu row, not bar width.
2. **The help (?) button is hidden** — there is no hover on a phone to open it.
3. **The remaining controls are drawn more compactly** (narrower arrows, a tighter Send).
   The step counter is NOT among them — it stays at its full desktop size, because the
   ⋯ menu frees more width than the big counter costs.

On a screen narrow enough that it still wouldn't fit, the **single/multi toggle** is the one
control that gives up space — it has a touch equivalent anyway, since long-pressing an
option turns multi-select on — while Send and the ⋯ button keep their full size.

Two incidents shaped this, both on 2026-09-10. First, nothing gave way at all, so the
dismiss (×), last in the row, was silently cut off outside the card and the widget could
not be turned off. The fix for that bought width by shrinking the step counter to 14px,
which was then too small to read on a phone — so the ⋯ menu replaced the pixel budget with
a structural one and the counter went back to full size.

Hovering the **?** opens a live shortcut cheat-sheet keyed to the current question — it
lists the actual option-letter range (e.g. "A–C", not a fixed "A–F"), the **Custom answer**
letter, **Space** (pick the suggested option), **R** (jump to the chat composer), **Esc**
(release the widget's focus), and — on a multi-question widget — the **◀ ▶** arrow keys for
Previous / Next.

This works on **desktop and mobile alike**. Under the hood the bar never actually moves in
the document — Omniscio nudges it down visually as you scroll (a paint-only offset that stops at
the bottom edge of the widget), so it can't fall out of step with the virtualized message
list the way a plain CSS sticky header would. There is no setting; the pin is always on.

The question text is a touch larger on mobile (16px) for readability; pill labels stay at
14px.

#### Mobile — the next question scrolls to the top when you advance

On a phone, if a question has a long list of options and you've scrolled down to read them,
tapping **Next ▶** (or picking an answer that auto-advances) scrolls the page so the **new
question's heading lands at the top** — you don't have to scroll back up to see what's being
asked. It only nudges when you'd actually scrolled past the question, so a question already
in view doesn't jump; and it **never** fires when you first open a session (opening a session
never yanks you down to the widget — that was an old bug, deliberately ruled out here).
Desktop is unchanged: questions there usually fit on screen, so no auto-scroll happens. There
is no setting; like the pinned control bar, it's always on (in the default stable-position
mode).

#### Mobile — "Custom answer" opens as a bottom sheet

On a phone (narrow viewport, < 768px), tapping **Custom answer** doesn't just reveal a
textarea in place — it lifts the whole widget (question text, pills, and the typing box) up
into a **bottom sheet** that slides in from the bottom of the screen and stays pinned just
above the on-screen keyboard, so the text you're typing is never hidden behind the keyboard.
The chat behind it dims slightly so the input stands out, and the sheet stays up while you
move between questions until you Send, Dismiss, or the widget is superseded.

**A tap on the dimmed area closes the sheet**, like any pop-up (added 2026-09-24 — before
that the only ways out were Send, ⋯ → Dismiss, or the phone's Back button, which read as a
locked screen when the sheet was opened by mistake). The widget goes back to its spot in the
chat, still unanswered, and the chat behind it is exactly where you left it: while the sheet
is up, an invisible placeholder keeps the widget's space in the chat, so nothing shifts. If
you moved to another question inside the sheet, the chat scrolls that question's heading into
view as it returns — the same nudge **Next ▶** gives. **A typed answer is never thrown away:**
while any question holds text you typed into its Custom answer box, a tap outside only hides
the keyboard and the sheet stays, one tap from Send. While the sheet is up the chat behind it
does not respond to taps, as with any pop-up. The **⋯** menu (where Dismiss lives on a phone)
opens on top of the sheet.

Opening Custom answer **does not change the widget's background** — the inline widget and the
bottom sheet are the same surface, so the only thing that visibly happens on tap is the
slide-up and the dim, not a recolor. They cannot differ: the widget's colour and blur are
described once and every state reads that one description, rather than each state carrying
its own copy that somebody has to keep in step. (The phone gets a solid fill rather than the
translucent "glass" look because a translucent fixed sheet renders incorrectly on some
Android browsers.) Desktop keeps the in-place reveal, the glass look, and no bottom sheet.

The typing box renders at **16px on a phone**, which is deliberate and is the one size it may
never drop below. Mobile Safari silently zooms the whole page in whenever you focus a text
field smaller than 16px, and it does **not** zoom back out when you finish — so the page just
stays magnified until you pinch it back yourself. The box used to be 14px, which is why
tapping **Custom answer** on an iPhone zoomed in and never zoomed out, while the normal
message box (already 16px) behaved fine. The surrounding pills and the collapsed "Custom
answer" button stay at 14px on purpose: iOS only auto-zooms real text fields, never buttons.
See [mobile-font-size-contract.md](../../.claude/memory/contracts/mobile-font-size-contract.md)
§11 for the cascade detail and the guard test that keeps it from regressing.

#### Edge case — small viewports + very tall questions

The widget locks to the height of the tallest question. On a small screen with one very tall question (say, a 40-line dump of options), that locked height can push controls above the visible chat — natural scroll recovers them. This is by design; the alternative would be reverting to the legacy behavior where every question owns its own height.

### Custom-answer textarea — undo / redo

The "Custom answer" textarea (revealed by clicking **Custom answer** next to the pills) supports the usual editor undo / redo against what you have typed:

- **Ctrl+Z** undoes the last typing burst.
- **Ctrl+Shift+Z** or **Ctrl+Y** redo it.

The undo stack is **per-slot per-message** — moving to a different question (Previous / Next) or to a different agent message clears the stack. So Ctrl+Z always undoes inside the Custom-answer textarea you are currently typing in, never across questions.

Snapshots **coalesce on a 500ms debounce**: rapid typing produces one undo step (a word or burst), not one per keystroke. Pause for half a second, keep typing, and a fresh snapshot is recorded — so a single Ctrl+Z rolls back the last burst rather than the last character.

Ctrl+Z is handled inside the textarea so it does not bubble up to any window-level binding while you are editing. **Ctrl+Enter** still bubbles normally and submits the answer.

### Settings

Open **Settings → Sessions** and look for these toggles:

- **Question widget — replace plain text** (`questionWidgetEnabled`, default **on** as of 2026-06-02): turns the parser on/off. When off, the agent's question is rendered as plain markdown without pills. Installs that still carried the old default-off were force-flipped on once by a one-shot migration (`migrateQuestionWidgetDefault`); turning it back off here sticks — the migration never re-flips a deliberate opt-out.
- **Confirm before submitting with unanswered questions** (`confirmUnansweredQuestions`, default on): the safety dialog described above.
- **Keep question position stable** (`stableQuestionPosition`, default **on**): turn this OFF to revert to legacy single-slot rendering where each question's height grows/shrinks the widget. Useful if the locked-height behavior feels wrong on your screen size, or if a buggy parse renders one question ridiculously tall.
- **Raised 3D buttons** (`qwRaisedChromeEnabled`, default **on**): gives the QuestionWidget a **raised 3D** look. The big option boxes and the card **float** on a drop shadow with **no face sheen** (the wide stacked boxes read calmer that way); the **letter badges** (A/B/C/D) keep their glossy chip, and the **Send** key carries the full gloss — a tinted, sheened key when idle, going to the glossiest "ready" look (sheen layered over its spinning accent border) the moment you pick an option. Turn this **off** for flat buttons. It's pure styling; nothing about how questions are parsed, answered, or submitted changes, and it reads correctly in every theme (white-on-top / shadow-on-bottom, the same trick the app's buttons use).
- ~~**Parser version**~~ — the v1/v2/v3 segmented picker was **removed 2026-05-31**; there is no parser-version control in Settings anymore. Every install runs the `v3` default — see "Parser version" on the [second page](question-widget-part-2.md).

In **Settings → Lab**:

- **QW format hint injection** (`qwFormatHintInjectionEnabled`, default **on**): see "Format hint injection" on the [second page](question-widget-part-2.md).

The settings are findable via Ctrl+K (settings search): try "question", "stable", "unanswered", "parser", or "hint".

## For agents

The parser itself — the six-pattern `v3` cascade with its Plain Speak canonical-priority pre-pass, the format hint Omniscio injects into the first message of a session, the parser version history, the widget's saved-state persistence, and the file-level pointers for anyone touching this code — is in [Question Widget (inline answer pills with stable position) (part 2)](question-widget-part-2.md).

## Related

- [use-quick-responses.md](use-quick-responses.md) — AI suggestion chips appear in the same input toolbar; both surfaces handle the "agent asked a question" moment.
- [notifications-and-silence.md](notifications-and-silence.md) — `postSendNavigation` controls what happens after you submit an answer (stay, advance, etc.).
