+ {activityStatus !== undefined ? (
+
+ ) : (
+
+
+
)}
- data-composer-tasks-badge="true"
- >
-
event.preventDefault()}
- >
-
- Tasks
+
+ {activityStatus !== undefined ? (
+
+ ) : (
+ Tasks
+ )}
+ {activityStatus !== undefined ? : null}
{progress.step}
-
+
+ = progress.totalSteps ? "text-success" : undefined}
+ data-composer-task-progress="true"
>
{progress.completedSteps}/{progress.totalSteps}
-
-
-
-
event.preventDefault()}
- >
-
-
+
+
+
+
+ >
+ );
+}
+
+export const ComposerTasksBadge = memo(function ComposerTasksBadge({
+ expanded,
+ onToggle,
+ placement = "tab",
+ progress,
+ steps,
+ activityStatus,
+}: {
+ readonly expanded: boolean;
+ readonly onToggle: () => void;
+ readonly placement?: "inline" | "tab";
+ readonly progress: ComposerTasksProgress;
+ readonly steps: readonly ComposerTaskStep[];
+ readonly activityStatus?: ComposerActivityStatus | undefined;
+}) {
+ if (progress.totalSteps <= 0) return null;
+
+ const row = (
+
}
+ aria-expanded={expanded}
+ aria-label={`${expanded ? "Collapse tasks" : "Tasks"}: ${progress.completedSteps} of ${progress.totalSteps} complete. Current task: ${progress.step}`}
+ data-composer-tasks-badge="true"
+ onClick={onToggle}
+ onPointerDown={(event) => event.preventDefault()}
+ >
+
+
+ );
+ return placement === "inline" ? (
+ row
+ ) : (
+
{row}
+ );
+});
+
+export const ComposerTasksContent = memo(function ComposerTasksContent({
+ expanded,
+ onToggle,
+ progress,
+ steps,
+ activityStatus,
+}: {
+ readonly expanded: boolean;
+ readonly onToggle: () => void;
+ readonly progress: ComposerTasksProgress;
+ readonly steps: readonly ComposerTaskStep[];
+ readonly activityStatus?: ComposerActivityStatus | undefined;
+}) {
+ return (
+
+
+ {expanded ? (
+
+ }
+ aria-label={`Task list. ${progress.completedSteps} of ${progress.totalSteps} complete.`}
+ data-composer-tasks-list="true"
+ >
+ {keyedTaskSteps(steps).map(({ key, step }) => (
+ }>
+
+ {step.status === "completed" ? "✓" : step.status === "inProgress" ? "●" : "○"}
+
+
+ {step.step}
+
+
+
+ {step.durationMs !== undefined
+ ? formatDuration(step.durationMs)
+ : step.status === "inProgress"
+ ? "now"
+ : null}
+
+
+
+ ))}
+
+
+ ) : null}
);
});
export const ComposerTasksDrawer = memo(function ComposerTasksDrawer({
- onDismiss,
onCollapse,
- progress,
- steps,
-}: {
- readonly onDismiss: () => void;
+ ...props
+}: Omit
, "expanded" | "onToggle"> & {
readonly onCollapse: () => void;
- readonly progress: ComposerTasksProgress;
- readonly steps: readonly ComposerTaskStep[];
}) {
return (
-
-
- event.preventDefault()}
- >
-
- Tasks
-
- {progress.completedSteps}/{progress.totalSteps}
-
-
-
- event.preventDefault()}
- >
-
-
-
-
- {keyedTaskSteps(steps).map(({ key, step }) => (
-
-
- {step.status === "completed" ? "✓" : step.status === "inProgress" ? "●" : "○"}
-
-
- {step.step}
-
-
- {step.durationMs !== undefined
- ? formatDuration(step.durationMs)
- : step.status === "inProgress"
- ? "now"
- : null}
-
-
- ))}
-
-
+
+
+
+
+
);
});
diff --git a/apps/web/src/components/chat/MessagesTimeline.logic.test.ts b/apps/web/src/components/chat/MessagesTimeline.logic.test.ts
index 81c9fdacdb9a0da75c6ffd7802714d544247fad3..bd6ffffe15dce9ae7ec60c8428076c54e0da0354 100644
--- a/apps/web/src/components/chat/MessagesTimeline.logic.test.ts
+++ b/apps/web/src/components/chat/MessagesTimeline.logic.test.ts
@@ -321,7 +321,6 @@ describe("deriveMessagesTimelineRows", () => {
],
expandedTurnIds: new Set(["turn-1" as never]),
isWorking: false,
- activeTurnStartedAt: null,
turnDiffSummaryByAssistantMessageId: new Map(),
revertTurnCountByUserMessageId: new Map(),
});
@@ -375,7 +374,6 @@ describe("deriveMessagesTimelineRows", () => {
completedAt: null,
},
isWorking: false,
- activeTurnStartedAt: null,
turnDiffSummaryByAssistantMessageId: new Map(),
revertTurnCountByUserMessageId: new Map(),
});
@@ -432,7 +430,6 @@ describe("deriveMessagesTimelineRows", () => {
},
],
isWorking: false,
- activeTurnStartedAt: null,
turnDiffSummaryByAssistantMessageId: new Map([
["assistant-1" as never, assistantTurnDiffSummary],
]),
@@ -513,7 +510,6 @@ describe("deriveMessagesTimelineRows", () => {
const collapsedRows = deriveMessagesTimelineRows({
timelineEntries,
isWorking: false,
- activeTurnStartedAt: null,
turnDiffSummaryByAssistantMessageId: new Map(),
revertTurnCountByUserMessageId: new Map(),
});
@@ -537,7 +533,6 @@ describe("deriveMessagesTimelineRows", () => {
timelineEntries,
expandedTurnIds: new Set(["turn-1" as never]),
isWorking: false,
- activeTurnStartedAt: null,
turnDiffSummaryByAssistantMessageId: new Map(),
revertTurnCountByUserMessageId: new Map(),
});
@@ -603,7 +598,6 @@ describe("deriveMessagesTimelineRows", () => {
const rows = deriveMessagesTimelineRows({
timelineEntries,
isWorking: false,
- activeTurnStartedAt: null,
turnDiffSummaryByAssistantMessageId: new Map(),
revertTurnCountByUserMessageId: new Map(),
});
@@ -698,7 +692,6 @@ describe("deriveMessagesTimelineRows", () => {
completedAt: null,
},
isWorking: true,
- activeTurnStartedAt: "2026-01-01T00:00:14Z",
turnDiffSummaryByAssistantMessageId: new Map(),
revertTurnCountByUserMessageId: new Map(),
});
@@ -735,7 +728,6 @@ describe("deriveMessagesTimelineRows", () => {
completedAt: "2026-01-01T00:00:47Z",
},
isWorking: false,
- activeTurnStartedAt: null,
turnDiffSummaryByAssistantMessageId: new Map(),
revertTurnCountByUserMessageId: new Map(),
});
@@ -803,7 +795,6 @@ describe("deriveMessagesTimelineRows", () => {
completedAt: "2026-01-01T00:00:22Z",
},
isWorking: true,
- activeTurnStartedAt: "2026-01-01T00:01:00Z",
turnDiffSummaryByAssistantMessageId: new Map(),
revertTurnCountByUserMessageId: new Map(),
});
@@ -812,7 +803,6 @@ describe("deriveMessagesTimelineRows", () => {
"turn-fold:turn-1",
"assistant-final-entry",
"user-followup-entry",
- "working-indicator-row",
]);
const finalRow = rows.find((row) => row.id === "assistant-final-entry");
expect(finalRow?.kind === "message" && finalRow.showAssistantMeta).toBe(true);
@@ -855,14 +845,12 @@ describe("deriveMessagesTimelineRows", () => {
completedAt: null,
},
isWorking: true,
- activeTurnStartedAt: "2026-01-01T00:00:00Z",
turnDiffSummaryByAssistantMessageId: new Map(),
revertTurnCountByUserMessageId: new Map(),
});
expect(rows.some((row) => row.kind === "turn-fold")).toBe(false);
expect(rows.map((row) => row.id)).toEqual([
- "working-indicator-row",
"assistant-thought-entry",
"work-live:work-entry-1",
]);
@@ -924,12 +912,11 @@ describe("deriveMessagesTimelineRows", () => {
completedAt: null,
},
isWorking: true,
- activeTurnStartedAt: "2026-01-01T00:00:00Z",
turnDiffSummaryByAssistantMessageId: new Map(),
revertTurnCountByUserMessageId: new Map(),
});
- expect(rows.map((row) => row.kind)).toEqual(["working", "work-live"]);
+ expect(rows.map((row) => row.kind)).toEqual(["work-live"]);
expect(rows.find((row) => row.kind === "work-live")).toMatchObject({
entry: { id: "running-command" },
groupedEntries: [
@@ -995,12 +982,11 @@ describe("deriveMessagesTimelineRows", () => {
completedAt: null,
},
isWorking: true,
- activeTurnStartedAt: "2026-01-01T00:00:00Z",
turnDiffSummaryByAssistantMessageId: new Map(),
revertTurnCountByUserMessageId: new Map(),
});
- expect(rows.map((row) => row.kind)).toEqual(["working", "work-toggle", "message", "work-live"]);
+ expect(rows.map((row) => row.kind)).toEqual(["work-toggle", "message", "work-live"]);
expect(rows.find((row) => row.kind === "work-toggle")).toMatchObject({
hiddenCount: 1,
summary: "Ran 1 command",
@@ -1062,12 +1048,11 @@ describe("deriveMessagesTimelineRows", () => {
completedAt: null,
},
isWorking: true,
- activeTurnStartedAt: "2026-01-01T00:00:00Z",
turnDiffSummaryByAssistantMessageId: new Map(),
revertTurnCountByUserMessageId: new Map(),
});
- expect(rows.map((row) => row.kind)).toEqual(["working", "work-live", "message", "work-live"]);
+ expect(rows.map((row) => row.kind)).toEqual(["work-live", "message", "work-live"]);
expect(rows.filter((row) => row.kind === "work-live").map((row) => row.entry.id)).toEqual([
"first-running",
"second-running",
@@ -1109,7 +1094,6 @@ describe("deriveMessagesTimelineRows", () => {
],
latestTurn: null,
isWorking: true,
- activeTurnStartedAt: "2026-01-01T00:01:00Z",
turnDiffSummaryByAssistantMessageId: new Map(),
revertTurnCountByUserMessageId: new Map(),
});
@@ -1170,7 +1154,6 @@ describe("deriveMessagesTimelineRows", () => {
completedAt: null,
},
isWorking: true,
- activeTurnStartedAt: "2026-01-01T00:00:00Z",
turnDiffSummaryByAssistantMessageId: new Map(),
revertTurnCountByUserMessageId: new Map(),
});
@@ -1206,12 +1189,11 @@ describe("deriveMessagesTimelineRows", () => {
completedAt: null,
},
isWorking: true,
- activeTurnStartedAt: "2026-01-01T00:00:00Z",
turnDiffSummaryByAssistantMessageId: new Map(),
revertTurnCountByUserMessageId: new Map(),
});
- expect(rows.map((row) => row.kind)).toEqual(["working", "work-live"]);
+ expect(rows.map((row) => row.kind)).toEqual(["work-live"]);
expect(rows.find((row) => row.kind === "work-live")).toMatchObject({
entry: { id: "latest-command" },
groupedEntries: [{ id: "latest-command" }],
@@ -1268,7 +1250,6 @@ describe("deriveMessagesTimelineRows", () => {
},
runningTurnId: "turn-2" as never,
isWorking: true,
- activeTurnStartedAt: "2026-01-01T00:01:00Z",
turnDiffSummaryByAssistantMessageId: new Map(),
revertTurnCountByUserMessageId: new Map(),
});
@@ -1313,7 +1294,6 @@ describe("deriveMessagesTimelineRows", () => {
],
expandedTurnIds: new Set(["turn-1" as never]),
isWorking: false,
- activeTurnStartedAt: null,
turnDiffSummaryByAssistantMessageId: new Map(),
revertTurnCountByUserMessageId: new Map(),
});
@@ -1351,7 +1331,6 @@ describe("deriveMessagesTimelineRows", () => {
completedAt: null,
},
isWorking: true,
- activeTurnStartedAt: "2026-01-01T00:00:00Z",
turnDiffSummaryByAssistantMessageId: new Map(),
revertTurnCountByUserMessageId: new Map(),
});
@@ -1365,7 +1344,10 @@ describe("deriveMessagesTimelineRows", () => {
expect(assistantRow?.showAssistantCopyButton).toBe(false);
});
- it("models work log overflow expansion as inserted list rows", () => {
+ it.each([
+ ["tools", "tool", "Used 3 tools"],
+ ["tools and status updates", "info", "Used 2 tools and received 1 update"],
+ ] as const)("expands %s through the same activity group", (_, middleTone, summary) => {
const timelineEntries = [
{
id: "work-entry-1",
@@ -1386,9 +1368,9 @@ describe("deriveMessagesTimelineRows", () => {
entry: {
id: "work-2",
createdAt: "2026-01-01T00:00:02Z",
- label: "edit",
+ label: "Status updated",
detail: "Editing MessagesTimeline.tsx",
- tone: "tool" as const,
+ tone: middleTone,
},
},
{
@@ -1408,7 +1390,6 @@ describe("deriveMessagesTimelineRows", () => {
const baseInput = {
timelineEntries,
isWorking: false,
- activeTurnStartedAt: null,
turnDiffSummaryByAssistantMessageId: new Map(),
revertTurnCountByUserMessageId: new Map(),
};
@@ -1423,8 +1404,7 @@ describe("deriveMessagesTimelineRows", () => {
groupId: "work-group:work-entry-1",
hiddenCount: 3,
expanded: false,
- onlyToolEntries: true,
- summary: "Used 3 tools",
+ summary,
});
expect(expandedRows.map((row) => row.id)).toEqual([
"work-toggle:work-entry-1",
@@ -1459,7 +1439,6 @@ describe("deriveMessagesTimelineRows", () => {
const rows = deriveMessagesTimelineRows({
timelineEntries,
isWorking: false,
- activeTurnStartedAt: null,
turnDiffSummaryByAssistantMessageId: new Map(),
revertTurnCountByUserMessageId: new Map(),
});
@@ -1476,7 +1455,7 @@ describe("deriveMessagesTimelineRows", () => {
["an error-toned entry recovers", ["error", "info", "completed"], false],
["the final failure is hidden", ["completed", "failed", "info"], true],
["the final failure is visible", ["failed", "info", "failed"], true],
- ["the only failure is visible", ["completed", "info", "failed"], false],
+ ["the only failure is visible", ["completed", "info", "failed"], true],
] as const)(
"uses the final tool call for mixed work groups when %s",
(_, statuses, hasFailure) => {
@@ -1506,16 +1485,23 @@ describe("deriveMessagesTimelineRows", () => {
const rows = deriveMessagesTimelineRows({
timelineEntries,
isWorking: false,
- activeTurnStartedAt: null,
turnDiffSummaryByAssistantMessageId: new Map(),
revertTurnCountByUserMessageId: new Map(),
});
expect(rows.find((row) => row.kind === "work-toggle")).toMatchObject({
- hiddenCount: 2,
- summary: null,
+ hiddenCount: statuses.some((status) => status === "error") ? 2 : 3,
+ summary: statuses.some((status) => status === "error")
+ ? "Received 1 update and used 1 tool"
+ : "Used 2 tools and received 1 update",
hasFailure,
});
+ if (statuses.some((status) => status === "error")) {
+ expect(rows[0]).toMatchObject({
+ kind: "work",
+ groupedEntries: [{ tone: "error", label: "Command failed" }],
+ });
+ }
},
);
});
@@ -1557,7 +1543,6 @@ describe("computeStableMessagesTimelineRows", () => {
},
],
isWorking: false,
- activeTurnStartedAt: null,
turnDiffSummaryByAssistantMessageId: new Map(),
revertTurnCountByUserMessageId: new Map(),
});
@@ -1606,7 +1591,6 @@ describe("computeStableMessagesTimelineRows", () => {
},
],
isWorking: false,
- activeTurnStartedAt: null,
turnDiffSummaryByAssistantMessageId: new Map(),
revertTurnCountByUserMessageId: new Map(),
});
@@ -1662,7 +1646,6 @@ describe("computeStableMessagesTimelineRows", () => {
},
],
isWorking: false,
- activeTurnStartedAt: null,
turnDiffSummaryByAssistantMessageId: new Map(),
revertTurnCountByUserMessageId: new Map(),
});
diff --git a/apps/web/src/components/chat/MessagesTimeline.logic.ts b/apps/web/src/components/chat/MessagesTimeline.logic.ts
index d398583430f55ecba7f81961d499f4222b991024..895141f2bef143f1bb30a61611a85c41a099b5d4 100644
--- a/apps/web/src/components/chat/MessagesTimeline.logic.ts
+++ b/apps/web/src/components/chat/MessagesTimeline.logic.ts
@@ -11,7 +11,6 @@ import {
import { type ChatMessage, type ProposedPlan, type TurnDiffSummary } from "../../types";
import { type MessageId, type OrchestrationLatestTurn, type TurnId } from "@t3tools/contracts";
-export const MAX_VISIBLE_WORK_LOG_ENTRIES = 1;
export const TIMELINE_MINIMAP_ITEM_SPACING = 8;
export const TIMELINE_MINIMAP_MIN_ITEMS = 2;
export const TIMELINE_MINIMAP_MAX_HEIGHT_CSS = "calc(100vh - 18rem)";
@@ -202,9 +201,8 @@ export type MessagesTimelineRow =
groupId: string;
hiddenCount: number;
expanded: boolean;
- onlyToolEntries: boolean;
- summary: string | null;
- summaryKind: ToolGroupSummaryKind | null;
+ summary: string;
+ summaryKind: ToolGroupSummaryKind;
hasFailure: boolean;
}
| {
@@ -238,12 +236,6 @@ export type MessagesTimelineRow =
id: string;
createdAt: string;
turnPlan: TurnPlanEntry;
- }
- | {
- kind: "working";
- id: string;
- createdAt: string | null;
- showThinking: boolean;
};
export interface StableMessagesTimelineRowsState {
@@ -274,7 +266,7 @@ export function normalizeCompactToolLabel(value: string): string {
return value.replace(/\s+(?:complete|completed)\s*$/i, "").trim();
}
-type ToolGroupAction = "read" | "edit" | "command" | "code-search" | "search" | "other";
+type ToolGroupAction = "read" | "edit" | "command" | "code-search" | "search" | "other" | "update";
type ToolGroupSummaryKind = ToolGroupAction | "dynamic-tool" | "agent-tool" | "tone-tool" | "mixed";
export function workLogEntryIsLocalCodeSearch(entry: WorkLogEntry): boolean {
@@ -304,7 +296,7 @@ export function toolGroupAction(entry: WorkLogEntry): ToolGroupAction {
}
if (workLogEntryIsLocalCodeSearch(entry)) return "code-search";
if (entry.itemType === "web_search") return "search";
- return "other";
+ return workLogEntryIsToolLike(entry) ? "other" : "update";
}
function toolGroupActionCount(
@@ -339,6 +331,8 @@ function toolGroupActionLabel(action: ToolGroupAction, count: number): string {
return `Searched code ${count} ${count === 1 ? "time" : "times"}`;
case "other":
return `Used ${count} ${count === 1 ? "tool" : "tools"}`;
+ case "update":
+ return `Received ${count} ${count === 1 ? "update" : "updates"}`;
}
}
@@ -664,7 +658,6 @@ export function deriveMessagesTimelineRows(input: {
expandedTurnIds?: ReadonlySet;
expandedWorkGroupIds?: ReadonlySet;
isWorking: boolean;
- activeTurnStartedAt: string | null;
turnDiffSummaryByAssistantMessageId: ReadonlyMap;
revertTurnCountByUserMessageId: ReadonlyMap;
}): MessagesTimelineRow[] {
@@ -714,26 +707,6 @@ export function deriveMessagesTimelineRows(input: {
unsettledTurnId !== null &&
entry.toolLifecycleStatus === "inProgress" &&
entry.turnId === unsettledTurnId;
- const isVisibleActiveToolEntry = (entry: WorkLogEntry) =>
- workLogEntryIsToolLike(entry) && workEntryIsVisibleInGroup(entry, true);
- const activeEntries = input.isWorking
- ? input.timelineEntries.filter((entry, index) => entryBelongsToActiveTurn(entry, index))
- : [];
- const activeTurnHasVisibleContent = activeEntries.some((entry) => {
- if (entry.kind === "message") {
- return entry.message.role === "assistant" && (entry.message.text?.trim().length ?? 0) > 0;
- }
- if (entry.kind === "work") {
- return (
- entry.entry.agentSpawn === undefined &&
- workLogEntryIsToolLike(entry.entry) &&
- entry.entry.toolLifecycleStatus === "inProgress"
- );
- }
- if (entry.kind === "proposed-plan" || entry.kind === "turn-plan") return true;
- return false;
- });
-
const activeToolEntries: Array> = [];
for (let index = input.timelineEntries.length - 1; index >= activeTurnHeaderIndex; index -= 1) {
const entry = input.timelineEntries[index]!;
@@ -741,8 +714,7 @@ export function deriveMessagesTimelineRows(input: {
!entryBelongsToActiveTurn(entry, index) ||
entry.kind !== "work" ||
entry.entry.agentSpawn !== undefined ||
- entry.entry.tone === "error" ||
- !workLogEntryIsToolLike(entry.entry)
+ entry.entry.tone === "error"
) {
break;
}
@@ -750,7 +722,7 @@ export function deriveMessagesTimelineRows(input: {
}
const activeWorkEntryIds = new Set(activeToolEntries.map((entry) => entry.id));
const visibleActiveToolEntries = omitSupersededLifecycleMarkers(
- activeToolEntries.filter((entry) => isVisibleActiveToolEntry(entry.entry)),
+ activeToolEntries.filter((entry) => workEntryIsVisibleInGroup(entry.entry, true)),
(entry) => entry.entry,
);
const activeWorkAnchor = activeToolEntries[0];
@@ -771,14 +743,6 @@ export function deriveMessagesTimelineRows(input: {
};
})()
: null;
- const appendWorkingRow = () => {
- nextRows.push({
- kind: "working",
- id: "working-indicator-row",
- createdAt: input.activeTurnStartedAt,
- showThinking: activeWorkRow === null && !activeTurnHasVisibleContent,
- });
- };
const appendActiveWorkRows = () => {
if (activeWorkRow === null) return;
nextRows.push(activeWorkRow);
@@ -801,10 +765,6 @@ export function deriveMessagesTimelineRows(input: {
continue;
}
- if (input.isWorking && index === activeTurnHeaderIndex) {
- appendWorkingRow();
- }
-
if (timelineEntry.id === activeWorkPlacementEntryId) {
appendActiveWorkRows();
}
@@ -830,6 +790,17 @@ export function deriveMessagesTimelineRows(input: {
}
if (timelineEntry.kind === "work") {
+ if (timelineEntry.entry.agentSpawn !== undefined || timelineEntry.entry.tone === "error") {
+ nextRows.push({
+ kind: "work",
+ id: timelineEntry.id,
+ createdAt: timelineEntry.createdAt,
+ groupedEntries: [timelineEntry.entry],
+ isExpandedToolGroupEntry: false,
+ isLastExpandedToolGroupEntry: false,
+ });
+ continue;
+ }
const groupedEntries = [timelineEntry.entry];
let cursor = index + 1;
while (cursor < input.timelineEntries.length) {
@@ -837,6 +808,8 @@ export function deriveMessagesTimelineRows(input: {
if (
!nextEntry ||
nextEntry.kind !== "work" ||
+ nextEntry.entry.agentSpawn !== undefined ||
+ nextEntry.entry.tone === "error" ||
activeWorkEntryIds.has(nextEntry.id) ||
collapsedEntryIds.has(nextEntry.id) ||
foldsByAnchorEntryId.has(nextEntry.id)
@@ -853,14 +826,8 @@ export function deriveMessagesTimelineRows(input: {
(entry) => entry,
);
if (visibleGroupedEntries.length > 0) {
- const onlyToolEntries = visibleGroupedEntries.every(
- (entry) =>
- workLogEntryIsToolLike(entry) &&
- entry.agentSpawn === undefined &&
- entry.tone !== "error",
- );
const activeInProgressToolEntries = visibleGroupedEntries.filter(workEntryIsInActiveRun);
- if (onlyToolEntries && activeInProgressToolEntries.length > 0) {
+ if (activeInProgressToolEntries.length > 0) {
const groupId = workGroupId(timelineEntry.id, timelineEntry.entry);
const expanded = input.expandedWorkGroupIds?.has(groupId) ?? false;
const latestActiveToolEntry = activeInProgressToolEntries.at(-1)!;
@@ -885,10 +852,11 @@ export function deriveMessagesTimelineRows(input: {
});
}
}
- } else if (onlyToolEntries) {
+ } else {
const groupId = workGroupId(timelineEntry.id, timelineEntry.entry);
const expanded = input.expandedWorkGroupIds?.has(groupId) ?? false;
const summaryKind = toolGroupSummaryKind(visibleGroupedEntries);
+ const latestToolEntry = visibleGroupedEntries.findLast(workLogEntryIsToolLike);
nextRows.push({
kind: "work-toggle",
id: `work-toggle:${timelineEntry.id}`,
@@ -896,10 +864,15 @@ export function deriveMessagesTimelineRows(input: {
groupId,
hiddenCount: visibleGroupedEntries.length,
expanded,
- onlyToolEntries: true,
- summary: summarizeToolGroup(visibleGroupedEntries),
+ summary:
+ visibleGroupedEntries.length === 1 &&
+ !workLogEntryIsToolLike(visibleGroupedEntries[0]!)
+ ? visibleGroupedEntries[0]!.label
+ : summarizeToolGroup(visibleGroupedEntries),
summaryKind,
- hasFailure: workEntryDisplayIndicatesToolFailure(visibleGroupedEntries.at(-1)!),
+ hasFailure:
+ latestToolEntry !== undefined &&
+ workEntryDisplayIndicatesToolFailure(latestToolEntry),
});
if (expanded) {
for (const [entryIndex, workEntry] of visibleGroupedEntries.entries()) {
@@ -913,64 +886,6 @@ export function deriveMessagesTimelineRows(input: {
});
}
}
- } else if (visibleGroupedEntries.length <= MAX_VISIBLE_WORK_LOG_ENTRIES) {
- nextRows.push({
- kind: "work",
- id: timelineEntry.id,
- createdAt: timelineEntry.createdAt,
- groupedEntries: visibleGroupedEntries,
- isExpandedToolGroupEntry: false,
- isLastExpandedToolGroupEntry: false,
- });
- } else {
- const groupId = workGroupId(timelineEntry.id, timelineEntry.entry);
- const expanded = input.expandedWorkGroupIds?.has(groupId) ?? false;
- // Agent-spawn CTA rows are always visible: a running fleet must
- // never hide behind a "+N tool calls" toggle. Selection is by
- // membership (spawn OR recent-tail), preserving the group's
- // chronological order in both collapsed and expanded states
- // (review finding: concatenating two filtered lists moved a
- // mid-group spawn row above earlier tool rows).
- const overflowCandidates = visibleGroupedEntries.filter(
- (entry) => entry.agentSpawn === undefined,
- );
- const hiddenEntries = overflowCandidates.slice(0, -MAX_VISIBLE_WORK_LOG_ENTRIES);
- const hiddenIds = new Set(hiddenEntries.map((entry) => entry.id));
- const visibleEntries = visibleGroupedEntries.filter(
- (entry) => entry.agentSpawn !== undefined || !hiddenIds.has(entry.id),
- );
- const renderedEntries = expanded ? visibleGroupedEntries : visibleEntries;
-
- for (const workEntry of renderedEntries) {
- nextRows.push({
- kind: "work",
- id: workEntry.id,
- createdAt: workEntry.createdAt,
- groupedEntries: [workEntry],
- isExpandedToolGroupEntry: false,
- isLastExpandedToolGroupEntry: false,
- });
- }
-
- if (hiddenEntries.length > 0) {
- const latestToolEntry = visibleGroupedEntries.findLast(workLogEntryIsToolLike);
-
- nextRows.push({
- kind: "work-toggle",
- id: `work-toggle:${timelineEntry.id}`,
- createdAt: timelineEntry.createdAt,
- groupId,
- hiddenCount: hiddenEntries.length,
- expanded,
- onlyToolEntries: hiddenEntries.every(workLogEntryIsToolLike),
- summary: null,
- summaryKind: null,
- hasFailure:
- latestToolEntry !== undefined &&
- workEntryDisplayIndicatesToolFailure(latestToolEntry) &&
- hiddenEntries.some(workEntryDisplayIndicatesToolFailure),
- });
- }
}
}
index = cursor - 1;
@@ -1033,10 +948,6 @@ export function deriveMessagesTimelineRows(input: {
});
}
- if (input.isWorking && activeTurnHeaderIndex === input.timelineEntries.length) {
- appendWorkingRow();
- }
-
return nextRows;
}
@@ -1065,11 +976,6 @@ function isRowUnchanged(a: MessagesTimelineRow, b: MessagesTimelineRow): boolean
if (a.kind !== b.kind || a.id !== b.id) return false;
switch (a.kind) {
- case "working":
- return (
- a.createdAt === (b as typeof a).createdAt && a.showThinking === (b as typeof a).showThinking
- );
-
case "turn-fold": {
const bf = b as typeof a;
return a.createdAt === bf.createdAt && a.label === bf.label && a.expanded === bf.expanded;
@@ -1112,7 +1018,6 @@ function isRowUnchanged(a: MessagesTimelineRow, b: MessagesTimelineRow): boolean
a.groupId === bw.groupId &&
a.hiddenCount === bw.hiddenCount &&
a.expanded === bw.expanded &&
- a.onlyToolEntries === bw.onlyToolEntries &&
a.summary === bw.summary &&
a.summaryKind === bw.summaryKind &&
a.hasFailure === bw.hasFailure
diff --git a/apps/web/src/components/chat/MessagesTimeline.test.tsx b/apps/web/src/components/chat/MessagesTimeline.test.tsx
index 4c7cf29969d9612f721542e59c9b720f07852635..67060a28d64795ce7e6fcf57e7ebb62f2539102f 100644
--- a/apps/web/src/components/chat/MessagesTimeline.test.tsx
+++ b/apps/web/src/components/chat/MessagesTimeline.test.tsx
@@ -177,7 +177,6 @@ const MESSAGE_CREATED_AT = "2026-03-17T19:12:28.000Z";
function buildProps() {
return {
isWorking: false,
- activeTurnStartedAt: null,
listRef: createRef(),
latestTurn: null,
runningTurnId: null,
@@ -675,7 +674,6 @@ describe("MessagesTimeline", () => {
{
);
expect(markup).toContain("Context compacted");
- expect(markup).toContain("Work Log");
});
it("summarizes changed files in one line", () => {
@@ -1172,7 +1169,7 @@ describe("MessagesTimeline", () => {
/>,
);
- expect(markup).toContain("+2 previous log entries");
+ expect(markup).toContain("Ran 2 commands and received 1 update");
expect(markup).not.toContain('aria-label="Hidden work includes a failure"');
});
@@ -1182,7 +1179,6 @@ describe("MessagesTimeline", () => {
{
/>,
);
- expect(markup).toContain("Working for");
expect(markup).toContain("Running pnpm");
expect(markup).toContain("live-activity-focus");
});
@@ -1222,7 +1217,6 @@ describe("MessagesTimeline", () => {
{
{
expect(markup).toContain("tool call failed");
});
- it("aligns the iconless Thinking row with the working timer", () => {
- const markup = renderToStaticMarkup(
- ,
- );
-
- expect(markup).toContain("Working for");
- expect(markup).toContain("Thinking");
- expect(markup).toContain("gap-1.5 py-0.5 px-1");
- });
-
it("renders review comment contexts as structured cards instead of raw tags", () => {
const markup = renderToStaticMarkup(
{
expect(markup).not.toContain('data-testid="file-diff"');
});
- it("renders a muted failure marker for failed tool lifecycle entries", () => {
+ it("keeps failed lifecycle entries discoverable in mixed activity summaries", () => {
const markup = renderToStaticMarkup(
{
/>,
);
- expect(markup).toContain("lucide-x");
- expect(markup).toContain('aria-label="Tool call failed"');
+ expect(markup).toContain('aria-label="Received 1 update and used 1 tool, tool call failed"');
// Ordinary tool failures render muted, not red.
expect(markup).not.toContain("text-destructive");
});
diff --git a/apps/web/src/components/chat/MessagesTimeline.tsx b/apps/web/src/components/chat/MessagesTimeline.tsx
index dc0c03709e6ca0c3c349c27332551d640c4f1728..d8c88c088648519be0192e90ec818c4a87b1bd27 100644
--- a/apps/web/src/components/chat/MessagesTimeline.tsx
+++ b/apps/web/src/components/chat/MessagesTimeline.tsx
@@ -138,7 +138,7 @@ import {
// Context — shared state consumed by every row component via Context.
// Propagates through LegendList's memo boundaries for shared callbacks and
// non-row-scoped state. `nowIso` is intentionally excluded — self-ticking
-// components (WorkingTimer, LiveElapsed) handle it.
+// components (LiveElapsed) handle it.
// ---------------------------------------------------------------------------
interface TimelineRowSharedState {
@@ -164,8 +164,6 @@ interface TimelineRowActivityState {
isWorking: boolean;
isRevertingCheckpoint: boolean;
latestTurnId: TurnId | null;
- /** Current plan step label for the working row, when the turn has a plan. */
- workingStepLabel: string | null;
}
const TimelineRowCtx = createContext(null!);
@@ -218,8 +216,6 @@ interface MessagesTimelineProps {
agentPanelModel?: AgentPanelModel;
onOpenAgents?: () => void;
isWorking: boolean;
- workingStepLabel?: string | null;
- activeTurnStartedAt: string | null;
listRef: React.RefObject;
timelineEntries: ReturnType;
latestTurn: TimelineLatestTurn | null;
@@ -262,8 +258,6 @@ interface MessagesTimelineProps {
export const MessagesTimeline = memo(function MessagesTimeline({
isWorking,
- workingStepLabel = null,
- activeTurnStartedAt,
agentPanelModel = EMPTY_AGENT_PANEL_MODEL,
onOpenAgents = NOOP_OPEN_AGENTS,
listRef,
@@ -426,7 +420,6 @@ export const MessagesTimeline = memo(function MessagesTimeline({
expandedTurnIds,
expandedWorkGroupIds,
isWorking,
- activeTurnStartedAt,
turnDiffSummaryByAssistantMessageId,
revertTurnCountByUserMessageId,
}),
@@ -437,7 +430,6 @@ export const MessagesTimeline = memo(function MessagesTimeline({
expandedTurnIds,
expandedWorkGroupIds,
isWorking,
- activeTurnStartedAt,
turnDiffSummaryByAssistantMessageId,
revertTurnCountByUserMessageId,
],
@@ -566,9 +558,8 @@ export const MessagesTimeline = memo(function MessagesTimeline({
isWorking,
isRevertingCheckpoint,
latestTurnId: latestTurn?.turnId ?? null,
- workingStepLabel,
}),
- [isRevertingCheckpoint, isWorking, latestTurn?.turnId, workingStepLabel],
+ [isRevertingCheckpoint, isWorking, latestTurn?.turnId],
);
// Stable renderItem — no closure deps. Row components read shared state
@@ -946,8 +937,7 @@ const TimelineRowContent = memo(function TimelineRowContent({ row }: { row: Time
const isExpandedToolGroupEntry = row.kind === "work" && row.isExpandedToolGroupEntry;
const isLastExpandedToolGroupEntry = row.kind === "work" && row.isLastExpandedToolGroupEntry;
const isExpandedToolGroupHeader =
- (row.kind === "work-toggle" && row.summary !== null && row.onlyToolEntries && row.expanded) ||
- (row.kind === "work-live" && row.expanded);
+ (row.kind === "work-toggle" && row.expanded) || (row.kind === "work-live" && row.expanded);
return (
: null}
{row.kind === "turn-plan" ? : null}
- {row.kind === "working" ? : null}
);
});
@@ -1277,7 +1266,7 @@ function ProposedPlanTimelineRow({
/**
* Inline folded plan chip: one row per turn that produced plan/todo steps.
- * Collapsed by default — a segment bar plus the in-progress step label —
+ * Collapsed by default, with a segment bar and the current step label,
* and expands in place to the full step list. Replaces the old plan sidebar.
*/
const TurnPlanTimelineRow = memo(function TurnPlanTimelineRow({
@@ -1299,15 +1288,19 @@ const TurnPlanTimelineRow = memo(function TurnPlanTimelineRow({
const Chevron = expanded ? ChevronDownIcon : ChevronRightIcon;
return (
-