diff --git a/apps/webclaw/src/routes/api/connect-test.ts b/apps/webclaw/src/routes/api/connect-test.ts new file mode 100644 index 0000000..00ccd7c --- /dev/null +++ b/apps/webclaw/src/routes/api/connect-test.ts @@ -0,0 +1,41 @@ +import { createFileRoute } from '@tanstack/react-router' +import { json } from '@tanstack/react-start' +import { gatewayConnectTest } from '../../server/gateway' + +export const Route = createFileRoute('/api/connect-test')({ + server: { + handlers: { + POST: async ({ request }) => { + try { + const body = (await request.json()) as { + url?: string + token?: string + password?: string + } + + const url = (body.url || 'ws://127.0.0.1:18789').trim() + const token = (body.token || '').trim() + const password = (body.password || '').trim() + + if (!token && !password) { + return json( + { ok: false, error: 'Provide a token or password.' }, + { status: 400 }, + ) + } + + await gatewayConnectTest(url, token, password) + return json({ ok: true }) + } catch (err) { + return json( + { + ok: false, + error: err instanceof Error ? err.message : String(err), + }, + { status: 503 }, + ) + } + }, + }, + }, +}) diff --git a/apps/webclaw/src/routes/connect.tsx b/apps/webclaw/src/routes/connect.tsx index 1cd613f..e1f8f8c 100644 --- a/apps/webclaw/src/routes/connect.tsx +++ b/apps/webclaw/src/routes/connect.tsx @@ -1,54 +1,298 @@ +import { useState, useCallback, useMemo } from 'react' import { createFileRoute } from '@tanstack/react-router' -import { CodeBlock } from '../components/prompt-kit/code-block' +import { HugeiconsIcon } from '@hugeicons/react' +import { + ArrowDown01Icon, + Copy01Icon, + Tick02Icon, +} from '@hugeicons/core-free-icons' +import { Button } from '@/components/ui/button' +import { + Collapsible, + CollapsibleTrigger, + CollapsiblePanel, +} from '@/components/ui/collapsible' +import { CodeBlock } from '@/components/prompt-kit/code-block' export const Route = createFileRoute('/connect')({ component: ConnectRoute, }) +type TestStatus = 'idle' | 'testing' | 'success' | 'error' + function ConnectRoute() { + const [gatewayUrl, setGatewayUrl] = useState('ws://127.0.0.1:18789') + const [token, setToken] = useState('') + const [password, setPassword] = useState('') + const [status, setStatus] = useState('idle') + const [errorMessage, setErrorMessage] = useState('') + + const envSnippet = useMemo(() => { + const lines = [`CLAWDBOT_GATEWAY_URL=${gatewayUrl}`] + if (token) { + lines.push(`CLAWDBOT_GATEWAY_TOKEN=${token}`) + } else if (password) { + lines.push(`CLAWDBOT_GATEWAY_PASSWORD=${password}`) + } else { + lines.push(`CLAWDBOT_GATEWAY_TOKEN=YOUR_TOKEN_HERE`) + } + return lines.join('\n') + }, [gatewayUrl, token, password]) + + const canTest = token.trim() !== '' || password.trim() !== '' + + const handleTest = useCallback(async () => { + if (!canTest) return + setStatus('testing') + setErrorMessage('') + + try { + const res = await fetch('/api/connect-test', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + url: gatewayUrl.trim(), + token: token.trim() || undefined, + password: password.trim() || undefined, + }), + }) + + if (!res.ok) { + const text = await res.text().catch(() => 'Request failed') + setStatus('error') + setErrorMessage(text) + return + } + + const data = (await res.json().catch(() => ({ ok: false, error: 'Invalid response' }))) as { + ok: boolean + error?: string + } + + if (data.ok) { + setStatus('success') + } else { + setStatus('error') + setErrorMessage(data.error ?? 'Connection failed.') + } + } catch (err) { + setStatus('error') + setErrorMessage( + err instanceof Error ? err.message : 'Network error — is the dev server running?', + ) + } + }, [canTest, gatewayUrl, token, password]) + return (
-

+

Connect to WebClaw

-

- This client needs access to your OpenClaw gateway before you can - start chatting. +

+ Enter your OpenClaw gateway details to get started.

-
-

- At the root of the project, create a new file named{' '} - .env.local. -

-
-

Paste this into it:

- -

or:

- + + {/* Connection form */} +
+
+ {/* Gateway URL */} +
+ + { + setGatewayUrl(e.target.value) + setStatus('idle') + }} + placeholder="ws://127.0.0.1:18789" + className="w-full rounded-lg border border-primary-200 bg-primary-50 px-3 py-2 text-sm text-primary-900 placeholder:text-primary-400 focus:border-primary-400 focus:outline-none focus:ring-2 focus:ring-primary-200 transition-colors" + /> +

+ Your OpenClaw gateway WebSocket endpoint. +

+
+ + {/* Token */} +
+ + { + setToken(e.target.value) + setPassword('') + setStatus('idle') + }} + placeholder="Paste your gateway token" + className="w-full rounded-lg border border-primary-200 bg-primary-50 px-3 py-2 text-sm text-primary-900 placeholder:text-primary-400 focus:border-primary-400 focus:outline-none focus:ring-2 focus:ring-primary-200 transition-colors" + /> +

+ Matches{' '} + gateway.auth.token or{' '} + OPENCLAW_GATEWAY_TOKEN. +

+
+ + {/* Password */} +
+ + { + setPassword(e.target.value) + setToken('') + setStatus('idle') + }} + placeholder="Or use your gateway password" + className="w-full rounded-lg border border-primary-200 bg-primary-50 px-3 py-2 text-sm text-primary-900 placeholder:text-primary-400 focus:border-primary-400 focus:outline-none focus:ring-2 focus:ring-primary-200 transition-colors" + /> +

+ Matches{' '} + gateway.auth.password. +

+
-

- Environment variables are loaded at startup. Restart your dev - server: + + {/* Test Connection button */} + + + {/* Status messages */} + {status === 'success' && ( +

+

Connected successfully!

+

+ Your credentials are valid. Save the{' '} + .env.local file below + and restart the dev server to start chatting. +

+
+ )} + {status === 'error' && ( +
+

Connection failed

+

{errorMessage}

+
+ )} +
+ + {/* Generated .env.local snippet */} +
+

+ {status === 'success' + ? '✓ Save this as .env.local in the project root, then restart:' + : 'Your .env.local — save this file and restart the dev server:'}

+ -

Refresh the page after the restart and you should be connected.

+

+ After restarting, refresh the page and you should be connected. +

+ {/* Manual setup collapsible */} + + + + Manual setup instructions + + +
+

+ At the root of the project, create a new file named{' '} + .env.local. +

+
+

Paste this into it:

+ +

or:

+ +
+

+ Environment variables are loaded at startup. Restart your dev + server: +

+ +

+ Refresh the page after the restart and you should be connected. +

+
+
+
+ + {/* Where to find values */}

Where to find these values @@ -57,7 +301,7 @@ function ConnectRoute() {

CLAWDBOT_GATEWAY_URL
- Your OpenClaw gateway endpoint (default is + Your OpenClaw gateway endpoint (default is{' '} ws://127.0.0.1:18789).

@@ -65,7 +309,7 @@ function ConnectRoute() { (recommended)
Matches your Gateway token ( - gateway.auth.token or + gateway.auth.token or{' '} OPENCLAW_GATEWAY_TOKEN).

diff --git a/apps/webclaw/src/server/gateway.ts b/apps/webclaw/src/server/gateway.ts index e6d6e5e..4a64164 100644 --- a/apps/webclaw/src/server/gateway.ts +++ b/apps/webclaw/src/server/gateway.ts @@ -209,3 +209,44 @@ export async function gatewayConnectCheck(): Promise { } } } + +/** + * Test a gateway connection with user-provided credentials. + * Used by the connect screen to validate settings before the user + * saves them to .env.local. + */ +export async function gatewayConnectTest( + url: string, + token: string, + password: string, +): Promise { + if (!token && !password) { + throw new Error('Provide a token or password.') + } + + const ws = new WebSocket(url) + try { + await wsOpen(ws) + + const connectId = randomUUID() + const connectParams = buildConnectParams(token, password) + const connectReq: GatewayFrame = { + type: 'req', + id: connectId, + method: 'connect', + params: connectParams, + } + + const waiter = createGatewayWaiter() + ws.addEventListener('message', waiter.handleMessage) + ws.send(JSON.stringify(connectReq)) + await waiter.waitForRes(connectId) + ws.removeEventListener('message', waiter.handleMessage) + } finally { + try { + await wsClose(ws) + } catch { + // ignore + } + } +}