fix(site): remove last-checked label from git diff panel (#24675)

Removes `LastCheckedLabel.tsx` component and all related plumbing.

Note: chromatic is failing on main, seems to be pre-existing.

> 🤖
This commit is contained in:
Cian Johnston
2026-04-23 16:45:05 +01:00
committed by GitHub
parent e56b409873
commit a13f7f18e5
7 changed files with 10 additions and 225 deletions
@@ -86,7 +86,6 @@ const buildGitWatcher = (): ComponentProps<
>["gitWatcher"] => ({
repositories: new Map(),
everDirty: new Set(),
lastCheckedAt: undefined,
refresh: fn().mockReturnValue(true),
});
@@ -128,7 +128,6 @@ interface AgentChatPageViewProps {
gitWatcher: {
repositories: ReadonlyMap<string, TypesGen.WorkspaceAgentRepoChanges>;
everDirty: ReadonlySet<string>;
lastCheckedAt: Date | undefined;
refresh: () => boolean;
};
@@ -346,7 +345,6 @@ export const AgentChatPageView: FC<AgentChatPageViewProps> = ({
}
repositories={gitWatcher.repositories}
everDirty={gitWatcher.everDirty}
lastCheckedAt={gitWatcher.lastCheckedAt}
onRefresh={handleRefresh}
onCommit={handleCommit}
isExpanded={visualExpanded}
@@ -389,44 +389,3 @@ export const CleanRepoFromStart: Story = {
expect(tabs).toHaveLength(0);
},
};
/**
* Renders the relative-time label once a scan has been observed.
*/
export const ShowsLastCheckedLabel: Story = {
args: {
repositories: new Map([["/home/coder/coder", makeRepo()]]),
// Fixed past date keeps the rendered "ago" text deterministic
// for pixel snapshots. dayjs formats "X months ago" or "X
// years ago" at this scale, and those buckets do not flip
// between story collection and story render.
lastCheckedAt: new Date("2024-01-01T00:00:00Z"),
},
play: async ({ canvasElement }) => {
const label = canvasElement.querySelector(
'[data-testid="git-last-checked"]',
);
expect(label).not.toBeNull();
// dayjs' relativeTime renders sub-45s as 'a few seconds ago'
// and longer spans as '<n> <unit> ago'. Accept either shape
// so the story is not coupled to dayjs' specific bucketing.
expect(label?.textContent ?? "").toMatch(/^checked .+ ago$/);
},
};
/**
* With no scan observed yet, the label renders nothing so the
* toolbar collapses cleanly.
*/
export const NoLastCheckedYet: Story = {
args: {
repositories: new Map([["/home/coder/coder", makeRepo()]]),
lastCheckedAt: undefined,
},
play: async ({ canvasElement }) => {
const label = canvasElement.querySelector(
'[data-testid="git-last-checked"]',
);
expect(label).toBeNull();
},
};
@@ -29,7 +29,6 @@ import {
} from "../DiffViewer/DiffViewer";
import { LocalDiffPanel } from "../DiffViewer/LocalDiffPanel";
import { RemoteDiffPanel } from "../DiffViewer/RemoteDiffPanel";
import { LastCheckedLabel } from "./LastCheckedLabel";
type GitView = { type: "remote" } | { type: "local"; repoRoot: string };
@@ -63,11 +62,6 @@ interface GitPanelProps {
* then reverts it.
*/
everDirty?: ReadonlySet<string>;
/**
* Timestamp of the last scan received from the server. Rendered as a
* live-updating "checked Ns ago" label next to the refresh button.
*/
lastCheckedAt?: Date | undefined;
}
function repoTabLabel(repoRoot: string): string {
@@ -84,7 +78,6 @@ export const GitPanel: FC<GitPanelProps> = ({
remoteDiffStats,
chatInputRef,
everDirty,
lastCheckedAt,
}) => {
const hasRemoteStats =
(remoteDiffStats?.additions ?? 0) > 0 ||
@@ -258,10 +251,6 @@ export const GitPanel: FC<GitPanelProps> = ({
</ScrollArea>
{/* Controls */}
<div className="flex shrink-0 items-center gap-1 py-1.5">
<LastCheckedLabel
at={lastCheckedAt}
className="mr-1 whitespace-nowrap text-[11px] text-content-secondary"
/>
<div className="flex h-6 items-stretch overflow-hidden rounded-md border border-solid border-border-default">
<button
type="button"
@@ -1,32 +0,0 @@
import type { FC } from "react";
import { cn } from "#/utils/cn";
import { relativeTime } from "#/utils/time";
interface LastCheckedLabelProps {
at: Date | undefined;
className?: string;
}
/**
* Renders "checked <relative time>" next to the refresh button.
* Compiled by React Compiler, so re-renders are driven by a fresh
* `at` reference; the server's 5s scan heartbeat supplies that.
* Returns null before the first scan so the toolbar collapses.
*/
export const LastCheckedLabel: FC<LastCheckedLabelProps> = ({
at,
className,
}) => {
if (!at) {
return null;
}
return (
<span
data-testid="git-last-checked"
className={cn(["whitespace-nowrap", className])}
title={at.toLocaleString()}
>
checked {relativeTime(at)}
</span>
);
};
@@ -842,46 +842,18 @@ describe("useGitWatcher", () => {
await waitFor(() => {
expect(result.current.everDirty.has("/repo")).toBe(true);
});
expect(result.current.lastCheckedAt).toBeInstanceOf(Date);
// Switch to a different chat. The hook tears down and recreates
// the socket; everDirty, repositories, and lastCheckedAt must
// all reset so chat-B starts with a clean slate.
// the socket; everDirty and repositories must all reset so
// chat-B starts with a clean slate.
createMockSocket();
rerender({ chatId: "chat-B" });
expect(result.current.repositories.size).toBe(0);
expect(result.current.everDirty.size).toBe(0);
expect(result.current.lastCheckedAt).toBeUndefined();
});
it("tracks lastCheckedAt from scanned_at", async () => {
const socket = createMockSocket();
const { result } = renderHook(() =>
useGitWatcher({ chatId: "chat-123", agentStatus: "connected" }),
);
act(() => socket.simulateOpen());
expect(result.current.lastCheckedAt).toBeUndefined();
act(() => {
socket.simulateMessage({
type: "changes",
scanned_at: "2024-01-02T03:04:05Z",
repositories: [],
});
});
await waitFor(() => {
expect(result.current.lastCheckedAt).toBeInstanceOf(Date);
});
expect(result.current.lastCheckedAt?.toISOString()).toBe(
"2024-01-02T03:04:05.000Z",
);
});
it("heartbeat message (no repositories) advances lastCheckedAt without touching repos", async () => {
it("heartbeat message (no repositories) does not touch repos", async () => {
const socket = createMockSocket();
const { result } = renderHook(() =>
@@ -921,11 +893,6 @@ describe("useGitWatcher", () => {
});
});
await waitFor(() => {
expect(result.current.lastCheckedAt?.toISOString()).toBe(
"2024-01-02T03:04:10.000Z",
);
});
// Heartbeat must not mutate repo state.
expect(result.current.repositories).toBe(repoStateBeforeHeartbeat);
expect(result.current.everDirty).toBe(everDirtyBeforeHeartbeat);
@@ -968,52 +935,11 @@ describe("useGitWatcher", () => {
});
});
await waitFor(() => {
expect(result.current.lastCheckedAt?.toISOString()).toBe(
"2024-01-02T03:04:15.000Z",
);
});
// Repo entry survives an empty-array heartbeat.
expect(result.current.repositories.has("/repo")).toBe(true);
expect(result.current.everDirty.has("/repo")).toBe(true);
});
it("ignores malformed scanned_at without clearing existing value", async () => {
const socket = createMockSocket();
const { result } = renderHook(() =>
useGitWatcher({ chatId: "chat-123", agentStatus: "connected" }),
);
act(() => socket.simulateOpen());
// Prime a valid timestamp.
act(() => {
socket.simulateMessage({
type: "changes",
scanned_at: "2024-01-02T03:04:05Z",
repositories: [],
});
});
await waitFor(() => {
expect(result.current.lastCheckedAt).toBeDefined();
expect(result.current.repositories.has("/repo")).toBe(true);
});
const firstIso = result.current.lastCheckedAt?.toISOString();
// A malformed scanned_at must not wipe the previous value.
act(() => {
socket.simulateMessage({
type: "changes",
scanned_at: "not-a-date",
repositories: [],
});
});
// Compare by ISO string, not reference. A future refactor that
// re-creates the Date object identically should still pass; what
// matters is that the observed timestamp did not change.
expect(result.current.lastCheckedAt?.toISOString()).toBe(firstIso);
expect(result.current.everDirty.has("/repo")).toBe(true);
});
it("tracks everDirty: preserves across agentStatus flap on the same chat", async () => {
@@ -1053,43 +979,4 @@ describe("useGitWatcher", () => {
rerender({ agentStatus: "connected" as WorkspaceAgentStatus });
expect(result.current.everDirty.has("/repo")).toBe(true);
});
it("preserves lastCheckedAt across agentStatus flap on the same chat", async () => {
const socket1 = createMockSocket();
const { result, rerender } = renderHook(
({ agentStatus }: { agentStatus: WorkspaceAgentStatus | undefined }) =>
useGitWatcher({ chatId: "chat-stable", agentStatus }),
{ initialProps: { agentStatus: "connected" as WorkspaceAgentStatus } },
);
act(() => socket1.simulateOpen());
act(() => {
socket1.simulateMessage({
type: "changes",
scanned_at: "2024-01-02T03:04:05Z",
repositories: [],
});
});
await waitFor(() => {
expect(result.current.lastCheckedAt?.toISOString()).toBe(
"2024-01-02T03:04:05.000Z",
);
});
// Transient flap: the socket is torn down but the stale-data
// timestamp must keep advancing so the UI's "checked Ns ago"
// label does not disappear during reconnection backoff.
createMockSocket();
rerender({ agentStatus: "connecting" as WorkspaceAgentStatus });
expect(result.current.lastCheckedAt?.toISOString()).toBe(
"2024-01-02T03:04:05.000Z",
);
createMockSocket();
rerender({ agentStatus: "connected" as WorkspaceAgentStatus });
expect(result.current.lastCheckedAt?.toISOString()).toBe(
"2024-01-02T03:04:05.000Z",
);
});
});
@@ -35,10 +35,6 @@ interface UseGitWatcherResult {
* chatId change. Consumers should intersect with `repositories`.
*/
everDirty: ReadonlySet<string>;
/** ScannedAt from the latest server message. Undefined until the
* first message arrives. Preserved across reconnects so the UI's
* relative-time label keeps advancing during backoff. */
lastCheckedAt: Date | undefined;
/** Whether the WebSocket is currently connected. */
isConnected: boolean;
/** Send a refresh request. Returns true if sent, false if disconnected. */
@@ -55,20 +51,16 @@ export function useGitWatcher({
const [everDirty, setEverDirty] = useState<ReadonlySet<string>>(
() => new Set(),
);
const [lastCheckedAt, setLastCheckedAt] = useState<Date | undefined>(
undefined,
);
const [isConnected, setIsConnected] = useState(false);
const socketRef = useRef<WebSocket | null>(null);
// Chat-scoped state (everDirty, lastCheckedAt) resets on chatId
// change but must survive agentStatus flaps on the same chat.
// Chat-scoped state (everDirty) resets on chatId change but
// must survive agentStatus flaps on the same chat.
// https://react.dev/reference/react/useState#storing-information-from-previous-renders
const [lastChatId, setLastChatId] = useState<string | undefined>(chatId);
if (lastChatId !== chatId) {
setLastChatId(chatId);
setEverDirty((prev) => (prev.size === 0 ? prev : new Set()));
setLastCheckedAt(undefined);
}
const sendMessage = (msg: WorkspaceAgentGitClientMessage): boolean => {
@@ -112,12 +104,6 @@ export function useGitWatcher({
}
if (data.type === "changes") {
if (data.scanned_at) {
const parsed = new Date(data.scanned_at);
if (!Number.isNaN(parsed.getTime())) {
setLastCheckedAt(parsed);
}
}
if (data.repositories) {
setRepositories((prev) => {
let changed = false;
@@ -182,9 +168,8 @@ export function useGitWatcher({
});
return () => {
// Reset connection-scoped state only. `everDirty` and
// `lastCheckedAt` are chat-scoped and persist across
// reconnects, so a slow backoff keeps the label advancing.
// Reset connection-scoped state only. `everDirty` is
// chat-scoped and persists across reconnects.
dispose();
setIsConnected(false);
setRepositories(new Map());
@@ -192,5 +177,5 @@ export function useGitWatcher({
};
}, [chatId, agentStatus]);
return { repositories, everDirty, lastCheckedAt, isConnected, refresh };
return { repositories, everDirty, isConnected, refresh };
}