mirror of
https://github.com/coder/coder.git
synced 2026-09-21 20:51:01 +08:00
fix(site/src/pages/AgentsPage): toast when git refresh fails due to disconnection (#23779)
This commit is contained in:
@@ -75,7 +75,7 @@ const buildGitWatcher = (): ComponentProps<
|
||||
typeof AgentChatPageView
|
||||
>["gitWatcher"] => ({
|
||||
repositories: new Map(),
|
||||
refresh: fn(),
|
||||
refresh: fn().mockReturnValue(true),
|
||||
});
|
||||
|
||||
const agentsRouting = [
|
||||
|
||||
@@ -99,7 +99,7 @@ interface AgentChatPageViewProps {
|
||||
diffStatusData: ChatDiffStatus | undefined;
|
||||
gitWatcher: {
|
||||
repositories: ReadonlyMap<string, TypesGen.WorkspaceAgentRepoChanges>;
|
||||
refresh: () => void;
|
||||
refresh: () => boolean;
|
||||
};
|
||||
|
||||
// Workspace action handlers.
|
||||
|
||||
@@ -99,7 +99,7 @@ const meta: Meta<typeof GitPanel> = {
|
||||
title: "pages/AgentsPage/GitPanel",
|
||||
component: GitPanel,
|
||||
args: {
|
||||
onRefresh: fn(),
|
||||
onRefresh: fn().mockReturnValue(true),
|
||||
onCommit: fn(),
|
||||
repositories: new Map(),
|
||||
},
|
||||
|
||||
@@ -12,6 +12,7 @@ import {
|
||||
RowsIcon,
|
||||
} from "lucide-react";
|
||||
import { type FC, type RefObject, useEffect, useRef, useState } from "react";
|
||||
import { toast } from "sonner";
|
||||
import type {
|
||||
ChatDiffStatus,
|
||||
WorkspaceAgentRepoChanges,
|
||||
@@ -44,8 +45,8 @@ interface GitPanelProps {
|
||||
};
|
||||
/** Repository data from git watcher. */
|
||||
repositories: ReadonlyMap<string, WorkspaceAgentRepoChanges>;
|
||||
/** Callback to send a refresh to the git watcher. */
|
||||
onRefresh: () => void;
|
||||
/** Callback to send a refresh to the git watcher. Returns false when disconnected. */
|
||||
onRefresh: () => boolean;
|
||||
/** Called when the user clicks the Commit button in any repo tab. */
|
||||
onCommit: (repoRoot: string) => void;
|
||||
/** Whether the panel is in expanded/fullscreen mode. */
|
||||
@@ -142,7 +143,14 @@ export const GitPanel: FC<GitPanelProps> = ({
|
||||
const spinTimerRef = useRef<ReturnType<typeof setTimeout>>(undefined);
|
||||
useEffect(() => () => clearTimeout(spinTimerRef.current), []);
|
||||
const handleRefresh = () => {
|
||||
onRefresh();
|
||||
const sent = onRefresh();
|
||||
if (!sent) {
|
||||
toast.error("Unable to refresh git status.", {
|
||||
id: "git-refresh-disconnected",
|
||||
description: "Connection lost. Reconnecting\u2026",
|
||||
});
|
||||
return;
|
||||
}
|
||||
setSpinning(true);
|
||||
clearTimeout(spinTimerRef.current);
|
||||
spinTimerRef.current = setTimeout(() => setSpinning(false), 1000);
|
||||
|
||||
@@ -331,14 +331,60 @@ describe("useGitWatcher", () => {
|
||||
|
||||
act(() => socket.simulateOpen());
|
||||
|
||||
act(() => result.current.refresh());
|
||||
let sent: boolean | undefined;
|
||||
act(() => {
|
||||
sent = result.current.refresh();
|
||||
});
|
||||
|
||||
expect(sent).toBe(true);
|
||||
expect(socket.send).toHaveBeenCalledTimes(1);
|
||||
expect(socket.send).toHaveBeenCalledWith(
|
||||
JSON.stringify({ type: "refresh" }),
|
||||
);
|
||||
});
|
||||
|
||||
it("refresh returns false when the socket is not connected", () => {
|
||||
vi.useFakeTimers();
|
||||
|
||||
try {
|
||||
const socket = createMockSocket();
|
||||
|
||||
const { result } = renderHook(() =>
|
||||
useGitWatcher({ chatId: "chat-123", agentStatus: "connected" }),
|
||||
);
|
||||
|
||||
act(() => socket.simulateOpen());
|
||||
|
||||
// Close the socket. The close handler sets socketRef to
|
||||
// null and schedules a reconnect timer, but we don't
|
||||
// advance timers so the socket stays null.
|
||||
act(() => socket.simulateClose());
|
||||
|
||||
let sent: boolean | undefined;
|
||||
act(() => {
|
||||
sent = result.current.refresh();
|
||||
});
|
||||
|
||||
expect(sent).toBe(false);
|
||||
} finally {
|
||||
vi.useRealTimers();
|
||||
}
|
||||
});
|
||||
|
||||
it("refresh returns false before the socket connects", () => {
|
||||
// Don't connect at all — agentStatus prevents effect from running.
|
||||
const { result } = renderHook(() =>
|
||||
useGitWatcher({ chatId: "chat-123", agentStatus: "connecting" }),
|
||||
);
|
||||
|
||||
let sent: boolean | undefined;
|
||||
act(() => {
|
||||
sent = result.current.refresh();
|
||||
});
|
||||
|
||||
expect(sent).toBe(false);
|
||||
});
|
||||
|
||||
it("cleans up WebSocket and timers on unmount", () => {
|
||||
vi.useFakeTimers();
|
||||
|
||||
|
||||
@@ -30,8 +30,8 @@ interface UseGitWatcherResult {
|
||||
repositories: ReadonlyMap<string, WorkspaceAgentRepoChanges>;
|
||||
/** Whether the WebSocket is currently connected. */
|
||||
isConnected: boolean;
|
||||
/** Send a refresh request. */
|
||||
refresh: () => void;
|
||||
/** Send a refresh request. Returns true if sent, false if disconnected. */
|
||||
refresh: () => boolean;
|
||||
}
|
||||
|
||||
const MAX_BACKOFF_MS = 30_000;
|
||||
@@ -51,15 +51,17 @@ export function useGitWatcher({
|
||||
// Track whether we've been disposed to avoid reconnecting after unmount.
|
||||
const disposedRef = useRef(false);
|
||||
|
||||
const sendMessage = (msg: WorkspaceAgentGitClientMessage) => {
|
||||
const sendMessage = (msg: WorkspaceAgentGitClientMessage): boolean => {
|
||||
const socket = socketRef.current;
|
||||
if (socket && socket.readyState === WebSocket.OPEN) {
|
||||
socket.send(JSON.stringify(msg));
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
};
|
||||
|
||||
const refresh = () => {
|
||||
sendMessage({ type: "refresh" });
|
||||
const refresh = (): boolean => {
|
||||
return sendMessage({ type: "refresh" });
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
|
||||
Reference in New Issue
Block a user