diff --git a/apps/examples/desktop-app/webview/components/site-loader.test.tsx b/apps/examples/desktop-app/webview/components/site-loader.test.tsx index 2e077125c3..93fa560ccd 100644 --- a/apps/examples/desktop-app/webview/components/site-loader.test.tsx +++ b/apps/examples/desktop-app/webview/components/site-loader.test.tsx @@ -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()); + 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()); + expect( + [...container.querySelectorAll("button")].map((b) => b.textContent), + ).toEqual(["Retry"]); +}); it("shows actionable native startup diagnostics independently of authentication", () => { const state = readiness(); state.startup = { diff --git a/apps/examples/desktop-app/webview/components/views/loading/LoadingScreen.tsx b/apps/examples/desktop-app/webview/components/views/loading/LoadingScreen.tsx index 5b1168280d..7b3a76d808 100644 --- a/apps/examples/desktop-app/webview/components/views/loading/LoadingScreen.tsx +++ b/apps/examples/desktop-app/webview/components/views/loading/LoadingScreen.tsx @@ -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(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({ )} - +
+ + {connected && ( + + )} +
+ {exportStatus && ( +

+ {exportStatus} +

+ )} )}