Skip to content

Commit c8a04bd

Browse files
Finale: upgrade changed files card to fix various UI issues (#4113)
Co-authored-by: Julius Marminge <julius0216@outlook.com>
1 parent db4b2d8 commit c8a04bd

4 files changed

Lines changed: 201 additions & 60 deletions

File tree

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

Lines changed: 26 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,32 @@ import { TurnId } from "@t3tools/contracts";
22
import { renderToStaticMarkup } from "react-dom/server";
33
import { describe, expect, it } from "vite-plus/test";
44

5-
import { ChangedFilesTree } from "./ChangedFilesTree";
5+
import { ChangedFilesCard, ChangedFilesTree } from "./ChangedFilesTree";
6+
7+
describe("ChangedFilesCard", () => {
8+
it("keeps its compact header sticky while preserving singular labels", () => {
9+
const markup = renderToStaticMarkup(
10+
<ChangedFilesCard
11+
turnId={TurnId.make("turn-1")}
12+
files={[{ path: "README.md", kind: "modified", additions: 2, deletions: 1 }]}
13+
allDirectoriesExpanded
14+
resolvedTheme="light"
15+
onToggleAllDirectories={() => {}}
16+
onOpenTurnDiff={() => {}}
17+
/>,
18+
);
19+
20+
expect(markup).toContain('class="sticky top-0 z-10');
21+
expect(markup).not.toContain("self-start");
22+
expect(markup).toContain("whitespace-nowrap");
23+
expect(markup).toContain("!size-[22px]");
24+
expect(markup).toContain("size-3");
25+
expect(markup).toContain('aria-label="Collapse all"');
26+
expect(markup).toContain('aria-label="View diff"');
27+
expect(markup).toContain("1 changed file");
28+
expect(markup).not.toContain("1 changed files");
29+
});
30+
});
631

732
describe("ChangedFilesTree", () => {
833
it.each([

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

Lines changed: 64 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -6,11 +6,19 @@ import {
66
summarizeTurnDiffStats,
77
type TurnDiffTreeNode,
88
} from "../../lib/turnDiffTree";
9-
import { ChevronRightIcon, FolderIcon, FolderClosedIcon } from "lucide-react";
9+
import {
10+
ChevronsDownUpIcon,
11+
ChevronsUpDownIcon,
12+
ChevronRightIcon,
13+
FileDiffIcon,
14+
FolderIcon,
15+
FolderClosedIcon,
16+
} from "lucide-react";
1017
import { cn } from "~/lib/utils";
1118
import { DiffStatLabel, hasNonZeroStat } from "./DiffStatLabel";
1219
import { PierreEntryIcon } from "./PierreEntryIcon";
1320
import { Button } from "../ui/button";
21+
import { Tooltip, TooltipPopup, TooltipTrigger } from "../ui/tooltip";
1422

1523
const EMPTY_DIRECTORY_OVERRIDES: Record<string, boolean> = {};
1624

@@ -33,10 +41,12 @@ export const ChangedFilesCard = memo(function ChangedFilesCard(props: {
3341
const summaryStat = useMemo(() => summarizeTurnDiffStats(files), [files]);
3442

3543
return (
36-
<div className="relative mt-4 rounded-2xl bg-card/40 shadow-xs/5 not-dark:bg-clip-padding after:pointer-events-none after:absolute after:inset-0 after:z-20 after:rounded-2xl after:border after:border-input">
37-
<div className="sticky top-0 z-10 mb-3 flex items-center justify-between gap-2 rounded-t-2xl bg-card/72 p-3 backdrop-blur-md">
38-
<p className="flex items-center gap-1 font-medium text-foreground text-xs leading-4">
39-
<span>{files.length} changed files</span>
44+
<div className="mt-4 rounded-2xl border border-input bg-background p-2 pt-4 shadow-xs/5 not-dark:bg-clip-padding dark:bg-input/32">
45+
<div className="sticky top-0 z-10 mb-3 flex items-center justify-between gap-2 bg-background px-2 before:absolute before:inset-x-0 before:-top-4 before:h-4 before:bg-background before:content-[''] dark:bg-[color-mix(in_srgb,var(--foreground)_2.5%,var(--background))] dark:before:bg-[color-mix(in_srgb,var(--foreground)_2.5%,var(--background))]">
46+
<p className="flex items-center gap-1 whitespace-nowrap font-medium text-foreground text-xs leading-4">
47+
<span>
48+
{files.length} changed file{files.length === 1 ? "" : "s"}
49+
</span>
4050
{hasNonZeroStat(summaryStat) && (
4151
<DiffStatLabel
4252
additions={summaryStat.additions}
@@ -47,35 +57,57 @@ export const ChangedFilesCard = memo(function ChangedFilesCard(props: {
4757
)}
4858
</p>
4959
<div className="flex items-center gap-1.5">
50-
<Button
51-
type="button"
52-
size="xs"
53-
variant="outline"
54-
data-scroll-anchor-ignore
55-
onClick={onToggleAllDirectories}
56-
>
57-
{allDirectoriesExpanded ? "Collapse all" : "Expand all"}
58-
</Button>
59-
<Button
60-
type="button"
61-
size="xs"
62-
variant="outline"
63-
onClick={() => onOpenTurnDiff(turnId, files[0]?.path)}
64-
>
65-
View diff
66-
</Button>
60+
<Tooltip>
61+
<TooltipTrigger
62+
render={
63+
<Button
64+
type="button"
65+
size="icon-xs"
66+
variant="outline"
67+
className="!size-[22px]"
68+
aria-label={allDirectoriesExpanded ? "Collapse all" : "Expand all"}
69+
data-scroll-anchor-ignore
70+
onClick={onToggleAllDirectories}
71+
/>
72+
}
73+
>
74+
{allDirectoriesExpanded ? (
75+
<ChevronsDownUpIcon className="size-3" />
76+
) : (
77+
<ChevronsUpDownIcon className="size-3" />
78+
)}
79+
</TooltipTrigger>
80+
<TooltipPopup side="top">
81+
{allDirectoriesExpanded ? "Collapse all" : "Expand all"}
82+
</TooltipPopup>
83+
</Tooltip>
84+
<Tooltip>
85+
<TooltipTrigger
86+
render={
87+
<Button
88+
type="button"
89+
size="icon-xs"
90+
variant="outline"
91+
className="!size-[22px]"
92+
aria-label="View diff"
93+
onClick={() => onOpenTurnDiff(turnId, files[0]?.path)}
94+
/>
95+
}
96+
>
97+
<FileDiffIcon className="size-3" />
98+
</TooltipTrigger>
99+
<TooltipPopup side="top">View diff</TooltipPopup>
100+
</Tooltip>
67101
</div>
68102
</div>
69-
<div className="px-2 pb-2">
70-
<ChangedFilesTree
71-
key={`changed-files-tree:${turnId}`}
72-
turnId={turnId}
73-
files={files}
74-
allDirectoriesExpanded={allDirectoriesExpanded}
75-
resolvedTheme={resolvedTheme}
76-
onOpenTurnDiff={onOpenTurnDiff}
77-
/>
78-
</div>
103+
<ChangedFilesTree
104+
key={`changed-files-tree:${turnId}`}
105+
turnId={turnId}
106+
files={files}
107+
allDirectoriesExpanded={allDirectoriesExpanded}
108+
resolvedTheme={resolvedTheme}
109+
onOpenTurnDiff={onOpenTurnDiff}
110+
/>
79111
</div>
80112
);
81113
});

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

Lines changed: 53 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { EnvironmentId, MessageId } from "@t3tools/contracts";
1+
import { CheckpointRef, EnvironmentId, MessageId, TurnId } from "@t3tools/contracts";
22
import { createRef, type ReactNode, type Ref } from "react";
33
import { renderToStaticMarkup } from "react-dom/server";
44
import { beforeAll, describe, expect, it, vi } from "vite-plus/test";
@@ -219,6 +219,58 @@ function buildUserTimelineEntry(text: string) {
219219
}
220220

221221
describe("MessagesTimeline", () => {
222+
it("keeps assistant changed-files headers sticky below the thread header", async () => {
223+
const { MessagesTimeline } = await import("./MessagesTimeline");
224+
const assistantMessageId = MessageId.make("message-assistant-with-files");
225+
const turnId = TurnId.make("turn-with-files");
226+
const markup = renderToStaticMarkup(
227+
<MessagesTimeline
228+
{...buildProps()}
229+
timelineEntries={[
230+
{
231+
id: "entry-assistant-with-files",
232+
kind: "message",
233+
createdAt: MESSAGE_CREATED_AT,
234+
message: {
235+
id: assistantMessageId,
236+
role: "assistant",
237+
text: "Updated the fixture.",
238+
turnId,
239+
createdAt: MESSAGE_CREATED_AT,
240+
updatedAt: MESSAGE_CREATED_AT,
241+
streaming: false,
242+
},
243+
},
244+
]}
245+
turnDiffSummaryByAssistantMessageId={
246+
new Map([
247+
[
248+
assistantMessageId,
249+
{
250+
turnId,
251+
checkpointTurnCount: 1,
252+
checkpointRef: CheckpointRef.make("checkpoint-with-files"),
253+
status: "ready",
254+
files: [{ path: "README.md", kind: "modified", additions: 2, deletions: 1 }],
255+
assistantMessageId,
256+
completedAt: MESSAGE_CREATED_AT,
257+
},
258+
],
259+
])
260+
}
261+
/>,
262+
);
263+
264+
expect(markup).toContain('class="sticky top-2 z-10');
265+
expect(markup).not.toContain("self-start");
266+
expect(markup).toContain("whitespace-nowrap");
267+
expect(markup).toContain("!size-[22px]");
268+
expect(markup).toContain("size-3");
269+
expect(markup).toContain('aria-label="Collapse all"');
270+
expect(markup).toContain('aria-label="View diff"');
271+
expect(markup).toContain("1 changed file");
272+
});
273+
222274
it("uses LegendList isNearEnd when deciding whether the live edge is visible", async () => {
223275
const {
224276
resolveTimelineIsAtEnd,

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

Lines changed: 58 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -44,8 +44,11 @@ import {
4444
CheckIcon,
4545
ChevronDownIcon,
4646
ChevronRightIcon,
47+
ChevronsDownUpIcon,
48+
ChevronsUpDownIcon,
4749
CircleAlertIcon,
4850
EyeIcon,
51+
FileDiffIcon,
4952
GlobeIcon,
5053
HammerIcon,
5154
MessageCircleIcon,
@@ -1274,38 +1277,67 @@ function AssistantChangedFilesSectionInner({
12741277
);
12751278
const setExpanded = useUiStateStore((store) => store.setThreadChangedFilesExpanded);
12761279
const summaryStat = summarizeTurnDiffStats(checkpointFiles);
1277-
const changedFileCountLabel = String(checkpointFiles.length);
12781280

12791281
return (
1280-
<div className="mt-2 rounded-lg border border-border/80 bg-card/45 p-2.5">
1281-
<div className="sticky top-2 z-10 mb-1.5 flex items-center justify-between gap-2 bg-[color-mix(in_srgb,var(--card)_45%,var(--background))] before:absolute before:inset-x-0 before:-top-2 before:h-2 before:bg-[color-mix(in_srgb,var(--card)_45%,var(--background))] before:content-['']">
1282-
<p className="text-[10px] uppercase tracking-[0.12em] text-muted-foreground/65">
1283-
<span>Changed files ({changedFileCountLabel})</span>
1282+
<div className="mt-4 rounded-2xl border border-input bg-background p-2 pt-4 shadow-xs/5 not-dark:bg-clip-padding dark:bg-input/32">
1283+
<div className="sticky top-2 z-10 mb-3 flex items-center justify-between gap-2 bg-background px-2 before:absolute before:inset-x-0 before:-top-4 before:h-4 before:bg-background before:content-[''] dark:bg-[color-mix(in_srgb,var(--foreground)_2.5%,var(--background))] dark:before:bg-[color-mix(in_srgb,var(--foreground)_2.5%,var(--background))]">
1284+
<p className="flex items-center gap-1 whitespace-nowrap font-medium text-foreground text-xs leading-4">
1285+
<span>
1286+
{checkpointFiles.length} changed file{checkpointFiles.length === 1 ? "" : "s"}
1287+
</span>
12841288
{hasNonZeroStat(summaryStat) && (
1285-
<>
1286-
<span className="mx-1"></span>
1287-
<DiffStatLabel additions={summaryStat.additions} deletions={summaryStat.deletions} />
1288-
</>
1289+
<DiffStatLabel
1290+
additions={summaryStat.additions}
1291+
className="text-xs leading-4"
1292+
deletions={summaryStat.deletions}
1293+
layout="inline"
1294+
/>
12891295
)}
12901296
</p>
12911297
<div className="flex items-center gap-1.5">
1292-
<Button
1293-
type="button"
1294-
size="xs"
1295-
variant="outline"
1296-
data-scroll-anchor-ignore
1297-
onClick={() => setExpanded(routeThreadKey, turnSummary.turnId, !allDirectoriesExpanded)}
1298-
>
1299-
{allDirectoriesExpanded ? "Collapse all" : "Expand all"}
1300-
</Button>
1301-
<Button
1302-
type="button"
1303-
size="xs"
1304-
variant="outline"
1305-
onClick={() => onOpenTurnDiff(turnSummary.turnId, checkpointFiles[0]?.path)}
1306-
>
1307-
View diff
1308-
</Button>
1298+
<Tooltip>
1299+
<TooltipTrigger
1300+
render={
1301+
<Button
1302+
type="button"
1303+
size="icon-xs"
1304+
variant="outline"
1305+
className="!size-[22px]"
1306+
aria-label={allDirectoriesExpanded ? "Collapse all" : "Expand all"}
1307+
data-scroll-anchor-ignore
1308+
onClick={() =>
1309+
setExpanded(routeThreadKey, turnSummary.turnId, !allDirectoriesExpanded)
1310+
}
1311+
/>
1312+
}
1313+
>
1314+
{allDirectoriesExpanded ? (
1315+
<ChevronsDownUpIcon className="size-3" />
1316+
) : (
1317+
<ChevronsUpDownIcon className="size-3" />
1318+
)}
1319+
</TooltipTrigger>
1320+
<TooltipPopup side="top">
1321+
{allDirectoriesExpanded ? "Collapse all" : "Expand all"}
1322+
</TooltipPopup>
1323+
</Tooltip>
1324+
<Tooltip>
1325+
<TooltipTrigger
1326+
render={
1327+
<Button
1328+
type="button"
1329+
size="icon-xs"
1330+
variant="outline"
1331+
className="!size-[22px]"
1332+
aria-label="View diff"
1333+
onClick={() => onOpenTurnDiff(turnSummary.turnId, checkpointFiles[0]?.path)}
1334+
/>
1335+
}
1336+
>
1337+
<FileDiffIcon className="size-3" />
1338+
</TooltipTrigger>
1339+
<TooltipPopup side="top">View diff</TooltipPopup>
1340+
</Tooltip>
13091341
</div>
13101342
</div>
13111343
<ChangedFilesTree

0 commit comments

Comments
 (0)