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

Real Conversation Layout — part 2 (how the panel decides what to render)

The decision layer behind the Real Conversation Layout: which rows the chat panel is allowed to show, how it decides a turn was real conversation rather than plumbing, the opt-in marker that pins a final answer, and the gate reports and no-answer turns it handles specially. The visible result is described in part 1.

What it is

The rules underneath the Real Conversation Layout: what the chat panel is allowed to render, and how it decides that a turn was real conversation rather than plumbing. Part 1, real-conversation-layout.md, describes what a reader actually sees on screen; this part is the machinery that decides it.

Where to find it

There is nothing to open. This is the chat panel's own rendering logic, so it has no menu, button or setting of its own — the one control a reader can actuate, the layout toggle and its kill switch, is described in part 1.

How it behaves

How the chat panel decides what to render

When the panel mounts, the cold-mount fetch goes through getSessionHistoryLite() — the same lite path lazy-content-load already uses, so each row arrives with summary_prose and has_tool_activity instead of full content. With the layout on, those rows are then passed through a single pure function:

buildRealConversationTurns(messages, { excludeAutoResponses: true })

which walks the array once and produces an array of "turns" plus a top-level system-row filter:

  • Filter at source. Plain source === 'system' rows with no compactMeta.kind are dropped before bucketing — that's the "Session ready" fix. Every kinded system row (rate-limit, snooze, seed-context, subagent-result, auth-retry, compact-divider) keeps flowing.
  • Classifier. Inside each turn, classifyAgentRows walks the agent rows backwards from the end. A trailing agent row joins the real reply when it carries a non-empty prose answer — whether it is pure-prose OR a mixed row (tool calls + a trailing answer; R13/rev-4 — a single Anthropic row can stream both text and tool_use blocks); the run stops at the first pure-tool row, and everything before it folds into the pill. On a turn the agent has MOVED PAST (a non-terminal close), a mixed row whose trailing prose is NON-substantive intermediate narration ("Let me try X:") ALSO folds instead of promoting — so a dangling "let me…" intro, left behind when a park-and-wake turnBoundary split cut the turn mid-sentence, never shows as a fake final message (R69/Rev 46, 2026-07-15). Woken-no-op and canonical "Waiting…" replies are left promoted for their own dedicated fold; the live/terminal turn still shows even a short reply.

Each turn record carries:

  • the real user message that opened it — or, for a turn that opens with no user message (a deferred background task finishing and pinging the session back to life, OR a recovery re-running a turn that already answered — auth/token refresh, rate-limit move, account switch, stuck-turn sweep), the backend stamps a turnBoundary marker on that resume reply and the renderer opens a fresh turn on it, so the real answer renders as its own turn instead of collapsing into the prior one (R54, 2026-05-25; extended to recovery re-runs 2026-06-19),
  • the agent's real reply rows (the trailing pure-prose run; may be empty for in-flight or tool-ending turns),
  • the hidden agent rows that get folded into the pill,
  • the action count that goes on the pill label,
  • the system rows that fall between turns (top-level kinded markers),
  • per compact-divider row, a windowMessages: ConversationMessage[] bucket — every agent + kinded-system row between this divider and the previous landmark, NOT also surfaced at top level. The bucketing IS the hide for in-window rows — the rev-9 CompactionExpander no longer renders these rows visibly inside the chevron (the body is JUST the summary), but the bucketing still happens so the live turn body doesn't double-show pre-compact rows. The bucketed rows remain in the DB and stay visible via search / export / share / audit.

The renderer then walks the turn array and emits one <TurnGroup> per turn. For each turn, TurnGroup picks between two shapes (rev-13, 2026-05-22):

  1. Merged shape — turn has hidden activity OR is in flight: one <MergedTurnBubble> whose header carries the activity toggle (chevron + bot icon + a viewport-responsive activity label + "took Xs" + timestamp — the activity label renders in every state including collapsed-settled (R67, 2026-06-21) so the count is a glanceable "worth expanding?" signal, while the "took Xs" duration is still shown only when the turn is expanded or streaming) and whose body is the agent's final-answer prose. The per-turn tool detail renders in a panel between header and body when the user expands.
  2. Plain shape — settled turn with zero hidden activity: a plain <MessageBubble suppressActivityPill> for the final reply, no merged wrapper. Common for turns where the agent just replied with prose and no tools.

Each top-level kinded system row becomes its own MessageBubble. Each compact-divider row becomes a <CompactionExpander> showing a > Compaction N chevron whose body lazy-fetches and renders the model-generated summary as markdown (single-level expander, rev-9 — no nested sub-chevron, no in-expander windowed rows). If the divider claimed agent rows in its metadata.compactWindow.rows (the inter-compaction case, rev-11), a <TurnActivityBubble> mounts directly above the expander to surface that claimed work — this is the only remaining direct caller of TurnActivityBubble after rev-13; the per-turn position routes through MergedTurnBubble. After rev-23 (2026-05-25), TurnActivityBubble wraps MessageBubble via the same headerIdentitySlot + aboveBodySlot slot pattern MergedTurnBubble uses, so the inter-compaction bubble paints with identical chrome (border, surface background, left emerald rail) to every other agent row — single source of chrome truth, no drift. Its header label is a viewport-responsive pair (R56, 2026-05-26): desktop renders C comments, A actions in Z from the v233 persisted comment_count / tool_count column sums, while a md:hidden mobile span drops the action count to C comments in Z (falling through to A actions in Z for a comment-less window so it never reads "0 comments"). The split is CSS-only — two spans toggled at the md: breakpoint, no useIsMobile() hook, so there is no SSR / first-paint mismatch. When any claimed row's persisted count is null (a pre-v233 row, an un-backfilled install) the per-render sum collapses to null and both spans degrade to the legacy N actions took Z label off the lite-safe action count. A kinded compaction-in-progress system row (emitted while Omniscio is actively compacting) is intercepted by TurnGroup and rendered as a pulsing amber <CompactingIndicator> below the final answer, not as its own message bubble.

MergedTurnBubble delegates the bubble chrome (border, hover, action menu, share, ARIA, autoresponse badge, plain-speak toggle) to MessageBubble via two new slot props: headerIdentitySlot (replaces the default Bot · Agent · timestamp trio with the activity toggle) and aboveBodySlot (renders the expansion panel between header and body). The merged bubble owns the per-turn state — open/close, lite-fetch on first expand, thinking timer — while MessageBubble owns the visual shell. The synthesized body that paints in the bubble swaps between the final-answer content (settled) and the aggregated live content (in flight) based on the streaming gate.

Multi-row real replies render the last row. Most turns produce a single real-reply row, but a long turn can produce 2+ — the most common shape is [mixed-row-with-narration, pure-prose-final], where an earlier agent row mixed connective prose with a tool call (and was still classified as a real reply because its prose tail was pure) and a later pure-prose row capped the turn. As of 2026-05-24, TurnGroup renders ONLY the last real-reply row's content in the final bubble — earlier rows already ride in the activity bubble above via turn.hiddenAgentMessages, so joining them with blank lines into the final bubble was duplication. The earlier mergeFinalMessageMetadata synthesis that OR-combined hasToolActivity flags for the deleted AgentMarkdown narration rescue was removed in the same change — schema v227 re-derives summary_prose correctly at the source, so the renderer no longer needs the rescue gate. One consequence of "render the last row": if a later resume merges a throwaway reply into a turn that already held your real answer, that reply becomes the last row and displaces the answer into the activity zone. The turn builder guards the known case of this — a rate-limit-recovery / lb-rebalance no-op (the exact "Nothing to do here…" sentinel) — by splitting it into its own turn so it can never become the last row of your answer's turn (2026-06-30, buried-final-answer fix); see woken-noop-sentinel.md.

The merged bubble's header label reads N comments · N tool calls (desktop) / N comments (mobile) — it describes the turn's WORK and never the word "Thinking" (the former extended-thinking prefix was removed 2026-06-22). The comment + tool counts are the persisted comment_count / tool_count columns, derived ONCE at finalize and surfaced to the renderer TOP-LEVEL (ConversationMessage.commentCount / toolCount) on EVERY path — the full-history read (mapMessage), the lite read, AND the live finalize push (streaming-finalizer → SESSION_OUTPUT) — so a turn shows the same count while you watch it live as after you reopen the session. When those persisted counts are absent (a pre-v233 row), the label degrades to a lite-safe turn.actionCount tool-call count (derived at finalize from has_tool_activity), which survives a compaction reload unchanged. The counts are never recomputed on the renderer. Full invariant: merged-turn-bubble-contract.md header-label-describes-the-work-only. When the user clicks the header to expand, a settled lite turn fires one SESSION_GET_TURN_CONTENT IPC per hidden row keyed on the row's metadata.liteContent.fullContentMessageId, then joins the responses into the expansion panel; a live (in-flight) turn aggregates its hidden rows' content from props synchronously with no IPC. Collapsing the header releases the fetched body so memory stays flat on long threads.

For streaming, the useSessionConversationTurns sub-hook (under useSessionPanel/) derives an in-flight flag from existing run-state metadata + the row position. When the flag is set, MergedTurnBubble auto-expands the header (aria-expanded="true") and renders the aggregated live content in the bubble body (interleaved tools + connective prose, with isStreaming={true}) — the streaming prose stays visible mid-stream per R8. The expansion panel between header and body stays empty during streaming so the same prose never shows twice. (A mixed row that carries both tool calls and prose rides in the hidden rows and contributes to the aggregated body, so its prose stays visible mid-stream; a pure-prose final row sits in realReplyMessages and is held back until settle.) The instant the turn settles, the header collapses, the body switches to the final-answer prose, and isStreaming flips to false.

When the layout is off, the panel falls back to the prior render path (VirtualMessageList / MessageList over the unfiltered row array). The unified "Load older messages" pill that legacy layout used to render above the first message is no longer re-enabled by default — it is gated by the LOAD_OLDER_MESSAGES_PILL_ENABLED code-level flag in SessionPanel.tsx, which is false for every user (see load-older-pill-disabled postmortem).

Detection — what counts as "real"

The classifier reuses signals that already exist. It does not invent a new detector and does not re-run prose heuristics at render time.

  • has_tool_activity — the schema v218 column on conversation_messages, derived once at finalize-time by extractProse. This is the authoritative per-row "did this row contain a tool call" signal, and it is what the layout classifier reads. Cheap, deterministic, zero render-time work.
  • isRealMessageProse (Heuristic D) in src/renderer/src/components/ui/agent-message-heuristics.ts — kept only as the kill-switch fallback. When the layout is forced off and rows arrive without has_tool_activity (legacy / non-lite path), the classifier falls back to running this heuristic over the last agent row.
  • computeActivityOverview in src/renderer/src/components/ui/agent-markdown-content.ts — the per-turn action overview that feeds the "N actions" pill label.

The layout reads the already-derived summary_prose and has_tool_activity columns shipped on each lite row, so the work stays cheap even on a 4,000-row session.

The classification rule, in short — the unbroken trailing pure-prose run:

  • Walk the turn's agent rows backwards from the end. A row is part of the real reply if and only if it is an agent row with has_tool_activity === false and every row after it in the turn is also an agent row with has_tool_activity === false.
  • The first row (scanning backwards) that breaks the run — any row with tool activity — ends the real reply. That row and everything before it fold into the "N actions" pill.
  • A turn whose last agent row has tool activity has an empty real reply: only the pill renders, no agent bubble. This is correct — the agent's final act was a tool call, not a message to you.
  • A turn with zero agent rows (a user message still awaiting its reply — streaming or stalled) renders the user bubble only, no pill.

A question is always part of the final message (R-QW-FINAL). The rule above is per-row; a second rule governs the split within a single row's content. If the agent asks a question and then runs only bookkeeping tools (TaskUpdate / TaskCreate) or extended-thinking before its sign-off, the within-row split point pulls back to the last substantive tool — so the question and its sign-off render together as ONE final-answer bubble, with the bookkeeping still shown in the pill above. Without this, a turn that asked a question and then ticked off a to-do rendered as three blocks (question, pill, then a second final-looking message). A real tool after the question (Bash, Edit, …) still splits the turn. Full detail: the summary_prose contract § R-QW-FINAL. (One residual: while a turn is still streaming, the live in-flight view may briefly show the wedge before it heals to this settled view the instant the turn settles.)

Why "walk backwards" and not "promote the last row"? A single Anthropic assistant row streams text and tool calls together. An earlier implementation promoted the last row unconditionally, so a row that mixed preface narration with tool calls was shown as the final reply — the agent's "I'll start by checking X" leaked out as if it were the answer. Reading has_tool_activity per row and requiring an unbroken pure-prose tail fixes that. See real-conversation-layout-rev1-postmortem.md.

Pin the final answer with a marker ([[OMNISCIO_FINAL]], opt-in)

The classifier above is a heuristic — it infers where your final answer starts by walking back through the agent's rows and the within-row tool markers. That inference is right the vast majority of the time, but a few shapes can fool it (a stray bookkeeping tool call or a long stretch of narration right before the answer). If you'd rather the agent tell Omniscio exactly where its answer begins, turn on Settings → Features → "Show only the final message" (off by default).

When it's on, Omniscio adds one line to the teaching note every session already receives: print the marker [[OMNISCIO_FINAL]] on its own line immediately before your final message. When the agent does that, Omniscio shows the text after the marker as the final answer and folds the working notes before it — tool calls, thinking, and throat-clearing narration — into the collapsed activity pill: reachable when you expand the turn, never dropped. The marker line itself never appears, and the "N actions" count is unchanged. It is a deterministic boundary that overrides the heuristic — with one line it will never cross: it never folds a substantive answer (see the last bullet below).

It can only ever make the split more exact, never worse:

  • Marker absent — the toggle is off, an older session, a non-Claude engine, or the agent simply forgot — falls back to the heuristic above, byte-for-byte.
  • Detection is always-on. Omniscio honors an emitted marker whenever it is present, even after you turn the toggle back off — so a message that used the marker still reads correctly later. The toggle only controls whether new sessions are taught to emit it.
  • The legacy [[AMC_FINAL]] marker still counts. The marker was renamed from the old AMC_ prefix to OMNISCIO_, but Omniscio detects BOTH — so a historical message or an in-flight session that emitted [[AMC_FINAL]] folds exactly the same. Only [[OMNISCIO_FINAL]] is taught going forward. (The same dual detection applies to the Plain Speak, narration, and self-archive markers.)
  • Safe by construction. Only the LAST marker outside a code fence counts (an agent explaining the feature inside a ``` block can't trip it), and a marker with nothing after it is ignored (never a blank bubble).
  • Tolerant of light formatting. A marker the agent wrapped to make it stand out still counts — whether in bold or italics (**[[OMNISCIO_FINAL]]**, *…*, _…_) or in a markdown heading (## [[OMNISCIO_FINAL]], any level #…######). Both used to silently break the split and leak the raw marker onto the screen — the heading form rendered as a big title and hid nothing. A backtick-wrapped marker is the deliberate exception: that reads as a code example, so it is never a boundary.
  • Tolerant of a missing line break. If the agent forgets the newline and glues the marker onto the start of the next line ([[OMNISCIO_FINAL]]## Ready to Merge), Omniscio repairs it — splitting the marker back onto its own line — so the raw marker never leaks and the line after it renders normally. A mid-sentence mention of the marker, or one shown as a code example, is left alone.
  • Never hides your answer (2026-07-04, extended 2026-07-09). The marker folds working notes — never a substantive answer, on either side of it. If the agent writes its real answer and then drops the marker right before a question box or a one-line sign-off ("Waiting on a background process…"), Omniscio shows that answer together with the question/sign-off instead of collapsing it — a question always keeps the context you need to answer it. And if the agent writes a substantive summary before the marker (a headline, a conclusion, a deliverable link) followed by its detailed answer after it, both are shown — the summary is no longer swept into the activity pill (the "ALL CLEAR summary vanished" fix). The one thing that still folds a substantive wrap-up is a clean dev-pipeline gate report, which is meant to stand on its own; trivial throat-clearing chatter before the marker also still folds. (Older messages already affected re-derive correctly the next time Omniscio loads them.)
  • A repeated lead line shows once (2026-07-06). On a turn that did no tool work (pure thinking, so there's no activity pill to fold notes into), the marker can't collapse the notes above it — showing them is the safe choice, since there's nowhere to tuck them. But if the agent happened to write the very same opening line twice — once as a note right before the marker and again as its answer's first line — Omniscio now shows that line once instead of twice. It only ever removes an exact, byte-for-byte duplicate that still appears in the answer, so nothing is lost; the instant anything differs, both are kept.
  • Make the cut absolute — even on a no-tool turn (opt-in sub-toggle, 2026-07-11). By default the marker only folds the notes above it when the turn did real tool work — there's an activity pill to tuck them into; on a pure-thinking answer it leaves them visible (previous bullet). Turn on the nested "Always hide everything before the marker (even without tool activity)" — under "Show only the final message," off by default — to make the marker an absolute cut on every turn: everything before it folds into the same standard collapsed activity row the rest of the app already uses — a pure-thinking answer into the normal Thinking row, tool work into the N actions row (2026-07-13: a thinking-only turn no longer draws a one-off "hidden" band; it's the same collapse you see on every other turn). When the notes it folds also carry prose, the turn keeps that same N actions row as its header — its collapse holds the thinking, the tool calls and the notes together (2026-09-19), so the fold always lives in the header you already know rather than anywhere else. It stays reachable, never deleted, so nothing is lost; it just isn't shown until you expand it. (Turn it off to restore the behavior above, byte-for-byte.)
  • Show every final message when an agent posts more than one (opt-in sub-toggle, 2026-07-13). A long or autonomous session sometimes marks more than one final message in a single turn — a status report, then a follow-up, then a sign-off. By default only the last marked message is shown and everything before it folds, so an earlier report can end up hidden even though the agent meant it as a final message (the "see the full report above" that wasn't actually above). Turn on the nested "Show every final message when an agent posts more than one" — under "Show only the final message," off by default — and Omniscio opens the final region at the first marker instead: every marked block is shown, in order, with only the tool work between them folded into the activity row. It is the explicit-marker version of how dev-pipeline gate reports already behave, and it never hides the agent's writing — only tool activity folds. Single-marker turns are completely unaffected. (Turn it off to restore the last-marker-only behavior above, byte-for-byte.)

Full mechanics + the three lockstep walkers that honor it (write-side extractProse, the renderer bridge, and the render-path computeActivityOverview): the summary_prose contract § "Final-message marker" (.claude/memory/contracts/summary-prose-contract.md).

Dev-pipeline gate reports show a pipeline stepper

When a session runs the dev-pipeline skill, each phase ends with a status line — [DEV-PIPELINE | GATE 5 | AUTO-APPROVE: OK]. Instead of that raw text, Omniscio draws a compact pipeline stepper at the end of the message: the full pipeline row of eight phase discs (🔍 Plan · ⚔️ Red Team · 🚦 Standards Check · 🔨 Build · 💎 Elegance · 📝 Docs · 🔬 Standards Audit · 🚀 Merge) with the completed gates filled and the current one lit at its true position in the verdict colour — green when the phase is done, amber when it needs you, a brighter green when the branch is tagged (READY-TO-MERGE), red when it's blocked (NOT-READY). The widget is topped by a "Dev Pipeline" labeled divider so it's self-identifying in chat, and the caption is minimal — a single amber "phase needs you — reason" line only when a gate needs you, and nothing when a gate is flowing through (the labeled, colour-coded track already carries the phase, progress, and status; an off-screen summary keeps it audible to screen readers). The final Ready-to-Merge gate uses that same track — the rocket is the lit disc — capped by a quiet caption: a green check beside "Branch tagged · every gate green" — a still, one-line confirmation in the same shape as the other gates' captions, with no box and no motion. A blocked finish shows the blocker reason instead. Because that caption already says the branch is tagged and every gate is green, Omniscio also hides the report's own duplicate bottom line (Branch … tagged ready to merge, whose branch/commit pills wrapped awkwardly on a phone) so the finish is just the clean caption. It is render-only: the raw marker — and that summary line — stay in the saved message, so copy, search, and the pipeline's own auto-advance are unaffected. The two always-on standards phases (🚦 Standards Check, 🔬 Standards Audit) carry a named gate marker (GATE CUSTOM_…) instead of a number, but because they live in the pipeline they light their own disc in that SAME eight-disc row — so every gate bubble shows the identical row — and they split into their own bubble in an autonomous run just like every numbered phase. Only a user-defined custom phase the row can't place falls back to a compact gate badge — the phase's own icon + name, green when it passed or amber with its reason when it needs you. Off-switch: build with AMC_DISABLE_GATE_STEPPER=1. Full mechanics: the gate-stepper contract.

A turn that ended with no answer collapses to a one-line summary

Some turns genuinely finish on tool/thinking work with no closing message — for example, when the conversation hit an automatic compaction mid-turn and the actual answer came afterward in a fresh turn, or when a work segment simply ended on a tool call. These reply-less turns used to auto-expand every step inline the moment you opened the session — a wall of "thinking… → ran a tool → thought again" with nothing tying it off. Users read that as "why is this showing me all the thinking steps?"

Now such a turn renders as a clean, collapsed one-line summary — 🤖 4 comments · 6 tool calls · 2:25 PM — with the steps one click away, exactly like a normal finished turn. Nothing is lost: the expansion is still there, just closed by default. Compaction itself stays the quiet Compaction N divider between turns; no banner, no callout.

The collapse fires when the system is certain the turn has no answer to show. Concretely, the turn produced no promoted reply and every folded-away row is confirmed answer-less by one of three signals: it carries the producer's confirmed-empty mark (summary_prose = '', written once at finalize time); it's an interrupted attempt whose derived answer is empty; or it was cut off mid-tool — its content ends on an unfinished tool call, which is the boundary a final message would sit after, so there's no room for a trailing answer. The one case still deliberately left auto-expanding: an older backlog row where an answer might be buried just before a final bookkeeping tool call — a bookkeeping tail (▸ TaskUpdate) and a completed tool result are never treated as a cut-off, so a real answer can never be hidden. Nothing is lost either way: a collapsed turn always shows its labeled header with the full content one click away. The render-side behavior (which turns collapse vs auto-expand, and the collapsed summary label) lives in MergedTurnBubble.tsx; the certainty flag (confidentlyReplyless) is computed once by the turn classifier in src/shared/real-conversation-turns.ts. Full invariant: merged-turn-bubble-contract.md (replyless-shape-3a-collapses-cleanly + the shape #3a/#3b table).

Related

Part 1, real-conversation-layout.md, is the page to read first — it covers the visible layout, when it applies and the setting that governs it. Part 3, real-conversation-layout-part-3.md, covers the cold-mount fetch, the compact clamp, and the map of the code the feature lives in.

Last verified 2026-10-05