Some checks failed
ClawSweeper Dispatch / dispatch (push) Has been cancelled
CodeQL / Security High (actions) (push) Has been cancelled
CodeQL / Security High (channel-runtime-boundary) (push) Has been cancelled
CodeQL / Security High (core-auth-secrets) (push) Has been cancelled
CodeQL / Security High (mcp-process-tool-boundary) (push) Has been cancelled
CodeQL / Security High (network-ssrf-boundary) (push) Has been cancelled
CodeQL / Security High (plugin-trust-boundary) (push) Has been cancelled
CodeQL / Security High (process-exec-boundary) (push) Has been cancelled
Docs Sync Publish Repo / sync-publish-repo (push) Has been cancelled
Docs / docs (push) Has been cancelled
OpenClaw Stable Main Closeout / Resolve stable release closeout inputs (push) Has been cancelled
OpenClaw Stable Main Closeout / Verify stable main closeout (push) Has been cancelled
Workflow Sanity / no-tabs (push) Has been cancelled
Workflow Sanity / actionlint (push) Has been cancelled
Workflow Sanity / generated-doc-baselines (push) Has been cancelled
CI / runner-admission (push) Has been cancelled
CI / preflight (push) Has been cancelled
CI / security-fast (push) Has been cancelled
CI / pnpm-store-warmup (push) Has been cancelled
CI / build-artifacts (push) Has been cancelled
CI / native-i18n (push) Has been cancelled
CI / ${{ matrix.check_name }} (push) Has been cancelled
CI / ${{ matrix.checkName }} (push) Has been cancelled
CI / checks-node-compat-node22 (push) Has been cancelled
CI / check-bundled-channel-config-metadata (push) Has been cancelled
CI / check-dependencies (push) Has been cancelled
CI / check-guards (push) Has been cancelled
CI / check-lint (push) Has been cancelled
CI / check-prod-types (push) Has been cancelled
CI / check-shrinkwrap (push) Has been cancelled
CI / check-test-types (push) Has been cancelled
CI / check-additional-boundaries-a (push) Has been cancelled
CI / check-additional-boundaries-bcd (push) Has been cancelled
CI / check-additional-extension-bundled (push) Has been cancelled
CI / check-additional-extension-channels (push) Has been cancelled
CI / check-additional-extension-package-boundary (push) Has been cancelled
CI / check-additional-runtime-topology-architecture (push) Has been cancelled
CI / check-session-accessor-boundary (push) Has been cancelled
CI / check-session-transcript-reader-boundary (push) Has been cancelled
CI / check-docs (push) Has been cancelled
CI / skills-python (push) Has been cancelled
CI / macos-swift (push) Has been cancelled
CI / ios-build (push) Has been cancelled
CI / ci-timings-summary (push) Has been cancelled
Native App Locale Refresh / Refresh native fa (push) Has been cancelled
Native App Locale Refresh / Refresh native fr (push) Has been cancelled
Native App Locale Refresh / Refresh native hi (push) Has been cancelled
Native App Locale Refresh / Refresh native id (push) Has been cancelled
Native App Locale Refresh / Refresh native it (push) Has been cancelled
Native App Locale Refresh / Refresh native ja-JP (push) Has been cancelled
Control UI Locale Refresh / plan (push) Has been cancelled
Control UI Locale Refresh / Refresh ${{ matrix.locale }} (push) Has been cancelled
Control UI Locale Refresh / Commit control UI locale refresh (push) Has been cancelled
Live Media Runner Image / Build live media runner image (push) Has been cancelled
Native App Locale Refresh / Refresh native ar (push) Has been cancelled
Native App Locale Refresh / Refresh native de (push) Has been cancelled
Native App Locale Refresh / Refresh native es (push) Has been cancelled
Native App Locale Refresh / Refresh native ko (push) Has been cancelled
Native App Locale Refresh / Refresh native nl (push) Has been cancelled
Native App Locale Refresh / Refresh native pl (push) Has been cancelled
Native App Locale Refresh / Refresh native pt-BR (push) Has been cancelled
Native App Locale Refresh / Refresh native ru (push) Has been cancelled
Native App Locale Refresh / Refresh native sv (push) Has been cancelled
Native App Locale Refresh / Refresh native th (push) Has been cancelled
Native App Locale Refresh / Refresh native tr (push) Has been cancelled
Native App Locale Refresh / Refresh native uk (push) Has been cancelled
Native App Locale Refresh / Refresh native vi (push) Has been cancelled
Native App Locale Refresh / Refresh native zh-CN (push) Has been cancelled
Native App Locale Refresh / Refresh native zh-TW (push) Has been cancelled
Native App Locale Refresh / Commit native locale refresh (push) Has been cancelled
Plugin Init Scaffold Validation / Validate provider scaffold (push) Has been cancelled
Plugin NPM Release / preview_plugins_npm (push) Has been cancelled
Plugin NPM Release / Validate release publish approval (push) Has been cancelled
Plugin NPM Release / preview_plugin_pack (push) Has been cancelled
Plugin NPM Release / publish_plugins_npm (push) Has been cancelled
Sandbox Common Smoke / sandbox-common-smoke (push) Has been cancelled
Website Installer Sync / static (push) Has been cancelled
Website Installer Sync / linux-docker (push) Has been cancelled
Website Installer Sync / macos-installer (push) Has been cancelled
Website Installer Sync / windows-installer (push) Has been cancelled
Website Installer Sync / sync-website (push) Has been cancelled
Adolf is a fork/vendored clone of github.com/openclaw/openclaw (v2026.6.11), free to diverge. Tree copied sans upstream .git; upstream remote added for future syncs. Node pinned to 24 (.nvmrc); engines already require >=22.19. Preserves docs/ARCHITECTURE.md. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01LeqyaxJF2nbRXJtae2kNB2
631 lines
19 KiB
TypeScript
631 lines
19 KiB
TypeScript
import type { RouteLocation } from "@openclaw/uirouter";
|
|
import type { EventLogEntry } from "../api/event-log.ts";
|
|
import {
|
|
GatewayBrowserClient,
|
|
type GatewayEventListener,
|
|
type GatewayHelloOk,
|
|
} from "../api/gateway.ts";
|
|
import {
|
|
createApplicationRouter,
|
|
inferBasePathFromPathname,
|
|
locationForRoute,
|
|
normalizeBasePath,
|
|
pathForRoute,
|
|
routeIdFromPath,
|
|
startApplicationRouter,
|
|
type ApplicationRouter,
|
|
type RouteId,
|
|
} from "../app-routes.ts";
|
|
import { createAgentIdentityCapability } from "../lib/agents/identity.ts";
|
|
import { createAgentCapability } from "../lib/agents/index.ts";
|
|
import { createChannelCapability } from "../lib/channels/index.ts";
|
|
import { createRuntimeConfigCapability } from "../lib/config/index.ts";
|
|
import { createSessionCapability, resolveSessionKey } from "../lib/sessions/index.ts";
|
|
import { generateUUID } from "../lib/uuid.ts";
|
|
import { createWorkboardCapability } from "../lib/workboard/capability.ts";
|
|
import { createAgentSelectionCapability } from "./agent-selection.ts";
|
|
import { createBrowserHistory } from "./browser.ts";
|
|
import { createApplicationConfigCapability } from "./config.ts";
|
|
import type {
|
|
ApplicationGateway,
|
|
ApplicationGatewayConnectOptions,
|
|
ApplicationGatewayConnection,
|
|
ApplicationNavigationOptions,
|
|
ApplicationGatewaySnapshot,
|
|
ApplicationContext,
|
|
ApplicationNavigationPreferences,
|
|
ApplicationNavigationPreferencesSnapshot,
|
|
ApplicationSkillWorkshopRevisionHandoff,
|
|
ApplicationTheme,
|
|
} from "./context.ts";
|
|
import { syncCustomThemeStyleTag } from "./custom-theme.ts";
|
|
import { createNativeChatDrafts } from "./native-bridge.ts";
|
|
import { createApplicationOverlays } from "./overlays.ts";
|
|
import {
|
|
loadSettings,
|
|
patchSettings,
|
|
resolveApplicationStartupSettings,
|
|
saveSettings,
|
|
type UiSettings,
|
|
} from "./settings.ts";
|
|
import { startThemeTransition } from "./theme-transition.ts";
|
|
import { resolveTheme, type ThemeMode } from "./theme.ts";
|
|
import { createWebPushCapability } from "./web-push.ts";
|
|
|
|
function normalizeInitialApplicationLocation(
|
|
location: RouteLocation,
|
|
basePath: string,
|
|
sessionKey: string,
|
|
) {
|
|
const routeId = routeIdFromPath(location.pathname, basePath);
|
|
if ((routeId !== null && routeId !== "chat") || !sessionKey.trim()) {
|
|
return location;
|
|
}
|
|
|
|
const search = new URLSearchParams(location.search);
|
|
if (!search.get("session")?.trim()) {
|
|
search.set("session", sessionKey);
|
|
}
|
|
return {
|
|
...location,
|
|
pathname: routeId === null ? pathForRoute("chat", basePath) : location.pathname,
|
|
search: `?${search.toString()}`,
|
|
};
|
|
}
|
|
|
|
function applyStartupPresentation(settings: ReturnType<typeof loadSettings>): void {
|
|
if (typeof document === "undefined") {
|
|
return;
|
|
}
|
|
const root = document.documentElement;
|
|
const resolvedTheme = resolveTheme(settings.theme, settings.themeMode);
|
|
root.dataset.theme = resolvedTheme;
|
|
root.dataset.themeMode = resolvedTheme.endsWith("light") ? "light" : "dark";
|
|
root.style.colorScheme = root.dataset.themeMode;
|
|
root.style.setProperty("--control-ui-text-scale", `${(settings.textScale ?? 100) / 100}`);
|
|
syncCustomThemeStyleTag(settings.customTheme);
|
|
}
|
|
|
|
function createApplicationTheme(
|
|
initialSettings: UiSettings,
|
|
): ApplicationTheme & { dispose: () => void } {
|
|
let settings = initialSettings;
|
|
let systemThemeCleanup: (() => void) | undefined;
|
|
const listeners = new Set<() => void>();
|
|
|
|
const publish = () => {
|
|
applyStartupPresentation(settings);
|
|
for (const listener of listeners) {
|
|
listener();
|
|
}
|
|
};
|
|
|
|
const detachSystemThemeListener = () => {
|
|
systemThemeCleanup?.();
|
|
systemThemeCleanup = undefined;
|
|
};
|
|
|
|
const syncSystemThemeListener = () => {
|
|
detachSystemThemeListener();
|
|
if (settings.themeMode !== "system" || typeof globalThis.matchMedia !== "function") {
|
|
return;
|
|
}
|
|
const mediaQuery = globalThis.matchMedia("(prefers-color-scheme: light)");
|
|
const onChange = () => {
|
|
if (settings.themeMode === "system") {
|
|
publish();
|
|
}
|
|
};
|
|
if (typeof mediaQuery.addEventListener === "function") {
|
|
mediaQuery.addEventListener("change", onChange);
|
|
systemThemeCleanup = () => mediaQuery.removeEventListener("change", onChange);
|
|
} else if (typeof mediaQuery.addListener === "function") {
|
|
mediaQuery.addListener(onChange);
|
|
systemThemeCleanup = () => mediaQuery.removeListener(onChange);
|
|
}
|
|
};
|
|
|
|
syncSystemThemeListener();
|
|
|
|
return {
|
|
get mode() {
|
|
return settings.themeMode;
|
|
},
|
|
setMode(mode: ThemeMode, element) {
|
|
const currentSettings = loadSettings();
|
|
const nextSettings = { ...currentSettings, themeMode: mode };
|
|
const currentTheme = resolveTheme(currentSettings.theme, currentSettings.themeMode);
|
|
const nextTheme = resolveTheme(nextSettings.theme, nextSettings.themeMode);
|
|
startThemeTransition({
|
|
nextTheme,
|
|
currentTheme,
|
|
context: { element },
|
|
applyTheme: () => {
|
|
settings = patchSettings({ themeMode: mode });
|
|
publish();
|
|
syncSystemThemeListener();
|
|
},
|
|
});
|
|
},
|
|
refresh() {
|
|
settings = loadSettings();
|
|
publish();
|
|
syncSystemThemeListener();
|
|
},
|
|
subscribe(listener) {
|
|
listeners.add(listener);
|
|
return () => listeners.delete(listener);
|
|
},
|
|
dispose() {
|
|
detachSystemThemeListener();
|
|
listeners.clear();
|
|
},
|
|
};
|
|
}
|
|
|
|
function createApplicationNavigationPreferences(
|
|
initialSettings: UiSettings,
|
|
): ApplicationNavigationPreferences {
|
|
let settings = initialSettings;
|
|
let snapshot: ApplicationNavigationPreferencesSnapshot = {
|
|
navCollapsed: settings.navCollapsed,
|
|
navGroupsCollapsed: settings.navGroupsCollapsed,
|
|
recentSessionsCollapsed: settings.recentSessionsCollapsed ?? false,
|
|
};
|
|
const listeners = new Set<(next: ApplicationNavigationPreferencesSnapshot) => void>();
|
|
|
|
return {
|
|
get snapshot() {
|
|
return snapshot;
|
|
},
|
|
update(patch) {
|
|
const nextSnapshot = { ...snapshot, ...patch };
|
|
if (
|
|
nextSnapshot.navCollapsed === snapshot.navCollapsed &&
|
|
nextSnapshot.recentSessionsCollapsed === snapshot.recentSessionsCollapsed &&
|
|
nextSnapshot.navGroupsCollapsed === snapshot.navGroupsCollapsed
|
|
) {
|
|
return;
|
|
}
|
|
settings = patchSettings({
|
|
navCollapsed: nextSnapshot.navCollapsed,
|
|
navGroupsCollapsed: nextSnapshot.navGroupsCollapsed,
|
|
recentSessionsCollapsed: nextSnapshot.recentSessionsCollapsed,
|
|
});
|
|
snapshot = nextSnapshot;
|
|
for (const listener of listeners) {
|
|
listener(snapshot);
|
|
}
|
|
},
|
|
subscribe(listener) {
|
|
listeners.add(listener);
|
|
return () => listeners.delete(listener);
|
|
},
|
|
};
|
|
}
|
|
|
|
function createSkillWorkshopRevisionHandoff(): ApplicationSkillWorkshopRevisionHandoff {
|
|
let pending: Parameters<ApplicationSkillWorkshopRevisionHandoff["prepare"]>[0] | null = null;
|
|
return {
|
|
prepare: (handoff) => {
|
|
pending = handoff;
|
|
},
|
|
consume: (sessionKey) => {
|
|
if (!pending || pending.sessionKey !== sessionKey) {
|
|
return null;
|
|
}
|
|
const handoff = pending;
|
|
pending = null;
|
|
return handoff;
|
|
},
|
|
clear: () => {
|
|
pending = null;
|
|
},
|
|
};
|
|
}
|
|
|
|
function createApplicationGateway(
|
|
initialSettings: ReturnType<typeof loadSettings>,
|
|
initialPassword = "",
|
|
): ApplicationGateway {
|
|
let settings = initialSettings;
|
|
let connection: ApplicationGatewayConnection = {
|
|
gatewayUrl: settings.gatewayUrl,
|
|
token: settings.token,
|
|
password: initialPassword,
|
|
};
|
|
let snapshot: ApplicationGatewaySnapshot = {
|
|
client: null,
|
|
connected: false,
|
|
hello: null,
|
|
assistantAgentId: "main",
|
|
sessionKey: settings.sessionKey,
|
|
lastError: null,
|
|
lastErrorCode: null,
|
|
};
|
|
let client: GatewayBrowserClient | null = null;
|
|
const listeners = new Set<(next: ApplicationGatewaySnapshot) => void>();
|
|
const eventListeners = new Set<GatewayEventListener>();
|
|
const eventLogListeners = new Set<(events: readonly EventLogEntry[]) => void>();
|
|
let eventLog: EventLogEntry[] = [];
|
|
let stopClientEvents: (() => void) | undefined;
|
|
const syncClientEvents = (nextClient: GatewayBrowserClient | null) => {
|
|
stopClientEvents?.();
|
|
stopClientEvents = undefined;
|
|
if (!nextClient || eventListeners.size === 0) {
|
|
return;
|
|
}
|
|
const removers = [...eventListeners].map((listener) => nextClient.addEventListener(listener));
|
|
stopClientEvents = () => {
|
|
for (const remove of removers) {
|
|
remove();
|
|
}
|
|
};
|
|
};
|
|
const notify = () => {
|
|
for (const listener of listeners) {
|
|
listener(snapshot);
|
|
}
|
|
};
|
|
const setSnapshot = (next: ApplicationGatewaySnapshot) => {
|
|
snapshot = next;
|
|
notify();
|
|
};
|
|
const publishEventLog = () => {
|
|
for (const listener of eventLogListeners) {
|
|
listener(eventLog);
|
|
}
|
|
};
|
|
const recordGatewayEvent = (event: Parameters<GatewayEventListener>[0]) => {
|
|
eventLog = [{ ts: Date.now(), event: event.event, payload: event.payload }, ...eventLog].slice(
|
|
0,
|
|
250,
|
|
);
|
|
publishEventLog();
|
|
};
|
|
|
|
const connect = (overrides: ApplicationGatewayConnectOptions = {}) => {
|
|
const { sessionKey: requestedSessionKey, ...connectionOverrides } = overrides;
|
|
const nextConnection = { ...connection, ...connectionOverrides };
|
|
const hasRequestedSessionKey = requestedSessionKey !== undefined;
|
|
const nextSessionKey = hasRequestedSessionKey
|
|
? requestedSessionKey.trim()
|
|
: snapshot.sessionKey;
|
|
connection = nextConnection;
|
|
settings = patchSettings({
|
|
gatewayUrl: nextConnection.gatewayUrl,
|
|
token: nextConnection.token,
|
|
...(hasRequestedSessionKey
|
|
? {
|
|
sessionKey: nextSessionKey,
|
|
lastActiveSessionKey: nextSessionKey,
|
|
}
|
|
: {}),
|
|
});
|
|
client?.stop();
|
|
stopClientEvents?.();
|
|
stopClientEvents = undefined;
|
|
|
|
const nextClient = new GatewayBrowserClient({
|
|
url: nextConnection.gatewayUrl,
|
|
token: nextConnection.token.trim() ? nextConnection.token : undefined,
|
|
password: nextConnection.password.trim() ? nextConnection.password : undefined,
|
|
clientName: "openclaw-control-ui",
|
|
clientVersion: "dev",
|
|
mode: "webchat",
|
|
instanceId: generateUUID(),
|
|
onHello: (hello: GatewayHelloOk) => {
|
|
if (client !== nextClient) {
|
|
return;
|
|
}
|
|
settings = loadSettings();
|
|
const sessionDefaults = readSessionDefaults(hello);
|
|
const sessionKey = resolveSessionKey(snapshot.sessionKey, hello);
|
|
const lastActiveSessionKey = resolveSessionKey(settings.lastActiveSessionKey, hello);
|
|
if (
|
|
sessionKey !== settings.sessionKey ||
|
|
lastActiveSessionKey !== settings.lastActiveSessionKey
|
|
) {
|
|
settings = patchSettings({
|
|
sessionKey,
|
|
lastActiveSessionKey,
|
|
});
|
|
}
|
|
setSnapshot({
|
|
...snapshot,
|
|
client: nextClient,
|
|
connected: true,
|
|
hello,
|
|
assistantAgentId: sessionDefaults?.defaultAgentId ?? "main",
|
|
sessionKey,
|
|
lastError: null,
|
|
lastErrorCode: null,
|
|
});
|
|
},
|
|
onClose: ({ code, reason, error }) => {
|
|
if (client !== nextClient) {
|
|
return;
|
|
}
|
|
setSnapshot({
|
|
...snapshot,
|
|
client: nextClient,
|
|
connected: false,
|
|
hello: null,
|
|
lastError: error?.message ?? `disconnected (${code}): ${reason || "no reason"}`,
|
|
lastErrorCode: error?.code ?? null,
|
|
});
|
|
},
|
|
onGap: ({ expected, received }) => {
|
|
if (client !== nextClient) {
|
|
return;
|
|
}
|
|
setSnapshot({
|
|
...snapshot,
|
|
lastError: `event gap detected (expected seq ${expected}, got ${received}); reconnecting`,
|
|
lastErrorCode: null,
|
|
});
|
|
connect();
|
|
},
|
|
onEvent: recordGatewayEvent,
|
|
});
|
|
client = nextClient;
|
|
syncClientEvents(nextClient);
|
|
setSnapshot({
|
|
...snapshot,
|
|
client: nextClient,
|
|
connected: false,
|
|
hello: null,
|
|
sessionKey: nextSessionKey,
|
|
lastError: null,
|
|
lastErrorCode: null,
|
|
});
|
|
nextClient.start();
|
|
};
|
|
|
|
const gateway: ApplicationGateway = {
|
|
get snapshot() {
|
|
return snapshot;
|
|
},
|
|
get connection() {
|
|
return connection;
|
|
},
|
|
get eventLog() {
|
|
return eventLog;
|
|
},
|
|
connect,
|
|
setSessionKey: (sessionKey) => {
|
|
const nextSessionKey = sessionKey.trim();
|
|
if (!nextSessionKey || nextSessionKey === snapshot.sessionKey) {
|
|
return;
|
|
}
|
|
settings = patchSettings({
|
|
sessionKey: nextSessionKey,
|
|
lastActiveSessionKey: nextSessionKey,
|
|
});
|
|
setSnapshot({ ...snapshot, sessionKey: nextSessionKey });
|
|
},
|
|
start: () => connect(),
|
|
stop: () => {
|
|
stopClientEvents?.();
|
|
stopClientEvents = undefined;
|
|
client?.stop();
|
|
client = null;
|
|
setSnapshot({
|
|
...snapshot,
|
|
client: null,
|
|
connected: false,
|
|
hello: null,
|
|
lastError: null,
|
|
lastErrorCode: null,
|
|
});
|
|
},
|
|
subscribe: (listener) => {
|
|
listeners.add(listener);
|
|
return () => listeners.delete(listener);
|
|
},
|
|
subscribeEventLog: (listener) => {
|
|
eventLogListeners.add(listener);
|
|
return () => eventLogListeners.delete(listener);
|
|
},
|
|
subscribeEvents: (listener) => {
|
|
eventListeners.add(listener);
|
|
syncClientEvents(client);
|
|
return () => {
|
|
if (eventListeners.delete(listener)) {
|
|
syncClientEvents(client);
|
|
}
|
|
};
|
|
},
|
|
};
|
|
return gateway;
|
|
}
|
|
|
|
function readSessionDefaults(
|
|
hello: GatewayHelloOk,
|
|
): { defaultAgentId?: string | null } | undefined {
|
|
const snapshot = hello.snapshot;
|
|
if (!snapshot || typeof snapshot !== "object" || !("sessionDefaults" in snapshot)) {
|
|
return undefined;
|
|
}
|
|
const defaults = snapshot.sessionDefaults;
|
|
return defaults && typeof defaults === "object"
|
|
? (defaults as { defaultAgentId?: string | null })
|
|
: undefined;
|
|
}
|
|
|
|
export type ApplicationRuntime = {
|
|
readonly context: ApplicationContext<RouteId>;
|
|
readonly router: ApplicationRouter;
|
|
readonly pendingGatewayConnection: {
|
|
readonly gatewayUrl: string;
|
|
readonly token: string;
|
|
} | null;
|
|
readonly confirmPendingGatewayConnection: () => void;
|
|
readonly cancelPendingGatewayConnection: () => void;
|
|
start: () => Promise<void>;
|
|
stop: () => void;
|
|
};
|
|
|
|
export function bootstrapApplication(): ApplicationRuntime {
|
|
const initialSettings = loadSettings();
|
|
const history = createBrowserHistory();
|
|
const startup = resolveApplicationStartupSettings(initialSettings, history.location());
|
|
if (startup.changed) {
|
|
saveSettings(startup.settings);
|
|
}
|
|
const basePath = normalizeBasePath(
|
|
inferBasePathFromPathname(startup.location.pathname || globalThis.location?.pathname || "/"),
|
|
);
|
|
const initialLocation = normalizeInitialApplicationLocation(
|
|
startup.location,
|
|
basePath,
|
|
startup.settings.sessionKey,
|
|
);
|
|
const currentLocation = history.location();
|
|
if (
|
|
currentLocation.pathname !== initialLocation.pathname ||
|
|
currentLocation.search !== initialLocation.search ||
|
|
currentLocation.hash !== initialLocation.hash
|
|
) {
|
|
history.replace(initialLocation);
|
|
}
|
|
|
|
const settings = startup.settings;
|
|
const gateway = createApplicationGateway(settings, startup.password ?? "");
|
|
const agents = createAgentCapability(gateway);
|
|
const agentIdentity = createAgentIdentityCapability(gateway);
|
|
const agentSelection = createAgentSelectionCapability(gateway);
|
|
const channels = createChannelCapability(gateway);
|
|
const config = createApplicationConfigCapability({
|
|
basePath,
|
|
auth: {
|
|
settings: { token: settings.token },
|
|
password: startup.password ?? "",
|
|
},
|
|
});
|
|
const sessions = createSessionCapability(gateway);
|
|
const workboard = createWorkboardCapability();
|
|
const runtimeConfig = createRuntimeConfigCapability(gateway);
|
|
const overlays = createApplicationOverlays(gateway);
|
|
const navigation = createApplicationNavigationPreferences(settings);
|
|
const theme = createApplicationTheme(settings);
|
|
const nativeChatDrafts = createNativeChatDrafts();
|
|
const webPush = createWebPushCapability(gateway);
|
|
const skillWorkshopRevision = createSkillWorkshopRevisionHandoff();
|
|
applyStartupPresentation(settings);
|
|
const router = createApplicationRouter();
|
|
let pendingGatewayConnection =
|
|
startup.pendingGatewayUrl !== null
|
|
? {
|
|
gatewayUrl: startup.pendingGatewayUrl,
|
|
token: startup.pendingGatewayToken ?? "",
|
|
}
|
|
: null;
|
|
let lastConfigRefreshClient: GatewayBrowserClient | null = null;
|
|
const stopConfigRefresh = gateway.subscribe((snapshot) => {
|
|
if (!snapshot.connected || !snapshot.client) {
|
|
lastConfigRefreshClient = null;
|
|
return;
|
|
}
|
|
if (lastConfigRefreshClient === snapshot.client) {
|
|
return;
|
|
}
|
|
lastConfigRefreshClient = snapshot.client;
|
|
void config.refresh({
|
|
auth: {
|
|
hello: snapshot.hello,
|
|
settings: { token: gateway.connection.token },
|
|
password: gateway.connection.password,
|
|
},
|
|
});
|
|
});
|
|
const routeLocation = (routeId: RouteId, options?: ApplicationNavigationOptions) => {
|
|
const location = locationForRoute(routeId, basePath);
|
|
if (options?.search !== undefined || options?.hash !== undefined) {
|
|
return {
|
|
...location,
|
|
search: options?.search ?? "",
|
|
hash: options?.hash ?? "",
|
|
};
|
|
}
|
|
return location;
|
|
};
|
|
const confirmPendingGatewayConnection = () => {
|
|
const pending = pendingGatewayConnection;
|
|
if (!pending) {
|
|
return;
|
|
}
|
|
pendingGatewayConnection = null;
|
|
gateway.connect({
|
|
gatewayUrl: pending.gatewayUrl,
|
|
token: pending.token,
|
|
});
|
|
};
|
|
const cancelPendingGatewayConnection = () => {
|
|
pendingGatewayConnection = null;
|
|
};
|
|
const context: ApplicationContext<RouteId> = {
|
|
basePath,
|
|
gateway,
|
|
agents,
|
|
agentIdentity,
|
|
agentSelection,
|
|
channels,
|
|
config,
|
|
runtimeConfig,
|
|
sessions,
|
|
workboard,
|
|
overlays,
|
|
navigation,
|
|
theme,
|
|
nativeChatDrafts,
|
|
webPush,
|
|
skillWorkshopRevision,
|
|
navigate: (routeId, options) => {
|
|
void router
|
|
.navigate(routeId, context, { history: "push" }, routeLocation(routeId, options))
|
|
.catch((error: unknown) => {
|
|
console.error("[openclaw] route navigation failed", error);
|
|
});
|
|
},
|
|
replace: (routeId, options) => {
|
|
void router
|
|
.navigate(routeId, context, { history: "replace" }, routeLocation(routeId, options))
|
|
.catch((error: unknown) => {
|
|
console.error("[openclaw] route replacement failed", error);
|
|
});
|
|
},
|
|
preload: (routeId) => router.preloadRoute(routeId, context),
|
|
};
|
|
return {
|
|
context,
|
|
router,
|
|
get pendingGatewayConnection() {
|
|
return pendingGatewayConnection;
|
|
},
|
|
confirmPendingGatewayConnection,
|
|
cancelPendingGatewayConnection,
|
|
start: async () => {
|
|
void config.refresh({ skipWithoutAuthCandidate: true });
|
|
const routerStart = startApplicationRouter(router, history, basePath, context);
|
|
gateway.start();
|
|
await routerStart;
|
|
},
|
|
stop: () => {
|
|
stopConfigRefresh();
|
|
router.stop();
|
|
gateway.stop();
|
|
agents.dispose();
|
|
channels.dispose();
|
|
sessions.dispose();
|
|
workboard.dispose();
|
|
runtimeConfig.dispose();
|
|
overlays.dispose();
|
|
theme.dispose();
|
|
nativeChatDrafts.dispose();
|
|
webPush.dispose();
|
|
skillWorkshopRevision.clear();
|
|
},
|
|
};
|
|
}
|