Skip to content
Open
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
114 changes: 94 additions & 20 deletions apps/web/src/components/chat/MessagesTimeline.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -43,15 +43,34 @@ beforeAll(() => {
});

describe("MessagesTimeline", () => {
const baseProps = {
hasMessages: true,
isWorking: false,
activeTurnInProgress: false,
activeTurnStartedAt: null,
scrollContainer: null,
completionDividerBeforeEntryId: null,
completionSummary: null,
turnDiffSummaryByAssistantMessageId: new Map(),
nowIso: "2026-03-17T19:12:30.000Z",
expandedWorkGroups: {},
onToggleWorkGroup: () => {},
onOpenTurnDiff: () => {},
revertTurnCountByUserMessageId: new Map(),
onRevertUserMessage: () => {},
isRevertingCheckpoint: false,
onImageExpand: () => {},
markdownCwd: undefined,
resolvedTheme: "light" as const,
timestampFormat: "locale" as const,
workspaceRoot: undefined,
};

it("renders inline terminal labels with the composer chip UI", async () => {
const { MessagesTimeline } = await import("./MessagesTimeline");
const markup = renderToStaticMarkup(
<MessagesTimeline
hasMessages
isWorking={false}
activeTurnInProgress={false}
activeTurnStartedAt={null}
scrollContainer={null}
{...baseProps}
timelineEntries={[
{
id: "entry-1",
Expand All @@ -74,26 +93,81 @@ describe("MessagesTimeline", () => {
},
},
]}
completionDividerBeforeEntryId={null}
completionSummary={null}
turnDiffSummaryByAssistantMessageId={new Map()}
nowIso="2026-03-17T19:12:30.000Z"
expandedWorkGroups={{}}
onToggleWorkGroup={() => {}}
onOpenTurnDiff={() => {}}
revertTurnCountByUserMessageId={new Map()}
onRevertUserMessage={() => {}}
isRevertingCheckpoint={false}
onImageExpand={() => {}}
markdownCwd={undefined}
resolvedTheme="light"
timestampFormat="locale"
workspaceRoot={undefined}
/>,
);

expect(markup).toContain("Terminal 1 lines 1-5");
expect(markup).toContain("lucide-terminal");
expect(markup).toContain("yoo what&#x27;s ");
});

it("renders tool call groups collapsed by default", async () => {
const { MessagesTimeline } = await import("./MessagesTimeline");
const markup = renderToStaticMarkup(
<MessagesTimeline
{...baseProps}
timelineEntries={[
{
id: "work-group-1",
kind: "work",
createdAt: "2026-03-17T19:12:28.000Z",
entry: {
id: "work-entry-1",
createdAt: "2026-03-17T19:12:28.000Z",
label: "Ran command",
tone: "tool",
command: "bun lint",
},
},
]}
/>,
);

expect(markup).toContain("Tool calls (1)");
expect(markup).toContain("lucide-chevron-down");
expect(markup).not.toContain("Ran command");
expect(markup).not.toContain("bun lint");
});

it("renders expanded work log groups with their entries", async () => {
const { MessagesTimeline } = await import("./MessagesTimeline");
const markup = renderToStaticMarkup(
<MessagesTimeline
{...baseProps}
expandedWorkGroups={{ "work-group-1": true }}
timelineEntries={[
{
id: "work-group-1",
kind: "work",
createdAt: "2026-03-17T19:12:28.000Z",
entry: {
id: "work-entry-1",
createdAt: "2026-03-17T19:12:28.000Z",
label: "Approval resolved",
tone: "info",
detail: "Command approved",
},
},
{
id: "work-group-2",
kind: "work",
createdAt: "2026-03-17T19:12:29.000Z",
entry: {
id: "work-entry-2",
createdAt: "2026-03-17T19:12:29.000Z",
label: "Tool call",
tone: "tool",
detail: "Read: /tmp/app.ts",
},
},
]}
/>,
);

expect(markup).toContain("Work log (2)");
expect(markup).toContain("Approval resolved");
expect(markup).toContain("Command approved");
expect(markup).toContain("Tool call");
expect(markup).toContain("Read: /tmp/app.ts");
});
});
51 changes: 26 additions & 25 deletions apps/web/src/components/chat/MessagesTimeline.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import ChatMarkdown from "../ChatMarkdown";
import {
BotIcon,
CheckIcon,
ChevronDownIcon,
CircleAlertIcon,
EyeIcon,
GlobeIcon,
Expand All @@ -43,6 +44,7 @@ import { DiffStatLabel, hasNonZeroStat } from "./DiffStatLabel";
import { MessageCopyButton } from "./MessageCopyButton";
import { computeMessageDurationStart, normalizeCompactToolLabel } from "./MessagesTimeline.logic";
import { TerminalContextInlineChip } from "./TerminalContextInlineChip";
import { Collapsible, CollapsibleContent, CollapsibleTrigger } from "../ui/collapsible";
import {
deriveDisplayedUserMessageState,
type ParsedTerminalContextEntry,
Expand All @@ -56,7 +58,6 @@ import {
textContainsInlineTerminalContextLabels,
} from "./userMessageTerminalContexts";

const MAX_VISIBLE_WORK_LOG_ENTRIES = 6;
const ALWAYS_UNVIRTUALIZED_TAIL_ROWS = 8;

interface MessagesTimelineProps {
Expand Down Expand Up @@ -315,40 +316,40 @@ export const MessagesTimeline = memo(function MessagesTimeline({
const groupId = row.id;
const groupedEntries = row.groupedEntries;
const isExpanded = expandedWorkGroups[groupId] ?? false;
const hasOverflow = groupedEntries.length > MAX_VISIBLE_WORK_LOG_ENTRIES;
const visibleEntries =
hasOverflow && !isExpanded
? groupedEntries.slice(-MAX_VISIBLE_WORK_LOG_ENTRIES)
: groupedEntries;
const hiddenCount = groupedEntries.length - visibleEntries.length;
const onlyToolEntries = groupedEntries.every((entry) => entry.tone === "tool");
const showHeader = hasOverflow || !onlyToolEntries;
const groupLabel = onlyToolEntries ? "Tool calls" : "Work log";

return (
<div className="rounded-xl border border-border/45 bg-card/25 px-2 py-1.5">
{showHeader && (
<div className="mb-1.5 flex items-center justify-between gap-2 px-0.5">
<Collapsible
className="rounded-xl border border-border/45 bg-card/25 px-2 py-1.5"
onOpenChange={() => onToggleWorkGroup(groupId)}
open={isExpanded}
>
<CollapsibleTrigger className="group mb-0.5 flex w-full items-center justify-between gap-2 rounded-md px-0.5 py-1 text-left outline-hidden focus-visible:ring-2 focus-visible:ring-ring">
<div className="min-w-0">
<p className="text-[9px] uppercase tracking-[0.16em] text-muted-foreground/55">
{groupLabel} ({groupedEntries.length})
</p>
{hasOverflow && (
<button
type="button"
className="text-[9px] uppercase tracking-[0.12em] text-muted-foreground/55 transition-colors duration-150 hover:text-foreground/75"
onClick={() => onToggleWorkGroup(groupId)}
>
{isExpanded ? "Show less" : `Show ${hiddenCount} more`}
</button>
</div>
<ChevronDownIcon
aria-hidden="true"
className={cn(
"size-3.5 shrink-0 text-muted-foreground/55 transition-[color,transform] duration-200 group-hover:text-foreground",
isExpanded ? "rotate-180" : "",
)}
/>
</CollapsibleTrigger>
<CollapsibleContent>
<div className="space-y-0.5 pt-1">
{groupedEntries.map((workEntry) => (
<SimpleWorkEntryRow key={`work-row:${workEntry.id}`} workEntry={workEntry} />
))}
</div>
)}
<div className="space-y-0.5">
{visibleEntries.map((workEntry) => (
<SimpleWorkEntryRow key={`work-row:${workEntry.id}`} workEntry={workEntry} />
))}
</CollapsibleContent>
<div className="sr-only" aria-live="polite">
{isExpanded ? `${groupLabel} expanded` : `${groupLabel} collapsed`}
</div>
</div>
</Collapsible>
);
})()}

Expand Down