Skip to content

Commit bfb4e88

Browse files
fix(web,mobile): keep the reply to a steer visible when a settled run folds
Fixes #28161 Co-Authored-By: Claude Opus 5.5 <[email protected]>
1 parent 11181ce commit bfb4e88

3 files changed

Lines changed: 22 additions & 8 deletions

File tree

‎apps/mobile/src/lib/threadActivity.ts‎

Lines changed: 11 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1070,13 +1070,12 @@ function deriveThreadFeedRunFolds(
10701070
const interruptedRunIds = new Set<RunId>();
10711071
const failedRunIds = new Set(feedFailedRunIds);
10721072
const promptIndexByRunId = new Map<RunId, number>();
1073+
const isSteer = (entry: ThreadFeedEntry) =>
1074+
entry.type === "message" &&
1075+
(entry.message.inputIntent === "steer" ||
1076+
entry.message.inputIntent === "promoted_queued_to_steer");
10731077
for (const [index, entry] of feed.entries()) {
1074-
if (
1075-
entry.type !== "message" ||
1076-
entry.message.role !== "user" ||
1077-
entry.message.inputIntent === "steer" ||
1078-
entry.message.inputIntent === "promoted_queued_to_steer"
1079-
) {
1078+
if (entry.type !== "message" || entry.message.role !== "user" || isSteer(entry)) {
10801079
continue;
10811080
}
10821081
const runId = entry.message.runId;
@@ -1089,11 +1088,15 @@ function deriveThreadFeedRunFolds(
10891088
let runlessKey: RunId | null = null;
10901089
let responseKey: RunId | null = null;
10911090
let pendingUserBoundary: string | null = null;
1091+
// The first assistant message after a steer answers it, so it stays visible.
1092+
const runsAwaitingSteerReply = new Set<RunId>();
1093+
const steerReplyEntryIds = new Set<string>();
10921094
for (const [index, entry] of feed.entries()) {
10931095
if (entry.type === "message" && entry.message.role === "user") {
10941096
pendingUserBoundary = entry.message.createdAt;
10951097
runlessKey = entry.message.runId == null ? RunId.make(`runless:${entry.id}`) : null;
10961098
responseKey = entry.message.runId ?? runlessKey;
1099+
if (responseKey !== null && isSteer(entry)) runsAwaitingSteerReply.add(responseKey);
10971100
continue;
10981101
}
10991102
const ownRunId =
@@ -1119,6 +1122,7 @@ function deriveThreadFeedRunFolds(
11191122
group.entries.push(entry);
11201123
if (entry.type === "message") {
11211124
terminalAssistantMessageIdByRun.set(runId, entry.id);
1125+
if (runsAwaitingSteerReply.delete(runId)) steerReplyEntryIds.add(entry.id);
11221126
}
11231127
if (entry.type !== "activity-group") continue;
11241128
for (const activity of entry.activities) {
@@ -1153,6 +1157,7 @@ function deriveThreadFeedRunFolds(
11531157
.filter(
11541158
(entry) =>
11551159
entry.id !== terminalAssistantId &&
1160+
!steerReplyEntryIds.has(entry.id) &&
11561161
entry.type !== "html-render" &&
11571162
entry.type !== "mcp-app" &&
11581163
!(entry.type === "activity-group" && isContextCompactionActivityGroup(entry)) &&

‎apps/web/src/components/chat/MessagesTimeline.logic.test.ts‎

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3726,7 +3726,8 @@ describe("linked timeline resources", () => {
37263726
message("steer", "user", 1),
37273727
event("created", "thread_created"),
37283728
message("after-steer", "assistant", 3),
3729-
message("final", "assistant", 4),
3729+
message("progress", "assistant", 4),
3730+
message("final", "assistant", 5),
37303731
];
37313732
const segmentKey = RunId.make(`${runId}:steer:steer`);
37323733
const rowIds = (expanded: RunId[]) =>
@@ -3742,6 +3743,7 @@ describe("linked timeline resources", () => {
37423743
`turn-fold:${segmentKey}`,
37433744
"created",
37443745
"after-steer",
3746+
"progress",
37453747
"final",
37463748
"summary:created",
37473749
"assistant-meta:final",

‎apps/web/src/components/chat/MessagesTimeline.logic.ts‎

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1039,6 +1039,9 @@ function deriveTurnFolds(input: {
10391039
}
10401040
const groupsByRunId = new Map<RunId, TurnGroup>();
10411041
const runlessFailedKeys = new Set<RunId>();
1042+
// The first assistant message after a steer answers it, so it stays visible.
1043+
const runsAwaitingSteerReply = new Set<RunId>();
1044+
const steerReplyEntryIds = new Set<string>();
10421045

10431046
// Key runless responses per prompt to prevent V2 runs unfolding imported turns.
10441047
let runlessKey: RunId | null = null;
@@ -1057,6 +1060,7 @@ function deriveTurnFolds(input: {
10571060
}
10581061
if (entry.kind === "message" && timelineEntryIsSteer(entry)) {
10591062
const steerRunId = entry.message.runId ?? responseKey;
1063+
if (steerRunId !== null) runsAwaitingSteerReply.add(steerRunId);
10601064
const group = steerRunId === null ? undefined : groupsByRunId.get(steerRunId);
10611065
if (group !== undefined && group.entries.length > 0) {
10621066
// Back-to-back steers split once, after the last of them.
@@ -1094,6 +1098,9 @@ function deriveTurnFolds(input: {
10941098
}
10951099
group.entries.push(entry);
10961100
if (entry.kind === "message") {
1101+
if (runsAwaitingSteerReply.delete(runId)) {
1102+
steerReplyEntryIds.add(entry.id);
1103+
}
10971104
if (input.terminalAssistantMessageIds.has(entry.message.id)) {
10981105
group.terminalEntry = entry;
10991106
}
@@ -1121,7 +1128,7 @@ function deriveTurnFolds(input: {
11211128
? group.entries.findIndex((entry) => entry.id === group.terminalEntry?.id)
11221129
: group.entries.length;
11231130
for (const [index, entry] of group.entries.entries()) {
1124-
if (entry.id === group.terminalEntry?.id) {
1131+
if (entry.id === group.terminalEntry?.id || steerReplyEntryIds.has(entry.id)) {
11251132
continue;
11261133
}
11271134
const isCompaction = timelineEntryIsCompaction(entry);

0 commit comments

Comments
 (0)