mirror of
https://github.com/cline/cline.git
synced 2026-10-24 02:02:27 +08:00
fix(desktop): offer diagnostics export from the startup failure screen
The exhausted-retry message tells users to export diagnostics, but the loading screen hid the only export entry point (HomeShell). The sidecar's export_diagnostics command does not need the Hub, so call it directly from the loading screen while the transport is connected. Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>
This commit is contained in:
co-authored by
Saoud Rizwan
parent
660bbeaf25
commit
ec4fa92b50
@@ -27,6 +27,11 @@ import { LoadingScreen } from "./views/loading/LoadingScreen";
|
||||
import { SiteLoader } from "./views/loading/SiteLoader";
|
||||
import { AddProviderContent } from "./views/settings/add-provider";
|
||||
|
||||
const invoke = vi.fn();
|
||||
vi.mock("@/lib/desktop-client", () => ({
|
||||
desktopClient: { invoke: (...args: unknown[]) => invoke(...args) },
|
||||
}));
|
||||
|
||||
const container = document.createElement("div");
|
||||
document.body.append(container);
|
||||
let root = createRoot(container);
|
||||
@@ -209,6 +214,37 @@ it("distinguishes hub failure and allows retry", () => {
|
||||
act(() => container.querySelector("button")?.click());
|
||||
expect(state.retry).toHaveBeenCalledOnce();
|
||||
});
|
||||
it("exports diagnostics over the transport when the hub has failed", async () => {
|
||||
invoke.mockResolvedValueOnce({ path: "/tmp/cline-diagnostics.txt" });
|
||||
const state = readiness();
|
||||
state.transport = "connected";
|
||||
state.hub = { state: "failed", attempt: 4, message: "Hub failed" };
|
||||
act(() => root.render(<LoadingScreen readiness={state} />));
|
||||
const buttons = [...container.querySelectorAll("button")];
|
||||
expect(buttons.map((button) => button.textContent)).toEqual([
|
||||
"Retry",
|
||||
"Export diagnostics",
|
||||
]);
|
||||
await act(async () => buttons[1].click());
|
||||
expect(invoke).toHaveBeenCalledWith("export_diagnostics");
|
||||
expect(container.textContent).toContain(
|
||||
"Diagnostics saved to /tmp/cline-diagnostics.txt",
|
||||
);
|
||||
});
|
||||
it("offers no export while the desktop transport is unavailable", () => {
|
||||
const state = readiness();
|
||||
state.transport = "unavailable";
|
||||
state.startup = {
|
||||
state: "failed",
|
||||
error: "Sidecar exited",
|
||||
exitStatus: null,
|
||||
diagnostics: [],
|
||||
};
|
||||
act(() => root.render(<LoadingScreen readiness={state} />));
|
||||
expect(
|
||||
[...container.querySelectorAll("button")].map((b) => b.textContent),
|
||||
).toEqual(["Retry"]);
|
||||
});
|
||||
it("shows actionable native startup diagnostics independently of authentication", () => {
|
||||
const state = readiness();
|
||||
state.startup = {
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
"use client";
|
||||
|
||||
import { Loader2 } from "lucide-react";
|
||||
import { useState } from "react";
|
||||
import type { useDesktopReadiness } from "@/hooks/use-desktop-readiness";
|
||||
import { desktopClient } from "@/lib/desktop-client";
|
||||
|
||||
export function LoadingScreen({
|
||||
readiness,
|
||||
@@ -10,6 +12,21 @@ export function LoadingScreen({
|
||||
}) {
|
||||
const connected = readiness.transport === "connected";
|
||||
const { hub, startup } = readiness;
|
||||
const [exportStatus, setExportStatus] = useState<string | null>(null);
|
||||
// The sidecar writes the diagnostics report without the Hub, so the
|
||||
// existing export works over the transport while the session service
|
||||
// is still failing.
|
||||
const exportDiagnostics = async () => {
|
||||
setExportStatus("Exporting diagnostics…");
|
||||
try {
|
||||
const result = await desktopClient.invoke<{ path: string }>(
|
||||
"export_diagnostics",
|
||||
);
|
||||
setExportStatus(`Diagnostics saved to ${result.path}`);
|
||||
} catch (error) {
|
||||
setExportStatus(error instanceof Error ? error.message : String(error));
|
||||
}
|
||||
};
|
||||
const failed =
|
||||
!readiness.retrying &&
|
||||
(connected
|
||||
@@ -76,13 +93,29 @@ export function LoadingScreen({
|
||||
</pre>
|
||||
</details>
|
||||
)}
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => void readiness.retry()}
|
||||
className="rounded-md bg-primary px-4 py-2 text-primary-foreground"
|
||||
>
|
||||
Retry
|
||||
</button>
|
||||
<div className="flex flex-wrap items-center justify-center gap-2">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => void readiness.retry()}
|
||||
className="rounded-md bg-primary px-4 py-2 text-primary-foreground"
|
||||
>
|
||||
Retry
|
||||
</button>
|
||||
{connected && (
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => void exportDiagnostics()}
|
||||
className="rounded-md border border-border px-4 py-2 text-foreground hover:bg-surface-hover"
|
||||
>
|
||||
Export diagnostics
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
{exportStatus && (
|
||||
<p className="break-all text-xs text-muted-foreground">
|
||||
{exportStatus}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</main>
|
||||
|
||||
Reference in New Issue
Block a user