Repository navigation
[Bug]: Mobile shows a mid-turn progress note below "Worked for …", next to the final answer #15380
Description
Activity
juliusmarminge commented
on Oct 3, 2026 MemberMore actionsNote
Grok responding on behalf of Julius.
Triage
Thanks for the clear write-up and the pointer to the exact function, @ValeraZSD. This reproduces on
mainat00eb8f6183, and I didn't find another open issue for this split between mobile and web.What's happening
deriveThreadFeedRunFoldson mobile still leaves the run's first assistant message out ofhiddenEntryIds, along with the final message. That's the exemption #7723 added for both clients. #8828 removed it on web only, wherederiveTurnFoldsnow hides every assistant message except the last one.t3code/apps/mobile/src/lib/threadActivity.ts
Lines 1050 to 1099 in 00eb8f6
const firstAssistantId = firstAssistantMessageIdByRun.get(runId); const terminalAssistantId = terminalAssistantMessageIdByRun.get(runId); const hiddenEntryIds = new Set( group.entries .filter( (entry) => entry.id !== firstAssistantId && entry.id !== terminalAssistantId && !( entry.type === "activity-group" && entry.activities.some( (activity) => activity.prominent || activity.projectedItem.item.type === "notification" || activity.projectedItem.item.type === "handoff", ) ), ) .map((entry) => entry.id), ); const firstEntry = group.entries[0]; const firstHiddenEntry = group.entries.find((entry) => hiddenEntryIds.has(entry.id)); const lastEntry = group.entries.at(-1); if (!firstHiddenEntry || !firstEntry || !lastEntry) continue; const hidesNonCompactionWork = group.entries.some( (entry) => hiddenEntryIds.has(entry.id) && !(entry.type === "activity-group" && isContextCompactionActivityGroup(entry)), ); if (!hidesNonCompactionWork) continue; const terminalEntry = terminalAssistantId ? group.entries.find((entry) => entry.id === terminalAssistantId) : null; const latestRunMatches = latestRun?.runId === runId; const lastEntryEnd = lastEntry.type === "message" ? lastEntry.message.updatedAt : lastEntry.createdAt; const elapsedMs = latestRunMatches && latestRun.startedAt && latestRun.completedAt ? computeElapsedMs(latestRun.startedAt, latestRun.completedAt) : computeElapsedMs( group.startBoundary ?? firstEntry.createdAt, maxIsoTimestamp( terminalEntry?.type === "message" ? terminalEntry.message.updatedAt : null, lastEntryEnd, ) ?? lastEntryEnd, ); const duration = elapsedMs === null ? null : formatDuration(elapsedMs); const interrupted = latestRunMatches && (latestRun.status === "interrupted" || latestRun.status === "cancelled"); foldsByAnchorId.set(firstHiddenEntry.id, { t3code/apps/web/src/components/chat/MessagesTimeline.logic.ts
Lines 962 to 986 in 00eb8f6
const hiddenEntryIds = new Set<string>(); const terminalEntryIndex = group.terminalEntry ? group.entries.findIndex((entry) => entry.id === group.terminalEntry?.id) : group.entries.length; for (const [index, entry] of group.entries.entries()) { if (entry.id === group.terminalEntry?.id) { continue; } const isCompaction = entry.kind === "work" && entry.entry.sourceActivityKind === "context-compaction"; const isFoldableTrailingActivity = entry.kind === "work" && entry.entry.toolLifecycleStatus !== "inProgress" && !workEntryDisplayIndicatesToolFailure(entry.entry); if (!isCompaction && index > terminalEntryIndex && !isFoldableTrailingActivity) { continue; } // Linked resources can outlive their launching run and stay visible // after the surrounding work folds. if (timelineEntryIsPersistentResourceCard(entry)) { continue; } if (entry.kind === "work" && entry.entry.itemType === "notification") continue; hiddenEntryIds.add(entry.id); } The fold row is placed at the first hidden entry. A message splits activity groups, so the tools before the note and the tools after it end up in separate groups. When the run starts with a tool call, the fold is drawn at that first tool group. The exempt note then renders below the fold, and the later tool groups stay inside it. That's why the note looks like it came after the work and reads as the start of the answer. Expanding the fold shows the real order, so nothing is lost.
The two clients also disagree when the note comes first. The mobile test "keeps opening and final assistant messages around the first hidden work" expects prompt, opening note, fold, final answer. The web test "folds the first assistant message and settled work before the terminal response" expects prompt, fold, final answer. Only the tool-first order puts the note below the fold, though.
Likely fix area
- Match web: drop the first-assistant exemption in
deriveThreadFeedRunFoldsand update that mobile test. Settled turns would then read prompt, Worked for …, final answer on both clients, the way #8828 already works on web. This is the two-file port you described. - The opposite direction: draft #14698 keeps every assistant message visible on both clients and folds only the work. It isn't merged.
Either way, the two clients should probably agree. Which shared behavior to keep is a product call, so it makes sense to hold the port until that's settled.
A maintainer will decide on the fix direction.
- Match web: drop the first-assistant exemption in
- addedbugSomething is broken or behaving incorrectly.Something is broken or behaving incorrectly.via-triageFiled through npx t3 triageFiled through npx t3 triage
on Oct 3, 2026
Before submitting
Area
apps/mobile
Steps to reproduce
Expected behavior
The same as web since #8828: prompt → Worked for … → final answer, with the progress note inside the fold.
Actual behavior
Mobile shows prompt → Worked for … → progress note → final answer. The note sits under the fold as if it came after all the work, and reads as the first paragraph of the final answer, while most of the work happened between the two.
Cause:
deriveThreadFeedRunFoldsinapps/mobile/src/lib/threadActivity.tsstill exempts the run's first assistant message from the fold (the rule #7723 added for both clients). #8828 removed that rule from web only, so web and mobile now fold the same thread differently.Impact
Minor, but misleading: on the phone the order of what the agent did and said is misrepresented in every settled run that starts with a tool call and narrates once.
Version or commit
mainat00eb8f6183.Environment
Android phone app connected to a Windows desktop server, Claude provider.
Workaround
Expand the fold.
Note: the open draft #14698 proposes the opposite direction for both clients (keep every assistant message visible). I'd argue for keeping the fold: a settled turn that reads prompt → Worked for → answer is much easier to scan than a transcript with every interim note in it. Anything an interim note says that matters belongs in the final message, and that is the agent's job to restate there. Either way, web and mobile should agree. I have a two-file fix ready that ports #8828 to mobile, with a test that reproduces the row order above (red on main, green with the fix), and can open it if this direction is the one you want.