Skip to content

fix(frontend): interleave thinking messages correctly in snapshots#891

Merged
Gkrumbach07 merged 2 commits intomainfrom
fix/thinking-messages-stacking-52655
Mar 13, 2026
Merged

fix(frontend): interleave thinking messages correctly in snapshots#891
Gkrumbach07 merged 2 commits intomainfrom
fix/thinking-messages-stacking-52655

Conversation

@Gkrumbach07
Copy link
Contributor

@Gkrumbach07 Gkrumbach07 commented Mar 12, 2026

Summary

  • Implement stable sort in handleMessagesSnapshot that preserves original order for equal timestamps
  • Fixes thinking blocks stacking instead of interleaving with agent messages

Test plan

  • Frontend unit tests pass (471 passed)
  • ESLint passes
  • Build succeeds
  • Manual test: session with thinking, verify interleaved display on reconnect

Fixes: RHOAIENG-52655

🤖 Generated with Claude Code

Jira: RHOAIENG-52655

@coderabbitai
Copy link

coderabbitai bot commented Mar 12, 2026

Walkthrough

The message-sorting logic in handleMessagesSnapshot was changed to record original indices and use them as a tiebreaker when timestamps are equal or missing, preserving original interleaving while still ordering by timestamp when available.

Changes

Cohort / File(s) Summary
Message Sorting Logic
components/frontend/src/hooks/agui/event-handlers.ts
Added an originalOrder map and modified the comparator in handleMessagesSnapshot to first compare timestamps (when present) and fall back to original insertion order for equal or absent timestamps, ensuring stable, interleaved ordering.

Estimated code review effort

🎯 2 (Simple) | ⏱️ ~8 minutes

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Title check ✅ Passed The title accurately describes the main change: fixing the interleaving of thinking messages in snapshots through a stable sort implementation.
Description check ✅ Passed The description is clearly related to the changeset, explaining the stable sort fix, the problem it solves, and providing a test plan with issue reference.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
  • 📝 Generate docstrings (stacked PR)
  • 📝 Generate docstrings (commit on current branch)
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch fix/thinking-messages-stacking-52655
📝 Coding Plan
  • Generate coding plan for human review comments

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

MESSAGES_SNAPSHOT sort was unstable for messages with identical
timestamps, causing thinking blocks to group together instead of
being interleaved with agent messages. Now preserves original
snapshot order as tiebreaker.

Fixes: RHOAIENG-52655

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@ambient-code ambient-code bot force-pushed the fix/thinking-messages-stacking-52655 branch from db5f7b7 to 4a5e0b2 Compare March 12, 2026 16:41
@ambient-code
Copy link
Contributor

ambient-code bot commented Mar 12, 2026

🤖 PR Fix Report

Summary

PR #891 has been successfully rebased and validated. All checks passing.

Actions Taken

Rebased onto latest main

  • Picked up commit 538ccbd (ci(mergify): require 2 approvals for large PRs)
  • PR commit rebased: db5f7b74a5e0b2

Reviewed feedback

  • 1 comment from CodeRabbit (rate limit notice, no actionable feedback)
  • No inline review comments to address

Code quality review

  • Ran /simplify to review changes
  • Implementation verified correct (stable sort with tiebreaker for equal timestamps)
  • No issues found

Validation

  • ESLint: Passed (1 unrelated warning in MessagesTab.tsx)
  • Frontend unit tests: 471 passed, 12 skipped
  • All CI checks: 33 passing, 1 pending (fix-single - this workflow)

CI Status

  • ✅ 33 checks passing
  • ⏳ 1 check pending (fix-single)
  • ❌ 0 checks failing

Commits Pushed

4a5e0b2 fix(frontend): stable sort for thinking messages in snapshot

🤖 Automated fix by PR Fixer Bot

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@components/frontend/src/hooks/agui/event-handlers.ts`:
- Around line 853-864: The tie-breaker currently builds originalOrder from
filtered (variable filtered) so equal-timestamp messages keep the pre-reconnect
local ordering; change the comparator to use the snapshot/normalized order
instead: derive originalOrder (or a separate snapshotOrder map) from
normalizedMessages and use that map in the sort comparator (inside
filtered.sort) as the fallback when timestamps are equal or missing so messages
are reordered to match the snapshot's normalizedMessages order rather than the
existing filtered order.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: bf4ae894-ece2-4e0c-9564-181ef328ddf0

📥 Commits

Reviewing files that changed from the base of the PR and between 538ccbd and 4a5e0b2.

📒 Files selected for processing (1)
  • components/frontend/src/hooks/agui/event-handlers.ts

Comment on lines +853 to +864
const originalOrder = new Map(filtered.map((msg, idx) => [msg.id, idx]))
filtered.sort((a, b) => {
const ta = a.timestamp ? new Date(a.timestamp).getTime() : null
const tb = b.timestamp ? new Date(b.timestamp).getTime() : null
if (ta == null && tb == null) return 0
if (ta == null) return 0 // keep relative position
if (tb == null) return 0
return ta - tb

if (ta != null && tb != null) {
const diff = ta - tb
if (diff !== 0) return diff
}

// Equal or missing timestamps: preserve original snapshot order
return (originalOrder.get(a.id) ?? 0) - (originalOrder.get(b.id) ?? 0)
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Use snapshot order, not merged state order, as the tie-breaker.

originalOrder is derived from filtered, which already inherits state.messages ordering for existing IDs. If the local order is wrong before reconnect, equal-timestamp messages stay wrong after the snapshot too, because this sort never reorders them to match normalizedMessages. That means the reconnect path can still leave thinking blocks stacked instead of interleaved.

Proposed fix
-  const originalOrder = new Map(filtered.map((msg, idx) => [msg.id, idx]))
+  const snapshotOrder = new Map(normalizedMessages.map((msg, idx) => [msg.id, idx]))
+  const mergedOrder = new Map(filtered.map((msg, idx) => [msg.id, idx]))
   filtered.sort((a, b) => {
     const ta = a.timestamp ? new Date(a.timestamp).getTime() : null
     const tb = b.timestamp ? new Date(b.timestamp).getTime() : null

     if (ta != null && tb != null) {
       const diff = ta - tb
       if (diff !== 0) return diff
     }

-    // Equal or missing timestamps: preserve original snapshot order
-    return (originalOrder.get(a.id) ?? 0) - (originalOrder.get(b.id) ?? 0)
+    // Equal or missing timestamps: prefer authoritative snapshot order.
+    // Fall back to current merged order for messages not present in the snapshot.
+    return (snapshotOrder.get(a.id) ?? mergedOrder.get(a.id) ?? 0)
+      - (snapshotOrder.get(b.id) ?? mergedOrder.get(b.id) ?? 0)
   })
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const originalOrder = new Map(filtered.map((msg, idx) => [msg.id, idx]))
filtered.sort((a, b) => {
const ta = a.timestamp ? new Date(a.timestamp).getTime() : null
const tb = b.timestamp ? new Date(b.timestamp).getTime() : null
if (ta == null && tb == null) return 0
if (ta == null) return 0 // keep relative position
if (tb == null) return 0
return ta - tb
if (ta != null && tb != null) {
const diff = ta - tb
if (diff !== 0) return diff
}
// Equal or missing timestamps: preserve original snapshot order
return (originalOrder.get(a.id) ?? 0) - (originalOrder.get(b.id) ?? 0)
const snapshotOrder = new Map(normalizedMessages.map((msg, idx) => [msg.id, idx]))
const mergedOrder = new Map(filtered.map((msg, idx) => [msg.id, idx]))
filtered.sort((a, b) => {
const ta = a.timestamp ? new Date(a.timestamp).getTime() : null
const tb = b.timestamp ? new Date(b.timestamp).getTime() : null
if (ta != null && tb != null) {
const diff = ta - tb
if (diff !== 0) return diff
}
// Equal or missing timestamps: prefer authoritative snapshot order.
// Fall back to current merged order for messages not present in the snapshot.
return (snapshotOrder.get(a.id) ?? mergedOrder.get(a.id) ?? 0)
- (snapshotOrder.get(b.id) ?? mergedOrder.get(b.id) ?? 0)
})
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@components/frontend/src/hooks/agui/event-handlers.ts` around lines 853 - 864,
The tie-breaker currently builds originalOrder from filtered (variable filtered)
so equal-timestamp messages keep the pre-reconnect local ordering; change the
comparator to use the snapshot/normalized order instead: derive originalOrder
(or a separate snapshotOrder map) from normalizedMessages and use that map in
the sort comparator (inside filtered.sort) as the fallback when timestamps are
equal or missing so messages are reordered to match the snapshot's
normalizedMessages order rather than the existing filtered order.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

♻️ Duplicate comments (1)
components/frontend/src/hooks/agui/event-handlers.ts (1)

853-864: ⚠️ Potential issue | 🟠 Major

Use snapshot order as the tie-breaker, not merged filtered order.

At Line 853, originalOrder is derived from filtered, so equal/missing-timestamp items keep pre-reconnect local ordering. If that ordering is already wrong, reconnect replay still won’t re-interleave thinking blocks correctly.

Proposed fix
-  const originalOrder = new Map(filtered.map((msg, idx) => [msg.id, idx]))
+  const snapshotOrder = new Map(normalizedMessages.map((msg, idx) => [msg.id, idx]))
+  const mergedOrder = new Map(filtered.map((msg, idx) => [msg.id, idx]))
   filtered.sort((a, b) => {
     const ta = a.timestamp ? new Date(a.timestamp).getTime() : null
     const tb = b.timestamp ? new Date(b.timestamp).getTime() : null

     if (ta != null && tb != null) {
       const diff = ta - tb
       if (diff !== 0) return diff
     }

-    // Equal or missing timestamps: preserve original snapshot order
-    return (originalOrder.get(a.id) ?? 0) - (originalOrder.get(b.id) ?? 0)
+    // Equal or missing timestamps: prefer authoritative snapshot order,
+    // then fall back to merged order for non-snapshot entries.
+    return (snapshotOrder.get(a.id) ?? mergedOrder.get(a.id) ?? 0)
+      - (snapshotOrder.get(b.id) ?? mergedOrder.get(b.id) ?? 0)
   })

As per coding guidelines, "Focus on major issues impacting performance, readability, maintainability and security. Avoid nitpicks and avoid verbosity."

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@components/frontend/src/hooks/agui/event-handlers.ts` around lines 853 - 864,
The tie-breaker uses originalOrder built from filtered, which preserves the
current (possibly incorrect) merged order; change originalOrder to map IDs from
the pre-reconnect snapshot array (the saved snapshot used to rebuild messages)
so the sort in filtered.sort(...) uses snapshot order as the stable tie-breaker;
update the creation of originalOrder (and any variable name if needed) to
reference the snapshot message IDs instead of filtered to ensure reconnect
replay re-interleaves blocks correctly.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Duplicate comments:
In `@components/frontend/src/hooks/agui/event-handlers.ts`:
- Around line 853-864: The tie-breaker uses originalOrder built from filtered,
which preserves the current (possibly incorrect) merged order; change
originalOrder to map IDs from the pre-reconnect snapshot array (the saved
snapshot used to rebuild messages) so the sort in filtered.sort(...) uses
snapshot order as the stable tie-breaker; update the creation of originalOrder
(and any variable name if needed) to reference the snapshot message IDs instead
of filtered to ensure reconnect replay re-interleaves blocks correctly.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: 914456c1-1579-4ced-82a1-b061acaa5ef8

📥 Commits

Reviewing files that changed from the base of the PR and between 4a5e0b2 and 1f3694b.

📒 Files selected for processing (1)
  • components/frontend/src/hooks/agui/event-handlers.ts

@Gkrumbach07 Gkrumbach07 merged commit 310e369 into main Mar 13, 2026
32 checks passed
@Gkrumbach07 Gkrumbach07 deleted the fix/thinking-messages-stacking-52655 branch March 13, 2026 04:17
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant