Repository navigation
[Bug]: SwiftUI shows a turn's later tool calls above assistant text that came before them #14695
Description
Before submitting
- I searched existing issues and did not find a duplicate.
- I included enough detail to reproduce or investigate the problem.
Area
apps/swift-ios (SwiftUI mobile). This option isn't in the dropdown; this is not React Native apps/mobile.
Steps to reproduce
- Pair the SwiftUI app to a server, and open a project with a
README.mdand a few files underapps/. - Start a new task (here: Claude · Claude Sonnet 5.5) with this prompt:
Work in two separate steps. Before each step, write one short sentence saying what you are about to do. Step 1: read README.md and tell me its first heading. Step 2: list the files under apps/ and tell me how many files there are. Finish with a one-line summary.
- Watch the thread while the turn runs, then expand the Work log row after it settles.
The server recorded this order (scratch database, UTC, 2 Oct 2026):
| Time | Event |
|---|---|
| 00:08:18.060 | assistant: "Step 1: I'll read README.md…" |
| 00:08:18.060–18.744 | Read README.md (tool.started → tool.completed) |
| 00:08:19.769 | assistant: "The first heading … is "Demo Notes". Step 2: I'll list the files under apps/…" |
| 00:08:19.772–21.027 | Bash: find …/apps -type f (tool.started → tool.completed) |
| 00:08:22.033 | assistant: "There are 3 files…" and the summary |
Expected behavior
Tool activity appears where it happened relative to the assistant text: the find call 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 find call 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:
- grouping:
collapsedWorkLogsand the live path at L6302; - the row is dated by the turn's first tool event (L7650-L7651);
- rows are sorted by that date (L5984-L5985).
So text1 → toolA → text2 → toolB renders as text1, [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-group rows 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.
- (a) End a work-log group at assistant text, notices and turn boundaries, like web, and keep consecutive tool calls in one collapsed row; or
- (b) Keep one row per turn, but position it so later tool calls don't appear above earlier text; or
- (c) The current per-turn row is the intended SwiftUI design.
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: from makeDetailDelta to the end of NativeFeatureClient.swift, the only differences are one added newProjectsRoot mapping line and an afterSequence replay-skip in the reducer. Neither touches the turnId grouping, the first-event anchor or the sort. ThreadDetailView.swift differs 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
- iOS 26.5 Simulator, iPhone 17 Pro, light appearance.
- Provider: Claude · Claude Sonnet 5.5 (High).
- Local scratch server on 127.0.0.1.
Logs or stack traces
N/A. The event order above comes from the server's projection_thread_messages and projection_thread_activities tables 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 Read and the step 2 find, 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 recordVideo capture 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.



Activity
juliusmarminge commentedon Oct 2, 2026
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
NativeActivityNotice), and later tools shouldn't stay bundled above them.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:
#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.