diff --git a/apps/app/src/react-app/domains/connections/cloud-mcp-submit-readiness.ts b/apps/app/src/react-app/domains/connections/cloud-mcp-submit-readiness.ts
index a92f800d3a..9508c383e8 100644
--- a/apps/app/src/react-app/domains/connections/cloud-mcp-submit-readiness.ts
+++ b/apps/app/src/react-app/domains/connections/cloud-mcp-submit-readiness.ts
@@ -265,7 +265,7 @@ export function assessCloudMcpSubmissionReadiness(input: {
code: "provider_tool_projection_unverified",
stage: "provider_projection",
message: "OpenWork could not read tool capability information for the selected provider and model.",
- recommendedAction: "Retry, or check Settings → Advanced → Agent access diagnostics if the problem continues.",
+ recommendedAction: "Retry, or check Settings → Advanced → Diagnostics if the problem continues.",
}),
};
}
@@ -286,8 +286,8 @@ export function assessCloudMcpSubmissionReadiness(input: {
? "The selected model does not support tool calling."
: "OpenWork could not confirm that the selected model supports tool calling.",
recommendedAction: modelMissing
- ? "Choose a model available from this provider, or check Settings → Advanced → Agent access diagnostics."
- : "Choose a model with tool calling, or check Settings → Advanced → Agent access diagnostics.",
+ ? "Choose a model available from this provider, or check Settings → Advanced → Diagnostics."
+ : "Choose a model with tool calling, or check Settings → Advanced → Diagnostics.",
}),
};
}
@@ -300,7 +300,7 @@ export function assessCloudMcpSubmissionReadiness(input: {
stage: "provider_projection",
retryable: false,
message: "OpenWork received an unsupported tool capability result for the selected provider and model.",
- recommendedAction: "Choose a model with tool calling, or check Settings → Advanced → Agent access diagnostics.",
+ recommendedAction: "Choose a model with tool calling, or check Settings → Advanced → Diagnostics.",
}),
};
}
diff --git a/apps/app/src/react-app/domains/settings/advanced-sections.ts b/apps/app/src/react-app/domains/settings/advanced-sections.ts
index 72acc2673e..52817e5305 100644
--- a/apps/app/src/react-app/domains/settings/advanced-sections.ts
+++ b/apps/app/src/react-app/domains/settings/advanced-sections.ts
@@ -1,7 +1,7 @@
export const ADVANCED_SETTINGS_SECTIONS = [
{ id: "organization-server", title: "Organization server", keywords: ["den", "control plane", "server url", "reset server"] },
{ id: "runtime", title: "Runtime", keywords: ["opencode engine", "openwork server", "connection status"] },
- { id: "agent-access", title: "Agent access diagnostics", keywords: ["cloud mcp", "health", "tools", "connections"] },
+ { id: "agent-access", title: "Diagnostics", keywords: ["agent access", "cloud mcp", "health", "tools", "connections", "copy report", "support", "version"] },
{ id: "config-sources", title: "OpenCode config sources", keywords: ["runtime db", "injected config", "project config", "global config"] },
{ id: "experimental-engine", title: "Experimental engine", keywords: ["engine v2", "chat engine", "preview"] },
{ id: "workspace-run-mode", title: "Workspace run mode", keywords: ["approvals", "permissions", "keep going", "feature flag"] },
diff --git a/apps/app/src/react-app/domains/settings/pages/advanced-diagnostics-section.tsx b/apps/app/src/react-app/domains/settings/pages/advanced-diagnostics-section.tsx
new file mode 100644
index 0000000000..f5d5f0d0b5
--- /dev/null
+++ b/apps/app/src/react-app/domains/settings/pages/advanced-diagnostics-section.tsx
@@ -0,0 +1,228 @@
+/** @jsxImportSource react */
+import { useEffect, useRef, useState } from "react";
+import { ChevronRight, Copy } from "lucide-react";
+
+import { Button } from "@/components/ui/button";
+import type { OpenworkCloudMcpHealth } from "@/app/lib/openwork-server";
+import { sanitizeCloudMcpHealthDiagnostic } from "@/app/lib/diagnostic-sanitizer";
+import { RefreshButton, SettingsInset, SettingsNotice, SettingsStatusBadge } from "../settings-section";
+import {
+ LayoutSection,
+ LayoutSectionHeader,
+ LayoutSectionItem,
+ LayoutSectionTitle,
+} from "../settings-layout";
+import { diagnosticsVersionLine, summarizeDiagnostics } from "./diagnostics-summary";
+
+function DiagnosticRow(props: { label: string; value: string }) {
+ return (
+
+
{props.label}
+
{props.value}
+
+ );
+}
+
+function joinList(values: string[]): string {
+ return values.length ? values.join(", ") : "none";
+}
+
+function formatMaybe(value: string | number | boolean | null | undefined): string {
+ if (value === null || value === undefined || value === "") return "unknown";
+ return String(value);
+}
+
+function formatMetadataRecord(value: Record | null | undefined): string {
+ if (!value || Object.keys(value).length === 0) return "none";
+ return Object.entries(value).map(([key, nested]) => `${key}=${formatMaybe(nested)}`).join(", ");
+}
+
+function formatSupportedFeatures(features: OpenworkCloudMcpHealth["compatibility"]["supportedFeatures"]): string {
+ return Object.entries(features).map(([key, enabled]) => `${key}:${enabled ? "yes" : "no"}`).join(", ");
+}
+
+function formatPluginHashes(hashes: OpenworkCloudMcpHealth["compatibility"]["pluginFileHashes"]): string {
+ if (hashes.length === 0) return "none";
+ return hashes.map((hash) => `${hash.name}=${hash.sha256 ? hash.sha256.slice(0, 12) : `unavailable${hash.error ? ` (${hash.error})` : ""}`}`).join(", ");
+}
+
+function formatMcpToolExposure(input: { checked: boolean; includesMcpTools: boolean | null; present: string[]; missing: string[]; limitation?: string }): string {
+ if (!input.checked) return "not checked";
+ const includes = input.includesMcpTools === null ? "unknown" : input.includesMcpTools ? "yes" : "no";
+ return `includes MCP tools: ${includes}; present ${joinList(input.present)}; missing ${joinList(input.missing)}${input.limitation ? `; limitation: ${input.limitation}` : ""}`;
+}
+
+function formatCheckedAt(checkedAt: string): string | null {
+ const date = new Date(checkedAt);
+ if (Number.isNaN(date.getTime())) return null;
+ return `checked ${date.toLocaleTimeString([], { hour: "numeric", minute: "2-digit" })}`;
+}
+
+/** The exact runtime state, for the support engineer reading over someone's shoulder. Closed by default. */
+function TechnicalDetails(props: { health: OpenworkCloudMcpHealth }) {
+ const health = props.health;
+ const projection = health.tools.providerProjection;
+ const compatibility = health.compatibility;
+
+ return (
+
+
+
+ Technical details
+
+
+
+
+
+
+
+ {health.engineInspection?.checked ? (
+
`${server.name} ${server.status}${server.error ? ` (${server.error})` : ""}`).join("; ")
+ : "none tracked"}
+ />
+ ) : null}
+
+
+
+ {health.tools.direct.trace ? (
+ `${step.step} ${step.ok ? "ok" : "failed"}${step.httpStatus !== undefined ? ` (HTTP ${step.httpStatus})` : ""} ${Math.max(0, Math.round(step.latencyMs))}ms`).join(" → ") || "no steps"}`}
+ />
+ ) : null}
+ {health.firstFailure ? (
+
+ ) : null}
+
+
+ {compatibility ? (
+ <>
+
+
+
+
+
+
+ >
+ ) : null}
+
+
+ Show sanitized health JSON
+
+ {JSON.stringify(sanitizeCloudMcpHealthDiagnostic(health), null, 2)}
+
+
+
+
+ );
+}
+
+export interface AdvancedDiagnosticsSectionProps {
+ appVersion: string | null;
+ signedIn: boolean;
+ orgSelected: boolean;
+ cloudMcpHealth: OpenworkCloudMcpHealth | null;
+ /** False until the workspace has a server connection the check can ask. */
+ canCheck: boolean;
+ onRefresh: () => Promise;
+ /** Copies the full sanitized report and tells the person it worked. */
+ onCopyReport: () => Promise;
+}
+
+export function AdvancedDiagnosticsSection(props: AdvancedDiagnosticsSectionProps) {
+ const [checking, setChecking] = useState(false);
+ const [copying, setCopying] = useState(false);
+ const [checkError, setCheckError] = useState(null);
+ const autoChecked = useRef(false);
+
+ const check = async () => {
+ setChecking(true);
+ setCheckError(null);
+ try {
+ await props.onRefresh();
+ } catch (error) {
+ setCheckError(error instanceof Error ? error.message : "Could not run the check.");
+ } finally {
+ setChecking(false);
+ }
+ };
+
+ const copy = async () => {
+ setCopying(true);
+ try {
+ await props.onCopyReport();
+ } finally {
+ setCopying(false);
+ }
+ };
+
+ // "Run diagnostics" lands here: show a result instead of asking for another click.
+ // The page can open before the workspace connection exists, so wait for it.
+ useEffect(() => {
+ if (autoChecked.current || props.cloudMcpHealth || !props.canCheck || !props.signedIn || !props.orgSelected) return;
+ autoChecked.current = true;
+ void check();
+ }, [props.cloudMcpHealth, props.canCheck, props.signedIn, props.orgSelected]);
+
+ const summary = summarizeDiagnostics({
+ checking,
+ canCheck: props.canCheck,
+ signedIn: props.signedIn,
+ orgSelected: props.orgSelected,
+ health: props.cloudMcpHealth,
+ });
+ const facts = [
+ diagnosticsVersionLine({ appVersion: props.appVersion, health: props.cloudMcpHealth }),
+ props.cloudMcpHealth ? formatCheckedAt(props.cloudMcpHealth.checkedAt) : null,
+ ].filter((fact): fact is string => Boolean(fact));
+
+ return (
+
+
+ Diagnostics
+
+
+
+
+
+
+
{summary.detail}
+ {facts.length > 0 ? (
+
+ {facts.join(" — ")}
+
+ ) : null}
+
+
+
+ Check again
+
+
+
+
+
+ {checkError ? {checkError} : null}
+ {props.cloudMcpHealth ? : null}
+
+
+ );
+}
diff --git a/apps/app/src/react-app/domains/settings/pages/advanced-view-sections.tsx b/apps/app/src/react-app/domains/settings/pages/advanced-view-sections.tsx
index 4bde15876c..034070558e 100644
--- a/apps/app/src/react-app/domains/settings/pages/advanced-view-sections.tsx
+++ b/apps/app/src/react-app/domains/settings/pages/advanced-view-sections.tsx
@@ -8,8 +8,8 @@ import { Field, FieldLabel } from "@/components/ui/field";
import { Switch } from "@/components/ui/switch";
import { Textarea } from "@/components/ui/textarea";
import { ToggleGroup, ToggleGroupItem } from "@/components/ui/toggle-group";
-import type { OpenworkServerClient, OpenworkCloudMcpHealth, OpenworkRuntimeConfigStatus } from "@/app/lib/openwork-server";
-import { sanitizeCloudMcpHealthDiagnostic, sanitizeDiagnosticRecord } from "@/app/lib/diagnostic-sanitizer";
+import type { OpenworkServerClient, OpenworkRuntimeConfigStatus } from "@/app/lib/openwork-server";
+import { sanitizeDiagnosticRecord } from "@/app/lib/diagnostic-sanitizer";
import {
DEFAULT_DEN_API_BASE_URL,
DEFAULT_DEN_BASE_URL,
@@ -329,164 +329,6 @@ export function AdvancedRuntimeSection(props: AdvancedRuntimeSectionProps) {
);
}
-function DiagnosticRow(props: { label: string; value: string }) {
- return (
-
-
{props.label}
-
{props.value}
-
- );
-}
-
-function joinList(values: string[]): string {
- return values.length ? values.join(", ") : "none";
-}
-
-function formatMaybe(value: string | number | boolean | null | undefined): string {
- if (value === null || value === undefined || value === "") return "unknown";
- return String(value);
-}
-
-function formatMetadataRecord(value: Record | null | undefined): string {
- if (!value || Object.keys(value).length === 0) return "none";
- return Object.entries(value).map(([key, nested]) => `${key}=${formatMaybe(nested)}`).join(", ");
-}
-
-function formatSupportedFeatures(features: OpenworkCloudMcpHealth["compatibility"]["supportedFeatures"]): string {
- return Object.entries(features).map(([key, enabled]) => `${key}:${enabled ? "yes" : "no"}`).join(", ");
-}
-
-function formatPluginHashes(hashes: OpenworkCloudMcpHealth["compatibility"]["pluginFileHashes"]): string {
- if (hashes.length === 0) return "none";
- return hashes.map((hash) => `${hash.name}=${hash.sha256 ? hash.sha256.slice(0, 12) : `unavailable${hash.error ? ` (${hash.error})` : ""}`}`).join(", ");
-}
-
-function formatMcpToolExposure(input: { checked: boolean; includesMcpTools: boolean | null; present: string[]; missing: string[]; limitation?: string }): string {
- if (!input.checked) return "not checked";
- const includes = input.includesMcpTools === null ? "unknown" : input.includesMcpTools ? "yes" : "no";
- return `includes MCP tools: ${includes}; present ${joinList(input.present)}; missing ${joinList(input.missing)}${input.limitation ? `; limitation: ${input.limitation}` : ""}`;
-}
-
-interface AdvancedCloudMcpDiagnosticsSectionProps {
- cloudMcpHealth: OpenworkCloudMcpHealth | null;
- onRefresh: () => Promise;
-}
-
-export function AdvancedCloudMcpDiagnosticsSection(props: AdvancedCloudMcpDiagnosticsSectionProps) {
- const [busy, setBusy] = useState(false);
- const [copyStatus, setCopyStatus] = useState(null);
- const [refreshError, setRefreshError] = useState(null);
- const safeHealth = sanitizeCloudMcpHealthDiagnostic(props.cloudMcpHealth);
- const projection = props.cloudMcpHealth?.tools.providerProjection;
- const compatibility = props.cloudMcpHealth?.compatibility;
-
- const refresh = async () => {
- setBusy(true);
- setCopyStatus(null);
- setRefreshError(null);
- try {
- await props.onRefresh();
- } catch (error) {
- setRefreshError(error instanceof Error ? error.message : "Could not refresh Cloud MCP diagnostics.");
- } finally {
- setBusy(false);
- }
- };
-
- const copy = async () => {
- const payload = JSON.stringify({ cloudMcpHealth: safeHealth }, null, 2);
- await navigator.clipboard.writeText(payload);
- setCopyStatus("Copied sanitized Cloud diagnostic.");
- };
-
- return (
-
-
- Agent access diagnostics
-
- Technical details for OpenWork Cloud MCP delivery. Tokens and Authorization headers are redacted before display or copy.
-
-
-
-
-
- OpenWork Cloud MCP health
-
- Use this when support needs exact runtime state. The main Connect card stays user-facing.
-
-
-
-
-
-
-
- {copyStatus ? {copyStatus} : null}
- {refreshError ? {refreshError} : null}
- {props.cloudMcpHealth ? (
-
-
-
-
-
-
-
- {props.cloudMcpHealth.engineInspection?.checked ? (
- `${server.name} ${server.status}${server.error ? ` (${server.error})` : ""}`).join("; ")
- : "none tracked"}
- />
- ) : null}
-
-
-
- {props.cloudMcpHealth.tools.direct.trace ? (
- `${step.step} ${step.ok ? "ok" : "failed"}${step.httpStatus !== undefined ? ` (HTTP ${step.httpStatus})` : ""} ${Math.max(0, Math.round(step.latencyMs))}ms`).join(" → ") || "no steps"}`}
- />
- ) : null}
- {props.cloudMcpHealth.firstFailure ? (
-
- ) : null}
-
-
- {compatibility ? (
- <>
-
-
-
-
-
-
- >
- ) : null}
-
-
-
- Show sanitized health JSON
-
- {JSON.stringify(safeHealth, null, 2)}
-
-
-
- ) : (
- No Cloud MCP health has been loaded for this workspace yet.
- )}
-
-
- );
-}
-
interface AdvancedRuntimeConfigSourcesSectionProps {
busy: boolean;
canInspect: boolean;
diff --git a/apps/app/src/react-app/domains/settings/pages/advanced-view.tsx b/apps/app/src/react-app/domains/settings/pages/advanced-view.tsx
index 7122a936c3..91705dde8b 100644
--- a/apps/app/src/react-app/domains/settings/pages/advanced-view.tsx
+++ b/apps/app/src/react-app/domains/settings/pages/advanced-view.tsx
@@ -7,16 +7,17 @@ import { ADVANCED_SETTINGS_SECTIONS } from "../advanced-sections";
import { Separator } from "@/components/ui/separator";
import type { OpencodeConnectStatus } from "@/app/types";
+import { readDenSettings } from "@/app/lib/den";
import type { OpenworkServerClient, OpenworkCloudMcpHealth, OpenworkRuntimeConfigStatus, OpenworkServerStatus } from "@/app/lib/openwork-server";
import { t } from "@/i18n";
import { LayoutStack } from "../settings-layout";
import type { useDenSession } from "../cloud/use-den-session";
+import { AdvancedDiagnosticsSection } from "./advanced-diagnostics-section";
import { advancedLocalReducer, initialAdvancedLocalState } from "./advanced-view-state";
import {
AdvancedDeveloperSection,
AdvancedEngineV2PreviewSection,
- AdvancedCloudMcpDiagnosticsSection,
AdvancedOrganizationServerSection,
AdvancedRuntimeConfigSourcesSection,
AdvancedRuntimeSection,
@@ -53,6 +54,9 @@ export type AdvancedViewProps = {
cloudMcpUrl: string | null;
cloudMcpHealth: OpenworkCloudMcpHealth | null;
refreshCloudMcpHealth: () => Promise;
+ canCheckCloudMcpHealth: boolean;
+ appVersion: string | null;
+ copyDiagnosticsReport: () => Promise;
engineClient: OpenworkServerClient | null;
};
@@ -173,6 +177,8 @@ export function AdvancedView(props: AdvancedViewProps) {
void refreshRuntimeConfigStatus();
}, [props.canInspectRuntimeConfig]);
+ const denSettings = readDenSettings();
+
return (
@@ -199,9 +205,14 @@ export function AdvancedView(props: AdvancedViewProps) {
openworkDetailLines={openworkDetailLines}
/>
-
0 ? parts.join(", ") : null;
+}
diff --git a/apps/app/src/react-app/shell/settings-route.tsx b/apps/app/src/react-app/shell/settings-route.tsx
index 893b8a08e5..fa31521ef4 100644
--- a/apps/app/src/react-app/shell/settings-route.tsx
+++ b/apps/app/src/react-app/shell/settings-route.tsx
@@ -21,6 +21,7 @@ import {
resolveOpenWorkExtensionCatalogPlatform,
} from "@/app/constants";
import type { EnablementContext } from "@/app/enablement";
+import { buildDiagnosticsBundleJson } from "@/app/lib/diagnostics-bundle";
import { createClient, unwrap } from "@/app/lib/opencode";
import {
createOpenworkServerClient,
@@ -1310,6 +1311,23 @@ function SettingsRouteContent(props: SettingsSurfaceProps = {}) {
setCloudMcpHealthResult({ workspaceId, health });
return health;
}, [currentCloudMcpModel, openworkClient, runtimeWorkspaceId, selectedWorkspaceEndpoint]);
+ // Same report as the command palette's "Copy diagnostics", plus the Cloud check shown on the Advanced page.
+ const copyDiagnosticsReport = useCallback(async () => {
+ try {
+ const report = await buildDiagnosticsBundleJson({
+ clientConnected: Boolean(opencodeClient),
+ developerMode,
+ openworkServerStatus: openworkServerSnapshot.openworkServerStatus,
+ openworkServerUrl: openworkClient?.baseUrl ?? "",
+ runtimeWorkspaceId,
+ cloudMcpHealth,
+ });
+ await navigator.clipboard.writeText(report);
+ toast.success(t("session.diagnostics_copied"));
+ } catch (error) {
+ toast.error(t("session.diagnostics_failed"), { description: describeRouteError(error) });
+ }
+ }, [cloudMcpHealth, developerMode, opencodeClient, openworkClient, openworkServerSnapshot.openworkServerStatus, runtimeWorkspaceId]);
const { commandPaletteOpen, setCommandPaletteOpen } = useCommandPaletteShortcut(!props.embedded);
const developerModePaletteItem = useMemo(
() => settingsDeveloperModePaletteItem(developerMode, () => {
@@ -2641,6 +2659,9 @@ function SettingsRouteContent(props: SettingsSurfaceProps = {}) {
}}
cloudMcpHealth={cloudMcpHealth}
refreshCloudMcpHealth={refreshCloudMcpHealth}
+ canCheckCloudMcpHealth={Boolean((selectedWorkspaceEndpoint?.client ?? openworkClient) && runtimeWorkspaceId?.trim())}
+ appVersion={electronUpdaterState.appVersion}
+ copyDiagnosticsReport={copyDiagnosticsReport}
organizationServer={denSession}
engineClient={openworkClient}
/>
diff --git a/apps/app/tests/advanced-diagnostics-section.test.tsx b/apps/app/tests/advanced-diagnostics-section.test.tsx
new file mode 100644
index 0000000000..0c39f8b7c7
--- /dev/null
+++ b/apps/app/tests/advanced-diagnostics-section.test.tsx
@@ -0,0 +1,220 @@
+/** @jsxImportSource react */
+import { afterAll, afterEach, beforeAll, beforeEach, describe, expect, test } from "bun:test";
+import { GlobalRegistrator } from "@happy-dom/global-registrator";
+import { act } from "react";
+import type { Root } from "react-dom/client";
+
+import type { OpenworkCloudMcpHealth } from "../src/app/lib/openwork-server";
+import {
+ diagnosticsVersionLine,
+ summarizeDiagnostics,
+} from "../src/react-app/domains/settings/pages/diagnostics-summary";
+
+const ownedDom = !GlobalRegistrator.isRegistered && typeof document === "undefined";
+let createRoot: typeof import("react-dom/client")["createRoot"];
+let AdvancedDiagnosticsSection: typeof import("../src/react-app/domains/settings/pages/advanced-diagnostics-section")["AdvancedDiagnosticsSection"];
+let root: Root | null = null;
+let host: HTMLDivElement;
+let previousAct: unknown;
+
+function health(overrides: Partial = {}): OpenworkCloudMcpHealth {
+ return {
+ schemaVersion: 1,
+ phase: "ready",
+ usable: true,
+ usableByCurrentModel: true,
+ connectCatalogEnabled: true,
+ workspace: { id: "workspace-1", type: "local", directory: "/tmp/project", path: "/tmp/project" },
+ desired: {
+ present: true,
+ name: "openwork-cloud",
+ revision: "revision-1",
+ config: { enabled: true },
+ token: { present: true, metadata: {} },
+ },
+ delivery: {
+ state: "ready",
+ desiredRevision: "revision-1",
+ appliedRevision: "revision-1",
+ updatedAt: null,
+ appliedAt: null,
+ lastAttemptAt: null,
+ },
+ engine: { status: "connected" },
+ tools: {
+ expected: ["search_capabilities", "execute_capability"],
+ present: ["search_capabilities", "execute_capability"],
+ missing: [],
+ direct: { checked: true, source: "mcp_tools_list", expected: [], present: [], missing: [] },
+ providerProjection: { checked: false, present: [], missing: [] },
+ },
+ pluginCanaries: { expected: [], present: [], missing: [] },
+ compatibility: {
+ openwork: { serverVersion: "0.18.70", app: { version: "0.18.70" } },
+ opencode: { expectedVersion: "2.2.0", actualVersion: "2.2.0", probe: "ok" },
+ pluginFileHashes: [],
+ supportedFeatures: {
+ dynamicMcp: true,
+ directoryScoping: true,
+ toolIds: true,
+ providerToolProjection: true,
+ pluginCanaries: true,
+ },
+ experimentalToolIds: { checked: false, expected: [], present: [], missing: [], includesMcpTools: null },
+ experimentalProviderTools: { checked: false, expected: [], present: [], missing: [], includesMcpTools: null },
+ },
+ toolDenies: [],
+ firstFailure: null,
+ checkedAt: "2026-11-13T18:42:00.000Z",
+ ...overrides,
+ };
+}
+
+const expiredSignIn = health({
+ usable: false,
+ firstFailure: {
+ code: "openwork_cloud_token_expired",
+ stage: "cloud_auth",
+ retryable: true,
+ recommendedAction: "Sign in again.",
+ message: "The Cloud token expired.",
+ },
+});
+
+describe("diagnostics summary", () => {
+ const signedIn = { checking: false, canCheck: true, signedIn: true, orgSelected: true };
+
+ test("working tools read as one reassuring line", () => {
+ expect(summarizeDiagnostics({ ...signedIn, health: health() })).toEqual({
+ tone: "ready",
+ label: "Working",
+ detail: "Your connected tools are ready to use.",
+ });
+ });
+
+ test("a failure names what went wrong in plain words, not the failure code", () => {
+ const summary = summarizeDiagnostics({ ...signedIn, health: expiredSignIn });
+ expect(summary).toEqual({
+ tone: "error",
+ label: "Needs attention",
+ detail: "Cloud authentication expired. Copy the report and send it to OpenWork support.",
+ });
+ expect(summary.detail).not.toContain("openwork_cloud_token_expired");
+ });
+
+ test("tools switched off are not reported as a problem", () => {
+ const off = health({ usable: false, desired: { ...health().desired, config: { enabled: false } } });
+ expect(summarizeDiagnostics({ ...signedIn, health: off }).label).toBe("Turned off");
+ });
+
+ test("without a result it says what is missing instead of guessing a status", () => {
+ expect(summarizeDiagnostics({ ...signedIn, health: null }).label).toBe("Not checked");
+ expect(summarizeDiagnostics({ ...signedIn, signedIn: false, health: null }).label).toBe("Signed out");
+ expect(summarizeDiagnostics({ ...signedIn, orgSelected: false, health: null }).label).toBe("No organization");
+ expect(summarizeDiagnostics({ ...signedIn, checking: true, health: null }).label).toBe("Checking");
+ expect(summarizeDiagnostics({ ...signedIn, canCheck: false, health: null }).detail)
+ .toBe("The check runs once this workspace is connected.");
+ });
+
+ test("checking again keeps the last result on screen", () => {
+ expect(summarizeDiagnostics({ ...signedIn, checking: true, health: expiredSignIn }).label).toBe("Needs attention");
+ });
+
+ test("the version line leads with the app version and works before any check", () => {
+ expect(diagnosticsVersionLine({ appVersion: "0.18.70", health: null })).toBe("OpenWork v0.18.70");
+ expect(diagnosticsVersionLine({ appVersion: null, health: health() })).toBe("OpenWork v0.18.70, OpenCode 2.2.0");
+ expect(diagnosticsVersionLine({ appVersion: null, health: null })).toBeNull();
+ });
+});
+
+describe("Diagnostics section", () => {
+ beforeAll(async () => {
+ if (ownedDom) GlobalRegistrator.register({ url: "http://localhost/" });
+ createRoot = (await import("react-dom/client")).createRoot;
+ AdvancedDiagnosticsSection = (await import("../src/react-app/domains/settings/pages/advanced-diagnostics-section")).AdvancedDiagnosticsSection;
+ });
+ afterAll(async () => { if (ownedDom) await GlobalRegistrator.unregister(); });
+ beforeEach(() => {
+ previousAct = Reflect.get(globalThis, "IS_REACT_ACT_ENVIRONMENT");
+ Reflect.set(globalThis, "IS_REACT_ACT_ENVIRONMENT", true);
+ host = document.createElement("div");
+ document.body.append(host);
+ root = createRoot(host);
+ });
+ afterEach(async () => {
+ if (root) await act(async () => root?.unmount());
+ root = null;
+ host.remove();
+ Reflect.set(globalThis, "IS_REACT_ACT_ENVIRONMENT", previousAct);
+ });
+
+ async function render(props: Partial[0]> = {}) {
+ const calls = { refresh: 0, copy: 0 };
+ await act(async () => root?.render(
+ { calls.refresh += 1; return null; }}
+ onCopyReport={async () => { calls.copy += 1; }}
+ {...props}
+ />,
+ ));
+ return calls;
+ }
+
+ function button(label: string) {
+ return [...host.querySelectorAll("button")]
+ .find((candidate) => candidate.textContent?.trim() === label);
+ }
+
+ test("shows the status and app version up front and keeps the technical rows folded away", async () => {
+ await render();
+ const summary = host.querySelector('[data-testid="diagnostics-summary"]')?.textContent ?? "";
+ expect(summary).toContain("Working");
+ expect(summary).toContain("OpenWork v0.18.70, OpenCode 2.2.0 — checked ");
+ const details = host.querySelector("details");
+ expect(details?.querySelector("summary")?.textContent?.trim()).toBe("Technical details");
+ expect(details?.open).toBe(false);
+ expect(details?.textContent).toContain("Desired revision");
+ });
+
+ test("Copy report hands the whole report to the caller in one click", async () => {
+ const calls = await render();
+ await act(async () => { button("Copy report")?.click(); });
+ expect(calls.copy).toBe(1);
+ });
+
+ test("copying still works before anything has been checked", async () => {
+ const calls = await render({ cloudMcpHealth: null, signedIn: false });
+ expect(host.textContent).toContain("Signed out");
+ expect(host.textContent).toContain("OpenWork v0.18.70");
+ expect(host.querySelector("details")).toBeNull();
+ await act(async () => { button("Copy report")?.click(); });
+ expect(calls).toEqual({ refresh: 0, copy: 1 });
+ });
+
+ test("opening it signed in with no result runs the check once", async () => {
+ const first = await render({ cloudMcpHealth: null });
+ expect(first.refresh).toBe(1);
+ const rerender = await render({ cloudMcpHealth: null });
+ expect(rerender.refresh).toBe(0);
+ });
+
+ test("the page can open before the workspace is connected; the check runs when it is", async () => {
+ const early = await render({ cloudMcpHealth: null, canCheck: false });
+ expect(early.refresh).toBe(0);
+ expect(host.textContent).toContain("The check runs once this workspace is connected.");
+ expect(button("Check again")?.disabled).toBe(true);
+ const connected = await render({ cloudMcpHealth: null, canCheck: true });
+ expect(connected.refresh).toBe(1);
+ });
+
+ test("a failed check says so instead of leaving a stale status", async () => {
+ await render({ cloudMcpHealth: null, onRefresh: async () => { throw new Error("OpenWork server is not running."); } });
+ expect(host.textContent).toContain("OpenWork server is not running.");
+ expect(host.textContent).toContain("Not checked");
+ });
+});
diff --git a/evals/proofs/diagnostics-copy-report.proof.test.ts b/evals/proofs/diagnostics-copy-report.proof.test.ts
new file mode 100644
index 0000000000..bbe5482d18
--- /dev/null
+++ b/evals/proofs/diagnostics-copy-report.proof.test.ts
@@ -0,0 +1,79 @@
+import { expect } from "vitest";
+import { spec } from "@openwork/testkit";
+import { accountDiagnostics } from "../worlds/account-diagnostics.ts";
+
+const test = spec.world(accountDiagnostics, {
+ timeout: 300_000,
+ resources: { surfaces: ["appWeb"], services: ["den"] },
+ needs: { commands: ["bun"] },
+});
+
+const SECTION = "#advanced-agent-access";
+
+test("a member whose connected tools stopped working sees what is wrong and copies one report to share", async ({ world, user, probe, step, evidence }) => {
+ /** The status lines, as the member reads them. */
+ const status = async () => (await probe.dom('[data-testid="diagnostics-summary"]')).elements[0]?.text.replace(/\s+/g, " ").trim() ?? "";
+ const technicalDetails = async () => ({
+ all: (await probe.dom(`${SECTION} details`)).elements.length,
+ open: (await probe.dom(`${SECTION} details[open]`)).elements.length,
+ });
+ await user.resizeViewport({ width: 1280, height: 900, deviceScaleFactor: 1 });
+
+ await step("given: the member's connection is off, so the account menu offers Run diagnostics", async () => {
+ await user.click({ testId: "account-status-menu" });
+ await user.see({ role: "menuitem", label: "Run diagnostics" }, { timeoutMs: 30_000 });
+ await user.screenshot();
+ });
+
+ await step("after: Run diagnostics shows one status line, the versions and a Copy report button", async () => {
+ await user.click({ role: "menuitem", label: "Run diagnostics" });
+ await user.see({ role: "button", label: "Copy report" }, { timeoutMs: 30_000 });
+ // The check starts without another click and ends on a result, not a spinner.
+ const line = await probe.eventually(status, {
+ within: 90_000, label: "the check finishes without another click",
+ until: text => ["Working", "Turned off", "Needs attention"].some(result => text.startsWith(result)),
+ });
+ const details = await technicalDetails();
+ await user.notSee({ role: "button", label: "Copy sanitized diagnostic" });
+ const short = details.all > 0 && details.open === 0;
+ evidence.recordAssertionEvidence(
+ "The state is one plain line and the technical rows stay closed",
+ `status reads: ${line}; technical details open: ${details.open} of ${details.all}`,
+ short,
+ );
+ await user.screenshot();
+ expect(short, "the technical rows are present but closed").toBe(true);
+ });
+
+ await step("after: Copy report puts the full report on the clipboard in one click", async () => {
+ await user.click({ role: "button", label: "Copy report" });
+ await user.see({ text: "Diagnostics copied" }, { timeoutMs: 30_000 });
+ const copied = await probe.eventually(() => world.readClipboard(), {
+ within: 15_000, label: "the report reached the clipboard", until: value => value.includes('"capturedAt"'),
+ });
+ const report: unknown = JSON.parse(copied);
+ const has = (key: string) => typeof report === "object" && report !== null && Reflect.get(report, key) != null;
+ // Witness: the copied text is the whole report, including the check shown on screen.
+ const whole = has("capturedAt") && has("openworkServer") && has("cloudMcp") && has("developerLogs");
+ evidence.recordAssertionEvidence(
+ "One click copies the whole report, with the result of the check in it",
+ `clipboard: ${copied.length} characters of JSON; has the capture time, server state, connected-tools check and recent logs: ${whole}`,
+ whole,
+ );
+ await user.screenshot();
+ expect(whole, "the copied report carries the connected-tools check and recent logs").toBe(true);
+ });
+
+ await step("the technical rows open with one click for whoever is helping", async () => {
+ await user.click({ text: "Technical details" });
+ await user.see({ text: "Desired revision" });
+ const details = await technicalDetails();
+ evidence.recordAssertionEvidence(
+ "The exact runtime state is still one click away",
+ `technical details open: ${details.open} of ${details.all}; "Desired revision" row visible`,
+ details.open === 1,
+ );
+ await user.screenshot();
+ expect(details.open).toBe(1);
+ });
+});
diff --git a/evals/worlds/account-diagnostics.ts b/evals/worlds/account-diagnostics.ts
index 8ccabf001d..ab58b2dad7 100644
--- a/evals/worlds/account-diagnostics.ts
+++ b/evals/worlds/account-diagnostics.ts
@@ -15,5 +15,13 @@ export async function accountDiagnostics(seed: Seed) {
const { app, workspace } = await signedInAppWeb(seed, {
den, webPort, workspacePath, name: "account-diagnostics", engine: "v2", headless: true,
});
- return { app, workspace, advancedUrl: `${app.webUrl}/settings/advanced/developer` };
+ // Headless Chrome only lets the page write and read the clipboard once granted.
+ await app.client.send("Browser.grantPermissions", {
+ origin: new URL(app.webUrl).origin, permissions: ["clipboardReadWrite", "clipboardSanitizedWrite"],
+ });
+ return {
+ app, workspace, advancedUrl: `${app.webUrl}/settings/advanced/developer`,
+ // TODO(primitive): probe.clipboard should expose this read-only witness without browser evaluation.
+ readClipboard: () => seed.evalIn(app, () => navigator.clipboard.readText(), { awaitPromise: true }),
+ };
}