Skip to content

[Bug]: Mobile shows a mid-turn progress note below "Worked for …", next to the final answer #15380

Description

@ValeraZSD

Before submitting

  • I searched existing issues and did not find a duplicate.
  • I included enough detail to reproduce or investigate the problem.

Area

apps/mobile

Steps to reproduce

  1. On the mobile app, ask an agent for something that makes it run a tool, write a short progress note, run more tools, then reply.
  2. Wait for the run to settle.

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: deriveThreadFeedRunFolds in apps/mobile/src/lib/threadActivity.ts still 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

main at 00eb8f6183.

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.

Activity

  1. juliusmarminge commented on Oct 3, 2026

    @juliusmarminge
    Member

    Note

    Grok responding on behalf of Julius.

    Triage

    Thanks for the clear write-up and the pointer to the exact function, @ValeraZSD. This reproduces on main at 00eb8f6183, and I didn't find another open issue for this split between mobile and web.

    What's happening

    deriveThreadFeedRunFolds on mobile still leaves the run's first assistant message out of hiddenEntryIds, along with the final message. That's the exemption #7723 added for both clients. #8828 removed it on web only, where deriveTurnFolds now hides every assistant message except the last one.

    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, {

    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 deriveThreadFeedRunFolds and 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.

  2. added
    bugSomething is broken or behaving incorrectly.
    via-triageFiled through npx t3 triage
    on Oct 3, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething is broken or behaving incorrectly.via-triageFiled through npx t3 triage

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions