From de45cbc31984152d0a1fef67a11aae8e856ab0ed Mon Sep 17 00:00:00 2001 From: Val Alexander Date: Mon, 13 Apr 2026 09:10:31 -0500 Subject: [PATCH] Extract OpenClaw gateway settings flow --- .../OrchestrationEngineHarness.integration.ts | 2 + .../providerService.integration.test.ts | 2 + apps/server/src/openclaw/GatewayClient.ts | 2 +- .../Layers/CheckpointReactor.test.ts | 3 +- .../Layers/ProviderRuntimeIngestion.test.ts | 2 +- .../Layers/OpenclawGatewayConfig.ts | 2 +- .../Layers/ProviderRuntimeEventFeed.test.ts | 9 +- .../Layers/ProviderRuntimeEventFeed.ts | 6 +- .../src/components/chat/ProviderSetupCard.tsx | 5 - .../components/pr-review/PrReviewShell.tsx | 4 +- apps/web/src/lib/snapshotSyncManager.test.ts | 4 +- apps/web/src/routes/_chat.settings.tsx | 487 ++++++++++++++++-- 12 files changed, 472 insertions(+), 56 deletions(-) diff --git a/apps/server/integration/OrchestrationEngineHarness.integration.ts b/apps/server/integration/OrchestrationEngineHarness.integration.ts index b59bfa6de..30014dea4 100644 --- a/apps/server/integration/OrchestrationEngineHarness.integration.ts +++ b/apps/server/integration/OrchestrationEngineHarness.integration.ts @@ -39,6 +39,7 @@ import { EnvironmentVariablesLive } from "../src/persistence/Services/Environmen import { ProviderUnsupportedError } from "../src/provider/Errors.ts"; import { ProviderAdapterRegistry } from "../src/provider/Services/ProviderAdapterRegistry.ts"; import { ProviderSessionDirectoryLive } from "../src/provider/Layers/ProviderSessionDirectory.ts"; +import { ProviderRuntimeEventFeedLive } from "../src/provider/Layers/ProviderRuntimeEventFeed.ts"; import { makeProviderServiceLive } from "../src/provider/Layers/ProviderService.ts"; import { makeCodexAdapterLive } from "../src/provider/Layers/CodexAdapter.ts"; import { CodexAdapter } from "../src/provider/Services/CodexAdapter.ts"; @@ -298,6 +299,7 @@ export const makeOrchestrationIntegrationHarness = ( Layer.provideMerge(ProjectionPendingApprovalRepositoryLive), Layer.provideMerge(checkpointStoreLayer), Layer.provideMerge(providerLayer), + Layer.provideMerge(ProviderRuntimeEventFeedLive), Layer.provideMerge(RuntimeReceiptBusLive), ); const runtimeIngestionLayer = ProviderRuntimeIngestionLive.pipe( diff --git a/apps/server/integration/providerService.integration.test.ts b/apps/server/integration/providerService.integration.test.ts index 7576896ba..f39c42c49 100644 --- a/apps/server/integration/providerService.integration.test.ts +++ b/apps/server/integration/providerService.integration.test.ts @@ -7,6 +7,7 @@ import { Effect, FileSystem, Layer, Path, Queue, Stream } from "effect"; import { ProviderUnsupportedError } from "../src/provider/Errors.ts"; import { ProviderAdapterRegistry } from "../src/provider/Services/ProviderAdapterRegistry.ts"; import { ProviderSessionDirectoryLive } from "../src/provider/Layers/ProviderSessionDirectory.ts"; +import { ProviderRuntimeEventFeedLive } from "../src/provider/Layers/ProviderRuntimeEventFeed.ts"; import { makeProviderServiceLive } from "../src/provider/Layers/ProviderService.ts"; import { ProviderService, @@ -59,6 +60,7 @@ const makeIntegrationFixture = Effect.gen(function* () { const shared = Layer.mergeAll( directoryLayer, Layer.succeed(ProviderAdapterRegistry, registry), + ProviderRuntimeEventFeedLive, ).pipe(Layer.provide(SqlitePersistenceMemory)); const layer = makeProviderServiceLive().pipe(Layer.provide(shared)); diff --git a/apps/server/src/openclaw/GatewayClient.ts b/apps/server/src/openclaw/GatewayClient.ts index 3bff51c5f..734bbee23 100644 --- a/apps/server/src/openclaw/GatewayClient.ts +++ b/apps/server/src/openclaw/GatewayClient.ts @@ -466,7 +466,7 @@ export class OpenclawGatewayClient { if (frame.type === "event" && typeof frame.event === "string") { let matchedWaiter = false; - for (const waiter of this.pendingEventWaiters) { + for (const waiter of [...this.pendingEventWaiters]) { if (waiter.eventName === frame.event) { matchedWaiter = true; this.pendingEventWaiters.delete(waiter); diff --git a/apps/server/src/orchestration/Layers/CheckpointReactor.test.ts b/apps/server/src/orchestration/Layers/CheckpointReactor.test.ts index c7fad3350..1196c8e1c 100644 --- a/apps/server/src/orchestration/Layers/CheckpointReactor.test.ts +++ b/apps/server/src/orchestration/Layers/CheckpointReactor.test.ts @@ -99,6 +99,7 @@ function createProviderServiceHarness( return { service, + emit: (event: LegacyProviderRuntimeEvent) => event, rollbackConversation, }; } @@ -209,7 +210,7 @@ async function waitForGitRefExists(cwd: string, ref: string, timeoutMs = 15_000) describe("CheckpointReactor", () => { let runtime: ManagedRuntime.ManagedRuntime< - OrchestrationEngineService | CheckpointReactor | CheckpointStore, + OrchestrationEngineService | CheckpointReactor | CheckpointStore | ProviderRuntimeEventFeed, unknown > | null = null; let scope: Scope.Closeable | null = null; diff --git a/apps/server/src/orchestration/Layers/ProviderRuntimeIngestion.test.ts b/apps/server/src/orchestration/Layers/ProviderRuntimeIngestion.test.ts index 3697399fc..e006881a2 100644 --- a/apps/server/src/orchestration/Layers/ProviderRuntimeIngestion.test.ts +++ b/apps/server/src/orchestration/Layers/ProviderRuntimeIngestion.test.ts @@ -125,7 +125,7 @@ type ProviderRuntimeTestCheckpoint = ProviderRuntimeTestThread["checkpoints"][nu describe("ProviderRuntimeIngestion", () => { let runtime: ManagedRuntime.ManagedRuntime< - OrchestrationEngineService | ProviderRuntimeIngestionService, + OrchestrationEngineService | ProviderRuntimeIngestionService | ProviderRuntimeEventFeed, unknown > | null = null; let scope: Scope.Closeable | null = null; diff --git a/apps/server/src/persistence/Layers/OpenclawGatewayConfig.ts b/apps/server/src/persistence/Layers/OpenclawGatewayConfig.ts index 310e0939d..26ec67d32 100644 --- a/apps/server/src/persistence/Layers/OpenclawGatewayConfig.ts +++ b/apps/server/src/persistence/Layers/OpenclawGatewayConfig.ts @@ -68,7 +68,7 @@ function normalizeScopes(scopes: ReadonlyArray | undefined): string[] { unique.add(trimmed); } } - return [...unique].toSorted((left, right) => left.localeCompare(right)); + return [...unique].sort((left, right) => left.localeCompare(right)); } function fromGeneratedIdentity(identity: ReturnType) { diff --git a/apps/server/src/provider/Layers/ProviderRuntimeEventFeed.test.ts b/apps/server/src/provider/Layers/ProviderRuntimeEventFeed.test.ts index 207ee2288..073c6f6d1 100644 --- a/apps/server/src/provider/Layers/ProviderRuntimeEventFeed.test.ts +++ b/apps/server/src/provider/Layers/ProviderRuntimeEventFeed.test.ts @@ -1,7 +1,7 @@ import { EventId, ThreadId, TurnId, type ProviderRuntimeEvent } from "@okcode/contracts"; import { it } from "@effect/vitest"; import { describe, expect } from "vitest"; -import { Effect, Layer, Stream } from "effect"; +import { Effect, Fiber, Stream } from "effect"; import { ProviderRuntimeEventFeedLive } from "./ProviderRuntimeEventFeed.ts"; import { ProviderRuntimeEventFeed } from "../Services/ProviderRuntimeEventFeed.ts"; @@ -11,6 +11,7 @@ function makeTurnStartedEvent(id: string): ProviderRuntimeEvent { type: "turn.started", eventId: EventId.makeUnsafe(id), provider: "codex", + payload: {}, threadId: ThreadId.makeUnsafe("thread-1"), turnId: TurnId.makeUnsafe(`turn-${id}`), createdAt: "2026-01-01T00:00:00.000Z", @@ -27,17 +28,17 @@ describe("ProviderRuntimeEventFeedLive", () => { const events = yield* Stream.take(feed.subscribeWithReplay(), 3).pipe( Stream.runCollect, - Effect.fork, + Effect.forkChild, ); yield* feed.publish(makeTurnStartedEvent("evt-3")); - const collected = yield* Effect.fromFiber(events); + const collected = yield* Fiber.join(events); expect(Array.from(collected).map((event) => event.eventId)).toEqual([ "evt-1", "evt-2", "evt-3", ]); - }).pipe(Effect.provide(Layer.mergeAll(ProviderRuntimeEventFeedLive))), + }).pipe(Effect.provide(ProviderRuntimeEventFeedLive)), ); }); diff --git a/apps/server/src/provider/Layers/ProviderRuntimeEventFeed.ts b/apps/server/src/provider/Layers/ProviderRuntimeEventFeed.ts index 341c308a5..0a4c7a81a 100644 --- a/apps/server/src/provider/Layers/ProviderRuntimeEventFeed.ts +++ b/apps/server/src/provider/Layers/ProviderRuntimeEventFeed.ts @@ -48,8 +48,9 @@ const makeProviderRuntimeEventFeed = Effect.gen(function* () { ); const subscribeWithReplay: ProviderRuntimeEventFeedShape["subscribeWithReplay"] = () => - Stream.unwrapScoped( + Stream.unwrap( Effect.gen(function* () { + const scope = yield* Effect.scope; const subscriber = yield* Queue.unbounded(); const replay = yield* mutex.withPermits(1)( Ref.modify(stateRef, (state) => { @@ -69,7 +70,8 @@ const makeProviderRuntimeEventFeed = Effect.gen(function* () { discard: true, }); - yield* Scope.addFinalizer(() => + yield* Scope.addFinalizer( + scope, mutex.withPermits(1)( Ref.update(stateRef, (state) => { const subscribers = new Set(state.subscribers); diff --git a/apps/web/src/components/chat/ProviderSetupCard.tsx b/apps/web/src/components/chat/ProviderSetupCard.tsx index 62b4b19aa..811a0fa1e 100644 --- a/apps/web/src/components/chat/ProviderSetupCard.tsx +++ b/apps/web/src/components/chat/ProviderSetupCard.tsx @@ -37,11 +37,6 @@ const PROVIDER_CONFIG = { verifyCmd: "gh auth status", note: undefined, }, - copilot: { - installCmd: "npm install -g @github/copilot", - authCmd: "copilot login", - verifyCmd: "gh auth status", - }, } as const; function StatusIcon({ status }: { status: ServerProviderStatus["status"] }) { diff --git a/apps/web/src/components/pr-review/PrReviewShell.tsx b/apps/web/src/components/pr-review/PrReviewShell.tsx index 3c67d2969..6d6c726f5 100644 --- a/apps/web/src/components/pr-review/PrReviewShell.tsx +++ b/apps/web/src/components/pr-review/PrReviewShell.tsx @@ -410,7 +410,9 @@ export function PrReviewShell({ ...(conflictQuery.data?.status === "conflicted" ? ["Merge conflicts must be resolved"] : []), ...checksSummary.failing.map((name) => `Failing check: ${name}`), ...checksSummary.pending.map((name) => `Pending check: ${name}`), - ...blockingWorkflowStepsComputed.map((step) => `Workflow blocked: ${step.title}`), + ...blockingWorkflowStepsComputed.map( + (step) => `Workflow blocked: ${step.detail ?? step.stepId}`, + ), ]; const approveDisabled = submitReviewMutation.isPending || diff --git a/apps/web/src/lib/snapshotSyncManager.test.ts b/apps/web/src/lib/snapshotSyncManager.test.ts index d2ed06e9e..5639142e8 100644 --- a/apps/web/src/lib/snapshotSyncManager.test.ts +++ b/apps/web/src/lib/snapshotSyncManager.test.ts @@ -47,7 +47,7 @@ describe("createSnapshotSyncManager", () => { }); it("coalesces overlapping sync requests and reruns once after success", async () => { - let resolveFetch: ((snapshot: OrchestrationReadModel) => void) | null = null; + let resolveFetch!: (snapshot: OrchestrationReadModel) => void; const fetchSnapshot = vi .fn<() => Promise>() .mockImplementation( @@ -69,7 +69,7 @@ describe("createSnapshotSyncManager", () => { expect(fetchSnapshot).toHaveBeenCalledTimes(1); expect(firstSync).toBe(secondSync); - resolveFetch?.(makeSnapshot(1)); + resolveFetch(makeSnapshot(1)); await firstSync; await Promise.resolve(); diff --git a/apps/web/src/routes/_chat.settings.tsx b/apps/web/src/routes/_chat.settings.tsx index 6569ebd66..9a1366116 100644 --- a/apps/web/src/routes/_chat.settings.tsx +++ b/apps/web/src/routes/_chat.settings.tsx @@ -108,7 +108,11 @@ import { getSelectableThreadProviders, isProviderReadyForThreadSelection, } from "../lib/providerAvailability"; -import { serverConfigQueryOptions, serverQueryKeys } from "../lib/serverReactQuery"; +import { + openclawGatewayConfigQueryOptions, + serverConfigQueryOptions, + serverQueryKeys, +} from "../lib/serverReactQuery"; import { cn } from "../lib/utils"; import { ensureNativeApi, readNativeApi } from "../nativeApi"; import { useStore } from "../store"; @@ -797,6 +801,7 @@ function SettingsRouteView() { const { theme, setTheme, colorTheme, setColorTheme, fontFamily, setFontFamily } = useTheme(); const { settings, defaults, updateSettings, resetSettings } = useAppSettings(); const serverConfigQuery = useQuery(serverConfigQueryOptions()); + const openclawGatewayConfigQuery = useQuery(openclawGatewayConfigQueryOptions()); const queryClient = useQueryClient(); const trimmedBrowserPreviewStartPageUrl = settings.browserPreviewStartPageUrl.trim(); const browserPreviewStartPageValidation = @@ -847,6 +852,12 @@ function SettingsRouteView() { null, ); const [openclawTestLoading, setOpenclawTestLoading] = useState(false); + const [openclawGatewayDraft, setOpenclawGatewayDraft] = useState(null); + const [openclawSharedSecretDraft, setOpenclawSharedSecretDraft] = useState(""); + const [openclawSaveLoading, setOpenclawSaveLoading] = useState(false); + const [openclawResetLoading, setOpenclawResetLoading] = useState<"token" | "identity" | null>( + null, + ); const { copyToClipboard: copyOpenclawDebugReport, isCopied: openclawDebugReportCopied } = useCopyToClipboard(); @@ -887,14 +898,21 @@ function SettingsRouteView() { CLAUDE_AUTH_TOKEN_HELPER_PRESETS.find( (preset) => preset.command === claudeAuthTokenHelperCommand, )?.label ?? ""; + const savedOpenclawGatewayUrl = openclawGatewayConfigQuery.data?.gatewayUrl ?? ""; + const savedOpenclawHasSharedSecret = openclawGatewayConfigQuery.data?.hasSharedSecret ?? false; + const effectiveOpenclawGatewayUrl = openclawGatewayDraft ?? savedOpenclawGatewayUrl; const keybindingsConfigPath = serverConfigQuery.data?.keybindingsConfigPath ?? null; const availableEditors = serverConfigQuery.data?.availableEditors; const providerStatuses = serverConfigQuery.data?.providers ?? []; const selectableProviders = getSelectableThreadProviders({ statuses: providerStatuses, - openclawGatewayUrl: settings.openclawGatewayUrl, + openclawGatewayUrl: effectiveOpenclawGatewayUrl, claudeAuthTokenHelperCommand: settings.claudeAuthTokenHelperCommand, }); + const canImportLegacyOpenclawSettings = + openclawGatewayConfigQuery.isSuccess && + !savedOpenclawGatewayUrl && + Boolean(settings.openclawGatewayUrl || settings.openclawPassword); const gitTextGenerationModelOptions = getAppModelOptions( "codex", @@ -934,8 +952,8 @@ function SettingsRouteView() { settings.codexBinaryPath !== defaults.codexBinaryPath || settings.codexHomePath !== defaults.codexHomePath; const isOpenClawSettingsDirty = - settings.openclawGatewayUrl !== defaults.openclawGatewayUrl || - settings.openclawPassword !== defaults.openclawPassword; + (openclawGatewayDraft !== null && openclawGatewayDraft !== savedOpenclawGatewayUrl) || + openclawSharedSecretDraft.length > 0; const changedSettingLabels = [ ...(theme !== "system" ? ["Theme"] : []), ...(colorTheme !== DEFAULT_COLOR_THEME ? ["Color theme"] : []), @@ -1090,9 +1108,11 @@ function SettingsRouteView() { setOpenclawTestResult(null); try { const api = ensureNativeApi(); + const gatewayUrl = effectiveOpenclawGatewayUrl.trim(); + const sharedSecret = openclawSharedSecretDraft.trim(); const result = await api.server.testOpenclawGateway({ - gatewayUrl: settings.openclawGatewayUrl, - password: settings.openclawPassword || undefined, + ...(gatewayUrl ? { gatewayUrl } : {}), + password: sharedSecret || undefined, }); setOpenclawTestResult(result); } catch (err) { @@ -1105,13 +1125,110 @@ function SettingsRouteView() { } finally { setOpenclawTestLoading(false); } - }, [openclawTestLoading, settings.openclawGatewayUrl, settings.openclawPassword]); + }, [effectiveOpenclawGatewayUrl, openclawSharedSecretDraft, openclawTestLoading]); const handleCopyOpenclawDebugReport = useCallback(() => { if (!openclawTestResult) return; copyOpenclawDebugReport(formatOpenclawGatewayDebugReport(openclawTestResult), undefined); }, [copyOpenclawDebugReport, openclawTestResult]); + const saveOpenclawGatewayConfig = useCallback(async () => { + if (openclawSaveLoading) return; + const gatewayUrl = effectiveOpenclawGatewayUrl.trim(); + if (!gatewayUrl) { + throw new Error("Gateway URL is required."); + } + + setOpenclawSaveLoading(true); + try { + const api = ensureNativeApi(); + const sharedSecret = openclawSharedSecretDraft.trim(); + const summary = await api.server.saveOpenclawGatewayConfig({ + gatewayUrl, + ...(sharedSecret ? { sharedSecret } : {}), + }); + queryClient.setQueryData(serverQueryKeys.openclawGatewayConfig(), summary); + setOpenclawGatewayDraft(null); + setOpenclawSharedSecretDraft(""); + setOpenclawTestResult(null); + } finally { + setOpenclawSaveLoading(false); + } + }, [effectiveOpenclawGatewayUrl, openclawSaveLoading, openclawSharedSecretDraft, queryClient]); + + const clearSavedOpenclawSharedSecret = useCallback(async () => { + const gatewayUrl = effectiveOpenclawGatewayUrl.trim(); + if (!gatewayUrl) { + throw new Error("Gateway URL is required before clearing the saved secret."); + } + + setOpenclawSaveLoading(true); + try { + const api = ensureNativeApi(); + const summary = await api.server.saveOpenclawGatewayConfig({ + gatewayUrl, + clearSharedSecret: true, + }); + queryClient.setQueryData(serverQueryKeys.openclawGatewayConfig(), summary); + setOpenclawSharedSecretDraft(""); + setOpenclawTestResult(null); + } finally { + setOpenclawSaveLoading(false); + } + }, [effectiveOpenclawGatewayUrl, queryClient]); + + const resetOpenclawDeviceState = useCallback( + async (regenerateIdentity: boolean) => { + if (openclawResetLoading) return; + setOpenclawResetLoading(regenerateIdentity ? "identity" : "token"); + try { + const api = ensureNativeApi(); + const summary = await api.server.resetOpenclawGatewayDeviceState({ + regenerateIdentity, + }); + queryClient.setQueryData(serverQueryKeys.openclawGatewayConfig(), summary); + setOpenclawTestResult(null); + } finally { + setOpenclawResetLoading(null); + } + }, + [openclawResetLoading, queryClient], + ); + + const importLegacyOpenclawSettings = useCallback(async () => { + const gatewayUrl = settings.openclawGatewayUrl.trim(); + if (!gatewayUrl) { + throw new Error("Legacy OpenClaw settings do not contain a gateway URL."); + } + + setOpenclawSaveLoading(true); + try { + const api = ensureNativeApi(); + const sharedSecret = settings.openclawPassword.trim(); + const summary = await api.server.saveOpenclawGatewayConfig({ + gatewayUrl, + ...(sharedSecret ? { sharedSecret } : {}), + }); + queryClient.setQueryData(serverQueryKeys.openclawGatewayConfig(), summary); + updateSettings({ + openclawGatewayUrl: defaults.openclawGatewayUrl, + openclawPassword: defaults.openclawPassword, + }); + setOpenclawGatewayDraft(null); + setOpenclawSharedSecretDraft(""); + setOpenclawTestResult(null); + } finally { + setOpenclawSaveLoading(false); + } + }, [ + defaults.openclawGatewayUrl, + defaults.openclawPassword, + queryClient, + settings.openclawGatewayUrl, + settings.openclawPassword, + updateSettings, + ]); + const addCustomModel = useCallback( (provider: ProviderKind) => { const customModelInput = customModelInputByProvider[provider]; @@ -2499,7 +2616,7 @@ function SettingsRouteView() { providerStatuses.find((status) => status.provider === provider) ?? null } - openclawGatewayUrl={settings.openclawGatewayUrl} + openclawGatewayUrl={effectiveOpenclawGatewayUrl} claudeAuthTokenHelperCommand={settings.claudeAuthTokenHelperCommand} /> ))} @@ -2737,25 +2854,45 @@ function SettingsRouteView() { title="OpenClaw gateway" description="Connect to an OpenClaw gateway for remote agent sessions." status={ - settings.openclawGatewayUrl.trim().length > 0 - ? `Configured for ${settings.openclawGatewayUrl}` + savedOpenclawGatewayUrl + ? `Saved for ${savedOpenclawGatewayUrl}` : "Not configured" } resetAction={ isOpenClawSettingsDirty ? ( - updateSettings({ - openclawGatewayUrl: defaults.openclawGatewayUrl, - openclawPassword: defaults.openclawPassword, - }) - } + onClick={() => { + setOpenclawGatewayDraft(null); + setOpenclawSharedSecretDraft(""); + setOpenclawTestResult(null); + }} /> ) : null } >
+ {canImportLegacyOpenclawSettings ? ( +
+
+
+ Legacy local settings were found for OpenClaw. Import them into the + persisted gateway config to unlock saved credentials and device + state. +
+ +
+
+ ) : null} +
+
+ Saved gateway:{" "} + + {savedOpenclawGatewayUrl || "Not saved"} + +
+
+ Saved shared secret:{" "} + + {savedOpenclawHasSharedSecret ? "Configured" : "Not configured"} + +
+
+ Device fingerprint:{" "} + + {openclawGatewayConfigQuery.data?.deviceFingerprint ?? "Not created"} + +
+
+ Cached device token:{" "} + + {openclawGatewayConfigQuery.data?.hasDeviceToken + ? "Configured" + : "Not configured"} + +
+
+
- +
+ + + {savedOpenclawHasSharedSecret ? ( + + ) : null} + + +
{openclawTestResult ? ( @@ -2883,6 +3123,177 @@ function SettingsRouteView() {
) : null} + {openclawTestResult.serverInfo ? ( +
+ + Server Info + +
+ {openclawTestResult.serverInfo.version ? ( +
+ Version:{" "} + + {openclawTestResult.serverInfo.version} + +
+ ) : null} + {openclawTestResult.serverInfo.sessionId ? ( +
+ Session:{" "} + + {openclawTestResult.serverInfo.sessionId} + +
+ ) : null} +
+
+ ) : null} + + {openclawTestResult.diagnostics ? ( +
+ + Debugging Context + +
+ {openclawTestResult.diagnostics.normalizedUrl ? ( +
+ Endpoint:{" "} + + {openclawTestResult.diagnostics.normalizedUrl} + +
+ ) : null} + {openclawTestResult.diagnostics.hostKind ? ( +
+ Host type:{" "} + + {describeOpenclawGatewayHostKind( + openclawTestResult.diagnostics.hostKind, + )} + +
+ ) : null} + {openclawTestResult.diagnostics.resolvedAddresses.length > 0 ? ( +
+ Resolved:{" "} + + {openclawTestResult.diagnostics.resolvedAddresses.join( + ", ", + )} + +
+ ) : null} + {describeOpenclawGatewayHealthStatus(openclawTestResult) ? ( +
+ Health probe:{" "} + + {describeOpenclawGatewayHealthStatus(openclawTestResult)} + + {openclawTestResult.diagnostics.healthUrl ? ( + <> + {" "} + at{" "} + + {openclawTestResult.diagnostics.healthUrl} + + + ) : null} +
+ ) : null} + {openclawTestResult.diagnostics.socketCloseCode !== undefined ? ( +
+ Socket close:{" "} + + {openclawTestResult.diagnostics.socketCloseCode} + {openclawTestResult.diagnostics.socketCloseReason + ? ` (${openclawTestResult.diagnostics.socketCloseReason})` + : ""} + +
+ ) : null} + {openclawTestResult.diagnostics.socketError ? ( +
+ Socket error:{" "} + + {openclawTestResult.diagnostics.socketError} + +
+ ) : null} + {openclawTestResult.diagnostics.gatewayErrorCode ? ( +
+ Gateway error code:{" "} + + {openclawTestResult.diagnostics.gatewayErrorCode} + +
+ ) : null} + {openclawTestResult.diagnostics.gatewayErrorDetailCode ? ( +
+ Gateway detail code:{" "} + + {openclawTestResult.diagnostics.gatewayErrorDetailCode} + +
+ ) : null} + {openclawTestResult.diagnostics.gatewayErrorDetailReason ? ( +
+ Gateway detail reason:{" "} + + {openclawTestResult.diagnostics.gatewayErrorDetailReason} + +
+ ) : null} + {openclawTestResult.diagnostics.gatewayRecommendedNextStep ? ( +
+ Gateway next step:{" "} + + {openclawTestResult.diagnostics.gatewayRecommendedNextStep} + +
+ ) : null} + {openclawTestResult.diagnostics.gatewayCanRetryWithDeviceToken !== + undefined ? ( +
+ Device-token retry available:{" "} + + {openclawTestResult.diagnostics + .gatewayCanRetryWithDeviceToken + ? "Yes" + : "No"} + +
+ ) : null} + {openclawTestResult.diagnostics.observedNotifications.length > + 0 ? ( +
+ Gateway events:{" "} + + {openclawTestResult.diagnostics.observedNotifications.join( + ", ", + )} + +
+ ) : null} +
+
+ ) : null} + + {openclawTestResult.diagnostics?.hints.length ? ( +
+ + Troubleshooting + +
    + {openclawTestResult.diagnostics.hints.map((hint) => ( +
  • + + {hint} +
  • + ))} +
+
+ ) : null} + {openclawTestResult.error && !openclawTestResult.steps.some((step) => step.status === "fail") ? (