---
title: Agent Message Display (structured agent prose inline)
---

# Agent Message Display (structured agent prose inline)

## 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:

1. Open **Settings** (gear icon, or via the Settings sidebar entry).
2. Go to the **Appearance** section.
3. Find the **Show structured agent prose inline** toggle. Switch it off.
4. 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](../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](dev-pipeline.md) 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 `cleanMarkdown` in [MessageBubble.tsx](/src/renderer/src/components/ui/MessageBubble/MessageBubble.tsx): the settled path reconstructs from the exact segments the bubble renders (`segmentsToMarkdown(selectOverviewBodySegments(...))`), the streaming path is `stripToolLines(preprocessContent(content))`. Formatted writes `text/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 is `fullTranscriptMarkdown(preprocessContent(content))` — the fence-aware INVERSE of `stripToolLinesFenceAware` (keeps ▸/← + all prose; strips only the invisible `[[OMNISCIO_FINAL]]` sentinel + the Plain-Speak tail) — in [src/shared/agent-content-markers.ts](/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 via `IPC.SESSION_GET_TURN_CONTENT` first (memoized in a ref) — the same IPC the lazy activity pill uses ([lazy-content-load.md](lazy-content-load.md)).
- **Formatted-everything** renders the transcript markdown to portable HTML via `renderTranscriptHtml` ([transcript-to-html.ts](/src/renderer/src/components/ui/MessageBubble/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](/src/main/process/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](/src/shared/agent-content-markers.ts)). Until 2026-09-07 it was clipped to 80 chars at INGESTION by both `summarizeToolInput` and Codex's `clipDetail`, 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 (`ActivityLine` in [agent-markdown-tool-activity.tsx](/src/renderer/src/components/ui/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](/tests/unit/components/ui/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](/tests/unit/shared/agent-content-markers.test.ts) (`fullTranscriptMarkdown`), [tests/unit/components/MessageActionsMenu.copy-everything.test.tsx](/tests/unit/components/MessageActionsMenu.copy-everything.test.tsx), [tests/unit/components/transcript-to-html.test.ts](/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** — `parseTableDom` in [agent-markdown-table-utils.ts](/src/renderer/src/components/ui/agent-markdown-table-utils.ts) returns raw (whitespace-collapsed, trimmed) cell text plus the header/body split and a rectangularised view. `tableToMarkdown` escapes pipes, `tableToHtml` escapes HTML, `tableToTsv` joins on tabs. Sharing one parser is what keeps them from disagreeing about the content, and `tableToMarkdown`'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 and `data-label` attributes 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** — `copyHtmlToClipboard` in [clipboard.ts](/src/renderer/src/lib/clipboard.ts) is the same `navigator.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 table` chip 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 passes `noMobileTouchFloor`: without it the shared `IconButton` 44px 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 physical `right-1`, the chip's old spot, not a logical `end-1`: the shared panel opens from `right-0`, so a logical inset would put the trigger on the table's left edge under `dir="rtl"` and open the panel off it. Pinned by [agent-markdown-table-copy.test.tsx](/tests/unit/agent-markdown-table-copy.test.tsx) and the source guard [agent-markdown-group-scope.test.ts](/tests/unit/lint/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](/src/renderer/src/components/ui/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 to `pointerdown` (not `click`) 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](/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](/tests/unit/components/OverflowMenu-outside-dismiss.test.tsx).

## For agents

### Implementation pointers (for agents touching this code)

- Setting field: [src/shared/types.ts](/src/shared/types.ts) `AppSettings.promoteRealMessages: boolean` (default `true`); matching Zod field in `updateSettingsSchema` ([src/shared/ipc-schemas.ts](/src/shared/ipc-schemas.ts)).
- Toggle UI: [src/renderer/src/features/settings/AppearanceSettings.tsx](../../src/renderer/src/features/settings/sections/appearance/AppearanceSettings.tsx) — `data-setting-id="promote-real-messages"`.
- Heuristic + thresholds: [src/renderer/src/components/ui/agent-message-heuristics.ts](/src/renderer/src/components/ui/agent-message-heuristics.ts) — `isRealMessageProse(text)` and the `REAL_MESSAGE_THRESHOLDS` knob block (`minBullets`, `minBulletsCharLength`, `minPureProseLength`).
- Splitter: [src/renderer/src/components/ui/agent-markdown-content.ts](/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 pass `isRealMessageProse` get moved into `trailingSegments` (which the renderer puts inline) instead of `hiddenSegments` (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](lazy-content-load.md) § "The narration-leak rescue"). It was deleted once schema v227 re-derived `summary_prose` correctly for every `has_tool_activity = 1` row — any bubble that still looks wrong is now a backend re-derive bug, not a render-time patch. The kill switch `AMC_DISABLE_NARRATION_RESCUE` was 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](/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](/src/shared/agent-content-markers.ts) (`multiGateFoldActive` + `hasToolActivityAfterLastGateMarker` for the single-report case, `countDevPipelineGateHeaders`, `extractGateReportProse`; the transcript's per-phase split folds the same header-less trailing tail via `tailFoldsIntoReport`). Both walkers branch on it — `deriveAgentProse` ([src/shared/agent-prose-core.ts](/src/shared/agent-prose-core.ts)) + `computeActivityOverview`'s `computeMultiGateOverview` ([src/renderer/src/components/ui/agent-markdown-segments.ts](/src/renderer/src/components/ui/agent-markdown-segments.ts)). Gated on `settings.devPipelineSkillEnabled`, threaded live to both surfaces (the write-side `summary_prose` + the renderer). Contract + parity: [.claude/memory/contracts/summary-prose-contract.md](/.claude/memory/contracts/summary-prose-contract.md) "Implicit dev-pipeline gate boundary" + [.claude/memory/contracts/dev-pipeline-phase-bubbles-contract.md](/.claude/memory/contracts/dev-pipeline-phase-bubbles-contract.md) + [tests/unit/services/extract-prose-parity.test.ts](/tests/unit/services/extract-prose-parity.test.ts).

## Related

- [dev-pipeline.md](dev-pipeline.md) — the gated workflow whose autonomous multi-report turns the fold above cleans up.
- [lazy-content-load.md](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](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](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](../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](../plans/2026-04-26-promote-real-messages-backtest.md) — the 9,382-message corpus, rule comparisons, and threshold rationale.
