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:
Saoud Rizwan
2026-10-15 23:27:09 +00:00
committed by Cursor Agent
co-authored by Saoud Rizwan
parent 660bbeaf25
commit ec4fa92b50
2 changed files with 76 additions and 7 deletions
@@ -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>