Skip to content

fix(desktop): honor display.interim_assistant_messages — stop collapsing mid-turn narration#61447

Open
lucasfdale wants to merge 2 commits into
NousResearch:mainfrom
lucasfdale:fix/desktop-honor-interim-assistant-messages
Open

fix(desktop): honor display.interim_assistant_messages — stop collapsing mid-turn narration#61447
lucasfdale wants to merge 2 commits into
NousResearch:mainfrom
lucasfdale:fix/desktop-honor-interim-assistant-messages

Conversation

@lucasfdale

@lucasfdale lucasfdale commented Jul 9, 2026

Copy link
Copy Markdown

What does this PR do?

On the Desktop app, a multi-step turn (text → tool → text → tool → …)
streams all its mid-turn narration live, then collapses to only the final
message
the instant message.complete lands. Any substantive point the model
makes between tool calls that the final message doesn't restate disappears from
the transcript. The text is persisted to state.db and reappears on session
reload — the loss is render-only — but Desktop is the only surface that does
this.

Root cause: completeAssistantMessage in
apps/desktop/src/app/session/hooks/use-message-stream/index.ts ran a
replaceTextPart that unconditionally dropped every text part on
completion and re-appended the gateway's final_response (message.complete.text)
as one trailing part. final_response is only the agent's last assistant
segment (agent/conversation_loop.py: final_response = assistant_message.content
in the no-tool-calls branch), so every earlier narration segment was discarded.
git blame shows this filter is original to the desktop app's first commit —
incidental, never a deliberate design decision.

This is a real divergence: the setting that governs interim commentary already
exists product-wide — display.interim_assistant_messages (default true,
documented as "signal, not noise") — and every other surface already keeps
interim text
: the messaging gateway gates sending on it; the Ink TUI keeps
interleaved segments and appends only the final tail
(ui-tui/.../turnController.ts::recordMessageComplete); and Desktop's own
reload path
(toChatMessages) reconstructs the interleaving. Desktop's
live-completion path was the sole outlier and consulted the setting nowhere.

The fix wires Desktop to honor the existing display.interim_assistant_messages
setting (default true, so the improved behavior is on by default; an explicit
false preserves the old lean collapse) and corrects the merge so keep-mode
matches what the TUI and reload path already produce — no new config key, no new
IPC (the renderer already fetches config.display).

Why this content is worth keeping (not filler)

Because final_response is only the agent's last segment, the dropped text is
specifically everything the agent worked out along the way — root-cause
findings, decisions, caveats, partial results — that the closing line doesn't
restate. The screenshot below is a debugging turn: the agent diagnoses that a
bare catch {} is silently swallowing Stripe errors, then finishes with a terse
"Fixed." BEFORE, the user is told it's fixed but never sees why it broke —
the diagnosis is gone. AFTER, the root-cause finding is preserved. That's the
case for interim being signal: it frequently carries the single most valuable
sentence in the turn.

Screenshot

Before/after: BEFORE collapses to a terse "Fixed" line and loses the root-cause diagnosis; AFTER preserves the mid-turn root-cause finding

Both panels are rendered by the real shipping Thread component, and the two
message states are produced by running the actual mergeFinalAssistantText
function this PR adds over one shared streamed input (built with the real
appendAssistantTextPart / upsertToolPart builders): left = keepInterim:false
(today's collapse), right = keepInterim:true (this PR's default). It is a
faithful render of the function's output, not a hand-drawn mockup — though not a
live end-to-end gateway capture.

Related Issue

Fixes #54905. Also fixes #61297 (same Desktop root cause — the streamed analysis
collapses to only the brief final conclusion). Prior report #40903 was closed
completed without a fix — same bug.

Type of Change

  • 🐛 Bug fix (non-breaking change that fixes an issue)

Changes Made

  • apps/desktop/src/lib/chat-messages.ts — extract the completion-merge into a
    pure, exported mergeFinalAssistantText(parts, finalText, keepInterim).
    keepInterim: true keeps interim narration and upgrades the trailing streamed
    text segment in place (the one final_response restates), or appends when the
    final text is genuinely new; keepInterim: false is byte-identical to the old
    collapse. Reasoning-dedup behavior is unchanged.
  • apps/desktop/src/app/session/hooks/use-message-stream/index.ts — replace the
    inline replaceTextPart with a call to mergeFinalAssistantText, reading the
    new setting atom.
  • apps/desktop/src/store/session.ts — add $keepInterimAssistantMessages atom
    (default true) + setter, mirroring the backend config.
  • apps/desktop/src/app/session/hooks/use-hermes-config.ts — set the atom from
    config.display.interim_assistant_messages on config refresh (default true;
    only an explicit false opts into lean).
  • apps/desktop/src/types/hermes.ts — add interim_assistant_messages?: boolean
    to the HermesConfig.display type.
  • apps/desktop/src/lib/chat-messages.test.ts — 7 unit tests for
    mergeFinalAssistantText (keep + lean modes).
  • apps/desktop/src/app/session/hooks/use-message-stream/interim-narration.test.tsx
    — integration test driving the real hook through
    message.start → delta → tool.start → tool.complete → delta → message.complete,
    asserting narration survives when on and collapses when off.
  • cli-config.yaml.example + hermes_cli/config.py — update the
    interim_assistant_messages comments (were "Gateway-only") to document that it
    now also governs Desktop transcript behavior. Comment-only; no default change.

How to Test

  1. In the Desktop app, ask for something that produces a multi-step turn where
    the model narrates between tool calls (e.g. "check the repo layout, then
    summarize", where it says something substantive before the final answer).
  2. Before: on completion the bubble collapses to only the final message; the
    mid-turn narration vanishes until you reload the session.
    After (default): the mid-turn narration stays in the transcript alongside
    the tool calls and the final message.
  3. Set display.interim_assistant_messages: false in ~/.hermes/config.yaml and
    repeat — the old lean collapse (final message only) is preserved.
  4. Automated: from the repo root,
    npm run --prefix apps/desktop typecheck (clean),
    npm run --prefix apps/desktop build (clean),
    and cd apps/desktop && ../../node_modules/.bin/vitest run --environment jsdom src/lib/chat-messages.test.ts src/app/session/hooks/use-message-stream/interim-narration.test.tsx (37 passing).

Checklist

Code

  • I've read the Contributing Guide
  • My commit messages follow Conventional Commits (fix(desktop): …)
  • I searched for existing PRs to make sure this isn't a duplicate
  • My PR contains only changes related to this fix
  • I've run the relevant test suites (tests/hermes_cli/test_config.py 145,
    tests/gateway/test_display_config.py 52, desktop vitest 37) — all pass.
    Note: desktop CI is typecheck + production build (both pass here); vitest
    is included as fix evidence. The Python change is comment-only.
  • I've added tests for my changes (7 unit + 2 integration)
  • I've tested on my platform: macOS 26.5.2 (Apple Silicon)

Documentation & Housekeeping

  • I've updated relevant documentation — cli-config.yaml.example +
    hermes_cli/config.py comments for interim_assistant_messages
  • I've updated cli-config.yaml.example (comment clarified; the key already
    existed, no new key added)
  • I've updated CONTRIBUTING.md/AGENTS.md — N/A (no architecture change)
  • I've considered cross-platform impact — renderer logic only, no
    platform-specific code paths
  • I've updated tool descriptions/schemas — N/A (no tool change)

Open question for maintainers

Desktop ignored display.interim_assistant_messages while the TUI, messaging
gateway, and reload path all respect/implement interim text. This PR assumes that
was an oversight to bring in line, not a deliberate Desktop-only lean default. If
it was deliberate, what's the rationale — so the default can be revisited rather
than flipped. Happy to add a Settings → Display toggle that writes the key if a
GUI control is wanted; kept out of this PR to keep the diff minimal since the
config key already drives it.

…ing mid-turn narration

Desktop collapsed a multi-step turn to only the final message on
message.complete, dropping every interim assistant text segment streamed
between tool calls. The text is persisted and reappears on reload, and every
other surface (messaging gateway, Ink TUI, Desktop's own reload path) keeps it
— Desktop's live-completion path was the sole outlier and never consulted the
existing display.interim_assistant_messages setting.

Wire the setting through to completion (default true) and extract the merge
into a pure, tested mergeFinalAssistantText() that keeps interim narration and
upgrades the trailing segment in place, matching the TUI/reload contract.

Also clarify the interim_assistant_messages comments in config.py and
cli-config.yaml.example (comment-only; no default change).

Fixes NousResearch#54905.
@alt-glitch alt-glitch added type/bug Something isn't working comp/desktop Electron desktop app (apps/desktop/*) P3 Low — cosmetic, nice to have labels Jul 9, 2026
@kyssta-exe

Copy link
Copy Markdown
Contributor

This PR also fixes #61297 (same root cause — interim narration collapsing). Mentioning here for cross-reference.

@lucasfdale

Copy link
Copy Markdown
Author

Thanks for the cross-reference — confirmed, #61297 is the same root cause (Desktop's live-completion path dropping every interim segment and keeping only final_response, the agent's last segment). I've added Fixes #61297 to the description so it closes on merge.

Worth noting the report also ticks CLI (interactive chat) as a component, but the Ink TUI's recordMessageComplete already preserves interim segments — the defect is Desktop-only, which this PR covers in full.

@tonydwb tonydwb left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review Summary

Verdict: LGTM

What the PR Does

Honor display.interim_assistant_messages configuration, stopping mid-turn narration from being collapsed.

Assessment

  • Small config honoring fix.

Reviewed by Hermes Agent

@giggling-ginger

Copy link
Copy Markdown
Contributor

Review note / issue linkage

This also fixes #61822 (same Desktop root cause).

Why #61822 maps here

  • Report: intermediate thinking / tool narration flashes briefly, then is replaced by the final answer only.
  • Reporter has interim assistant messages enabled.
  • Debug dump shows Desktop + platform=tui / tui_gateway, not classic CLI-only.
  • That matches this PR's collapse-on-message.complete analysis exactly (replaceTextPart drops every text part and keeps only final_response).

Please add Fixes #61822 to the PR body (alongside #54905 / #61297) so the issue auto-closes on merge.

Sanity check done against current main

  • Cherry-picked this branch cleanly onto latest main; desktop vitest for mergeFinalAssistantText + interim narration integration: 37 passed.
  • Only real merge friction was an unrelated scripts/release.py AUTHOR_MAP collision if someone salvages later — not a problem for merging this PR as-is if it's rebased.

Scope caveat (not a blocker)

I closed the accidental salvage duplicate #61997 in favor of this original PR.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

comp/desktop Electron desktop app (apps/desktop/*) P3 Low — cosmetic, nice to have type/bug Something isn't working

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Bug]: desktop可视化的结果不全 Desktop: all intermediate assistant text lost during multi-tool turns — only final message survives

5 participants