feat(app): show assistant reasoning in the chat UI #333

Merged
rwachters merged 5 commits from feat/reasoning-display into master 2026-09-30 23:10:16 +00:00
rwachters commented 2026-09-30 23:09:21 +00:00 (Migrated from github.com)

Summary

Adds live and post-hoc display of a model's reasoning/thinking text in the chat UI, end to end: the server now streams plaintext reasoning deltas and persists what it streamed, and the clients render it in a collapsible section above the answer.

Changes

  • Added the assistant_message_reasoning_delta stream event and the REASONING_OUTPUT_LIMIT_EXCEEDED error code to the shared contracts.
  • Server: streamed reasoning deltas flow from the Responses strategy through the stream collector and step runner onto the wire, and the accumulated text is persisted as a derived Responses-shaped reasoning item on both normal and abnormal endings, with no new column and no migration.
  • Server: reasoning text is bounded by a fatal cap (MAX_REASONING_TEXT_CHARS = 100_000); exceeding it flags the message and suppresses the step's tool calls, because a response cut mid-generation is untrustworthy input for side-effecting tools.
  • Server: a public message edit now clears the stored reasoning of the edited message, since the new content no longer matches the reasoning that produced the old one; turn finalization keeps the non-clearing default so reasoning written moments earlier in the same step survives.
  • App: live deltas are appended into the cached placeholder in the same item shape the server persists, so the section renders identically during streaming and after the completing event replaces the message.
  • App: a new collapsible reasoning section above the message content shows the model's summary, the reasoning plaintext, an encrypted notice when only ciphertext is available, and a truncation line; it is collapsed by default and expandable per message.
  • App: added English and Spanish strings for the section label, thinking indicator, summary label, encrypted notice and truncation line.

Implementation notes

  • Only derived plaintext (summary[].text, content[].text) ever reaches the display model; raw reasoning item JSON and encrypted_content are never rendered or logged, and the KDoc on the affected contracts was updated to say so.
  • The display derivation caps each block at 32,000 characters as a presentation-only bound, announcing the cut instead of silently hiding characters; the cached and persisted reasoning keeps every character.
  • Reasoning is deliberately excluded from in-session search and from manual copy/clipboard actions, which continue to operate on message content only, while the session copy includes it; no code change was required for that behaviour.
  • The persisted streamed-text item is kept out of reasoning-mode capability detection, because on its own it would look like evidence that the model delivers plaintext reasoning.
  • Reasoning is not replayed into future requests: only the provider's own completed items are, sanitized to the Responses input shape as before.

Validation

  • ./gradlew :common:desktopTest
  • ./gradlew :app:desktopTest
  • ./gradlew :server:test
  • 84 new/updated reasoning tests run with 0 failures; the reviewer's independent run reproduced the same result, and the product owner completed a manual end-to-end pass.

Risks and follow-up

  • The new stream frame and the new error-code value are not decodable by older clients: server and clients must ship in the same release.
  • Two low-priority items are deferred: the reasoning header touch target is product-accepted as-is, and two edge-case tests are still missing.
## Summary Adds live and post-hoc display of a model's reasoning/thinking text in the chat UI, end to end: the server now streams plaintext reasoning deltas and persists what it streamed, and the clients render it in a collapsible section above the answer. ## Changes - Added the `assistant_message_reasoning_delta` stream event and the `REASONING_OUTPUT_LIMIT_EXCEEDED` error code to the shared contracts. - Server: streamed reasoning deltas flow from the Responses strategy through the stream collector and step runner onto the wire, and the accumulated text is persisted as a derived Responses-shaped reasoning item on both normal and abnormal endings, with no new column and no migration. - Server: reasoning text is bounded by a fatal cap (`MAX_REASONING_TEXT_CHARS = 100_000`); exceeding it flags the message and suppresses the step's tool calls, because a response cut mid-generation is untrustworthy input for side-effecting tools. - Server: a public message edit now clears the stored reasoning of the edited message, since the new content no longer matches the reasoning that produced the old one; turn finalization keeps the non-clearing default so reasoning written moments earlier in the same step survives. - App: live deltas are appended into the cached placeholder in the same item shape the server persists, so the section renders identically during streaming and after the completing event replaces the message. - App: a new collapsible reasoning section above the message content shows the model's summary, the reasoning plaintext, an encrypted notice when only ciphertext is available, and a truncation line; it is collapsed by default and expandable per message. - App: added English and Spanish strings for the section label, thinking indicator, summary label, encrypted notice and truncation line. ## Implementation notes - Only derived plaintext (`summary[].text`, `content[].text`) ever reaches the display model; raw reasoning item JSON and `encrypted_content` are never rendered or logged, and the KDoc on the affected contracts was updated to say so. - The display derivation caps each block at 32,000 characters as a presentation-only bound, announcing the cut instead of silently hiding characters; the cached and persisted reasoning keeps every character. - Reasoning is deliberately excluded from in-session search and from manual copy/clipboard actions, which continue to operate on message content only, while the session copy includes it; no code change was required for that behaviour. - The persisted streamed-text item is kept out of reasoning-mode capability detection, because on its own it would look like evidence that the model delivers plaintext reasoning. - Reasoning is not replayed into future requests: only the provider's own completed items are, sanitized to the Responses `input` shape as before. ## Validation - `./gradlew :common:desktopTest` - `./gradlew :app:desktopTest` - `./gradlew :server:test` - 84 new/updated reasoning tests run with 0 failures; the reviewer's independent run reproduced the same result, and the product owner completed a manual end-to-end pass. ## Risks and follow-up - The new stream frame and the new error-code value are not decodable by older clients: server and clients must ship in the same release. - Two low-priority items are deferred: the reasoning header touch target is product-accepted as-is, and two edge-case tests are still missing.
Sign in to join this conversation.
No reviewers
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set

Reference
Torvian.eu/chatbot!333
No description provided.