// Control Ui Mock Dev script supports OpenClaw repository automation. import path from "node:path"; import { fileURLToPath } from "node:url"; import qrcode from "qrcode"; import { createServer, type Plugin, type ViteDevServer } from "vite"; import { CONTROL_UI_BOOTSTRAP_CONFIG_PATH } from "../src/gateway/control-ui-contract.js"; import { createControlUiMockBootstrapConfig, createControlUiMockGatewayInitScript, type ControlUiMockGatewayScenario, } from "../ui/src/test-helpers/control-ui-e2e.ts"; import { resolveExternalPackageAliasesForVite, resolveSourcePackageAliasesForVite, resolveTsconfigPathAliasesForVite, } from "../ui/vite.config.ts"; type CliOptions = { allowedHosts: string[]; host: string; port: number; }; type SessionListOptions = { hasMore: boolean; nextOffset: number | null; offset?: number; totalCount: number; }; const SESSION_PAGE_SIZE = 50; const TOTAL_MOCK_SESSIONS = 650; const TOTAL_TELEGRAM_SESSIONS = 180; const repoRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), ".."); const uiRoot = path.join(repoRoot, "ui"); function parseArgs(args: string[]): CliOptions { const options: CliOptions = { allowedHosts: [], host: "127.0.0.1", port: 5187 }; for (let i = 0; i < args.length; i += 1) { const arg = args[i]; if (arg === "--allowed-host") { const allowedHost = args[++i]?.trim(); if (allowedHost) { options.allowedHosts.push(allowedHost); } } else if (arg.startsWith("--allowed-host=")) { const allowedHost = arg.slice("--allowed-host=".length).trim(); if (allowedHost) { options.allowedHosts.push(allowedHost); } } else if (arg === "--host") { options.host = args[++i] ?? options.host; } else if (arg.startsWith("--host=")) { options.host = arg.slice("--host=".length) || options.host; } else if (arg === "--port") { options.port = parsePort(args[++i], options.port); } else if (arg.startsWith("--port=")) { options.port = parsePort(arg.slice("--port=".length), options.port); } } return options; } function parsePort(value: string | undefined, fallback: number): number { const parsed = Number(value); return Number.isInteger(parsed) && parsed > 0 && parsed < 65_536 ? parsed : fallback; } function sessionRow( key: string, label: string, updatedAt: number, options: { model?: string; modelProvider?: string } = {}, ) { return { contextTokens: 200_000, displayName: label, hasActiveRun: false, key, kind: "direct", label, model: options.model ?? "gpt-5.6-luna", modelProvider: options.modelProvider ?? "openai", status: "done", totalTokens: 0, updatedAt, }; } function sessionsListResponse(sessions: unknown[], options: SessionListOptions) { return { count: sessions.length, defaults: { contextTokens: 200_000, model: "gpt-5.6-luna", modelProvider: "openai", }, hasMore: options.hasMore, limitApplied: 50, nextOffset: options.nextOffset, offset: options.offset ?? 0, path: "", sessions, totalCount: options.totalCount, ts: Date.now(), }; } function pagedSessionsListResponse(sessions: unknown[], offset: number) { const normalizedOffset = Math.max(0, Math.floor(offset)); const page = sessions.slice(normalizedOffset, normalizedOffset + SESSION_PAGE_SIZE); const nextOffset = normalizedOffset + SESSION_PAGE_SIZE; return sessionsListResponse(page, { hasMore: nextOffset < sessions.length, nextOffset: nextOffset < sessions.length ? nextOffset : null, offset: normalizedOffset, totalCount: sessions.length, }); } function buildSessionRows(params: { baseTime: number; count: number; keyPrefix: string; labelPrefix: string; model?: string; modelProvider?: string; }) { return Array.from({ length: params.count }, (_value, index) => { const ordinal = index + 1; const padded = String(ordinal).padStart(3, "0"); return sessionRow( `agent:${params.keyPrefix}-${padded}`, `${params.labelPrefix} ${padded}`, params.baseTime - ordinal * 60_000, { model: params.model, modelProvider: params.modelProvider }, ); }); } function buildSessionListCases( sessions: unknown[], matchBase: Record = {}, ): Array<{ match: Record; response: unknown }> { const cases: Array<{ match: Record; response: unknown }> = []; for (let offset = SESSION_PAGE_SIZE; offset < sessions.length; offset += SESSION_PAGE_SIZE) { cases.push({ match: { ...matchBase, offset }, response: pagedSessionsListResponse(sessions, offset), }); } cases.push({ match: matchBase, response: pagedSessionsListResponse(sessions, 0), }); return cases; } function buildSearchSessionListCases( sessions: unknown[], searchTerms: string[], ): Array<{ match: Record; response: unknown }> { return searchTerms.flatMap((search) => buildSessionListCases(sessions, { search })); } function usageCostTotals(totalTokens: number, totalCost = 0) { return { input: Math.round(totalTokens * 0.2), output: Math.round(totalTokens * 0.1), cacheRead: Math.round(totalTokens * 0.6), cacheWrite: Math.round(totalTokens * 0.1), totalTokens, totalCost, inputCost: totalCost, outputCost: 0, cacheReadCost: 0, cacheWriteCost: 0, missingCostEntries: 0, }; } // Model Providers settings fixtures: auth state plus live plan/quota/billing // snapshots so the /settings/model-providers page renders fully in the mock. function buildSessionDiffMock() { const appPatch = [ "diff --git a/src/app.ts b/src/app.ts", "index 1111111..2222222 100644", "--- a/src/app.ts", "+++ b/src/app.ts", "@@ -12,4 +12,5 @@ export function bootstrap() {", " const config = readSettings();", "- const client = createClient(config);", "+ const client = createClient(config, { retries: 3 });", '+ client.on("error", reportError);', " return client;", "@@ -181,3 +182,3 @@ export function shutdown() {", " flushQueues();", '- logger.info("bye");', '+ logger.info("shutdown complete");', "", ].join("\n"); const readmePatch = [ "diff --git a/README.md b/README.md", "new file mode 100644", "--- /dev/null", "+++ b/README.md", "@@ -0,0 +1,3 @@", "+# Demo", "+", "+Mock harness session diff fixture.", "", ].join("\n"); return { sessionKey: "main", root: "/tmp/openclaw-mock-checkout", branch: "feature/session-diff-panel", baseRef: "main", files: [ { path: "src/app.ts", status: "modified", additions: 3, deletions: 2, patch: appPatch, }, { path: "README.md", status: "added", additions: 3, deletions: 0, untracked: true, patch: readmePatch, }, { path: "assets/logo.png", status: "modified", additions: 0, deletions: 0, binary: true, }, ], additions: 6, deletions: 2, }; } function buildModelProviderMocks(baseTime: number) { const hour = 60 * 60 * 1000; const expiry = (remainingMs: number, label: string) => ({ at: baseTime + remainingMs, remainingMs, label, }); const costDaily = Array.from({ length: 14 }, (_, index) => { const date = new Date(baseTime - (13 - index) * 24 * hour); const iso = `${date.getUTCFullYear()}-${String(date.getUTCMonth() + 1).padStart(2, "0")}-${String(date.getUTCDate()).padStart(2, "0")}`; const amount = 4 + Math.round(Math.abs(Math.sin(index)) * 900) / 100; return { date: iso, amount, requests: 120 + index * 7, inputTokens: 2_400_000 + index * 90_000, cacheReadTokens: 9_000_000, cacheWriteTokens: 400_000, outputTokens: 310_000, totalTokens: 12_110_000 + index * 90_000, }; }); const anthropicUsage = { provider: "anthropic", displayName: "Claude", plan: "Max 20x", windows: [ { label: "5h", usedPercent: 38, resetAt: baseTime + 2.4 * hour }, { label: "Week", usedPercent: 61, resetAt: baseTime + 68 * hour }, { label: "Opus", usedPercent: 24, resetAt: baseTime + 68 * hour }, ], costHistory: { unit: "USD", periodDays: 14, daily: costDaily, models: [ { name: "claude-sonnet-4-6", inputTokens: 0, cacheReadTokens: 0, cacheWriteTokens: 0, outputTokens: 0, totalTokens: 96_000_000, }, { name: "claude-opus-4-8", inputTokens: 0, cacheReadTokens: 0, cacheWriteTokens: 0, outputTokens: 0, totalTokens: 31_000_000, }, ], categories: [ { name: "Sessions", amount: 61.13 }, { name: "Code Assist", amount: 18.4 }, ], }, }; const openaiUsage = { provider: "openai", displayName: "OpenAI", plan: "Pro", windows: [ { label: "5h", usedPercent: 12, resetAt: baseTime + 3.1 * hour }, { label: "Week", usedPercent: 44, resetAt: baseTime + 100 * hour }, ], billing: [{ type: "balance", label: "Credits", amount: 341, unit: "credits" }], }; const openrouterUsage = { provider: "openrouter", displayName: "OpenRouter", windows: [], billing: [{ type: "balance", amount: 12.34, unit: "USD" }], }; const copilotUsage = { provider: "github-copilot", displayName: "GitHub Copilot", plan: "Business", windows: [{ label: "Premium requests", usedPercent: 71, resetAt: baseTime + 21 * 24 * hour }], }; return { authStatus: { ts: baseTime, providers: [ { provider: "anthropic", displayName: "Claude", status: "ok", expiry: expiry(11 * 24 * hour, "11d"), profiles: [ { profileId: "anthropic:default", type: "oauth", status: "ok", expiry: expiry(11 * 24 * hour, "11d"), }, ], usage: { providerId: "anthropic", plan: anthropicUsage.plan, windows: anthropicUsage.windows, }, }, { provider: "openai", displayName: "OpenAI", status: "ok", expiry: expiry(6 * 24 * hour, "6d"), profiles: [ { profileId: "openai:codex", type: "oauth", status: "ok", expiry: expiry(6 * 24 * hour, "6d"), }, ], usage: { providerId: "openai", plan: openaiUsage.plan, windows: openaiUsage.windows, billing: openaiUsage.billing, }, }, { provider: "github-copilot", displayName: "GitHub Copilot", status: "expiring", expiry: expiry(26 * 60 * 1000, "26m"), profiles: [ { profileId: "github-copilot:default", type: "token", status: "expiring", expiry: expiry(26 * 60 * 1000, "26m"), }, ], usage: { providerId: "github-copilot", plan: copilotUsage.plan, windows: copilotUsage.windows, }, }, { provider: "openrouter", displayName: "OpenRouter", status: "static", profiles: [{ profileId: "openrouter:default", type: "api_key", status: "static" }], }, { provider: "google", displayName: "Gemini", status: "missing", profiles: [], }, ], }, usageStatus: { updatedAt: baseTime, providers: [anthropicUsage, openaiUsage, openrouterUsage, copilotUsage], }, models: [ { id: "claude-opus-4-8", name: "Claude Opus 4.8", provider: "anthropic", available: true }, { id: "claude-sonnet-4-6", name: "Claude Sonnet 4.6", provider: "anthropic", available: true, }, { id: "gpt-5.6-luna", name: "GPT-5.6 Luna", provider: "openai", available: true }, { id: "gpt-5.6-sol", name: "GPT-5.6 Sol", provider: "openai", available: true }, { id: "gemini-3-pro", name: "Gemini 3 Pro", provider: "google", available: false }, { id: "openrouter/auto", name: "OpenRouter Auto", provider: "openrouter", available: true }, ], }; } // Deterministic year of daily activity so the settings profile heatmap, // streaks, and stat strip render with a lively fixture in the mock harness. function buildProfileUsageMocks(baseTime: number) { const daily: Array> = []; let lifetimeTokens = 0; for (let daysAgo = 364; daysAgo >= 0; daysAgo -= 1) { const date = new Date(baseTime - daysAgo * 24 * 60 * 60 * 1000); const iso = `${date.getFullYear()}-${String(date.getMonth() + 1).padStart(2, "0")}-${String(date.getDate()).padStart(2, "0")}`; const weekendDamper = date.getDay() === 0 || date.getDay() === 6 ? 0.3 : 1; const quietDay = daysAgo % 19 === 4 ? 0 : 1; const wave = (Math.sin(daysAgo / 6) + 1.4) * 1_400_000_000; const spike = daysAgo % 47 === 0 ? 6_000_000_000 : 0; const tokens = Math.round((wave + spike) * weekendDamper * quietDay); lifetimeTokens += tokens; daily.push({ date: iso, ...usageCostTotals(tokens, tokens / 1e9) }); } return { cost: { updatedAt: baseTime, days: daily.length, daily, totals: usageCostTotals(lifetimeTokens, lifetimeTokens / 1e9), }, sessions: { updatedAt: baseTime, startDate: daily[0]?.date, endDate: daily[daily.length - 1]?.date, sessions: [ { key: "agent:openclaw-mock:marathon", label: "Release night marathon", usage: { ...usageCostTotals(4_000_000_000), durationMs: (59 * 60 + 4) * 60 * 1000 }, }, { key: "agent:openclaw-mock:daily", label: "Daily driver", usage: { ...usageCostTotals(900_000_000), durationMs: 3 * 60 * 60 * 1000 }, }, ], totals: usageCostTotals(lifetimeTokens, lifetimeTokens / 1e9), aggregates: { sessionCount: 48_212, longestSessionDurationMs: (59 * 60 + 4) * 60 * 1000, messages: { total: 2_787_815, user: 1_400_000, assistant: 1_387_815, toolCalls: 42_380, toolResults: 42_380, errors: 128, }, tools: { totalCalls: 42_380, uniqueTools: 205, tools: [ { name: "exec", count: 6_418 }, { name: "browser", count: 5_256 }, { name: "message", count: 4_708 }, { name: "read", count: 4_489 }, { name: "sessions_list", count: 3_066 }, ], }, byModel: [ { provider: "anthropic", model: "claude-sonnet-4-6", count: 9_000, totals: usageCostTotals(Math.round(lifetimeTokens * 0.7)), }, { provider: "openai", model: "gpt-5.6-luna", count: 4_000, totals: usageCostTotals(Math.round(lifetimeTokens * 0.3)), }, ], byProvider: [ { provider: "anthropic", count: 9_000, totals: usageCostTotals(Math.round(lifetimeTokens * 0.7), 184.2), }, { provider: "openai", count: 4_000, totals: usageCostTotals(Math.round(lifetimeTokens * 0.3), 96.4), }, ], byAgent: [ { agentId: "openclaw-mock", totals: usageCostTotals(Math.round(lifetimeTokens * 0.8)) }, { agentId: "alpha", totals: usageCostTotals(Math.round(lifetimeTokens * 0.2)) }, ], byChannel: [ { channel: "whatsapp", totals: usageCostTotals(Math.round(lifetimeTokens * 0.5)) }, { channel: "telegram", totals: usageCostTotals(Math.round(lifetimeTokens * 0.3)) }, { channel: "discord", totals: usageCostTotals(Math.round(lifetimeTokens * 0.2)) }, ], daily: [], }, }, }; } function chatHistoryMessage(role: "assistant" | "user", text: string, timestamp: number) { return { content: [{ text, type: "text" }], role, timestamp, }; } function buildScrollableChatHistory(baseTime: number): unknown[] { const messages: unknown[] = [ chatHistoryMessage( "assistant", `Mock Control UI is running with ${TOTAL_MOCK_SESSIONS} sessions. Open the chat picker, search for "telegram" or "claude", then use Load more repeatedly.`, baseTime, ), ]; for (let index = 1; index <= 36; index += 1) { const timestamp = baseTime + index * 60_000; messages.push( chatHistoryMessage( "user", `Mock scroll request ${index}: add enough transcript content to exercise the chat scroll container in focused mode.`, timestamp, ), chatHistoryMessage( "assistant", `Mock scroll response ${index}: this deterministic history keeps the mock chat long enough to scroll while testing focus mode, header collapse, and composer anchoring. `.repeat( 2, ), timestamp + 30_000, ), ); } return messages; } function searchPrefixes(term: string): string[] { return Array.from({ length: term.length }, (_value, index) => term.slice(0, index + 1)); } async function createChatPickerScenario(): Promise { const baseTime = Date.parse("2026-05-22T09:00:00.000Z"); const devicePairSetupCode = Buffer.from( JSON.stringify({ url: "wss://gateway.example.test", bootstrapToken: "mock-bootstrap-token", }), "utf8", ).toString("base64url"); const devicePairQrDataUrl = await qrcode.toDataURL(devicePairSetupCode, { errorCorrectionLevel: "M", margin: 2, width: 360, }); const workspaceFiles = [ { missing: false, name: "AGENTS.md", path: "/mock/workspace/AGENTS.md", size: 2148, updatedAtMs: baseTime - 120_000, }, { missing: false, name: "plan.md", path: "/mock/workspace/plan.md", size: 912, updatedAtMs: baseTime - 90_000, }, { missing: false, name: "notes/context.md", path: "/mock/workspace/notes/context.md", size: 1620, updatedAtMs: baseTime - 30_000, }, ]; const workspaceListCases = ["main", "alpha", "openclaw-mock"].map((agentId) => ({ match: { agentId }, response: { agentId, files: workspaceFiles, workspace: "/mock/workspace", }, })); const workspaceFileContentByName = new Map([ [ "AGENTS.md", "# AGENTS.md\n\nMock workspace instructions for the composer rail.\n\n- Keep tool output compact.\n- Prefer right-rail context over modal previews.\n", ], [ "plan.md", "# Composer polish plan\n\n1. Keep the composer controls calm.\n2. Move session selection into the sidebar.\n3. Keep model, reasoning, and speed choices discoverable without taking over the page.\n", ], [ "notes/context.md", "# Context notes\n\nThe right rail should feel like workspace context, not a modal pasted beside the chat.\n\n## Current focus\n\n- Markdown previews need readable dark-mode chrome.\n- Empty or unavailable content should show a quiet state instead of an empty card.\n- File previews should load from the same mock scenario as the file list.\n", ], ]); const workspaceFileCases = ["main", "alpha", "openclaw-mock"].flatMap((agentId) => workspaceFiles.map((file) => ({ match: { agentId, name: file.name }, response: { agentId, file: { ...file, content: workspaceFileContentByName.get(file.name) ?? "", }, workspace: "/mock/workspace", }, })), ); const sessionFiles = [ { kind: "modified", missing: false, name: "chat.ts", path: "ui/src/ui/views/chat.ts", size: 48320, updatedAtMs: baseTime - 20_000, }, { kind: "modified", missing: false, name: "sidebar.css", path: "ui/src/styles/chat/sidebar.css", size: 18840, updatedAtMs: baseTime - 18_000, }, { kind: "read", missing: false, name: "artifacts.ts", path: "src/gateway/server-methods/artifacts.ts", size: 21876, updatedAtMs: baseTime - 300_000, }, { kind: "read", missing: false, name: "sessions.ts", path: "packages/gateway-protocol/src/schema/sessions.ts", size: 16542, updatedAtMs: baseTime - 420_000, }, ]; const sessionWorkspaceRoot = repoRoot; const sessionFileContentByPath = new Map([ [ "ui/src/ui/views/chat.ts", 'function renderSessionWorkspaceRail() {\n return html``;\n}\n', ], [ "ui/src/styles/chat/sidebar.css", ".chat-workspace-rail__section-title {\n color: var(--muted);\n text-transform: uppercase;\n}\n", ], [ "src/gateway/server-methods/artifacts.ts", "// Artifact gateway methods collect generated artifacts from session transcripts.\n", ], [ "packages/gateway-protocol/src/schema/sessions.ts", "export const SessionsFilesListParamsSchema = Type.Object({ sessionKey: NonEmptyString });\n", ], [ "package.json", '{\n "name": "openclaw",\n "scripts": { "dev:ui:mock": "tsx scripts/control-ui-mock-dev.ts" }\n}\n', ], [ "ui/vite.config.ts", "export default function controlUiViteConfig() {\n return { server: { strictPort: true } };\n}\n", ], [ "ui/src/e2e/chat-flow.e2e.test.ts", "it('keeps the session workspace useful while browsing files', async () => {\n await page.getByText('Project files').waitFor();\n});\n", ], ]); const sessionFileCases = [ { match: { sessionKey: "agent:alpha" }, response: { browser: { entries: [ { kind: "directory", name: "packages", path: "packages", sessionKind: "read", updatedAtMs: baseTime - 420_000, }, { kind: "directory", name: "src", path: "src", sessionKind: "read", updatedAtMs: baseTime - 300_000, }, { kind: "directory", name: "ui", path: "ui", sessionKind: "modified", updatedAtMs: baseTime - 20_000, }, { kind: "file", name: "package.json", path: "package.json", size: 92750, updatedAtMs: baseTime - 800_000, }, ], path: "", }, files: sessionFiles, root: sessionWorkspaceRoot, sessionKey: "agent:alpha", }, }, ]; const sessionFileGetCases = sessionFiles.map((file) => ({ match: { sessionKey: "agent:alpha", path: file.path }, response: { file: { ...file, content: sessionFileContentByPath.get(file.path) ?? "", }, root: sessionWorkspaceRoot, sessionKey: "agent:alpha", }, })); const lobsterSvg = ` openclaw session artifact `; const lobsterArtifact = { id: "artifact-openclaw-lobster", type: "image", title: "openclaw-lobster-preview.svg", mimeType: "image/svg+xml", sizeBytes: Buffer.byteLength(lobsterSvg, "utf8"), source: "session-transcript", download: { mode: "bytes" }, }; const sessions = [ sessionRow("agent:alpha", "Alpha planning", baseTime - 1_000), ...buildSessionRows({ baseTime: baseTime - 60_000, count: TOTAL_MOCK_SESSIONS - 1, keyPrefix: "history", labelPrefix: "Long running session", }), ]; const telegramSessions = buildSessionRows({ baseTime: baseTime - 30_000, count: TOTAL_TELEGRAM_SESSIONS, keyPrefix: "telegram", labelPrefix: "Telegram investigation", }); const claudeSessions = buildSessionRows({ baseTime: baseTime - 45_000, count: 75, keyPrefix: "model-claude", labelPrefix: "Model search result", model: "claude-sonnet-4-6", modelProvider: "anthropic", }); // Profile fixtures track the real clock so streaks and the trailing-year // heatmap stay filled no matter when the mock harness runs. const profileUsage = buildProfileUsageMocks(Date.now()); const modelProviders = buildModelProviderMocks(Date.now()); return { assistantAgentId: "openclaw-mock", assistantName: "OpenClaw mock", defaultAgentId: "openclaw-mock", featureMethods: ["chat.metadata", "chat.startup", "sessions.diff"], historyMessages: buildScrollableChatHistory(baseTime), methodResponses: { "sessions.diff": buildSessionDiffMock(), "usage.cost": profileUsage.cost, "sessions.usage": profileUsage.sessions, "models.authStatus": modelProviders.authStatus, "usage.status": modelProviders.usageStatus, "device.pair.list": { paired: [ { deviceId: "a1b2c3d4e5f60718293a4b5c6d7e8f90a1b2c3d4e5f60718293a4b5c6d7e8f90", displayName: "Mac Studio", platform: "darwin", clientId: "node-host", clientMode: "node", roles: ["operator", "node"], scopes: ["operator.admin", "operator.read", "operator.write"], approvedVia: "trusted-cidr", approvedAtMs: baseTime - 3_600_000, lastSeenAtMs: baseTime - 60_000, tokens: [ { role: "node", scopes: [], createdAtMs: baseTime - 3_600_000 }, { role: "operator", scopes: ["operator.admin", "operator.read", "operator.write"], createdAtMs: baseTime - 3_600_000, }, ], }, { deviceId: "0f1e2d3c4b5a69788796a5b4c3d2e1f00f1e2d3c4b5a69788796a5b4c3d2e1f0", displayName: "Mac Studio", platform: "darwin", clientId: "node-host", clientMode: "node", roles: ["node"], approvedVia: "trusted-cidr", approvedAtMs: baseTime - 86_400_000, lastSeenAtMs: baseTime - 82_800_000, tokens: [{ role: "node", scopes: [], createdAtMs: baseTime - 86_400_000 }], }, { deviceId: "9988776655443322119988776655443322119988776655443322119988776655", clientId: "cli", clientMode: "cli", platform: "darwin", roles: ["operator"], scopes: ["operator.admin", "operator.read", "operator.write"], approvedVia: "silent", approvedAtMs: baseTime - 7_200_000, lastSeenAtMs: baseTime - 7_100_000, tokens: [ { role: "operator", scopes: ["operator.admin", "operator.read", "operator.write"], createdAtMs: baseTime - 7_200_000, }, ], }, { deviceId: "11223344556677889900aabbccddeeff11223344556677889900aabbccddeeff", displayName: "iPhone", platform: "iOS 26.4", clientId: "openclaw-ios", clientMode: "ui", roles: ["operator", "node"], scopes: ["operator.approvals", "operator.read", "operator.write"], approvedVia: "bootstrap", approvedAtMs: baseTime - 172_800_000, lastSeenAtMs: baseTime - 3_600_000, tokens: [ { role: "node", scopes: [], createdAtMs: baseTime - 172_800_000 }, { role: "operator", scopes: ["operator.approvals", "operator.read", "operator.write"], createdAtMs: baseTime - 172_800_000, }, ], }, ], pending: [ { requestId: "mock-pending-request", deviceId: "feedfacecafebeef0123456789abcdeffeedfacecafebeef0123456789abcdef", displayName: "MacBook Pro", role: "operator", roles: ["operator"], scopes: ["operator.read", "operator.write"], remoteIp: "192.168.1.20", ts: baseTime - 30_000, }, ], }, "device.pair.setupCode": { auth: "token", gatewayUrl: "wss://gateway.example.test", qrDataUrl: devicePairQrDataUrl, setupCode: devicePairSetupCode, urlSource: "mock", }, "node.list": { nodes: [ { nodeId: "a1b2c3d4e5f60718293a4b5c6d7e8f90a1b2c3d4e5f60718293a4b5c6d7e8f90", displayName: "Mac Studio", platform: "darwin", version: "2026.6.11", connected: true, paired: true, approvalState: "approved", connectedAtMs: baseTime - 60_000, caps: ["canvas", "screen"], commands: [ "screen.snapshot", "system.execApprovals.get", "system.execApprovals.set", "system.notify", "system.run", "system.which", ], }, { nodeId: "0f1e2d3c4b5a69788796a5b4c3d2e1f00f1e2d3c4b5a69788796a5b4c3d2e1f0", displayName: "Mac Studio", platform: "darwin", version: "2026.6.10", connected: false, paired: true, approvalState: "approved", lastSeenAtMs: baseTime - 82_800_000, caps: ["canvas", "screen"], commands: ["screen.snapshot", "system.run"], }, { nodeId: "11223344556677889900aabbccddeeff11223344556677889900aabbccddeeff", displayName: "iPhone", platform: "iOS 26.4", version: "2026.6.11", connected: false, paired: true, approvalState: "pending-reapproval", pendingRequestId: "mock-node-reapproval", lastSeenAtMs: baseTime - 3_600_000, caps: ["camera", "canvas", "contacts", "device", "location"], commands: ["camera.list", "contacts.search", "device.info", "location.get"], }, ], }, "agents.files.get": { cases: workspaceFileCases, }, "agents.files.list": { cases: workspaceListCases, }, "sessions.files.get": { cases: sessionFileGetCases, }, "sessions.files.list": { cases: [ { match: { sessionKey: "agent:alpha", path: "ui" }, response: { browser: { entries: [ { kind: "directory", name: "src", path: "ui/src", sessionKind: "modified", updatedAtMs: baseTime - 20_000, }, { kind: "file", name: "vite.config.ts", path: "ui/vite.config.ts", size: 9860, updatedAtMs: baseTime - 900_000, }, ], parentPath: "", path: "ui", }, files: sessionFiles, root: sessionWorkspaceRoot, sessionKey: "agent:alpha", }, }, { match: { sessionKey: "agent:alpha", search: "chat" }, response: { browser: { entries: [ { kind: "file", name: "chat.ts", path: "ui/src/ui/views/chat.ts", sessionKind: "modified", size: 48320, updatedAtMs: baseTime - 20_000, }, { kind: "file", name: "chat-flow.e2e.test.ts", path: "ui/src/e2e/chat-flow.e2e.test.ts", size: 24950, updatedAtMs: baseTime - 25_000, }, ], path: "", search: "chat", }, files: sessionFiles, root: sessionWorkspaceRoot, sessionKey: "agent:alpha", }, }, ...sessionFileCases, ], }, "artifacts.list": { cases: [ { match: { sessionKey: "agent:alpha" }, response: { artifacts: [lobsterArtifact] }, }, ], }, "artifacts.download": { cases: [ { match: { sessionKey: "agent:alpha", artifactId: lobsterArtifact.id }, response: { artifact: lobsterArtifact, data: Buffer.from(lobsterSvg, "utf8").toString("base64"), encoding: "base64", }, }, ], }, "sessions.list": { cases: [ ...buildSearchSessionListCases(telegramSessions, searchPrefixes("telegram")), ...buildSearchSessionListCases(claudeSessions, [ ...searchPrefixes("claude"), ...searchPrefixes("claude-sonnet-4-6"), ...searchPrefixes("anthropic"), ]), ...buildSessionListCases(sessions), ], }, }, models: modelProviders.models, sessionKey: "agent:alpha", }; } function escapeScriptContent(script: string): string { return script.replaceAll(" { res.statusCode = 200; res.setHeader("content-type", "application/json"); res.end(bootstrapBody); }); }, name: "openclaw-control-ui-mock-gateway", transformIndexHtml(html) { return html.replace( "", ` \n `, ); }, }; } function hostForUrl(boundAddress: string, requestedHost: string): string { const host = boundAddress === "0.0.0.0" || boundAddress === "::" ? requestedHost : boundAddress; const reachableHost = host === "0.0.0.0" || host === "::" ? "127.0.0.1" : host; return reachableHost.includes(":") ? `[${reachableHost}]` : reachableHost; } function resolveServerUrl(server: ViteDevServer, requestedHost: string): string { const address = server.httpServer?.address(); if (!address || typeof address === "string") { throw new Error("Control UI mock server did not expose a TCP port"); } return `http://${hostForUrl(address.address, requestedHost)}:${address.port}/chat`; } async function waitForShutdown(): Promise { await new Promise((resolve) => { process.once("SIGINT", resolve); process.once("SIGTERM", resolve); }); } const options = parseArgs(process.argv.slice(2)); const scenario = await createChatPickerScenario(); const server = await createServer({ base: "/", cacheDir: path.join(repoRoot, ".artifacts", "control-ui-mock-vite"), clearScreen: false, configFile: path.join(uiRoot, "vite.config.ts"), define: { "globalThis.OPENCLAW_CONTROL_UI_BUILD_INFO": JSON.stringify({ version: "2026.7.10", commit: "0123456789abcdef0123456789abcdef01234567", builtAt: "2026-07-10T12:34:56.000Z", buildId: "mock", }), }, logLevel: "error", optimizeDeps: { include: ["lit/directives/repeat.js"], }, plugins: [createMockGatewayPlugin(scenario)], publicDir: path.join(uiRoot, "public"), resolve: { alias: [ ...resolveExternalPackageAliasesForVite(), ...resolveSourcePackageAliasesForVite(), ...resolveTsconfigPathAliasesForVite(), ], }, root: uiRoot, server: { allowedHosts: options.allowedHosts, host: options.host, port: options.port, strictPort: true, }, }); await server.listen(); console.log(`[control-ui-mock] ${resolveServerUrl(server, options.host)}`); await waitForShutdown(); await server.close();