fix(ui): repair control-ui type drift

This commit is contained in:
Val Alexander
2026-03-10 20:37:10 -05:00
parent 0a14c5bd29
commit d19c14176c
10 changed files with 109 additions and 19 deletions

View File

@@ -315,6 +315,10 @@ describe("isCronSessionKey", () => {
describe("resolveSessionOptionGroups", () => {
const sessions: SessionsListResult = {
ts: 0,
path: "",
count: 3,
defaults: { model: null, contextTokens: null },
sessions: [
row({ key: "agent:main:main" }),
row({ key: "agent:main:cron:daily" }),

View File

@@ -277,9 +277,10 @@ function resolveAssistantAvatarUrl(state: AppViewState): string | undefined {
}
export function renderApp(state: AppViewState) {
const updatableState = state as AppViewState & { requestUpdate?: () => void };
const requestHostUpdate =
typeof (state as { requestUpdate?: unknown }).requestUpdate === "function"
? () => (state as { requestUpdate: () => void }).requestUpdate()
typeof updatableState.requestUpdate === "function"
? () => updatableState.requestUpdate?.()
: undefined;
// Gate: require successful gateway connection before showing the dashboard.
@@ -757,6 +758,7 @@ export function renderApp(state: AppViewState) {
error: state.cronError,
busy: state.cronBusy,
form: state.cronForm,
editingJobId: state.cronEditingJobId,
channels: state.channelsSnapshot?.channelMeta?.length
? state.channelsSnapshot.channelMeta.map((entry) => entry.id)
: (state.channelsSnapshot?.channelOrder ?? []),

View File

@@ -34,8 +34,6 @@ const createHost = (tab: Tab): SettingsHost => ({
eventLog: [],
eventLogBuffer: [],
basePath: "",
themeMedia: null,
themeMediaHandler: null,
logsPollInterval: null,
debugPollInterval: null,
systemThemeCleanup: null,

View File

@@ -204,7 +204,8 @@ describe("loadToolsCatalog", () => {
expect(state.toolsCatalogResult).toEqual(replacementPayload);
expect(state.toolsCatalogLoading).toBe(false);
resolveMain?.({
expect(resolveMain).not.toBeNull();
resolveMain!({
agentId: "main",
profiles: [{ id: "full", label: "Full" }],
groups: [],

View File

@@ -1,4 +1,5 @@
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
import type { UiSettings } from "./storage.ts";
function createStorageMock(): Storage {
const store = new Map<string, string>();
@@ -62,7 +63,7 @@ function expectedGatewayUrl(basePath: string): string {
return `${proto}://${location.host}${basePath}`;
}
function createSettings(overrides: Record<string, unknown> = {}) {
function createSettings(overrides: Partial<UiSettings> = {}): UiSettings {
return {
gatewayUrl: "wss://gateway.example:8443/openclaw",
token: "",

View File

@@ -410,6 +410,15 @@ export type {
SessionUsageTimeSeries,
} from "./usage-types.ts";
export type CronRunStatus = "ok" | "error" | "skipped";
export type CronDeliveryStatus = "delivered" | "not-delivered" | "unknown" | "not-requested";
export type CronJobsEnabledFilter = "all" | "enabled" | "disabled";
export type CronJobsSortBy = "nextRunAtMs" | "updatedAtMs" | "name";
export type CronRunScope = "job" | "all";
export type CronRunsStatusValue = CronRunStatus;
export type CronRunsStatusFilter = "all" | CronRunStatus;
export type CronSortDir = "asc" | "desc";
export type CronSchedule =
| { kind: "at"; at: string }
| { kind: "every"; everyMs: number; anchorMs?: number }
@@ -424,9 +433,15 @@ export type CronPayload =
kind: "agentTurn";
message: string;
model?: string;
fallbacks?: string[];
thinking?: string;
timeoutSeconds?: number;
allowUnsafeExternalContent?: boolean;
lightContext?: boolean;
deliver?: boolean;
channel?: string;
to?: string;
bestEffortDeliver?: boolean;
};
export type CronDelivery = {
@@ -458,9 +473,15 @@ export type CronJobState = {
nextRunAtMs?: number;
runningAtMs?: number;
lastRunAtMs?: number;
lastStatus?: "ok" | "error" | "skipped";
lastRunStatus?: CronRunStatus;
lastStatus?: CronRunStatus;
lastError?: string;
lastErrorReason?: string;
lastDurationMs?: number;
consecutiveErrors?: number;
lastDelivered?: boolean;
lastDeliveryStatus?: CronDeliveryStatus;
lastDeliveryError?: string;
lastFailureAlertAtMs?: number;
};
@@ -484,12 +505,46 @@ export type CronStatus = {
export type CronRunLogEntry = {
ts: number;
jobId: string;
status: "ok" | "error" | "skipped";
action?: "finished";
status?: CronRunStatus;
durationMs?: number;
error?: string;
summary?: string;
delivered?: boolean;
deliveryStatus?: CronDeliveryStatus;
deliveryError?: string;
sessionId?: string;
sessionKey?: string;
runAtMs?: number;
nextRunAtMs?: number;
model?: string;
provider?: string;
usage?: {
input_tokens?: number;
output_tokens?: number;
total_tokens?: number;
cache_read_tokens?: number;
cache_write_tokens?: number;
};
jobName?: string;
};
export type CronJobsListResult = {
jobs: CronJob[];
total?: number;
limit?: number;
offset?: number;
nextOffset?: number | null;
hasMore?: boolean;
};
export type CronRunsResult = {
entries: CronRunLogEntry[];
total?: number;
limit?: number;
offset?: number;
nextOffset?: number | null;
hasMore?: boolean;
};
export type SkillsStatusConfigCheck = {

View File

@@ -40,12 +40,15 @@ function createProps(overrides: Partial<ChatProps> = {}): ChatProps {
focusMode: false,
assistantName: "OpenClaw",
assistantAvatar: null,
agentsList: null,
currentAgentId: "main",
onRefresh: () => undefined,
onToggleFocusMode: () => undefined,
onDraftChange: () => undefined,
onSend: () => undefined,
onQueueRemove: () => undefined,
onNewSession: () => undefined,
onAgentChange: () => undefined,
...overrides,
};
}
@@ -190,18 +193,17 @@ describe("chat view", () => {
createProps({
canAbort: true,
onAbort,
stream: "in-flight",
}),
),
container,
);
const stopButton = Array.from(container.querySelectorAll("button")).find(
(btn) => btn.textContent?.trim() === "Stop",
);
expect(stopButton).not.toBeUndefined();
const stopButton = container.querySelector<HTMLButtonElement>('button[title="Stop"]');
expect(stopButton).not.toBeNull();
stopButton?.dispatchEvent(new MouseEvent("click", { bubbles: true }));
expect(onAbort).toHaveBeenCalledTimes(1);
expect(container.textContent).not.toContain("New session");
expect(container.querySelector('button[title="New session"]')).toBeNull();
});
it("shows a new session button when aborting is unavailable", () => {
@@ -217,13 +219,13 @@ describe("chat view", () => {
container,
);
const newSessionButton = Array.from(container.querySelectorAll("button")).find(
(btn) => btn.textContent?.trim() === "New session",
const newSessionButton = container.querySelector<HTMLButtonElement>(
'button[title="New session"]',
);
expect(newSessionButton).not.toBeUndefined();
expect(newSessionButton).not.toBeNull();
newSessionButton?.dispatchEvent(new MouseEvent("click", { bubbles: true }));
expect(onNewSession).toHaveBeenCalledTimes(1);
expect(container.textContent).not.toContain("Stop");
expect(container.querySelector('button[title="Stop"]')).toBeNull();
});
it("shows sender labels from sanitized gateway messages instead of generic You", () => {

View File

@@ -1234,6 +1234,20 @@ export function renderChat(props: ChatProps) {
<div class="agent-chat__toolbar-right">
${nothing /* search hidden for now */}
${
canAbort
? nothing
: html`
<button
class="btn-ghost"
@click=${props.onNewSession}
title="New session"
aria-label="New session"
>
${icons.plus}
</button>
`
}
<button class="btn-ghost" @click=${() => exportMarkdown(props)} title="Export" ?disabled=${props.messages.length === 0}>
${icons.download}
</button>

View File

@@ -360,7 +360,9 @@ export function renderCron(props: CronProps) {
props.runsScope === "all"
? t("cron.jobList.allJobs")
: (selectedJob?.name ?? props.runsJobId ?? t("cron.jobList.selectJob"));
const runs = props.runs;
const runs = props.runs.toSorted((a, b) =>
props.runsSortDir === "asc" ? a.ts - b.ts : b.ts - a.ts,
);
const runStatusOptions = getRunStatusOptions();
const runDeliveryOptions = getRunDeliveryOptions();
const selectedStatusLabels = runStatusOptions
@@ -1569,7 +1571,7 @@ function renderJob(job: CronJob, props: CronProps) {
?disabled=${props.busy}
@click=${(event: Event) => {
event.stopPropagation();
selectAnd(() => props.onLoadRuns(job.id));
props.onLoadRuns(job.id);
}}
>
${t("cron.jobList.history")}

View File

@@ -23,7 +23,18 @@ function buildProps(result: SessionsListResult): SessionsProps {
includeGlobal: false,
includeUnknown: false,
basePath: "",
searchQuery: "",
sortColumn: "updated",
sortDir: "desc",
page: 0,
pageSize: 25,
actionsOpenKey: null,
onFiltersChange: () => undefined,
onSearchChange: () => undefined,
onSortChange: () => undefined,
onPageChange: () => undefined,
onPageSizeChange: () => undefined,
onActionsOpenChange: () => undefined,
onRefresh: () => undefined,
onPatch: () => undefined,
onDelete: () => undefined,