Omniscio documentation
Browse all documentation
  1. Getting Started13
  2. Sessions & Agents115
  3. Inbox & Notifications59
  4. Projects & Tasks95
  5. Automation & Scheduling75
  6. Knowledge & Memory26
  7. AI Features60
  8. Integrations100
  9. Plugins & Marketplace33
  10. Cloud & Teams56
  11. Settings & Customization58
  12. Account & Billing28
  13. Troubleshooting84
  14. CLI & API Reference22
  15. Legal & Policies4
  16. Uncategorised22

Question Widget (inline answer pills with stable position) (part 2)

The second half of the Question Widget page: the Plain Speak canonical-priority trigger and its aggressive numbered-bullet mode, the format hint Omniscio adds to the first message of a session, the v3 parser's history, how widget state is saved, and the file-level pointers an engineer touching this code needs.

What it is

This is part 2 of the Question Widget page. That page covers what the widget is, where its settings live, and the whole life of a question — how it renders, which message it may render on, how you answer it, the layout that holds the question still, and what is remembered while you fill it in. This half carries the machinery behind that: the Plain Speak rule that decides which part of a rewritten message becomes a widget, the format hint Omniscio quietly adds to your first message of a session, the parser's version history, the widget's saved state, and the file-level pointers for anyone touching the code.

Where to find it

Nothing on this half has a surface of its own — every rule here runs behind the same inline widget the main page describes, so there is no button to press to reach any of it. Two things are worth knowing about where it becomes visible: the Plain Speak rules apply only while you are looking at a Plain Speak rewrite of a message (the V hotkey or the header pill switches you back to the original, and the rule stops applying), and the format hint has a toggle plus a text preview of its own in Settings → Lab.

How it behaves

Canonical priority trigger — Plain Speak ## Questions heading

When Omniscio's Plain Speak rewrite of an agent message contains a literal ## Questions heading as the last h2 in the rewrite, that heading anchors the Question Widget. Anything above the heading (including the heading itself) renders as plain markdown, and only the content below it is fed to the parser. This lets a Plain Speak rewrite include lead-in prose with a bold "Should we ship this?" question or A/B options without confusing the parser into rendering an extra widget — the parser only sees the canonical block.

If nothing parseable lives below the ## Questions heading, the whole message falls back to plain markdown — no empty widget, no partial render.

Scope. Only Plain Speak rewrites. The original agent message keeps the unscoped parser behavior (any pattern can fire from anywhere in the message), so a hand-written agent question that doesn't include the canonical heading still gets a widget the same way it always did. The trigger is opt-in via the renderer prop plainSpeakScope, set by PlainEnglishOverlay whenever its view is 'plain'. Toggling to "View original" with the V hotkey or the header pill turns the flag back off and the original message re-renders under the unscoped parser.

Aggressive numbered-bullet treatment under ## Questions (Pattern F). When plainSpeakScope === true, a sixth pattern (Pattern F) fires FIRST in the try chain. Every numbered bullet (1. / 1), optionally bold-wrapped) under the LAST ## Questions h2 becomes a question — with or without lettered options, with or without a ? terminator. The contract: under Plain Speak, everything emitted under the questions heading is intended as a question, so the parser stops requiring per-question anchor tokens. Mixed shapes (Q1 has A/B, Q2 is open-ended) and pure open-ended shapes (3 questions, no options anywhere) both render as widgets. The Custom-answer textarea handles open-ended questions naturally.

Bolded numbered headers with a trailing aside (fixed 2026-08-03). Pattern F recognizes the same three bolded-number header shapes the regular Pattern B does, not just a full-line **1. …** wrap: (1) full-line wrap **1. Question?**, (2) the bold closing before a trailing parenthetical — **1. Which entity should own this?** (this becomes your public name), and (3) the number inside the bold with the ? outside — **1. the mobile app** (Omniscio or a different product)?. Before this fix, Pattern F stripped only shape (1), so a question whose header ended in a parenthetical was silently dropped while a clean full-bold sibling still rendered — the reported "only question 2 showed in the widget" bug on a 2-question card. A bolded numbered statement with a parenthetical but no ? (**1. Ship the redesign** (already approved)) is still NOT promoted to a question, so the relaxation can't over-fire. This is the exact shape-stripping Pattern B has used since 2026-06-03, now shared between the two paths via one helper so they can never drift apart again. (Pattern B gained a FOURTH shape on 2026-08-09 — a mid-line bold-close with the ? OUTSIDE the bold and no parenthetical, e.g. **3. When X**, what happens? — but that one lives in Pattern B's candidate collector, gated on lettered options directly below the header, and is deliberately NOT shared into Pattern F, whose aggressive numbered-item mode has no options-below signal to gate on.)

(The legacy v1/v2 parsers had no Pattern F — they routed the same heading-scoped slice through their own cascades, which still required per-question anchor tokens. Both were removed in audit F019; v3 is the only parser now, so Pattern F is the behavior in every install.)

Match rules.

  • Heading text must be ## Questions — whole-line, case-insensitive (## QUESTIONS and ## Questions both match), no trailing text (## Questions for you does NOT match).
  • Must be the last h2 in the rewrite. Earlier h2 headings are ignored when picking the anchor.
  • Sub-headings (h3+) below ## Questions are passed through to the parser unchanged.
  • Code blocks above the heading are restored before the prose-above slice is handed back to the renderer.

Why. A Plain Speak rewrite is a Sonnet pass that re-shapes the agent's terse question dump into reader-friendly prose, often re-introducing emphatic "Should we proceed?" sentences in the lead-in that v1's permissive cascade anchors on as a false-positive question. The canonical heading gives the rewriter a stable, parser-recognized place to put the actual questions. v3 takes the contract one step further with Pattern F: since Sonnet was told to put only questions under the heading, the parser trusts that signal and skips the per-question anchor requirement that the regular feed enforces.

Format hint injection — teaching the agent to emit parseable widgets

The parser is strict: a question header missing the ?, numbered options instead of lettered, a fenced code block around the block, or a multi-question round that numbers topic labels with one shared question repeated under each (instead of numbering the questions themselves) all silently fall back to plain markdown — that last case renders only the first question as a widget. To raise the hit rate without asking you to coach the model yourself, Omniscio silently includes a short formatting hint with the FIRST message you send in each new session. The hint mirrors the "QuestionWidget rendering" section users typically add to their global CLAUDE.md by hand — it ships with Omniscio out-of-box. The hint also asks the agent not to add an "Other / Something else / I'll explain" escape-hatch option, since the built-in Custom-answer field already covers that — the belt-and-suspenders partner to the automatic filter described under "Redundant 'Other' options are hidden automatically" on the main page.

What you see: your chat bubble shows exactly what you typed. The hint is not visible to you; it's stitched onto the outgoing prompt at send time so only the model sees it.

When it fires: on the FIRST operator message of a session only (getOperatorMessageCount(sessionId) === 0). Subsequent messages, replies, follow-ups — no hint. Blank input — no hint. If you disable the toggle — no hint. The check is priorOperatorCount !== 0 → return displayText unchanged, so the second message is always clean.

Every path is covered: the hint rides the in-session send path, the session-launch path (the first prompt at /sessions/new or recipe start), and every engine (Claude, Codex, Gemini, …). It's placed as a preamble before your message — so your actual message is always the last thing the model reads, never buried under a trailing hint — via qwFormatHintForFirstMessage + withUserMessageLast in /src/shared/qw-format-hint.ts. One source of truth, no inline copies.

Preview the text: Settings → Lab → "QW format hint injection" has a Preview the hint text collapsible that shows the exact string being appended, so you can decide whether to leave it on.

Toggle off if: you've put an equivalent hint in your own global CLAUDE.md, you want full control over the outgoing prompt, or you're debugging a model that misreads the hint as a literal user instruction. The toggle is in Settings → Lab because it's a Lab feature, not the Sessions tab where the rendering toggles live.

Parser version

Every QuestionWidget extraction runs through v3, the only parser. The user-facing picker was removed 2026-05-31, and v1 + v2 were then DELETED (audit F019, behavior-preserving — every path already resolved to v3, so nothing user-visible changed). resolveQwParser is now an always-v3 shim (see qw-parser-contract.md). v3 is what every user runs today.

  • v3 (default) — minimal high-precision parser at question-widget-parser-v3.ts. Six named patterns, all-or-nothing within each pattern. Regular (non-overlay) try order is B → A → D → C → E: A (single question + ? + lettered options; since 2026-07-04 the options may sit below the header across ONE short prose or bullet bridge, which attaches as the question's note — a two-paragraph gap or non-lettered options still fall back to plain markdown), B (multi-question numbered list, all-or-nothing letter sequence with anySiblingHasOptions allowing some open-ended slots; since 2026-07-13 a clean numbered list of STATEMENTS above a genuine question no longer suppresses it when the question's ? is followed by a bold-close and/or a single trailing parenthetical aside — **Want me to…?** (I'd back up first.) — while a ? sitting mid-sentence with prose after it still stays dark, so the relaxation can't open a false positive; and since 2026-08-09 a numbered header whose bold closes MID-LINE with the ? OUTSIDE it and no parenthetical — **3. When X**, what happens? — is recognized as a question when lettered options sit DIRECTLY below it, so a partial-bold header no longer drops to plain text, while a rhetorical **1. Context** why? with no options below stays dark; and since 2026-09-28 a mixed round whose first question is written as prose with lettered options flush above a numbered header starting at N≥2 is admitted as ONE whole widget — the prose header becomes question N−1 — so the numbered questions no longer drop to plain text, while a leading list marker on a displayed question (- **1. …**) is now stripped from its text), C (decision verb + : + lettered options), D (multi-section h2 with decision-verb trailers), E (statement / options-announce lead-in — your call/up to you/your choice/your options/your choices as the line immediately above the options; all five may carry a trailing clause, e.g. "Your call on what's next" or "Your options for next steps", as long as the line still starts with the phrase; the phrase is also recognized inside a line-final trailing parenthetical (Next steps (your call)) or after a short prefix clause + separator, still closing the line (Next step, your call: — comma/semicolon/colon/em-dash/en-dash only, no bare "the timeline is your call" and no trailing content after the phrase); a trailing recommendation paragraph is allowed and renders as after-prose). Since 2026-08-07, Pattern E also fires on a whole-line-only announce header, what I need from you (an ## What I need from you / bold / plain line directly above the options); because the Pattern B reject-rescue reuses the same whole-line matcher, this one phrase also un-suppresses a message whose preceding numbered-statement list would otherwise trip Pattern B's hard reject. Launch-hardening (2026-08-07) additionally recognizes the wider declarative decision-header set diverse agents emit — deferral siblings (your decision · the choice is yours · over to you · you decide · I'll leave it to you · …), more announce headers (here are your options · a couple of options), a distinctive phrase that CLOSES the line after a prefix ("Here's what I need from you:"), and curly or straight apostrophes alike — each back-tested for zero over-fires; generic words (options · next steps · summary) are deliberately excluded. When plainSpeakScope === true (Plain Speak overlay), Pattern F is prepended: every numbered bullet under the LAST ## Questions h2 becomes a question with options optional — see "Canonical priority trigger" above. No partial fallback — if the message doesn't match a pattern cleanly, v3 returns shouldRender: false and the message renders as plain markdown. This is what real users see today.
  • v1 / v2 — DELETED (audit F019). The legacy 8-rule cascade (v1) and its experimental rewrite (v2) no longer exist; their source files are gone. Their history lives in qw-parser-snapshots.md and the postmortem ledger. New work goes in v3.

How routing works. resolveQwParser() is a compatibility shim that now ALWAYS returns 'v3' (it ignores its argument). Every render site — and the main-process re-parse path in ndjson-text-analysis.ts that sets the pendingAction "Question" dot — calls parseQuestionWidgetV3(...) directly. There is no fallback chain: v3 returning shouldRender: false renders the message as plain markdown.

The "Question" dot is engine-agnostic. That ndjson-text-analysis.ts path is Claude's. For non-Claude engines (Codex, Gemini, OpenCode, slim-ACP, …) the SAME session dot + chime are set at turn conclusion by external-question-pending.ts, which reuses these exact detectors (isTextAskingQuestion / isTextTriggeringQuestionWidget). So a question from any engine flips the session dot to the blue "Question" state and chimes "is asking you a question", exactly like Claude — not the generic amber "Needs You". (The interactive widget + the in-message "Question" chip were already engine-agnostic — they parse the message text client-side; this closes the session-dot + chime gap.) Contract: external-question-pending-contract.md.

Legacy settings. The qwParserVersion and useQwParserV2 AppSettings fields were removed (audit F019). A value persisted before the picker's removal is silently dropped by the settings schema's unknown-key strip and has no effect — every install runs v3 via the single-source QW_PARSER_DEFAULT constant.

Corpus testing. The fixture corpus suite at tests/unit/components/question-widget-corpus.test.ts runs every no-override fixture on the product default (v3) — re-baselined 2026-07-12 (parser-consolidation S1); before that the harness defaulted to v1. Fixtures whose v1-era expectations diverge under v3 are pending: true with a humanNote naming the gap (documented, not gated). A fixture may still carry togglesOverride.qwParserVersion, but it is accepted-and-ignored now (v3 is the only parser). In addition, qw-product-path-characterization.test.ts locks the full product-path parse output of every fixture message against a committed baseline — the identity proof that consolidation work never changes what users see.

Persistence

Your in-progress widget state is debounced-saved to localStorage every 300ms while you click around: the active index, picked options per question, custom-answer text, and the multi-select mode flag. So if you accidentally collapse the message, navigate away, or refresh the renderer, your half-filled answers reload exactly as you left them. The state is cleared on successful submit.

Persistence keys look like qw:<sessionId>:<messageId> and only run when the widget has a messageId — agent-streamed widgets without a stable message id (rare) skip persistence entirely.

The saved-state format carries a schema version (currently v3). A half-filled answer saved under an older version is discarded once on reload (you'd just re-pick) — this is deliberate: an option-shape change can leave an old saved pick pointing at something no longer on screen, which would silently submit the wrong answer, so discarding the stale blob makes that impossible. (v2 landed with the redundant-option filter's re-lettering; v3 landed when free-form questions dropped their synthetic Yes/No pills.)

For agents

Implementation pointers (for agents touching this code)

  • Component: /src/renderer/src/components/ui/QuestionWidget.tsx — single file, <Slot> sub-component renders one question. The verbose multi-line send format is built by composeVerboseResponse(questions, answers, otherTexts, { header, keepSkipped }) + formatAnswerLines() (top-level helpers in the same file; formatAnswerLines returns one answer "atom" per selected option plus any custom text, and composeVerboseResponse renders each atom as its own nested bullet — so multiple picks never collapse into a comma run-on); composeResponse() collapses to a single call that branches on plainSpeakScope to pass the provenance header when the QW rendered out of the Plain Speak overlay (and null otherwise). Both call sites share the same selection/skipped-question Maps — the body shape is identical; only the leading header differs.
  • Parser: /src/shared/question-widget-parser-v3.ts — parseQuestionWidgetV3(content, opts). The six-pattern cascade lives here. Pass opts.plainSpeakScope: true to enable the canonical-priority pre-pass described above. Plumbing chain: PlainEnglishOverlay → AgentMarkdown (plainSpeakScope prop, threaded through every renderSegments(segments, opts) call site) → agent-markdown-segments.ts parseContentSegments (PreprocessConfig.plainSpeakScope) → parseQuestionWidgetV3(opts) for parsing, AND → <QuestionWidget plainSpeakScope={...} /> for the send-format header branch. Every layer in the chain defaults the flag to false; both branches emit the same verbose body, the flag only controls whether the provenance header is prepended.
  • Grid CSS: .qw-stable .qw-slot { grid-area: 1 / 1 } in /src/renderer/src/styles/globals.css. Active-slot slide classes: qw-slot-fwd, qw-slot-bck.
  • Raised-chrome CSS: the .qw-glass.raised … block in /src/renderer/src/styles/globals.css (just above the mobile @media section). Not uniform gloss — the look is tiered: the big option boxes (.qw-pill / .qw-other-btn) and the selected box (.qw-pill.sel) are lift-only (drop shadow + a faint inset 0 1px 0 top edge, no face gradient — the wide stacked boxes read too strong with the sheen); the letter badges (.qw-pill-badge) keep their glossy chip; the Send key carries the full chrome in BOTH states — idle (.qw-glass.raised .qw-send) is a tinted accent key with the sheen, and the picked ready state (.qw-glass.raised .qw-send.ready) is the MAX gloss. QuestionWidget.tsx adds the raised class to .qw-glass when qwRaisedChromeEnabled (default true, via ?? true). Trap: the ready Send's animated conic-gradient border lives in background, so its gloss is layered as a multi-layer background (sheen padding-box + accent fill padding-box + conic border-box) — never via background-image, which would erase the conic. The card rule re-states the accent focus ring so :focus-within isn't clobbered. Setting field: /src/shared/types/settings/chat-ui-settings.ts qwRaisedChromeEnabled?: boolean (default in CHAT_UI_DEFAULTS) + Zod in /src/shared/ipc-schemas/settings/chat-ui-settings.ts + toggle in /src/renderer/src/features/settings/QuestionsSettings-definitions.tsx. Locked by the raised chrome describe block in /tests/unit/question-widget.test.tsx.
  • Question-area weight: .qw-question is font-weight: 700, and a scoped .qw-question strong { font-weight: inherit } rule (same file, just below .qw-question) caps inline **bold** inside a question to the question's own weight. Without it, a question with a partial bold span — e.g. **Approve this?** then a trailing clause…, where the parser keeps the ** in question.text — rendered the bold clause as <strong> at the UA-default bolder = weight 900, visibly heavier than the rest of the already-bold question ("extra bolding"). Fully-wrapped headers (**Q?**) are unaffected — the parser strips those markers, so no <strong> is produced. Locked by /tests/unit/lint/qw-question-no-extra-bold-css-contract.test.ts.
  • Setting field: /src/shared/types.ts AppSettings.stableQuestionPosition: boolean (default true); Zod field in updateSettingsSchema.
  • Tests: /tests/unit/question-widget.test.tsx — see the stable position describe block for slot rendering, slide direction, persistence hydration, and the stableQuestionPosition=false escape-hatch coverage.
  • Canonical-priority fixtures: /tests/fixtures/question-widget/_archived/qw-canonical-priority/ — 3 fixtures covering the trigger (false-positive lead-in suppression, scope-off symmetry, streaming partial). Drive the corpus via npm run test:qw-corpus. The pivotal RED case is 02-false-positive-suppressed/ — without the pre-pass the parser returns 3 questions (lead-in bold + 2 canonical); with the pre-pass it returns the canonical 2.
  • NAV_COOLDOWN: 300ms between Previous/Next clicks. Tests must vi.advanceTimersByTime(350) between sequential nav clicks.
  • Undo / redo hook: /src/renderer/src/hooks/useTextUndoRedo.ts — generic textarea undo/redo with debounced snapshot coalescing; resetKey is composed as ${sessionId}:${messageId}:${slotIndex} so the stack is per-slot per-message.

Performance invariant — inactive Slots don't re-render on typing

Inactive question Slots do not re-render when the user types into the active Custom-answer textarea. This is locked in by the regression tests in /tests/unit/question-widget.test.tsx under describe('typing perf regression', ...). If you add a prop to <Slot>, ensure it is referentially stable across parent renders (no inline objects/arrays/closures from the parent) — otherwise the React.memo wrapper silently un-binds and per-keystroke lag returns. The same invariant applies to the parseContentSegments call in /src/renderer/src/components/ui/AgentMarkdown.tsx and /src/renderer/src/components/ui/MessageBubble/MessageBubble.tsx: both wrap the call in useMemo keyed on the inputs the parser actually reads, so unrelated parent re-renders do not re-tokenize the message.

Related

Question Widget (inline answer pills with stable position) is the half a user reads — what the widget is, where its settings live, and how to answer a question. The parser contract and the other files listed above hold the invariants behind what this half describes.

Last verified 2026-10-06