Skip to content

fix(native-chat): anchor a mid-turn send where it was issued - #20847

Open
topnguyen wants to merge 5 commits into
stablyai:mainfrom
topnguyen:fix/native-chat-anchor-mid-turn-echo
Open

topnguyen wants to merge 5 commits into
stablyai:mainfrom
topnguyen:fix/native-chat-anchor-mid-turn-echo

Conversation

@topnguyen

@topnguyen topnguyen commented Sep 15, 2026 •

Copy link
Copy Markdown

ELI5

If you type a second message while the agent is still working, Claude folds it into the turn already running and never writes it down as a message of its own. The chat view had nowhere to put that bubble, so it parked it at the bottom — underneath the answer it caused. This puts the bubble back where you typed it, without costing the turn anything else.

What Changed

An echo the transcript will never match has to be placed, not pinned.

  • native-chat-pending-anchor.ts — anchorPendingMessagesToSendBoundary puts an echo directly after the row it was sent against; nativeChatMessagesWithPending assembles transcript → markers/streaming → the echoes still belonging at the tail. NativeChatResolvedView calls it instead of spreading pendingMessages onto the end.
  • Position has to survive orderNativeChatMessages. It sorts by rank before timestamp, and messageSortRank pins every pending:* row to rank 2, so array order never reaches the DOM. An anchored echo is minted under a second id form, pending-at:, ranked as content, and takes its anchor's timestamp plus half a millisecond — transcript rows carry whole milliseconds, so it lands after the anchor and before the next row. Echoes sharing an anchor step 0.5/1.0/1.5 and keep send order.
  • An echo must not break the turn's tool attribution. A non-transcript row ends the foldToolMessages run, which strands a tool-result-only row and lets dropUnattributableToolResults delete it — the call renders in flight with no output. The pending id prefixes therefore live in native-chat-types, and the fold pass skips them when deciding to end a run.
  • isPendingMessageId re-exports the shared predicate and accepts both forms, so pruning, visibility and occurrence matching are unchanged.

Deliberately narrow:

  • A still-at-tail echo keeps pending: and rank 2, so the streaming preview still sorts ahead of it — the tier the comment on messageSortRank exists to hold. Only an echo the transcript has moved past is repositioned.
  • An anchor that is absent, or carries a null timestamp, falls back to the tail rather than guessing.
  • No change to matching, pruning, occurrence counting, or the glue path.

Why

Claude consumes a mid-turn send through a queued_command attachment and writes no type:"user" row for it, so that echo has no row to match, ever. Held at the tail it re-reads below every turn of the reply it triggered, which is what makes the conversation look re-ordered.

Mobile already fixes this and its test says so directly (mobile-native-chat-render-data.test.ts):

What made the conversation read as scrambled was not the unmatched echo itself but where it rendered: appended after every turn, so it re-read below each new one.

Mobile anchors on baselineTailMessageId. Desktop already records the same boundary — NativeChatPendingSend.afterMessageId, "Last authoritative transcript message visible when this send was issued" — and only ever used it for matching. This reuses it for placement rather than adding a parallel concept.

Scope vs the overlapping PRs

This is a renderer-layer change. It touches no decoder and no main-process transcript path, so it neither competes with nor duplicates the queued-prompt admission work:

Layer Core file
#14309, #16121, #19428, and lurunzi's fork commit main process transcript-line-decoders-claude.ts
this PR renderer only native-chat-pending-anchor.ts

Those admit the persisted queued_command record so a transcript row exists, after which the existing pending reconciliation removes the echo. This decides where an echo is placed when no row will ever exist.

That case is not hypothetical. As lurunzi notes, the admission filter deliberately excludes older records lacking origin.kind / humanTurn, and cannot help "prompts that never reached the process". Those echoes have no row to match, ever, and are exactly what this anchors.

The two layers compose: with admission landed, fewer echoes reach the fallback; without it, none of them are placed correctly. Happy to land this under #19428 if that is the preferred shape.

Linked Issue

Fixes #20846

Visual Proof

order shown
Terminal pane (already correct) msg 1 → tools → msg 2 → more tools → final answer
Chat view before msg 1 → tools → final answer → msg 2
Chat view after msg 1 → tools → msg 2 → more tools → final answer

Before

image image

After

image

Testing

Windows 11 (26200), against Claude Code v2.1.272.

  • pnpm test src/renderer/src/components/native-chat — 152 files, 1547 tests pass; src/shared/native-chat — 457 pass.
  • native-chat-pending-anchor.test.ts (10 tests). Three of them only mean anything because they fail without the change:
    • anchored order through orderNativeChatMessages — without the rank work, pending:p1 sorts to the tail;
    • a tool result whose call sits before the echo, through createNativeChatMessageListProjection — without the fold work, expected [] to deeply equal [ 'a.txt' ];
    • a still-at-tail echo staying behind the streaming preview.
  • pnpm tc clean; oxlint and the pre-commit oxlint --config config/oxlint-react-doctor.json + oxfmt clean.
  • 3 failures in src/shared (agent-hook-listener-relay-dependency, git-fetch-head-lock-key-derivation) are pre-existing on a Windows host — verified by stashing this change and re-running them. Mobile deps do not install on this host (react-native patch fails to parse), so mobile is left to CI; the fold change touches it.

Verified in a real build, not just unit tests: pnpm build:unpack, sent a second message mid-turn, and the bubble held its position while the rest of the reply landed below it — chat view and terminal pane finally in the same order. Two earlier attempts on this branch each passed their tests and still failed here, which is why that check is worth calling out separately.

  • I manually tested these changes locally
  • Automated tests added/updated, or explained why not below

AI Disclosure

Written with Claude Opus 5 (Claude Code). The transcript evidence in the linked issue is a real capture from my own machine.

Worth stating plainly for reviewers: the first three commits on this branch were wrong in ways the automated review caught — the anchored array never reached the DOM, then the fix dropped a tool result and flipped the streaming tier. Every one of those was found by @pullfrog and @coderabbitai, not by me. The tests that now guard them were each written to fail first.

Review

Agent skill upstream boundary

  • Not applicable, or this change follows docs/reference/agent-skill-sharing-upstream-boundary.md and copies or mechanically translates no upstream skill-installer source, tests, fixtures, registry entries, path tables, comments, or documentation.

Notes

  • Cross-platform: pure list logic, no platform branch, no paths, no shortcuts.
  • Backwards compatibility: afterMessageId is already written by every send path; entries missing it fall through to today's tail behaviour.
  • Performance: one Map and one Set over the rendered window, inside the existing useMemo; the early return is unchanged when nothing is pending.
  • Mobile: shares foldToolMessages. The change there is one added exclusion in a reset condition and cannot affect a transcript row; mobile keeps its own anchoring.

Checklist

  • This PR is small and focused
  • I explained what changed and why (including ELI5)
  • Before/after screenshots or videos attached for UI changes, or N/A with reason
  • Self-reviewed for correctness, security, and performance
  • Cross-platform, SSH/remote, and path/shortcut impact considered (or N/A)
  • pnpm lint, pnpm typecheck, pnpm test, and pnpm build pass (or CI will cover; local preferred)

Claude consumes a send made mid-turn through a `queued_command` attachment and
writes no `type:"user"` row for it, so its optimistic echo has no row to match —
ever. Held at the list tail, it re-reads below every turn of the reply it
triggered, which is what makes the conversation look re-ordered: the answer
appears above the question still sitting underneath it.

Observed in a Claude Code transcript, line numbers as written:

   6  {"type":"user", ...}                          "check status, first time"
  25  {"type":"assistant", ...}                     "I'll check the current state..."
  27  {"type":"queue-operation","operation":"enqueue","content":"check status, second time"}
  28  {"type":"queue-operation","operation":"remove", ...,"reason":"absorbed_mid_turn"}
  33  {"type":"attachment","attachment":{"type":"queued_command","prompt":"check status, second time"}}
  41  {"type":"assistant", ...}                     "Working tree is clean..."
  68  {"type":"assistant", ...}                     "The discriminating test has fired..."

The echo belongs at 33; it rendered after 68.

Mobile already anchors for this reason (`baselineTailMessageId`). Desktop
records the same boundary as `NativeChatPendingSend.afterMessageId` and only
ever used it for matching, so this reuses it for placement.

Only an echo the transcript has since moved past is repositioned: one still at
the tail keeps today's order, after the streaming bubble. A boundary a bounded
read has paged out falls back to the tail rather than guessing.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@coderabbitai

coderabbitai Bot commented Sep 15, 2026 •

Copy link
Copy Markdown
Contributor

Review Change StackReview Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Advanced

Run ID: e685e36c-11ae-46d9-8b02-4b2505e33ad7

📥 Commits

Reviewing files that changed from the base of the PR and between cf56081 and 65e6e2b.

📒 Files selected for processing (3)
  • src/renderer/src/components/native-chat/native-chat-pending-anchor.test.ts
  • src/renderer/src/components/native-chat/native-chat-pending-anchor.ts
  • src/renderer/src/components/native-chat/native-chat-session-assembler.ts

Included review availability: Your plan provides up to 10 included reviews per hour; 9 remain after this review.


📝 Walkthrough

Walkthrough

Pending echoes are anchored after the transcript message identified by their send boundary. Echoes without a usable boundary remain trailing. nativeChatMessagesWithPending combines anchored messages, markers, streaming output, and trailing echoes. NativeChatResolvedView uses this helper. Shared pending-row classification preserves anchored order during sorting and tool-result folding. Tests cover boundary matching, fallback placement, ordering, timestamp ties, and passthrough behavior.

Priority: ➖ Normal

Severity of issue fixed: Medium

Merge Risk: ⚪ Minimal · up to 65e6e

Pending echoes retain their intended placement in the reachable ordering flow, including timestamp ties. No actionable merge-blocking risk remains.

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 61.54% which is insufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 13 functions across 7 files. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Linked Issues check ✅ Passed Issue #20846 requires mid-turn echoes without transcript rows to render after NativeChatPendingSend.afterMessageId. anchorPendingMessagesToSendBoundary places qualifying echoes after the matching …
Out of Scope Changes check ✅ Passed The changes stay within Issue #20846. They update native chat assembly, pending-row identification and sorting, tool-result folding needed to preserve the affected turn, view integration, and focused …
Title check ✅ Passed The title clearly and concisely describes the primary change: anchoring a mid-turn native-chat send at its issue position.
Description check ✅ Passed The description follows the repository template and explains the user impact, implementation, rationale, linked issue, visual proof, testing, AI disclosure, scope, compatibility, and checklist status.…
  • Fix all pre-merge checks with AI

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.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 1


ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Advanced

Run ID: 400d0fea-1b28-487f-b7d8-2404dc6dbf0d

📥 Commits

Reviewing files that changed from the base of the PR and between caa465d and 62d0c94.

📒 Files selected for processing (3)
  • src/renderer/src/components/native-chat/NativeChatResolvedView.tsx
  • src/renderer/src/components/native-chat/native-chat-pending-anchor.test.ts
  • src/renderer/src/components/native-chat/native-chat-pending-anchor.ts

Included review availability: Your plan provides up to 10 included reviews per hour; 9 remain after this review.

…ming

Review note on stablyai#20847: the tests exercised `anchorPendingMessagesToSendBoundary`
directly, so nothing pinned what `NativeChatResolvedView` actually renders —
the anchored echo has to land before the command markers and the streaming
bubble, and the trailing one after both.

Asserts the whole sequence for one anchored and one trailing send, so a
regression in the assembly order fails rather than just the anchoring.

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

@pullfrog pullfrog Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Caution

The anchoring this PR adds is discarded before render, so #20846 is not fixed. The list re-sorts every pending:* row back to the tail after nativeChatMessagesWithPending has placed it — see the inline comment for the trace.

Reviewed changes — reviewed the full diff (3 files, 2 commits): a new native-chat-pending-anchor.ts, its unit tests, and the NativeChatResolvedView wiring; plus the follow-up assembly-order test at 053ce516e3.

  • Anchor pure function — anchorPendingMessagesToSendBoundary interleaves an echo after its afterMessageId boundary and trails echoes that are missing, null, or still at the tail.
  • Assembly helper — nativeChatMessagesWithPending orders transcript → anchored echoes → markers → streaming → trailing echoes.
  • View wiring — NativeChatResolvedView now assembles the pending list through the helper instead of appending pendingMessages.
  • Tests — cover the pure function and the assembly output, but never the order the list actually renders.

⚠️ The tests stop one layer short of the render path

Both the anchor tests and the follow-up nativeChatMessagesWithPending test assert the array produced before NativeChatMessageList projects it, and that projection is exactly what reorders echoes today. Add a test that runs the assembled messages through createNativeChatMessageListProjection (or orderNativeChatMessages) and asserts the echo lands between its boundary and the next turn; it should fail against the current comparator.

Technical details
# Anchored order is discarded by the list projection

## Affected sites
- src/renderer/src/components/native-chat/NativeChatResolvedView.tsx:287 — anchored array assigned to `sessionWithPending.messages`
- src/renderer/src/components/native-chat/NativeChatMessageList.tsx:142-145 — `projectMessages(session.messages)`
- src/renderer/src/components/native-chat/native-chat-message-list-projection.ts:29 — `orderNativeChatMessages(messages)`
- src/renderer/src/components/native-chat/native-chat-message-grouping.ts:51-53 — `[...messages].sort(compareMessages)`
- src/renderer/src/components/native-chat/native-chat-session-assembler.ts:97-105 — `messageSortRank` returns 2 for any `pending:*` / `launch-pending:*` id
- src/renderer/src/components/native-chat/native-chat-pending.ts:254 — echoes are minted as `pending:${entry.id}`

## Required outcome
- The position chosen by `anchorPendingMessagesToSendBoundary` must survive to the rendered row order.
- Add a test that feeds the assembled array through the projection and asserts the anchored position (the existing `native-chat-message-grouping.test.ts:36-43` pins the opposite, so it will need to change with the fix).

## Suggested approach (optional)
- Make the comparator honor the anchor (e.g. a sort-rank/timestamp override carried on anchored echoes), apply anchoring after the projection rather than before, or exempt already-ordered rows from the defensive re-sort.

## Open questions for the human (optional)
- Is the rank-2 tail pin load-bearing for the still-at-tail echo (streaming renders before pending)? Any fix must keep that case.

Pullfrog  | Fix all ➔ | Fix 👍s ➔ | View workflow run | Using DeepSeek Flash (free via Pullfrog for OSS) | 𝕏

Comment thread src/renderer/src/components/native-chat/NativeChatResolvedView.tsx
…vives sorting

@pullfrog was right on stablyai#20847: the anchored array never reached the DOM.
`NativeChatMessageList` re-sorts through `orderNativeChatMessages`, and
`messageSortRank` pins every `pending:*` row to rank 2 — the tail — so array
order alone proved nothing, and the earlier tests asserted the wrong layer.

A test driving `orderNativeChatMessages` over the anchored array reproduces it:

    - "pending:p1"
        "a2",
        "a3",
    + "pending:p1"

Fix, keeping the tier the rank exists to protect:

- `pending-at:` is a second pending id form. `isPendingMessageId` accepts both,
  so pruning and visibility are unchanged; `isAnchoredPendingMessageId` marks
  only the positioned ones.
- `messageSortRank` returns 0 for an anchored echo — it owns a place between
  real rows — while a still-at-tail echo keeps rank 2, so the streaming bubble
  still sorts ahead of it, which is what the existing comment guards.
- An anchored echo takes its anchor's timestamp plus half a millisecond, so it
  sorts after the anchor and before the next row: transcript rows carry whole
  milliseconds. Echoes sharing an anchor step 0.5/1.0/1.5 and keep send order.
- An anchor with a null timestamp (transcripts that supply none) falls back to
  the tail rather than guessing.

Side effect worth a maintainer's eye: the projection folds after ordering, so
tool rows produced after the send no longer fold into the assistant message
above the echo — they render as their own rows beneath it. That matches the
terminal pane, where those calls also come after the prompt.

Verified in a local `build:unpack` on Windows: sent a second message mid-turn
and the bubble held its place while the rest of the reply landed below it, with
the chat view and the terminal pane finally in the same order.

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

@pullfrog pullfrog Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Important

The anchored echo now survives the list re-sort, but it interrupts turn folding: tool results emitted after a mid-turn send are dropped from the rendered list instead of shown.

Reviewed changes — reviewed the delta since the prior pullfrog review (053ce516e3 → 69a11fc525): the pending-at: id form and rank-0 override that let the anchored position survive orderNativeChatMessages, plus the projection-level test.

  • Anchored echoes re-minted under pending-at: — anchorPendingMessagesToSendBoundary gives a positioned echo its anchor's timestamp plus a 0.5n nudge, and messageSortRank now ranks pending-at: rows as content while pending: rows keep the rank-2 tail tier.
  • Tail-boundary trailing removed — a boundary that is still the tail (with a timestamp) is now anchored rather than trailed, so a folded turn that grows in place keeps the echo above its later output.
  • Projection-level test added — native-chat-pending-anchor.test.ts drives the anchored array through orderNativeChatMessages, which fails against the old rank-2 comparator.

The comment below is the one thing I would not merge without addressing.

Pullfrog  | Fix all ➔ | Fix 👍s ➔ | View workflow run | Using DeepSeek Flash (free via Pullfrog for OSS) | 𝕏

…tput

Two regressions @pullfrog found in the anchoring itself, both real, both mine.

**A tool result went missing.** An echo placed between an assistant message and
the tool rows that followed ends the `foldToolMessages` run, so a
tool-result-only row is no longer attributed and `dropUnattributableToolResults`
deletes it — the call renders in flight, with no output. Worse than the bug this
PR set out to fix, and only in the scenario it targets.

An optimistic echo is a view artefact sitting where the user typed, not a
transcript row, so it no longer ends the run. The id prefixes move to
`native-chat-types` because the shared fold pass has to recognise them;
`isPendingMessageId` now just re-exports the shared predicate.

**The streaming tier flipped.** Dropping the tail-boundary case in the previous
commit gave a still-at-tail echo rank 0, so it sorted ahead of the streaming
preview — the tier the comment on `messageSortRank` and this PR both call
load-bearing. The guard is back: an echo still at the tail keeps `pending:` and
rank 2, and only an echo the transcript has moved past is repositioned.

Worth recording why that guard was dropped at all: I read its failure as folded
turns growing in place. Wrong — the first attempt failed because of the rank pin
alone, which is now fixed, so the guard does what it was written to do.

Both are pinned by tests through the path the list actually renders. The
tool-result one fails without the fold change — `expected [] to deeply equal
[ 'a.txt' ]` — which my first version of it did not, because I modelled the
call as a text assistant message that re-opened the fold run on its own.

1547 tests in native-chat, 457 in shared/native-chat, typecheck and oxlint
clean. Mobile deps do not install on this host (react-native patch fails to
parse), so mobile is left to CI; the fold change touches it.

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

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 1


ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Advanced

Run ID: 9ebc384d-307b-485f-a271-2cbac4591fd6

📥 Commits

Reviewing files that changed from the base of the PR and between 62d0c94 and cf56081.

📒 Files selected for processing (6)
  • src/renderer/src/components/native-chat/native-chat-pending-anchor.test.ts
  • src/renderer/src/components/native-chat/native-chat-pending-anchor.ts
  • src/renderer/src/components/native-chat/native-chat-pending.ts
  • src/renderer/src/components/native-chat/native-chat-session-assembler.ts
  • src/shared/native-chat-tool-fold.ts
  • src/shared/native-chat-types.ts

Included review availability: Your plan provides up to 10 included reviews per hour; 8 remain after this review.

Comment thread src/renderer/src/components/native-chat/native-chat-pending-anchor.ts Outdated

@pullfrog pullfrog Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

✅ No new issues found. The prior review's two findings are addressed and their threads resolved.

Reviewed changes — reviewed the delta since the prior pullfrog review (69a11fc525 → cf56081), which closes out both findings from that review.

  • Fold tolerates a pending echo — foldToolMessages no longer ends its run on a pending:/pending-at: row, so tool results that follow the echo stay attributable instead of being deleted by dropUnattributableToolResults; pinned through createNativeChatMessageListProjection.
  • Still-at-tail echo keeps its tier — the tail-boundary guard is restored, so an echo whose boundary is still the transcript tail stays pending: / rank 2 and the streaming preview sorts ahead of it; pinned by an orderNativeChatMessages test.
  • Id prefixes shared with the fold pass — NATIVE_CHAT_PENDING_ID_PREFIX, NATIVE_CHAT_ANCHORED_PENDING_ID_PREFIX, and isNativeChatPendingRowId move to native-chat-types, with native-chat-pending re-exporting them so renderer call sites keep their names.

Verified locally: the full src/renderer/src/components/native-chat suite passes (153 files, 1548 tests); no other consumer of isPendingMessageId depends on the narrower pending:-only meaning.

ℹ️ The PR description now states the opposite tool-row behavior

The "side effect a maintainer should weigh" paragraph says tool rows produced after the send "no longer fold into the assistant message above the echo — they render as their own rows beneath it." cf56081 deliberately reverses that: the fold run stays open across the echo, so those rows fold into the assistant above it. The code is the intended fix, but the description no longer matches it.

Technical details
# Stale "side effect" paragraph in the PR body

## Affected sites
- PR #20847 body, "Side effect a maintainer should weigh" paragraph
- src/shared/native-chat-tool-fold.ts:123 — pending echo no longer ends the fold run

## Required outcome
- The description should describe the shipped behavior: tool rows emitted after the echo fold into the assistant above it (their results are preserved, which is the point of the fold exemption).

## Open questions for the human (optional)
- If the tool-only-assistant-after-echo ordering matters for the visual proof, a capture of that shape would confirm whether the folded placement is what you want.

Pullfrog  | View workflow run | Using DeepSeek Flash (free via Pullfrog for OSS) | 𝕏

…stamp ties

@coderabbitai on stablyai#20847: the half-millisecond nudge only worked while the next
row sat on a later millisecond. Two transcript rows sharing one millisecond —
common enough — put the echo after the row it was sent before, because the tie
broke on id.

Taking its advice, which is simpler than the nudge it replaces: an anchored echo
now carries its anchor's own timestamp, and `compareMessages` returns 0 on a tie
involving a `pending-at:` row instead of comparing ids. `sort` is stable, so
placement order survives — for the anchor/echo pair and for several echoes on
one anchor, without a nudge to tune or an anchor field on the shared message
type. Unrelated messages keep id tie-breaking exactly as before.

Both conditions are pinned by tests through `orderNativeChatMessages`, and the
first fails without the change:

    expected [ 'u1', 'a1', 'a2', 'pending-at:p1' ]
    to deeply equal [ 'u1', 'a1', 'pending-at:p1', 'a2' ]

1549 tests in native-chat, 457 in shared/native-chat, typecheck and oxlint clean.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

@pullfrog pullfrog Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

✅ No new issues found. The prior review's findings remain addressed, and the tie-break added since then is verified correct.

Reviewed changes — reviewed the delta since the prior pullfrog review (cf5608175f → 65e6e2b20e): the timestamp tie-break that keeps an anchored echo beside its anchor.

  • Anchored echoes carry their anchor's exact timestamp — anchorPendingMessagesToSendBoundary drops the 0.5ms nudge, so an echo no longer sorts past a following row that shares the anchor's millisecond.
  • Tie-break defers to placement order — compareMessages returns 0 when either side is a pending-at: id, relying on Array.prototype.sort stability to preserve the anchor → echo adjacency the anchoring step built.
  • Two tie tests added — a same-millisecond following row, and multiple echoes under one anchor, both asserted through orderNativeChatMessages.

I fuzzed the full nativeChatMessagesWithPending output (content + markers + streaming + trailing) through compareMessages and the echo stayed adjacent to its anchor in every case; the invariant holds because the view feeds content already sorted by (timestamp, id). The new test genuinely fails against the old nudge (a1, a2, pending-at:p1), so it guards the regression.

Pullfrog  | View workflow run | Using DeepSeek Flash (free via Pullfrog for OSS) | 𝕏

This branch has not been deployed

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Bug]: chat view renders a mid-turn queued message after the reply instead of where it was sent

1 participant