Skip to content

Commit 998484e

Browse files
committed
Show open diff action when file summary is unavailable
- Keep turn diff controls visible even when file summaries are empty - Add fallback copy and tests for the empty-summary state
1 parent fbe55c9 commit 998484e

2 files changed

Lines changed: 171 additions & 26 deletions

File tree

apps/web/src/components/chat/MessagesTimeline.test.tsx

Lines changed: 127 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { MessageId } from "@okcode/contracts";
1+
import { MessageId, TurnId } from "@okcode/contracts";
22
import type { ReactElement } from "react";
33
import { renderToStaticMarkup } from "react-dom/server";
44
import { beforeAll, describe, expect, it, vi } from "vitest";
@@ -201,4 +201,130 @@ describe("MessagesTimeline", () => {
201201
expect(markup).toContain("Manage hotkeys");
202202
expect(markup).toContain("No shortcut assigned");
203203
});
204+
205+
it("renders an open diff action when a turn diff summary has files", async () => {
206+
const { MessagesTimeline } = await import("./MessagesTimeline");
207+
const assistantMessageId = MessageId.makeUnsafe("assistant-1");
208+
const markup = renderWithI18n(
209+
<MessagesTimeline
210+
threadId={"thread-1" as never}
211+
hasMessages
212+
isWorking={false}
213+
activeTurnInProgress={false}
214+
activeTurnStartedAt={null}
215+
scrollContainer={null}
216+
timelineEntries={[
217+
{
218+
id: "entry-1",
219+
kind: "message",
220+
createdAt: "2026-03-17T19:12:28.000Z",
221+
message: {
222+
id: assistantMessageId,
223+
role: "assistant",
224+
text: "Updated the repo.",
225+
createdAt: "2026-03-17T19:12:28.000Z",
226+
completedAt: "2026-03-17T19:12:30.000Z",
227+
streaming: false,
228+
},
229+
},
230+
]}
231+
completionDividerBeforeEntryId={null}
232+
completionSummary={null}
233+
turnDiffSummaryByAssistantMessageId={
234+
new Map([
235+
[
236+
assistantMessageId,
237+
{
238+
turnId: TurnId.makeUnsafe("turn-1"),
239+
completedAt: "2026-03-17T19:12:30.000Z",
240+
files: [{ path: "src/index.ts", additions: 1, deletions: 0 }],
241+
},
242+
],
243+
])
244+
}
245+
nowIso="2026-03-17T19:12:30.000Z"
246+
expandedWorkGroups={{}}
247+
onToggleWorkGroup={() => {}}
248+
revertTurnCountByUserMessageId={new Map()}
249+
onRevertUserMessage={() => {}}
250+
isRevertingCheckpoint={false}
251+
onImageExpand={() => {}}
252+
markdownCwd={undefined}
253+
resolvedTheme="light"
254+
showReasoningContent={false}
255+
timestampFormat="locale"
256+
workspaceRoot={undefined}
257+
onRemoveQueuedMessage={() => {}}
258+
shortcutGuides={EMPTY_SHORTCUT_GUIDES}
259+
onOpenSettings={() => {}}
260+
onOpenTurnDiff={() => {}}
261+
/>,
262+
);
263+
264+
expect(markup).toContain("Open diff");
265+
expect(markup).toContain("Changed files (1)");
266+
});
267+
268+
it("renders an open diff action when a turn diff exists but the file summary is empty", async () => {
269+
const { MessagesTimeline } = await import("./MessagesTimeline");
270+
const assistantMessageId = MessageId.makeUnsafe("assistant-2");
271+
const markup = renderWithI18n(
272+
<MessagesTimeline
273+
threadId={"thread-1" as never}
274+
hasMessages
275+
isWorking={false}
276+
activeTurnInProgress={false}
277+
activeTurnStartedAt={null}
278+
scrollContainer={null}
279+
timelineEntries={[
280+
{
281+
id: "entry-1",
282+
kind: "message",
283+
createdAt: "2026-03-17T19:12:28.000Z",
284+
message: {
285+
id: assistantMessageId,
286+
role: "assistant",
287+
text: "Updated the repo.",
288+
createdAt: "2026-03-17T19:12:28.000Z",
289+
completedAt: "2026-03-17T19:12:30.000Z",
290+
streaming: false,
291+
},
292+
},
293+
]}
294+
completionDividerBeforeEntryId={null}
295+
completionSummary={null}
296+
turnDiffSummaryByAssistantMessageId={
297+
new Map([
298+
[
299+
assistantMessageId,
300+
{
301+
turnId: TurnId.makeUnsafe("turn-2"),
302+
completedAt: "2026-03-17T19:12:30.000Z",
303+
files: [],
304+
},
305+
],
306+
])
307+
}
308+
nowIso="2026-03-17T19:12:30.000Z"
309+
expandedWorkGroups={{}}
310+
onToggleWorkGroup={() => {}}
311+
revertTurnCountByUserMessageId={new Map()}
312+
onRevertUserMessage={() => {}}
313+
isRevertingCheckpoint={false}
314+
onImageExpand={() => {}}
315+
markdownCwd={undefined}
316+
resolvedTheme="light"
317+
showReasoningContent={false}
318+
timestampFormat="locale"
319+
workspaceRoot={undefined}
320+
onRemoveQueuedMessage={() => {}}
321+
shortcutGuides={EMPTY_SHORTCUT_GUIDES}
322+
onOpenSettings={() => {}}
323+
onOpenTurnDiff={() => {}}
324+
/>,
325+
);
326+
327+
expect(markup).toContain("Open diff");
328+
expect(markup).toContain("Diff available");
329+
});
204330
});

apps/web/src/components/chat/MessagesTimeline.tsx

Lines changed: 44 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -563,7 +563,6 @@ export const MessagesTimeline = memo(function MessagesTimeline({
563563
const turnSummary = turnDiffSummaryByAssistantMessageId.get(row.message.id);
564564
if (!turnSummary) return null;
565565
const checkpointFiles = turnSummary.files;
566-
if (checkpointFiles.length === 0) return null;
567566
const summaryStat = summarizeTurnDiffStats(checkpointFiles);
568567
const changedFileCountLabel = String(checkpointFiles.length);
569568
const allDirectoriesExpanded =
@@ -573,31 +572,46 @@ export const MessagesTimeline = memo(function MessagesTimeline({
573572
return (
574573
<div className="mt-2 rounded-lg border border-border/80 bg-card/45 p-2.5">
575574
<div className="flex items-center justify-between gap-2">
576-
<button
577-
type="button"
578-
className="group flex items-center gap-1.5 text-[10px] uppercase tracking-[0.12em] text-muted-foreground/65 hover:text-muted-foreground/90 transition-colors duration-150"
579-
onClick={() => onToggleFileSection(turnSummary.turnId)}
580-
>
581-
<ChevronRightIcon
582-
aria-hidden="true"
583-
className={cn(
584-
"size-3 shrink-0 transition-transform duration-150",
585-
!isFileSectionCollapsed && "rotate-90",
575+
{checkpointFiles.length > 0 ? (
576+
<button
577+
type="button"
578+
className="group flex items-center gap-1.5 text-[10px] uppercase tracking-[0.12em] text-muted-foreground/65 transition-colors duration-150 hover:text-muted-foreground/90"
579+
onClick={() => onToggleFileSection(turnSummary.turnId)}
580+
>
581+
<ChevronRightIcon
582+
aria-hidden="true"
583+
className={cn(
584+
"size-3 shrink-0 transition-transform duration-150",
585+
!isFileSectionCollapsed && "rotate-90",
586+
)}
587+
/>
588+
<span>Changed files ({changedFileCountLabel})</span>
589+
{hasNonZeroStat(summaryStat) && (
590+
<>
591+
<span className="mx-1"></span>
592+
<DiffStatLabel
593+
additions={summaryStat.additions}
594+
deletions={summaryStat.deletions}
595+
/>
596+
</>
586597
)}
587-
/>
588-
<span>Changed files ({changedFileCountLabel})</span>
589-
{hasNonZeroStat(summaryStat) && (
590-
<>
591-
<span className="mx-1"></span>
592-
<DiffStatLabel
593-
additions={summaryStat.additions}
594-
deletions={summaryStat.deletions}
595-
/>
596-
</>
597-
)}
598-
</button>
598+
</button>
599+
) : (
600+
<div className="flex items-center gap-1.5 text-[10px] uppercase tracking-[0.12em] text-muted-foreground/65">
601+
<EyeIcon className="size-3 shrink-0" />
602+
<span>Diff available</span>
603+
</div>
604+
)}
599605
<div className="flex items-center gap-1.5">
600-
{!isFileSectionCollapsed && (
606+
<Button
607+
type="button"
608+
size="xs"
609+
variant="outline"
610+
onClick={() => onOpenTurnDiff(turnSummary.turnId)}
611+
>
612+
Open diff
613+
</Button>
614+
{checkpointFiles.length > 0 && !isFileSectionCollapsed && (
601615
<Button
602616
type="button"
603617
size="xs"
@@ -609,7 +623,7 @@ export const MessagesTimeline = memo(function MessagesTimeline({
609623
)}
610624
</div>
611625
</div>
612-
{!isFileSectionCollapsed && (
626+
{checkpointFiles.length > 0 && !isFileSectionCollapsed && (
613627
<div className="mt-1.5">
614628
<ChangedFilesTree
615629
key={`changed-files-tree:${turnSummary.turnId}`}
@@ -622,6 +636,11 @@ export const MessagesTimeline = memo(function MessagesTimeline({
622636
/>
623637
</div>
624638
)}
639+
{checkpointFiles.length === 0 && (
640+
<p className="mt-1.5 text-xs text-muted-foreground/75">
641+
Open the diff to inspect changes when the file summary is unavailable.
642+
</p>
643+
)}
625644
</div>
626645
);
627646
})()}

0 commit comments

Comments
 (0)