openclaw/scripts/control-ui-mock-dev.ts
Peter Steinberger c8d3f81045
fix: isolate subagent concurrency per session (#156381)
* fix(agents): scope subagent concurrency to spawning sessions

Give each immediate spawning session its own configured execution budget,
including nested orchestrators, instead of making unrelated sessions share
one Gateway-wide queue. Preserve queue ordering, cancellation, hot limit
updates, and idle cleanup; retain the same parent identity during compaction.

Show aggregate activity with an explicit per-session limit in diagnostics.
Keep the existing child admission cap and Codex-native scheduling separate.

Verify with focused queue/runtime/UI tests and an isolated real-provider
Gateway case that holds one session's slot while another session and its
nested child run, then awaits all completion acknowledgments.

Closes #156370

* test(telegram): match sticker fixture to ingress dispatch

Register the native sticker pipeline's provider runtime prerequisite for
local and CI execution. Preserve the same Request JSON decoder while
exercising the registered bot handler, as durable webhook ingress does
after acknowledgment, and join the held turn before fixture cleanup.

The original CI file order reproduced the grammY timeout. Runtime
preparation alone was insufficient; the test's generic webhook adapter
imposed a deadline that is absent from the production dispatch boundary.
Keep the cache, media, and model-admission assertions unchanged, with the
real webhook acknowledgment contract covered by its existing test.

* test: repair packaging and runtime-owner CI fixtures

Copy the newly imported check-limits helper into the trusted packaging
harness so its dependency-free startup assertion reaches the CLI.

Compare prepared Telegram runtime files against the same config that
supplies the expected files. Keep worker-envelope coverage, packing,
concurrency and execution-budget assertions unchanged.

Both regressions reproduced before their fixes. The packaging owner passed
64 tests and ten targeted packing/prerequisite checks passed afterward.

* test(matrix): await follow-up adoption while active turn is held

Replace the 500 ms post-routing poll with resolver-owned adoption completion. Preserve foreground FIFO settlement by releasing the active turn before joining handlers, and settle fixture gates on timeout.

Validation: all four owner cases pass (37.31 s wrapper, 3.32 s bodies); selected changed checks and independent P0-P2 review pass.

* test(ci): recognize stopped Linux fixture process groups

Reuse the existing all-thread process-group assertion after detached fixture leaders are joined. Kernel kill probes also include exited zombies awaiting reaping; retain rejection of live descendants and uncertain observations.

Validation: 64 Docker scheduler cases and 18 retained-zombie/census cases passed on Linux Testbox; selected changed checks and independent P0-P2 review pass. The original CI process state did not reproduce in the original-order diagnostic.

* test(ui): count roster refreshes independently of child lookups

Install the browser clock before navigation and match roster sessions.list requests by includeGlobal. Preserve the 4999/+2 ms event window and avatar checks, and require exactly one roster refresh after the boundary.

Validation: reproduced the failing count and traced its extra request to a spawnedBy child lookup while the roster timer remained pending. All 14 browser cases pass (38.15 s; changed case 1.063 s). Selected changed gates and independent P0-P2 review pass.
2026-09-23 06:46:57 -07:00

3820 lines
130 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// Control Ui Mock Dev script supports OpenClaw repository automation.
import { createHash } from "node:crypto";
import fs, { rmSync } from "node:fs";
import { mkdir, mkdtemp } from "node:fs/promises";
import path from "node:path";
import { fileURLToPath } from "node:url";
import qrcode from "qrcode";
import { createServer, type Plugin, type ViteDevServer } from "vite";
import type {
SystemAgentChatHistoryResult,
SystemChangesListResult,
UserProfile,
} from "../packages/gateway-protocol/src/index.js";
import { expectDefined } from "../packages/normalization-core/src/expect.js";
import { applySharedChannelFieldHelp } from "../src/config/schema.channel-field-help.js";
import { buildBaseHints } from "../src/config/schema.hints.js";
import { applyConfigTierHints, applyResolvedConfigTierHints } from "../src/config/schema.tiers.js";
import { CONTROL_UI_BOOTSTRAP_CONFIG_PATH } from "../src/gateway/control-ui-contract.js";
import {
controlUiPluginAssetPrefix,
controlUiPluginAssetRoot,
} from "../src/gateway/control-ui-plugin-assets-contract.js";
import { buildUpdateRestartSentinelPayload } from "../src/infra/update-restart-sentinel-payload.js";
import type { UpdateRunResult } from "../src/infra/update-runner-types.js";
import { buildPluginLoaderAliasMap } from "../src/plugins/sdk-alias.js";
import { buildNewAgentWelcome } from "../src/system-agent/new-agent-welcome.js";
import type { UpdateAvailable, UpdateScheduleState } from "../ui/src/api/types.ts";
import {
controlUiSessionPath,
createControlUiMockBootstrapConfig,
createControlUiMockGatewayInitScript,
createControlUiMockSameOriginGatewayScript,
prepareControlUiMockGatewayScenario,
type ControlUiMockGatewayScenario,
} from "../ui/src/test-helpers/control-ui-e2e.ts";
import {
createControlUiChatHistoryMessage as chatHistoryMessage,
createControlUiMockSessionRow as sessionRow,
} from "../ui/src/test-helpers/control-ui-session-fixtures.ts";
import { workboardUi } from "../ui/src/test-helpers/control-ui-workboard-fixture.ts";
import { buildWorkboardMocks } from "../ui/src/test-helpers/control-ui-workboard-fixtures.ts";
import { installWorkboardBoardMock } from "../ui/src/test-helpers/control-ui-workboard-mocks.ts";
import { createOfflineDeviceNode } from "../ui/src/test-helpers/devices-fixtures.ts";
import {
resolveExternalPackageAliasesForVite,
resolveSourcePackageAliasesForVite,
resolveTsconfigPathAliasesForVite,
} from "../ui/vite.config.ts";
import { approvalMockInitScript } from "./control-ui-mock-approvals.ts";
import {
buildChatAttachmentHistory,
createChatAttachmentFixturePlugin,
} from "./control-ui-mock-attachments.ts";
import {
backgroundTasksMockInitScript,
buildBackgroundTasksMock,
} from "./control-ui-mock-background-tasks.ts";
import {
buildChannelsPairingMock,
buildChannelsStatusMock,
buildChannelWizardMocks,
} from "./control-ui-mock-channels.ts";
import { buildCronMocks } from "./control-ui-mock-cron.ts";
import { createStandaloneMockIsolationPlugins } from "./control-ui-mock-isolation.ts";
import {
buildPluginCatalogMock,
buildPluginDiscoveryCategoriesMock,
buildPluginDiscoveryMock,
buildPluginInspectMock,
pluginLifecycleMockInitScript,
} from "./control-ui-mock-plugins.ts";
import { createControlUiPreviewInitScript } from "./control-ui-mock-preview.ts";
import { skillLibraryMockInitScript } from "./control-ui-mock-skill-library.ts";
import {
buildSkillWorkshopMocks,
skillWorkshopMockInitScript,
} from "./control-ui-mock-skill-workshop.js";
import { buildProfileUsageMocks } from "./control-ui-mock-usage.ts";
type CliOptions = {
allowedHosts: string[];
fixture?:
| "approval"
| "attachments"
| "avatars"
| "board"
| "code-fences"
| "dashboards"
| "goal"
| "plugins-dense"
| "sidebar-roster"
| "swarm"
| "update-available"
| "update-blocked"
| "update-failed"
| "workboard"
| "workboard-states";
host: string;
operatorScopes?: string[];
port: number;
};
type SessionListOptions = {
owners?: readonly SessionActorFixture[];
hasMore: boolean;
nextOffset: number | null;
offset?: number;
totalCount: number;
};
type SessionActorFixture = { type: "human" | "agent"; id: string; label: string };
const MOCK_ACTOR_PETER: SessionActorFixture = {
type: "human",
id: "profile-peter",
label: "Peter",
};
const MOCK_ACTOR_MIRA: SessionActorFixture = {
type: "human",
id: "profile-mira",
label: "Mira",
};
// Rows carry explicit owners the way the gateway projects createdActor fallbacks.
const MOCK_SESSION_OWNERS: readonly SessionActorFixture[] = [MOCK_ACTOR_PETER, MOCK_ACTOR_MIRA];
const MOCK_AVATAR_PROFILES = [
{ id: "profile-avatar-normal", name: "Mira", fill: "#5d7fc5", label: "Normal profile image" },
{
id: "profile-avatar-dark",
name: "Rowan",
fill: "#0e1015",
label: "Dark surface-matching profile image",
},
{
id: "profile-avatar-light",
name: "Sol",
fill: "#faf9f7",
label: "Light surface-matching profile image",
},
] as const;
const SESSION_PAGE_SIZE = 50;
const TOTAL_TELEGRAM_SESSIONS = 180;
const ATTENTION_FIXTURE_EXPIRES_AT = Date.parse("2099-01-01T00:00:00.000Z");
const NARRATION_DEMO_SESSION_KEY = "agent:main:sidebar-narration-demo";
const NARRATION_DEMO_RUN_ID = "mock-sidebar-narration-run";
const OBSERVER_DEMO_SESSION_KEY = "agent:main:session-observer-demo";
const OBSERVER_DEMO_RUN_ID = "mock-session-observer-run";
const PLAN_DEMO_RUN_ID = "mock-plan-run";
type UpdateFixture = {
available: UpdateAvailable;
runResponse: unknown;
schedule: UpdateScheduleState;
statusResponse: unknown;
};
function buildUpdateFixture(fixture: CliOptions["fixture"], nowMs: number): UpdateFixture | null {
if (
fixture !== "update-available" &&
fixture !== "update-blocked" &&
fixture !== "update-failed"
) {
return null;
}
if (fixture === "update-available") {
const available: UpdateAvailable = {
currentVersion: "2026.8.1",
latestVersion: "2026.8.2",
channel: "latest",
};
const schedule: UpdateScheduleState = {
channel: "stable",
autoEnabled: false,
install: { kind: "package" },
target: { kind: "package", version: available.latestVersion },
};
return {
available,
schedule,
statusResponse: {
sentinel: null,
updateAvailable: available,
effectiveChannel: "stable",
schedule,
},
runResponse: {
ok: true,
result: {
status: "ok",
mode: "global",
before: { version: available.currentVersion },
after: { version: available.latestVersion },
steps: [],
durationMs: 12_000,
},
},
};
}
const currentSha = "83b321ba7d31c04cc6f7a38c87932ec0172b5461";
const upstreamSha = "ea2ab707d3ab6f9351dcdb2f3c05054097fbfa62";
const available: UpdateAvailable = {
currentVersion: "2026.8.1",
latestVersion: "2026.8.1",
channel: "dev",
currentSha,
upstreamRef: "origin/main",
upstreamSha,
commitsBehind: 2,
commits: [
{ sha: "f6c71c4", subject: "Keep update status authoritative" },
{ sha: "ea2ab70", subject: "Move update actions into Inbox" },
],
};
const baseSchedule: UpdateScheduleState = {
channel: "dev",
autoEnabled: true,
install: {
kind: "git",
git: {
status: "behind",
currentSha,
commitsBehind: 2,
commitAtMs: nowMs - 2 * 86_400_000,
installedAtMs: nowMs - 7 * 86_400_000,
},
},
target: {
kind: "git",
upstreamRef: available.upstreamRef ?? "origin/main",
upstreamSha,
commitsBehind: 2,
},
};
if (fixture === "update-blocked") {
const schedule: UpdateScheduleState = {
...baseSchedule,
campaign: {
id: "mock-update-waiting-for-idle",
state: "waiting-for-idle",
announcedAtMs: nowMs - 2 * 60_000,
forceAtMs: nowMs + 13 * 60_000,
updatedAtMs: nowMs,
},
};
return {
available,
schedule,
statusResponse: {
sentinel: null,
updateAvailable: available,
effectiveChannel: "dev",
schedule,
},
runResponse: {
ok: true,
result: {
status: "skipped",
mode: "git",
reason: "managed-service-handoff-started",
before: { version: available.currentVersion, sha: currentSha },
steps: [],
durationMs: 0,
},
handoff: { status: "started" },
},
};
}
const schedule: UpdateScheduleState = {
...baseSchedule,
campaign: {
id: "mock-update-before-failure",
state: "waiting-for-idle",
announcedAtMs: nowMs - 2 * 60_000,
forceAtMs: nowMs + 13 * 60_000,
updatedAtMs: nowMs,
},
};
const result: UpdateRunResult = {
status: "error",
mode: "git",
root: "/mock/openclaw",
reason: "build-failed",
before: { version: available.currentVersion, sha: currentSha },
after: { version: available.latestVersion, sha: upstreamSha },
steps: [
{
name: "build",
command: "pnpm build",
cwd: "/mock/openclaw",
durationMs: 8_420,
stdoutTail: "",
stderrTail: "tsc: error TS2345",
exitCode: 1,
},
],
durationMs: 11_640,
};
return {
available,
schedule,
runResponse: { ok: false, result },
statusResponse: {
sentinel: buildUpdateRestartSentinelPayload({ result, meta: {}, nowMs }),
updateAvailable: available,
effectiveChannel: "dev",
schedule: baseSchedule,
},
};
}
const repoRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
const uiRoot = path.join(repoRoot, "ui");
const boardFixturePath = "/__fixtures/board/";
const boardFixtureHtml = `<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta name="color-scheme" content="dark light" />
<title>OpenClaw Board Fixture</title>
<script>
// This standalone fixture bypasses app bootstrap, so mirror its root theme contract.
const mediaQuery = matchMedia("(prefers-color-scheme: light)");
const applyTheme = () => {
const mode = mediaQuery.matches ? "light" : "dark";
document.documentElement.dataset.theme = mode;
document.documentElement.dataset.themeMode = mode;
document.documentElement.classList.toggle("wa-light", mode === "light");
document.documentElement.classList.toggle("wa-dark", mode === "dark");
document.documentElement.style.colorScheme = mode;
};
applyTheme();
if (typeof mediaQuery.addEventListener === "function") {
mediaQuery.addEventListener("change", applyTheme);
} else {
mediaQuery.addListener(applyTheme);
}
</script>
<link rel="stylesheet" href="/src/styles.css" />
<style>
body { margin: 0; min-width: 320px; min-height: 100vh; background: var(--bg); }
#app { height: 100%; overflow: auto; }
.board-fixture-shell { box-sizing: border-box; margin: 0 auto; max-width: 1440px; padding: 36px; }
.board-fixture-header { align-items: end; display: flex; justify-content: space-between; margin-bottom: 24px; }
.board-fixture-header span { color: var(--muted); font: 10px ui-monospace, monospace; letter-spacing: .15em; }
.board-fixture-header h1 { color: var(--text-strong); font-size: 24px; letter-spacing: -.03em; margin: 5px 0 0; }
.board-fixture-status { color: var(--muted); font: 11px ui-monospace, monospace; }
.board-fixture-status i { background: var(--accent-2); border-radius: 50%; display: inline-block; height: 7px; margin-right: 6px; width: 7px; }
@media (max-width: 700px) { .board-fixture-shell { padding: 18px; } }
</style>
</head>
<body>
<div id="app"></div>
<script type="module" src="/src/test-helpers/board-fixture.ts"></script>
</body>
</html>`;
function mockFileHash(value: string): string {
return createHash("sha256").update(value, "utf8").digest("hex");
}
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 = expectDefined(args[i], `control UI mock argument at index ${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 === "--fixture") {
options.fixture = parseFixture(args[++i]);
} else if (arg.startsWith("--fixture=")) {
options.fixture = parseFixture(arg.slice("--fixture=".length));
} 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);
} else if (arg === "--operator-scopes") {
options.operatorScopes = parseOperatorScopes(args[++i]);
} else if (arg.startsWith("--operator-scopes=")) {
options.operatorScopes = parseOperatorScopes(arg.slice("--operator-scopes=".length));
}
}
return options;
}
function parseFixture(value: string | undefined): CliOptions["fixture"] {
if (!value) {
return undefined;
}
if (
value !== "approval" &&
value !== "attachments" &&
value !== "avatars" &&
value !== "board" &&
value !== "code-fences" &&
value !== "dashboards" &&
value !== "goal" &&
value !== "plugins-dense" &&
value !== "sidebar-roster" &&
value !== "swarm" &&
value !== "update-available" &&
value !== "update-blocked" &&
value !== "update-failed" &&
value !== "workboard" &&
value !== "workboard-states"
) {
throw new Error(`Unknown Control UI mock fixture: ${value}`);
}
return value;
}
function parsePort(value: string | undefined, fallback: number): number {
const parsed = Number(value);
return Number.isInteger(parsed) && parsed > 0 && parsed < 65_536 ? parsed : fallback;
}
function parseOperatorScopes(value: string | undefined): string[] | undefined {
const scopes = (value ?? "")
.split(",")
.map((scope) => scope.trim())
.filter(Boolean);
return scopes.length > 0 ? scopes : undefined;
}
function sessionsListResponse(sessions: Array<{ key: string }>, options: SessionListOptions) {
return {
count: sessions.length,
defaults: {
contextTokens: 200_000,
model: "gpt-5-mini",
modelProvider: "openai",
},
hasMore: options.hasMore,
limitApplied: 50,
nextOffset: options.nextOffset,
...(options.owners ? { owners: options.owners } : {}),
offset: options.offset ?? 0,
path: "",
// Cases select membership; canonical metadata comes from the scenario's rows.
sessions: sessions.map(({ key }) => ({ key })),
totalCount: options.totalCount,
ts: Date.now(),
};
}
function pagedSessionsListResponse(
sessions: Array<{ key: string }>,
offset: number,
owners?: readonly SessionActorFixture[],
) {
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, {
owners,
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: Array<{ key: string }>,
matchBase: Record<string, unknown> = {},
owners?: readonly SessionActorFixture[],
): Array<{ match: Record<string, unknown>; response: unknown }> {
const cases: Array<{ match: Record<string, unknown>; response: unknown }> = [];
for (let offset = SESSION_PAGE_SIZE; offset < sessions.length; offset += SESSION_PAGE_SIZE) {
cases.push({
match: { ...matchBase, offset },
response: pagedSessionsListResponse(sessions, offset, owners),
});
}
cases.push({
match: matchBase,
response: pagedSessionsListResponse(sessions, 0, owners),
});
return cases;
}
function buildSearchSessionListCases(
sessions: Array<{ key: string }>,
searchTerms: string[],
): Array<{ match: Record<string, unknown>; response: unknown }> {
return searchTerms.flatMap((search) => buildSessionListCases(sessions, { search }));
}
function buildActivitySessionRows(baseTime: number) {
const owners = {
molty: { type: "agent", id: "profile-molty", label: "Molty" },
riley: { type: "human", id: "presence-riley", label: "Riley" },
colin: { type: "human", id: "presence-colin", label: "Colin" },
patrick: {
type: "human",
id: "presence-patricia",
label: "Patrick",
},
unresolved: { type: "human", id: "147591189530201337" },
} as const;
const hour = 60 * 60 * 1000;
const day = 24 * hour;
const fixtures = [
["release-check", "Release readiness check", owners.riley, 5 * 60_000],
["api-notes", "Gateway API notes", owners.molty, 20 * 60_000],
["design-review", "Activity feed design review", owners.colin, hour],
["archive-audit", "Archive retention audit", owners.unresolved, 3 * hour],
["support-handoff", "Support handoff", owners.patrick, 6 * hour],
["mobile-smoke", "Mobile layout smoke test", owners.riley, 18 * hour],
["provider-matrix", "Provider matrix cleanup", owners.molty, 30 * hour],
["docs-pass", "Operator docs pass", owners.colin, 2 * day],
["queue-review", "Queue behavior review", owners.patrick, 2.5 * day],
["identity-trace", "Identity trace", owners.unresolved, 3 * day],
["channel-followup", "Channel delivery follow-up", owners.riley, 4 * day],
["tooling-refresh", "Tooling refresh", owners.molty, 5 * day],
["fixture-polish", "Mock fixture polish", owners.colin, 6 * day],
["weekly-summary", "Weekly activity summary", owners.patrick, 6.5 * day],
] as const;
const automationKeys = new Set(["release-check", "api-notes", "design-review"]);
return fixtures.map(([key, label, owner, age]) =>
sessionRow(`agent:activity:${key}`, label, baseTime - age, {
...(key === "archive-audit"
? {
activeRunIds: ["mock-activity-live-run"],
hasActiveRun: true,
observerDigest: {
headline: "Waiting on a fictional mock approval",
health: "waiting-on-user",
revision: 1,
runId: "mock-activity-live-run",
updatedAt: baseTime - age,
},
status: "running",
}
: {}),
createdActor: owner,
hasAutomation: automationKeys.has(key),
owner: { actor: owner },
}),
);
}
// 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-fable-5",
name: "Claude Fable 5",
provider: "anthropic",
available: true,
contextWindow: 1_000_000,
contextWindows: [
{ id: "200k", label: "200K", contextWindow: 200_000 },
{ id: "1m", label: "1M", contextWindow: 1_000_000 },
],
contextWindowDefault: "1m",
},
{
id: "claude-sonnet-4-6",
name: "Claude Sonnet 4.6",
provider: "anthropic",
available: true,
contextWindow: 200_000,
thinkingLevels: [
{ id: "low", label: "Low" },
{ id: "medium", label: "Medium" },
{ id: "high", label: "High" },
],
thinkingDefault: "medium",
reasoning: true,
supportsTools: true,
},
{
id: "gpt-5-mini",
name: "GPT-5 Mini",
provider: "openai",
available: true,
contextWindow: 200_000,
thinkingLevels: [
{ id: "low", label: "Low" },
{ id: "medium", label: "Medium" },
{ id: "high", label: "High" },
],
thinkingDefault: "medium",
reasoning: true,
supportsTools: true,
},
{ id: "gpt-5", name: "GPT-5", 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 },
],
};
}
/**
* Small but coherent config fixture so the schema-driven settings pages are
* demoable: `config.schema` covers a boolean, an enum, numbers, and strings
* across a few real section keys, and `config.get` returns a matching
* snapshot with the hash `config.set`/`config.apply` are guarded by.
*/
function buildConfigMocks(options: { swarmEnabled?: boolean; workboardEnabled?: boolean } = {}) {
const config = {
logging: { level: "info", consoleTimestamps: true },
messages: { queueLimit: 5, responsePrefix: "" },
gateway: { port: 18789, bind: "127.0.0.1", publicOrigin: "https://gateway.example" },
agents: { defaults: { thinkingDefault: "medium" } },
commands: { native: "auto", nativeSkills: "auto" },
models: { mode: "merge" },
ui: { prefs: { locale: "en" } },
...(options.swarmEnabled ? { tools: { swarm: true } } : {}),
...(options.workboardEnabled ? { plugins: { entries: { workboard: { enabled: true } } } } : {}),
channels: {
whatsapp: {
enabled: true,
allowFrom: ["whatsapp-demo-user"],
dmPolicy: "pairing",
groupPolicy: "allowlist",
selfChatMode: "off",
},
},
mcp: {
servers: {
context7: { url: "https://mcp.context7.com/mcp", transport: "streamable-http" },
github: {
url: "https://api.githubcopilot.com/mcp/",
transport: "streamable-http",
auth: "oauth",
},
"local-tools": { command: "npx", args: ["some-mcp-server", "--stdio"], enabled: false },
},
},
};
const schema = {
type: "object",
title: "OpenClaw config",
properties: {
logging: {
type: "object",
title: "Logging",
properties: {
level: {
type: "string",
title: "Log level",
description: "Minimum severity written to the gateway log.",
enum: ["silent", "error", "warn", "info", "debug"],
},
consoleTimestamps: {
type: "boolean",
title: "Console timestamps",
description: "Prefix console log lines with a timestamp.",
},
},
},
talk: {
type: "object",
title: "Talk",
properties: {
interruptOnSpeech: {
type: "boolean",
title: "Talk Interrupt on Speech",
description: "Stop speaking when the user talks over the assistant.",
},
realtime: {
type: "object",
title: "Talk Realtime",
properties: {
provider: {
type: "string",
title: "Talk Realtime Provider",
description: "Active realtime voice provider id, such as openai or google.",
},
model: {
type: "string",
title: "Talk Realtime Model",
description: "Realtime voice model for browser Talk sessions.",
},
speakerVoice: {
type: "string",
title: "Talk Realtime Speaker Voice",
description: "Built-in realtime voice id.",
},
},
},
},
},
messages: {
type: "object",
title: "Messages",
properties: {
queueLimit: {
type: "integer",
title: "Queue limit",
description: "Maximum queued inbound messages per session.",
minimum: 0,
},
responsePrefix: {
type: "string",
title: "Response prefix",
description: "Optional text prepended to outbound replies.",
},
},
},
commands: {
type: "object",
title: "Commands",
properties: {
native: {
title: "Native Commands",
default: "auto",
anyOf: [{ type: "boolean" }, { type: "string", const: "auto" }],
},
nativeSkills: {
title: "Native Skill Commands",
default: "auto",
anyOf: [{ type: "boolean" }, { type: "string", const: "auto" }],
},
},
},
gateway: {
type: "object",
title: "Gateway",
properties: {
port: { type: "integer", title: "Port", minimum: 1, maximum: 65535 },
bind: { type: "string", title: "Bind address" },
// Zod's .url() emits `format`; keep one such leaf in the fixture so the
// form stays provably editable for plugin URL/email settings.
publicOrigin: { type: "string", title: "Public origin", format: "uri" },
},
},
agents: {
type: "object",
title: "Agents",
properties: {
defaults: {
type: "object",
title: "Defaults",
properties: {
thinkingDefault: {
type: "string",
title: "Default thinking level",
enum: ["off", "low", "medium", "high"],
},
},
},
},
},
models: {
type: "object",
title: "Models",
properties: {
mode: {
type: "string",
title: "Catalog mode",
enum: ["merge", "replace"],
},
},
},
// Channel settings are the one schema surface the channels page renders,
// so the fixture keeps both tiers represented.
channels: {
type: "object",
title: "Channels",
properties: {
whatsapp: {
type: "object",
title: "WhatsApp",
properties: {
enabled: { type: "boolean", title: "Enabled" },
allowFrom: { type: "array", title: "Allow from", items: { type: "string" } },
dmPolicy: { type: "string", title: "DM policy", enum: ["pairing", "open", "off"] },
groupPolicy: {
type: "string",
title: "Group policy",
enum: ["allowlist", "open", "off"],
},
// Channel-specific leaves carry their help from the plugin's own
// uiHints in production; the fixture uses schema descriptions,
// which resolve through the same field-meta fallback.
selfChatMode: {
type: "string",
title: "Self chat mode",
description: "Same-phone setup (bot uses your personal WhatsApp number).",
enum: ["off", "notes"],
},
configWrites: { type: "boolean", title: "Config writes" },
streaming: {
type: "object",
title: "Streaming",
properties: {
progress: {
type: "object",
properties: {
maxLines: { type: "integer", title: "Progress max lines" },
toolProgress: { type: "boolean", title: "Progress tool lines" },
},
},
},
},
healthMonitor: {
type: "object",
title: "Health monitor",
properties: {
enabled: { type: "boolean", title: "Enabled" },
},
},
mediaMaxMb: { type: "number", title: "Media max MB" },
},
},
},
},
},
};
const get = {
path: "~/.openclaw/openclaw.json",
exists: true,
raw: `${JSON.stringify(config, null, 2)}\n`,
hash: "mock-config-hash",
appliedConfigHash: "mock-config-hash",
valid: true,
config,
issues: [],
};
const writeAck = { ok: true, path: get.path, hash: get.hash, config };
return {
get,
set: writeAck,
apply: {
...writeAck,
sentinel: {
persisted: true,
payload: {
kind: "config-apply",
status: "ok",
ts: 0,
message: null,
doctorHint: "openclaw doctor --non-interactive",
stats: { mode: "config.apply", root: get.path, requiresRestart: false },
},
},
},
schema: {
schema,
// Resolve tiers and shared channel help the way the gateway does so the
// mock reproduces the real split and subtext instead of bare labels.
uiHints: applySharedChannelFieldHelp(
applyResolvedConfigTierHints(
schema,
// Seed with base hints so the mock carries the gateway's labels,
// help, and docsUrl metadata instead of bare tier scaffolding.
applyConfigTierHints(buildBaseHints(), { includePluginOwnedChannels: true }),
),
),
version: "mock-config-schema",
generatedAt: new Date(0).toISOString(),
},
};
}
function buildScrollableChatHistory(baseTime: number): unknown[] {
const messages: unknown[] = Array.from({ length: 10 }, (_, index) => {
const timestamp = baseTime + index * 3 * 60_000;
return [
chatHistoryMessage("user", `Review section ${index + 1}.`, timestamp),
chatHistoryMessage(
"assistant",
`## Section ${index + 1}\n\n- Source reviewed.\n- **Follow-up:** capture the result.`,
timestamp + 35_000,
),
];
}).flat();
// Paired tool calls/results exercise the collapsed activity rollup.
const workTurnBase = baseTime + 31 * 60_000;
messages.push(
chatHistoryMessage(
"user",
"Mock work request: refactor the render guard and rerun the suite.",
workTurnBase,
),
chatHistoryMessage(
"assistant",
"Checking the guard implementation before editing.",
workTurnBase + 5_000,
),
{
role: "assistant",
content: [
{
type: "tool_call",
id: "mock-work-read",
name: "read",
args: { path: "ui/src/pages/chat/chat-thread.ts" },
},
{
type: "tool_result",
id: "mock-work-read",
name: "read",
text: "Read the transcript grouping boundary and its caller.",
},
],
timestamp: workTurnBase + 12_000,
},
{
role: "assistant",
content: [
{
type: "tool_call",
id: "mock-work-exec",
name: "exec",
args: { command: "focused transcript tests" },
},
{
type: "tool_result",
id: "mock-work-exec",
name: "exec",
text: "Synthetic command completed.",
},
],
timestamp: workTurnBase + 95_000,
},
chatHistoryMessage("assistant", "The sample command completed.", workTurnBase + 172_000),
{
role: "assistant",
content: [
{
type: "toolCall",
id: "mock-work-yield",
name: "yield",
arguments: {
message:
"Waiting for the two visible implementation owners; resume on progress/completion to coordinate shared review and verification.",
},
},
],
timestamp: workTurnBase + 173_000,
},
);
return messages;
}
function buildApprovalChatHistory(baseTime: number): unknown[] {
const runId = "mock-production-export-run";
return [
chatHistoryMessage(
"user",
"Trace the scroll-anchor regression and prepare a safe production export.",
baseTime,
),
{
role: "assistant",
content: [
{
type: "text",
text: "I found the expansion boundary and will inspect the export command before running it.",
},
{
type: "tool_call",
id: "approval-read",
name: "read",
args: { path: "ui/src/pages/chat/chat-thread.ts" },
},
{
type: "tool_result",
id: "approval-read",
name: "read",
text: "The final code block expands after the virtual row is measured.",
},
],
timestamp: baseTime + 20_000,
__openclaw: { id: "approval-analysis", runId, seq: 1 },
},
{
...chatHistoryMessage(
"assistant",
"The export requires operator approval before it can leave the mock workstation.",
baseTime + 45_000,
),
__openclaw: { id: "approval-wait", runId, seq: 2 },
},
{
role: "assistant",
content: [
{ type: "text", text: "The preview process exited before the export receipt arrived." },
],
stopReason: "error",
errorMessage: "Preview process exited with status 1.",
timestamp: baseTime + 90_000,
__openclaw: { id: "approval-error", runId, seq: 3 },
},
];
}
function buildCancelledChatHistory(baseTime: number): unknown[] {
const runId = "mock-cancelled-run";
return [
chatHistoryMessage(
"user",
"Reconcile model usage and stop if the provider session expires.",
baseTime,
),
{
...chatHistoryMessage(
"assistant",
"I collected the usage window and started comparing the configured default.",
baseTime + 15_000,
),
__openclaw: { id: "cancelled-partial", runId, seq: 1 },
},
{
...chatHistoryMessage(
"assistant",
"The operator cancelled the run before it changed any defaults.",
baseTime + 35_000,
),
stopReason: "stop",
openclawAbort: { aborted: true, origin: "operator", runId },
__openclaw: { id: "cancelled-terminal", runId, seq: 2 },
},
];
}
function buildFixtureSummaryHistory(baseTime: number, title: string, details: string): unknown[] {
return [
chatHistoryMessage("user", title, baseTime),
chatHistoryMessage("assistant", details, baseTime + 35_000),
];
}
function buildAvatarChatHistory(baseTime: number): unknown[] {
return [
...MOCK_AVATAR_PROFILES.map(({ id, label, name }, index) => ({
...chatHistoryMessage("user", label, baseTime + index * 60_000),
__openclaw: {
senderId: id,
senderIdentity: { type: "profile", id },
senderName: name,
senderProfileAvatarUrl: `/api/users/${id}/avatar`,
},
})),
{
...chatHistoryMessage("user", "Attributed initials", baseTime + 180_000),
__openclaw: {
senderId: "profile-avatar-initials",
senderIdentity: { type: "profile", id: "profile-avatar-initials" },
senderName: "Alex Morgan",
},
},
];
}
function buildCodeFenceChatHistory(baseTime: number): unknown[] {
const proseFence = (language: string, label: string) => {
const lines = Array.from({ length: 16 }, (_, index) => `${label} line ${index + 1}`);
return `\`\`\`${language}\n${lines.join("\n")}\n\`\`\``;
};
const jsonLines = Array.from({ length: 18 }, (_, index) => ` "item-${index + 1}",`);
jsonLines[jsonLines.length - 1] = ` "item-${jsonLines.length}"`;
return [
chatHistoryMessage("assistant", proseFence("text", "Plain text"), baseTime),
chatHistoryMessage("assistant", proseFence("md", "Markdown alias"), baseTime + 1_000),
chatHistoryMessage("assistant", proseFence("markdown", "Markdown"), baseTime + 2_000),
chatHistoryMessage(
"assistant",
`\`\`\`json\n[\n${jsonLines.join("\n")}\n]\n\`\`\``,
baseTime + 3_000,
),
];
}
function searchPrefixes(term: string): string[] {
return Array.from({ length: term.length }, (_value, index) => term.slice(0, index + 1));
}
function syntheticAvatar(color: "coral" | "green"): string {
const background = color === "coral" ? "#ffdbcc" : "#d5efd4";
const fill = color === "coral" ? "#e87355" : "#65a879";
const artwork = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64">
<rect width="64" height="64" rx="18" fill="${background}"/>
<path d="M12 28 16 10l13 10h6l13-10 4 18v10a20 20 0 0 1-40 0Z" fill="${fill}"/>
<g fill="#253331">
<circle cx="24" cy="33" r="3"/>
<circle cx="40" cy="33" r="3"/>
</g>
<path d="M26 43q6 6 12 0" fill="none" stroke="#253331" stroke-width="3" stroke-linecap="round"/>
</svg>`;
return `data:image/svg+xml;base64,${Buffer.from(artwork).toString("base64")}`;
}
async function createChatPickerScenario(
fixture?: CliOptions["fixture"],
): Promise<ControlUiMockGatewayScenario> {
const baseTime = Date.parse("2026-05-22T09:00:00.000Z");
const rosterTime = Date.now();
const rosterAgents = [
{
id: "main",
name: "Molty",
theme: "Your everyday coordinator",
emoji: "🦞",
avatar: syntheticAvatar("coral"),
preview: "I am organizing the next steps for the sample project.",
sessionLabels: ["Project next steps", "Weekly review"],
},
{
id: "forge",
name: "Forge",
theme: "Builds and maintains your projects",
emoji: "",
avatar: "",
preview: "The sample dashboard is ready for your review.",
sessionLabels: ["Sample dashboard", "Build notes"],
},
{
id: "scout",
name: "Scout",
theme: "Finds answers and connects ideas",
emoji: "🔭",
avatar: "",
preview: "I found three useful approaches for the research outline.",
sessionLabels: ["Research outline", "Source notes"],
},
{
id: "bloom",
name: "Bloom",
theme: "Turns rough drafts into clear stories",
emoji: "🌱",
avatar: syntheticAvatar("green"),
preview: "The welcome guide now has a shorter opening and clear examples.",
sessionLabels: ["Welcome guide", "Story ideas"],
},
] as const;
const pickerInventory = process.env.MOCK_PICKER_INVENTORY === "1";
const selfProfile: UserProfile = {
id: "presence-riley",
displayName: "Riley",
avatarMime: null,
mergedInto: null,
createdAt: baseTime,
updatedAt: baseTime,
emails: ["riley@example.com"],
githubIdentity: null,
hasAvatar: false,
};
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 whatsappLoginQrDataUrl = await qrcode.toDataURL("mock-whatsapp-login", {
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 = "/mock/workspace";
const sessionFileContentByPath = new Map([
[
"ui/src/ui/views/chat.ts",
'function renderSessionWorkspaceRail() {\n return html`<aside class="chat-workspace-rail">...</aside>`;\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:main:main" },
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:main:main",
},
},
];
const sessionFileGetCases = sessionFiles.map((file) => ({
match: { sessionKey: "agent:main:main", path: file.path },
response: {
file: {
...file,
content: sessionFileContentByPath.get(file.path) ?? "",
// Fake CAS token so the file panel offers edit mode against the mock.
hash: mockFileHash(sessionFileContentByPath.get(file.path) ?? ""),
},
root: sessionWorkspaceRoot,
sessionKey: "agent:main:main",
},
}));
const sessionFileSetCases = sessionFiles.map((file) => ({
match: { sessionKey: "agent:main:main", path: file.path },
response: {
file: {
...file,
kind: "modified",
workspacePath: file.path,
hash: mockFileHash(`${file.path}:saved`),
updatedAtMs: baseTime,
},
root: sessionWorkspaceRoot,
sessionKey: "agent:main:main",
},
}));
const lobsterSvg = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 640 360">
<rect width="640" height="360" fill="#10151d"/>
<circle cx="320" cy="185" r="76" fill="#e23f3f"/>
<ellipse cx="250" cy="178" rx="54" ry="38" fill="#f05a52"/>
<ellipse cx="390" cy="178" rx="54" ry="38" fill="#f05a52"/>
<circle cx="292" cy="145" r="10" fill="#0b0f14"/>
<circle cx="348" cy="145" r="10" fill="#0b0f14"/>
<path d="M232 114c-72-44-135-22-146 35 52 9 91-4 125-39" fill="none" stroke="#f06b5f" stroke-width="28" stroke-linecap="round"/>
<path d="M408 114c72-44 135-22 146 35-52 9-91-4-125-39" fill="none" stroke="#f06b5f" stroke-width="28" stroke-linecap="round"/>
<path d="M232 246c-45 28-91 35-142 23M408 246c45 28 91 35 142 23" fill="none" stroke="#e14b47" stroke-width="16" stroke-linecap="round"/>
<text x="320" y="326" text-anchor="middle" font-family="ui-sans-serif, system-ui" font-size="24" fill="#f6f7f9">openclaw session artifact</text>
</svg>`;
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" },
};
// Five-zone sidebar fixture: main session (hidden behind the identity card,
// its child promoted to Threads), threads with a running tree, group rows,
// and a worktree row for the Coding zone.
const mainChildRow = sessionRow(
"agent:main:lisbon-trip",
"Lisbon trip planning",
baseTime - 120_000,
{
spawnedBy: "agent:main:main",
unread: true,
},
);
const taxChildRow = sessionRow(
"agent:main:subagent:tax-receipts",
"Reading receipts",
baseTime - 30_000,
{
spawnedBy: "agent:main:tax-research",
hasActiveRun: true,
status: "running",
startedAt: baseTime - 200_000,
runtimeMs: 200_000,
},
);
const swarmGroupId = "swarm:agent:main:main:mock-turn";
const swarmChildRows =
fixture === "swarm"
? [
sessionRow("agent:main:subagent:swarm-plan", "National polling", baseTime - 9_000, {
parentSessionKey: "agent:main:main",
spawnedBy: "agent:main:main",
swarmGroupId,
swarmPhase: "Research",
swarmPhaseRank: 0,
status: "done",
}),
sessionRow("agent:main:subagent:swarm-work", "Work and labor", baseTime - 8_000, {
hasActiveRun: true,
parentSessionKey: "agent:main:main",
spawnedBy: "agent:main:main",
swarmGroupId,
swarmLog: "Comparing labor, education, and consumer signals.",
swarmPhase: "Research",
swarmPhaseRank: 0,
status: "running",
}),
sessionRow("agent:main:subagent:swarm-health", "Health", baseTime - 7_000, {
hasActiveRun: true,
parentSessionKey: "agent:main:main",
spawnedBy: "agent:main:main",
swarmGroupId,
swarmPhase: "Research",
swarmPhaseRank: 0,
status: "running",
}),
sessionRow("agent:main:subagent:swarm-trust", "Governance and trust", baseTime - 6_000, {
parentSessionKey: "agent:main:main",
spawnedBy: "agent:main:main",
subagentRunState: "active",
swarmGroupId,
swarmPhase: "Research",
swarmPhaseRank: 0,
status: undefined,
}),
sessionRow("agent:main:subagent:swarm-media", "Media signals", baseTime - 5_000, {
parentSessionKey: "agent:main:main",
spawnedBy: "agent:main:main",
swarmGroupId,
swarmPhase: "Research",
swarmPhaseRank: 0,
status: "failed",
}),
]
: [];
const workboardMocks = buildWorkboardMocks(
Date.now(),
MOCK_ACTOR_PETER,
fixture === "workboard-states",
);
const activityTime = Date.now();
const activitySessions = buildActivitySessionRows(activityTime);
const dashboardGallerySessions =
fixture === "dashboards"
? [
sessionRow("agent:main:dashboard:release-health", "Release health", baseTime - 3_000, {
boardFace: "dashboard",
createdActor: MOCK_ACTOR_MIRA,
hasActiveRun: true,
status: "running",
}),
sessionRow("agent:main:dashboard:model-spend", "Model spend", baseTime - 8_000, {
boardFace: "dashboard",
createdActor: MOCK_ACTOR_PETER,
}),
sessionRow("agent:main:dashboard:support-radar", "Support radar", baseTime - 18_000, {
boardFace: "dashboard",
createdActor: MOCK_ACTOR_MIRA,
}),
sessionRow("agent:main:dashboard:ci-signal", "CI signal", baseTime - 42_000, {
boardFace: "dashboard",
createdActor: MOCK_ACTOR_PETER,
}),
sessionRow("agent:main:dashboard:community-pulse", "Community pulse", baseTime - 75_000, {
boardFace: "dashboard",
createdActor: MOCK_ACTOR_MIRA,
}),
sessionRow("agent:main:dashboard:gateway-fleet", "Gateway fleet", baseTime - 130_000, {
boardFace: "dashboard",
createdActor: MOCK_ACTOR_PETER,
}),
]
: [];
const activeGoal = {
schemaVersion: 1 as const,
id: "goal-mobile-parity",
objective:
"Make Goal work on mobile exactly as it does on desktop while preserving action access, progress visibility, timing, token usage, and composer space across narrow viewports. Keep the collapsed state compact enough for active conversations, while making the expanded state comfortable to scan and operate with one hand. Preserve clear hierarchy between the objective, elapsed time, token budget, and available actions without letting long content push the composer out of reach. Ensure the full objective remains readable when it contains detailed constraints, acceptance criteria, rollout notes, and operational context that cannot be reduced to a short summary. Account for long-running sessions whose goals accumulate multiple requirements, edge cases, validation steps, ownership notes, and deployment considerations. The operator should be able to review all of that context without losing access to the Goal controls or forcing the message composer below the visible viewport.",
status: "active" as const,
createdAt: activityTime - 14 * 60_000,
updatedAt: activityTime - 30_000,
tokenStart: 120_000,
tokenStartFresh: true,
tokensUsed: 127_000,
tokenBudget: 300_000,
continuationTurns: 3,
};
const rosterSessions = rosterAgents.flatMap((agent, index) => [
sessionRow(`agent:${agent.id}:main`, agent.name, rosterTime - (index + 1) * 720_000, {
agentId: agent.id,
isMain: true,
lastMessagePreview: agent.preview,
}),
...agent.sessionLabels.map((label, sessionIndex) => {
const working = agent.id === "forge" && sessionIndex === 0;
return sessionRow(
`agent:${agent.id}:sample-${sessionIndex + 1}`,
label,
rosterTime - (index + 1) * 60_000 - sessionIndex * 5_000,
{
agentId: agent.id,
pinned: sessionIndex === 0,
lastMessagePreview: agent.preview,
unread: agent.id === "scout" && sessionIndex === 1,
hasActiveRun: working,
...(working ? { activeRunIds: ["mock-forge-dashboard"], status: "running" } : {}),
},
);
}),
]);
if (fixture === "sidebar-roster") {
rosterSessions.push(
sessionRow("agent:forge:subagent:layout", "Check sidebar layout", rosterTime - 20_000, {
agentId: "forge",
spawnedBy: "agent:forge:sample-1",
status: "done",
}),
sessionRow("agent:forge:subagent:contrast", "Check nested contrast", rosterTime - 10_000, {
agentId: "forge",
spawnedBy: "agent:forge:subagent:layout",
hasActiveRun: true,
status: "running",
unread: true,
}),
);
}
const gallerySessions = [
...rosterSessions.filter((row) => !row.key.startsWith("agent:main:")),
...activitySessions,
...dashboardGallerySessions,
...(fixture === "workboard" || fixture === "workboard-states"
? [
sessionRow(workboardMocks.sessionKey, "Product operations dashboard", baseTime, {
boardFace: "dashboard",
pinned: true,
}),
...workboardMocks.cardSessions,
]
: []),
sessionRow("agent:main:main", "Molty", rosterTime - 1_000, {
agentId: "main",
isMain: true,
lastMessagePreview: rosterAgents[0].preview,
activeRunIds: [PLAN_DEMO_RUN_ID],
childSessions: ["agent:main:lisbon-trip", ...swarmChildRows.map((row) => row.key)],
hasActiveRun: true,
...(fixture === "avatars" ? { kind: "global" as const } : {}),
status: "running",
totalTokens: 170_000,
totalTokensFresh: true,
...(fixture === "goal" ? { goal: activeGoal } : {}),
}),
...swarmChildRows,
sessionRow(OBSERVER_DEMO_SESSION_KEY, "Session observer demo", baseTime - 3_000, {
activeRunIds: [OBSERVER_DEMO_RUN_ID],
hasActiveRun: true,
lastReadAt: baseTime + 2_000,
observerDigest: {
headline: "Opening the focused observer tests",
health: "on-track",
revision: 1,
runId: OBSERVER_DEMO_RUN_ID,
updatedAt: baseTime - 2_000,
},
startedAt: baseTime - 4_000,
status: "running",
}),
sessionRow(NARRATION_DEMO_SESSION_KEY, "Sidebar narration demo", baseTime - 15_000, {
createdActor: MOCK_ACTOR_MIRA,
hasActiveRun: true,
owner: { actor: MOCK_ACTOR_MIRA },
startedAt: baseTime - 45_000,
status: "running",
}),
sessionRow("agent:main:tax-research", "Tax filing research", baseTime - 60_000, {
hasActiveRun: true,
status: "running",
childSessions: ["agent:main:subagent:tax-receipts"],
pinned: true,
}),
sessionRow("agent:main:cloud-refactor", "Cloud refactor worker", baseTime - 70_000, {
hasActiveRun: true,
status: "running",
startedAt: baseTime - 3_500_000,
execCwd: "/workspace/openclaw",
placement: {
state: "active",
generation: 3,
createdAtMs: baseTime - 3_600_000,
updatedAtMs: baseTime - 20_000,
stateChangedAtMs: baseTime - 3_500_000,
environmentId: "worker:9f2c4e7a81d24b06a5c3f8e1b7d94c1a",
providerId: "machine0",
profileId: "team",
machine: { class: "medium", os: "linux", osLabel: "Linux", cpu: 4, memoryGb: 16 },
activeOwnerEpoch: 4,
workerBundleHash: "b".repeat(64),
workspaceBaseManifestRef: "sha256:cloud-refactor-base",
remoteWorkspaceDir: "/workspace/openclaw",
diskSpace: {
status: "ok",
availableBytes: 61 * 1024 ** 3,
totalBytes: 100 * 1024 ** 3,
observedAtMs: baseTime - 20_000,
},
},
}),
sessionRow(
"agent:main:production-export",
"Investigate transcript scroll-anchor regression when the final code block expands",
baseTime - 75_000,
{
category: "Research",
createdActor: MOCK_ACTOR_MIRA,
execCwd: "/Users/demo/Projects/clawdbot",
owner: { actor: MOCK_ACTOR_MIRA },
},
),
sessionRow("agent:main:model-budget", "Model budget review", baseTime - 80_000, {
category: "Research",
execCwd: "/Users/demo/Projects/openclaw",
owner: { actor: { type: "human", id: "presence-riley", label: "Riley" } },
status: "failed",
lastRunError: "Model access expired: openai/gpt-5-mini",
}),
// Running rows with participants exercise the paired run trace in the lead slot.
sessionRow("agent:main:release-prep", "Release 2026.9.4 preparation", baseTime - 82_000, {
createdActor: MOCK_ACTOR_PETER,
execCwd: "/Users/demo/Work/openclaw",
hasActiveRun: true,
owner: { actor: MOCK_ACTOR_PETER },
participantCount: 1,
participants: [{ identity: { type: "profile", id: "profile-mira" }, label: "Mira" }],
status: "running",
}),
sessionRow("agent:main:release-notes", "Release notes review", baseTime - 83_000, {
createdActor: MOCK_ACTOR_MIRA,
execCwd: "/Users/demo/Work/openclaw",
hasActiveRun: true,
owner: { actor: MOCK_ACTOR_MIRA },
participantCount: 2,
participants: [
{ identity: { type: "profile", id: "profile-riley" }, label: "Riley" },
{ identity: { type: "profile", id: "profile-sam" }, label: "Sam" },
],
status: "running",
}),
sessionRow("agent:main:work-openclaw", "OpenClaw work checkout", baseTime - 85_000, {
createdActor: MOCK_ACTOR_PETER,
execCwd: "/Users/demo/Work/openclaw",
lastReadAt: baseTime - 120_000,
owner: { actor: MOCK_ACTOR_PETER },
participantCount: 4,
participants: [
{ identity: { type: "profile", id: "profile-mira" }, label: "Mira" },
{ identity: { type: "profile", id: "profile-riley" }, label: "Riley" },
{ identity: { type: "profile", id: "profile-sam" }, label: "Sam" },
{ identity: { type: "profile", id: "profile-lee" }, label: "Lee" },
],
observerDigest: {
headline: "Done: fixed the flaky retry-window test",
health: "done",
revision: 1,
runId: "mock-idle-final-run",
updatedAt: baseTime - 40_000,
},
unread: true,
}),
mainChildRow,
sessionRow("agent:main:home-server", "Home server migration", baseTime - 240_000, {
execCwd: "/Users/demo/Projects",
execNode: "a1b2c3d4e5f60718293a4b5c6d7e8f90a1b2c3d4e5f60718293a4b5c6d7e8f90",
hasAutomation: true,
pinned: true,
}),
sessionRow("agent:main:whatsapp:group:family", "Family", baseTime - 90_000, {
kind: "group",
channel: "whatsapp",
unread: true,
}),
sessionRow("agent:main:discord:channel:openclaw-dev", "#openclaw-dev", baseTime - 300_000, {
kind: "group",
channel: "discord",
}),
sessionRow("agent:main:sidebar-zones", "sidebar zones", baseTime - 150_000, {
worktree: {
id: "wt-sidebar-zones",
branch: "claude/sidebar-agent-zones",
repoRoot: "~/Projects/openclaw",
},
}),
// Second repo plus a spawned worktree checkout so the sidebar's
// Project grouping shows several sections and the worktree fold.
sessionRow("agent:main:clawdbot-vite", "Vite upgrade spike", baseTime - 160_000, {
worktree: {
id: "wt-clawdbot-vite",
branch: "openclaw/vite-upgrade",
repoRoot: "~/Projects/clawdbot",
},
}),
sessionRow("agent:main:project-grouping", "Sidebar project grouping", baseTime - 170_000, {
spawnedCwd: "~/Projects/openclaw/.claude/worktrees/groups-c7c338",
}),
...buildSessionRows({
baseTime: baseTime - 400_000,
count: 3,
keyPrefix: "main:history",
labelPrefix: "Long running session",
}),
];
const sessions = fixture === "sidebar-roster" ? rosterSessions : gallerySessions;
const archivedSessions = [
sessionRow("agent:main:archived-launch-notes", "Archived launch notes", baseTime - 86_400_000, {
archived: true,
archivedBy: MOCK_ACTOR_MIRA,
createdActor: MOCK_ACTOR_PETER,
totalTokens: 42_000,
}),
sessionRow(
"agent:main:discord:channel:archived-lounge",
"#archived-lounge",
baseTime - 172_800_000,
{
archived: true,
channel: "discord",
kind: "group",
totalTokens: 18_000,
},
),
];
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());
const skillWorkshop = buildSkillWorkshopMocks(Date.now());
const richAttention = fixture === "approval";
const cronMocks = buildCronMocks(Date.now(), {
richAttention,
...(fixture === "sidebar-roster" ? { secondAgentId: "forge" } : {}),
});
const updateFixtureNow = Date.now();
const updateFixture = buildUpdateFixture(fixture, updateFixtureNow);
const updateSchedule = updateFixture?.schedule ?? null;
const heldUpdateSchedule: UpdateScheduleState | null = updateSchedule?.campaign
? {
...updateSchedule,
campaign: {
...updateSchedule.campaign,
holdUntilMs: updateFixtureNow + 60 * 60_000,
updatedAtMs: updateFixtureNow,
},
}
: null;
const modelAuthStatus = richAttention
? {
...modelProviders.authStatus,
providers: modelProviders.authStatus.providers.map((provider) =>
provider.provider === "google"
? {
...provider,
displayName: "Google Gemini",
status: "expired" as const,
profiles: [
{
profileId: "shared engineering",
type: "oauth" as const,
status: "expired" as const,
expiry: {
at: updateFixtureNow - 12 * 60_000,
remainingMs: -12 * 60_000,
label: "12m ago",
},
},
],
}
: provider,
),
}
: modelProviders.authStatus;
const channelWizard = buildChannelWizardMocks();
const configMocks = buildConfigMocks({
swarmEnabled: fixture === "swarm",
workboardEnabled: fixture === "workboard" || fixture === "workboard-states",
});
const fixtureSessionKey =
fixture === "approval"
? "agent:main:production-export"
: fixture === "dashboards"
? "agent:main:dashboard:release-health"
: fixture === "update-available"
? "agent:main:home-server"
: fixture === "update-blocked"
? "agent:main:model-budget"
: fixture === "update-failed"
? "agent:main:cloud-refactor"
: fixture === "workboard" || fixture === "workboard-states"
? workboardMocks.sessionKey
: "agent:main:main";
const summaryHistory = buildFixtureSummaryHistory(
baseTime,
"Inspect this session.",
"The current state is available in the session controls.",
);
const fixtureHistories: Partial<Record<NonNullable<CliOptions["fixture"]>, unknown[]>> = {
approval: buildApprovalChatHistory(baseTime),
attachments: buildChatAttachmentHistory(baseTime),
avatars: buildAvatarChatHistory(baseTime),
"code-fences": buildCodeFenceChatHistory(baseTime),
"sidebar-roster": [
chatHistoryMessage("user", "Help me organize the sample project.", rosterTime - 120_000),
chatHistoryMessage(
"assistant",
"The project outline is ready. Forge is building the sample dashboard, Scout is reviewing sources, and Bloom is drafting the welcome guide.",
rosterTime - 60_000,
),
],
"update-blocked": buildCancelledChatHistory(baseTime),
};
const historyMessages = fixture
? (fixtureHistories[fixture] ?? summaryHistory)
: buildScrollableChatHistory(baseTime);
const planInFlightRun = {
runId: PLAN_DEMO_RUN_ID,
text: "",
plan: {
explanation: "Keep the Control UI change focused",
steps: [
{ step: "Inspect the transcript renderer", status: "completed" },
{ step: "Confirm the plan event contract", status: "completed" },
{ step: "Remove the duplicate card summary", status: "in_progress" },
{ step: "Run focused UI tests", status: "pending" },
{ step: "Capture browser proof", status: "pending" },
],
},
};
const backgroundTasks = buildBackgroundTasksMock(baseTime);
const custodianHistory = {
turns: [
{
role: "user",
text: "Can you check whether this system is ready?",
at: baseTime - 18 * 60_000,
},
{
role: "assistant",
text: "Everything important is connected. I’ll keep watching for changes.",
at: baseTime - 17 * 60_000,
},
{
role: "user",
text: "Please remember that I prefer concise updates.",
at: baseTime - 16 * 60_000,
},
],
} satisfies SystemAgentChatHistoryResult;
const custodianChanges = {
entries: [
{
id: "mock-system-agent-model",
at: baseTime - 6 * 60_000,
kind: "operation",
source: "system-agent",
summary: "Updated the default model for the main agent",
changedPaths: ["agents.defaults.model"],
},
{
id: "mock-plugin-install",
at: baseTime - 42 * 60_000,
kind: "config-write",
source: "plugin-install",
summary: "Enabled the Telegram plugin",
changedPaths: ["plugins.entries.telegram.enabled"],
},
{
id: "mock-doctor-repair",
at: baseTime - 2 * 60 * 60_000,
kind: "config-write",
source: "doctor",
summary: "Repaired a stale channel account reference",
changedPaths: ["channels.whatsapp.defaultAccount"],
},
],
} satisfies SystemChangesListResult;
const scenario: ControlUiMockGatewayScenario = {
assistantAgentId: "main",
assistantName: "Molty",
defaultAgentId: "main",
gatewayBootId: "mock-gateway-boot-1",
serverBuildId: "mock",
updateSchedule,
updateAvailable: updateFixture?.available ?? null,
// Advertised Gateway methods gate session actions (see
// ui/src/lib/session-method-access.ts). Omitting the mutation methods left
// every session context-menu row disabled, so the harness could not show
// the menu operators actually see. browser.request/terminal.open likewise
// gate the chat header's panel toggles, which stayed invisible here.
featureMethods: [
"browser.request",
"chat.abort",
"chat.history",
"chat.send",
"config.patch",
"config.schema",
"cron.add",
"cron.list",
"cron.remove",
"cron.run",
"cron.runs",
"cron.status",
"cron.update",
"chat.metadata",
"chat.startup",
"question.list",
"openclaw.changes.list",
"openclaw.chat",
"openclaw.chat.history",
"progressCard.get",
"plugins.inspect",
"plugins.install",
"plugins.list",
"plugins.search",
"plugins.setEnabled",
"plugins.uninstall",
"sessions.delete",
"sessions.diff",
"sessions.files.set",
"sessions.fork",
"sessions.groups.delete",
"sessions.groups.list",
"sessions.groups.put",
"sessions.groups.rename",
"sessions.patch",
"sessions.patchMany",
"sessions.search",
"skills.workshop.read",
"skills.proposals.apply",
"skills.proposals.evaluate",
"skills.proposals.historyScan",
"skills.proposals.historyStatus",
"skills.proposals.inspect",
"skills.proposals.list",
"skills.proposals.reject",
"skills.proposals.requestRevision",
"skills.library.activate",
"skills.library.import",
"skills.library.list",
"skills.library.mutate",
"skills.library.read",
"skills.library.save",
"skills.library.upload",
"tasks.cancel",
"tasks.get",
"tasks.list",
"sessions.catalog.list",
"sessions.catalog.read",
"sessions.create",
"system.info",
"desktop.observe",
"environments.list",
"terminal.open",
...(updateFixture ? ["update.hold", "update.run", "update.status"] : []),
...(fixture === "workboard" || fixture === "workboard-states"
? [
"board.get",
"cron.get",
"workboard.boards.list",
"workboard.boards.upsert",
"workboard.cards.list",
"workboard.cards.move",
"workboard.cards.create",
"workboard.cards.update",
"workboard.cards.comment",
"workboard.cards.archive",
"workboard.cards.delete",
"workboard.cards.stats",
]
: []),
],
...(fixture === "workboard" || fixture === "workboard-states" ? workboardUi : {}),
controlUiWidgetKinds: [
{ pluginId: "session", kind: "session:progress", label: "Session progress" },
...(fixture === "workboard" || fixture === "workboard-states"
? [
{ pluginId: "workboard", kind: "workboard:board", label: "Workboard board" },
{ pluginId: "workboard", kind: "workboard:card", label: "Workboard card" },
{ pluginId: "workboard", kind: "workboard:mini", label: "Workboard summary" },
]
: []),
],
// Terminal has a second gate beyond the advertised method (see
// ui/src/lib/terminal-availability.ts).
terminalEnabled: true,
// The mock rows span several owners; advertise the multi-identity policy
// so people-aware UI (People sort, Person grouping) is exercisable here.
hasMultipleSessionSharingIdentities: true,
historyMessages,
sessionGroups: ["Research"],
sessionTranscripts: {
...backgroundTasks.sessionTranscripts,
"agent:main:main": {
messages:
fixtureSessionKey === "agent:main:main"
? historyMessages
: buildScrollableChatHistory(baseTime),
...(fixture === undefined || fixture === "goal" ? { inFlightRun: planInFlightRun } : {}),
},
"agent:main:production-export": { messages: buildApprovalChatHistory(baseTime) },
"agent:main:tax-research": { messages: summaryHistory },
"agent:main:model-budget": { messages: buildCancelledChatHistory(baseTime) },
"agent:main:archived-launch-notes": {
messages: buildFixtureSummaryHistory(
baseTime,
"Save the launch decision for the archive.",
"Launch notes archived: rollout completed, the rollback window closed, and no follow-up action remains.",
),
},
[OBSERVER_DEMO_SESSION_KEY]: {
messages: buildFixtureSummaryHistory(
baseTime,
"Rerun the focused observer test and stream the result.",
"I reproduced the assertion and am checking the narrowed event path now.",
),
inFlightRun: {
runId: OBSERVER_DEMO_RUN_ID,
text: "Rerunning the focused test and comparing the latest event sequence…",
startedAt: baseTime - 4_000,
events: [],
},
},
"agent:main:dashboard:release-health": {
messages: summaryHistory,
},
"agent:main:home-server": { messages: summaryHistory },
"agent:main:cloud-refactor": { messages: summaryHistory },
[workboardMocks.sessionKey]: { messages: summaryHistory },
...(fixture === "workboard" || fixture === "workboard-states"
? workboardMocks.cardSessionHistories
: {}),
},
// Lights up the footer facepile and who's-online roster; the email-only
// entry keeps the roster's no-display-name row exercised.
presenceUsers: [
...(fixture === "avatars"
? MOCK_AVATAR_PROFILES.map(({ id, name }) => ({
id,
name,
avatarUrl: `/api/users/${id}/avatar`,
}))
: []),
{
self: true,
id: selfProfile.id,
name: selfProfile.displayName ?? undefined,
email: selfProfile.emails[0],
avatarUrl: `/api/users/${selfProfile.id}/avatar`,
},
{
id: "presence-colin",
name: "Colin",
email: "colin@example.com",
onlineSince: activityTime - 47 * 60_000,
lastActivityAt: activityTime - 2 * 60_000,
deviceFamily: "Mac",
platform: "macOS",
timeZone: "America/Los_Angeles",
watchedSessions: ["agent:activity:design-review", "agent:main:main"],
},
{
id: "presence-colin",
name: "Colin",
email: "colin@example.com",
onlineSince: activityTime - 47 * 60_000,
lastActivityAt: activityTime - 2 * 60_000,
deviceFamily: "Mac",
platform: "macOS",
timeZone: "America/Los_Angeles",
watchedSessions: ["agent:activity:design-review"],
},
{
id: "presence-patricia",
name: "Patrick",
email: "patrick.erichsen@example.com",
onlineSince: activityTime - 12 * 60_000,
lastActivityAt: activityTime - 30_000,
deviceFamily: "iPhone",
platform: "iOS",
timeZone: "Europe/Stockholm",
watchedSessions: ["agent:activity:support-handoff"],
},
],
methodResponses: {
...cronMocks,
"agents.list": {
agents: rosterAgents.map(({ id, name, theme, emoji, avatar }) => ({
id,
name,
identity: { name, theme, emoji, avatar },
model: { primary: "example/model-small" },
})),
defaultId: "main",
mainKey: "main",
scope: "per-sender",
},
"agent.identity.get": {
cases: rosterAgents.map(({ id, name, theme, emoji, avatar }) => ({
match: { agentId: id },
response: { agentId: id, name, theme, emoji, avatar, avatarStatus: "data" },
})),
},
"progressCard.get": { card: null },
"users.self": { profile: selfProfile },
// Talk settings page pickers: realtime catalog with the model/voice
// suggestion lists the gateway emits for provider entries.
"talk.catalog": {
modes: ["realtime", "stt-tts", "transcription"],
transports: ["webrtc", "provider-websocket", "gateway-relay", "managed-room"],
brains: ["agent-consult", "direct-tools", "none"],
speech: { providers: [] },
transcription: { providers: [] },
realtime: {
ready: true,
activeProvider: "openai",
providers: [
{
id: "openai",
label: "OpenAI Realtime Voice",
configured: true,
defaultModel: "gpt-realtime-2.1",
transports: ["webrtc", "gateway-relay"],
models: ["gpt-realtime-2.1", "gpt-realtime-2.1-mini", "gpt-realtime-2"],
voices: [
"alloy",
"ash",
"ballad",
"cedar",
"coral",
"echo",
"marin",
"sage",
"shimmer",
"verse",
],
modes: ["realtime"],
brains: ["agent-consult"],
supportsBrowserSession: true,
},
{
id: "xai",
label: "xAI Grok Voice",
configured: false,
defaultModel: "grok-voice-latest",
transports: ["gateway-relay"],
voices: ["eve", "ara", "rex", "sal", "leo"],
modes: ["realtime"],
brains: ["agent-consult"],
supportsBrowserSession: false,
},
],
},
},
// Coding session catalogs so the sidebar's catalog sections (header
// right-click menu, hide/restore preference) are exercised in the mock.
// Ids must match registered plugin catalogs (`claude`, `codex`) or the
// sidebar cannot resolve bundled brand marks.
"sessions.catalog.list": {
catalogs: [
{
id: "codex",
label: "Codex",
capabilities: { continueSession: true, archive: false, startTerminal: true },
hosts: [
{
hostId: "gateway",
label: "This Mac",
kind: "gateway",
connected: true,
sessions: [
{
threadId: "codex-thread-1",
name: "Release checklist sweep",
cwd: "/Users/demo/projects/openclaw",
status: "idle",
updatedAt: baseTime - 10 * 60_000,
archived: false,
canContinue: true,
canArchive: false,
},
{
threadId: "codex-thread-2",
name: "Sidebar context-menu proof",
cwd: "/Users/demo/projects/openclaw",
status: "idle",
updatedAt: baseTime - 45 * 60_000,
archived: false,
canContinue: true,
canArchive: false,
},
],
},
],
},
{
id: "claude",
label: "Claude Code",
capabilities: { continueSession: true, archive: false },
hosts: [
{
hostId: "gateway",
label: "This Mac",
kind: "gateway",
connected: true,
sessions: [
{
threadId: "claude-thread-1",
name: "Docs refresh",
cwd: "/Users/demo/projects/peekaboo",
status: "idle",
updatedAt: baseTime - 30 * 60_000,
archived: false,
canContinue: true,
canArchive: false,
},
],
},
],
},
],
},
"sessions.catalog.read": {
cases: [
{
match: { catalogId: "codex", hostId: "gateway", threadId: "codex-thread-1" },
response: {
hostId: "gateway",
threadId: "codex-thread-1",
items: [
{
id: "release-checklist-answer",
type: "agentMessage",
text: "The release checklist is complete and ready for review.",
},
{
id: "release-checklist-request",
type: "userMessage",
text: "Please sweep the release checklist for anything we missed.",
},
],
},
},
{
match: { catalogId: "codex", hostId: "gateway", threadId: "codex-thread-2" },
response: {
hostId: "gateway",
threadId: "codex-thread-2",
items: [
{
id: "sidebar-context-menu-answer",
type: "agentMessage",
text: "The sidebar context menu behaves as expected.",
},
],
},
},
{
match: {
catalogId: "claude",
hostId: "gateway",
threadId: "claude-thread-1",
},
response: {
hostId: "gateway",
threadId: "claude-thread-1",
items: [
{
id: "docs-refresh-answer",
type: "agentMessage",
text: "The documentation refresh is ready for review.",
},
],
},
},
],
},
"system.info": {
machineName: "Mock-Workstation",
hostname: "mock-workstation.invalid",
platform: "darwin",
release: "25.0.0",
arch: "arm64",
osLabel: "macOS 26.5",
nodeVersion: "24.15.0",
pid: 4242,
uptimeMs: (11 * 24 + 4) * 3_600_000,
loadAverage: [3.2, 2.8, 2.4],
cpuCount: 16,
memoryTotalBytes: 68_719_476_736,
memoryFreeBytes: 34_359_738_368,
diskTotalBytes: 1_000_000_000_000,
diskAvailableBytes: 640_000_000_000,
diskPath: "/Users/demo/.openclaw",
defaultAgentUtilityModel: {
status: "auto",
model: "anthropic/claude-haiku-4-5",
},
},
"fs.listDir": {
cases: [
{
match: { path: "/Users/demo/Projects/openclaw" },
response: {
path: "/Users/demo/Projects/openclaw",
parent: "/Users/demo/Projects",
home: "/Users/demo",
entries: [
{ name: "ui", path: "/Users/demo/Projects/openclaw/ui" },
{ name: "src", path: "/Users/demo/Projects/openclaw/src" },
{ name: "docs", path: "/Users/demo/Projects/openclaw/docs" },
{ name: "packages", path: "/Users/demo/Projects/openclaw/packages" },
],
},
},
{
match: { path: "/Users/demo/Projects" },
response: {
path: "/Users/demo/Projects",
parent: "/Users/demo",
home: "/Users/demo",
entries: [
{ name: "openclaw", path: "/Users/demo/Projects/openclaw" },
{ name: "clawdbot", path: "/Users/demo/Projects/clawdbot" },
{ name: "sweetistics", path: "/Users/demo/Projects/sweetistics" },
{ name: "Peekaboo", path: "/Users/demo/Projects/Peekaboo" },
],
},
},
{
match: {},
response: {
path: "/Users/demo",
parent: "/Users",
home: "/Users/demo",
entries: [
{ name: "Projects", path: "/Users/demo/Projects" },
{ name: "Downloads", path: "/Users/demo/Downloads" },
{ name: ".config", path: "/Users/demo/.config", hidden: true },
],
},
},
],
},
"worktrees.branches": {
cases: [
{
match: { repoRoot: "/Users/demo/Projects/openclaw" },
response: {
repoRoot: "/Users/demo/Projects/openclaw",
branches: [
{ kind: "local", name: "main" },
{ kind: "local", name: "steipete/place-picker" },
],
repositoryStatus: "git",
defaultBranch: "main",
headBranch: "main",
},
},
{
match: { repoRoot: "/Users/demo/Projects/clawdbot" },
response: {
repoRoot: "/Users/demo/Projects/clawdbot",
branches: [
{ kind: "local", name: "main" },
{ kind: "local", name: "steipete/storage-selector-design" },
],
repositoryStatus: "git",
defaultBranch: "main",
headBranch: "main",
},
},
],
},
"environments.list": {
environments: [
{
id: "gateway",
type: "gateway",
label: "Gateway host",
status: "available",
desktop: true,
},
{
id: "node:a1b2c3d4e5f60718293a4b5c6d7e8f90a1b2c3d4e5f60718293a4b5c6d7e8f90",
type: "node",
label: "Mac Studio",
status: "available",
desktop: true,
...(pickerInventory
? {
platform: "darwin",
sessionHost: true,
workerSlots: { total: 4, available: 3 },
}
: {}),
},
...(pickerInventory
? [
{
id: "node:mock-macbook-offline",
type: "node",
label: "MacBook Pro",
platform: "darwin",
status: "unavailable",
sessionHost: true,
lastConnectedAtMs: baseTime - 5 * 86_400_000,
lastDisconnectedAtMs: baseTime - (4 * 24 + 13) * 3_600_000,
},
]
: []),
],
profiles: [
{
id: "aws",
providerId: "aws",
...(pickerInventory ? { executionModes: ["worker-turn", "remote-exec"] } : {}),
},
...(pickerInventory
? [
{
id: "crabbox",
providerId: "crabbox",
executionModes: ["worker-turn", "remote-exec"],
},
{
id: "test-cloud",
providerId: "test-cloud",
executionModes: ["worker-turn", "remote-exec"],
},
]
: []),
],
},
// config.set/config.apply are served statefully by the mock gateway
// (raw persists, hash advances) because config.get ships a raw fixture.
"config.get": configMocks.get,
"config.set": configMocks.set,
"config.apply": configMocks.apply,
"config.schema": configMocks.schema,
"openclaw.chat.history": custodianHistory,
"openclaw.changes.list": custodianChanges,
// The sidebar recovers pending questions through question.list after the
// hello handshake, so this remains visible after a mock-page refresh.
"question.list": {
questions: [
{
id: "mock_tax_question",
agentId: "main",
sessionKey: "agent:main:tax-research",
questions: [
{
id: "filing_status",
header: "Tax filing",
question: "Should I submit the draft return?",
options: [
{ label: "Submit", description: "File the prepared return." },
{ label: "Review", description: "Keep the draft open for review." },
],
},
],
createdAtMs: baseTime - 60_000,
expiresAtMs: ATTENTION_FIXTURE_EXPIRES_AT,
status: "pending",
},
],
},
"plugin.approval.list": [],
"openclaw.approval.list": [],
"plugin.approval.resolve": { ok: true },
"sessions.patch": { ok: true },
"sessions.diff": buildSessionDiffMock(),
// The worktrees page assumes the gateway contract shape; without this
// fixture the mock's {} fallback surfaces as a TypeError banner.
"worktrees.list": {
worktrees: [
{
id: "wt-mock-1",
name: "fix-session-icons",
repoFingerprint: "a1b2c3d4e5f60718",
repoRoot: "/Users/demo/Projects/openclaw",
path: "/Users/demo/Projects/openclaw/.openclaw/worktrees/fix-session-icons",
branch: "openclaw/fix-session-icons",
baseRef: "origin/main",
ownerKind: "session",
createdAt: baseTime - 3 * 86_400_000,
lastActiveAt: baseTime - 2 * 3_600_000,
},
{
id: "wt-mock-2",
name: "dashboard-polish",
repoFingerprint: "a1b2c3d4e5f60718",
repoRoot: "/Users/demo/Projects/openclaw",
path: "/Users/demo/Projects/openclaw/.openclaw/worktrees/dashboard-polish",
branch: "openclaw/dashboard-polish",
baseRef: "origin/main",
ownerKind: "manual",
createdAt: baseTime - 9 * 86_400_000,
lastActiveAt: baseTime - 26 * 3_600_000,
},
],
},
"plugins.list": buildPluginCatalogMock({
installedCopies: fixture === "plugins-dense" ? 10 : 1,
}),
"plugins.catalog.browse": buildPluginDiscoveryMock(),
"plugins.catalog.categories": buildPluginDiscoveryCategoriesMock(),
"plugins.inspect": buildPluginInspectMock({
installedCopies: fixture === "plugins-dense" ? 10 : 1,
}),
"skills.status": {
workspaceDir: "/Users/demo/Projects/openclaw",
managedSkillsDir: "/Users/demo/.openclaw/skills",
skills: [],
},
"channels.status": buildChannelsStatusMock(baseTime),
"channels.pairing.list": buildChannelsPairingMock(baseTime),
"channels.pairing.approve": {
cases: [
{
match: { requestId: "pairing-req-1" },
response: {
requestId: "pairing-req-1",
senderId: "telegram-demo-user",
notification: "sent",
commandOwnerBootstrap: "not-requested",
},
},
{
response: {
requestId: "pairing-req-2",
senderId: "whatsapp-demo-user",
notification: "unsupported",
commandOwnerBootstrap: "not-requested",
},
},
],
},
"channels.pairing.dismiss": {
cases: [
{
match: { requestId: "pairing-req-1" },
response: { requestId: "pairing-req-1", senderId: "telegram-demo-user" },
},
{ response: { requestId: "pairing-req-2", senderId: "whatsapp-demo-user" } },
],
},
"web.login.start": {
message: "Scan the QR code with WhatsApp to link this device.",
qrDataUrl: whatsappLoginQrDataUrl,
},
"web.login.wait": { message: "Linked.", connected: true },
"wizard.start": channelWizard.start,
"wizard.next": channelWizard.next,
"wizard.cancel": { status: "cancelled" },
"skills.proposals.requestRevision": skillWorkshop.requestRevision,
"usage.cost": profileUsage.cost,
"sessions.usage": profileUsage.sessions,
"models.authStatus": modelAuthStatus,
"update.hold": heldUpdateSchedule
? { ok: true, schedule: heldUpdateSchedule }
: { ok: false },
"update.run": updateFixture?.runResponse ?? {},
"update.status": updateFixture?.statusResponse ?? {},
"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 mini",
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: [
createOfflineDeviceNode(),
{
nodeId: "a1b2c3d4e5f60718293a4b5c6d7e8f90a1b2c3d4e5f60718293a4b5c6d7e8f90",
displayName: "Mac Studio",
platform: "darwin",
deviceFamily: "Mac",
modelIdentifier: "Mac15,14",
remoteIp: "192.168.1.11",
version: "2026.6.11",
connected: true,
paired: true,
approvalState: "approved",
connectedAtMs: baseTime - 60_000,
hostStats: {
cpuCount: 24,
loadAverage: [3.2, 2.8, 2.4],
memoryTotalBytes: 192 * 1024 ** 3,
memoryFreeBytes: 41 * 1024 ** 3,
diskTotalBytes: 2 * 1024 ** 4,
diskAvailableBytes: 1.2 * 1024 ** 4,
updatedAtMs: baseTime,
},
caps: [
"browser",
"canvas",
"screen",
"computer",
"file",
"system",
"mcp",
"local-inference",
"claude-sessions",
"codex-cli-sessions",
"pi-sessions",
],
commands: [
"desktop.stream",
"screen.snapshot",
"system.execApprovals.get",
"system.execApprovals.set",
"system.notify",
"system.run",
"system.which",
],
},
{
nodeId: "0f1e2d3c4b5a69788796a5b4c3d2e1f00f1e2d3c4b5a69788796a5b4c3d2e1f0",
displayName: "Mac mini",
platform: "darwin",
deviceFamily: "Mac",
modelIdentifier: "Mac16,11",
remoteIp: "192.168.1.12",
version: "2026.6.10",
connected: true,
paired: true,
approvalState: "approved",
lastSeenAtMs: baseTime - 82_800_000,
hostStats: {
cpuCount: 12,
loadAverage: [15.4, 13.8, 11.2],
memoryTotalBytes: 32 * 1024 ** 3,
memoryFreeBytes: 2 * 1024 ** 3,
diskTotalBytes: 1024 ** 4,
diskAvailableBytes: 64 * 1024 ** 3,
updatedAtMs: baseTime,
},
caps: ["browser", "computer", "file", "system", "codex-cli-sessions"],
commands: ["desktop.stream", "screen.snapshot", "system.run"],
},
{
nodeId: "11223344556677889900aabbccddeeff11223344556677889900aabbccddeeff",
displayName: "iPhone",
platform: "iOS 26.4",
deviceFamily: "iPhone",
modelIdentifier: "iPhone17,2",
remoteIp: "192.168.1.30",
version: "2026.6.11",
connected: true,
paired: true,
approvalState: "approved",
lastSeenAtMs: baseTime - 3_600_000,
caps: ["camera", "canvas", "contacts", "device", "location"],
commands: [
"camera.list",
"contacts.search",
"device.info",
"location.get",
"system.run",
],
},
],
},
"system-presence": [
{
host: "gateway-mock.local",
ip: "192.168.1.10",
version: "2026.6.11",
platform: "macos 26.5.2",
deviceFamily: "Mac",
modelIdentifier: "Mac14,12",
lastInputSeconds: 42,
mode: "gateway",
reason: "self",
instanceId: "mock-gateway-instance",
text: "Gateway: gateway-mock.local (192.168.1.10) · app 2026.6.11 · mode gateway · reason self",
ts: baseTime,
},
{
host: "Mac Studio",
ip: "192.168.1.11",
version: "2026.6.11",
platform: "macos 26.5.2",
deviceFamily: "Mac",
modelIdentifier: "Mac15,14",
lastInputSeconds: 177,
mode: "node",
reason: "periodic",
deviceId: "a1b2c3d4e5f60718293a4b5c6d7e8f90a1b2c3d4e5f60718293a4b5c6d7e8f90",
instanceId: "a1b2c3d4e5f60718293a4b5c6d7e8f90a1b2c3d4e5f60718293a4b5c6d7e8f90",
roles: ["node"],
text: "Node: Mac Studio (192.168.1.11) · app 2026.6.11 · last input 177s ago · mode node · reason periodic",
ts: baseTime - 30_000,
},
{
host: "openclaw-control-ui",
version: "2026.6.11",
platform: "macos 26.5.2",
mode: "webchat",
reason: "connect",
roles: ["operator"],
instanceId: "mock-unpaired-webchat",
text: "Node: openclaw-control-ui · mode webchat",
ts: baseTime - 10_000,
},
],
"agents.files.get": {
cases: workspaceFileCases,
},
"agents.files.list": {
cases: workspaceListCases,
},
"sessions.files.get": {
cases: sessionFileGetCases,
},
"sessions.files.set": {
cases: sessionFileSetCases,
},
"sessions.files.list": {
cases: [
{
match: { sessionKey: "agent:main:main", 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:main:main",
},
},
{
match: { sessionKey: "agent:main:main", 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:main:main",
},
},
...sessionFileCases,
],
},
"artifacts.list": {
cases: [
{
match: { sessionKey: "agent:main:main" },
response: { artifacts: [lobsterArtifact] },
},
],
},
"artifacts.download": {
cases: [
{
match: { sessionKey: "agent:main:main", artifactId: lobsterArtifact.id },
response: {
artifact: lobsterArtifact,
data: Buffer.from(lobsterSvg, "utf8").toString("base64"),
encoding: "base64",
},
},
],
},
"sessions.companion.ask": {
cases: [
{
match: { sessionKey: OBSERVER_DEMO_SESSION_KEY },
response: {
answer: "It is rerunning the focused test to check whether the latest fix is stable.",
ts: baseTime + 2_000,
},
},
],
},
// Saturated-main fixture so the debug page and overlay render queued and
// group-budget states, not just idle lanes.
"diagnostics.lanes": {
ts: baseTime,
lanes: [
{
lane: "cron",
queuedCount: 0,
activeCount: 1,
maxConcurrent: 4,
draining: false,
generation: 1,
},
{
lane: "cron-nested",
queuedCount: 0,
activeCount: 1,
maxConcurrent: 4,
draining: false,
generation: 1,
group: "cron-hooks",
groupActive: 2,
groupBudget: 4,
},
{
lane: "hook-dispatch",
queuedCount: 2,
activeCount: 1,
maxConcurrent: 4,
draining: false,
generation: 1,
group: "cron-hooks",
groupActive: 2,
groupBudget: 4,
reservedForLane: 1,
blockedBy: "group-budget",
},
{
lane: "main",
queuedCount: 3,
activeCount: 16,
maxConcurrent: 16,
draining: false,
generation: 7,
blockedBy: "lane",
},
{
lane: "nested",
queuedCount: 0,
activeCount: 0,
maxConcurrent: 1,
draining: false,
generation: 1,
},
{
lane: "subagent",
queuedCount: 5,
activeCount: 8,
maxConcurrent: 8,
concurrencyScope: "session",
saturatedLaneCount: 1,
draining: false,
generation: 4,
blockedBy: "lane",
},
],
dynamic: {
laneCount: 23,
activeCount: 9,
queuedCount: 4,
queuedLaneCount: 3,
},
},
status: {
eventLoop: { utilization: 0.42, cpuCoreRatio: 0.24, delayP99Ms: 12, delayMaxMs: 87 },
processMemory: {
rssBytes: 432 * 1_048_576,
heapUsedBytes: 210 * 1_048_576,
heapTotalBytes: 280 * 1_048_576,
},
uptimeMs: 5_412_000,
},
"last-heartbeat": { ts: baseTime },
"sessions.list": {
cases: [
// Child fetches must precede the catch-all page case (subset match).
{
match: { spawnedBy: "agent:main:main" },
response: pagedSessionsListResponse([mainChildRow, ...swarmChildRows], 0),
},
{
match: { spawnedBy: "agent:main:tax-research" },
response: pagedSessionsListResponse([taxChildRow], 0),
},
...buildSearchSessionListCases(telegramSessions, searchPrefixes("telegram")),
...buildSearchSessionListCases(claudeSessions, [
...searchPrefixes("claude"),
...searchPrefixes("claude-sonnet-4-6"),
...searchPrefixes("anthropic"),
]),
...buildSessionListCases(
fixture === "sidebar-roster" ? sessions : [...sessions, ...archivedSessions],
{},
MOCK_SESSION_OWNERS,
),
],
},
"sessions.search": { results: [] },
...(fixture === "workboard" || fixture === "workboard-states"
? workboardMocks.methodResponses
: {}),
},
models: modelProviders.models,
repeatingSessionEvents: {
intervalMs: 3_000,
events: [
{
event: "session.observer",
payload: {
headline: "Reading the failing test and its board caller",
health: "on-track",
revision: 2,
runId: OBSERVER_DEMO_RUN_ID,
sessionKey: OBSERVER_DEMO_SESSION_KEY,
updatedAt: baseTime + 1_000,
},
},
{
event: "session.observer",
payload: {
assessment:
"The first fix was incomplete, so the agent is narrowing the assertion path.",
headline: "Third run of the same vitest file - two assertions still failing",
health: "grinding",
planProgress: { completed: 2, total: 4 },
revision: 3,
runId: OBSERVER_DEMO_RUN_ID,
sessionKey: OBSERVER_DEMO_SESSION_KEY,
updatedAt: baseTime + 4_000,
},
},
{
event: "session.observer",
payload: {
assessment: "Repeated identical failures suggest the current approach needs a reset.",
headline: "Same failure five runs in a row - it may be circling",
health: "stuck",
planProgress: { completed: 2, total: 4 },
revision: 4,
runId: OBSERVER_DEMO_RUN_ID,
sessionKey: OBSERVER_DEMO_SESSION_KEY,
updatedAt: baseTime + 7_000,
},
},
{
event: "agent",
payload: {
// replace: the demo replays the same snapshot each cycle; without
// it the controller's cumulative-length dedupe drops the repeats.
data: { replace: true, text: "Rebasing onto main and rerunning the sidebar suite." },
runId: NARRATION_DEMO_RUN_ID,
sessionKey: NARRATION_DEMO_SESSION_KEY,
stream: "assistant",
},
},
{
event: "session.tool",
payload: {
data: { name: "exec" },
runId: NARRATION_DEMO_RUN_ID,
sessionKey: NARRATION_DEMO_SESSION_KEY,
stream: "tool",
},
},
],
},
sessionArchiveFiltering: true,
sessions: [
...sessions,
...backgroundTasks.sessions,
...archivedSessions,
...telegramSessions,
...claudeSessions,
taxChildRow,
],
sessionKey: fixtureSessionKey,
workspace: "/Users/demo/Projects/openclaw",
workspaceGit: true,
};
if (fixture === "sidebar-roster") {
const teamTasks = backgroundTasks.tasks.slice(0, 2).map((task, index) => {
const agent = expectDefined(rosterAgents[index], "team task agent");
return Object.assign({}, task, {
agentId: agent.id,
title: agent.sessionLabels[0],
sessionKey: `agent:${agent.id}:main`,
ownerKey: `agent:${agent.id}:main`,
childSessionKey: `agent:${agent.id}:sample-1`,
});
});
scenario.methodResponses = {
...scenario.methodResponses,
"sessions.catalog.list": { catalogs: [] },
"tasks.list": {
cases: [
...rosterAgents.map(({ id }) => ({
match: { agentId: id },
response: { tasks: teamTasks.filter((task) => task.agentId === id) },
})),
{ response: { tasks: teamTasks } },
],
},
"tasks.get": {
cases: teamTasks.map((task) => ({
match: { taskId: task.id },
response: { task },
})),
},
"cron.list": {
cases: [
...rosterAgents.flatMap(({ id }) =>
cronMocks["cron.list"].cases.map((entry) => {
const jobs = entry.response.jobs.filter((job) => job.agentId === id);
return {
match: { ...entry.match, agentId: id },
response: { ...entry.response, jobs, total: jobs.length },
};
}),
),
...cronMocks["cron.list"].cases,
],
},
};
scenario.sessions = rosterSessions;
scenario.repeatingSessionEvents = { events: [] };
scenario.sessionGroups = [];
}
return scenario;
}
function escapeScriptContent(script: string): string {
return script.replaceAll("</script", "<\\/script");
}
async function createMockGatewayPlugin(
scenario: ControlUiMockGatewayScenario,
fixture?: CliOptions["fixture"],
): Promise<Plugin> {
const prepared = await prepareControlUiMockGatewayScenario(
fixture === "workboard-states" ? { ...scenario, nativePlugins: [] } : scenario,
);
if (fixture === "workboard-states") {
const { assets, catalog } = await buildWorkboardStatesAssets();
for (const [url, asset] of assets) {
prepared.assets.set(url, asset);
}
prepared.scenario = {
...prepared.scenario,
featureMethods: [
...new Set([
...(prepared.scenario.featureMethods ?? []),
"plugins.controlUi.list",
"plugins.controlUi.report",
]),
],
methodResponses: {
...prepared.scenario.methodResponses,
"plugins.controlUi.list": catalog,
"plugins.controlUi.report": { ok: true },
},
};
}
const initScript = escapeScriptContent(createControlUiMockGatewayInitScript(prepared.scenario));
const sameOriginGatewayScript = escapeScriptContent(createControlUiMockSameOriginGatewayScript());
const newAgentWelcome = await buildNewAgentWelcome({
engine: { noteAssistantMessage: () => {} },
});
const statefulInitScript = escapeScriptContent(
createControlUiPreviewInitScript(newAgentWelcome) +
skillLibraryMockInitScript(prepared.scenario.models) +
pluginLifecycleMockInitScript() +
skillWorkshopMockInitScript(Date.now()) +
(fixture === "sidebar-roster" ? "" : backgroundTasksMockInitScript(Date.now())) +
approvalMockInitScript(fixture === "approval") +
(fixture === "workboard" || fixture === "workboard-states"
? `(() => { const __name = (target) => target; (${installWorkboardBoardMock.toString()})(${JSON.stringify(buildWorkboardMocks(Date.now(), MOCK_ACTOR_PETER, fixture === "workboard-states"))}); })();`
: ""),
);
const bootstrapBody = JSON.stringify(createControlUiMockBootstrapConfig(prepared.scenario));
const pluginIconIds = new Set(
buildPluginCatalogMock()
.plugins.filter((plugin) => plugin.hasIcon)
.map((plugin) => plugin.id),
);
const attachmentThemeToggle =
fixture === "attachments" || fixture === "avatars"
? ` <style data-openclaw-control-ui-mock-theme-toggle>
.control-ui-mock-theme-toggle { position: fixed; right: 16px; bottom: 16px; z-index: 1000; display: inline-flex; gap: 2px; padding: 3px; border: 1px solid var(--border-strong); border-radius: 999px; background: var(--card); box-shadow: var(--shadow-md); }
.control-ui-mock-theme-toggle button { min-height: 28px; padding: 0 10px; border: 0; border-radius: 999px; color: var(--muted); background: transparent; font: inherit; font-size: 11px; font-weight: 600; cursor: pointer; }
.control-ui-mock-theme-toggle button[aria-pressed="true"] { color: var(--text); background: var(--bg-hover); }
</style>
<script type="module" data-openclaw-control-ui-mock-theme-toggle>
import { syncControlUiSystemChrome } from "/src/app/control-ui-presentation.ts";
addEventListener("DOMContentLoaded", () => {
const control = document.createElement("div");
control.className = "control-ui-mock-theme-toggle";
control.setAttribute("aria-label", "Theme");
const apply = (mode) => {
const root = document.documentElement;
root.dataset.theme = mode;
root.dataset.themeMode = mode;
root.dataset.themeResolved = mode;
root.classList.toggle("wa-light", mode === "light");
root.classList.toggle("wa-dark", mode === "dark");
root.style.colorScheme = mode;
syncControlUiSystemChrome();
for (const button of control.querySelectorAll("button")) {
button.setAttribute("aria-pressed", String(button.dataset.mode === mode));
}
};
for (const mode of ["dark", "light"]) {
const button = document.createElement("button");
button.type = "button";
button.dataset.mode = mode;
button.textContent = mode === "dark" ? "Dark" : "Light";
button.addEventListener("click", () => apply(mode));
control.append(button);
}
document.body.append(control);
apply(document.documentElement.dataset.themeMode === "light" ? "light" : "dark");
});
</script>
`
: "";
return {
configureServer(server) {
if (fixture === "avatars") {
const avatarFills = new Map(
MOCK_AVATAR_PROFILES.map(({ fill, id }) => [`/api/users/${id}/avatar`, fill]),
);
server.middlewares.use((req, res, next) => {
const pathname = new URL(req.url ?? "/", "http://openclaw.invalid").pathname;
const fill = avatarFills.get(pathname);
if (!fill) {
next();
return;
}
res.statusCode = 200;
res.setHeader("content-type", "image/svg+xml");
res.end(
`<svg xmlns="http://www.w3.org/2000/svg" width="36" height="36"><rect width="36" height="36" fill="${fill}"/></svg>`,
);
});
}
server.middlewares.use((req, res, next) => {
const prefix = "/__openclaw__/plugin-icon/";
const pathname = new URL(req.url ?? "/", "http://openclaw.invalid").pathname;
if (!pathname.startsWith(prefix)) {
next();
return;
}
const pluginId = decodeURIComponent(pathname.slice(prefix.length));
if (!pluginIconIds.has(pluginId)) {
next();
return;
}
const icon = path.join(repoRoot, "extensions", pluginId, "assets", "icon.png");
if (!fs.existsSync(icon)) {
next();
return;
}
res.statusCode = 200;
res.setHeader("content-type", "image/png");
res.end(fs.readFileSync(icon));
});
server.middlewares.use((req, res, next) => {
const pathname = req.url?.split("?", 1)[0];
if (!pathname?.startsWith(controlUiPluginAssetRoot())) {
next();
return;
}
const asset = prepared.assets.get(pathname);
res.statusCode = asset ? 200 : 404;
if (asset) {
res.setHeader("content-type", asset.contentType);
}
res.end(asset?.body);
});
server.middlewares.use(CONTROL_UI_BOOTSTRAP_CONFIG_PATH, (_req, res) => {
res.statusCode = 200;
res.setHeader("content-type", "application/json");
res.end(bootstrapBody);
});
},
// ui/vite.config.ts registers a placeholder bootstrap-config middleware and
// config-file plugins load first, so without "pre" its stub answers every
// request and the scenario's bootstrap fields never reach the app.
enforce: "pre",
name: "openclaw-control-ui-mock-gateway",
transformIndexHtml(html) {
const rosterPreferenceScript = `<script data-openclaw-sidebar-roster>
${fixture === "sidebar-roster" ? 'localStorage.setItem("openclaw:control-ui:community-invite", JSON.stringify({ dismissedAtMs: Date.now() }));' : ""}
if (new URLSearchParams(location.search).get("sidebarAgents") === "roster") {
const gatewayUrl = window["__OPENCLAW_NATIVE_CONTROL_AUTH__"].gatewayUrl;
const key = "openclaw.control.settings.v1:" + gatewayUrl;
const prefs = JSON.parse(localStorage.getItem(key) || "{}");
localStorage.setItem(key, JSON.stringify({ ...prefs, gatewayUrl, sidebarAgentsMode: "roster" }));
localStorage.setItem("openclaw.control.currentGateway.v1:" + new URL(gatewayUrl).origin, gatewayUrl);
}
</script>`;
return html.replace(
"</head>",
`${attachmentThemeToggle} <script data-openclaw-control-ui-mock-storage>\n try {\n localStorage.setItem("openclaw.i18n.locale", "en");\n } catch {}\n </script>\n <script data-openclaw-control-ui-mock-gateway>\n${sameOriginGatewayScript}\n${initScript}\n${statefulInitScript}\n </script>\n${rosterPreferenceScript}\n </head>`,
);
},
};
}
async function buildWorkboardStatesAssets() {
const { build } = await import("esbuild");
const rootDir = path.join(repoRoot, "extensions/workboard");
const entry = path.join(rootDir, "browser/index.ts");
let transformed = false;
const result = await build({
absWorkingDir: rootDir,
entryPoints: { index: entry },
outdir: path.join(rootDir, "dist/control-ui/fixture"),
platform: "browser",
target: "es2022",
format: "esm",
bundle: true,
write: false,
minify: true,
legalComments: "none",
sourcemap: false,
tsconfigRaw: {
compilerOptions: { experimentalDecorators: true, useDefineForClassFields: false },
},
alias: buildPluginLoaderAliasMap(entry, process.argv[1], import.meta.url, "src"),
plugins: [
{
name: "workboard-state-projections",
setup(builder) {
builder.onLoad(
{ filter: /[/\\]pages[/\\]workboard[/\\]view-card\.ts$/ },
async ({ path: sourcePath }) => {
const source = await fs.promises.readFile(sourcePath, "utf8");
const lifecycleImport =
/\bgetWorkboardLifecycle,(?=[\s\S]*?from "\.\.\/\.\.\/lib\/workboard\/index\.ts")/g;
const alertImport =
/\bgetCardAlerts,(?=[\s\S]*?from "\.\.\/\.\.\/lib\/workboard\/card-alerts\.ts")/g;
if (
[...source.matchAll(lifecycleImport)].length !== 1 ||
[...source.matchAll(alertImport)].length !== 1
) {
throw new Error(
"Workboard state fixture import seam changed; update its exact import transform.",
);
}
transformed = true;
return {
loader: "ts",
resolveDir: path.dirname(sourcePath),
contents:
source.replace(lifecycleImport, "").replace(alertImport, "") +
`\nimport { getFixtureLifecycle as getWorkboardLifecycle, getFixtureAlerts as getCardAlerts } from ${JSON.stringify(fileURLToPath(new URL("./control-ui-workboard-state-projections.ts", import.meta.url)))};\n`,
};
},
);
},
},
],
});
if (!transformed) {
throw new Error("Workboard state fixture did not reach the real card renderer.");
}
const revision = createHash("sha256");
for (const file of result.outputFiles) {
revision.update(file.contents);
}
const hash = revision.digest("hex");
const prefix = `${controlUiPluginAssetPrefix("workboard")}${hash}/`;
const assets = new Map(
result.outputFiles.map((file) => [
`${prefix}${path.basename(file.path)}`,
{
body: Buffer.from(file.contents),
contentType: file.path.endsWith(".css") ? "text/css" : "text/javascript",
},
]),
);
return {
assets,
catalog: {
revision: hash,
diagnostics: [],
plugins: [
{
pluginId: "workboard",
name: "workboard",
revision: hash,
entryUrl: `${prefix}index.js`,
styles: [`${prefix}index.css`],
},
],
},
};
}
function createBoardFixturePlugin(): Plugin {
return {
name: "openclaw-control-ui-board-fixture",
configureServer(server) {
server.middlewares.use(boardFixturePath, (_req, res, next) => {
void server
.transformIndexHtml(boardFixturePath, boardFixtureHtml)
.then((html) => {
res.statusCode = 200;
res.setHeader("content-type", "text/html; charset=utf-8");
res.end(html);
})
.catch((error: unknown) => {
next(error as Error);
});
});
},
};
}
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,
pathname = "/chat",
): 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}${pathname}`;
}
async function waitForShutdown(): Promise<void> {
await new Promise<void>((resolve) => {
process.once("SIGINT", resolve);
process.once("SIGTERM", resolve);
});
}
const options = parseArgs(process.argv.slice(2));
const scenario = await createChatPickerScenario(options.fixture);
if (options.operatorScopes) {
scenario.operatorScopes = options.operatorScopes;
}
// Vite replaces its deps cache when fixture plugins or mode change. Concurrent
// mocks need separate owners so one startup cannot invalidate another's modules.
const cacheRoot = path.join(repoRoot, ".artifacts", "control-ui-mock-vite");
await mkdir(cacheRoot, { recursive: true });
const cacheDir = await mkdtemp(path.join(cacheRoot, "server-"));
// Vite's SIGTERM handler calls process.exit() after closing, bypassing finally.
// The process owns this cache, so its exit hook is the single cleanup path.
process.once("exit", () => rmSync(cacheDir, { recursive: true, force: true }));
let server: ViteDevServer | undefined;
try {
server = await createServer({
base: "/",
cacheDir,
clearScreen: false,
configFile: path.join(uiRoot, "vite.config.ts"),
define: {
"globalThis.OPENCLAW_CONTROL_UI_BUILD_INFO": JSON.stringify({
version: "2026.7.10",
commit: "0123456789abcdef0123456789abcdef01234567",
commitAt: "2026-07-10T11:22:33.000Z",
builtAt: "2026-07-10T12:34:56.000Z",
branch: null,
dirty: null,
release: false,
buildId: scenario.serverBuildId,
}),
},
logLevel: "error",
optimizeDeps: {
...(options.fixture === "board"
? { entries: [path.join(uiRoot, "src", "test-helpers", "board-fixture.ts")] }
: {}),
include: ["diff", "lit/directives/repeat.js"],
},
plugins: [
...createStandaloneMockIsolationPlugins(),
await createMockGatewayPlugin(scenario, options.fixture),
createBoardFixturePlugin(),
...(options.fixture === "attachments" ? [createChatAttachmentFixturePlugin()] : []),
],
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, controlUiSessionPath(scenario.sessionKey ?? "agent:main:main"))}`,
);
console.log(
`[control-ui-mock] board fixture: ${resolveServerUrl(server, options.host, boardFixturePath)}`,
);
await waitForShutdown();
} finally {
await server?.close();
}