Skip to content

Commit fe219c4

Browse files
shivamhwpPR Batch Testerclaude
authored
fix(web): keep tool groups collapsed after a usage-limit pause (pingdotgg#24937)
Co-authored-by: PR Batch Tester <[email protected]> Co-authored-by: Claude Opus 5.5 <[email protected]>
1 parent 8298664 commit fe219c4

2 files changed

Lines changed: 66 additions & 31 deletions

File tree

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

Lines changed: 44 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -3841,9 +3841,15 @@ it("keeps the working header in place across worktree setup handoff", () => {
38413841
});
38423842

38433843
describe("failed turn transcript", () => {
3844-
it.each(["provider_error", "usage_limit"] as const)(
3845-
"keeps historical %s failures and preceding work visible without disclosures",
3846-
(failureClass) => {
3844+
it.each([
3845+
{ position: "historical", failureClass: "provider_error" },
3846+
{ position: "historical", failureClass: "usage_limit" },
3847+
{ position: "latest", failureClass: "usage_limit" },
3848+
] as const)(
3849+
"keeps $position $failureClass failures visible with preceding work grouped appropriately",
3850+
({ position, failureClass }) => {
3851+
const isLatest = position === "latest";
3852+
const commands = isLatest ? ["pwd", "ls", "false"] : ["pwd"];
38473853
const runId = RunId.make("failed-run");
38483854
const threadId = ThreadId.make("failed-thread");
38493855
const at = DateTime.makeUnsafe("2026-09-20T12:00:00Z");
@@ -3880,23 +3886,24 @@ describe("failed turn transcript", () => {
38803886
attachments: [],
38813887
},
38823888
},
3883-
{
3884-
position: 1,
3889+
...commands.map((input, index): OrchestrationV2ProjectedTurnItem => ({
3890+
position: index + 1,
38853891
visibility: "local",
38863892
sourceThreadId: threadId,
3887-
sourceItemId: TurnItemId.make("command"),
3893+
sourceItemId: TurnItemId.make(index === 0 ? "command" : `command-${index}`),
38883894
item: {
38893895
...base,
3890-
id: TurnItemId.make("command"),
3896+
id: TurnItemId.make(index === 0 ? "command" : `command-${index}`),
3897+
ordinal: index + 1,
38913898
type: "command_execution",
38923899
status: "completed",
3893-
input: "pwd",
3900+
input,
38943901
output: "",
38953902
exitCode: 0,
38963903
},
3897-
},
3904+
})),
38983905
{
3899-
position: 2,
3906+
position: commands.length + 1,
39003907
visibility: "local",
39013908
sourceThreadId: threadId,
39023909
sourceItemId: TurnItemId.make("failure"),
@@ -3918,37 +3925,44 @@ describe("failed turn transcript", () => {
39183925
timelineEntries: deriveTimelineEntriesFromVisibleTurnItems({
39193926
visibleTurnItems: items,
39203927
optimisticMessages: [],
3921-
}).map((entry) => ({
3922-
...entry,
3923-
attempt: {
3924-
id: RunAttemptId.make("superseded-attempt"),
3925-
runId,
3926-
attemptOrdinal: 1,
3927-
rootNodeId: NodeId.make("superseded-root"),
3928-
status: "superseded" as const,
3929-
completedAt: null,
3930-
},
3931-
})),
3928+
}).map((entry) =>
3929+
isLatest
3930+
? entry
3931+
: {
3932+
...entry,
3933+
attempt: {
3934+
id: RunAttemptId.make("superseded-attempt"),
3935+
runId,
3936+
attemptOrdinal: 1,
3937+
rootNodeId: NodeId.make("superseded-root"),
3938+
status: "superseded" as const,
3939+
completedAt: null,
3940+
},
3941+
},
3942+
),
39323943
latestRun: {
3933-
runId: RunId.make("newer-run"),
3934-
status: "completed",
3944+
runId: isLatest ? runId : RunId.make("newer-run"),
3945+
status: isLatest ? "failed" : "completed",
39353946
startedAt: DateTime.formatIso(at),
39363947
completedAt: DateTime.formatIso(at),
39373948
},
39383949
isWorking: false,
39393950
turnDiffSummaries: [],
39403951
supportsConversationRollback: false,
39413952
});
3942-
expect(
3943-
rows.some(
3944-
(row) =>
3945-
row.kind === "turn-fold" || row.kind === "attempt-fold" || row.kind === "work-toggle",
3946-
),
3947-
).toBe(false);
3953+
expect(rows.some((row) => row.kind === "turn-fold" || row.kind === "attempt-fold")).toBe(
3954+
false,
3955+
);
39483956
const work = rows.flatMap((row) => (row.kind === "work" ? row.groupedEntries : []));
3949-
expect(work.map((entry) => entry.id)).toEqual(["command", "failure"]);
3957+
expect(rows.find((row) => row.kind === "work-toggle")).toEqual(
3958+
isLatest ? expect.objectContaining({ hiddenCount: 3, expanded: false }) : undefined,
3959+
);
3960+
expect(work.map((entry) => entry.id)).toEqual(
3961+
isLatest ? ["failure"] : ["command", "failure"],
3962+
);
39503963
expect(work.at(-1)?.detail).toBe("The provider stopped this turn.\nRetry later.");
39513964
expect(work.at(-1)?.createdAt).toBe("2026-09-20T12:00:00.000Z");
3965+
if (isLatest) return;
39523966
const entriesWithoutTools = deriveTimelineEntriesFromVisibleTurnItems({
39533967
visibleTurnItems: items.filter((row) => row.item.type !== "command_execution"),
39543968
optimisticMessages: [],

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

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -973,6 +973,24 @@ function failedTimelineRunIds(
973973
return failed;
974974
}
975975

976+
/** Usage-limit pauses keep their tool groups; any other failure itemizes the run's work. */
977+
function itemizedFailedRunIds(
978+
entries: ReadonlyArray<TimelineEntry>,
979+
failedRunIds: ReadonlySet<RunId>,
980+
) {
981+
const usageLimited = new Set<RunId>();
982+
const otherFailures = new Set<RunId>();
983+
for (const entry of entries) {
984+
const item = timelineEntryFailedItem(entry);
985+
if (item?.runId) {
986+
(item.failure.class === "usage_limit" ? usageLimited : otherFailures).add(item.runId);
987+
}
988+
}
989+
return new Set(
990+
[...failedRunIds].filter((runId) => !usageLimited.has(runId) || otherFailures.has(runId)),
991+
);
992+
}
993+
976994
function deriveTurnFolds(input: {
977995
timelineEntries: ReadonlyArray<TimelineEntry>;
978996
terminalAssistantMessageIds: ReadonlySet<string>;
@@ -1410,6 +1428,7 @@ export function deriveMessagesTimelineRows(input: {
14101428
const nextRows: MessagesTimelineRow[] = [];
14111429
const terminalAssistantMessageIds = deriveTerminalAssistantMessageIds(timelineEntries);
14121430
const failedRunIds = failedTimelineRunIds(timelineEntries, input.latestRun ?? null);
1431+
const itemizedRunIds = itemizedFailedRunIds(timelineEntries, failedRunIds);
14131432
const liveSubagentEntryIds = liveSubagentCardEntryIds(timelineEntries);
14141433
const completeSupersededReplyIds = completeSupersededReplyEntryIds(timelineEntries);
14151434
const supersededFoldsByAnchorEntryId = deriveSupersededAttemptFolds(
@@ -1772,7 +1791,7 @@ export function deriveMessagesTimelineRows(input: {
17721791
if (timelineEntry.kind === "work") {
17731792
if (
17741793
isPendingApproval(timelineEntry.entry) ||
1775-
(timelineEntry.entry.runId != null && failedRunIds.has(timelineEntry.entry.runId)) ||
1794+
(timelineEntry.entry.runId != null && itemizedRunIds.has(timelineEntry.entry.runId)) ||
17761795
(timelineEntry.entry.itemType === "error" &&
17771796
timelineEntry.entry.toolLifecycleStatus === "failed") ||
17781797
timelineEntry.entry.tone === "error" ||
@@ -1798,6 +1817,8 @@ export function deriveMessagesTimelineRows(input: {
17981817
!nextEntry ||
17991818
nextEntry.kind !== "work" ||
18001819
nextEntry.entry.sourceActivityKind === "context-compaction" ||
1820+
(nextEntry.entry.itemType === "error" &&
1821+
nextEntry.entry.toolLifecycleStatus === "failed") ||
18011822
nextEntry.entry.tone === "error" ||
18021823
nextEntry.entry.sourceActivityKind === "runtime.error" ||
18031824
nextEntry.entry.itemType === "system_notice" ||

0 commit comments

Comments
 (0)