chore: lint the ui folder.

This commit is contained in:
cpojer
2026-02-02 15:08:47 +09:00
parent e25f8ed56c
commit 5ba4586e58
73 changed files with 571 additions and 579 deletions

View File

@@ -31,7 +31,6 @@
"skills/", "skills/",
"src/canvas-host/a2ui/a2ui.bundle.js", "src/canvas-host/a2ui/a2ui.bundle.js",
"Swabble/", "Swabble/",
"vendor/", "vendor/"
"ui/"
] ]
} }

View File

@@ -36,15 +36,15 @@ export async function handleChannelConfigReload(host: OpenClawApp) {
} }
function parseValidationErrors(details: unknown): Record<string, string> { function parseValidationErrors(details: unknown): Record<string, string> {
if (!Array.isArray(details)) return {}; if (!Array.isArray(details)) {return {};}
const errors: Record<string, string> = {}; const errors: Record<string, string> = {};
for (const entry of details) { for (const entry of details) {
if (typeof entry !== "string") continue; if (typeof entry !== "string") {continue;}
const [rawField, ...rest] = entry.split(":"); const [rawField, ...rest] = entry.split(":");
if (!rawField || rest.length === 0) continue; if (!rawField || rest.length === 0) {continue;}
const field = rawField.trim(); const field = rawField.trim();
const message = rest.join(":").trim(); const message = rest.join(":").trim();
if (field && message) errors[field] = message; if (field && message) {errors[field] = message;}
} }
return errors; return errors;
} }
@@ -78,7 +78,7 @@ export function handleNostrProfileFieldChange(
value: string, value: string,
) { ) {
const state = host.nostrProfileFormState; const state = host.nostrProfileFormState;
if (!state) return; if (!state) {return;}
host.nostrProfileFormState = { host.nostrProfileFormState = {
...state, ...state,
values: { values: {
@@ -94,7 +94,7 @@ export function handleNostrProfileFieldChange(
export function handleNostrProfileToggleAdvanced(host: OpenClawApp) { export function handleNostrProfileToggleAdvanced(host: OpenClawApp) {
const state = host.nostrProfileFormState; const state = host.nostrProfileFormState;
if (!state) return; if (!state) {return;}
host.nostrProfileFormState = { host.nostrProfileFormState = {
...state, ...state,
showAdvanced: !state.showAdvanced, showAdvanced: !state.showAdvanced,
@@ -103,7 +103,7 @@ export function handleNostrProfileToggleAdvanced(host: OpenClawApp) {
export async function handleNostrProfileSave(host: OpenClawApp) { export async function handleNostrProfileSave(host: OpenClawApp) {
const state = host.nostrProfileFormState; const state = host.nostrProfileFormState;
if (!state || state.saving) return; if (!state || state.saving) {return;}
const accountId = resolveNostrAccountId(host); const accountId = resolveNostrAccountId(host);
host.nostrProfileFormState = { host.nostrProfileFormState = {
@@ -172,7 +172,7 @@ export async function handleNostrProfileSave(host: OpenClawApp) {
export async function handleNostrProfileImport(host: OpenClawApp) { export async function handleNostrProfileImport(host: OpenClawApp) {
const state = host.nostrProfileFormState; const state = host.nostrProfileFormState;
if (!state || state.importing) return; if (!state || state.importing) {return;}
const accountId = resolveNostrAccountId(host); const accountId = resolveNostrAccountId(host);
host.nostrProfileFormState = { host.nostrProfileFormState = {

View File

@@ -32,9 +32,9 @@ export function isChatBusy(host: ChatHost) {
export function isChatStopCommand(text: string) { export function isChatStopCommand(text: string) {
const trimmed = text.trim(); const trimmed = text.trim();
if (!trimmed) return false; if (!trimmed) {return false;}
const normalized = trimmed.toLowerCase(); const normalized = trimmed.toLowerCase();
if (normalized === "/stop") return true; if (normalized === "/stop") {return true;}
return ( return (
normalized === "stop" || normalized === "stop" ||
normalized === "esc" || normalized === "esc" ||
@@ -46,14 +46,14 @@ export function isChatStopCommand(text: string) {
function isChatResetCommand(text: string) { function isChatResetCommand(text: string) {
const trimmed = text.trim(); const trimmed = text.trim();
if (!trimmed) return false; if (!trimmed) {return false;}
const normalized = trimmed.toLowerCase(); const normalized = trimmed.toLowerCase();
if (normalized === "/new" || normalized === "/reset") return true; if (normalized === "/new" || normalized === "/reset") {return true;}
return normalized.startsWith("/new ") || normalized.startsWith("/reset "); return normalized.startsWith("/new ") || normalized.startsWith("/reset ");
} }
export async function handleAbortChat(host: ChatHost) { export async function handleAbortChat(host: ChatHost) {
if (!host.connected) return; if (!host.connected) {return;}
host.chatMessage = ""; host.chatMessage = "";
await abortChatRun(host as unknown as OpenClawApp); await abortChatRun(host as unknown as OpenClawApp);
} }
@@ -66,7 +66,7 @@ function enqueueChatMessage(
) { ) {
const trimmed = text.trim(); const trimmed = text.trim();
const hasAttachments = Boolean(attachments && attachments.length > 0); const hasAttachments = Boolean(attachments && attachments.length > 0);
if (!trimmed && !hasAttachments) return; if (!trimmed && !hasAttachments) {return;}
host.chatQueue = [ host.chatQueue = [
...host.chatQueue, ...host.chatQueue,
{ {
@@ -123,9 +123,9 @@ async function sendChatMessageNow(
} }
async function flushChatQueue(host: ChatHost) { async function flushChatQueue(host: ChatHost) {
if (!host.connected || isChatBusy(host)) return; if (!host.connected || isChatBusy(host)) {return;}
const [next, ...rest] = host.chatQueue; const [next, ...rest] = host.chatQueue;
if (!next) return; if (!next) {return;}
host.chatQueue = rest; host.chatQueue = rest;
const ok = await sendChatMessageNow(host, next.text, { const ok = await sendChatMessageNow(host, next.text, {
attachments: next.attachments, attachments: next.attachments,
@@ -145,7 +145,7 @@ export async function handleSendChat(
messageOverride?: string, messageOverride?: string,
opts?: { restoreDraft?: boolean }, opts?: { restoreDraft?: boolean },
) { ) {
if (!host.connected) return; if (!host.connected) {return;}
const previousDraft = host.chatMessage; const previousDraft = host.chatMessage;
const message = (messageOverride ?? host.chatMessage).trim(); const message = (messageOverride ?? host.chatMessage).trim();
const attachments = host.chatAttachments ?? []; const attachments = host.chatAttachments ?? [];
@@ -153,7 +153,7 @@ export async function handleSendChat(
const hasAttachments = attachmentsToSend.length > 0; const hasAttachments = attachmentsToSend.length > 0;
// Allow sending with just attachments (no message text required) // Allow sending with just attachments (no message text required)
if (!message && !hasAttachments) return; if (!message && !hasAttachments) {return;}
if (isChatStopCommand(message)) { if (isChatStopCommand(message)) {
await handleAbortChat(host); await handleAbortChat(host);
@@ -201,7 +201,7 @@ type SessionDefaultsSnapshot = {
function resolveAgentIdForSession(host: ChatHost): string | null { function resolveAgentIdForSession(host: ChatHost): string | null {
const parsed = parseAgentSessionKey(host.sessionKey); const parsed = parseAgentSessionKey(host.sessionKey);
if (parsed?.agentId) return parsed.agentId; if (parsed?.agentId) {return parsed.agentId;}
const snapshot = host.hello?.snapshot as const snapshot = host.hello?.snapshot as
| { sessionDefaults?: SessionDefaultsSnapshot } | { sessionDefaults?: SessionDefaultsSnapshot }
| undefined; | undefined;

View File

@@ -64,8 +64,8 @@ function normalizeSessionKeyForDefaults(
): string { ): string {
const raw = (value ?? "").trim(); const raw = (value ?? "").trim();
const mainSessionKey = defaults.mainSessionKey?.trim(); const mainSessionKey = defaults.mainSessionKey?.trim();
if (!mainSessionKey) return raw; if (!mainSessionKey) {return raw;}
if (!raw) return mainSessionKey; if (!raw) {return mainSessionKey;}
const mainKey = defaults.mainKey?.trim() || "main"; const mainKey = defaults.mainKey?.trim() || "main";
const defaultAgentId = defaults.defaultAgentId?.trim(); const defaultAgentId = defaults.defaultAgentId?.trim();
const isAlias = const isAlias =
@@ -77,7 +77,7 @@ function normalizeSessionKeyForDefaults(
} }
function applySessionDefaults(host: GatewayHost, defaults?: SessionDefaultsSnapshot) { function applySessionDefaults(host: GatewayHost, defaults?: SessionDefaultsSnapshot) {
if (!defaults?.mainSessionKey) return; if (!defaults?.mainSessionKey) {return;}
const resolvedSessionKey = normalizeSessionKeyForDefaults(host.sessionKey, defaults); const resolvedSessionKey = normalizeSessionKeyForDefaults(host.sessionKey, defaults);
const resolvedSettingsSessionKey = normalizeSessionKeyForDefaults( const resolvedSettingsSessionKey = normalizeSessionKeyForDefaults(
host.settings.sessionKey, host.settings.sessionKey,
@@ -168,7 +168,7 @@ function handleGatewayEventUnsafe(host: GatewayHost, evt: GatewayEventFrame) {
} }
if (evt.event === "agent") { if (evt.event === "agent") {
if (host.onboarding) return; if (host.onboarding) {return;}
handleAgentEvent( handleAgentEvent(
host as unknown as Parameters<typeof handleAgentEvent>[0], host as unknown as Parameters<typeof handleAgentEvent>[0],
evt.payload as AgentEventPayload | undefined, evt.payload as AgentEventPayload | undefined,
@@ -198,7 +198,7 @@ function handleGatewayEventUnsafe(host: GatewayHost, evt: GatewayEventFrame) {
} }
} }
} }
if (state === "final") void loadChatHistory(host as unknown as OpenClawApp); if (state === "final") {void loadChatHistory(host as unknown as OpenClawApp);}
return; return;
} }

View File

@@ -77,7 +77,8 @@ export function handleUpdated(host: LifecycleHost, changed: Map<PropertyKey, unk
const forcedByLoad = const forcedByLoad =
changed.has("chatLoading") && changed.has("chatLoading") &&
changed.get("chatLoading") === true && changed.get("chatLoading") === true &&
host.chatLoading === false; !
host.chatLoading;
scheduleChatScroll( scheduleChatScroll(
host as unknown as Parameters<typeof scheduleChatScroll>[0], host as unknown as Parameters<typeof scheduleChatScroll>[0],
forcedByTab || forcedByLoad || !host.chatHasAutoScrolled, forcedByTab || forcedByLoad || !host.chatHasAutoScrolled,

View File

@@ -11,7 +11,7 @@ type PollingHost = {
}; };
export function startNodesPolling(host: PollingHost) { export function startNodesPolling(host: PollingHost) {
if (host.nodesPollInterval != null) return; if (host.nodesPollInterval != null) {return;}
host.nodesPollInterval = window.setInterval( host.nodesPollInterval = window.setInterval(
() => void loadNodes(host as unknown as OpenClawApp, { quiet: true }), () => void loadNodes(host as unknown as OpenClawApp, { quiet: true }),
5000, 5000,
@@ -19,35 +19,35 @@ export function startNodesPolling(host: PollingHost) {
} }
export function stopNodesPolling(host: PollingHost) { export function stopNodesPolling(host: PollingHost) {
if (host.nodesPollInterval == null) return; if (host.nodesPollInterval == null) {return;}
clearInterval(host.nodesPollInterval); clearInterval(host.nodesPollInterval);
host.nodesPollInterval = null; host.nodesPollInterval = null;
} }
export function startLogsPolling(host: PollingHost) { export function startLogsPolling(host: PollingHost) {
if (host.logsPollInterval != null) return; if (host.logsPollInterval != null) {return;}
host.logsPollInterval = window.setInterval(() => { host.logsPollInterval = window.setInterval(() => {
if (host.tab !== "logs") return; if (host.tab !== "logs") {return;}
void loadLogs(host as unknown as OpenClawApp, { quiet: true }); void loadLogs(host as unknown as OpenClawApp, { quiet: true });
}, 2000); }, 2000);
} }
export function stopLogsPolling(host: PollingHost) { export function stopLogsPolling(host: PollingHost) {
if (host.logsPollInterval == null) return; if (host.logsPollInterval == null) {return;}
clearInterval(host.logsPollInterval); clearInterval(host.logsPollInterval);
host.logsPollInterval = null; host.logsPollInterval = null;
} }
export function startDebugPolling(host: PollingHost) { export function startDebugPolling(host: PollingHost) {
if (host.debugPollInterval != null) return; if (host.debugPollInterval != null) {return;}
host.debugPollInterval = window.setInterval(() => { host.debugPollInterval = window.setInterval(() => {
if (host.tab !== "debug") return; if (host.tab !== "debug") {return;}
void loadDebug(host as unknown as OpenClawApp); void loadDebug(host as unknown as OpenClawApp);
}, 3000); }, 3000);
} }
export function stopDebugPolling(host: PollingHost) { export function stopDebugPolling(host: PollingHost) {
if (host.debugPollInterval == null) return; if (host.debugPollInterval == null) {return;}
clearInterval(host.debugPollInterval); clearInterval(host.debugPollInterval);
host.debugPollInterval = null; host.debugPollInterval = null;
} }

View File

@@ -134,7 +134,7 @@ export function renderChatControls(state: AppViewState) {
class="btn btn--sm btn--icon ${showThinking ? "active" : ""}" class="btn btn--sm btn--icon ${showThinking ? "active" : ""}"
?disabled=${disableThinkingToggle} ?disabled=${disableThinkingToggle}
@click=${() => { @click=${() => {
if (disableThinkingToggle) return; if (disableThinkingToggle) {return;}
state.applySettings({ state.applySettings({
...state.settings, ...state.settings,
chatShowThinking: !state.settings.chatShowThinking, chatShowThinking: !state.settings.chatShowThinking,
@@ -153,7 +153,7 @@ export function renderChatControls(state: AppViewState) {
class="btn btn--sm btn--icon ${focusActive ? "active" : ""}" class="btn btn--sm btn--icon ${focusActive ? "active" : ""}"
?disabled=${disableFocusToggle} ?disabled=${disableFocusToggle}
@click=${() => { @click=${() => {
if (disableFocusToggle) return; if (disableFocusToggle) {return;}
state.applySettings({ state.applySettings({
...state.settings, ...state.settings,
chatFocusMode: !state.settings.chatFocusMode, chatFocusMode: !state.settings.chatFocusMode,
@@ -183,18 +183,18 @@ function resolveMainSessionKey(
): string | null { ): string | null {
const snapshot = hello?.snapshot as { sessionDefaults?: SessionDefaultsSnapshot } | undefined; const snapshot = hello?.snapshot as { sessionDefaults?: SessionDefaultsSnapshot } | undefined;
const mainSessionKey = snapshot?.sessionDefaults?.mainSessionKey?.trim(); const mainSessionKey = snapshot?.sessionDefaults?.mainSessionKey?.trim();
if (mainSessionKey) return mainSessionKey; if (mainSessionKey) {return mainSessionKey;}
const mainKey = snapshot?.sessionDefaults?.mainKey?.trim(); const mainKey = snapshot?.sessionDefaults?.mainKey?.trim();
if (mainKey) return mainKey; if (mainKey) {return mainKey;}
if (sessions?.sessions?.some((row) => row.key === "main")) return "main"; if (sessions?.sessions?.some((row) => row.key === "main")) {return "main";}
return null; return null;
} }
function resolveSessionDisplayName(key: string, row?: SessionsListResult["sessions"][number]) { function resolveSessionDisplayName(key: string, row?: SessionsListResult["sessions"][number]) {
const label = row?.label?.trim(); const label = row?.label?.trim();
if (label) return `${label} (${key})`; if (label) {return `${label} (${key})`;}
const displayName = row?.displayName?.trim(); const displayName = row?.displayName?.trim();
if (displayName) return displayName; if (displayName) {return displayName;}
return key; return key;
} }

View File

@@ -98,8 +98,8 @@ function resolveAssistantAvatarUrl(state: AppViewState): string | undefined {
const agent = list.find((entry) => entry.id === agentId); const agent = list.find((entry) => entry.id === agentId);
const identity = agent?.identity; const identity = agent?.identity;
const candidate = identity?.avatarUrl ?? identity?.avatar; const candidate = identity?.avatarUrl ?? identity?.avatar;
if (!candidate) return undefined; if (!candidate) {return undefined;}
if (AVATAR_DATA_RE.test(candidate) || AVATAR_HTTP_RE.test(candidate)) return candidate; if (AVATAR_DATA_RE.test(candidate) || AVATAR_HTTP_RE.test(candidate)) {return candidate;}
return identity?.avatarUrl; return identity?.avatarUrl;
} }
@@ -486,7 +486,7 @@ export function renderApp(state: AppViewState) {
return Promise.all([loadChatHistory(state), refreshChatAvatar(state)]); return Promise.all([loadChatHistory(state), refreshChatAvatar(state)]);
}, },
onToggleFocusMode: () => { onToggleFocusMode: () => {
if (state.onboarding) return; if (state.onboarding) {return;}
state.applySettings({ state.applySettings({
...state.settings, ...state.settings,
chatFocusMode: !state.settings.chatFocusMode, chatFocusMode: !state.settings.chatFocusMode,

View File

@@ -12,7 +12,7 @@ type ScrollHost = {
}; };
export function scheduleChatScroll(host: ScrollHost, force = false) { export function scheduleChatScroll(host: ScrollHost, force = false) {
if (host.chatScrollFrame) cancelAnimationFrame(host.chatScrollFrame); if (host.chatScrollFrame) {cancelAnimationFrame(host.chatScrollFrame);}
if (host.chatScrollTimeout != null) { if (host.chatScrollTimeout != null) {
clearTimeout(host.chatScrollTimeout); clearTimeout(host.chatScrollTimeout);
host.chatScrollTimeout = null; host.chatScrollTimeout = null;
@@ -25,7 +25,7 @@ export function scheduleChatScroll(host: ScrollHost, force = false) {
overflowY === "auto" || overflowY === "auto" ||
overflowY === "scroll" || overflowY === "scroll" ||
container.scrollHeight - container.clientHeight > 1; container.scrollHeight - container.clientHeight > 1;
if (canScroll) return container; if (canScroll) {return container;}
} }
return (document.scrollingElement ?? document.documentElement) as HTMLElement | null; return (document.scrollingElement ?? document.documentElement) as HTMLElement | null;
}; };
@@ -34,22 +34,22 @@ export function scheduleChatScroll(host: ScrollHost, force = false) {
host.chatScrollFrame = requestAnimationFrame(() => { host.chatScrollFrame = requestAnimationFrame(() => {
host.chatScrollFrame = null; host.chatScrollFrame = null;
const target = pickScrollTarget(); const target = pickScrollTarget();
if (!target) return; if (!target) {return;}
const distanceFromBottom = target.scrollHeight - target.scrollTop - target.clientHeight; const distanceFromBottom = target.scrollHeight - target.scrollTop - target.clientHeight;
const shouldStick = force || host.chatUserNearBottom || distanceFromBottom < 200; const shouldStick = force || host.chatUserNearBottom || distanceFromBottom < 200;
if (!shouldStick) return; if (!shouldStick) {return;}
if (force) host.chatHasAutoScrolled = true; if (force) {host.chatHasAutoScrolled = true;}
target.scrollTop = target.scrollHeight; target.scrollTop = target.scrollHeight;
host.chatUserNearBottom = true; host.chatUserNearBottom = true;
const retryDelay = force ? 150 : 120; const retryDelay = force ? 150 : 120;
host.chatScrollTimeout = window.setTimeout(() => { host.chatScrollTimeout = window.setTimeout(() => {
host.chatScrollTimeout = null; host.chatScrollTimeout = null;
const latest = pickScrollTarget(); const latest = pickScrollTarget();
if (!latest) return; if (!latest) {return;}
const latestDistanceFromBottom = const latestDistanceFromBottom =
latest.scrollHeight - latest.scrollTop - latest.clientHeight; latest.scrollHeight - latest.scrollTop - latest.clientHeight;
const shouldStickRetry = force || host.chatUserNearBottom || latestDistanceFromBottom < 200; const shouldStickRetry = force || host.chatUserNearBottom || latestDistanceFromBottom < 200;
if (!shouldStickRetry) return; if (!shouldStickRetry) {return;}
latest.scrollTop = latest.scrollHeight; latest.scrollTop = latest.scrollHeight;
host.chatUserNearBottom = true; host.chatUserNearBottom = true;
}, retryDelay); }, retryDelay);
@@ -58,16 +58,16 @@ export function scheduleChatScroll(host: ScrollHost, force = false) {
} }
export function scheduleLogsScroll(host: ScrollHost, force = false) { export function scheduleLogsScroll(host: ScrollHost, force = false) {
if (host.logsScrollFrame) cancelAnimationFrame(host.logsScrollFrame); if (host.logsScrollFrame) {cancelAnimationFrame(host.logsScrollFrame);}
void host.updateComplete.then(() => { void host.updateComplete.then(() => {
host.logsScrollFrame = requestAnimationFrame(() => { host.logsScrollFrame = requestAnimationFrame(() => {
host.logsScrollFrame = null; host.logsScrollFrame = null;
const container = host.querySelector(".log-stream") as HTMLElement | null; const container = host.querySelector(".log-stream") as HTMLElement | null;
if (!container) return; if (!container) {return;}
const distanceFromBottom = const distanceFromBottom =
container.scrollHeight - container.scrollTop - container.clientHeight; container.scrollHeight - container.scrollTop - container.clientHeight;
const shouldStick = force || distanceFromBottom < 80; const shouldStick = force || distanceFromBottom < 80;
if (!shouldStick) return; if (!shouldStick) {return;}
container.scrollTop = container.scrollHeight; container.scrollTop = container.scrollHeight;
}); });
}); });
@@ -75,14 +75,14 @@ export function scheduleLogsScroll(host: ScrollHost, force = false) {
export function handleChatScroll(host: ScrollHost, event: Event) { export function handleChatScroll(host: ScrollHost, event: Event) {
const container = event.currentTarget as HTMLElement | null; const container = event.currentTarget as HTMLElement | null;
if (!container) return; if (!container) {return;}
const distanceFromBottom = container.scrollHeight - container.scrollTop - container.clientHeight; const distanceFromBottom = container.scrollHeight - container.scrollTop - container.clientHeight;
host.chatUserNearBottom = distanceFromBottom < 200; host.chatUserNearBottom = distanceFromBottom < 200;
} }
export function handleLogsScroll(host: ScrollHost, event: Event) { export function handleLogsScroll(host: ScrollHost, event: Event) {
const container = event.currentTarget as HTMLElement | null; const container = event.currentTarget as HTMLElement | null;
if (!container) return; if (!container) {return;}
const distanceFromBottom = container.scrollHeight - container.scrollTop - container.clientHeight; const distanceFromBottom = container.scrollHeight - container.scrollTop - container.clientHeight;
host.logsAtBottom = distanceFromBottom < 80; host.logsAtBottom = distanceFromBottom < 80;
} }
@@ -93,7 +93,7 @@ export function resetChatScroll(host: ScrollHost) {
} }
export function exportLogs(lines: string[], label: string) { export function exportLogs(lines: string[], label: string) {
if (lines.length === 0) return; if (lines.length === 0) {return;}
const blob = new Blob([`${lines.join("\n")}\n`], { type: "text/plain" }); const blob = new Blob([`${lines.join("\n")}\n`], { type: "text/plain" });
const url = URL.createObjectURL(blob); const url = URL.createObjectURL(blob);
const anchor = document.createElement("a"); const anchor = document.createElement("a");
@@ -105,9 +105,9 @@ export function exportLogs(lines: string[], label: string) {
} }
export function observeTopbar(host: ScrollHost) { export function observeTopbar(host: ScrollHost) {
if (typeof ResizeObserver === "undefined") return; if (typeof ResizeObserver === "undefined") {return;}
const topbar = host.querySelector(".topbar"); const topbar = host.querySelector(".topbar");
if (!topbar) return; if (!topbar) {return;}
const update = () => { const update = () => {
const { height } = topbar.getBoundingClientRect(); const { height } = topbar.getBoundingClientRect();
host.style.setProperty("--topbar-height", `${height}px`); host.style.setProperty("--topbar-height", `${height}px`);

View File

@@ -64,13 +64,13 @@ export function applySettings(host: SettingsHost, next: UiSettings) {
export function setLastActiveSessionKey(host: SettingsHost, next: string) { export function setLastActiveSessionKey(host: SettingsHost, next: string) {
const trimmed = next.trim(); const trimmed = next.trim();
if (!trimmed) return; if (!trimmed) {return;}
if (host.settings.lastActiveSessionKey === trimmed) return; if (host.settings.lastActiveSessionKey === trimmed) {return;}
applySettings(host, { ...host.settings, lastActiveSessionKey: trimmed }); applySettings(host, { ...host.settings, lastActiveSessionKey: trimmed });
} }
export function applySettingsFromUrl(host: SettingsHost) { export function applySettingsFromUrl(host: SettingsHost) {
if (!window.location.search) return; if (!window.location.search) {return;}
const params = new URLSearchParams(window.location.search); const params = new URLSearchParams(window.location.search);
const tokenRaw = params.get("token"); const tokenRaw = params.get("token");
const passwordRaw = params.get("password"); const passwordRaw = params.get("password");
@@ -117,20 +117,20 @@ export function applySettingsFromUrl(host: SettingsHost) {
shouldCleanUrl = true; shouldCleanUrl = true;
} }
if (!shouldCleanUrl) return; if (!shouldCleanUrl) {return;}
const url = new URL(window.location.href); const url = new URL(window.location.href);
url.search = params.toString(); url.search = params.toString();
window.history.replaceState({}, "", url.toString()); window.history.replaceState({}, "", url.toString());
} }
export function setTab(host: SettingsHost, next: Tab) { export function setTab(host: SettingsHost, next: Tab) {
if (host.tab !== next) host.tab = next; if (host.tab !== next) {host.tab = next;}
if (next === "chat") host.chatHasAutoScrolled = false; if (next === "chat") {host.chatHasAutoScrolled = false;}
if (next === "logs") startLogsPolling(host as unknown as Parameters<typeof startLogsPolling>[0]); if (next === "logs") {startLogsPolling(host as unknown as Parameters<typeof startLogsPolling>[0]);}
else stopLogsPolling(host as unknown as Parameters<typeof stopLogsPolling>[0]); else {stopLogsPolling(host as unknown as Parameters<typeof stopLogsPolling>[0]);}
if (next === "debug") if (next === "debug")
startDebugPolling(host as unknown as Parameters<typeof startDebugPolling>[0]); {startDebugPolling(host as unknown as Parameters<typeof startDebugPolling>[0]);}
else stopDebugPolling(host as unknown as Parameters<typeof stopDebugPolling>[0]); else {stopDebugPolling(host as unknown as Parameters<typeof stopDebugPolling>[0]);}
void refreshActiveTab(host); void refreshActiveTab(host);
syncUrlWithTab(host, next, false); syncUrlWithTab(host, next, false);
} }
@@ -150,12 +150,12 @@ export function setTheme(host: SettingsHost, next: ThemeMode, context?: ThemeTra
} }
export async function refreshActiveTab(host: SettingsHost) { export async function refreshActiveTab(host: SettingsHost) {
if (host.tab === "overview") await loadOverview(host); if (host.tab === "overview") {await loadOverview(host);}
if (host.tab === "channels") await loadChannelsTab(host); if (host.tab === "channels") {await loadChannelsTab(host);}
if (host.tab === "instances") await loadPresence(host as unknown as OpenClawApp); if (host.tab === "instances") {await loadPresence(host as unknown as OpenClawApp);}
if (host.tab === "sessions") await loadSessions(host as unknown as OpenClawApp); if (host.tab === "sessions") {await loadSessions(host as unknown as OpenClawApp);}
if (host.tab === "cron") await loadCron(host); if (host.tab === "cron") {await loadCron(host);}
if (host.tab === "skills") await loadSkills(host as unknown as OpenClawApp); if (host.tab === "skills") {await loadSkills(host as unknown as OpenClawApp);}
if (host.tab === "nodes") { if (host.tab === "nodes") {
await loadNodes(host as unknown as OpenClawApp); await loadNodes(host as unknown as OpenClawApp);
await loadDevices(host as unknown as OpenClawApp); await loadDevices(host as unknown as OpenClawApp);
@@ -185,7 +185,7 @@ export async function refreshActiveTab(host: SettingsHost) {
} }
export function inferBasePath() { export function inferBasePath() {
if (typeof window === "undefined") return ""; if (typeof window === "undefined") {return "";}
const configured = window.__OPENCLAW_CONTROL_UI_BASE_PATH__; const configured = window.__OPENCLAW_CONTROL_UI_BASE_PATH__;
if (typeof configured === "string" && configured.trim()) { if (typeof configured === "string" && configured.trim()) {
return normalizeBasePath(configured); return normalizeBasePath(configured);
@@ -200,17 +200,17 @@ export function syncThemeWithSettings(host: SettingsHost) {
export function applyResolvedTheme(host: SettingsHost, resolved: ResolvedTheme) { export function applyResolvedTheme(host: SettingsHost, resolved: ResolvedTheme) {
host.themeResolved = resolved; host.themeResolved = resolved;
if (typeof document === "undefined") return; if (typeof document === "undefined") {return;}
const root = document.documentElement; const root = document.documentElement;
root.dataset.theme = resolved; root.dataset.theme = resolved;
root.style.colorScheme = resolved; root.style.colorScheme = resolved;
} }
export function attachThemeListener(host: SettingsHost) { export function attachThemeListener(host: SettingsHost) {
if (typeof window === "undefined" || typeof window.matchMedia !== "function") return; if (typeof window === "undefined" || typeof window.matchMedia !== "function") {return;}
host.themeMedia = window.matchMedia("(prefers-color-scheme: dark)"); host.themeMedia = window.matchMedia("(prefers-color-scheme: dark)");
host.themeMediaHandler = (event) => { host.themeMediaHandler = (event) => {
if (host.theme !== "system") return; if (host.theme !== "system") {return;}
applyResolvedTheme(host, event.matches ? "dark" : "light"); applyResolvedTheme(host, event.matches ? "dark" : "light");
}; };
if (typeof host.themeMedia.addEventListener === "function") { if (typeof host.themeMedia.addEventListener === "function") {
@@ -224,7 +224,7 @@ export function attachThemeListener(host: SettingsHost) {
} }
export function detachThemeListener(host: SettingsHost) { export function detachThemeListener(host: SettingsHost) {
if (!host.themeMedia || !host.themeMediaHandler) return; if (!host.themeMedia || !host.themeMediaHandler) {return;}
if (typeof host.themeMedia.removeEventListener === "function") { if (typeof host.themeMedia.removeEventListener === "function") {
host.themeMedia.removeEventListener("change", host.themeMediaHandler); host.themeMedia.removeEventListener("change", host.themeMediaHandler);
return; return;
@@ -238,16 +238,16 @@ export function detachThemeListener(host: SettingsHost) {
} }
export function syncTabWithLocation(host: SettingsHost, replace: boolean) { export function syncTabWithLocation(host: SettingsHost, replace: boolean) {
if (typeof window === "undefined") return; if (typeof window === "undefined") {return;}
const resolved = tabFromPath(window.location.pathname, host.basePath) ?? "chat"; const resolved = tabFromPath(window.location.pathname, host.basePath) ?? "chat";
setTabFromRoute(host, resolved); setTabFromRoute(host, resolved);
syncUrlWithTab(host, resolved, replace); syncUrlWithTab(host, resolved, replace);
} }
export function onPopState(host: SettingsHost) { export function onPopState(host: SettingsHost) {
if (typeof window === "undefined") return; if (typeof window === "undefined") {return;}
const resolved = tabFromPath(window.location.pathname, host.basePath); const resolved = tabFromPath(window.location.pathname, host.basePath);
if (!resolved) return; if (!resolved) {return;}
const url = new URL(window.location.href); const url = new URL(window.location.href);
const session = url.searchParams.get("session")?.trim(); const session = url.searchParams.get("session")?.trim();
@@ -264,18 +264,18 @@ export function onPopState(host: SettingsHost) {
} }
export function setTabFromRoute(host: SettingsHost, next: Tab) { export function setTabFromRoute(host: SettingsHost, next: Tab) {
if (host.tab !== next) host.tab = next; if (host.tab !== next) {host.tab = next;}
if (next === "chat") host.chatHasAutoScrolled = false; if (next === "chat") {host.chatHasAutoScrolled = false;}
if (next === "logs") startLogsPolling(host as unknown as Parameters<typeof startLogsPolling>[0]); if (next === "logs") {startLogsPolling(host as unknown as Parameters<typeof startLogsPolling>[0]);}
else stopLogsPolling(host as unknown as Parameters<typeof stopLogsPolling>[0]); else {stopLogsPolling(host as unknown as Parameters<typeof stopLogsPolling>[0]);}
if (next === "debug") if (next === "debug")
startDebugPolling(host as unknown as Parameters<typeof startDebugPolling>[0]); {startDebugPolling(host as unknown as Parameters<typeof startDebugPolling>[0]);}
else stopDebugPolling(host as unknown as Parameters<typeof stopDebugPolling>[0]); else {stopDebugPolling(host as unknown as Parameters<typeof stopDebugPolling>[0]);}
if (host.connected) void refreshActiveTab(host); if (host.connected) {void refreshActiveTab(host);}
} }
export function syncUrlWithTab(host: SettingsHost, tab: Tab, replace: boolean) { export function syncUrlWithTab(host: SettingsHost, tab: Tab, replace: boolean) {
if (typeof window === "undefined") return; if (typeof window === "undefined") {return;}
const targetPath = normalizePath(pathForTab(tab, host.basePath)); const targetPath = normalizePath(pathForTab(tab, host.basePath));
const currentPath = normalizePath(window.location.pathname); const currentPath = normalizePath(window.location.pathname);
const url = new URL(window.location.href); const url = new URL(window.location.href);
@@ -298,11 +298,11 @@ export function syncUrlWithTab(host: SettingsHost, tab: Tab, replace: boolean) {
} }
export function syncUrlWithSessionKey(host: SettingsHost, sessionKey: string, replace: boolean) { export function syncUrlWithSessionKey(host: SettingsHost, sessionKey: string, replace: boolean) {
if (typeof window === "undefined") return; if (typeof window === "undefined") {return;}
const url = new URL(window.location.href); const url = new URL(window.location.href);
url.searchParams.set("session", sessionKey); url.searchParams.set("session", sessionKey);
if (replace) window.history.replaceState({}, "", url.toString()); if (replace) {window.history.replaceState({}, "", url.toString());}
else window.history.pushState({}, "", url.toString()); else {window.history.pushState({}, "", url.toString());}
} }
export async function loadOverview(host: SettingsHost) { export async function loadOverview(host: SettingsHost) {

View File

@@ -35,25 +35,25 @@ type ToolStreamHost = {
}; };
function extractToolOutputText(value: unknown): string | null { function extractToolOutputText(value: unknown): string | null {
if (!value || typeof value !== "object") return null; if (!value || typeof value !== "object") {return null;}
const record = value as Record<string, unknown>; const record = value as Record<string, unknown>;
if (typeof record.text === "string") return record.text; if (typeof record.text === "string") {return record.text;}
const content = record.content; const content = record.content;
if (!Array.isArray(content)) return null; if (!Array.isArray(content)) {return null;}
const parts = content const parts = content
.map((item) => { .map((item) => {
if (!item || typeof item !== "object") return null; if (!item || typeof item !== "object") {return null;}
const entry = item as Record<string, unknown>; const entry = item as Record<string, unknown>;
if (entry.type === "text" && typeof entry.text === "string") return entry.text; if (entry.type === "text" && typeof entry.text === "string") {return entry.text;}
return null; return null;
}) })
.filter((part): part is string => Boolean(part)); .filter((part): part is string => Boolean(part));
if (parts.length === 0) return null; if (parts.length === 0) {return null;}
return parts.join("\n"); return parts.join("\n");
} }
function formatToolOutput(value: unknown): string | null { function formatToolOutput(value: unknown): string | null {
if (value === null || value === undefined) return null; if (value === null || value === undefined) {return null;}
if (typeof value === "number" || typeof value === "boolean") { if (typeof value === "number" || typeof value === "boolean") {
return String(value); return String(value);
} }
@@ -71,7 +71,7 @@ function formatToolOutput(value: unknown): string | null {
} }
} }
const truncated = truncateText(text, TOOL_OUTPUT_CHAR_LIMIT); const truncated = truncateText(text, TOOL_OUTPUT_CHAR_LIMIT);
if (!truncated.truncated) return truncated.text; if (!truncated.truncated) {return truncated.text;}
return `${truncated.text}\n\n… truncated (${truncated.total} chars, showing first ${truncated.text.length}).`; return `${truncated.text}\n\n… truncated (${truncated.total} chars, showing first ${truncated.text.length}).`;
} }
@@ -99,10 +99,10 @@ function buildToolStreamMessage(entry: ToolStreamEntry): Record<string, unknown>
} }
function trimToolStream(host: ToolStreamHost) { function trimToolStream(host: ToolStreamHost) {
if (host.toolStreamOrder.length <= TOOL_STREAM_LIMIT) return; if (host.toolStreamOrder.length <= TOOL_STREAM_LIMIT) {return;}
const overflow = host.toolStreamOrder.length - TOOL_STREAM_LIMIT; const overflow = host.toolStreamOrder.length - TOOL_STREAM_LIMIT;
const removed = host.toolStreamOrder.splice(0, overflow); const removed = host.toolStreamOrder.splice(0, overflow);
for (const id of removed) host.toolStreamById.delete(id); for (const id of removed) {host.toolStreamById.delete(id);}
} }
function syncToolStreamMessages(host: ToolStreamHost) { function syncToolStreamMessages(host: ToolStreamHost) {
@@ -124,7 +124,7 @@ export function scheduleToolStreamSync(host: ToolStreamHost, force = false) {
flushToolStreamSync(host); flushToolStreamSync(host);
return; return;
} }
if (host.toolStreamSyncTimer != null) return; if (host.toolStreamSyncTimer != null) {return;}
host.toolStreamSyncTimer = window.setTimeout( host.toolStreamSyncTimer = window.setTimeout(
() => flushToolStreamSync(host), () => flushToolStreamSync(host),
TOOL_STREAM_THROTTLE_MS, TOOL_STREAM_THROTTLE_MS,
@@ -182,7 +182,7 @@ export function handleCompactionEvent(host: CompactionHost, payload: AgentEventP
} }
export function handleAgentEvent(host: ToolStreamHost, payload?: AgentEventPayload) { export function handleAgentEvent(host: ToolStreamHost, payload?: AgentEventPayload) {
if (!payload) return; if (!payload) {return;}
// Handle compaction events // Handle compaction events
if (payload.stream === "compaction") { if (payload.stream === "compaction") {
@@ -190,17 +190,17 @@ export function handleAgentEvent(host: ToolStreamHost, payload?: AgentEventPaylo
return; return;
} }
if (payload.stream !== "tool") return; if (payload.stream !== "tool") {return;}
const sessionKey = typeof payload.sessionKey === "string" ? payload.sessionKey : undefined; const sessionKey = typeof payload.sessionKey === "string" ? payload.sessionKey : undefined;
if (sessionKey && sessionKey !== host.sessionKey) return; if (sessionKey && sessionKey !== host.sessionKey) {return;}
// Fallback: only accept session-less events for the active run. // Fallback: only accept session-less events for the active run.
if (!sessionKey && host.chatRunId && payload.runId !== host.chatRunId) return; if (!sessionKey && host.chatRunId && payload.runId !== host.chatRunId) {return;}
if (host.chatRunId && payload.runId !== host.chatRunId) return; if (host.chatRunId && payload.runId !== host.chatRunId) {return;}
if (!host.chatRunId) return; if (!host.chatRunId) {return;}
const data = payload.data ?? {}; const data = payload.data ?? {};
const toolCallId = typeof data.toolCallId === "string" ? data.toolCallId : ""; const toolCallId = typeof data.toolCallId === "string" ? data.toolCallId : "";
if (!toolCallId) return; if (!toolCallId) {return;}
const name = typeof data.name === "string" ? data.name : "tool"; const name = typeof data.name === "string" ? data.name : "tool";
const phase = typeof data.phase === "string" ? data.phase : ""; const phase = typeof data.phase === "string" ? data.phase : "";
const args = phase === "start" ? data.args : undefined; const args = phase === "start" ? data.args : undefined;
@@ -229,8 +229,8 @@ export function handleAgentEvent(host: ToolStreamHost, payload?: AgentEventPaylo
host.toolStreamOrder.push(toolCallId); host.toolStreamOrder.push(toolCallId);
} else { } else {
entry.name = name; entry.name = name;
if (args !== undefined) entry.args = args; if (args !== undefined) {entry.args = args;}
if (output !== undefined) entry.output = output; if (output !== undefined) {entry.output = output;}
entry.updatedAt = now; entry.updatedAt = now;
} }

View File

@@ -84,10 +84,10 @@ declare global {
const injectedAssistantIdentity = resolveInjectedAssistantIdentity(); const injectedAssistantIdentity = resolveInjectedAssistantIdentity();
function resolveOnboardingMode(): boolean { function resolveOnboardingMode(): boolean {
if (!window.location.search) return false; if (!window.location.search) {return false;}
const params = new URLSearchParams(window.location.search); const params = new URLSearchParams(window.location.search);
const raw = params.get("onboarding"); const raw = params.get("onboarding");
if (!raw) return false; if (!raw) {return false;}
const normalized = raw.trim().toLowerCase(); const normalized = raw.trim().toLowerCase();
return normalized === "1" || normalized === "true" || normalized === "yes" || normalized === "on"; return normalized === "1" || normalized === "true" || normalized === "yes" || normalized === "on";
} }
@@ -406,7 +406,7 @@ export class OpenClawApp extends LitElement {
async handleExecApprovalDecision(decision: "allow-once" | "allow-always" | "deny") { async handleExecApprovalDecision(decision: "allow-once" | "allow-always" | "deny") {
const active = this.execApprovalQueue[0]; const active = this.execApprovalQueue[0];
if (!active || !this.client || this.execApprovalBusy) return; if (!active || !this.client || this.execApprovalBusy) {return;}
this.execApprovalBusy = true; this.execApprovalBusy = true;
this.execApprovalError = null; this.execApprovalError = null;
try { try {
@@ -424,7 +424,7 @@ export class OpenClawApp extends LitElement {
handleGatewayUrlConfirm() { handleGatewayUrlConfirm() {
const nextGatewayUrl = this.pendingGatewayUrl; const nextGatewayUrl = this.pendingGatewayUrl;
if (!nextGatewayUrl) return; if (!nextGatewayUrl) {return;}
this.pendingGatewayUrl = null; this.pendingGatewayUrl = null;
applySettingsInternal(this as unknown as Parameters<typeof applySettingsInternal>[0], { applySettingsInternal(this as unknown as Parameters<typeof applySettingsInternal>[0], {
...this.settings, ...this.settings,
@@ -455,7 +455,7 @@ export class OpenClawApp extends LitElement {
window.clearTimeout(this.sidebarCloseTimer); window.clearTimeout(this.sidebarCloseTimer);
} }
this.sidebarCloseTimer = window.setTimeout(() => { this.sidebarCloseTimer = window.setTimeout(() => {
if (this.sidebarOpen) return; if (this.sidebarOpen) {return;}
this.sidebarContent = null; this.sidebarContent = null;
this.sidebarError = null; this.sidebarError = null;
this.sidebarCloseTimer = null; this.sidebarCloseTimer = null;

View File

@@ -18,10 +18,10 @@ declare global {
} }
function coerceIdentityValue(value: string | undefined, maxLength: number): string | undefined { function coerceIdentityValue(value: string | undefined, maxLength: number): string | undefined {
if (typeof value !== "string") return undefined; if (typeof value !== "string") {return undefined;}
const trimmed = value.trim(); const trimmed = value.trim();
if (!trimmed) return undefined; if (!trimmed) {return undefined;}
if (trimmed.length <= maxLength) return trimmed; if (trimmed.length <= maxLength) {return trimmed;}
return trimmed.slice(0, maxLength); return trimmed.slice(0, maxLength);
} }

View File

@@ -13,7 +13,7 @@ type CopyButtonOptions = {
}; };
async function copyTextToClipboard(text: string): Promise<boolean> { async function copyTextToClipboard(text: string): Promise<boolean> {
if (!text) return false; if (!text) {return false;}
try { try {
await navigator.clipboard.writeText(text); await navigator.clipboard.writeText(text);
@@ -40,14 +40,14 @@ function createCopyButton(options: CopyButtonOptions): TemplateResult {
const btn = e.currentTarget as HTMLButtonElement | null; const btn = e.currentTarget as HTMLButtonElement | null;
const iconContainer = btn?.querySelector(".chat-copy-btn__icon") as HTMLElement | null; const iconContainer = btn?.querySelector(".chat-copy-btn__icon") as HTMLElement | null;
if (!btn || btn.dataset.copying === "1") return; if (!btn || btn.dataset.copying === "1") {return;}
btn.dataset.copying = "1"; btn.dataset.copying = "1";
btn.setAttribute("aria-busy", "true"); btn.setAttribute("aria-busy", "true");
btn.disabled = true; btn.disabled = true;
const copied = await copyTextToClipboard(options.text()); const copied = await copyTextToClipboard(options.text());
if (!btn.isConnected) return; if (!btn.isConnected) {return;}
delete btn.dataset.copying; delete btn.dataset.copying;
btn.removeAttribute("aria-busy"); btn.removeAttribute("aria-busy");
@@ -58,7 +58,7 @@ function createCopyButton(options: CopyButtonOptions): TemplateResult {
setButtonLabel(btn, ERROR_LABEL); setButtonLabel(btn, ERROR_LABEL);
window.setTimeout(() => { window.setTimeout(() => {
if (!btn.isConnected) return; if (!btn.isConnected) {return;}
delete btn.dataset.error; delete btn.dataset.error;
setButtonLabel(btn, idleLabel); setButtonLabel(btn, idleLabel);
}, ERROR_FOR_MS); }, ERROR_FOR_MS);
@@ -69,7 +69,7 @@ function createCopyButton(options: CopyButtonOptions): TemplateResult {
setButtonLabel(btn, COPIED_LABEL); setButtonLabel(btn, COPIED_LABEL);
window.setTimeout(() => { window.setTimeout(() => {
if (!btn.isConnected) return; if (!btn.isConnected) {return;}
delete btn.dataset.copied; delete btn.dataset.copied;
setButtonLabel(btn, idleLabel); setButtonLabel(btn, idleLabel);
}, COPIED_FOR_MS); }, COPIED_FOR_MS);

View File

@@ -24,14 +24,14 @@ function extractImages(message: unknown): ImageBlock[] {
if (Array.isArray(content)) { if (Array.isArray(content)) {
for (const block of content) { for (const block of content) {
if (typeof block !== "object" || block === null) continue; if (typeof block !== "object" || block === null) {continue;}
const b = block as Record<string, unknown>; const b = block as Record<string, unknown>;
if (b.type === "image") { if (b.type === "image") {
// Handle source object format (from sendChatMessage) // Handle source object format (from sendChatMessage)
const source = b.source as Record<string, unknown> | undefined; const source = b.source as Record<string, unknown> | undefined;
if (source?.type === "base64" && typeof source.data === "string") { if (source?.type === "base64" && typeof source.data === "string") {
const data = source.data as string; const data = source.data;
const mediaType = (source.media_type as string) || "image/png"; const mediaType = (source.media_type as string) || "image/png";
// If data is already a data URL, use it directly // If data is already a data URL, use it directly
const url = data.startsWith("data:") ? data : `data:${mediaType};base64,${data}`; const url = data.startsWith("data:") ? data : `data:${mediaType};base64,${data}`;
@@ -188,12 +188,12 @@ function renderAvatar(role: string, assistant?: Pick<AssistantIdentity, "name" |
function isAvatarUrl(value: string): boolean { function isAvatarUrl(value: string): boolean {
return ( return (
/^https?:\/\//i.test(value) || /^data:image\//i.test(value) || /^\//.test(value) // Relative paths from avatar endpoint /^https?:\/\//i.test(value) || /^data:image\//i.test(value) || value.startsWith('/') // Relative paths from avatar endpoint
); );
} }
function renderMessageImages(images: ImageBlock[]) { function renderMessageImages(images: ImageBlock[]) {
if (images.length === 0) return nothing; if (images.length === 0) {return nothing;}
return html` return html`
<div class="chat-message-images"> <div class="chat-message-images">
@@ -251,7 +251,7 @@ function renderGroupedMessage(
return html`${toolCards.map((card) => renderToolCardSidebar(card, onOpenSidebar))}`; return html`${toolCards.map((card) => renderToolCardSidebar(card, onOpenSidebar))}`;
} }
if (!markdown && !hasToolCards && !hasImages) return nothing; if (!markdown && !hasToolCards && !hasImages) {return nothing;}
return html` return html`
<div class="${bubbleClasses}"> <div class="${bubbleClasses}">

View File

@@ -20,16 +20,16 @@ const textCache = new WeakMap<object, string | null>();
const thinkingCache = new WeakMap<object, string | null>(); const thinkingCache = new WeakMap<object, string | null>();
function looksLikeEnvelopeHeader(header: string): boolean { function looksLikeEnvelopeHeader(header: string): boolean {
if (/\d{4}-\d{2}-\d{2}T\d{2}:\d{2}Z\b/.test(header)) return true; if (/\d{4}-\d{2}-\d{2}T\d{2}:\d{2}Z\b/.test(header)) {return true;}
if (/\d{4}-\d{2}-\d{2} \d{2}:\d{2}\b/.test(header)) return true; if (/\d{4}-\d{2}-\d{2} \d{2}:\d{2}\b/.test(header)) {return true;}
return ENVELOPE_CHANNELS.some((label) => header.startsWith(`${label} `)); return ENVELOPE_CHANNELS.some((label) => header.startsWith(`${label} `));
} }
export function stripEnvelope(text: string): string { export function stripEnvelope(text: string): string {
const match = text.match(ENVELOPE_PREFIX); const match = text.match(ENVELOPE_PREFIX);
if (!match) return text; if (!match) {return text;}
const header = match[1] ?? ""; const header = match[1] ?? "";
if (!looksLikeEnvelopeHeader(header)) return text; if (!looksLikeEnvelopeHeader(header)) {return text;}
return text.slice(match[0].length); return text.slice(match[0].length);
} }
@@ -45,7 +45,7 @@ export function extractText(message: unknown): string | null {
const parts = content const parts = content
.map((p) => { .map((p) => {
const item = p as Record<string, unknown>; const item = p as Record<string, unknown>;
if (item.type === "text" && typeof item.text === "string") return item.text; if (item.type === "text" && typeof item.text === "string") {return item.text;}
return null; return null;
}) })
.filter((v): v is string => typeof v === "string"); .filter((v): v is string => typeof v === "string");
@@ -63,9 +63,9 @@ export function extractText(message: unknown): string | null {
} }
export function extractTextCached(message: unknown): string | null { export function extractTextCached(message: unknown): string | null {
if (!message || typeof message !== "object") return extractText(message); if (!message || typeof message !== "object") {return extractText(message);}
const obj = message as object; const obj = message;
if (textCache.has(obj)) return textCache.get(obj) ?? null; if (textCache.has(obj)) {return textCache.get(obj) ?? null;}
const value = extractText(message); const value = extractText(message);
textCache.set(obj, value); textCache.set(obj, value);
return value; return value;
@@ -80,15 +80,15 @@ export function extractThinking(message: unknown): string | null {
const item = p as Record<string, unknown>; const item = p as Record<string, unknown>;
if (item.type === "thinking" && typeof item.thinking === "string") { if (item.type === "thinking" && typeof item.thinking === "string") {
const cleaned = item.thinking.trim(); const cleaned = item.thinking.trim();
if (cleaned) parts.push(cleaned); if (cleaned) {parts.push(cleaned);}
} }
} }
} }
if (parts.length > 0) return parts.join("\n"); if (parts.length > 0) {return parts.join("\n");}
// Back-compat: older logs may still have <think> tags inside text blocks. // Back-compat: older logs may still have <think> tags inside text blocks.
const rawText = extractRawText(message); const rawText = extractRawText(message);
if (!rawText) return null; if (!rawText) {return null;}
const matches = [ const matches = [
...rawText.matchAll(/<\s*think(?:ing)?\s*>([\s\S]*?)<\s*\/\s*think(?:ing)?\s*>/gi), ...rawText.matchAll(/<\s*think(?:ing)?\s*>([\s\S]*?)<\s*\/\s*think(?:ing)?\s*>/gi),
]; ];
@@ -97,9 +97,9 @@ export function extractThinking(message: unknown): string | null {
} }
export function extractThinkingCached(message: unknown): string | null { export function extractThinkingCached(message: unknown): string | null {
if (!message || typeof message !== "object") return extractThinking(message); if (!message || typeof message !== "object") {return extractThinking(message);}
const obj = message as object; const obj = message;
if (thinkingCache.has(obj)) return thinkingCache.get(obj) ?? null; if (thinkingCache.has(obj)) {return thinkingCache.get(obj) ?? null;}
const value = extractThinking(message); const value = extractThinking(message);
thinkingCache.set(obj, value); thinkingCache.set(obj, value);
return value; return value;
@@ -108,24 +108,24 @@ export function extractThinkingCached(message: unknown): string | null {
export function extractRawText(message: unknown): string | null { export function extractRawText(message: unknown): string | null {
const m = message as Record<string, unknown>; const m = message as Record<string, unknown>;
const content = m.content; const content = m.content;
if (typeof content === "string") return content; if (typeof content === "string") {return content;}
if (Array.isArray(content)) { if (Array.isArray(content)) {
const parts = content const parts = content
.map((p) => { .map((p) => {
const item = p as Record<string, unknown>; const item = p as Record<string, unknown>;
if (item.type === "text" && typeof item.text === "string") return item.text; if (item.type === "text" && typeof item.text === "string") {return item.text;}
return null; return null;
}) })
.filter((v): v is string => typeof v === "string"); .filter((v): v is string => typeof v === "string");
if (parts.length > 0) return parts.join("\n"); if (parts.length > 0) {return parts.join("\n");}
} }
if (typeof m.text === "string") return m.text; if (typeof m.text === "string") {return m.text;}
return null; return null;
} }
export function formatReasoningMarkdown(text: string): string { export function formatReasoningMarkdown(text: string): string {
const trimmed = text.trim(); const trimmed = text.trim();
if (!trimmed) return ""; if (!trimmed) {return "";}
const lines = trimmed const lines = trimmed
.split(/\r?\n/) .split(/\r?\n/)
.map((line) => line.trim()) .map((line) => line.trim())

View File

@@ -26,8 +26,8 @@ export function normalizeMessage(message: unknown): NormalizedMessage {
}); });
const hasToolName = const hasToolName =
typeof (m as Record<string, unknown>).toolName === "string" || typeof (m).toolName === "string" ||
typeof (m as Record<string, unknown>).tool_name === "string"; typeof (m).tool_name === "string";
if (hasToolId || hasToolContent || hasToolName) { if (hasToolId || hasToolContent || hasToolName) {
role = "toolResult"; role = "toolResult";
@@ -61,9 +61,9 @@ export function normalizeMessage(message: unknown): NormalizedMessage {
export function normalizeRoleForGrouping(role: string): string { export function normalizeRoleForGrouping(role: string): string {
const lower = role.toLowerCase(); const lower = role.toLowerCase();
// Preserve original casing when it's already a core role. // Preserve original casing when it's already a core role.
if (role === "user" || role === "User") return role; if (role === "user" || role === "User") {return role;}
if (role === "assistant") return "assistant"; if (role === "assistant") {return "assistant";}
if (role === "system") return "system"; if (role === "system") {return "system";}
// Keep tool-related roles distinct so the UI can style/toggle them. // Keep tool-related roles distinct so the UI can style/toggle them.
if ( if (
lower === "toolresult" || lower === "toolresult" ||

View File

@@ -28,7 +28,7 @@ export function extractToolCards(message: unknown): ToolCard[] {
for (const item of content) { for (const item of content) {
const kind = String(item.type ?? "").toLowerCase(); const kind = String(item.type ?? "").toLowerCase();
if (kind !== "toolresult" && kind !== "tool_result") continue; if (kind !== "toolresult" && kind !== "tool_result") {continue;}
const text = extractToolText(item); const text = extractToolText(item);
const name = typeof item.name === "string" ? item.name : "tool"; const name = typeof item.name === "string" ? item.name : "tool";
cards.push({ kind: "result", name, text }); cards.push({ kind: "result", name, text });
@@ -79,7 +79,7 @@ export function renderToolCardSidebar(card: ToolCard, onOpenSidebar?: (content:
@keydown=${ @keydown=${
canClick canClick
? (e: KeyboardEvent) => { ? (e: KeyboardEvent) => {
if (e.key !== "Enter" && e.key !== " ") return; if (e.key !== "Enter" && e.key !== " ") {return;}
e.preventDefault(); e.preventDefault();
handleClick?.(); handleClick?.();
} }
@@ -117,15 +117,15 @@ export function renderToolCardSidebar(card: ToolCard, onOpenSidebar?: (content:
} }
function normalizeContent(content: unknown): Array<Record<string, unknown>> { function normalizeContent(content: unknown): Array<Record<string, unknown>> {
if (!Array.isArray(content)) return []; if (!Array.isArray(content)) {return [];}
return content.filter(Boolean) as Array<Record<string, unknown>>; return content.filter(Boolean) as Array<Record<string, unknown>>;
} }
function coerceArgs(value: unknown): unknown { function coerceArgs(value: unknown): unknown {
if (typeof value !== "string") return value; if (typeof value !== "string") {return value;}
const trimmed = value.trim(); const trimmed = value.trim();
if (!trimmed) return value; if (!trimmed) {return value;}
if (!trimmed.startsWith("{") && !trimmed.startsWith("[")) return value; if (!trimmed.startsWith("{") && !trimmed.startsWith("[")) {return value;}
try { try {
return JSON.parse(trimmed); return JSON.parse(trimmed);
} catch { } catch {
@@ -134,7 +134,7 @@ function coerceArgs(value: unknown): unknown {
} }
function extractToolText(item: Record<string, unknown>): string | undefined { function extractToolText(item: Record<string, unknown>): string | undefined {
if (typeof item.text === "string") return item.text; if (typeof item.text === "string") {return item.text;}
if (typeof item.content === "string") return item.content; if (typeof item.content === "string") {return item.content;}
return undefined; return undefined;
} }

View File

@@ -74,10 +74,10 @@ export class ResizableDivider extends LitElement {
}; };
private handleMouseMove = (e: MouseEvent) => { private handleMouseMove = (e: MouseEvent) => {
if (!this.isDragging) return; if (!this.isDragging) {return;}
const container = this.parentElement; const container = this.parentElement;
if (!container) return; if (!container) {return;}
const containerWidth = container.getBoundingClientRect().width; const containerWidth = container.getBoundingClientRect().width;
const deltaX = e.clientX - this.startX; const deltaX = e.clientX - this.startX;

View File

@@ -51,9 +51,9 @@ describe("config form renderer", () => {
container, container,
); );
const tokenInput = container.querySelector("input[type='password']") as HTMLInputElement | null; const tokenInput = container.querySelector("input[type='password']");
expect(tokenInput).not.toBeNull(); expect(tokenInput).not.toBeNull();
if (!tokenInput) return; if (!tokenInput) {return;}
tokenInput.value = "abc123"; tokenInput.value = "abc123";
tokenInput.dispatchEvent(new Event("input", { bubbles: true })); tokenInput.dispatchEvent(new Event("input", { bubbles: true }));
expect(onPatch).toHaveBeenCalledWith(["gateway", "auth", "token"], "abc123"); expect(onPatch).toHaveBeenCalledWith(["gateway", "auth", "token"], "abc123");
@@ -65,9 +65,9 @@ describe("config form renderer", () => {
tokenButton?.dispatchEvent(new MouseEvent("click", { bubbles: true })); tokenButton?.dispatchEvent(new MouseEvent("click", { bubbles: true }));
expect(onPatch).toHaveBeenCalledWith(["mode"], "token"); expect(onPatch).toHaveBeenCalledWith(["mode"], "token");
const checkbox = container.querySelector("input[type='checkbox']") as HTMLInputElement | null; const checkbox = container.querySelector("input[type='checkbox']");
expect(checkbox).not.toBeNull(); expect(checkbox).not.toBeNull();
if (!checkbox) return; if (!checkbox) {return;}
checkbox.checked = true; checkbox.checked = true;
checkbox.dispatchEvent(new Event("change", { bubbles: true })); checkbox.dispatchEvent(new Event("change", { bubbles: true }));
expect(onPatch).toHaveBeenCalledWith(["enabled"], true); expect(onPatch).toHaveBeenCalledWith(["enabled"], true);
@@ -88,14 +88,14 @@ describe("config form renderer", () => {
container, container,
); );
const addButton = container.querySelector(".cfg-array__add") as HTMLButtonElement | null; const addButton = container.querySelector(".cfg-array__add");
expect(addButton).not.toBeUndefined(); expect(addButton).not.toBeUndefined();
addButton?.dispatchEvent(new MouseEvent("click", { bubbles: true })); addButton?.dispatchEvent(new MouseEvent("click", { bubbles: true }));
expect(onPatch).toHaveBeenCalledWith(["allowFrom"], ["+1", ""]); expect(onPatch).toHaveBeenCalledWith(["allowFrom"], ["+1", ""]);
const removeButton = container.querySelector( const removeButton = container.querySelector(
".cfg-array__item-remove", ".cfg-array__item-remove",
) as HTMLButtonElement | null; );
expect(removeButton).not.toBeUndefined(); expect(removeButton).not.toBeUndefined();
removeButton?.dispatchEvent(new MouseEvent("click", { bubbles: true })); removeButton?.dispatchEvent(new MouseEvent("click", { bubbles: true }));
expect(onPatch).toHaveBeenCalledWith(["allowFrom"], []); expect(onPatch).toHaveBeenCalledWith(["allowFrom"], []);
@@ -152,7 +152,7 @@ describe("config form renderer", () => {
const removeButton = container.querySelector( const removeButton = container.querySelector(
".cfg-map__item-remove", ".cfg-map__item-remove",
) as HTMLButtonElement | null; );
expect(removeButton).not.toBeUndefined(); expect(removeButton).not.toBeUndefined();
removeButton?.dispatchEvent(new MouseEvent("click", { bubbles: true })); removeButton?.dispatchEvent(new MouseEvent("click", { bubbles: true }));
expect(onPatch).toHaveBeenCalledWith(["slack"], {}); expect(onPatch).toHaveBeenCalledWith(["slack"], {});

View File

@@ -10,13 +10,13 @@ export type AgentsState = {
}; };
export async function loadAgents(state: AgentsState) { export async function loadAgents(state: AgentsState) {
if (!state.client || !state.connected) return; if (!state.client || !state.connected) {return;}
if (state.agentsLoading) return; if (state.agentsLoading) {return;}
state.agentsLoading = true; state.agentsLoading = true;
state.agentsError = null; state.agentsError = null;
try { try {
const res = (await state.client.request("agents.list", {})) as AgentsListResult | undefined; const res = (await state.client.request("agents.list", {}));
if (res) state.agentsList = res; if (res) {state.agentsList = res;}
} catch (err) { } catch (err) {
state.agentsError = String(err); state.agentsError = String(err);
} finally { } finally {

View File

@@ -14,14 +14,12 @@ export async function loadAssistantIdentity(
state: AssistantIdentityState, state: AssistantIdentityState,
opts?: { sessionKey?: string }, opts?: { sessionKey?: string },
) { ) {
if (!state.client || !state.connected) return; if (!state.client || !state.connected) {return;}
const sessionKey = opts?.sessionKey?.trim() || state.sessionKey.trim(); const sessionKey = opts?.sessionKey?.trim() || state.sessionKey.trim();
const params = sessionKey ? { sessionKey } : {}; const params = sessionKey ? { sessionKey } : {};
try { try {
const res = (await state.client.request("agent.identity.get", params)) as const res = (await state.client.request("agent.identity.get", params));
| Partial<AssistantIdentity> if (!res) {return;}
| undefined;
if (!res) return;
const normalized = normalizeAssistantIdentity(res); const normalized = normalizeAssistantIdentity(res);
state.assistantName = normalized.name; state.assistantName = normalized.name;
state.assistantAvatar = normalized.avatar; state.assistantAvatar = normalized.avatar;

View File

@@ -4,15 +4,15 @@ import type { ChannelsState } from "./channels.types";
export type { ChannelsState }; export type { ChannelsState };
export async function loadChannels(state: ChannelsState, probe: boolean) { export async function loadChannels(state: ChannelsState, probe: boolean) {
if (!state.client || !state.connected) return; if (!state.client || !state.connected) {return;}
if (state.channelsLoading) return; if (state.channelsLoading) {return;}
state.channelsLoading = true; state.channelsLoading = true;
state.channelsError = null; state.channelsError = null;
try { try {
const res = (await state.client.request("channels.status", { const res = (await state.client.request("channels.status", {
probe, probe,
timeoutMs: 8000, timeoutMs: 8000,
})) as ChannelsStatusSnapshot; }));
state.channelsSnapshot = res; state.channelsSnapshot = res;
state.channelsLastSuccess = Date.now(); state.channelsLastSuccess = Date.now();
} catch (err) { } catch (err) {
@@ -23,13 +23,13 @@ export async function loadChannels(state: ChannelsState, probe: boolean) {
} }
export async function startWhatsAppLogin(state: ChannelsState, force: boolean) { export async function startWhatsAppLogin(state: ChannelsState, force: boolean) {
if (!state.client || !state.connected || state.whatsappBusy) return; if (!state.client || !state.connected || state.whatsappBusy) {return;}
state.whatsappBusy = true; state.whatsappBusy = true;
try { try {
const res = (await state.client.request("web.login.start", { const res = (await state.client.request("web.login.start", {
force, force,
timeoutMs: 30000, timeoutMs: 30000,
})) as { message?: string; qrDataUrl?: string }; }));
state.whatsappLoginMessage = res.message ?? null; state.whatsappLoginMessage = res.message ?? null;
state.whatsappLoginQrDataUrl = res.qrDataUrl ?? null; state.whatsappLoginQrDataUrl = res.qrDataUrl ?? null;
state.whatsappLoginConnected = null; state.whatsappLoginConnected = null;
@@ -43,15 +43,15 @@ export async function startWhatsAppLogin(state: ChannelsState, force: boolean) {
} }
export async function waitWhatsAppLogin(state: ChannelsState) { export async function waitWhatsAppLogin(state: ChannelsState) {
if (!state.client || !state.connected || state.whatsappBusy) return; if (!state.client || !state.connected || state.whatsappBusy) {return;}
state.whatsappBusy = true; state.whatsappBusy = true;
try { try {
const res = (await state.client.request("web.login.wait", { const res = (await state.client.request("web.login.wait", {
timeoutMs: 120000, timeoutMs: 120000,
})) as { connected?: boolean; message?: string }; }));
state.whatsappLoginMessage = res.message ?? null; state.whatsappLoginMessage = res.message ?? null;
state.whatsappLoginConnected = res.connected ?? null; state.whatsappLoginConnected = res.connected ?? null;
if (res.connected) state.whatsappLoginQrDataUrl = null; if (res.connected) {state.whatsappLoginQrDataUrl = null;}
} catch (err) { } catch (err) {
state.whatsappLoginMessage = String(err); state.whatsappLoginMessage = String(err);
state.whatsappLoginConnected = null; state.whatsappLoginConnected = null;
@@ -61,7 +61,7 @@ export async function waitWhatsAppLogin(state: ChannelsState) {
} }
export async function logoutWhatsApp(state: ChannelsState) { export async function logoutWhatsApp(state: ChannelsState) {
if (!state.client || !state.connected || state.whatsappBusy) return; if (!state.client || !state.connected || state.whatsappBusy) {return;}
state.whatsappBusy = true; state.whatsappBusy = true;
try { try {
await state.client.request("channels.logout", { channel: "whatsapp" }); await state.client.request("channels.logout", { channel: "whatsapp" });

View File

@@ -28,14 +28,14 @@ export type ChatEventPayload = {
}; };
export async function loadChatHistory(state: ChatState) { export async function loadChatHistory(state: ChatState) {
if (!state.client || !state.connected) return; if (!state.client || !state.connected) {return;}
state.chatLoading = true; state.chatLoading = true;
state.lastError = null; state.lastError = null;
try { try {
const res = (await state.client.request("chat.history", { const res = (await state.client.request("chat.history", {
sessionKey: state.sessionKey, sessionKey: state.sessionKey,
limit: 200, limit: 200,
})) as { messages?: unknown[]; thinkingLevel?: string | null }; }));
state.chatMessages = Array.isArray(res.messages) ? res.messages : []; state.chatMessages = Array.isArray(res.messages) ? res.messages : [];
state.chatThinkingLevel = res.thinkingLevel ?? null; state.chatThinkingLevel = res.thinkingLevel ?? null;
} catch (err) { } catch (err) {
@@ -47,7 +47,7 @@ export async function loadChatHistory(state: ChatState) {
function dataUrlToBase64(dataUrl: string): { content: string; mimeType: string } | null { function dataUrlToBase64(dataUrl: string): { content: string; mimeType: string } | null {
const match = /^data:([^;]+);base64,(.+)$/.exec(dataUrl); const match = /^data:([^;]+);base64,(.+)$/.exec(dataUrl);
if (!match) return null; if (!match) {return null;}
return { mimeType: match[1], content: match[2] }; return { mimeType: match[1], content: match[2] };
} }
@@ -56,10 +56,10 @@ export async function sendChatMessage(
message: string, message: string,
attachments?: ChatAttachment[], attachments?: ChatAttachment[],
): Promise<string | null> { ): Promise<string | null> {
if (!state.client || !state.connected) return null; if (!state.client || !state.connected) {return null;}
const msg = message.trim(); const msg = message.trim();
const hasAttachments = attachments && attachments.length > 0; const hasAttachments = attachments && attachments.length > 0;
if (!msg && !hasAttachments) return null; if (!msg && !hasAttachments) {return null;}
const now = Date.now(); const now = Date.now();
@@ -99,7 +99,7 @@ export async function sendChatMessage(
? attachments ? attachments
.map((att) => { .map((att) => {
const parsed = dataUrlToBase64(att.dataUrl); const parsed = dataUrlToBase64(att.dataUrl);
if (!parsed) return null; if (!parsed) {return null;}
return { return {
type: "image", type: "image",
mimeType: parsed.mimeType, mimeType: parsed.mimeType,
@@ -139,7 +139,7 @@ export async function sendChatMessage(
} }
export async function abortChatRun(state: ChatState): Promise<boolean> { export async function abortChatRun(state: ChatState): Promise<boolean> {
if (!state.client || !state.connected) return false; if (!state.client || !state.connected) {return false;}
const runId = state.chatRunId; const runId = state.chatRunId;
try { try {
await state.client.request( await state.client.request(
@@ -154,13 +154,13 @@ export async function abortChatRun(state: ChatState): Promise<boolean> {
} }
export function handleChatEvent(state: ChatState, payload?: ChatEventPayload) { export function handleChatEvent(state: ChatState, payload?: ChatEventPayload) {
if (!payload) return null; if (!payload) {return null;}
if (payload.sessionKey !== state.sessionKey) return null; if (payload.sessionKey !== state.sessionKey) {return null;}
// Final from another run (e.g. sub-agent announce): refresh history to show new message. // Final from another run (e.g. sub-agent announce): refresh history to show new message.
// See https://github.com/openclaw/openclaw/issues/1909 // See https://github.com/openclaw/openclaw/issues/1909
if (payload.runId && state.chatRunId && payload.runId !== state.chatRunId) { if (payload.runId && state.chatRunId && payload.runId !== state.chatRunId) {
if (payload.state === "final") return "final"; if (payload.state === "final") {return "final";}
return null; return null;
} }

View File

@@ -35,11 +35,11 @@ export type ConfigState = {
}; };
export async function loadConfig(state: ConfigState) { export async function loadConfig(state: ConfigState) {
if (!state.client || !state.connected) return; if (!state.client || !state.connected) {return;}
state.configLoading = true; state.configLoading = true;
state.lastError = null; state.lastError = null;
try { try {
const res = (await state.client.request("config.get", {})) as ConfigSnapshot; const res = (await state.client.request("config.get", {}));
applyConfigSnapshot(state, res); applyConfigSnapshot(state, res);
} catch (err) { } catch (err) {
state.lastError = String(err); state.lastError = String(err);
@@ -49,11 +49,11 @@ export async function loadConfig(state: ConfigState) {
} }
export async function loadConfigSchema(state: ConfigState) { export async function loadConfigSchema(state: ConfigState) {
if (!state.client || !state.connected) return; if (!state.client || !state.connected) {return;}
if (state.configSchemaLoading) return; if (state.configSchemaLoading) {return;}
state.configSchemaLoading = true; state.configSchemaLoading = true;
try { try {
const res = (await state.client.request("config.schema", {})) as ConfigSchemaResponse; const res = (await state.client.request("config.schema", {}));
applyConfigSchema(state, res); applyConfigSchema(state, res);
} catch (err) { } catch (err) {
state.lastError = String(err); state.lastError = String(err);
@@ -74,7 +74,7 @@ export function applyConfigSnapshot(state: ConfigState, snapshot: ConfigSnapshot
typeof snapshot.raw === "string" typeof snapshot.raw === "string"
? snapshot.raw ? snapshot.raw
: snapshot.config && typeof snapshot.config === "object" : snapshot.config && typeof snapshot.config === "object"
? serializeConfigForm(snapshot.config as Record<string, unknown>) ? serializeConfigForm(snapshot.config)
: state.configRaw; : state.configRaw;
if (!state.configFormDirty || state.configFormMode === "raw") { if (!state.configFormDirty || state.configFormMode === "raw") {
state.configRaw = rawFromSnapshot; state.configRaw = rawFromSnapshot;
@@ -94,7 +94,7 @@ export function applyConfigSnapshot(state: ConfigState, snapshot: ConfigSnapshot
} }
export async function saveConfig(state: ConfigState) { export async function saveConfig(state: ConfigState) {
if (!state.client || !state.connected) return; if (!state.client || !state.connected) {return;}
state.configSaving = true; state.configSaving = true;
state.lastError = null; state.lastError = null;
try { try {
@@ -118,7 +118,7 @@ export async function saveConfig(state: ConfigState) {
} }
export async function applyConfig(state: ConfigState) { export async function applyConfig(state: ConfigState) {
if (!state.client || !state.connected) return; if (!state.client || !state.connected) {return;}
state.configApplying = true; state.configApplying = true;
state.lastError = null; state.lastError = null;
try { try {
@@ -146,7 +146,7 @@ export async function applyConfig(state: ConfigState) {
} }
export async function runUpdate(state: ConfigState) { export async function runUpdate(state: ConfigState) {
if (!state.client || !state.connected) return; if (!state.client || !state.connected) {return;}
state.updateRunning = true; state.updateRunning = true;
state.lastError = null; state.lastError = null;
try { try {

View File

@@ -14,19 +14,19 @@ export function setPathValue(
path: Array<string | number>, path: Array<string | number>,
value: unknown, value: unknown,
) { ) {
if (path.length === 0) return; if (path.length === 0) {return;}
let current: Record<string, unknown> | unknown[] = obj; let current: Record<string, unknown> | unknown[] = obj;
for (let i = 0; i < path.length - 1; i += 1) { for (let i = 0; i < path.length - 1; i += 1) {
const key = path[i]; const key = path[i];
const nextKey = path[i + 1]; const nextKey = path[i + 1];
if (typeof key === "number") { if (typeof key === "number") {
if (!Array.isArray(current)) return; if (!Array.isArray(current)) {return;}
if (current[key] == null) { if (current[key] == null) {
current[key] = typeof nextKey === "number" ? [] : ({} as Record<string, unknown>); current[key] = typeof nextKey === "number" ? [] : ({} as Record<string, unknown>);
} }
current = current[key] as Record<string, unknown> | unknown[]; current = current[key] as Record<string, unknown> | unknown[];
} else { } else {
if (typeof current !== "object" || current == null) return; if (typeof current !== "object" || current == null) {return;}
const record = current as Record<string, unknown>; const record = current as Record<string, unknown>;
if (record[key] == null) { if (record[key] == null) {
record[key] = typeof nextKey === "number" ? [] : ({} as Record<string, unknown>); record[key] = typeof nextKey === "number" ? [] : ({} as Record<string, unknown>);
@@ -36,7 +36,7 @@ export function setPathValue(
} }
const lastKey = path[path.length - 1]; const lastKey = path[path.length - 1];
if (typeof lastKey === "number") { if (typeof lastKey === "number") {
if (Array.isArray(current)) current[lastKey] = value; if (Array.isArray(current)) {current[lastKey] = value;}
return; return;
} }
if (typeof current === "object" && current != null) { if (typeof current === "object" && current != null) {
@@ -48,22 +48,22 @@ export function removePathValue(
obj: Record<string, unknown> | unknown[], obj: Record<string, unknown> | unknown[],
path: Array<string | number>, path: Array<string | number>,
) { ) {
if (path.length === 0) return; if (path.length === 0) {return;}
let current: Record<string, unknown> | unknown[] = obj; let current: Record<string, unknown> | unknown[] = obj;
for (let i = 0; i < path.length - 1; i += 1) { for (let i = 0; i < path.length - 1; i += 1) {
const key = path[i]; const key = path[i];
if (typeof key === "number") { if (typeof key === "number") {
if (!Array.isArray(current)) return; if (!Array.isArray(current)) {return;}
current = current[key] as Record<string, unknown> | unknown[]; current = current[key] as Record<string, unknown> | unknown[];
} else { } else {
if (typeof current !== "object" || current == null) return; if (typeof current !== "object" || current == null) {return;}
current = (current as Record<string, unknown>)[key] as Record<string, unknown> | unknown[]; current = (current as Record<string, unknown>)[key] as Record<string, unknown> | unknown[];
} }
if (current == null) return; if (current == null) {return;}
} }
const lastKey = path[path.length - 1]; const lastKey = path[path.length - 1];
if (typeof lastKey === "number") { if (typeof lastKey === "number") {
if (Array.isArray(current)) current.splice(lastKey, 1); if (Array.isArray(current)) {current.splice(lastKey, 1);}
return; return;
} }
if (typeof current === "object" && current != null) { if (typeof current === "object" && current != null) {

View File

@@ -17,9 +17,9 @@ export type CronState = {
}; };
export async function loadCronStatus(state: CronState) { export async function loadCronStatus(state: CronState) {
if (!state.client || !state.connected) return; if (!state.client || !state.connected) {return;}
try { try {
const res = (await state.client.request("cron.status", {})) as CronStatus; const res = (await state.client.request("cron.status", {}));
state.cronStatus = res; state.cronStatus = res;
} catch (err) { } catch (err) {
state.cronError = String(err); state.cronError = String(err);
@@ -27,14 +27,14 @@ export async function loadCronStatus(state: CronState) {
} }
export async function loadCronJobs(state: CronState) { export async function loadCronJobs(state: CronState) {
if (!state.client || !state.connected) return; if (!state.client || !state.connected) {return;}
if (state.cronLoading) return; if (state.cronLoading) {return;}
state.cronLoading = true; state.cronLoading = true;
state.cronError = null; state.cronError = null;
try { try {
const res = (await state.client.request("cron.list", { const res = (await state.client.request("cron.list", {
includeDisabled: true, includeDisabled: true,
})) as { jobs?: CronJob[] }; }));
state.cronJobs = Array.isArray(res.jobs) ? res.jobs : []; state.cronJobs = Array.isArray(res.jobs) ? res.jobs : [];
} catch (err) { } catch (err) {
state.cronError = String(err); state.cronError = String(err);
@@ -46,29 +46,29 @@ export async function loadCronJobs(state: CronState) {
export function buildCronSchedule(form: CronFormState) { export function buildCronSchedule(form: CronFormState) {
if (form.scheduleKind === "at") { if (form.scheduleKind === "at") {
const ms = Date.parse(form.scheduleAt); const ms = Date.parse(form.scheduleAt);
if (!Number.isFinite(ms)) throw new Error("Invalid run time."); if (!Number.isFinite(ms)) {throw new Error("Invalid run time.");}
return { kind: "at" as const, atMs: ms }; return { kind: "at" as const, atMs: ms };
} }
if (form.scheduleKind === "every") { if (form.scheduleKind === "every") {
const amount = toNumber(form.everyAmount, 0); const amount = toNumber(form.everyAmount, 0);
if (amount <= 0) throw new Error("Invalid interval amount."); if (amount <= 0) {throw new Error("Invalid interval amount.");}
const unit = form.everyUnit; const unit = form.everyUnit;
const mult = unit === "minutes" ? 60_000 : unit === "hours" ? 3_600_000 : 86_400_000; const mult = unit === "minutes" ? 60_000 : unit === "hours" ? 3_600_000 : 86_400_000;
return { kind: "every" as const, everyMs: amount * mult }; return { kind: "every" as const, everyMs: amount * mult };
} }
const expr = form.cronExpr.trim(); const expr = form.cronExpr.trim();
if (!expr) throw new Error("Cron expression required."); if (!expr) {throw new Error("Cron expression required.");}
return { kind: "cron" as const, expr, tz: form.cronTz.trim() || undefined }; return { kind: "cron" as const, expr, tz: form.cronTz.trim() || undefined };
} }
export function buildCronPayload(form: CronFormState) { export function buildCronPayload(form: CronFormState) {
if (form.payloadKind === "systemEvent") { if (form.payloadKind === "systemEvent") {
const text = form.payloadText.trim(); const text = form.payloadText.trim();
if (!text) throw new Error("System event text required."); if (!text) {throw new Error("System event text required.");}
return { kind: "systemEvent" as const, text }; return { kind: "systemEvent" as const, text };
} }
const message = form.payloadText.trim(); const message = form.payloadText.trim();
if (!message) throw new Error("Agent message required."); if (!message) {throw new Error("Agent message required.");}
const payload: { const payload: {
kind: "agentTurn"; kind: "agentTurn";
message: string; message: string;
@@ -77,16 +77,16 @@ export function buildCronPayload(form: CronFormState) {
to?: string; to?: string;
timeoutSeconds?: number; timeoutSeconds?: number;
} = { kind: "agentTurn", message }; } = { kind: "agentTurn", message };
if (form.deliver) payload.deliver = true; if (form.deliver) {payload.deliver = true;}
if (form.channel) payload.channel = form.channel; if (form.channel) {payload.channel = form.channel;}
if (form.to.trim()) payload.to = form.to.trim(); if (form.to.trim()) {payload.to = form.to.trim();}
const timeoutSeconds = toNumber(form.timeoutSeconds, 0); const timeoutSeconds = toNumber(form.timeoutSeconds, 0);
if (timeoutSeconds > 0) payload.timeoutSeconds = timeoutSeconds; if (timeoutSeconds > 0) {payload.timeoutSeconds = timeoutSeconds;}
return payload; return payload;
} }
export async function addCronJob(state: CronState) { export async function addCronJob(state: CronState) {
if (!state.client || !state.connected || state.cronBusy) return; if (!state.client || !state.connected || state.cronBusy) {return;}
state.cronBusy = true; state.cronBusy = true;
state.cronError = null; state.cronError = null;
try { try {
@@ -107,7 +107,7 @@ export async function addCronJob(state: CronState) {
? { postToMainPrefix: state.cronForm.postToMainPrefix.trim() } ? { postToMainPrefix: state.cronForm.postToMainPrefix.trim() }
: undefined, : undefined,
}; };
if (!job.name) throw new Error("Name required."); if (!job.name) {throw new Error("Name required.");}
await state.client.request("cron.add", job); await state.client.request("cron.add", job);
state.cronForm = { state.cronForm = {
...state.cronForm, ...state.cronForm,
@@ -125,7 +125,7 @@ export async function addCronJob(state: CronState) {
} }
export async function toggleCronJob(state: CronState, job: CronJob, enabled: boolean) { export async function toggleCronJob(state: CronState, job: CronJob, enabled: boolean) {
if (!state.client || !state.connected || state.cronBusy) return; if (!state.client || !state.connected || state.cronBusy) {return;}
state.cronBusy = true; state.cronBusy = true;
state.cronError = null; state.cronError = null;
try { try {
@@ -140,7 +140,7 @@ export async function toggleCronJob(state: CronState, job: CronJob, enabled: boo
} }
export async function runCronJob(state: CronState, job: CronJob) { export async function runCronJob(state: CronState, job: CronJob) {
if (!state.client || !state.connected || state.cronBusy) return; if (!state.client || !state.connected || state.cronBusy) {return;}
state.cronBusy = true; state.cronBusy = true;
state.cronError = null; state.cronError = null;
try { try {
@@ -154,7 +154,7 @@ export async function runCronJob(state: CronState, job: CronJob) {
} }
export async function removeCronJob(state: CronState, job: CronJob) { export async function removeCronJob(state: CronState, job: CronJob) {
if (!state.client || !state.connected || state.cronBusy) return; if (!state.client || !state.connected || state.cronBusy) {return;}
state.cronBusy = true; state.cronBusy = true;
state.cronError = null; state.cronError = null;
try { try {
@@ -173,12 +173,12 @@ export async function removeCronJob(state: CronState, job: CronJob) {
} }
export async function loadCronRuns(state: CronState, jobId: string) { export async function loadCronRuns(state: CronState, jobId: string) {
if (!state.client || !state.connected) return; if (!state.client || !state.connected) {return;}
try { try {
const res = (await state.client.request("cron.runs", { const res = (await state.client.request("cron.runs", {
id: jobId, id: jobId,
limit: 50, limit: 50,
})) as { entries?: CronRunLogEntry[] }; }));
state.cronRunsJobId = jobId; state.cronRunsJobId = jobId;
state.cronRuns = Array.isArray(res.entries) ? res.entries : []; state.cronRuns = Array.isArray(res.entries) ? res.entries : [];
} catch (err) { } catch (err) {

View File

@@ -16,8 +16,8 @@ export type DebugState = {
}; };
export async function loadDebug(state: DebugState) { export async function loadDebug(state: DebugState) {
if (!state.client || !state.connected) return; if (!state.client || !state.connected) {return;}
if (state.debugLoading) return; if (state.debugLoading) {return;}
state.debugLoading = true; state.debugLoading = true;
try { try {
const [status, health, models, heartbeat] = await Promise.all([ const [status, health, models, heartbeat] = await Promise.all([
@@ -30,7 +30,7 @@ export async function loadDebug(state: DebugState) {
state.debugHealth = health as HealthSnapshot; state.debugHealth = health as HealthSnapshot;
const modelPayload = models as { models?: unknown[] } | undefined; const modelPayload = models as { models?: unknown[] } | undefined;
state.debugModels = Array.isArray(modelPayload?.models) ? modelPayload?.models : []; state.debugModels = Array.isArray(modelPayload?.models) ? modelPayload?.models : [];
state.debugHeartbeat = heartbeat as unknown; state.debugHeartbeat = heartbeat;
} catch (err) { } catch (err) {
state.debugCallError = String(err); state.debugCallError = String(err);
} finally { } finally {
@@ -39,7 +39,7 @@ export async function loadDebug(state: DebugState) {
} }
export async function callDebugMethod(state: DebugState) { export async function callDebugMethod(state: DebugState) {
if (!state.client || !state.connected) return; if (!state.client || !state.connected) {return;}
state.debugCallError = null; state.debugCallError = null;
state.debugCallResult = null; state.debugCallResult = null;
try { try {

View File

@@ -46,25 +46,25 @@ export type DevicesState = {
}; };
export async function loadDevices(state: DevicesState, opts?: { quiet?: boolean }) { export async function loadDevices(state: DevicesState, opts?: { quiet?: boolean }) {
if (!state.client || !state.connected) return; if (!state.client || !state.connected) {return;}
if (state.devicesLoading) return; if (state.devicesLoading) {return;}
state.devicesLoading = true; state.devicesLoading = true;
if (!opts?.quiet) state.devicesError = null; if (!opts?.quiet) {state.devicesError = null;}
try { try {
const res = (await state.client.request("device.pair.list", {})) as DevicePairingList | null; const res = (await state.client.request("device.pair.list", {}));
state.devicesList = { state.devicesList = {
pending: Array.isArray(res?.pending) ? res!.pending : [], pending: Array.isArray(res?.pending) ? res.pending : [],
paired: Array.isArray(res?.paired) ? res!.paired : [], paired: Array.isArray(res?.paired) ? res.paired : [],
}; };
} catch (err) { } catch (err) {
if (!opts?.quiet) state.devicesError = String(err); if (!opts?.quiet) {state.devicesError = String(err);}
} finally { } finally {
state.devicesLoading = false; state.devicesLoading = false;
} }
} }
export async function approveDevicePairing(state: DevicesState, requestId: string) { export async function approveDevicePairing(state: DevicesState, requestId: string) {
if (!state.client || !state.connected) return; if (!state.client || !state.connected) {return;}
try { try {
await state.client.request("device.pair.approve", { requestId }); await state.client.request("device.pair.approve", { requestId });
await loadDevices(state); await loadDevices(state);
@@ -74,9 +74,9 @@ export async function approveDevicePairing(state: DevicesState, requestId: strin
} }
export async function rejectDevicePairing(state: DevicesState, requestId: string) { export async function rejectDevicePairing(state: DevicesState, requestId: string) {
if (!state.client || !state.connected) return; if (!state.client || !state.connected) {return;}
const confirmed = window.confirm("Reject this device pairing request?"); const confirmed = window.confirm("Reject this device pairing request?");
if (!confirmed) return; if (!confirmed) {return;}
try { try {
await state.client.request("device.pair.reject", { requestId }); await state.client.request("device.pair.reject", { requestId });
await loadDevices(state); await loadDevices(state);
@@ -89,11 +89,9 @@ export async function rotateDeviceToken(
state: DevicesState, state: DevicesState,
params: { deviceId: string; role: string; scopes?: string[] }, params: { deviceId: string; role: string; scopes?: string[] },
) { ) {
if (!state.client || !state.connected) return; if (!state.client || !state.connected) {return;}
try { try {
const res = (await state.client.request("device.token.rotate", params)) as const res = (await state.client.request("device.token.rotate", params));
| { token?: string; role?: string; deviceId?: string; scopes?: string[] }
| undefined;
if (res?.token) { if (res?.token) {
const identity = await loadOrCreateDeviceIdentity(); const identity = await loadOrCreateDeviceIdentity();
const role = res.role ?? params.role; const role = res.role ?? params.role;
@@ -117,9 +115,9 @@ export async function revokeDeviceToken(
state: DevicesState, state: DevicesState,
params: { deviceId: string; role: string }, params: { deviceId: string; role: string },
) { ) {
if (!state.client || !state.connected) return; if (!state.client || !state.connected) {return;}
const confirmed = window.confirm(`Revoke token for ${params.deviceId} (${params.role})?`); const confirmed = window.confirm(`Revoke token for ${params.deviceId} (${params.role})?`);
if (!confirmed) return; if (!confirmed) {return;}
try { try {
await state.client.request("device.token.revoke", params); await state.client.request("device.token.revoke", params);
const identity = await loadOrCreateDeviceIdentity(); const identity = await loadOrCreateDeviceIdentity();

View File

@@ -28,15 +28,15 @@ function isRecord(value: unknown): value is Record<string, unknown> {
} }
export function parseExecApprovalRequested(payload: unknown): ExecApprovalRequest | null { export function parseExecApprovalRequested(payload: unknown): ExecApprovalRequest | null {
if (!isRecord(payload)) return null; if (!isRecord(payload)) {return null;}
const id = typeof payload.id === "string" ? payload.id.trim() : ""; const id = typeof payload.id === "string" ? payload.id.trim() : "";
const request = payload.request; const request = payload.request;
if (!id || !isRecord(request)) return null; if (!id || !isRecord(request)) {return null;}
const command = typeof request.command === "string" ? request.command.trim() : ""; const command = typeof request.command === "string" ? request.command.trim() : "";
if (!command) return null; if (!command) {return null;}
const createdAtMs = typeof payload.createdAtMs === "number" ? payload.createdAtMs : 0; const createdAtMs = typeof payload.createdAtMs === "number" ? payload.createdAtMs : 0;
const expiresAtMs = typeof payload.expiresAtMs === "number" ? payload.expiresAtMs : 0; const expiresAtMs = typeof payload.expiresAtMs === "number" ? payload.expiresAtMs : 0;
if (!createdAtMs || !expiresAtMs) return null; if (!createdAtMs || !expiresAtMs) {return null;}
return { return {
id, id,
request: { request: {
@@ -55,9 +55,9 @@ export function parseExecApprovalRequested(payload: unknown): ExecApprovalReques
} }
export function parseExecApprovalResolved(payload: unknown): ExecApprovalResolved | null { export function parseExecApprovalResolved(payload: unknown): ExecApprovalResolved | null {
if (!isRecord(payload)) return null; if (!isRecord(payload)) {return null;}
const id = typeof payload.id === "string" ? payload.id.trim() : ""; const id = typeof payload.id === "string" ? payload.id.trim() : "";
if (!id) return null; if (!id) {return null;}
return { return {
id, id,
decision: typeof payload.decision === "string" ? payload.decision : null, decision: typeof payload.decision === "string" ? payload.decision : null,

View File

@@ -56,7 +56,7 @@ function resolveExecApprovalsRpc(target?: ExecApprovalsTarget | null): {
return { method: "exec.approvals.get", params: {} }; return { method: "exec.approvals.get", params: {} };
} }
const nodeId = target.nodeId.trim(); const nodeId = target.nodeId.trim();
if (!nodeId) return null; if (!nodeId) {return null;}
return { method: "exec.approvals.node.get", params: { nodeId } }; return { method: "exec.approvals.node.get", params: { nodeId } };
} }
@@ -68,7 +68,7 @@ function resolveExecApprovalsSaveRpc(
return { method: "exec.approvals.set", params }; return { method: "exec.approvals.set", params };
} }
const nodeId = target.nodeId.trim(); const nodeId = target.nodeId.trim();
if (!nodeId) return null; if (!nodeId) {return null;}
return { method: "exec.approvals.node.set", params: { ...params, nodeId } }; return { method: "exec.approvals.node.set", params: { ...params, nodeId } };
} }
@@ -76,8 +76,8 @@ export async function loadExecApprovals(
state: ExecApprovalsState, state: ExecApprovalsState,
target?: ExecApprovalsTarget | null, target?: ExecApprovalsTarget | null,
) { ) {
if (!state.client || !state.connected) return; if (!state.client || !state.connected) {return;}
if (state.execApprovalsLoading) return; if (state.execApprovalsLoading) {return;}
state.execApprovalsLoading = true; state.execApprovalsLoading = true;
state.lastError = null; state.lastError = null;
try { try {
@@ -86,7 +86,7 @@ export async function loadExecApprovals(
state.lastError = "Select a node before loading exec approvals."; state.lastError = "Select a node before loading exec approvals.";
return; return;
} }
const res = (await state.client.request(rpc.method, rpc.params)) as ExecApprovalsSnapshot; const res = (await state.client.request(rpc.method, rpc.params));
applyExecApprovalsSnapshot(state, res); applyExecApprovalsSnapshot(state, res);
} catch (err) { } catch (err) {
state.lastError = String(err); state.lastError = String(err);
@@ -109,7 +109,7 @@ export async function saveExecApprovals(
state: ExecApprovalsState, state: ExecApprovalsState,
target?: ExecApprovalsTarget | null, target?: ExecApprovalsTarget | null,
) { ) {
if (!state.client || !state.connected) return; if (!state.client || !state.connected) {return;}
state.execApprovalsSaving = true; state.execApprovalsSaving = true;
state.lastError = null; state.lastError = null;
try { try {

View File

@@ -19,12 +19,12 @@ const LOG_BUFFER_LIMIT = 2000;
const LEVELS = new Set<LogLevel>(["trace", "debug", "info", "warn", "error", "fatal"]); const LEVELS = new Set<LogLevel>(["trace", "debug", "info", "warn", "error", "fatal"]);
function parseMaybeJsonString(value: unknown) { function parseMaybeJsonString(value: unknown) {
if (typeof value !== "string") return null; if (typeof value !== "string") {return null;}
const trimmed = value.trim(); const trimmed = value.trim();
if (!trimmed.startsWith("{") || !trimmed.endsWith("}")) return null; if (!trimmed.startsWith("{") || !trimmed.endsWith("}")) {return null;}
try { try {
const parsed = JSON.parse(trimmed) as unknown; const parsed = JSON.parse(trimmed) as unknown;
if (!parsed || typeof parsed !== "object") return null; if (!parsed || typeof parsed !== "object") {return null;}
return parsed as Record<string, unknown>; return parsed as Record<string, unknown>;
} catch { } catch {
return null; return null;
@@ -32,13 +32,13 @@ function parseMaybeJsonString(value: unknown) {
} }
function normalizeLevel(value: unknown): LogLevel | null { function normalizeLevel(value: unknown): LogLevel | null {
if (typeof value !== "string") return null; if (typeof value !== "string") {return null;}
const lowered = value.toLowerCase() as LogLevel; const lowered = value.toLowerCase() as LogLevel;
return LEVELS.has(lowered) ? lowered : null; return LEVELS.has(lowered) ? lowered : null;
} }
export function parseLogLine(line: string): LogEntry { export function parseLogLine(line: string): LogEntry {
if (!line.trim()) return { raw: line, message: line }; if (!line.trim()) {return { raw: line, message: line };}
try { try {
const obj = JSON.parse(line) as Record<string, unknown>; const obj = JSON.parse(line) as Record<string, unknown>;
const meta = const meta =
@@ -51,24 +51,24 @@ export function parseLogLine(line: string): LogEntry {
const contextCandidate = const contextCandidate =
typeof obj["0"] === "string" typeof obj["0"] === "string"
? (obj["0"] as string) ? (obj["0"])
: typeof meta?.name === "string" : typeof meta?.name === "string"
? (meta?.name as string) ? (meta?.name)
: null; : null;
const contextObj = parseMaybeJsonString(contextCandidate); const contextObj = parseMaybeJsonString(contextCandidate);
let subsystem: string | null = null; let subsystem: string | null = null;
if (contextObj) { if (contextObj) {
if (typeof contextObj.subsystem === "string") subsystem = contextObj.subsystem; if (typeof contextObj.subsystem === "string") {subsystem = contextObj.subsystem;}
else if (typeof contextObj.module === "string") subsystem = contextObj.module; else if (typeof contextObj.module === "string") {subsystem = contextObj.module;}
} }
if (!subsystem && contextCandidate && contextCandidate.length < 120) { if (!subsystem && contextCandidate && contextCandidate.length < 120) {
subsystem = contextCandidate; subsystem = contextCandidate;
} }
let message: string | null = null; let message: string | null = null;
if (typeof obj["1"] === "string") message = obj["1"] as string; if (typeof obj["1"] === "string") {message = obj["1"];}
else if (!contextObj && typeof obj["0"] === "string") message = obj["0"] as string; else if (!contextObj && typeof obj["0"] === "string") {message = obj["0"];}
else if (typeof obj.message === "string") message = obj.message as string; else if (typeof obj.message === "string") {message = obj.message;}
return { return {
raw: line, raw: line,
@@ -84,9 +84,9 @@ export function parseLogLine(line: string): LogEntry {
} }
export async function loadLogs(state: LogsState, opts?: { reset?: boolean; quiet?: boolean }) { export async function loadLogs(state: LogsState, opts?: { reset?: boolean; quiet?: boolean }) {
if (!state.client || !state.connected) return; if (!state.client || !state.connected) {return;}
if (state.logsLoading && !opts?.quiet) return; if (state.logsLoading && !opts?.quiet) {return;}
if (!opts?.quiet) state.logsLoading = true; if (!opts?.quiet) {state.logsLoading = true;}
state.logsError = null; state.logsError = null;
try { try {
const res = await state.client.request("logs.tail", { const res = await state.client.request("logs.tail", {
@@ -103,20 +103,20 @@ export async function loadLogs(state: LogsState, opts?: { reset?: boolean; quiet
reset?: boolean; reset?: boolean;
}; };
const lines = Array.isArray(payload.lines) const lines = Array.isArray(payload.lines)
? (payload.lines.filter((line) => typeof line === "string") as string[]) ? (payload.lines.filter((line) => typeof line === "string"))
: []; : [];
const entries = lines.map(parseLogLine); const entries = lines.map(parseLogLine);
const shouldReset = Boolean(opts?.reset || payload.reset || state.logsCursor == null); const shouldReset = Boolean(opts?.reset || payload.reset || state.logsCursor == null);
state.logsEntries = shouldReset state.logsEntries = shouldReset
? entries ? entries
: [...state.logsEntries, ...entries].slice(-LOG_BUFFER_LIMIT); : [...state.logsEntries, ...entries].slice(-LOG_BUFFER_LIMIT);
if (typeof payload.cursor === "number") state.logsCursor = payload.cursor; if (typeof payload.cursor === "number") {state.logsCursor = payload.cursor;}
if (typeof payload.file === "string") state.logsFile = payload.file; if (typeof payload.file === "string") {state.logsFile = payload.file;}
state.logsTruncated = Boolean(payload.truncated); state.logsTruncated = Boolean(payload.truncated);
state.logsLastFetchAt = Date.now(); state.logsLastFetchAt = Date.now();
} catch (err) { } catch (err) {
state.logsError = String(err); state.logsError = String(err);
} finally { } finally {
if (!opts?.quiet) state.logsLoading = false; if (!opts?.quiet) {state.logsLoading = false;}
} }
} }

View File

@@ -9,17 +9,15 @@ export type NodesState = {
}; };
export async function loadNodes(state: NodesState, opts?: { quiet?: boolean }) { export async function loadNodes(state: NodesState, opts?: { quiet?: boolean }) {
if (!state.client || !state.connected) return; if (!state.client || !state.connected) {return;}
if (state.nodesLoading) return; if (state.nodesLoading) {return;}
state.nodesLoading = true; state.nodesLoading = true;
if (!opts?.quiet) state.lastError = null; if (!opts?.quiet) {state.lastError = null;}
try { try {
const res = (await state.client.request("node.list", {})) as { const res = (await state.client.request("node.list", {}));
nodes?: Array<Record<string, unknown>>;
};
state.nodes = Array.isArray(res.nodes) ? res.nodes : []; state.nodes = Array.isArray(res.nodes) ? res.nodes : [];
} catch (err) { } catch (err) {
if (!opts?.quiet) state.lastError = String(err); if (!opts?.quiet) {state.lastError = String(err);}
} finally { } finally {
state.nodesLoading = false; state.nodesLoading = false;
} }

View File

@@ -11,13 +11,13 @@ export type PresenceState = {
}; };
export async function loadPresence(state: PresenceState) { export async function loadPresence(state: PresenceState) {
if (!state.client || !state.connected) return; if (!state.client || !state.connected) {return;}
if (state.presenceLoading) return; if (state.presenceLoading) {return;}
state.presenceLoading = true; state.presenceLoading = true;
state.presenceError = null; state.presenceError = null;
state.presenceStatus = null; state.presenceStatus = null;
try { try {
const res = (await state.client.request("system-presence", {})) as PresenceEntry[] | undefined; const res = (await state.client.request("system-presence", {}));
if (Array.isArray(res)) { if (Array.isArray(res)) {
state.presenceEntries = res; state.presenceEntries = res;
state.presenceStatus = res.length === 0 ? "No instances yet." : null; state.presenceStatus = res.length === 0 ? "No instances yet." : null;

View File

@@ -23,8 +23,8 @@ export async function loadSessions(
includeUnknown?: boolean; includeUnknown?: boolean;
}, },
) { ) {
if (!state.client || !state.connected) return; if (!state.client || !state.connected) {return;}
if (state.sessionsLoading) return; if (state.sessionsLoading) {return;}
state.sessionsLoading = true; state.sessionsLoading = true;
state.sessionsError = null; state.sessionsError = null;
try { try {
@@ -36,12 +36,10 @@ export async function loadSessions(
includeGlobal, includeGlobal,
includeUnknown, includeUnknown,
}; };
if (activeMinutes > 0) params.activeMinutes = activeMinutes; if (activeMinutes > 0) {params.activeMinutes = activeMinutes;}
if (limit > 0) params.limit = limit; if (limit > 0) {params.limit = limit;}
const res = (await state.client.request("sessions.list", params)) as const res = (await state.client.request("sessions.list", params));
| SessionsListResult if (res) {state.sessionsResult = res;}
| undefined;
if (res) state.sessionsResult = res;
} catch (err) { } catch (err) {
state.sessionsError = String(err); state.sessionsError = String(err);
} finally { } finally {
@@ -59,12 +57,12 @@ export async function patchSession(
reasoningLevel?: string | null; reasoningLevel?: string | null;
}, },
) { ) {
if (!state.client || !state.connected) return; if (!state.client || !state.connected) {return;}
const params: Record<string, unknown> = { key }; const params: Record<string, unknown> = { key };
if ("label" in patch) params.label = patch.label; if ("label" in patch) {params.label = patch.label;}
if ("thinkingLevel" in patch) params.thinkingLevel = patch.thinkingLevel; if ("thinkingLevel" in patch) {params.thinkingLevel = patch.thinkingLevel;}
if ("verboseLevel" in patch) params.verboseLevel = patch.verboseLevel; if ("verboseLevel" in patch) {params.verboseLevel = patch.verboseLevel;}
if ("reasoningLevel" in patch) params.reasoningLevel = patch.reasoningLevel; if ("reasoningLevel" in patch) {params.reasoningLevel = patch.reasoningLevel;}
try { try {
await state.client.request("sessions.patch", params); await state.client.request("sessions.patch", params);
await loadSessions(state); await loadSessions(state);
@@ -74,12 +72,12 @@ export async function patchSession(
} }
export async function deleteSession(state: SessionsState, key: string) { export async function deleteSession(state: SessionsState, key: string) {
if (!state.client || !state.connected) return; if (!state.client || !state.connected) {return;}
if (state.sessionsLoading) return; if (state.sessionsLoading) {return;}
const confirmed = window.confirm( const confirmed = window.confirm(
`Delete session "${key}"?\n\nDeletes the session entry and archives its transcript.`, `Delete session "${key}"?\n\nDeletes the session entry and archives its transcript.`,
); );
if (!confirmed) return; if (!confirmed) {return;}
state.sessionsLoading = true; state.sessionsLoading = true;
state.sessionsError = null; state.sessionsError = null;
try { try {

View File

@@ -24,15 +24,15 @@ type LoadSkillsOptions = {
}; };
function setSkillMessage(state: SkillsState, key: string, message?: SkillMessage) { function setSkillMessage(state: SkillsState, key: string, message?: SkillMessage) {
if (!key.trim()) return; if (!key.trim()) {return;}
const next = { ...state.skillMessages }; const next = { ...state.skillMessages };
if (message) next[key] = message; if (message) {next[key] = message;}
else delete next[key]; else {delete next[key];}
state.skillMessages = next; state.skillMessages = next;
} }
function getErrorMessage(err: unknown) { function getErrorMessage(err: unknown) {
if (err instanceof Error) return err.message; if (err instanceof Error) {return err.message;}
return String(err); return String(err);
} }
@@ -40,13 +40,13 @@ export async function loadSkills(state: SkillsState, options?: LoadSkillsOptions
if (options?.clearMessages && Object.keys(state.skillMessages).length > 0) { if (options?.clearMessages && Object.keys(state.skillMessages).length > 0) {
state.skillMessages = {}; state.skillMessages = {};
} }
if (!state.client || !state.connected) return; if (!state.client || !state.connected) {return;}
if (state.skillsLoading) return; if (state.skillsLoading) {return;}
state.skillsLoading = true; state.skillsLoading = true;
state.skillsError = null; state.skillsError = null;
try { try {
const res = (await state.client.request("skills.status", {})) as SkillStatusReport | undefined; const res = (await state.client.request("skills.status", {}));
if (res) state.skillsReport = res; if (res) {state.skillsReport = res;}
} catch (err) { } catch (err) {
state.skillsError = getErrorMessage(err); state.skillsError = getErrorMessage(err);
} finally { } finally {
@@ -59,7 +59,7 @@ export function updateSkillEdit(state: SkillsState, skillKey: string, value: str
} }
export async function updateSkillEnabled(state: SkillsState, skillKey: string, enabled: boolean) { export async function updateSkillEnabled(state: SkillsState, skillKey: string, enabled: boolean) {
if (!state.client || !state.connected) return; if (!state.client || !state.connected) {return;}
state.skillsBusyKey = skillKey; state.skillsBusyKey = skillKey;
state.skillsError = null; state.skillsError = null;
try { try {
@@ -82,7 +82,7 @@ export async function updateSkillEnabled(state: SkillsState, skillKey: string, e
} }
export async function saveSkillApiKey(state: SkillsState, skillKey: string) { export async function saveSkillApiKey(state: SkillsState, skillKey: string) {
if (!state.client || !state.connected) return; if (!state.client || !state.connected) {return;}
state.skillsBusyKey = skillKey; state.skillsBusyKey = skillKey;
state.skillsError = null; state.skillsError = null;
try { try {
@@ -111,7 +111,7 @@ export async function installSkill(
name: string, name: string,
installId: string, installId: string,
) { ) {
if (!state.client || !state.connected) return; if (!state.client || !state.connected) {return;}
state.skillsBusyKey = skillKey; state.skillsBusyKey = skillKey;
state.skillsError = null; state.skillsError = null;
try { try {
@@ -119,7 +119,7 @@ export async function installSkill(
name, name,
installId, installId,
timeoutMs: 120000, timeoutMs: 120000,
})) as { ok?: boolean; message?: string }; }));
await loadSkills(state); await loadSkills(state);
setSkillMessage(state, skillKey, { setSkillMessage(state, skillKey, {
kind: "success", kind: "success",

View File

@@ -18,23 +18,23 @@ function normalizeRole(role: string): string {
} }
function normalizeScopes(scopes: string[] | undefined): string[] { function normalizeScopes(scopes: string[] | undefined): string[] {
if (!Array.isArray(scopes)) return []; if (!Array.isArray(scopes)) {return [];}
const out = new Set<string>(); const out = new Set<string>();
for (const scope of scopes) { for (const scope of scopes) {
const trimmed = scope.trim(); const trimmed = scope.trim();
if (trimmed) out.add(trimmed); if (trimmed) {out.add(trimmed);}
} }
return [...out].sort(); return [...out].toSorted();
} }
function readStore(): DeviceAuthStore | null { function readStore(): DeviceAuthStore | null {
try { try {
const raw = window.localStorage.getItem(STORAGE_KEY); const raw = window.localStorage.getItem(STORAGE_KEY);
if (!raw) return null; if (!raw) {return null;}
const parsed = JSON.parse(raw) as DeviceAuthStore; const parsed = JSON.parse(raw) as DeviceAuthStore;
if (!parsed || parsed.version !== 1) return null; if (!parsed || parsed.version !== 1) {return null;}
if (!parsed.deviceId || typeof parsed.deviceId !== "string") return null; if (!parsed.deviceId || typeof parsed.deviceId !== "string") {return null;}
if (!parsed.tokens || typeof parsed.tokens !== "object") return null; if (!parsed.tokens || typeof parsed.tokens !== "object") {return null;}
return parsed; return parsed;
} catch { } catch {
return null; return null;
@@ -54,10 +54,10 @@ export function loadDeviceAuthToken(params: {
role: string; role: string;
}): DeviceAuthEntry | null { }): DeviceAuthEntry | null {
const store = readStore(); const store = readStore();
if (!store || store.deviceId !== params.deviceId) return null; if (!store || store.deviceId !== params.deviceId) {return null;}
const role = normalizeRole(params.role); const role = normalizeRole(params.role);
const entry = store.tokens[role]; const entry = store.tokens[role];
if (!entry || typeof entry.token !== "string") return null; if (!entry || typeof entry.token !== "string") {return null;}
return entry; return entry;
} }
@@ -90,9 +90,9 @@ export function storeDeviceAuthToken(params: {
export function clearDeviceAuthToken(params: { deviceId: string; role: string }) { export function clearDeviceAuthToken(params: { deviceId: string; role: string }) {
const store = readStore(); const store = readStore();
if (!store || store.deviceId !== params.deviceId) return; if (!store || store.deviceId !== params.deviceId) {return;}
const role = normalizeRole(params.role); const role = normalizeRole(params.role);
if (!store.tokens[role]) return; if (!store.tokens[role]) {return;}
const next = { ...store, tokens: { ...store.tokens } }; const next = { ...store, tokens: { ...store.tokens } };
delete next.tokens[role]; delete next.tokens[role];
writeStore(next); writeStore(next);

View File

@@ -18,7 +18,7 @@ const STORAGE_KEY = "openclaw-device-identity-v1";
function base64UrlEncode(bytes: Uint8Array): string { function base64UrlEncode(bytes: Uint8Array): string {
let binary = ""; let binary = "";
for (const byte of bytes) binary += String.fromCharCode(byte); for (const byte of bytes) {binary += String.fromCharCode(byte);}
return btoa(binary).replaceAll("+", "-").replaceAll("/", "_").replace(/=+$/g, ""); return btoa(binary).replaceAll("+", "-").replaceAll("/", "_").replace(/=+$/g, "");
} }
@@ -27,7 +27,7 @@ function base64UrlDecode(input: string): Uint8Array {
const padded = normalized + "=".repeat((4 - (normalized.length % 4)) % 4); const padded = normalized + "=".repeat((4 - (normalized.length % 4)) % 4);
const binary = atob(padded); const binary = atob(padded);
const out = new Uint8Array(binary.length); const out = new Uint8Array(binary.length);
for (let i = 0; i < binary.length; i += 1) out[i] = binary.charCodeAt(i); for (let i = 0; i < binary.length; i += 1) {out[i] = binary.charCodeAt(i);}
return out; return out;
} }

View File

@@ -1,44 +1,44 @@
import { stripReasoningTagsFromText } from "../../../src/shared/text/reasoning-tags.js"; import { stripReasoningTagsFromText } from "../../../src/shared/text/reasoning-tags.js";
export function formatMs(ms?: number | null): string { export function formatMs(ms?: number | null): string {
if (!ms && ms !== 0) return "n/a"; if (!ms && ms !== 0) {return "n/a";}
return new Date(ms).toLocaleString(); return new Date(ms).toLocaleString();
} }
export function formatAgo(ms?: number | null): string { export function formatAgo(ms?: number | null): string {
if (!ms && ms !== 0) return "n/a"; if (!ms && ms !== 0) {return "n/a";}
const diff = Date.now() - ms; const diff = Date.now() - ms;
if (diff < 0) return "just now"; if (diff < 0) {return "just now";}
const sec = Math.round(diff / 1000); const sec = Math.round(diff / 1000);
if (sec < 60) return `${sec}s ago`; if (sec < 60) {return `${sec}s ago`;}
const min = Math.round(sec / 60); const min = Math.round(sec / 60);
if (min < 60) return `${min}m ago`; if (min < 60) {return `${min}m ago`;}
const hr = Math.round(min / 60); const hr = Math.round(min / 60);
if (hr < 48) return `${hr}h ago`; if (hr < 48) {return `${hr}h ago`;}
const day = Math.round(hr / 24); const day = Math.round(hr / 24);
return `${day}d ago`; return `${day}d ago`;
} }
export function formatDurationMs(ms?: number | null): string { export function formatDurationMs(ms?: number | null): string {
if (!ms && ms !== 0) return "n/a"; if (!ms && ms !== 0) {return "n/a";}
if (ms < 1000) return `${ms}ms`; if (ms < 1000) {return `${ms}ms`;}
const sec = Math.round(ms / 1000); const sec = Math.round(ms / 1000);
if (sec < 60) return `${sec}s`; if (sec < 60) {return `${sec}s`;}
const min = Math.round(sec / 60); const min = Math.round(sec / 60);
if (min < 60) return `${min}m`; if (min < 60) {return `${min}m`;}
const hr = Math.round(min / 60); const hr = Math.round(min / 60);
if (hr < 48) return `${hr}h`; if (hr < 48) {return `${hr}h`;}
const day = Math.round(hr / 24); const day = Math.round(hr / 24);
return `${day}d`; return `${day}d`;
} }
export function formatList(values?: Array<string | null | undefined>): string { export function formatList(values?: Array<string | null | undefined>): string {
if (!values || values.length === 0) return "none"; if (!values || values.length === 0) {return "none";}
return values.filter((v): v is string => Boolean(v && v.trim())).join(", "); return values.filter((v): v is string => Boolean(v && v.trim())).join(", ");
} }
export function clampText(value: string, max = 120): string { export function clampText(value: string, max = 120): string {
if (value.length <= max) return value; if (value.length <= max) {return value;}
return `${value.slice(0, Math.max(0, max - 1))}`; return `${value.slice(0, Math.max(0, max - 1))}`;
} }

View File

@@ -91,7 +91,7 @@ export class GatewayBrowserClient {
} }
private connect() { private connect() {
if (this.closed) return; if (this.closed) {return;}
this.ws = new WebSocket(this.opts.url); this.ws = new WebSocket(this.opts.url);
this.ws.onopen = () => this.queueConnect(); this.ws.onopen = () => this.queueConnect();
this.ws.onmessage = (ev) => this.handleMessage(String(ev.data ?? "")); this.ws.onmessage = (ev) => this.handleMessage(String(ev.data ?? ""));
@@ -108,19 +108,19 @@ export class GatewayBrowserClient {
} }
private scheduleReconnect() { private scheduleReconnect() {
if (this.closed) return; if (this.closed) {return;}
const delay = this.backoffMs; const delay = this.backoffMs;
this.backoffMs = Math.min(this.backoffMs * 1.7, 15_000); this.backoffMs = Math.min(this.backoffMs * 1.7, 15_000);
window.setTimeout(() => this.connect(), delay); window.setTimeout(() => this.connect(), delay);
} }
private flushPending(err: Error) { private flushPending(err: Error) {
for (const [, p] of this.pending) p.reject(err); for (const [, p] of this.pending) {p.reject(err);}
this.pending.clear(); this.pending.clear();
} }
private async sendConnect() { private async sendConnect() {
if (this.connectSent) return; if (this.connectSent) {return;}
this.connectSent = true; this.connectSent = true;
if (this.connectTimer !== null) { if (this.connectTimer !== null) {
window.clearTimeout(this.connectTimer); window.clearTimeout(this.connectTimer);
@@ -265,10 +265,10 @@ export class GatewayBrowserClient {
if (frame.type === "res") { if (frame.type === "res") {
const res = parsed as GatewayResponseFrame; const res = parsed as GatewayResponseFrame;
const pending = this.pending.get(res.id); const pending = this.pending.get(res.id);
if (!pending) return; if (!pending) {return;}
this.pending.delete(res.id); this.pending.delete(res.id);
if (res.ok) pending.resolve(res.payload); if (res.ok) {pending.resolve(res.payload);}
else pending.reject(new Error(res.error?.message ?? "request failed")); else {pending.reject(new Error(res.error?.message ?? "request failed"));}
return; return;
} }
} }
@@ -289,7 +289,7 @@ export class GatewayBrowserClient {
private queueConnect() { private queueConnect() {
this.connectNonce = null; this.connectNonce = null;
this.connectSent = false; this.connectSent = false;
if (this.connectTimer !== null) window.clearTimeout(this.connectTimer); if (this.connectTimer !== null) {window.clearTimeout(this.connectTimer);}
this.connectTimer = window.setTimeout(() => { this.connectTimer = window.setTimeout(() => {
void this.sendConnect(); void this.sendConnect();
}, 750); }, 750);

View File

@@ -243,6 +243,6 @@ export function renderEmojiIcon(
} }
export function setEmojiIcon(target: HTMLElement | null, icon: string): void { export function setEmojiIcon(target: HTMLElement | null, icon: string): void {
if (!target) return; if (!target) {return;}
target.textContent = icon; target.textContent = icon;
} }

View File

@@ -47,7 +47,7 @@ const markdownCache = new Map<string, string>();
function getCachedMarkdown(key: string): string | null { function getCachedMarkdown(key: string): string | null {
const cached = markdownCache.get(key); const cached = markdownCache.get(key);
if (cached === undefined) return null; if (cached === undefined) {return null;}
markdownCache.delete(key); markdownCache.delete(key);
markdownCache.set(key, cached); markdownCache.set(key, cached);
return cached; return cached;
@@ -55,19 +55,19 @@ function getCachedMarkdown(key: string): string | null {
function setCachedMarkdown(key: string, value: string) { function setCachedMarkdown(key: string, value: string) {
markdownCache.set(key, value); markdownCache.set(key, value);
if (markdownCache.size <= MARKDOWN_CACHE_LIMIT) return; if (markdownCache.size <= MARKDOWN_CACHE_LIMIT) {return;}
const oldest = markdownCache.keys().next().value; const oldest = markdownCache.keys().next().value;
if (oldest) markdownCache.delete(oldest); if (oldest) {markdownCache.delete(oldest);}
} }
function installHooks() { function installHooks() {
if (hooksInstalled) return; if (hooksInstalled) {return;}
hooksInstalled = true; hooksInstalled = true;
DOMPurify.addHook("afterSanitizeAttributes", (node) => { DOMPurify.addHook("afterSanitizeAttributes", (node) => {
if (!(node instanceof HTMLAnchorElement)) return; if (!(node instanceof HTMLAnchorElement)) {return;}
const href = node.getAttribute("href"); const href = node.getAttribute("href");
if (!href) return; if (!href) {return;}
node.setAttribute("rel", "noreferrer noopener"); node.setAttribute("rel", "noreferrer noopener");
node.setAttribute("target", "_blank"); node.setAttribute("target", "_blank");
}); });
@@ -75,11 +75,11 @@ function installHooks() {
export function toSanitizedMarkdownHtml(markdown: string): string { export function toSanitizedMarkdownHtml(markdown: string): string {
const input = markdown.trim(); const input = markdown.trim();
if (!input) return ""; if (!input) {return "";}
installHooks(); installHooks();
if (input.length <= MARKDOWN_CACHE_MAX_CHARS) { if (input.length <= MARKDOWN_CACHE_MAX_CHARS) {
const cached = getCachedMarkdown(input); const cached = getCachedMarkdown(input);
if (cached !== null) return cached; if (cached !== null) {return cached;}
} }
const truncated = truncateText(input, MARKDOWN_CHAR_LIMIT); const truncated = truncateText(input, MARKDOWN_CHAR_LIMIT);
const suffix = truncated.truncated const suffix = truncated.truncated

View File

@@ -89,13 +89,13 @@ describe("control UI routing", () => {
expect(window.matchMedia("(max-width: 768px)").matches).toBe(true); expect(window.matchMedia("(max-width: 768px)").matches).toBe(true);
const split = app.querySelector(".chat-split-container") as HTMLElement | null; const split = app.querySelector(".chat-split-container");
expect(split).not.toBeNull(); expect(split).not.toBeNull();
if (split) { if (split) {
expect(getComputedStyle(split).position).not.toBe("fixed"); expect(getComputedStyle(split).position).not.toBe("fixed");
} }
const chatMain = app.querySelector(".chat-main") as HTMLElement | null; const chatMain = app.querySelector(".chat-main");
expect(chatMain).not.toBeNull(); expect(chatMain).not.toBeNull();
if (chatMain) { if (chatMain) {
expect(getComputedStyle(chatMain).display).not.toBe("none"); expect(getComputedStyle(chatMain).display).not.toBe("none");
@@ -115,9 +115,9 @@ describe("control UI routing", () => {
const app = mountApp("/chat"); const app = mountApp("/chat");
await app.updateComplete; await app.updateComplete;
const initialContainer = app.querySelector(".chat-thread") as HTMLElement | null; const initialContainer = app.querySelector(".chat-thread");
expect(initialContainer).not.toBeNull(); expect(initialContainer).not.toBeNull();
if (!initialContainer) return; if (!initialContainer) {return;}
initialContainer.style.maxHeight = "180px"; initialContainer.style.maxHeight = "180px";
initialContainer.style.overflow = "auto"; initialContainer.style.overflow = "auto";
@@ -132,13 +132,13 @@ describe("control UI routing", () => {
await nextFrame(); await nextFrame();
} }
const container = app.querySelector(".chat-thread") as HTMLElement | null; const container = app.querySelector(".chat-thread");
expect(container).not.toBeNull(); expect(container).not.toBeNull();
if (!container) return; if (!container) {return;}
const maxScroll = container.scrollHeight - container.clientHeight; const maxScroll = container.scrollHeight - container.clientHeight;
expect(maxScroll).toBeGreaterThan(0); expect(maxScroll).toBeGreaterThan(0);
for (let i = 0; i < 10; i++) { for (let i = 0; i < 10; i++) {
if (container.scrollTop === maxScroll) break; if (container.scrollTop === maxScroll) {break;}
await nextFrame(); await nextFrame();
} }
expect(container.scrollTop).toBe(maxScroll); expect(container.scrollTop).toBe(maxScroll);

View File

@@ -40,18 +40,18 @@ const TAB_PATHS: Record<Tab, string> = {
const PATH_TO_TAB = new Map(Object.entries(TAB_PATHS).map(([tab, path]) => [path, tab as Tab])); const PATH_TO_TAB = new Map(Object.entries(TAB_PATHS).map(([tab, path]) => [path, tab as Tab]));
export function normalizeBasePath(basePath: string): string { export function normalizeBasePath(basePath: string): string {
if (!basePath) return ""; if (!basePath) {return "";}
let base = basePath.trim(); let base = basePath.trim();
if (!base.startsWith("/")) base = `/${base}`; if (!base.startsWith("/")) {base = `/${base}`;}
if (base === "/") return ""; if (base === "/") {return "";}
if (base.endsWith("/")) base = base.slice(0, -1); if (base.endsWith("/")) {base = base.slice(0, -1);}
return base; return base;
} }
export function normalizePath(path: string): string { export function normalizePath(path: string): string {
if (!path) return "/"; if (!path) {return "/";}
let normalized = path.trim(); let normalized = path.trim();
if (!normalized.startsWith("/")) normalized = `/${normalized}`; if (!normalized.startsWith("/")) {normalized = `/${normalized}`;}
if (normalized.length > 1 && normalized.endsWith("/")) { if (normalized.length > 1 && normalized.endsWith("/")) {
normalized = normalized.slice(0, -1); normalized = normalized.slice(0, -1);
} }
@@ -75,8 +75,8 @@ export function tabFromPath(pathname: string, basePath = ""): Tab | null {
} }
} }
let normalized = normalizePath(path).toLowerCase(); let normalized = normalizePath(path).toLowerCase();
if (normalized.endsWith("/index.html")) normalized = "/"; if (normalized.endsWith("/index.html")) {normalized = "/";}
if (normalized === "/") return "chat"; if (normalized === "/") {return "chat";}
return PATH_TO_TAB.get(normalized) ?? null; return PATH_TO_TAB.get(normalized) ?? null;
} }
@@ -85,9 +85,9 @@ export function inferBasePathFromPathname(pathname: string): string {
if (normalized.endsWith("/index.html")) { if (normalized.endsWith("/index.html")) {
normalized = normalizePath(normalized.slice(0, -"/index.html".length)); normalized = normalizePath(normalized.slice(0, -"/index.html".length));
} }
if (normalized === "/") return ""; if (normalized === "/") {return "";}
const segments = normalized.split("/").filter(Boolean); const segments = normalized.split("/").filter(Boolean);
if (segments.length === 0) return ""; if (segments.length === 0) {return "";}
for (let i = 0; i < segments.length; i++) { for (let i = 0; i < segments.length; i++) {
const candidate = `/${segments.slice(i).join("/")}`.toLowerCase(); const candidate = `/${segments.slice(i).join("/")}`.toLowerCase();
if (PATH_TO_TAB.has(candidate)) { if (PATH_TO_TAB.has(candidate)) {

View File

@@ -15,19 +15,19 @@ export function formatPresenceAge(entry: PresenceEntry): string {
} }
export function formatNextRun(ms?: number | null) { export function formatNextRun(ms?: number | null) {
if (!ms) return "n/a"; if (!ms) {return "n/a";}
return `${formatMs(ms)} (${formatAgo(ms)})`; return `${formatMs(ms)} (${formatAgo(ms)})`;
} }
export function formatSessionTokens(row: GatewaySessionRow) { export function formatSessionTokens(row: GatewaySessionRow) {
if (row.totalTokens == null) return "n/a"; if (row.totalTokens == null) {return "n/a";}
const total = row.totalTokens ?? 0; const total = row.totalTokens ?? 0;
const ctx = row.contextTokens ?? 0; const ctx = row.contextTokens ?? 0;
return ctx ? `${total} / ${ctx}` : String(total); return ctx ? `${total} / ${ctx}` : String(total);
} }
export function formatEventPayload(payload: unknown): string { export function formatEventPayload(payload: unknown): string {
if (payload == null) return ""; if (payload == null) {return "";}
try { try {
return JSON.stringify(payload, null, 2); return JSON.stringify(payload, null, 2);
} catch { } catch {
@@ -45,13 +45,13 @@ export function formatCronState(job: CronJob) {
export function formatCronSchedule(job: CronJob) { export function formatCronSchedule(job: CronJob) {
const s = job.schedule; const s = job.schedule;
if (s.kind === "at") return `At ${formatMs(s.atMs)}`; if (s.kind === "at") {return `At ${formatMs(s.atMs)}`;}
if (s.kind === "every") return `Every ${formatDurationMs(s.everyMs)}`; if (s.kind === "every") {return `Every ${formatDurationMs(s.everyMs)}`;}
return `Cron ${s.expr}${s.tz ? ` (${s.tz})` : ""}`; return `Cron ${s.expr}${s.tz ? ` (${s.tz})` : ""}`;
} }
export function formatCronPayload(job: CronJob) { export function formatCronPayload(job: CronJob) {
const p = job.payload; const p = job.payload;
if (p.kind === "systemEvent") return `System: ${p.text}`; if (p.kind === "systemEvent") {return `System: ${p.text}`;}
return `Agent: ${p.message}`; return `Agent: ${p.message}`;
} }

View File

@@ -36,7 +36,7 @@ export function loadSettings(): UiSettings {
try { try {
const raw = localStorage.getItem(KEY); const raw = localStorage.getItem(KEY);
if (!raw) return defaults; if (!raw) {return defaults;}
const parsed = JSON.parse(raw) as Partial<UiSettings>; const parsed = JSON.parse(raw) as Partial<UiSettings>;
return { return {
gatewayUrl: gatewayUrl:

View File

@@ -18,9 +18,9 @@ type DocumentWithViewTransition = Document & {
}; };
const clamp01 = (value: number) => { const clamp01 = (value: number) => {
if (Number.isNaN(value)) return 0.5; if (Number.isNaN(value)) {return 0.5;}
if (value <= 0) return 0; if (value <= 0) {return 0;}
if (value >= 1) return 1; if (value >= 1) {return 1;}
return value; return value;
}; };
@@ -43,7 +43,7 @@ export const startThemeTransition = ({
context, context,
currentTheme, currentTheme,
}: ThemeTransitionOptions) => { }: ThemeTransitionOptions) => {
if (currentTheme === nextTheme) return; if (currentTheme === nextTheme) {return;}
const documentReference = globalThis.document ?? null; const documentReference = globalThis.document ?? null;
if (!documentReference) { if (!documentReference) {

View File

@@ -9,6 +9,6 @@ export function getSystemTheme(): ResolvedTheme {
} }
export function resolveTheme(mode: ThemeMode): ResolvedTheme { export function resolveTheme(mode: ThemeMode): ResolvedTheme {
if (mode === "system") return getSystemTheme(); if (mode === "system") {return getSystemTheme();}
return mode; return mode;
} }

View File

@@ -39,7 +39,7 @@ function normalizeToolName(name?: string): string {
function defaultTitle(name: string): string { function defaultTitle(name: string): string {
const cleaned = name.replace(/_/g, " ").trim(); const cleaned = name.replace(/_/g, " ").trim();
if (!cleaned) return "Tool"; if (!cleaned) {return "Tool";}
return cleaned return cleaned
.split(/\s+/) .split(/\s+/)
.map((part) => .map((part) =>
@@ -52,17 +52,17 @@ function defaultTitle(name: string): string {
function normalizeVerb(value?: string): string | undefined { function normalizeVerb(value?: string): string | undefined {
const trimmed = value?.trim(); const trimmed = value?.trim();
if (!trimmed) return undefined; if (!trimmed) {return undefined;}
return trimmed.replace(/_/g, " "); return trimmed.replace(/_/g, " ");
} }
function coerceDisplayValue(value: unknown): string | undefined { function coerceDisplayValue(value: unknown): string | undefined {
if (value === null || value === undefined) return undefined; if (value === null || value === undefined) {return undefined;}
if (typeof value === "string") { if (typeof value === "string") {
const trimmed = value.trim(); const trimmed = value.trim();
if (!trimmed) return undefined; if (!trimmed) {return undefined;}
const firstLine = trimmed.split(/\r?\n/)[0]?.trim() ?? ""; const firstLine = trimmed.split(/\r?\n/)[0]?.trim() ?? "";
if (!firstLine) return undefined; if (!firstLine) {return undefined;}
return firstLine.length > 160 ? `${firstLine.slice(0, 157)}` : firstLine; return firstLine.length > 160 ? `${firstLine.slice(0, 157)}` : firstLine;
} }
if (typeof value === "number" || typeof value === "boolean") { if (typeof value === "number" || typeof value === "boolean") {
@@ -72,7 +72,7 @@ function coerceDisplayValue(value: unknown): string | undefined {
const values = value const values = value
.map((item) => coerceDisplayValue(item)) .map((item) => coerceDisplayValue(item))
.filter((item): item is string => Boolean(item)); .filter((item): item is string => Boolean(item));
if (values.length === 0) return undefined; if (values.length === 0) {return undefined;}
const preview = values.slice(0, 3).join(", "); const preview = values.slice(0, 3).join(", ");
return values.length > 3 ? `${preview}` : preview; return values.length > 3 ? `${preview}` : preview;
} }
@@ -80,11 +80,11 @@ function coerceDisplayValue(value: unknown): string | undefined {
} }
function lookupValueByPath(args: unknown, path: string): unknown { function lookupValueByPath(args: unknown, path: string): unknown {
if (!args || typeof args !== "object") return undefined; if (!args || typeof args !== "object") {return undefined;}
let current: unknown = args; let current: unknown = args;
for (const segment of path.split(".")) { for (const segment of path.split(".")) {
if (!segment) return undefined; if (!segment) {return undefined;}
if (!current || typeof current !== "object") return undefined; if (!current || typeof current !== "object") {return undefined;}
const record = current as Record<string, unknown>; const record = current as Record<string, unknown>;
current = record[segment]; current = record[segment];
} }
@@ -95,16 +95,16 @@ function resolveDetailFromKeys(args: unknown, keys: string[]): string | undefine
for (const key of keys) { for (const key of keys) {
const value = lookupValueByPath(args, key); const value = lookupValueByPath(args, key);
const display = coerceDisplayValue(value); const display = coerceDisplayValue(value);
if (display) return display; if (display) {return display;}
} }
return undefined; return undefined;
} }
function resolveReadDetail(args: unknown): string | undefined { function resolveReadDetail(args: unknown): string | undefined {
if (!args || typeof args !== "object") return undefined; if (!args || typeof args !== "object") {return undefined;}
const record = args as Record<string, unknown>; const record = args as Record<string, unknown>;
const path = typeof record.path === "string" ? record.path : undefined; const path = typeof record.path === "string" ? record.path : undefined;
if (!path) return undefined; if (!path) {return undefined;}
const offset = typeof record.offset === "number" ? record.offset : undefined; const offset = typeof record.offset === "number" ? record.offset : undefined;
const limit = typeof record.limit === "number" ? record.limit : undefined; const limit = typeof record.limit === "number" ? record.limit : undefined;
if (offset !== undefined && limit !== undefined) { if (offset !== undefined && limit !== undefined) {
@@ -114,7 +114,7 @@ function resolveReadDetail(args: unknown): string | undefined {
} }
function resolveWriteDetail(args: unknown): string | undefined { function resolveWriteDetail(args: unknown): string | undefined {
if (!args || typeof args !== "object") return undefined; if (!args || typeof args !== "object") {return undefined;}
const record = args as Record<string, unknown>; const record = args as Record<string, unknown>;
const path = typeof record.path === "string" ? record.path : undefined; const path = typeof record.path === "string" ? record.path : undefined;
return path; return path;
@@ -124,7 +124,7 @@ function resolveActionSpec(
spec: ToolDisplaySpec | undefined, spec: ToolDisplaySpec | undefined,
action: string | undefined, action: string | undefined,
): ToolDisplayActionSpec | undefined { ): ToolDisplayActionSpec | undefined {
if (!spec || !action) return undefined; if (!spec || !action) {return undefined;}
return spec.actions?.[action] ?? undefined; return spec.actions?.[action] ?? undefined;
} }
@@ -148,7 +148,7 @@ export function resolveToolDisplay(params: {
const verb = normalizeVerb(actionSpec?.label ?? action); const verb = normalizeVerb(actionSpec?.label ?? action);
let detail: string | undefined; let detail: string | undefined;
if (key === "read") detail = resolveReadDetail(params.args); if (key === "read") {detail = resolveReadDetail(params.args);}
if (!detail && (key === "write" || key === "edit" || key === "attach")) { if (!detail && (key === "write" || key === "edit" || key === "attach")) {
detail = resolveWriteDetail(params.args); detail = resolveWriteDetail(params.args);
} }
@@ -178,9 +178,9 @@ export function resolveToolDisplay(params: {
export function formatToolDetail(display: ToolDisplay): string | undefined { export function formatToolDetail(display: ToolDisplay): string | undefined {
const parts: string[] = []; const parts: string[] = [];
if (display.verb) parts.push(display.verb); if (display.verb) {parts.push(display.verb);}
if (display.detail) parts.push(display.detail); if (display.detail) {parts.push(display.detail);}
if (parts.length === 0) return undefined; if (parts.length === 0) {return undefined;}
return parts.join(" · "); return parts.join(" · ");
} }
@@ -190,6 +190,6 @@ export function formatToolSummary(display: ToolDisplay): string {
} }
function shortenHomeInString(input: string): string { function shortenHomeInString(input: string): string {
if (!input) return input; if (!input) {return input;}
return input.replace(/\/Users\/[^/]+/g, "~").replace(/\/home\/[^/]+/g, "~"); return input.replace(/\/Users\/[^/]+/g, "~").replace(/\/home\/[^/]+/g, "~");
} }

View File

@@ -16,7 +16,7 @@ describe("generateUUID", () => {
it("falls back to crypto.getRandomValues", () => { it("falls back to crypto.getRandomValues", () => {
const id = generateUUID({ const id = generateUUID({
getRandomValues: (bytes) => { getRandomValues: (bytes) => {
for (let i = 0; i < bytes.length; i++) bytes[i] = i; for (let i = 0; i < bytes.length; i++) {bytes[i] = i;}
return bytes; return bytes;
}, },
}); });

View File

@@ -11,7 +11,7 @@ function uuidFromBytes(bytes: Uint8Array): string {
let hex = ""; let hex = "";
for (let i = 0; i < bytes.length; i++) { for (let i = 0; i < bytes.length; i++) {
hex += bytes[i]!.toString(16).padStart(2, "0"); hex += bytes[i].toString(16).padStart(2, "0");
} }
return `${hex.slice(0, 8)}-${hex.slice(8, 12)}-${hex.slice(12, 16)}-${hex.slice( return `${hex.slice(0, 8)}-${hex.slice(8, 12)}-${hex.slice(12, 16)}-${hex.slice(
@@ -23,7 +23,7 @@ function uuidFromBytes(bytes: Uint8Array): string {
function weakRandomBytes(): Uint8Array { function weakRandomBytes(): Uint8Array {
const bytes = new Uint8Array(16); const bytes = new Uint8Array(16);
const now = Date.now(); const now = Date.now();
for (let i = 0; i < bytes.length; i++) bytes[i] = Math.floor(Math.random() * 256); for (let i = 0; i < bytes.length; i++) {bytes[i] = Math.floor(Math.random() * 256);}
bytes[0] ^= now & 0xff; bytes[0] ^= now & 0xff;
bytes[1] ^= (now >>> 8) & 0xff; bytes[1] ^= (now >>> 8) & 0xff;
bytes[2] ^= (now >>> 16) & 0xff; bytes[2] ^= (now >>> 16) & 0xff;
@@ -32,13 +32,13 @@ function weakRandomBytes(): Uint8Array {
} }
function warnWeakCryptoOnce() { function warnWeakCryptoOnce() {
if (warnedWeakCrypto) return; if (warnedWeakCrypto) {return;}
warnedWeakCrypto = true; warnedWeakCrypto = true;
console.warn("[uuid] crypto API missing; falling back to weak randomness"); console.warn("[uuid] crypto API missing; falling back to weak randomness");
} }
export function generateUUID(cryptoLike: CryptoLike | null = globalThis.crypto): string { export function generateUUID(cryptoLike: CryptoLike | null = globalThis.crypto): string {
if (cryptoLike && typeof cryptoLike.randomUUID === "function") return cryptoLike.randomUUID(); if (cryptoLike && typeof cryptoLike.randomUUID === "function") {return cryptoLike.randomUUID();}
if (cryptoLike && typeof cryptoLike.getRandomValues === "function") { if (cryptoLike && typeof cryptoLike.getRandomValues === "function") {
const bytes = new Uint8Array(16); const bytes = new Uint8Array(16);

View File

@@ -18,7 +18,7 @@ function resolveSchemaNode(
): JsonSchema | null { ): JsonSchema | null {
let current = schema; let current = schema;
for (const key of path) { for (const key of path) {
if (!current) return null; if (!current) {return null;}
const type = schemaType(current); const type = schemaType(current);
if (type === "object") { if (type === "object") {
const properties = current.properties ?? {}; const properties = current.properties ?? {};
@@ -28,13 +28,13 @@ function resolveSchemaNode(
} }
const additional = current.additionalProperties; const additional = current.additionalProperties;
if (typeof key === "string" && additional && typeof additional === "object") { if (typeof key === "string" && additional && typeof additional === "object") {
current = additional as JsonSchema; current = additional;
continue; continue;
} }
return null; return null;
} }
if (type === "array") { if (type === "array") {
if (typeof key !== "number") return null; if (typeof key !== "number") {return null;}
const items = Array.isArray(current.items) ? current.items[0] : current.items; const items = Array.isArray(current.items) ? current.items[0] : current.items;
current = items ?? null; current = items ?? null;
continue; continue;

View File

@@ -140,7 +140,7 @@ export function renderNostrProfileForm(params: {
const renderPicturePreview = () => { const renderPicturePreview = () => {
const picture = state.values.picture; const picture = state.values.picture;
if (!picture) return nothing; if (!picture) {return nothing;}
return html` return html`
<div style="margin-bottom: 12px;"> <div style="margin-bottom: 12px;">

View File

@@ -13,8 +13,8 @@ import {
* Truncate a pubkey for display (shows first and last 8 chars) * Truncate a pubkey for display (shows first and last 8 chars)
*/ */
function truncatePubkey(pubkey: string | null | undefined): string { function truncatePubkey(pubkey: string | null | undefined): string {
if (!pubkey) return "n/a"; if (!pubkey) {return "n/a";}
if (pubkey.length <= 20) return pubkey; if (pubkey.length <= 20) {return pubkey;}
return `${pubkey.slice(0, 8)}...${pubkey.slice(-8)}`; return `${pubkey.slice(0, 8)}...${pubkey.slice(-8)}`;
} }

View File

@@ -3,11 +3,11 @@ import type { ChannelAccountSnapshot } from "../types";
import type { ChannelKey, ChannelsProps } from "./channels.types"; import type { ChannelKey, ChannelsProps } from "./channels.types";
export function formatDuration(ms?: number | null) { export function formatDuration(ms?: number | null) {
if (!ms && ms !== 0) return "n/a"; if (!ms && ms !== 0) {return "n/a";}
const sec = Math.round(ms / 1000); const sec = Math.round(ms / 1000);
if (sec < 60) return `${sec}s`; if (sec < 60) {return `${sec}s`;}
const min = Math.round(sec / 60); const min = Math.round(sec / 60);
if (min < 60) return `${min}m`; if (min < 60) {return `${min}m`;}
const hr = Math.round(min / 60); const hr = Math.round(min / 60);
return `${hr}h`; return `${hr}h`;
} }
@@ -15,7 +15,7 @@ export function formatDuration(ms?: number | null) {
export function channelEnabled(key: ChannelKey, props: ChannelsProps) { export function channelEnabled(key: ChannelKey, props: ChannelsProps) {
const snapshot = props.snapshot; const snapshot = props.snapshot;
const channels = snapshot?.channels as Record<string, unknown> | null; const channels = snapshot?.channels as Record<string, unknown> | null;
if (!snapshot || !channels) return false; if (!snapshot || !channels) {return false;}
const channelStatus = channels[key] as Record<string, unknown> | undefined; const channelStatus = channels[key] as Record<string, unknown> | undefined;
const configured = typeof channelStatus?.configured === "boolean" && channelStatus.configured; const configured = typeof channelStatus?.configured === "boolean" && channelStatus.configured;
const running = typeof channelStatus?.running === "boolean" && channelStatus.running; const running = typeof channelStatus?.running === "boolean" && channelStatus.running;
@@ -39,6 +39,6 @@ export function renderChannelAccountCount(
channelAccounts?: Record<string, ChannelAccountSnapshot[]> | null, channelAccounts?: Record<string, ChannelAccountSnapshot[]> | null,
) { ) {
const count = getChannelAccountCount(key, channelAccounts); const count = getChannelAccountCount(key, channelAccounts);
if (count < 2) return nothing; if (count < 2) {return nothing;}
return html`<div class="account-count">Accounts (${count})</div>`; return html`<div class="account-count">Accounts (${count})</div>`;
} }

View File

@@ -43,8 +43,8 @@ export function renderChannels(props: ChannelsProps) {
enabled: channelEnabled(key, props), enabled: channelEnabled(key, props),
order: index, order: index,
})) }))
.sort((a, b) => { .toSorted((a, b) => {
if (a.enabled !== b.enabled) return a.enabled ? -1 : 1; if (a.enabled !== b.enabled) {return a.enabled ? -1 : 1;}
return a.order - b.order; return a.order - b.order;
}); });
@@ -89,7 +89,7 @@ ${props.snapshot ? JSON.stringify(props.snapshot, null, 2) : "No snapshot yet."}
function resolveChannelOrder(snapshot: ChannelsStatusSnapshot | null): ChannelKey[] { function resolveChannelOrder(snapshot: ChannelsStatusSnapshot | null): ChannelKey[] {
if (snapshot?.channelMeta?.length) { if (snapshot?.channelMeta?.length) {
return snapshot.channelMeta.map((entry) => entry.id) as ChannelKey[]; return snapshot.channelMeta.map((entry) => entry.id);
} }
if (snapshot?.channelOrder?.length) { if (snapshot?.channelOrder?.length) {
return snapshot.channelOrder; return snapshot.channelOrder;
@@ -236,7 +236,7 @@ function renderGenericChannelCard(
function resolveChannelMetaMap( function resolveChannelMetaMap(
snapshot: ChannelsStatusSnapshot | null, snapshot: ChannelsStatusSnapshot | null,
): Record<string, ChannelUiMetaEntry> { ): Record<string, ChannelUiMetaEntry> {
if (!snapshot?.channelMeta?.length) return {}; if (!snapshot?.channelMeta?.length) {return {};}
return Object.fromEntries(snapshot.channelMeta.map((entry) => [entry.id, entry])); return Object.fromEntries(snapshot.channelMeta.map((entry) => [entry.id, entry]));
} }
@@ -248,22 +248,22 @@ function resolveChannelLabel(snapshot: ChannelsStatusSnapshot | null, key: strin
const RECENT_ACTIVITY_THRESHOLD_MS = 10 * 60 * 1000; // 10 minutes const RECENT_ACTIVITY_THRESHOLD_MS = 10 * 60 * 1000; // 10 minutes
function hasRecentActivity(account: ChannelAccountSnapshot): boolean { function hasRecentActivity(account: ChannelAccountSnapshot): boolean {
if (!account.lastInboundAt) return false; if (!account.lastInboundAt) {return false;}
return Date.now() - account.lastInboundAt < RECENT_ACTIVITY_THRESHOLD_MS; return Date.now() - account.lastInboundAt < RECENT_ACTIVITY_THRESHOLD_MS;
} }
function deriveRunningStatus(account: ChannelAccountSnapshot): "Yes" | "No" | "Active" { function deriveRunningStatus(account: ChannelAccountSnapshot): "Yes" | "No" | "Active" {
if (account.running) return "Yes"; if (account.running) {return "Yes";}
// If we have recent inbound activity, the channel is effectively running // If we have recent inbound activity, the channel is effectively running
if (hasRecentActivity(account)) return "Active"; if (hasRecentActivity(account)) {return "Active";}
return "No"; return "No";
} }
function deriveConnectedStatus(account: ChannelAccountSnapshot): "Yes" | "No" | "Active" | "n/a" { function deriveConnectedStatus(account: ChannelAccountSnapshot): "Yes" | "No" | "Active" | "n/a" {
if (account.connected === true) return "Yes"; if (account.connected === true) {return "Yes";}
if (account.connected === false) return "No"; if (account.connected === false) {return "No";}
// If connected is null/undefined but we have recent activity, show as active // If connected is null/undefined but we have recent activity, show as active
if (hasRecentActivity(account)) return "Active"; if (hasRecentActivity(account)) {return "Active";}
return "n/a"; return "n/a";
} }

View File

@@ -75,7 +75,7 @@ function adjustTextareaHeight(el: HTMLTextAreaElement) {
} }
function renderCompactionIndicator(status: CompactionIndicatorStatus | null | undefined) { function renderCompactionIndicator(status: CompactionIndicatorStatus | null | undefined) {
if (!status) return nothing; if (!status) {return nothing;}
// Show "compacting..." while active // Show "compacting..." while active
if (status.active) { if (status.active) {
@@ -107,7 +107,7 @@ function generateAttachmentId(): string {
function handlePaste(e: ClipboardEvent, props: ChatProps) { function handlePaste(e: ClipboardEvent, props: ChatProps) {
const items = e.clipboardData?.items; const items = e.clipboardData?.items;
if (!items || !props.onAttachmentsChange) return; if (!items || !props.onAttachmentsChange) {return;}
const imageItems: DataTransferItem[] = []; const imageItems: DataTransferItem[] = [];
for (let i = 0; i < items.length; i++) { for (let i = 0; i < items.length; i++) {
@@ -117,13 +117,13 @@ function handlePaste(e: ClipboardEvent, props: ChatProps) {
} }
} }
if (imageItems.length === 0) return; if (imageItems.length === 0) {return;}
e.preventDefault(); e.preventDefault();
for (const item of imageItems) { for (const item of imageItems) {
const file = item.getAsFile(); const file = item.getAsFile();
if (!file) continue; if (!file) {continue;}
const reader = new FileReader(); const reader = new FileReader();
reader.onload = () => { reader.onload = () => {
@@ -142,7 +142,7 @@ function handlePaste(e: ClipboardEvent, props: ChatProps) {
function renderAttachmentPreview(props: ChatProps) { function renderAttachmentPreview(props: ChatProps) {
const attachments = props.attachments ?? []; const attachments = props.attachments ?? [];
if (attachments.length === 0) return nothing; if (attachments.length === 0) {return nothing;}
return html` return html`
<div class="chat-attachments"> <div class="chat-attachments">
@@ -286,7 +286,7 @@ export function renderChat(props: ChatProps) {
error: props.sidebarError ?? null, error: props.sidebarError ?? null,
onClose: props.onCloseSidebar!, onClose: props.onCloseSidebar!,
onViewRawText: () => { onViewRawText: () => {
if (!props.sidebarContent || !props.onOpenSidebar) return; if (!props.sidebarContent || !props.onOpenSidebar) {return;}
props.onOpenSidebar(`\`\`\`\n${props.sidebarContent}\n\`\`\``); props.onOpenSidebar(`\`\`\`\n${props.sidebarContent}\n\`\`\``);
}, },
})} })}
@@ -338,12 +338,12 @@ export function renderChat(props: ChatProps) {
.value=${props.draft} .value=${props.draft}
?disabled=${!props.connected} ?disabled=${!props.connected}
@keydown=${(e: KeyboardEvent) => { @keydown=${(e: KeyboardEvent) => {
if (e.key !== "Enter") return; if (e.key !== "Enter") {return;}
if (e.isComposing || e.keyCode === 229) return; if (e.isComposing || e.keyCode === 229) {return;}
if (e.shiftKey) return; // Allow Shift+Enter for line breaks if (e.shiftKey) {return;} // Allow Shift+Enter for line breaks
if (!props.connected) return; if (!props.connected) {return;}
e.preventDefault(); e.preventDefault();
if (canCompose) props.onSend(); if (canCompose) {props.onSend();}
}} }}
@input=${(e: Event) => { @input=${(e: Event) => {
const target = e.target as HTMLTextAreaElement; const target = e.target as HTMLTextAreaElement;
@@ -397,7 +397,7 @@ function groupMessages(items: ChatItem[]): Array<ChatItem | MessageGroup> {
const timestamp = normalized.timestamp || Date.now(); const timestamp = normalized.timestamp || Date.now();
if (!currentGroup || currentGroup.role !== role) { if (!currentGroup || currentGroup.role !== role) {
if (currentGroup) result.push(currentGroup); if (currentGroup) {result.push(currentGroup);}
currentGroup = { currentGroup = {
kind: "group", kind: "group",
key: `group:${role}:${item.key}`, key: `group:${role}:${item.key}`,
@@ -411,7 +411,7 @@ function groupMessages(items: ChatItem[]): Array<ChatItem | MessageGroup> {
} }
} }
if (currentGroup) result.push(currentGroup); if (currentGroup) {result.push(currentGroup);}
return result; return result;
} }
@@ -475,13 +475,13 @@ function buildChatItems(props: ChatProps): Array<ChatItem | MessageGroup> {
function messageKey(message: unknown, index: number): string { function messageKey(message: unknown, index: number): string {
const m = message as Record<string, unknown>; const m = message as Record<string, unknown>;
const toolCallId = typeof m.toolCallId === "string" ? m.toolCallId : ""; const toolCallId = typeof m.toolCallId === "string" ? m.toolCallId : "";
if (toolCallId) return `tool:${toolCallId}`; if (toolCallId) {return `tool:${toolCallId}`;}
const id = typeof m.id === "string" ? m.id : ""; const id = typeof m.id === "string" ? m.id : "";
if (id) return `msg:${id}`; if (id) {return `msg:${id}`;}
const messageId = typeof m.messageId === "string" ? m.messageId : ""; const messageId = typeof m.messageId === "string" ? m.messageId : "";
if (messageId) return `msg:${messageId}`; if (messageId) {return `msg:${messageId}`;}
const timestamp = typeof m.timestamp === "number" ? m.timestamp : null; const timestamp = typeof m.timestamp === "number" ? m.timestamp : null;
const role = typeof m.role === "string" ? m.role : "unknown"; const role = typeof m.role === "string" ? m.role : "unknown";
if (timestamp != null) return `msg:${role}:${timestamp}:${index}`; if (timestamp != null) {return `msg:${role}:${timestamp}:${index}`;}
return `msg:${role}:${index}`; return `msg:${role}:${index}`;
} }

View File

@@ -41,7 +41,7 @@ function normalizeSchemaNode(
if (schema.anyOf || schema.oneOf || schema.allOf) { if (schema.anyOf || schema.oneOf || schema.allOf) {
const union = normalizeUnion(schema, path); const union = normalizeUnion(schema, path);
if (union) return union; if (union) {return union;}
return { schema, unsupportedPaths: [pathLabel] }; return { schema, unsupportedPaths: [pathLabel] };
} }
@@ -54,8 +54,8 @@ function normalizeSchemaNode(
if (normalized.enum) { if (normalized.enum) {
const { enumValues, nullable: enumNullable } = normalizeEnum(normalized.enum); const { enumValues, nullable: enumNullable } = normalizeEnum(normalized.enum);
normalized.enum = enumValues; normalized.enum = enumValues;
if (enumNullable) normalized.nullable = true; if (enumNullable) {normalized.nullable = true;}
if (enumValues.length === 0) unsupported.add(pathLabel); if (enumValues.length === 0) {unsupported.add(pathLabel);}
} }
if (type === "object") { if (type === "object") {
@@ -63,8 +63,8 @@ function normalizeSchemaNode(
const normalizedProps: Record<string, JsonSchema> = {}; const normalizedProps: Record<string, JsonSchema> = {};
for (const [key, value] of Object.entries(properties)) { for (const [key, value] of Object.entries(properties)) {
const res = normalizeSchemaNode(value, [...path, key]); const res = normalizeSchemaNode(value, [...path, key]);
if (res.schema) normalizedProps[key] = res.schema; if (res.schema) {normalizedProps[key] = res.schema;}
for (const entry of res.unsupportedPaths) unsupported.add(entry); for (const entry of res.unsupportedPaths) {unsupported.add(entry);}
} }
normalized.properties = normalizedProps; normalized.properties = normalizedProps;
@@ -73,10 +73,10 @@ function normalizeSchemaNode(
} else if (schema.additionalProperties === false) { } else if (schema.additionalProperties === false) {
normalized.additionalProperties = false; normalized.additionalProperties = false;
} else if (schema.additionalProperties && typeof schema.additionalProperties === "object") { } else if (schema.additionalProperties && typeof schema.additionalProperties === "object") {
if (!isAnySchema(schema.additionalProperties as JsonSchema)) { if (!isAnySchema(schema.additionalProperties)) {
const res = normalizeSchemaNode(schema.additionalProperties as JsonSchema, [...path, "*"]); const res = normalizeSchemaNode(schema.additionalProperties, [...path, "*"]);
normalized.additionalProperties = res.schema ?? (schema.additionalProperties as JsonSchema); normalized.additionalProperties = res.schema ?? (schema.additionalProperties);
if (res.unsupportedPaths.length > 0) unsupported.add(pathLabel); if (res.unsupportedPaths.length > 0) {unsupported.add(pathLabel);}
} }
} }
} else if (type === "array") { } else if (type === "array") {
@@ -86,7 +86,7 @@ function normalizeSchemaNode(
} else { } else {
const res = normalizeSchemaNode(itemsSchema, [...path, "*"]); const res = normalizeSchemaNode(itemsSchema, [...path, "*"]);
normalized.items = res.schema ?? itemsSchema; normalized.items = res.schema ?? itemsSchema;
if (res.unsupportedPaths.length > 0) unsupported.add(pathLabel); if (res.unsupportedPaths.length > 0) {unsupported.add(pathLabel);}
} }
} else if ( } else if (
type !== "string" && type !== "string" &&
@@ -108,20 +108,20 @@ function normalizeUnion(
schema: JsonSchema, schema: JsonSchema,
path: Array<string | number>, path: Array<string | number>,
): ConfigSchemaAnalysis | null { ): ConfigSchemaAnalysis | null {
if (schema.allOf) return null; if (schema.allOf) {return null;}
const union = schema.anyOf ?? schema.oneOf; const union = schema.anyOf ?? schema.oneOf;
if (!union) return null; if (!union) {return null;}
const literals: unknown[] = []; const literals: unknown[] = [];
const remaining: JsonSchema[] = []; const remaining: JsonSchema[] = [];
let nullable = false; let nullable = false;
for (const entry of union) { for (const entry of union) {
if (!entry || typeof entry !== "object") return null; if (!entry || typeof entry !== "object") {return null;}
if (Array.isArray(entry.enum)) { if (Array.isArray(entry.enum)) {
const { enumValues, nullable: enumNullable } = normalizeEnum(entry.enum); const { enumValues, nullable: enumNullable } = normalizeEnum(entry.enum);
literals.push(...enumValues); literals.push(...enumValues);
if (enumNullable) nullable = true; if (enumNullable) {nullable = true;}
continue; continue;
} }
if ("const" in entry) { if ("const" in entry) {
@@ -167,11 +167,11 @@ function normalizeUnion(
return res; return res;
} }
const primitiveTypes = ["string", "number", "integer", "boolean"]; const primitiveTypes = new Set(["string", "number", "integer", "boolean"]);
if ( if (
remaining.length > 0 && remaining.length > 0 &&
literals.length === 0 && literals.length === 0 &&
remaining.every((entry) => entry.type && primitiveTypes.includes(String(entry.type))) remaining.every((entry) => entry.type && primitiveTypes.has(String(entry.type)))
) { ) {
return { return {
schema: { schema: {

View File

@@ -18,7 +18,7 @@ function isAnySchema(schema: JsonSchema): boolean {
} }
function jsonValue(value: unknown): string { function jsonValue(value: unknown): string {
if (value === undefined) return ""; if (value === undefined) {return "";}
try { try {
return JSON.stringify(value, null, 2) ?? ""; return JSON.stringify(value, null, 2) ?? "";
} catch { } catch {
@@ -131,8 +131,8 @@ export function renderNode(params: {
// Check if it's a set of literal values (enum-like) // Check if it's a set of literal values (enum-like)
const extractLiteral = (v: JsonSchema): unknown | undefined => { const extractLiteral = (v: JsonSchema): unknown | undefined => {
if (v.const !== undefined) return v.const; if (v.const !== undefined) {return v.const;}
if (v.enum && v.enum.length === 1) return v.enum[0]; if (v.enum && v.enum.length === 1) {return v.enum[0];}
return undefined; return undefined;
}; };
const literals = nonNull.map(extractLiteral); const literals = nonNull.map(extractLiteral);
@@ -326,7 +326,7 @@ function renderTextInput(params: {
onPatch(path, raw); onPatch(path, raw);
}} }}
@change=${(e: Event) => { @change=${(e: Event) => {
if (inputType === "number") return; if (inputType === "number") {return;}
const raw = (e.target as HTMLInputElement).value; const raw = (e.target as HTMLInputElement).value;
onPatch(path, raw.trim()); onPatch(path, raw.trim());
}} }}
@@ -469,10 +469,10 @@ function renderObject(params: {
const entries = Object.entries(props); const entries = Object.entries(props);
// Sort by hint order // Sort by hint order
const sorted = entries.sort((a, b) => { const sorted = entries.toSorted((a, b) => {
const orderA = hintForPath([...path, a[0]], hints)?.order ?? 0; const orderA = hintForPath([...path, a[0]], hints)?.order ?? 0;
const orderB = hintForPath([...path, b[0]], hints)?.order ?? 0; const orderB = hintForPath([...path, b[0]], hints)?.order ?? 0;
if (orderA !== orderB) return orderA - orderB; if (orderA !== orderB) {return orderA - orderB;}
return a[0].localeCompare(b[0]); return a[0].localeCompare(b[0]);
}); });
@@ -498,7 +498,7 @@ function renderObject(params: {
${ ${
allowExtra allowExtra
? renderMapField({ ? renderMapField({
schema: additional as JsonSchema, schema: additional,
value: obj, value: obj,
path, path,
hints, hints,
@@ -536,7 +536,7 @@ function renderObject(params: {
${ ${
allowExtra allowExtra
? renderMapField({ ? renderMapField({
schema: additional as JsonSchema, schema: additional,
value: obj, value: obj,
path, path,
hints, hints,
@@ -671,7 +671,7 @@ function renderMapField(params: {
class="cfg-map__add" class="cfg-map__add"
?disabled=${disabled} ?disabled=${disabled}
@click=${() => { @click=${() => {
const next = { ...(value ?? {}) }; const next = { ...value };
let index = 1; let index = 1;
let key = `custom-${index}`; let key = `custom-${index}`;
while (key in next) { while (key in next) {
@@ -708,9 +708,9 @@ function renderMapField(params: {
?disabled=${disabled} ?disabled=${disabled}
@change=${(e: Event) => { @change=${(e: Event) => {
const nextKey = (e.target as HTMLInputElement).value.trim(); const nextKey = (e.target as HTMLInputElement).value.trim();
if (!nextKey || nextKey === key) return; if (!nextKey || nextKey === key) {return;}
const next = { ...(value ?? {}) }; const next = { ...value };
if (nextKey in next) return; if (nextKey in next) {return;}
next[nextKey] = next[key]; next[nextKey] = next[key];
delete next[key]; delete next[key];
onPatch(path, next); onPatch(path, next);
@@ -760,7 +760,7 @@ function renderMapField(params: {
title="Remove entry" title="Remove entry"
?disabled=${disabled} ?disabled=${disabled}
@click=${() => { @click=${() => {
const next = { ...(value ?? {}) }; const next = { ...value };
delete next[key]; delete next[key];
onPatch(path, next); onPatch(path, next);
}} }}

View File

@@ -279,49 +279,49 @@ function getSectionIcon(key: string) {
} }
function matchesSearch(key: string, schema: JsonSchema, query: string): boolean { function matchesSearch(key: string, schema: JsonSchema, query: string): boolean {
if (!query) return true; if (!query) {return true;}
const q = query.toLowerCase(); const q = query.toLowerCase();
const meta = SECTION_META[key]; const meta = SECTION_META[key];
// Check key name // Check key name
if (key.toLowerCase().includes(q)) return true; if (key.toLowerCase().includes(q)) {return true;}
// Check label and description // Check label and description
if (meta) { if (meta) {
if (meta.label.toLowerCase().includes(q)) return true; if (meta.label.toLowerCase().includes(q)) {return true;}
if (meta.description.toLowerCase().includes(q)) return true; if (meta.description.toLowerCase().includes(q)) {return true;}
} }
return schemaMatches(schema, q); return schemaMatches(schema, q);
} }
function schemaMatches(schema: JsonSchema, query: string): boolean { function schemaMatches(schema: JsonSchema, query: string): boolean {
if (schema.title?.toLowerCase().includes(query)) return true; if (schema.title?.toLowerCase().includes(query)) {return true;}
if (schema.description?.toLowerCase().includes(query)) return true; if (schema.description?.toLowerCase().includes(query)) {return true;}
if (schema.enum?.some((value) => String(value).toLowerCase().includes(query))) return true; if (schema.enum?.some((value) => String(value).toLowerCase().includes(query))) {return true;}
if (schema.properties) { if (schema.properties) {
for (const [propKey, propSchema] of Object.entries(schema.properties)) { for (const [propKey, propSchema] of Object.entries(schema.properties)) {
if (propKey.toLowerCase().includes(query)) return true; if (propKey.toLowerCase().includes(query)) {return true;}
if (schemaMatches(propSchema, query)) return true; if (schemaMatches(propSchema, query)) {return true;}
} }
} }
if (schema.items) { if (schema.items) {
const items = Array.isArray(schema.items) ? schema.items : [schema.items]; const items = Array.isArray(schema.items) ? schema.items : [schema.items];
for (const item of items) { for (const item of items) {
if (item && schemaMatches(item, query)) return true; if (item && schemaMatches(item, query)) {return true;}
} }
} }
if (schema.additionalProperties && typeof schema.additionalProperties === "object") { if (schema.additionalProperties && typeof schema.additionalProperties === "object") {
if (schemaMatches(schema.additionalProperties, query)) return true; if (schemaMatches(schema.additionalProperties, query)) {return true;}
} }
const unions = schema.anyOf ?? schema.oneOf ?? schema.allOf; const unions = schema.anyOf ?? schema.oneOf ?? schema.allOf;
if (unions) { if (unions) {
for (const entry of unions) { for (const entry of unions) {
if (entry && schemaMatches(entry, query)) return true; if (entry && schemaMatches(entry, query)) {return true;}
} }
} }
@@ -347,16 +347,16 @@ export function renderConfigForm(props: ConfigFormProps) {
const activeSection = props.activeSection; const activeSection = props.activeSection;
const activeSubsection = props.activeSubsection ?? null; const activeSubsection = props.activeSubsection ?? null;
const entries = Object.entries(properties).sort((a, b) => { const entries = Object.entries(properties).toSorted((a, b) => {
const orderA = hintForPath([a[0]], props.uiHints)?.order ?? 50; const orderA = hintForPath([a[0]], props.uiHints)?.order ?? 50;
const orderB = hintForPath([b[0]], props.uiHints)?.order ?? 50; const orderB = hintForPath([b[0]], props.uiHints)?.order ?? 50;
if (orderA !== orderB) return orderA - orderB; if (orderA !== orderB) {return orderA - orderB;}
return a[0].localeCompare(b[0]); return a[0].localeCompare(b[0]);
}); });
const filteredEntries = entries.filter(([key, node]) => { const filteredEntries = entries.filter(([key, node]) => {
if (activeSection && key !== activeSection) return false; if (activeSection && key !== activeSection) {return false;}
if (searchQuery && !matchesSearch(key, node, searchQuery)) return false; if (searchQuery && !matchesSearch(key, node, searchQuery)) {return false;}
return true; return true;
}); });
@@ -398,7 +398,7 @@ export function renderConfigForm(props: ConfigFormProps) {
const hint = hintForPath([sectionKey, subsectionKey], props.uiHints); const hint = hintForPath([sectionKey, subsectionKey], props.uiHints);
const label = hint?.label ?? node.title ?? humanize(subsectionKey); const label = hint?.label ?? node.title ?? humanize(subsectionKey);
const description = hint?.help ?? node.description ?? ""; const description = hint?.help ?? node.description ?? "";
const sectionValue = (value as Record<string, unknown>)[sectionKey]; const sectionValue = (value)[sectionKey];
const scopedValue = const scopedValue =
sectionValue && typeof sectionValue === "object" sectionValue && typeof sectionValue === "object"
? (sectionValue as Record<string, unknown>)[subsectionKey] ? (sectionValue as Record<string, unknown>)[subsectionKey]
@@ -454,7 +454,7 @@ export function renderConfigForm(props: ConfigFormProps) {
<div class="config-section-card__content"> <div class="config-section-card__content">
${renderNode({ ${renderNode({
schema: node, schema: node,
value: (value as Record<string, unknown>)[key], value: (value)[key],
path: [key], path: [key],
hints: props.uiHints, hints: props.uiHints,
unsupported, unsupported,

View File

@@ -17,7 +17,7 @@ export type JsonSchema = {
}; };
export function schemaType(schema: JsonSchema): string | undefined { export function schemaType(schema: JsonSchema): string | undefined {
if (!schema) return undefined; if (!schema) {return undefined;}
if (Array.isArray(schema.type)) { if (Array.isArray(schema.type)) {
const filtered = schema.type.filter((t) => t !== "null"); const filtered = schema.type.filter((t) => t !== "null");
return filtered[0] ?? schema.type[0]; return filtered[0] ?? schema.type[0];
@@ -26,8 +26,8 @@ export function schemaType(schema: JsonSchema): string | undefined {
} }
export function defaultValue(schema?: JsonSchema): unknown { export function defaultValue(schema?: JsonSchema): unknown {
if (!schema) return ""; if (!schema) {return "";}
if (schema.default !== undefined) return schema.default; if (schema.default !== undefined) {return schema.default;}
const type = schemaType(schema); const type = schemaType(schema);
switch (type) { switch (type) {
case "object": case "object":
@@ -53,12 +53,12 @@ export function pathKey(path: Array<string | number>): string {
export function hintForPath(path: Array<string | number>, hints: ConfigUiHints) { export function hintForPath(path: Array<string | number>, hints: ConfigUiHints) {
const key = pathKey(path); const key = pathKey(path);
const direct = hints[key]; const direct = hints[key];
if (direct) return direct; if (direct) {return direct;}
const segments = key.split("."); const segments = key.split(".");
for (const [hintKey, hint] of Object.entries(hints)) { for (const [hintKey, hint] of Object.entries(hints)) {
if (!hintKey.includes("*")) continue; if (!hintKey.includes("*")) {continue;}
const hintSegments = hintKey.split("."); const hintSegments = hintKey.split(".");
if (hintSegments.length !== segments.length) continue; if (hintSegments.length !== segments.length) {continue;}
let match = true; let match = true;
for (let i = 0; i < segments.length; i += 1) { for (let i = 0; i < segments.length; i += 1) {
if (hintSegments[i] !== "*" && hintSegments[i] !== segments[i]) { if (hintSegments[i] !== "*" && hintSegments[i] !== segments[i]) {
@@ -66,7 +66,7 @@ export function hintForPath(path: Array<string | number>, hints: ConfigUiHints)
break; break;
} }
} }
if (match) return hint; if (match) {return hint;}
} }
return undefined; return undefined;
} }

View File

@@ -60,7 +60,7 @@ describe("config view", () => {
const saveButton = Array.from(container.querySelectorAll("button")).find( const saveButton = Array.from(container.querySelectorAll("button")).find(
(btn) => btn.textContent?.trim() === "Save", (btn) => btn.textContent?.trim() === "Save",
) as HTMLButtonElement | undefined; );
expect(saveButton).not.toBeUndefined(); expect(saveButton).not.toBeUndefined();
expect(saveButton?.disabled).toBe(false); expect(saveButton?.disabled).toBe(false);
}); });
@@ -80,7 +80,7 @@ describe("config view", () => {
const saveButton = Array.from(container.querySelectorAll("button")).find( const saveButton = Array.from(container.querySelectorAll("button")).find(
(btn) => btn.textContent?.trim() === "Save", (btn) => btn.textContent?.trim() === "Save",
) as HTMLButtonElement | undefined; );
expect(saveButton).not.toBeUndefined(); expect(saveButton).not.toBeUndefined();
expect(saveButton?.disabled).toBe(true); expect(saveButton?.disabled).toBe(true);
}); });
@@ -99,10 +99,10 @@ describe("config view", () => {
const saveButton = Array.from(container.querySelectorAll("button")).find( const saveButton = Array.from(container.querySelectorAll("button")).find(
(btn) => btn.textContent?.trim() === "Save", (btn) => btn.textContent?.trim() === "Save",
) as HTMLButtonElement | undefined; );
const applyButton = Array.from(container.querySelectorAll("button")).find( const applyButton = Array.from(container.querySelectorAll("button")).find(
(btn) => btn.textContent?.trim() === "Apply", (btn) => btn.textContent?.trim() === "Apply",
) as HTMLButtonElement | undefined; );
expect(saveButton).not.toBeUndefined(); expect(saveButton).not.toBeUndefined();
expect(applyButton).not.toBeUndefined(); expect(applyButton).not.toBeUndefined();
expect(saveButton?.disabled).toBe(true); expect(saveButton?.disabled).toBe(true);
@@ -123,10 +123,10 @@ describe("config view", () => {
const saveButton = Array.from(container.querySelectorAll("button")).find( const saveButton = Array.from(container.querySelectorAll("button")).find(
(btn) => btn.textContent?.trim() === "Save", (btn) => btn.textContent?.trim() === "Save",
) as HTMLButtonElement | undefined; );
const applyButton = Array.from(container.querySelectorAll("button")).find( const applyButton = Array.from(container.querySelectorAll("button")).find(
(btn) => btn.textContent?.trim() === "Apply", (btn) => btn.textContent?.trim() === "Apply",
) as HTMLButtonElement | undefined; );
expect(saveButton).not.toBeUndefined(); expect(saveButton).not.toBeUndefined();
expect(applyButton).not.toBeUndefined(); expect(applyButton).not.toBeUndefined();
expect(saveButton?.disabled).toBe(false); expect(saveButton?.disabled).toBe(false);
@@ -146,7 +146,7 @@ describe("config view", () => {
const btn = Array.from(container.querySelectorAll("button")).find( const btn = Array.from(container.querySelectorAll("button")).find(
(b) => b.textContent?.trim() === "Raw", (b) => b.textContent?.trim() === "Raw",
) as HTMLButtonElement | undefined; );
expect(btn).toBeTruthy(); expect(btn).toBeTruthy();
btn?.click(); btn?.click();
expect(onFormModeChange).toHaveBeenCalledWith("raw"); expect(onFormModeChange).toHaveBeenCalledWith("raw");
@@ -172,7 +172,7 @@ describe("config view", () => {
const btn = Array.from(container.querySelectorAll("button")).find( const btn = Array.from(container.querySelectorAll("button")).find(
(b) => b.textContent?.trim() === "Gateway", (b) => b.textContent?.trim() === "Gateway",
) as HTMLButtonElement | undefined; );
expect(btn).toBeTruthy(); expect(btn).toBeTruthy();
btn?.click(); btn?.click();
expect(onSectionChange).toHaveBeenCalledWith("gateway"); expect(onSectionChange).toHaveBeenCalledWith("gateway");
@@ -189,9 +189,9 @@ describe("config view", () => {
container, container,
); );
const input = container.querySelector(".config-search__input") as HTMLInputElement | null; const input = container.querySelector(".config-search__input");
expect(input).not.toBeNull(); expect(input).not.toBeNull();
if (!input) return; if (!input) {return;}
input.value = "gateway"; input.value = "gateway";
input.dispatchEvent(new Event("input", { bubbles: true })); input.dispatchEvent(new Event("input", { bubbles: true }));
expect(onSearchChange).toHaveBeenCalledWith("gateway"); expect(onSearchChange).toHaveBeenCalledWith("gateway");

View File

@@ -299,7 +299,7 @@ function resolveSectionMeta(
description?: string; description?: string;
} { } {
const meta = SECTION_META[key]; const meta = SECTION_META[key];
if (meta) return meta; if (meta) {return meta;}
return { return {
label: schema?.title ?? humanize(key), label: schema?.title ?? humanize(key),
description: schema?.description ?? "", description: schema?.description ?? "",
@@ -312,7 +312,7 @@ function resolveSubsections(params: {
uiHints: ConfigUiHints; uiHints: ConfigUiHints;
}): SubsectionEntry[] { }): SubsectionEntry[] {
const { key, schema, uiHints } = params; const { key, schema, uiHints } = params;
if (!schema || schemaType(schema) !== "object" || !schema.properties) return []; if (!schema || schemaType(schema) !== "object" || !schema.properties) {return [];}
const entries = Object.entries(schema.properties).map(([subKey, node]) => { const entries = Object.entries(schema.properties).map(([subKey, node]) => {
const hint = hintForPath([key, subKey], uiHints); const hint = hintForPath([key, subKey], uiHints);
const label = hint?.label ?? node.title ?? humanize(subKey); const label = hint?.label ?? node.title ?? humanize(subKey);
@@ -328,11 +328,11 @@ function computeDiff(
original: Record<string, unknown> | null, original: Record<string, unknown> | null,
current: Record<string, unknown> | null, current: Record<string, unknown> | null,
): Array<{ path: string; from: unknown; to: unknown }> { ): Array<{ path: string; from: unknown; to: unknown }> {
if (!original || !current) return []; if (!original || !current) {return [];}
const changes: Array<{ path: string; from: unknown; to: unknown }> = []; const changes: Array<{ path: string; from: unknown; to: unknown }> = [];
function compare(orig: unknown, curr: unknown, path: string) { function compare(orig: unknown, curr: unknown, path: string) {
if (orig === curr) return; if (orig === curr) {return;}
if (typeof orig !== typeof curr) { if (typeof orig !== typeof curr) {
changes.push({ path, from: orig, to: curr }); changes.push({ path, from: orig, to: curr });
return; return;
@@ -369,7 +369,7 @@ function truncateValue(value: unknown, maxLen = 40): string {
} catch { } catch {
str = String(value); str = String(value);
} }
if (str.length <= maxLen) return str; if (str.length <= maxLen) {return str;}
return str.slice(0, maxLen - 3) + "..."; return str.slice(0, maxLen - 3) + "...";
} }
@@ -392,7 +392,7 @@ export function renderConfig(props: ConfigProps) {
const activeSectionSchema = const activeSectionSchema =
props.activeSection && analysis.schema && schemaType(analysis.schema) === "object" props.activeSection && analysis.schema && schemaType(analysis.schema) === "object"
? (analysis.schema.properties?.[props.activeSection] as JsonSchema | undefined) ? (analysis.schema.properties?.[props.activeSection])
: undefined; : undefined;
const activeSectionMeta = props.activeSection const activeSectionMeta = props.activeSection
? resolveSectionMeta(props.activeSection, activeSectionSchema) ? resolveSectionMeta(props.activeSection, activeSectionSchema)

View File

@@ -63,7 +63,7 @@ describe("cron view", () => {
container, container,
); );
const row = container.querySelector(".list-item-clickable") as HTMLElement | null; const row = container.querySelector(".list-item-clickable");
expect(row).not.toBeNull(); expect(row).not.toBeNull();
row?.dispatchEvent(new MouseEvent("click", { bubbles: true })); row?.dispatchEvent(new MouseEvent("click", { bubbles: true }));

View File

@@ -38,16 +38,16 @@ function buildChannelOptions(props: CronProps): string[] {
} }
const seen = new Set<string>(); const seen = new Set<string>();
return options.filter((value) => { return options.filter((value) => {
if (seen.has(value)) return false; if (seen.has(value)) {return false;}
seen.add(value); seen.add(value);
return true; return true;
}); });
} }
function resolveChannelLabel(props: CronProps, channel: string): string { function resolveChannelLabel(props: CronProps, channel: string): string {
if (channel === "last") return "last"; if (channel === "last") {return "last";}
const meta = props.channelMeta?.find((entry) => entry.id === channel); const meta = props.channelMeta?.find((entry) => entry.id === channel);
if (meta?.label) return meta.label; if (meta?.label) {return meta.label;}
return props.channelLabels?.[channel] ?? channel; return props.channelLabels?.[channel] ?? channel;
} }
@@ -213,7 +213,7 @@ export function renderCron(props: CronProps) {
@change=${(e: Event) => @change=${(e: Event) =>
props.onFormChange({ props.onFormChange({
channel: (e.target as HTMLSelectElement) channel: (e.target as HTMLSelectElement)
.value as CronFormState["channel"], .value,
})} })}
> >
${channelOptions.map( ${channelOptions.map(

View File

@@ -4,21 +4,21 @@ import type { AppViewState } from "../app-view-state";
function formatRemaining(ms: number): string { function formatRemaining(ms: number): string {
const remaining = Math.max(0, ms); const remaining = Math.max(0, ms);
const totalSeconds = Math.floor(remaining / 1000); const totalSeconds = Math.floor(remaining / 1000);
if (totalSeconds < 60) return `${totalSeconds}s`; if (totalSeconds < 60) {return `${totalSeconds}s`;}
const minutes = Math.floor(totalSeconds / 60); const minutes = Math.floor(totalSeconds / 60);
if (minutes < 60) return `${minutes}m`; if (minutes < 60) {return `${minutes}m`;}
const hours = Math.floor(minutes / 60); const hours = Math.floor(minutes / 60);
return `${hours}h`; return `${hours}h`;
} }
function renderMetaRow(label: string, value?: string | null) { function renderMetaRow(label: string, value?: string | null) {
if (!value) return nothing; if (!value) {return nothing;}
return html`<div class="exec-approval-meta-row"><span>${label}</span><span>${value}</span></div>`; return html`<div class="exec-approval-meta-row"><span>${label}</span><span>${value}</span></div>`;
} }
export function renderExecApprovalPrompt(state: AppViewState) { export function renderExecApprovalPrompt(state: AppViewState) {
const active = state.execApprovalQueue[0]; const active = state.execApprovalQueue[0];
if (!active) return nothing; if (!active) {return nothing;}
const request = active.request; const request = active.request;
const remainingMs = active.expiresAtMs - Date.now(); const remainingMs = active.expiresAtMs - Date.now();
const remaining = remainingMs > 0 ? `expires in ${formatRemaining(remainingMs)}` : "expired"; const remaining = remainingMs > 0 ? `expires in ${formatRemaining(remainingMs)}` : "expired";

View File

@@ -3,7 +3,7 @@ import type { AppViewState } from "../app-view-state";
export function renderGatewayUrlConfirmation(state: AppViewState) { export function renderGatewayUrlConfirmation(state: AppViewState) {
const { pendingGatewayUrl } = state; const { pendingGatewayUrl } = state;
if (!pendingGatewayUrl) return nothing; if (!pendingGatewayUrl) {return nothing;}
return html` return html`
<div class="exec-approval-overlay" role="dialog" aria-modal="true" aria-live="polite"> <div class="exec-approval-overlay" role="dialog" aria-modal="true" aria-live="polite">

View File

@@ -21,14 +21,14 @@ export type LogsProps = {
}; };
function formatTime(value?: string | null) { function formatTime(value?: string | null) {
if (!value) return ""; if (!value) {return "";}
const date = new Date(value); const date = new Date(value);
if (Number.isNaN(date.getTime())) return value; if (Number.isNaN(date.getTime())) {return value;}
return date.toLocaleTimeString(); return date.toLocaleTimeString();
} }
function matchesFilter(entry: LogEntry, needle: string) { function matchesFilter(entry: LogEntry, needle: string) {
if (!needle) return true; if (!needle) {return true;}
const haystack = [entry.message, entry.subsystem, entry.raw] const haystack = [entry.message, entry.subsystem, entry.raw]
.filter(Boolean) .filter(Boolean)
.join(" ") .join(" ")
@@ -40,7 +40,7 @@ export function renderLogs(props: LogsProps) {
const needle = props.filterText.trim().toLowerCase(); const needle = props.filterText.trim().toLowerCase();
const levelFiltered = LEVELS.some((level) => !props.levelFilters[level]); const levelFiltered = LEVELS.some((level) => !props.levelFilters[level]);
const filtered = props.entries.filter((entry) => { const filtered = props.entries.filter((entry) => {
if (entry.level && !props.levelFilters[entry.level]) return false; if (entry.level && !props.levelFilters[entry.level]) {return false;}
return matchesFilter(entry, needle); return matchesFilter(entry, needle);
}); });
const exportLabel = needle || levelFiltered ? "filtered" : "visible"; const exportLabel = needle || levelFiltered ? "filtered" : "visible";

View File

@@ -328,12 +328,12 @@ function resolveBindingsState(props: NodesProps): BindingState {
} }
function normalizeSecurity(value?: string): ExecSecurity { function normalizeSecurity(value?: string): ExecSecurity {
if (value === "allowlist" || value === "full" || value === "deny") return value; if (value === "allowlist" || value === "full" || value === "deny") {return value;}
return "deny"; return "deny";
} }
function normalizeAsk(value?: string): ExecAsk { function normalizeAsk(value?: string): ExecAsk {
if (value === "always" || value === "off" || value === "on-miss") return value; if (value === "always" || value === "off" || value === "on-miss") {return value;}
return "on-miss"; return "on-miss";
} }
@@ -354,10 +354,10 @@ function resolveConfigAgents(config: Record<string, unknown> | null): ExecApprov
const list = Array.isArray(agentsNode.list) ? agentsNode.list : []; const list = Array.isArray(agentsNode.list) ? agentsNode.list : [];
const agents: ExecApprovalsAgentOption[] = []; const agents: ExecApprovalsAgentOption[] = [];
list.forEach((entry) => { list.forEach((entry) => {
if (!entry || typeof entry !== "object") return; if (!entry || typeof entry !== "object") {return;}
const record = entry as Record<string, unknown>; const record = entry as Record<string, unknown>;
const id = typeof record.id === "string" ? record.id.trim() : ""; const id = typeof record.id === "string" ? record.id.trim() : "";
if (!id) return; if (!id) {return;}
const name = typeof record.name === "string" ? record.name.trim() : undefined; const name = typeof record.name === "string" ? record.name.trim() : undefined;
const isDefault = record.default === true; const isDefault = record.default === true;
agents.push({ id, name: name || undefined, isDefault }); agents.push({ id, name: name || undefined, isDefault });
@@ -374,7 +374,7 @@ function resolveExecApprovalsAgents(
const merged = new Map<string, ExecApprovalsAgentOption>(); const merged = new Map<string, ExecApprovalsAgentOption>();
configAgents.forEach((agent) => merged.set(agent.id, agent)); configAgents.forEach((agent) => merged.set(agent.id, agent));
approvalsAgents.forEach((id) => { approvalsAgents.forEach((id) => {
if (merged.has(id)) return; if (merged.has(id)) {return;}
merged.set(id, { id }); merged.set(id, { id });
}); });
const agents = Array.from(merged.values()); const agents = Array.from(merged.values());
@@ -382,8 +382,8 @@ function resolveExecApprovalsAgents(
agents.push({ id: "main", isDefault: true }); agents.push({ id: "main", isDefault: true });
} }
agents.sort((a, b) => { agents.sort((a, b) => {
if (a.isDefault && !b.isDefault) return -1; if (a.isDefault && !b.isDefault) {return -1;}
if (!a.isDefault && b.isDefault) return 1; if (!a.isDefault && b.isDefault) {return 1;}
const aLabel = a.name?.trim() ? a.name : a.id; const aLabel = a.name?.trim() ? a.name : a.id;
const bLabel = b.name?.trim() ? b.name : b.id; const bLabel = b.name?.trim() ? b.name : b.id;
return aLabel.localeCompare(bLabel); return aLabel.localeCompare(bLabel);
@@ -395,8 +395,8 @@ function resolveExecApprovalsScope(
selected: string | null, selected: string | null,
agents: ExecApprovalsAgentOption[], agents: ExecApprovalsAgentOption[],
): string { ): string {
if (selected === EXEC_APPROVALS_DEFAULT_SCOPE) return EXEC_APPROVALS_DEFAULT_SCOPE; if (selected === EXEC_APPROVALS_DEFAULT_SCOPE) {return EXEC_APPROVALS_DEFAULT_SCOPE;}
if (selected && agents.some((agent) => agent.id === selected)) return selected; if (selected && agents.some((agent) => agent.id === selected)) {return selected;}
return EXEC_APPROVALS_DEFAULT_SCOPE; return EXEC_APPROVALS_DEFAULT_SCOPE;
} }
@@ -1010,9 +1010,9 @@ function resolveExecNodes(nodes: Array<Record<string, unknown>>): BindingNode[]
for (const node of nodes) { for (const node of nodes) {
const commands = Array.isArray(node.commands) ? node.commands : []; const commands = Array.isArray(node.commands) ? node.commands : [];
const supports = commands.some((cmd) => String(cmd) === "system.run"); const supports = commands.some((cmd) => String(cmd) === "system.run");
if (!supports) continue; if (!supports) {continue;}
const nodeId = typeof node.nodeId === "string" ? node.nodeId.trim() : ""; const nodeId = typeof node.nodeId === "string" ? node.nodeId.trim() : "";
if (!nodeId) continue; if (!nodeId) {continue;}
const displayName = const displayName =
typeof node.displayName === "string" && node.displayName.trim() typeof node.displayName === "string" && node.displayName.trim()
? node.displayName.trim() ? node.displayName.trim()
@@ -1036,9 +1036,9 @@ function resolveExecApprovalsNodes(
(cmd) => (cmd) =>
String(cmd) === "system.execApprovals.get" || String(cmd) === "system.execApprovals.set", String(cmd) === "system.execApprovals.get" || String(cmd) === "system.execApprovals.set",
); );
if (!supports) continue; if (!supports) {continue;}
const nodeId = typeof node.nodeId === "string" ? node.nodeId.trim() : ""; const nodeId = typeof node.nodeId === "string" ? node.nodeId.trim() : "";
if (!nodeId) continue; if (!nodeId) {continue;}
const displayName = const displayName =
typeof node.displayName === "string" && node.displayName.trim() typeof node.displayName === "string" && node.displayName.trim()
? node.displayName.trim() ? node.displayName.trim()
@@ -1079,10 +1079,10 @@ function resolveAgentBindings(config: Record<string, unknown> | null): {
const agents: BindingAgent[] = []; const agents: BindingAgent[] = [];
list.forEach((entry, index) => { list.forEach((entry, index) => {
if (!entry || typeof entry !== "object") return; if (!entry || typeof entry !== "object") {return;}
const record = entry as Record<string, unknown>; const record = entry as Record<string, unknown>;
const id = typeof record.id === "string" ? record.id.trim() : ""; const id = typeof record.id === "string" ? record.id.trim() : "";
if (!id) return; if (!id) {return;}
const name = typeof record.name === "string" ? record.name.trim() : undefined; const name = typeof record.name === "string" ? record.name.trim() : undefined;
const isDefault = record.default === true; const isDefault = record.default === true;
const toolsEntry = (record.tools ?? {}) as Record<string, unknown>; const toolsEntry = (record.tools ?? {}) as Record<string, unknown>;

View File

@@ -29,10 +29,10 @@ export function renderOverview(props: OverviewProps) {
const uptime = snapshot?.uptimeMs ? formatDurationMs(snapshot.uptimeMs) : "n/a"; const uptime = snapshot?.uptimeMs ? formatDurationMs(snapshot.uptimeMs) : "n/a";
const tick = snapshot?.policy?.tickIntervalMs ? `${snapshot.policy.tickIntervalMs}ms` : "n/a"; const tick = snapshot?.policy?.tickIntervalMs ? `${snapshot.policy.tickIntervalMs}ms` : "n/a";
const authHint = (() => { const authHint = (() => {
if (props.connected || !props.lastError) return null; if (props.connected || !props.lastError) {return null;}
const lower = props.lastError.toLowerCase(); const lower = props.lastError.toLowerCase();
const authFailed = lower.includes("unauthorized") || lower.includes("connect failed"); const authFailed = lower.includes("unauthorized") || lower.includes("connect failed");
if (!authFailed) return null; if (!authFailed) {return null;}
const hasToken = Boolean(props.settings.token.trim()); const hasToken = Boolean(props.settings.token.trim());
const hasPassword = Boolean(props.password.trim()); const hasPassword = Boolean(props.password.trim());
if (!hasToken && !hasPassword) { if (!hasToken && !hasPassword) {
@@ -74,9 +74,9 @@ export function renderOverview(props: OverviewProps) {
`; `;
})(); })();
const insecureContextHint = (() => { const insecureContextHint = (() => {
if (props.connected || !props.lastError) return null; if (props.connected || !props.lastError) {return null;}
const isSecureContext = typeof window !== "undefined" ? window.isSecureContext : true; const isSecureContext = typeof window !== "undefined" ? window.isSecureContext : true;
if (isSecureContext !== false) return null; if (isSecureContext) {return null;}
const lower = props.lastError.toLowerCase(); const lower = props.lastError.toLowerCase();
if (!lower.includes("secure context") && !lower.includes("device identity required")) { if (!lower.includes("secure context") && !lower.includes("device identity required")) {
return null; return null;

View File

@@ -42,9 +42,9 @@ const VERBOSE_LEVELS = [
const REASONING_LEVELS = ["", "off", "on", "stream"] as const; const REASONING_LEVELS = ["", "off", "on", "stream"] as const;
function normalizeProviderId(provider?: string | null): string { function normalizeProviderId(provider?: string | null): string {
if (!provider) return ""; if (!provider) {return "";}
const normalized = provider.trim().toLowerCase(); const normalized = provider.trim().toLowerCase();
if (normalized === "z.ai" || normalized === "z-ai") return "zai"; if (normalized === "z.ai" || normalized === "z-ai") {return "zai";}
return normalized; return normalized;
} }
@@ -57,15 +57,15 @@ function resolveThinkLevelOptions(provider?: string | null): readonly string[] {
} }
function resolveThinkLevelDisplay(value: string, isBinary: boolean): string { function resolveThinkLevelDisplay(value: string, isBinary: boolean): string {
if (!isBinary) return value; if (!isBinary) {return value;}
if (!value || value === "off") return value; if (!value || value === "off") {return value;}
return "on"; return "on";
} }
function resolveThinkLevelPatchValue(value: string, isBinary: boolean): string | null { function resolveThinkLevelPatchValue(value: string, isBinary: boolean): string | null {
if (!value) return null; if (!value) {return null;}
if (!isBinary) return value; if (!isBinary) {return value;}
if (value === "on") return "low"; if (value === "on") {return "low";}
return value; return value;
} }

View File

@@ -85,8 +85,8 @@ function renderSkill(skill: SkillStatusEntry, props: SkillsProps) {
...skill.missing.os.map((o) => `os:${o}`), ...skill.missing.os.map((o) => `os:${o}`),
]; ];
const reasons: string[] = []; const reasons: string[] = [];
if (skill.disabled) reasons.push("disabled"); if (skill.disabled) {reasons.push("disabled");}
if (skill.blockedByAllowlist) reasons.push("blocked by allowlist"); if (skill.blockedByAllowlist) {reasons.push("blocked by allowlist");}
return html` return html`
<div class="list-item"> <div class="list-item">
<div class="list-main"> <div class="list-main">

View File

@@ -6,9 +6,9 @@ const here = path.dirname(fileURLToPath(import.meta.url));
function normalizeBase(input: string): string { function normalizeBase(input: string): string {
const trimmed = input.trim(); const trimmed = input.trim();
if (!trimmed) return "/"; if (!trimmed) {return "/";}
if (trimmed === "./") return "./"; if (trimmed === "./") {return "./";}
if (trimmed.endsWith("/")) return trimmed; if (trimmed.endsWith("/")) {return trimmed;}
return `${trimmed}/`; return `${trimmed}/`;
} }