Agent Message Display (structured agent prose inline)
How a session feed draws an agent's reply — substantive prose between tool calls is promoted inline while short narration folds into an activity pill, plus the settings, the copying controls, and the multi-gate fold that change what you see.
What it is
When the Claude agent works on a task, it usually interleaves narration with tool calls — for example, "Let me check this file" followed by a Read call, then "I'll update the imports" followed by an Edit, and so on. In a session feed, those tool calls and the short narration around them get folded into a small activity pill ("Used 7 tools ▾") so the chat stays readable. Click the pill to expand the full step-by-step.
The activity pill works fine for short narration ("Let me check this", "I'll edit that file"), but it is wrong for substantive content — when the agent writes a markdown heading, a real bullet list, or a long paragraph of analysis between tool calls, that content is the agent's actual answer to you. Hiding it behind the expander makes the message look empty: the agent appears to have done a bunch of tool calls and said nothing, when in reality the answer was sitting one click away.
Real-message promotion fixes this. When the agent's response includes prose that looks like a real answer — markdown headings, structured bullet lists, or a long paragraph — that prose now renders inline in the main message bubble alongside the activity pill, in the order it was written. Short narration like "Let me look at this file" still gets folded behind the pill. The agent's tool work and the agent's actual writing both stay visible without you having to expand anything.
The feature ships on by default. Toggle it off in Settings → Appearance → "Show structured agent prose inline" if you prefer the old fully-collapsed look where every word between tool calls hides behind the activity pill.
Where to find it
How to use it
You don't need to do anything. The feature is on by default. When the agent finishes a turn, you'll see:
- The agent's structured prose (headings, bullet lists, long paragraphs) rendered as part of the main message bubble.
- An activity pill ("Used 5 tools ▾") next to or above the prose, in the same source order it was emitted, holding the tool calls plus any short narration.
- Click the pill to expand the tool steps if you want to see what the agent did. Otherwise just read the prose.
If you want to turn it off:
- Open Settings (gear icon, or via the Settings sidebar entry).
- Go to the Appearance section.
- Find the Show structured agent prose inline toggle. Switch it off.
- From now on, all prose between tool calls — substantive or not — collapses into the activity-pill expander, just like it did before this feature shipped.
The setting is searchable via Ctrl+K (settings search): try "structured", "prose", "inline", or "activity pill".
Settings
Setting field name (in AppSettings) |
UI label | Section | Default |
|---|---|---|---|
promoteRealMessages |
Show structured agent prose inline | Appearance | on |
How it behaves
What gets promoted
Prose between tool calls counts as a "real message" — and renders inline — if any of these hold:
- It contains a markdown heading. Any line that starts with
#through######qualifies. - It is a structured bullet list. Three or more bullet items (
-or*) totalling at least ~300 characters. - It is long pure prose. A continuous paragraph of roughly 800+ characters with no headings or bullet structure.
Anything else — short asides, a single bullet, a one-line "Let me check" — stays hidden behind the activity pill, where it belongs.
These thresholds were tuned against 9,382 real agent messages with hidden prose; the chosen rules promote about 11.8% of cases (every clearly-valuable structured response) without dragging short tool narration along with them. See docs/plans/2026-04-26-promote-real-messages-backtest.md for the dataset, the rule alternatives that were tested, and the false-positive analysis.
When an agent hides its own work — "see above" about text you can't see
Agents mark where their final answer starts. Usually they get it right and everything before the mark — tool steps, thinking, working notes — folds into the activity row.
Sometimes they get it wrong. An agent writes something FOR you — a draft email, a document, a list — then puts the mark after it and signs off with "the draft is above." Everything above the mark is folded, so the reply you actually see is a pointer at text you cannot see. It reads as an empty answer, and the usual fix is to ask again.
Why it happens. Claude Code hands Omniscio one whole assistant message and never divides it. So when an agent puts its mark in the middle of that one message, the split is the agent's own claim, and nothing on Omniscio's side can check it.
Where it bites hardest: Ask Omniscio. The help agent is deliberately not allowed to create or edit files, so a "draft me an email" request runs with no tool steps at all — and the existing protection ("Never hide the answer after the last tool") only arms on turns that used tools. On a pure drafting reply it can never fire.
The setting. Settings → Features → "Trust Claude Code's own final answer over the agent's
marker" (finalMessageCliBoundaryWins, off by default). Turn it on and Claude Code's own
answer boundary wins over the agent's mark, so a deliverable written before the mark can no longer
be hidden.
The trade-off, and why it ships off: on some replies you will also see the agent's working notes above its clean report. Turn it on, watch a few replies, and decide.
Nothing was ever lost either way — the folded text is in the collapsed row above the reply and opens with a click. It is labelled like tool activity, which is why it reads as missing.
Dev-pipeline multi-gate fold
The dev-pipeline runs a task through gated phases, and each phase ends with a short "gate report" (## ⚔️ Red Team Complete, ## 🔨 Build Complete, …). Normally each report is its own message and renders cleanly. But in autonomous mode the agent self-advances the green gates within a single turn, so one message can carry several gate reports back-to-back — plus all the edits, tests, and step-by-step narration between them.
Now, when a single message holds 2 or more gate reports (and no explicit end-marker), the display shows just the stack of gate reports and folds all the in-between work into the one activity pill — one click away, nothing lost. Before this fix only the first report was recognized as the final message, so everything after it spilled into the bubble as a wall of activity.
The same fold also covers a single gate report that keeps working after it (2026-08-11). A turn that posts one report — say the always-on Standards Audit — and then flows straight into the next phase's tool work (or is interrupted mid-step) used to render the clean report card plus a stray "N actions" blob dangling underneath it. Now that trailing work folds into the report's own pill, so the turn is one clean card. A trailing chunk that is itself a genuine next-phase report (its own gate header) still gets its own card — only header-less wrap-up work folds.
It is scoped to the dev-pipeline: the fold only activates when the dev-pipeline system is enabled (the same master switch that turns the pipeline on), so it can never change how any other message renders — and a message that merely quotes gate headers inside a code block never triggers it.
Copying a message — answer vs. everything
A message's ⋯ actions menu copies the same body this display logic leaves visible, OR the full transcript behind it:
- Copy Markdown / Copy Formatted copy the final answer only — the visible body derived the same way the activity-pill fold derives it. It is
cleanMarkdownin MessageBubble.tsx: the settled path reconstructs from the exact segments the bubble renders (segmentsToMarkdown(selectOverviewBodySegments(...))), the streaming path isstripToolLines(preprocessContent(content)). Formatted writestext/html(from the rendered DOM) +text/plain. - Copy everything (agent messages only; an inline-expand submenu mirroring Export) → As Markdown / As Formatted copy the full transcript for the turn: every
▸tool call,←tool result, the agent's narration, and the final answer — everything the activity pill folds away, kept in. It isfullTranscriptMarkdown(preprocessContent(content))— the fence-aware INVERSE ofstripToolLinesFenceAware(keeps ▸/← + all prose; strips only the invisible[[OMNISCIO_FINAL]]sentinel + the Plain-Speak tail) — in src/shared/agent-content-markers.ts.
Notes for agents touching this:
- Cold-mount "lite" rows strip ▸/← markers from
message.content, so the everything-copy fetches the full turn text viaIPC.SESSION_GET_TURN_CONTENTfirst (memoized in a ref) — the same IPC the lazy activity pill uses (lazy-content-load.md). - Formatted-everything renders the transcript markdown to portable HTML via
renderTranscriptHtml(transcript-to-html.ts), dynamic-imported so react-markdown never enters the sync-preload bundle (the AgentMarkdown façade contract, I1); it falls back to plain text on any render failure. - The extended-thinking body is not stored — ingestion keeps only a
▸ Extended thinking (N chars)marker (ndjson-utils.ts) — so "everything" is tool activity + narration + answer, never the raw hidden reasoning. - The tool DETAIL — the actual shell command, search query, or delegation prompt — IS stored in full (up to
TOOL_MARKER_DETAIL_MAX_CHARS, 2,000 chars, in agent-content-markers.ts). Until 2026-09-07 it was clipped to 80 chars at INGESTION by bothsummarizeToolInputand Codex'sclipDetail, so the row only ever held a stub and no surface — expanded activity, copy-everything, or a later query — could recover what actually ran. Truncating for looks is the RENDERER's job: in the expanded list a line past 180 chars is clamped to two lines with a Show full command control beside it that reveals the whole thing (ActivityLinein agent-markdown-tool-activity.tsx). That control is a SIBLING of the text, never a wrapper around it — the text must stay a leaf<div>carrying[overflow-wrap:anywhere]or the QIIMY bubble-overflow lock stops pinning it and a long spaceless token can widen the bubble again (agent-markdown-tool-activity-overflow.test.tsx). Turns recorded before that date keep their baked-in...— the text was never written down. - Tests: tests/unit/shared/agent-content-markers.test.ts (
fullTranscriptMarkdown), tests/unit/components/MessageActionsMenu.copy-everything.test.tsx, tests/unit/components/transcript-to-html.test.ts.
Copying a table out of a message
A rendered markdown table (chat transcript, team chat, file previews, scratchpads, council) carries one copy control at its top-right: a ⋯ menu naming both styles — Copy as Markdown (GFM markdown) and Copy table for Excel / Google Sheets. On desktop it appears when you hover the table; on a touch screen it is always shown.
The spreadsheet style exists because markdown is not a format a spreadsheet understands: Sheets and Excel split a paste into cells only when the clipboard carries a text/html table, so a markdown paste lands in one cell. That item writes BOTH flavors — an escaped <table> (a real <thead>/<th> plus <tbody>/<td>) for the rich path, and a tab-separated plain twin for the fallback, which those apps also split into cells. The control's promise therefore holds even when the rich write is refused (window backgrounded, clipboard locked); only when BOTH paths fail does the user see a toast — and an empty table is a reported no-op, never a silent overwrite of whatever was already on the clipboard.
Notes for agents touching this:
- All three formats come from ONE row-walker —
parseTableDomin agent-markdown-table-utils.ts returns raw (whitespace-collapsed, trimmed) cell text plus the header/body split and a rectangularised view.tableToMarkdownescapes pipes,tableToHtmlescapes HTML,tableToTsvjoins on tabs. Sharing one parser is what keeps them from disagreeing about the content, andtableToMarkdown's output stays byte-identical to what it produced before the parser existed (its original assertions are the check). - The HTML is built from the parsed rows, never serialized from the live DOM — the rendered
<table>carries React classes anddata-labelattributes that would otherwise ride into the user's spreadsheet. - Cells are escaped text (the shared
escapeHtml) — a cell containing markup stays literal and can never become live HTML inside a pasted sheet. - The rich write reuses the shipped path —
copyHtmlToClipboardin clipboard.ts is the samenavigator.clipboard.write([ClipboardItem])shape the message-level Copy Formatted action already makes: no IPC channel, no main-process change, returns a boolean instead of throwing. - One control, compact on a phone (2026-09-25). A one-click
Copy tablechip used to sit beside the ⋯; it was folded into it — Copy as Markdown writes the same text, and the ⋯ flashes a check on a successful write — the same treatment the chat code block's corner got on 2026-09-16. The trigger passesnoMobileTouchFloor: without it the sharedIconButton44px mobile tap floor, under the trigger's translucent backdrop, painted a dark 44px square over the first row's text in the phone's card layout (reported as "a gigantic field around it"). It sits at physicalright-1, the chip's old spot, not a logicalend-1: the shared panel opens fromright-0, so a logical inset would put the trigger on the table's left edge underdir="rtl"and open the panel off it. Pinned by agent-markdown-table-copy.test.tsx and the source guard agent-markdown-group-scope.test.ts. - Out of scope: the KMS rich-text editor's own table node is a different component and gets none of this.
- The ⋯ control is the shared
OverflowMenu(OverflowMenu.tsx), so this table, the chat code block, the Drive/Dropbox rows and the session rows all dismiss it identically: a press anywhere outside the trigger + panel, Escape, Tab, picking an item, or re-pressing the trigger. The outside press is wired topointerdown(notclick) so it lands on a touch gesture too, and it is scoped to the whole widget — wiring it to the panel alone would read the trigger's own press as "outside", close on the way down, and let the trigger's toggle re-open it. Added 2026-09-17; until then the only exits were Escape/Tab/an item/re-press, which a phone cannot perform — reported as "I can't click out of this menu in the table". - Tests: tests/unit/agent-markdown-table-copy.test.tsx — converter units, cross-format agreement, the markup-injection case, and the exact payload a real click puts on the clipboard. Menu dismissal: tests/unit/components/OverflowMenu-outside-dismiss.test.tsx.
For agents
Implementation pointers (for agents touching this code)
- Setting field: src/shared/types.ts
AppSettings.promoteRealMessages: boolean(defaulttrue); matching Zod field inupdateSettingsSchema(src/shared/ipc-schemas.ts). - Toggle UI: src/renderer/src/features/settings/AppearanceSettings.tsx —
data-setting-id="promote-real-messages". - Heuristic + thresholds: src/renderer/src/components/ui/agent-message-heuristics.ts —
isRealMessageProse(text)and theREAL_MESSAGE_THRESHOLDSknob block (minBullets,minBulletsCharLength,minPureProseLength). - Splitter: src/renderer/src/components/ui/agent-markdown-content.ts —
computeActivityOverview(segments, { promoteRealMessages })is the consumer; when the flag is on, prose segments before the last activity boundary that passisRealMessageProseget moved intotrailingSegments(which the renderer puts inline) instead ofhiddenSegments(which the activity-pill expander shows). - Lite-mode rescue (2026-05-21, removed 2026-05-24): A renderer-only paragraph-walker (
splitLiteProseByHeuristic) used to fall back when cold-mount lite payloads had their tool markers stripped (see lazy-content-load.md § "The narration-leak rescue"). It was deleted once schema v227 re-derivedsummary_prosecorrectly for everyhas_tool_activity = 1row — any bubble that still looks wrong is now a backend re-derive bug, not a render-time patch. The kill switchAMC_DISABLE_NARRATION_RESCUEwas fully removed 2026-07-06 (a no-op since v227; its preload deprecation warning is gone). - Tests: tests/unit/agent-markdown-content-promotion.test.ts covers the splitter, the threshold edges, and the off-by-default behavior when the setting is false.
- Multi-gate dev-pipeline fold (2026-07-19; single-report extension 2026-08-11): an autonomous dev-pipeline turn with ≥2 gate reports in one message — OR a single gate report whose marker is FOLLOWED by trailing tool work (an interrupted / auto-advanced turn, e.g. a Standards Audit that flowed into git-prep) — shows only the report(s); the preamble + inter-phase narration + all activity fold into the pill. Predicate + region walker: src/shared/agent-content-markers.ts (
multiGateFoldActive+hasToolActivityAfterLastGateMarkerfor the single-report case,countDevPipelineGateHeaders,extractGateReportProse; the transcript's per-phase split folds the same header-less trailing tail viatailFoldsIntoReport). Both walkers branch on it —deriveAgentProse(src/shared/agent-prose-core.ts) +computeActivityOverview'scomputeMultiGateOverview(src/renderer/src/components/ui/agent-markdown-segments.ts). Gated onsettings.devPipelineSkillEnabled, threaded live to both surfaces (the write-sidesummary_prose+ the renderer). Contract + parity: .claude/memory/contracts/summary-prose-contract.md "Implicit dev-pipeline gate boundary" + .claude/memory/contracts/dev-pipeline-phase-bubbles-contract.md + tests/unit/services/extract-prose-parity.test.ts.
Related
- dev-pipeline.md — the gated workflow whose autonomous multi-report turns the fold above cleans up.
- lazy-content-load.md — the activity pill's expanded content is fetched on-demand at click time (cold mount no longer carries it); the inline-promoted prose described here rides on the lite cold-mount payload alongside the pill itself.
- question-widget.md — multiple-choice questions in the agent's prose render as interactive pills; that's a separate inline-rendering surface that runs after this one.
- compaction-summary.md — another "expand inline to see more" surface, but for context-window auto-compaction summaries on the divider, not for everyday turn output.
- Implementation plan: docs/plans/2026-04-26-promote-real-messages.md — the plan that shipped this feature.
- Backtest data: docs/plans/2026-04-26-promote-real-messages-backtest.md — the 9,382-message corpus, rule comparisons, and threshold rationale.
Last verified 2026-10-06