Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
42 changes: 42 additions & 0 deletions apps/web/src/components/chat/MessagesTimeline.logic.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1347,6 +1347,48 @@ describe("deriveMessagesTimelineRows", () => {
]);
});

it("keeps the working header below the initiating prompt across repeated steers", () => {
const runId = RunId.make("steered-run");
const startedAt = "2026-01-01T00:00:00Z";
const prompt = (id: string, inputIntent: "steer" | "send") => ({
id,
kind: "message" as const,
createdAt: startedAt,
message: {
id: MessageId.make(id),
role: "user" as const,
text: id,
runId,
...(inputIntent === "steer" ? { inputIntent } : {}),
createdAt: startedAt,
updatedAt: startedAt,
streaming: false,
},
});
const timelineEntries = [prompt("initial-prompt", "send")];
for (let index = 0; index < 3; index += 1) {
const rows = deriveMessagesTimelineRows({
timelineEntries,
latestRun: { runId, status: "running", startedAt, completedAt: null },
isWorking: true,
activeTurnStartedAt: startedAt,
turnDiffSummaries: [],
supportsConversationRollback: false,
});
expect(rows.slice(0, 2).map((row) => row.id)).toEqual([
"initial-prompt",
"working-indicator-row",
]);
expect(rows.filter((row) => row.kind === "working")).toEqual([
expect.objectContaining({ createdAt: startedAt }),
]);
expect(rows.filter((row) => row.kind === "message").map((row) => row.id)).toEqual(
timelineEntries.map((entry) => entry.id),
);
timelineEntries.push(prompt(`steer-${index}`, "steer"));
}
});

it("keeps the previous turn folded while a newly sent message awaits its turn", () => {
// Right after send, isWorking is true but latestRun still points at the
// previous, settled turn — it must stay folded through that window.
Expand Down
16 changes: 11 additions & 5 deletions apps/web/src/components/chat/MessagesTimeline.logic.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1000,13 +1000,19 @@ export function deriveMessagesTimelineRows(input: {
entry.toolLifecycleStatus === "inProgress" &&
entry.runId === unsettledRunId;

// The active run's header row ("Working for ...") anchors right after the
// latest user message or notification, or at the run's first owned work entry when one
// already rendered above it.
// A steer continues the current turn. Keep its elapsed-time header below
// the initiating prompt (or automatic wake), rather than moving it down.
let activeTurnHeaderIndex = input.timelineEntries.length;
if (input.isWorking) {
const latestResponseBoundaryIndex = lastResponseBoundaryIndex(input.timelineEntries);
activeTurnHeaderIndex = latestResponseBoundaryIndex + 1;
activeTurnHeaderIndex =
input.timelineEntries.findLastIndex(
(entry) =>
(entry.kind === "message" &&
entry.message.role === "user" &&
entry.message.inputIntent !== "steer" &&
entry.message.inputIntent !== "promoted_queued_to_steer") ||
(entry.kind === "work" && entry.entry.itemType === "notification"),
) + 1;
}

// Contiguous trailing work entries of the active run collapse into one live
Expand Down
Loading