fix(cycles): remove qa-lab and ui runtime seams

This commit is contained in:
Vincent Koc
2026-04-10 11:03:08 +01:00
parent 10b26ed2ec
commit dbe2a97e80
13 changed files with 225 additions and 137 deletions

View File

@@ -4,7 +4,8 @@ import { formatErrorMessage } from "openclaw/plugin-sdk/error-runtime";
import { runQaManualLane } from "./manual-lane.runtime.js";
import { isQaFastModeModelRef, type QaProviderMode } from "./model-selection.js";
import { type QaThinkingLevel } from "./qa-gateway-config.js";
import { runQaSuite, type QaSuiteResult } from "./suite.js";
import { runQaSuiteFromRuntime } from "./suite-launch.runtime.js";
import type { QaSuiteResult } from "./suite.js";
const DEFAULT_CHARACTER_SCENARIO_ID = "character-vibes-gollum";
const DEFAULT_CHARACTER_EVAL_MODELS = Object.freeze([
@@ -518,7 +519,7 @@ export async function runQaCharacterEval(params: QaCharacterEvalParams) {
const runsDir = path.join(outputDir, "runs");
await fs.mkdir(runsDir, { recursive: true });
const runSuite = params.runSuite ?? runQaSuite;
const runSuite = params.runSuite ?? runQaSuiteFromRuntime;
const candidateConcurrency = normalizeConcurrency(
params.candidateConcurrency,
DEFAULT_CHARACTER_EVAL_CONCURRENCY,

View File

@@ -3,7 +3,7 @@ import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
const {
runQaManualLane,
runQaSuite,
runQaSuiteFromRuntime,
runQaCharacterEval,
runQaMultipass,
startQaLabServer,
@@ -12,7 +12,7 @@ const {
runQaDockerUp,
} = vi.hoisted(() => ({
runQaManualLane: vi.fn(),
runQaSuite: vi.fn(),
runQaSuiteFromRuntime: vi.fn(),
runQaCharacterEval: vi.fn(),
runQaMultipass: vi.fn(),
startQaLabServer: vi.fn(),
@@ -25,8 +25,8 @@ vi.mock("./manual-lane.runtime.js", () => ({
runQaManualLane,
}));
vi.mock("./suite.js", () => ({
runQaSuite,
vi.mock("./suite-launch.runtime.js", () => ({
runQaSuiteFromRuntime,
}));
vi.mock("./character-eval.js", () => ({
@@ -65,7 +65,7 @@ describe("qa cli runtime", () => {
beforeEach(() => {
stdoutWrite = vi.spyOn(process.stdout, "write").mockReturnValue(true);
runQaSuite.mockReset();
runQaSuiteFromRuntime.mockReset();
runQaCharacterEval.mockReset();
runQaManualLane.mockReset();
runQaMultipass.mockReset();
@@ -73,7 +73,7 @@ describe("qa cli runtime", () => {
writeQaDockerHarnessFiles.mockReset();
buildQaDockerHarnessImage.mockReset();
runQaDockerUp.mockReset();
runQaSuite.mockResolvedValue({
runQaSuiteFromRuntime.mockResolvedValue({
watchUrl: "http://127.0.0.1:43124",
reportPath: "/tmp/report.md",
summaryPath: "/tmp/summary.json",
@@ -135,7 +135,7 @@ describe("qa cli runtime", () => {
scenarioIds: ["approval-turn-tool-followthrough"],
});
expect(runQaSuite).toHaveBeenCalledWith({
expect(runQaSuiteFromRuntime).toHaveBeenCalledWith({
repoRoot: path.resolve("/tmp/openclaw-repo"),
outputDir: path.resolve("/tmp/openclaw-repo", ".artifacts/qa/frontier"),
providerMode: "live-frontier",
@@ -153,7 +153,7 @@ describe("qa cli runtime", () => {
scenarioIds: ["approval-turn-tool-followthrough"],
});
expect(runQaSuite).toHaveBeenCalledWith(
expect(runQaSuiteFromRuntime).toHaveBeenCalledWith(
expect.objectContaining({
repoRoot: path.resolve("/tmp/openclaw-repo"),
providerMode: "live-frontier",
@@ -310,7 +310,7 @@ describe("qa cli runtime", () => {
memory: "4G",
disk: "24G",
});
expect(runQaSuite).not.toHaveBeenCalled();
expect(runQaSuiteFromRuntime).not.toHaveBeenCalled();
});
it("passes live suite selection through to the multipass runner", async () => {

View File

@@ -13,7 +13,7 @@ import {
type QaProviderMode,
type QaProviderModeInput,
} from "./run-config.js";
import { runQaSuite } from "./suite.js";
import { runQaSuiteFromRuntime } from "./suite-launch.runtime.js";
type InterruptibleServer = {
baseUrl: string;
@@ -241,7 +241,7 @@ export async function runQaSuiteCommand(opts: {
process.stdout.write(`QA Multipass bootstrap log: ${result.bootstrapLogPath}\n`);
return;
}
const result = await runQaSuite({
const result = await runQaSuiteFromRuntime({
repoRoot,
outputDir: opts.outputDir ? path.resolve(repoRoot, opts.outputDir) : undefined,
providerMode,

View File

@@ -659,8 +659,8 @@ export async function startQaLabServer(
};
activeSuiteRun = (async () => {
try {
const { runQaSuiteFromRuntime } = await import("./suite-launch.runtime.js");
const result = await runQaSuiteFromRuntime({
const { runQaSuite } = await import("./suite.js");
const result = await runQaSuite({
lab: labHandle ?? undefined,
outputDir: createQaRunOutputDir(repoRoot),
providerMode: selection.providerMode,

View File

@@ -1,6 +1,15 @@
export async function runQaSuiteFromRuntime(
...args: Parameters<typeof import("./suite.js").runQaSuite>
) {
const { runQaSuite } = await import("./suite.js");
return await runQaSuite(...args);
import type { QaSuiteRunParams } from "./suite.js";
async function loadQaLabServerRuntime() {
const { startQaLabServer } = await import("./lab-server.js");
return startQaLabServer;
}
export async function runQaSuiteFromRuntime(...args: [QaSuiteRunParams?]) {
const { runQaSuite } = await import("./suite.js");
const params = args[0];
return await runQaSuite({
...params,
startLab: params?.startLab ?? (await loadQaLabServerRuntime()),
});
}

View File

@@ -68,12 +68,20 @@ type QaSuiteEnvironment = {
alternateModel: string;
};
async function startQaLabServerRuntime(
params?: QaLabServerStartParams,
): Promise<QaLabServerHandle> {
const { startQaLabServer } = await import("./lab-server.js");
return await startQaLabServer(params);
}
export type QaSuiteStartLabFn = (params?: QaLabServerStartParams) => Promise<QaLabServerHandle>;
export type QaSuiteRunParams = {
repoRoot?: string;
outputDir?: string;
providerMode?: QaProviderMode | "live-openai";
primaryModel?: string;
alternateModel?: string;
fastMode?: boolean;
thinkingDefault?: QaThinkingLevel;
scenarioIds?: string[];
lab?: QaLabServerHandle;
startLab?: QaSuiteStartLabFn;
};
const _QA_IMAGE_UNDERSTANDING_PNG_BASE64 =
"iVBORw0KGgoAAAANSUhEUgAAAQAAAAEACAYAAABccqhmAAAAAklEQVR4AewaftIAAAK4SURBVO3BAQEAMAwCIG//znsQgXfJBZjUALMaYFYDzGqAWQ0wqwFmNcCsBpjVALMaYFYDzGqAWQ0wqwFmNcCsBpjVALMaYFYDzGqAWQ0wqwFmNcCsBpjVALMaYFYDzGqAWQ0wqwFmNcCsBpjVALMaYFYDzGqAWQ0wqwFmNcCsBpjVALMaYFYDzGqAWQ0wqwFmNcCsBpjVALMaYFYDzGqAWQ0wqwFmNcCsBpjVALMaYFYDzGqAWQ0wqwFmNcCsBpjVALMaYFYDzGqAWQ0wqwFmNcCsBpjVALMaYFYDzGqAWQ0wqwFmNcCsBpjVALMaYFYDzGqAWQ0wqwFmNcCsBpjVALMaYFYDzGqAWQ0wqwFmNcCsBpjVALMaYFYDzGqAWQ0wqwFmNcCsBpjVALMaYFYDzGqAWQ0wqwFmNcCsBpjVALMaYFYDzGqAWQ0wqwFmNcCsBpjVALMaYFYDzGqAWQ0wqwFmNcCsBpjVALMaYFYDzGqAWQ0wqwFmNcCsl9wFmNQAsxpgVgPMaoBZDTCrAWY1wKwGmNUAsxpgVgPMaoBZDTCrAWY1wKwGmNUAsxpgVgPMaoBZDTCrAWY1wKwGmNUAsxpgVgPMaoBZDTCrAWY1wKwGmNUAsxpgVgPMaoBZDTCrAWY1wKwGmNUAsxpgVgPMaoBZDTCrAWY1wKwGmNUAsxpgVgPMaoBZDTCrAWY1wKwGmNUAsxpgVgPMaoBZDTCrAWY1wKwGmNUAsxpgVgPMaoBZDTCrAWY1wKwGmNUAsxpgVgPMaoBZDTCrAWY1wKwGmNUAsxpgVgPMaoBZDTCrAWY1wKwGmNUAsxpgVgPMaoBZDTCrAWY1wKwGmNUAsxpgVgPMaoBZDTCrAWY1wKwGmNUAsxpgVgPMaoBZDTCrAWY1wKwGmNUAsxpgVgPMaoBZDTCrAWY1wKwP4TIF+7ciPkoAAAAASUVORK5CYII=";
@@ -1188,17 +1196,7 @@ async function runScenarioDefinition(
});
}
export async function runQaSuite(params?: {
repoRoot?: string;
outputDir?: string;
providerMode?: QaProviderMode | "live-openai";
primaryModel?: string;
alternateModel?: string;
fastMode?: boolean;
thinkingDefault?: QaThinkingLevel;
scenarioIds?: string[];
lab?: QaLabServerHandle;
}) {
export async function runQaSuite(params?: QaSuiteRunParams) {
const startedAt = new Date();
const repoRoot = path.resolve(params?.repoRoot ?? process.cwd());
const providerMode = normalizeQaProviderMode(params?.providerMode ?? "mock-openai");
@@ -1217,12 +1215,15 @@ export async function runQaSuite(params?: {
const ownsLab = !params?.lab;
const lab =
params?.lab ??
(await startQaLabServerRuntime({
(await params?.startLab?.({
repoRoot,
host: "127.0.0.1",
port: 0,
embeddedGateway: "disabled",
}));
if (!lab) {
throw new Error("QA suite requires lab or startLab runtime");
}
const mock =
providerMode === "mock-openai"
? await startQaMockOpenAiServer({

View File

@@ -1,39 +1,50 @@
import type { OpenClawApp } from "./app.ts";
import {
loadChannels,
logoutWhatsApp,
startWhatsAppLogin,
waitWhatsAppLogin,
type ChannelsState,
} from "./controllers/channels.ts";
import { loadConfig, saveConfig } from "./controllers/config.ts";
import { loadConfig, saveConfig, type ConfigState } from "./controllers/config.ts";
import { normalizeOptionalString } from "./string-coerce.ts";
import type { NostrProfile } from "./types.ts";
import { createNostrProfileFormState } from "./views/channels.nostr-profile-form.ts";
export async function handleWhatsAppStart(host: OpenClawApp, force: boolean) {
await startWhatsAppLogin(host, force);
await loadChannels(host, true);
type NostrProfileFormState = ReturnType<typeof createNostrProfileFormState> | null;
type ChannelsActionHost = ChannelsState &
ConfigState & {
hello?: { auth?: { deviceToken?: string | null } | null } | null;
password?: string;
settings: { token?: string };
nostrProfileFormState: NostrProfileFormState;
nostrProfileAccountId: string | null;
};
export async function handleWhatsAppStart(host: ChannelsActionHost, force: boolean) {
await startWhatsAppLogin(host as ChannelsState, force);
await loadChannels(host as ChannelsState, true);
}
export async function handleWhatsAppWait(host: OpenClawApp) {
await waitWhatsAppLogin(host);
await loadChannels(host, true);
export async function handleWhatsAppWait(host: ChannelsActionHost) {
await waitWhatsAppLogin(host as ChannelsState);
await loadChannels(host as ChannelsState, true);
}
export async function handleWhatsAppLogout(host: OpenClawApp) {
await logoutWhatsApp(host);
await loadChannels(host, true);
export async function handleWhatsAppLogout(host: ChannelsActionHost) {
await logoutWhatsApp(host as ChannelsState);
await loadChannels(host as ChannelsState, true);
}
export async function handleChannelConfigSave(host: OpenClawApp) {
await saveConfig(host);
await loadConfig(host);
await loadChannels(host, true);
export async function handleChannelConfigSave(host: ChannelsActionHost) {
await saveConfig(host as ConfigState);
await loadConfig(host as ConfigState);
await loadChannels(host as ChannelsState, true);
}
export async function handleChannelConfigReload(host: OpenClawApp) {
await loadConfig(host);
await loadChannels(host, true);
export async function handleChannelConfigReload(host: ChannelsActionHost) {
await loadConfig(host as ConfigState);
await loadChannels(host as ChannelsState, true);
}
function parseValidationErrors(details: unknown): Record<string, string> {
@@ -58,7 +69,7 @@ function parseValidationErrors(details: unknown): Record<string, string> {
return errors;
}
function resolveNostrAccountId(host: OpenClawApp): string {
function resolveNostrAccountId(host: ChannelsActionHost): string {
const accounts = host.channelsSnapshot?.channelAccounts?.nostr ?? [];
return accounts[0]?.accountId ?? host.nostrProfileAccountId ?? "default";
}
@@ -67,7 +78,7 @@ function buildNostrProfileUrl(accountId: string, suffix = ""): string {
return `/api/channels/nostr/${encodeURIComponent(accountId)}/profile${suffix}`;
}
function resolveGatewayHttpAuthHeader(host: OpenClawApp): string | null {
function resolveGatewayHttpAuthHeader(host: ChannelsActionHost): string | null {
const deviceToken = normalizeOptionalString(host.hello?.auth?.deviceToken);
if (deviceToken) {
return `Bearer ${deviceToken}`;
@@ -83,13 +94,13 @@ function resolveGatewayHttpAuthHeader(host: OpenClawApp): string | null {
return null;
}
function buildGatewayHttpHeaders(host: OpenClawApp): Record<string, string> {
function buildGatewayHttpHeaders(host: ChannelsActionHost): Record<string, string> {
const authorization = resolveGatewayHttpAuthHeader(host);
return authorization ? { Authorization: authorization } : {};
}
export function handleNostrProfileEdit(
host: OpenClawApp,
host: ChannelsActionHost,
accountId: string,
profile: NostrProfile | null,
) {
@@ -97,13 +108,13 @@ export function handleNostrProfileEdit(
host.nostrProfileFormState = createNostrProfileFormState(profile ?? undefined);
}
export function handleNostrProfileCancel(host: OpenClawApp) {
export function handleNostrProfileCancel(host: ChannelsActionHost) {
host.nostrProfileFormState = null;
host.nostrProfileAccountId = null;
}
export function handleNostrProfileFieldChange(
host: OpenClawApp,
host: ChannelsActionHost,
field: keyof NostrProfile,
value: string,
) {
@@ -124,7 +135,7 @@ export function handleNostrProfileFieldChange(
};
}
export function handleNostrProfileToggleAdvanced(host: OpenClawApp) {
export function handleNostrProfileToggleAdvanced(host: ChannelsActionHost) {
const state = host.nostrProfileFormState;
if (!state) {
return;
@@ -135,7 +146,7 @@ export function handleNostrProfileToggleAdvanced(host: OpenClawApp) {
};
}
export async function handleNostrProfileSave(host: OpenClawApp) {
export async function handleNostrProfileSave(host: ChannelsActionHost) {
const state = host.nostrProfileFormState;
if (!state || state.saving) {
return;
@@ -196,7 +207,7 @@ export async function handleNostrProfileSave(host: OpenClawApp) {
fieldErrors: {},
original: { ...state.values },
};
await loadChannels(host, true);
await loadChannels(host as ChannelsState, true);
} catch (err) {
host.nostrProfileFormState = {
...state,
@@ -207,7 +218,7 @@ export async function handleNostrProfileSave(host: OpenClawApp) {
}
}
export async function handleNostrProfileImport(host: OpenClawApp) {
export async function handleNostrProfileImport(host: ChannelsActionHost) {
const state = host.nostrProfileFormState;
if (!state || state.importing) {
return;

View File

@@ -1,12 +1,16 @@
import { setLastActiveSessionKey } from "./app-last-active-session.ts";
import { scheduleChatScroll, resetChatScroll } from "./app-scroll.ts";
import { setLastActiveSessionKey } from "./app-settings.ts";
import { resetToolStream } from "./app-tool-stream.ts";
import type { OpenClawApp } from "./app.ts";
import { executeSlashCommand } from "./chat/slash-command-executor.ts";
import { parseSlashCommand } from "./chat/slash-commands.ts";
import { abortChatRun, loadChatHistory, sendChatMessage } from "./controllers/chat.ts";
import {
abortChatRun,
loadChatHistory,
sendChatMessage,
type ChatState,
} from "./controllers/chat.ts";
import { loadModels } from "./controllers/models.ts";
import { loadSessions } from "./controllers/sessions.ts";
import { loadSessions, type SessionsState } from "./controllers/sessions.ts";
import type { GatewayBrowserClient, GatewayHelloOk } from "./gateway.ts";
import { normalizeBasePath } from "./navigation.ts";
import { parseAgentSessionKey } from "./session-key.ts";
@@ -82,7 +86,7 @@ export async function handleAbortChat(host: ChatHost) {
return;
}
host.chatMessage = "";
await abortChatRun(host as unknown as OpenClawApp);
await abortChatRun(host as unknown as ChatState);
}
function enqueueChatMessage(
@@ -142,7 +146,7 @@ async function sendChatMessageNow(
resetToolStream(host as unknown as Parameters<typeof resetToolStream>[0]);
// Reset scroll state before sending to ensure auto-scroll works for the response
resetChatScroll(host as unknown as Parameters<typeof resetChatScroll>[0]);
const runId = await sendChatMessage(host as unknown as OpenClawApp, message, opts?.attachments);
const runId = await sendChatMessage(host as unknown as ChatState, message, opts?.attachments);
const ok = Boolean(runId);
if (!ok && opts?.previousDraft != null) {
host.chatMessage = opts.previousDraft;
@@ -375,7 +379,7 @@ async function clearChatHistory(host: ChatHost) {
host.chatMessages = [];
host.chatStream = null;
host.chatRunId = null;
await loadChatHistory(host as unknown as OpenClawApp);
await loadChatHistory(host as unknown as ChatState);
} catch (err) {
host.lastError = String(err);
}
@@ -395,8 +399,8 @@ function injectCommandResult(host: ChatHost, content: string) {
export async function refreshChat(host: ChatHost, opts?: { scheduleScroll?: boolean }) {
await Promise.all([
loadChatHistory(host as unknown as OpenClawApp),
loadSessions(host as unknown as OpenClawApp, {
loadChatHistory(host as unknown as ChatState),
loadSessions(host as unknown as SessionsState, {
activeMinutes: 0,
limit: 0,
includeGlobal: true,

View File

@@ -15,14 +15,20 @@ import {
setLastActiveSessionKey,
} from "./app-settings.ts";
import { handleAgentEvent, resetToolStream, type AgentEventPayload } from "./app-tool-stream.ts";
import type { OpenClawApp } from "./app.ts";
import { shouldReloadHistoryForFinalEvent } from "./chat-event-reload.ts";
import { formatConnectError } from "./connect-error.ts";
import { loadAgents } from "./controllers/agents.ts";
import { loadAssistantIdentity } from "./controllers/assistant-identity.ts";
import { loadChatHistory } from "./controllers/chat.ts";
import { handleChatEvent, type ChatEventPayload } from "./controllers/chat.ts";
import { loadDevices } from "./controllers/devices.ts";
import { loadAgents, type AgentsState } from "./controllers/agents.ts";
import {
loadAssistantIdentity,
type AssistantIdentityState,
} from "./controllers/assistant-identity.ts";
import {
loadChatHistory,
handleChatEvent,
type ChatEventPayload,
type ChatState,
} from "./controllers/chat.ts";
import { loadDevices, type DevicesState } from "./controllers/devices.ts";
import type { ExecApprovalRequest } from "./controllers/exec-approval.ts";
import {
addExecApproval,
@@ -32,9 +38,9 @@ import {
pruneExecApprovalQueue,
removeExecApproval,
} from "./controllers/exec-approval.ts";
import { loadHealthState } from "./controllers/health.ts";
import { loadNodes } from "./controllers/nodes.ts";
import { loadSessions, subscribeSessions } from "./controllers/sessions.ts";
import { loadHealthState, type HealthState } from "./controllers/health.ts";
import { loadNodes, type NodesState } from "./controllers/nodes.ts";
import { loadSessions, subscribeSessions, type SessionsState } from "./controllers/sessions.ts";
import {
resolveGatewayErrorDetailCode,
type GatewayEventFrame,
@@ -248,12 +254,12 @@ export function connectGateway(host: GatewayHost, options?: ConnectGatewayOption
host as unknown as Parameters<typeof flushChatQueueForEvent>[0],
);
}
void subscribeSessions(host as unknown as OpenClawApp);
void loadAssistantIdentity(host as unknown as OpenClawApp);
void loadAgents(host as unknown as OpenClawApp);
void loadHealthState(host as unknown as OpenClawApp);
void loadNodes(host as unknown as OpenClawApp, { quiet: true });
void loadDevices(host as unknown as OpenClawApp, { quiet: true });
void subscribeSessions(host as unknown as SessionsState);
void loadAssistantIdentity(host as unknown as AssistantIdentityState);
void loadAgents(host as unknown as AgentsState);
void loadHealthState(host as unknown as HealthState);
void loadNodes(host as unknown as NodesState, { quiet: true });
void loadDevices(host as unknown as DevicesState, { quiet: true });
void refreshActiveTab(host as unknown as Parameters<typeof refreshActiveTab>[0]);
},
onClose: ({ code, reason, error }) => {
@@ -333,7 +339,7 @@ function handleTerminalChatEvent(
if (runId && host.refreshSessionsAfterChat.has(runId)) {
host.refreshSessionsAfterChat.delete(runId);
if (state === "final") {
void loadSessions(host as unknown as OpenClawApp, {
void loadSessions(host as unknown as SessionsState, {
activeMinutes: CHAT_SESSIONS_ACTIVE_MINUTES,
});
}
@@ -341,7 +347,7 @@ function handleTerminalChatEvent(
// Reload history when tools were used so the persisted tool results
// replace the now-cleared streaming state.
if (hadToolEvents && state === "final") {
void loadChatHistory(host as unknown as OpenClawApp);
void loadChatHistory(host as unknown as ChatState);
return true;
}
return false;
@@ -354,10 +360,10 @@ function handleChatGatewayEvent(host: GatewayHost, payload: ChatEventPayload | u
payload.sessionKey,
);
}
const state = handleChatEvent(host as unknown as OpenClawApp, payload);
const state = handleChatEvent(host as unknown as ChatState, payload);
const historyReloaded = handleTerminalChatEvent(host, payload, state);
if (state === "final" && !historyReloaded && shouldReloadHistoryForFinalEvent(payload)) {
void loadChatHistory(host as unknown as OpenClawApp);
void loadChatHistory(host as unknown as ChatState);
}
}
@@ -410,7 +416,7 @@ function handleGatewayEventUnsafe(host: GatewayHost, evt: GatewayEventFrame) {
}
if (evt.event === "sessions.changed") {
void loadSessions(host as unknown as OpenClawApp);
void loadSessions(host as unknown as SessionsState);
return;
}
@@ -419,7 +425,7 @@ function handleGatewayEventUnsafe(host: GatewayHost, evt: GatewayEventFrame) {
}
if (evt.event === "device.pair.requested" || evt.event === "device.pair.resolved") {
void loadDevices(host as unknown as OpenClawApp, { quiet: true });
void loadDevices(host as unknown as DevicesState, { quiet: true });
}
if (evt.event === "exec.approval.requested") {

View File

@@ -0,0 +1,14 @@
import type { UiSettings } from "./storage.ts";
type LastActiveSessionHost = {
settings: UiSettings;
applySettings(next: UiSettings): void;
};
export function setLastActiveSessionKey(host: LastActiveSessionHost, next: string) {
const trimmed = next.trim();
if (!trimmed || host.settings.lastActiveSessionKey === trimmed) {
return;
}
host.applySettings({ ...host.settings, lastActiveSessionKey: trimmed });
}

View File

@@ -1,6 +1,8 @@
import type { OpenClawApp } from "./app.ts";
import type { DebugState } from "./controllers/debug.ts";
import { loadDebug } from "./controllers/debug.ts";
import type { LogsState } from "./controllers/logs.ts";
import { loadLogs } from "./controllers/logs.ts";
import type { NodesState } from "./controllers/nodes.ts";
import { loadNodes } from "./controllers/nodes.ts";
type PollingHost = {
@@ -15,7 +17,7 @@ export function startNodesPolling(host: PollingHost) {
return;
}
host.nodesPollInterval = window.setInterval(
() => void loadNodes(host as unknown as OpenClawApp, { quiet: true }),
() => void loadNodes(host as unknown as NodesState, { quiet: true }),
5000,
);
}
@@ -36,7 +38,7 @@ export function startLogsPolling(host: PollingHost) {
if (host.tab !== "logs") {
return;
}
void loadLogs(host as unknown as OpenClawApp, { quiet: true });
void loadLogs(host as unknown as LogsState, { quiet: true });
}, 2000);
}
@@ -56,7 +58,7 @@ export function startDebugPolling(host: PollingHost) {
if (host.tab !== "debug") {
return;
}
void loadDebug(host as unknown as OpenClawApp);
void loadDebug(host as unknown as DebugState);
}, 3000);
}

View File

@@ -4,7 +4,6 @@ import { t } from "../i18n/index.ts";
import { refreshChat, refreshChatAvatar } from "./app-chat.ts";
import { syncUrlWithSessionKey } from "./app-settings.ts";
import type { AppViewState } from "./app-view-state.ts";
import { OpenClawApp } from "./app.ts";
import { createChatModelOverride } from "./chat-model-ref.ts";
import {
resolveChatModelOverrideValue,
@@ -30,6 +29,20 @@ type SessionDefaultsSnapshot = {
mainKey?: string;
};
type SessionSwitchHost = AppViewState & {
chatStreamStartedAt: number | null;
resetToolStream(): void;
resetChatScroll(): void;
};
type ChatRefreshHost = AppViewState & {
chatManualRefreshInFlight: boolean;
chatNewMessagesBelow: boolean;
resetToolStream(): void;
scrollToBottom(opts?: { smooth?: boolean }): void;
updateComplete?: Promise<unknown>;
};
function resolveSidebarChatSessionKey(state: AppViewState): string {
const snapshot = state.hello?.snapshot as
| { sessionDefaults?: SessionDefaultsSnapshot }
@@ -46,6 +59,7 @@ function resolveSidebarChatSessionKey(state: AppViewState): string {
}
function resetChatStateForSessionSwitch(state: AppViewState, sessionKey: string) {
const host = state as unknown as SessionSwitchHost;
state.sessionKey = sessionKey;
state.chatMessage = "";
state.chatAttachments = [];
@@ -59,10 +73,10 @@ function resetChatStateForSessionSwitch(state: AppViewState, sessionKey: string)
state.fallbackStatus = null;
state.chatAvatarUrl = null;
state.chatQueue = [];
(state as unknown as OpenClawApp).chatStreamStartedAt = null;
host.chatStreamStartedAt = null;
state.chatRunId = null;
(state as unknown as OpenClawApp).resetToolStream();
(state as unknown as OpenClawApp).resetChatScroll();
host.resetToolStream();
host.resetChatScroll();
state.applySettings({
...state.settings,
sessionKey,
@@ -252,7 +266,7 @@ export function renderChatControls(state: AppViewState) {
class="btn btn--sm btn--icon"
?disabled=${state.chatLoading || !state.connected}
@click=${async () => {
const app = state as unknown as OpenClawApp;
const app = state as unknown as ChatRefreshHost;
app.chatManualRefreshInFlight = true;
app.chatNewMessagesBelow = false;
await app.updateComplete;

View File

@@ -7,24 +7,32 @@ import {
stopDebugPolling,
} from "./app-polling.ts";
import { scheduleChatScroll, scheduleLogsScroll } from "./app-scroll.ts";
import type { OpenClawApp } from "./app.ts";
import { loadAgentFiles } from "./controllers/agent-files.ts";
import { loadAgentIdentities, loadAgentIdentity } from "./controllers/agent-identity.ts";
import { loadAgentSkills } from "./controllers/agent-skills.ts";
import { loadAgents } from "./controllers/agents.ts";
import { loadChannels } from "./controllers/channels.ts";
import { loadConfig, loadConfigSchema } from "./controllers/config.ts";
import { loadCronJobsPage, loadCronRuns, loadCronStatus } from "./controllers/cron.ts";
import { loadDebug } from "./controllers/debug.ts";
import { loadDevices } from "./controllers/devices.ts";
import { loadDreamDiary, loadDreamingStatus } from "./controllers/dreaming.ts";
import { loadExecApprovals } from "./controllers/exec-approvals.ts";
import { loadLogs } from "./controllers/logs.ts";
import { loadNodes } from "./controllers/nodes.ts";
import { loadPresence } from "./controllers/presence.ts";
import { loadSessions } from "./controllers/sessions.ts";
import { loadSkills } from "./controllers/skills.ts";
import { loadUsage } from "./controllers/usage.ts";
import { loadAgentFiles, type AgentFilesState } from "./controllers/agent-files.ts";
import {
loadAgentIdentities,
loadAgentIdentity,
type AgentIdentityState,
} from "./controllers/agent-identity.ts";
import { loadAgentSkills, type AgentSkillsState } from "./controllers/agent-skills.ts";
import { loadAgents, type AgentsState } from "./controllers/agents.ts";
import { loadChannels, type ChannelsState } from "./controllers/channels.ts";
import { loadConfig, loadConfigSchema, type ConfigState } from "./controllers/config.ts";
import {
loadCronJobsPage,
loadCronRuns,
loadCronStatus,
type CronState,
} from "./controllers/cron.ts";
import { loadDebug, type DebugState } from "./controllers/debug.ts";
import { loadDevices, type DevicesState } from "./controllers/devices.ts";
import { loadDreamDiary, loadDreamingStatus, type DreamingState } from "./controllers/dreaming.ts";
import { loadExecApprovals, type ExecApprovalsState } from "./controllers/exec-approvals.ts";
import { loadLogs, type LogsState } from "./controllers/logs.ts";
import { loadNodes, type NodesState } from "./controllers/nodes.ts";
import { loadPresence, type PresenceState } from "./controllers/presence.ts";
import { loadSessions, type SessionsState } from "./controllers/sessions.ts";
import { loadSkills, type SkillsState } from "./controllers/skills.ts";
import { loadUsage, type UsageState } from "./controllers/usage.ts";
import {
inferBasePathFromPathname,
normalizeBasePath,
@@ -40,6 +48,8 @@ import { resolveTheme, type ResolvedTheme, type ThemeMode, type ThemeName } from
import type { AgentsListResult, AttentionItem } from "./types.ts";
import { resetChatViewState } from "./views/chat.ts";
export { setLastActiveSessionKey } from "./app-last-active-session.ts";
type SettingsHost = {
settings: UiSettings;
password?: string;
@@ -71,6 +81,30 @@ type SettingsHost = {
dreamDiaryContent: string | null;
};
type SettingsAppHost = SettingsHost &
AgentFilesState &
AgentIdentityState &
AgentSkillsState &
AgentsState &
ChannelsState &
ConfigState &
CronState &
DebugState &
DevicesState &
DreamingState &
ExecApprovalsState &
LogsState &
NodesState &
PresenceState &
SessionsState &
SkillsState &
UsageState & {
overviewLogCursor: number | null;
overviewLogLines: string[];
attentionItems: AttentionItem[];
hello: { auth?: { role?: string; scopes?: string[] } } | null;
};
export function applySettings(host: SettingsHost, next: UiSettings) {
const normalized = {
...next,
@@ -90,14 +124,6 @@ export function applySettings(host: SettingsHost, next: UiSettings) {
host.applySessionKey = host.settings.lastActiveSessionKey;
}
export function setLastActiveSessionKey(host: SettingsHost, next: string) {
const trimmed = next.trim();
if (!trimmed || host.settings.lastActiveSessionKey === trimmed) {
return;
}
applySettings(host, { ...host.settings, lastActiveSessionKey: trimmed });
}
function applySessionSelection(host: SettingsHost, session: string) {
host.sessionKey = session;
applySettings(host, {
@@ -231,7 +257,7 @@ export function setThemeMode(
);
}
async function refreshAgentsTab(host: SettingsHost, app: OpenClawApp) {
async function refreshAgentsTab(host: SettingsHost, app: SettingsAppHost) {
await loadAgents(app);
await loadConfig(app);
const agentIds = host.agentsList?.agents?.map((entry) => entry.id) ?? [];
@@ -257,7 +283,7 @@ async function refreshAgentsTab(host: SettingsHost, app: OpenClawApp) {
}
export async function refreshActiveTab(host: SettingsHost) {
const app = host as unknown as OpenClawApp;
const app = host as unknown as SettingsAppHost;
switch (host.tab) {
case "config":
case "communications":
@@ -547,7 +573,7 @@ export function hasMissingSkillDependencies(
return Object.values(missing).some((value) => Array.isArray(value) && value.length > 0);
}
async function loadOverviewLogs(host: OpenClawApp) {
async function loadOverviewLogs(host: SettingsAppHost) {
if (!host.client || !host.connected) {
return;
}
@@ -573,7 +599,7 @@ async function loadOverviewLogs(host: OpenClawApp) {
}
}
function buildAttentionItems(host: OpenClawApp) {
function buildAttentionItems(host: SettingsAppHost) {
const items: AttentionItem[] = [];
if (host.lastError) {
@@ -650,12 +676,12 @@ function buildAttentionItems(host: OpenClawApp) {
}
export async function loadChannelsTab(host: SettingsHost) {
const app = host as unknown as OpenClawApp;
const app = host as unknown as SettingsAppHost;
await Promise.all([loadChannels(app, true), loadConfigSchema(app), loadConfig(app)]);
}
export async function loadCron(host: SettingsHost) {
const app = host as unknown as OpenClawApp;
const app = host as unknown as SettingsAppHost;
const activeCronJobId = app.cronRunsScope === "job" ? app.cronRunsJobId : null;
await Promise.all([
loadChannels(app, false),