Repository navigation
[Bug]: SwiftUI shows a turn's later tool calls above assistant text that came before them #14695
Description
Activity
Note
Grok responding on behalf of Julius.
Triage
Thanks for the event table and the live and settled captures, @saphid! This is a real bug in the experimental SwiftUI client. I confirmed it on
t3code/rebuild-mobile-app-swiftatbb01adbf90, the branch tip (open PR #5178).collapsedWorkLogs, the liveturnIdgrouping inapplyActivityMutation, and the first-event timestamp inNativeWorkLogAccumulatorare all unchanged from your build285d2ab80c.To answer your question: it's (a). A work-log group should end at assistant text and at other non-tool rows, while consecutive tool calls stay together in one collapsed row. A new turn already starts a new row. Neither (b) nor (c) is the intended behavior.
What happens today
Every tool event in a turn goes into a single
work-log-<turnId>row. The snapshot path groups byturnIdincollapsedWorkLogs, and the live path uses the same key. The row's date comes from the first tool event and doesn't change when later tools arrive. The transcript is sorted by that date, sotext → tool → text → toolshows up astext, [both tools], text. Your Step 2findappears above the Step 2 sentence because the row is still dated from the Step 1Read. That's true both live and after the turn settles.The comment above
collapsedWorkLogsis about keeping rows a bounded size on long turns. It doesn't mean tool calls on opposite sides of assistant text should share one bubble. Since a single timestamp can't sit both before and after that text, re-dating the row to the last tool or the end of the turn would only put a different call on the wrong side.Intended behavior
- Consecutive tool calls, with only lifecycle updates between them, stay in one collapsed Work log row.
- A visible assistant message ends that row. The next tool starts a new row after the message, dated from that tool.
- A notice ends the row the same way. Errors, runtime warnings, and context compaction already get their own timeline rows (
NativeActivityNotice), and later tools shouldn't stay bundled above them. - A new turn starts a new row, which already works.
- Expanding a row shows only the calls in that group, in order.
Web already splits groups at assistant text (
isActivityEntryand the consecutive-work loop inMessagesTimeline.logic.ts), and React Native's grouping stops at a non-reasoning message too (activityRunTurnIdinthreadActivity.ts). Settled web turns also fold earlier activity behind "Worked for…", but this report isn't asking for that.Fix scope
Both paths need the new boundary. The snapshot rebuild groups activities without looking at messages, and the live updater keeps adding to
work-log-<turnId>after an assistant message arrives.Still out of scope, as your report says:
- One row per tool call. Consecutive calls with no text between them should stay together.
- Relative ages, the header receipt, and the last-message age line.
#10766 (closed PR) was closed under the one problem rule because it mixed the receipt line into the ordering change. #10767 (closed PR) was closed under prior approval because there was no triaged direction for the grouping-plus-ages presentation. This issue settles the grouping boundary only. It doesn't approve the age display, and it doesn't reopen those PRs as they stand.
A focused fix should show this same two-step prompt, both live and settled. The
Readshould appear under the Step 1 sentence, thefindshould appear under the Step 2 sentence, and two calls with no assistant text between them should still share one row.- addedbugSomething is broken or behaving incorrectly.Something is broken or behaving incorrectly.via-triageFiled through npx t3 triageFiled through npx t3 triage
on Oct 2, 2026
Before submitting
Area
apps/swift-ios(SwiftUI mobile). This option isn't in the dropdown; this is not React Nativeapps/mobile.Steps to reproduce
README.mdand a few files underapps/.The server recorded this order (scratch database, UTC, 2 Oct 2026):
Read README.md(tool.started→tool.completed)Bash: find …/apps -type f(tool.started→tool.completed)Expected behavior
Tool activity appears where it happened relative to the assistant text: the
findcall appears after "Step 2: I'll list the files…", not before it.Actual behavior
Both tool calls are folded into one Work log · 2 row directly under "Step 1". The step 2
findcall is therefore shown above the "Step 2: I'll list the files…" text that preceded it, both live and after the turn settles.On SwiftUI, every tool event in a turn goes into one work-log row keyed by
turnId:collapsedWorkLogsand the live path at L6302;So
text1 → toolA → text2 → toolBrenders astext1, [A+B], text2.Web groups consecutive work entries and ends a group at any non-work entry (MessagesTimeline.logic.ts#L1220-L1260). React Native builds
activity-grouprows separately (threadActivity.ts#L2182). I have not captured web or React Native screenshots of this thread.Question for maintainers: what is the intended SwiftUI grouping? No fix is proposed here until that is triaged.
Earlier, unmerged attempts in this area were closed PRs #10766 (chronology fix; it split rows per tool call) and #10767 (consecutive-call grouping with relative ages). Neither is being re-proposed. Timestamps and ages on rows are a separate topic and not part of this report.
Impact
Minor bug or occasional failure. The transcript misstates the order in which the agent worked whenever it writes text between tool calls.
Version or commit
SwiftUI app: a Debug build of
t3code/rebuild-mobile-app-swift@285d2ab80c, from a clean tree. I reused an existing build rather than building the current target.Current target:
bb01adbf90. The relevant code is unchanged from that build: frommakeDetailDeltato the end ofNativeFeatureClient.swift, the only differences are one addednewProjectsRootmapping line and anafterSequencereplay-skip in the reducer. Neither touches theturnIdgrouping, the first-event anchor or the sort.ThreadDetailView.swiftdiffers only by an added "Restart agent session" menu item.Server: the same repository revision (
node apps/server/src/bin.ts serve), with an empty scratch home and a scratch project.Environment
Logs or stack traces
N/A. The event order above comes from the server's
projection_thread_messagesandprojection_thread_activitiestables for that thread.Screenshots, recordings, or supporting files
Settled, collapsed. One Work log · 2 row under "Step 1", above "Step 2: I'll list the files…":
Settled, expanded. The row contains both the step 1
Readand the step 2find, above the Step 2 text:Live turn. Mid-turn, Work log · 1 / Command run (the step 2
find) is already above "Step 2: I'll list the files…", then becomes Work log · 2:About the GIF. Its frames come from an unedited
simctl recordVideocapture of the whole turn (295 s, H.264). Frames were sampled at 4 fps and play at 0.25 s each, with the last frame held. The source has irregular timestamps, so the GIF does not show exact timing.How it was driven. A GPT-6 Astra agent operated the Simulator UI: it chose the model, typed the prompt, and captured screenshots and the recording. The T3 Device panel listed and opened the device, but its UI automation failed twice (no active session, then a daemon timeout), so the agent used the AXe Simulator CLI. Earlier, Claude Opus 5.5 paired the app and added the scratch project with the same CLI. The agent turn itself was real, not a fixture.
Not checked: a physical device, the current-target build itself, and web or React Native captures of the same thread.
Reported with Claude Opus 5.5 (Claude Code), with Simulator UI driven by GPT-6 Astra (Codex) via T3.