chore: Manually fix lint issues in ui.
This commit is contained in:
parent
5ba4586e58
commit
e9a32b83c2
74 changed files with 1552 additions and 600 deletions
|
|
@ -36,15 +36,23 @@ 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 +86,9 @@ 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 +104,9 @@ 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 +115,9 @@ 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 +186,9 @@ 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 = {
|
||||||
|
|
|
||||||
|
|
@ -32,9 +32,13 @@ 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 +50,20 @@ 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 +76,9 @@ 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 +135,13 @@ 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 +161,9 @@ 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 +171,9 @@ 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 +221,9 @@ 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;
|
||||||
|
|
|
||||||
|
|
@ -64,8 +64,12 @@ 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) {
|
||||||
if (!raw) {return mainSessionKey;}
|
return raw;
|
||||||
|
}
|
||||||
|
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 +81,9 @@ 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 +174,9 @@ 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 +206,9 @@ 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;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -75,10 +75,7 @@ export function handleUpdated(host: LifecycleHost, changed: Map<PropertyKey, unk
|
||||||
) {
|
) {
|
||||||
const forcedByTab = changed.has("tab");
|
const forcedByTab = changed.has("tab");
|
||||||
const forcedByLoad =
|
const forcedByLoad =
|
||||||
changed.has("chatLoading") &&
|
changed.has("chatLoading") && changed.get("chatLoading") === true && !host.chatLoading;
|
||||||
changed.get("chatLoading") === true &&
|
|
||||||
!
|
|
||||||
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,
|
||||||
|
|
|
||||||
|
|
@ -11,7 +11,9 @@ 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 +21,49 @@ 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;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -134,7 +134,9 @@ 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 +155,9 @@ 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 +187,28 @@ 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) {
|
||||||
if (sessions?.sessions?.some((row) => row.key === "main")) {return "main";}
|
return mainKey;
|
||||||
|
}
|
||||||
|
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;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,24 +1,5 @@
|
||||||
import { html, nothing } from "lit";
|
import { html, nothing } from "lit";
|
||||||
import type { AppViewState } from "./app-view-state";
|
import type { AppViewState } from "./app-view-state";
|
||||||
import type { GatewayBrowserClient, GatewayHelloOk } from "./gateway";
|
|
||||||
import type { UiSettings } from "./storage";
|
|
||||||
import type { ThemeMode } from "./theme";
|
|
||||||
import type { ThemeTransitionContext } from "./theme-transition";
|
|
||||||
import type {
|
|
||||||
ConfigSnapshot,
|
|
||||||
CronJob,
|
|
||||||
CronRunLogEntry,
|
|
||||||
CronStatus,
|
|
||||||
HealthSnapshot,
|
|
||||||
LogEntry,
|
|
||||||
LogLevel,
|
|
||||||
PresenceEntry,
|
|
||||||
ChannelsStatusSnapshot,
|
|
||||||
SessionsListResult,
|
|
||||||
SkillStatusReport,
|
|
||||||
StatusSummary,
|
|
||||||
} from "./types";
|
|
||||||
import type { ChatQueueItem, CronFormState } from "./ui-types";
|
|
||||||
import { parseAgentSessionKey } from "../../../src/routing/session-key.js";
|
import { parseAgentSessionKey } from "../../../src/routing/session-key.js";
|
||||||
import { refreshChatAvatar } from "./app-chat";
|
import { refreshChatAvatar } from "./app-chat";
|
||||||
import { renderChatControls, renderTab, renderThemeToggle } from "./app-render.helpers";
|
import { renderChatControls, renderTab, renderThemeToggle } from "./app-render.helpers";
|
||||||
|
|
@ -63,17 +44,9 @@ import {
|
||||||
saveSkillApiKey,
|
saveSkillApiKey,
|
||||||
updateSkillEdit,
|
updateSkillEdit,
|
||||||
updateSkillEnabled,
|
updateSkillEnabled,
|
||||||
type SkillMessage,
|
|
||||||
} from "./controllers/skills";
|
} from "./controllers/skills";
|
||||||
import { icons } from "./icons";
|
import { icons } from "./icons";
|
||||||
import {
|
import { TAB_GROUPS, subtitleForTab, titleForTab } from "./navigation";
|
||||||
TAB_GROUPS,
|
|
||||||
iconForTab,
|
|
||||||
pathForTab,
|
|
||||||
subtitleForTab,
|
|
||||||
titleForTab,
|
|
||||||
type Tab,
|
|
||||||
} from "./navigation";
|
|
||||||
import { renderChannels } from "./views/channels";
|
import { renderChannels } from "./views/channels";
|
||||||
import { renderChat } from "./views/chat";
|
import { renderChat } from "./views/chat";
|
||||||
import { renderConfig } from "./views/config";
|
import { renderConfig } from "./views/config";
|
||||||
|
|
@ -98,8 +71,12 @@ 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) {
|
||||||
if (AVATAR_DATA_RE.test(candidate) || AVATAR_HTTP_RE.test(candidate)) {return candidate;}
|
return undefined;
|
||||||
|
}
|
||||||
|
if (AVATAR_DATA_RE.test(candidate) || AVATAR_HTTP_RE.test(candidate)) {
|
||||||
|
return candidate;
|
||||||
|
}
|
||||||
return identity?.avatarUrl;
|
return identity?.avatarUrl;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -486,7 +463,9 @@ 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,
|
||||||
|
|
|
||||||
|
|
@ -12,7 +12,9 @@ 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 +27,9 @@ 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 +38,32 @@ 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) {
|
||||||
if (force) {host.chatHasAutoScrolled = true;}
|
return;
|
||||||
|
}
|
||||||
|
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 +72,22 @@ 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 +95,18 @@ 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 +117,9 @@ 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 +131,13 @@ 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`);
|
||||||
|
|
|
||||||
|
|
@ -64,13 +64,19 @@ 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) {
|
||||||
if (host.settings.lastActiveSessionKey === trimmed) {return;}
|
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 +123,31 @@ 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) {
|
||||||
if (next === "chat") {host.chatHasAutoScrolled = false;}
|
host.tab = next;
|
||||||
if (next === "logs") {startLogsPolling(host as unknown as Parameters<typeof startLogsPolling>[0]);}
|
}
|
||||||
else {stopLogsPolling(host as unknown as Parameters<typeof stopLogsPolling>[0]);}
|
if (next === "chat") {
|
||||||
if (next === "debug")
|
host.chatHasAutoScrolled = false;
|
||||||
{startDebugPolling(host as unknown as Parameters<typeof startDebugPolling>[0]);}
|
}
|
||||||
else {stopDebugPolling(host as unknown as Parameters<typeof stopDebugPolling>[0]);}
|
if (next === "logs") {
|
||||||
|
startLogsPolling(host as unknown as Parameters<typeof startLogsPolling>[0]);
|
||||||
|
} else {
|
||||||
|
stopLogsPolling(host as unknown as Parameters<typeof stopLogsPolling>[0]);
|
||||||
|
}
|
||||||
|
if (next === "debug") {
|
||||||
|
startDebugPolling(host as unknown as Parameters<typeof startDebugPolling>[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 +167,24 @@ 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") {
|
||||||
if (host.tab === "channels") {await loadChannelsTab(host);}
|
await loadOverview(host);
|
||||||
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 === "channels") {
|
||||||
if (host.tab === "cron") {await loadCron(host);}
|
await loadChannelsTab(host);
|
||||||
if (host.tab === "skills") {await loadSkills(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 === "cron") {
|
||||||
|
await loadCron(host);
|
||||||
|
}
|
||||||
|
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 +214,9 @@ 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 +231,23 @@ 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 +261,9 @@ 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 +277,22 @@ 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 +309,31 @@ 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) {
|
||||||
if (next === "chat") {host.chatHasAutoScrolled = false;}
|
host.tab = next;
|
||||||
if (next === "logs") {startLogsPolling(host as unknown as Parameters<typeof startLogsPolling>[0]);}
|
}
|
||||||
else {stopLogsPolling(host as unknown as Parameters<typeof stopLogsPolling>[0]);}
|
if (next === "chat") {
|
||||||
if (next === "debug")
|
host.chatHasAutoScrolled = false;
|
||||||
{startDebugPolling(host as unknown as Parameters<typeof startDebugPolling>[0]);}
|
}
|
||||||
else {stopDebugPolling(host as unknown as Parameters<typeof stopDebugPolling>[0]);}
|
if (next === "logs") {
|
||||||
if (host.connected) {void refreshActiveTab(host);}
|
startLogsPolling(host as unknown as Parameters<typeof startLogsPolling>[0]);
|
||||||
|
} else {
|
||||||
|
stopLogsPolling(host as unknown as Parameters<typeof stopLogsPolling>[0]);
|
||||||
|
}
|
||||||
|
if (next === "debug") {
|
||||||
|
startDebugPolling(host as unknown as Parameters<typeof startDebugPolling>[0]);
|
||||||
|
} else {
|
||||||
|
stopDebugPolling(host as unknown as Parameters<typeof stopDebugPolling>[0]);
|
||||||
|
}
|
||||||
|
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 +356,16 @@ 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) {
|
||||||
else {window.history.pushState({}, "", url.toString());}
|
window.history.replaceState({}, "", url.toString());
|
||||||
|
} else {
|
||||||
|
window.history.pushState({}, "", url.toString());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function loadOverview(host: SettingsHost) {
|
export async function loadOverview(host: SettingsHost) {
|
||||||
|
|
|
||||||
|
|
@ -35,25 +35,39 @@ 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);
|
||||||
}
|
}
|
||||||
|
|
@ -67,11 +81,14 @@ function formatToolOutput(value: unknown): string | null {
|
||||||
try {
|
try {
|
||||||
text = JSON.stringify(value, null, 2);
|
text = JSON.stringify(value, null, 2);
|
||||||
} catch {
|
} catch {
|
||||||
|
// oxlint-disable typescript/no-base-to-string
|
||||||
text = String(value);
|
text = String(value);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
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 +116,14 @@ 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 +145,9 @@ 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 +205,9 @@ 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 +215,29 @@ 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) {
|
||||||
if (host.chatRunId && payload.runId !== host.chatRunId) {return;}
|
return;
|
||||||
if (!host.chatRunId) {return;}
|
}
|
||||||
|
if (host.chatRunId && payload.runId !== 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 +266,12 @@ 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) {
|
||||||
if (output !== undefined) {entry.output = output;}
|
entry.args = args;
|
||||||
|
}
|
||||||
|
if (output !== undefined) {
|
||||||
|
entry.output = output;
|
||||||
|
}
|
||||||
entry.updatedAt = now;
|
entry.updatedAt = now;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
import { LitElement, html, nothing } from "lit";
|
import { LitElement } from "lit";
|
||||||
import { customElement, state } from "lit/decorators.js";
|
import { customElement, state } from "lit/decorators.js";
|
||||||
import type { EventLogEntry } from "./app-events";
|
import type { EventLogEntry } from "./app-events";
|
||||||
import type { DevicePairingList } from "./controllers/devices";
|
import type { DevicePairingList } from "./controllers/devices";
|
||||||
|
|
@ -84,10 +84,14 @@ 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 +410,9 @@ 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 +430,9 @@ 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 +463,9 @@ 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;
|
||||||
|
|
|
||||||
|
|
@ -18,10 +18,16 @@ 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) {
|
||||||
if (trimmed.length <= maxLength) {return trimmed;}
|
return undefined;
|
||||||
|
}
|
||||||
|
if (trimmed.length <= maxLength) {
|
||||||
|
return trimmed;
|
||||||
|
}
|
||||||
return trimmed.slice(0, maxLength);
|
return trimmed.slice(0, maxLength);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
import { afterEach, beforeEach, describe, expect, it } from "vitest";
|
import { afterEach, beforeEach, describe, expect, it } from "vitest";
|
||||||
import { OpenClawApp } from "./app";
|
import { OpenClawApp } from "./app";
|
||||||
|
|
||||||
|
// oxlint-disable-next-line typescript/unbound-method
|
||||||
const originalConnect = OpenClawApp.prototype.connect;
|
const originalConnect = OpenClawApp.prototype.connect;
|
||||||
|
|
||||||
function mountApp(pathname: string) {
|
function mountApp(pathname: string) {
|
||||||
|
|
|
||||||
|
|
@ -13,7 +13,9 @@ 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);
|
||||||
|
|
@ -38,16 +40,19 @@ function createCopyButton(options: CopyButtonOptions): TemplateResult {
|
||||||
aria-label=${idleLabel}
|
aria-label=${idleLabel}
|
||||||
@click=${async (e: Event) => {
|
@click=${async (e: Event) => {
|
||||||
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;
|
|
||||||
|
|
||||||
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 +63,9 @@ 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 +76,9 @@ 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);
|
||||||
|
|
|
||||||
|
|
@ -24,7 +24,9 @@ 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") {
|
||||||
|
|
@ -188,12 +190,14 @@ 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) || value.startsWith('/') // 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 +255,9 @@ 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}">
|
||||||
|
|
|
||||||
|
|
@ -20,16 +20,24 @@ 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)) {
|
||||||
if (/\d{4}-\d{2}-\d{2} \d{2}:\d{2}\b/.test(header)) {return true;}
|
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 +53,9 @@ 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 +73,13 @@ 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;
|
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 +94,21 @@ 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 +117,13 @@ 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;
|
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 +132,34 @@ 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())
|
||||||
|
|
|
||||||
|
|
@ -21,13 +21,11 @@ export function normalizeMessage(message: unknown): NormalizedMessage {
|
||||||
Array.isArray(contentItems) &&
|
Array.isArray(contentItems) &&
|
||||||
contentItems.some((item) => {
|
contentItems.some((item) => {
|
||||||
const x = item as Record<string, unknown>;
|
const x = item as Record<string, unknown>;
|
||||||
const t = String(x.type ?? "").toLowerCase();
|
const t = (typeof x.type === "string" ? x.type : "").toLowerCase();
|
||||||
return t === "toolresult" || t === "tool_result";
|
return t === "toolresult" || t === "tool_result";
|
||||||
});
|
});
|
||||||
|
|
||||||
const hasToolName =
|
const hasToolName = typeof m.toolName === "string" || typeof m.tool_name === "string";
|
||||||
typeof (m).toolName === "string" ||
|
|
||||||
typeof (m).tool_name === "string";
|
|
||||||
|
|
||||||
if (hasToolId || hasToolContent || hasToolName) {
|
if (hasToolId || hasToolContent || hasToolName) {
|
||||||
role = "toolResult";
|
role = "toolResult";
|
||||||
|
|
@ -61,9 +59,15 @@ 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") {
|
||||||
if (role === "assistant") {return "assistant";}
|
return role;
|
||||||
if (role === "system") {return "system";}
|
}
|
||||||
|
if (role === "assistant") {
|
||||||
|
return "assistant";
|
||||||
|
}
|
||||||
|
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" ||
|
||||||
|
|
|
||||||
|
|
@ -13,7 +13,7 @@ export function extractToolCards(message: unknown): ToolCard[] {
|
||||||
const cards: ToolCard[] = [];
|
const cards: ToolCard[] = [];
|
||||||
|
|
||||||
for (const item of content) {
|
for (const item of content) {
|
||||||
const kind = String(item.type ?? "").toLowerCase();
|
const kind = (typeof item.type === "string" ? item.type : "").toLowerCase();
|
||||||
const isToolCall =
|
const isToolCall =
|
||||||
["toolcall", "tool_call", "tooluse", "tool_use"].includes(kind) ||
|
["toolcall", "tool_call", "tooluse", "tool_use"].includes(kind) ||
|
||||||
(typeof item.name === "string" && item.arguments != null);
|
(typeof item.name === "string" && item.arguments != null);
|
||||||
|
|
@ -27,8 +27,10 @@ export function extractToolCards(message: unknown): ToolCard[] {
|
||||||
}
|
}
|
||||||
|
|
||||||
for (const item of content) {
|
for (const item of content) {
|
||||||
const kind = String(item.type ?? "").toLowerCase();
|
const kind = (typeof item.type === "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 +81,9 @@ 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 +121,23 @@ 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) {
|
||||||
if (!trimmed.startsWith("{") && !trimmed.startsWith("[")) {return value;}
|
return value;
|
||||||
|
}
|
||||||
|
if (!trimmed.startsWith("{") && !trimmed.startsWith("[")) {
|
||||||
|
return value;
|
||||||
|
}
|
||||||
try {
|
try {
|
||||||
return JSON.parse(trimmed);
|
return JSON.parse(trimmed);
|
||||||
} catch {
|
} catch {
|
||||||
|
|
@ -134,7 +146,11 @@ 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") {
|
||||||
if (typeof item.content === "string") {return item.content;}
|
return item.text;
|
||||||
|
}
|
||||||
|
if (typeof item.content === "string") {
|
||||||
|
return item.content;
|
||||||
|
}
|
||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -74,10 +74,14 @@ 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;
|
||||||
|
|
|
||||||
|
|
@ -53,7 +53,9 @@ describe("config form renderer", () => {
|
||||||
|
|
||||||
const tokenInput = container.querySelector("input[type='password']");
|
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");
|
||||||
|
|
@ -67,7 +69,9 @@ describe("config form renderer", () => {
|
||||||
|
|
||||||
const checkbox = container.querySelector("input[type='checkbox']");
|
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);
|
||||||
|
|
@ -93,9 +97,7 @@ describe("config form renderer", () => {
|
||||||
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",
|
|
||||||
);
|
|
||||||
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"], []);
|
||||||
|
|
@ -150,9 +152,7 @@ describe("config form renderer", () => {
|
||||||
container,
|
container,
|
||||||
);
|
);
|
||||||
|
|
||||||
const removeButton = container.querySelector(
|
const removeButton = container.querySelector(".cfg-map__item-remove");
|
||||||
".cfg-map__item-remove",
|
|
||||||
);
|
|
||||||
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"], {});
|
||||||
|
|
|
||||||
|
|
@ -10,13 +10,19 @@ 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) {
|
||||||
if (state.agentsLoading) {return;}
|
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", {}));
|
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 {
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
import type { GatewayBrowserClient } from "../gateway";
|
import type { GatewayBrowserClient } from "../gateway";
|
||||||
import { normalizeAssistantIdentity, type AssistantIdentity } from "../assistant-identity";
|
import { normalizeAssistantIdentity } from "../assistant-identity";
|
||||||
|
|
||||||
export type AssistantIdentityState = {
|
export type AssistantIdentityState = {
|
||||||
client: GatewayBrowserClient | null;
|
client: GatewayBrowserClient | null;
|
||||||
|
|
@ -14,12 +14,16 @@ 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));
|
const res = await state.client.request("agent.identity.get", params);
|
||||||
if (!res) {return;}
|
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;
|
||||||
|
|
|
||||||
|
|
@ -1,18 +1,21 @@
|
||||||
import type { ChannelsStatusSnapshot } from "../types";
|
|
||||||
import type { ChannelsState } from "./channels.types";
|
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) {
|
||||||
if (state.channelsLoading) {return;}
|
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,
|
||||||
}));
|
});
|
||||||
state.channelsSnapshot = res;
|
state.channelsSnapshot = res;
|
||||||
state.channelsLastSuccess = Date.now();
|
state.channelsLastSuccess = Date.now();
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
|
|
@ -23,13 +26,15 @@ 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,
|
||||||
}));
|
});
|
||||||
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 +48,19 @@ 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,
|
||||||
}));
|
});
|
||||||
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 +70,9 @@ 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" });
|
||||||
|
|
|
||||||
|
|
@ -28,14 +28,16 @@ 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,
|
||||||
}));
|
});
|
||||||
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 +49,9 @@ 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 +60,14 @@ 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 +107,9 @@ 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 +149,9 @@ 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 +166,19 @@ 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) {
|
||||||
if (payload.sessionKey !== state.sessionKey) {return null;}
|
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;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -35,11 +35,13 @@ 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", {}));
|
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 +51,15 @@ 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) {
|
||||||
if (state.configSchemaLoading) {return;}
|
return;
|
||||||
|
}
|
||||||
|
if (state.configSchemaLoading) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
state.configSchemaLoading = true;
|
state.configSchemaLoading = true;
|
||||||
try {
|
try {
|
||||||
const res = (await state.client.request("config.schema", {}));
|
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);
|
||||||
|
|
@ -94,7 +100,9 @@ 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 +126,9 @@ 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 +156,9 @@ 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 {
|
||||||
|
|
|
||||||
|
|
@ -14,19 +14,25 @@ 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 +42,9 @@ 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 +56,32 @@ 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) {
|
||||||
|
|
|
||||||
|
|
@ -17,9 +17,11 @@ 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", {}));
|
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 +29,18 @@ 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) {
|
||||||
if (state.cronLoading) {return;}
|
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,
|
||||||
}));
|
});
|
||||||
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 +52,39 @@ 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 +93,26 @@ 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) {
|
||||||
if (form.channel) {payload.channel = form.channel;}
|
payload.deliver = true;
|
||||||
if (form.to.trim()) {payload.to = form.to.trim();}
|
}
|
||||||
|
if (form.channel) {
|
||||||
|
payload.channel = form.channel;
|
||||||
|
}
|
||||||
|
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 +133,9 @@ 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 +153,9 @@ 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 +170,9 @@ 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 +186,9 @@ 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 +207,14 @@ 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,
|
||||||
}));
|
});
|
||||||
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) {
|
||||||
|
|
|
||||||
|
|
@ -16,8 +16,12 @@ 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) {
|
||||||
if (state.debugLoading) {return;}
|
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([
|
||||||
|
|
@ -39,7 +43,9 @@ 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 {
|
||||||
|
|
|
||||||
|
|
@ -46,25 +46,35 @@ 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) {
|
||||||
if (state.devicesLoading) {return;}
|
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", {}));
|
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 +84,13 @@ 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,9 +103,11 @@ 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));
|
const res = await state.client.request("device.token.rotate", params);
|
||||||
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;
|
||||||
|
|
@ -115,9 +131,13 @@ 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();
|
||||||
|
|
|
||||||
|
|
@ -28,15 +28,23 @@ 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 +63,13 @@ 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,
|
||||||
|
|
|
||||||
|
|
@ -56,7 +56,9 @@ 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 +70,9 @@ 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 +80,12 @@ 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) {
|
||||||
if (state.execApprovalsLoading) {return;}
|
return;
|
||||||
|
}
|
||||||
|
if (state.execApprovalsLoading) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
state.execApprovalsLoading = true;
|
state.execApprovalsLoading = true;
|
||||||
state.lastError = null;
|
state.lastError = null;
|
||||||
try {
|
try {
|
||||||
|
|
@ -86,7 +94,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));
|
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 +117,9 @@ 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 {
|
||||||
|
|
|
||||||
|
|
@ -19,12 +19,18 @@ 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 +38,17 @@ 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 =
|
||||||
|
|
@ -50,25 +60,28 @@ export function parseLogLine(line: string): LogEntry {
|
||||||
const level = normalizeLevel(meta?.logLevelName ?? meta?.level);
|
const level = normalizeLevel(meta?.logLevelName ?? meta?.level);
|
||||||
|
|
||||||
const contextCandidate =
|
const contextCandidate =
|
||||||
typeof obj["0"] === "string"
|
typeof obj["0"] === "string" ? obj["0"] : typeof meta?.name === "string" ? meta?.name : null;
|
||||||
? (obj["0"])
|
|
||||||
: typeof meta?.name === "string"
|
|
||||||
? (meta?.name)
|
|
||||||
: 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") {
|
||||||
else if (typeof contextObj.module === "string") {subsystem = contextObj.module;}
|
subsystem = contextObj.subsystem;
|
||||||
|
} 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"];}
|
if (typeof obj["1"] === "string") {
|
||||||
else if (!contextObj && typeof obj["0"] === "string") {message = obj["0"];}
|
message = obj["1"];
|
||||||
else if (typeof obj.message === "string") {message = obj.message;}
|
} else if (!contextObj && typeof obj["0"] === "string") {
|
||||||
|
message = obj["0"];
|
||||||
|
} else if (typeof obj.message === "string") {
|
||||||
|
message = obj.message;
|
||||||
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
raw: line,
|
raw: line,
|
||||||
|
|
@ -84,9 +97,15 @@ 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) {
|
||||||
if (state.logsLoading && !opts?.quiet) {return;}
|
return;
|
||||||
if (!opts?.quiet) {state.logsLoading = true;}
|
}
|
||||||
|
if (state.logsLoading && !opts?.quiet) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
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 +122,26 @@ 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"))
|
? 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") {
|
||||||
if (typeof payload.file === "string") {state.logsFile = payload.file;}
|
state.logsCursor = payload.cursor;
|
||||||
|
}
|
||||||
|
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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -9,15 +9,23 @@ 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) {
|
||||||
if (state.nodesLoading) {return;}
|
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", {}));
|
const res = await state.client.request("node.list", {});
|
||||||
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;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -11,13 +11,17 @@ 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) {
|
||||||
if (state.presenceLoading) {return;}
|
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", {}));
|
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;
|
||||||
|
|
|
||||||
|
|
@ -23,8 +23,12 @@ export async function loadSessions(
|
||||||
includeUnknown?: boolean;
|
includeUnknown?: boolean;
|
||||||
},
|
},
|
||||||
) {
|
) {
|
||||||
if (!state.client || !state.connected) {return;}
|
if (!state.client || !state.connected) {
|
||||||
if (state.sessionsLoading) {return;}
|
return;
|
||||||
|
}
|
||||||
|
if (state.sessionsLoading) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
state.sessionsLoading = true;
|
state.sessionsLoading = true;
|
||||||
state.sessionsError = null;
|
state.sessionsError = null;
|
||||||
try {
|
try {
|
||||||
|
|
@ -36,10 +40,16 @@ export async function loadSessions(
|
||||||
includeGlobal,
|
includeGlobal,
|
||||||
includeUnknown,
|
includeUnknown,
|
||||||
};
|
};
|
||||||
if (activeMinutes > 0) {params.activeMinutes = activeMinutes;}
|
if (activeMinutes > 0) {
|
||||||
if (limit > 0) {params.limit = limit;}
|
params.activeMinutes = activeMinutes;
|
||||||
const res = (await state.client.request("sessions.list", params));
|
}
|
||||||
if (res) {state.sessionsResult = res;}
|
if (limit > 0) {
|
||||||
|
params.limit = limit;
|
||||||
|
}
|
||||||
|
const res = await state.client.request("sessions.list", params);
|
||||||
|
if (res) {
|
||||||
|
state.sessionsResult = res;
|
||||||
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
state.sessionsError = String(err);
|
state.sessionsError = String(err);
|
||||||
} finally {
|
} finally {
|
||||||
|
|
@ -57,12 +67,22 @@ 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) {
|
||||||
if ("thinkingLevel" in patch) {params.thinkingLevel = patch.thinkingLevel;}
|
params.label = patch.label;
|
||||||
if ("verboseLevel" in patch) {params.verboseLevel = patch.verboseLevel;}
|
}
|
||||||
if ("reasoningLevel" in patch) {params.reasoningLevel = patch.reasoningLevel;}
|
if ("thinkingLevel" in patch) {
|
||||||
|
params.thinkingLevel = patch.thinkingLevel;
|
||||||
|
}
|
||||||
|
if ("verboseLevel" in patch) {
|
||||||
|
params.verboseLevel = patch.verboseLevel;
|
||||||
|
}
|
||||||
|
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);
|
||||||
|
|
@ -72,12 +92,18 @@ 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) {
|
||||||
if (state.sessionsLoading) {return;}
|
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 {
|
||||||
|
|
|
||||||
|
|
@ -24,15 +24,22 @@ 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) {
|
||||||
else {delete next[key];}
|
next[key] = message;
|
||||||
|
} 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 +47,19 @@ 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) {
|
||||||
if (state.skillsLoading) {return;}
|
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", {}));
|
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 +72,9 @@ 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 +97,9 @@ 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,15 +128,17 @@ 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 {
|
||||||
const result = (await state.client.request("skills.install", {
|
const result = await state.client.request("skills.install", {
|
||||||
name,
|
name,
|
||||||
installId,
|
installId,
|
||||||
timeoutMs: 120000,
|
timeoutMs: 120000,
|
||||||
}));
|
});
|
||||||
await loadSkills(state);
|
await loadSkills(state);
|
||||||
setSkillMessage(state, skillKey, {
|
setSkillMessage(state, skillKey, {
|
||||||
kind: "success",
|
kind: "success",
|
||||||
|
|
|
||||||
|
|
@ -18,11 +18,15 @@ 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].toSorted();
|
return [...out].toSorted();
|
||||||
}
|
}
|
||||||
|
|
@ -30,11 +34,19 @@ function normalizeScopes(scopes: string[] | undefined): string[] {
|
||||||
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) {
|
||||||
if (!parsed.deviceId || typeof parsed.deviceId !== "string") {return null;}
|
return null;
|
||||||
if (!parsed.tokens || typeof parsed.tokens !== "object") {return null;}
|
}
|
||||||
|
if (!parsed.deviceId || typeof parsed.deviceId !== "string") {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
if (!parsed.tokens || typeof parsed.tokens !== "object") {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
return parsed;
|
return parsed;
|
||||||
} catch {
|
} catch {
|
||||||
return null;
|
return null;
|
||||||
|
|
@ -54,10 +66,14 @@ 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 +106,13 @@ 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);
|
||||||
|
|
|
||||||
|
|
@ -18,7 +18,9 @@ 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 +29,9 @@ 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;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
import { afterEach, beforeEach, describe, expect, it } from "vitest";
|
import { afterEach, beforeEach, describe, expect, it } from "vitest";
|
||||||
import { OpenClawApp } from "./app";
|
import { OpenClawApp } from "./app";
|
||||||
|
|
||||||
|
// oxlint-disable-next-line typescript/unbound-method
|
||||||
const originalConnect = OpenClawApp.prototype.connect;
|
const originalConnect = OpenClawApp.prototype.connect;
|
||||||
|
|
||||||
function mountApp(pathname: string) {
|
function mountApp(pathname: string) {
|
||||||
|
|
|
||||||
|
|
@ -1,44 +1,70 @@
|
||||||
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) {
|
||||||
if (ms < 1000) {return `${ms}ms`;}
|
return "n/a";
|
||||||
|
}
|
||||||
|
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))}…`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -91,36 +91,44 @@ 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.addEventListener("open", () => this.queueConnect());
|
||||||
this.ws.onmessage = (ev) => this.handleMessage(String(ev.data ?? ""));
|
this.ws.addEventListener("message", (ev) => this.handleMessage(String(ev.data ?? "")));
|
||||||
this.ws.onclose = (ev) => {
|
this.ws.addEventListener("close", (ev) => {
|
||||||
const reason = String(ev.reason ?? "");
|
const reason = String(ev.reason ?? "");
|
||||||
this.ws = null;
|
this.ws = null;
|
||||||
this.flushPending(new Error(`gateway closed (${ev.code}): ${reason}`));
|
this.flushPending(new Error(`gateway closed (${ev.code}): ${reason}`));
|
||||||
this.opts.onClose?.({ code: ev.code, reason });
|
this.opts.onClose?.({ code: ev.code, reason });
|
||||||
this.scheduleReconnect();
|
this.scheduleReconnect();
|
||||||
};
|
});
|
||||||
this.ws.onerror = () => {
|
this.ws.addEventListener("error", () => {
|
||||||
// ignored; close handler will fire
|
// ignored; close handler will fire
|
||||||
};
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
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 +273,15 @@ 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) {
|
||||||
else {pending.reject(new Error(res.error?.message ?? "request failed"));}
|
pending.resolve(res.payload);
|
||||||
|
} else {
|
||||||
|
pending.reject(new Error(res.error?.message ?? "request failed"));
|
||||||
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -289,7 +302,9 @@ 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);
|
||||||
|
|
|
||||||
|
|
@ -243,6 +243,8 @@ 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;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -47,7 +47,9 @@ 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 +57,29 @@ 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 +87,15 @@ 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
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,7 @@ import { afterEach, beforeEach, describe, expect, it } from "vitest";
|
||||||
import { OpenClawApp } from "./app";
|
import { OpenClawApp } from "./app";
|
||||||
import "../styles.css";
|
import "../styles.css";
|
||||||
|
|
||||||
|
// oxlint-disable-next-line typescript/unbound-method
|
||||||
const originalConnect = OpenClawApp.prototype.connect;
|
const originalConnect = OpenClawApp.prototype.connect;
|
||||||
|
|
||||||
function mountApp(pathname: string) {
|
function mountApp(pathname: string) {
|
||||||
|
|
@ -117,7 +118,9 @@ describe("control UI routing", () => {
|
||||||
|
|
||||||
const initialContainer = app.querySelector(".chat-thread");
|
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";
|
||||||
|
|
||||||
|
|
@ -134,11 +137,15 @@ describe("control UI routing", () => {
|
||||||
|
|
||||||
const container = app.querySelector(".chat-thread");
|
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);
|
||||||
|
|
|
||||||
|
|
@ -40,18 +40,30 @@ 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("/")) {
|
||||||
if (base === "/") {return "";}
|
base = `/${base}`;
|
||||||
if (base.endsWith("/")) {base = base.slice(0, -1);}
|
}
|
||||||
|
if (base === "/") {
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
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 +87,12 @@ 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")) {
|
||||||
if (normalized === "/") {return "chat";}
|
normalized = "/";
|
||||||
|
}
|
||||||
|
if (normalized === "/") {
|
||||||
|
return "chat";
|
||||||
|
}
|
||||||
return PATH_TO_TAB.get(normalized) ?? null;
|
return PATH_TO_TAB.get(normalized) ?? null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -85,9 +101,13 @@ 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)) {
|
||||||
|
|
|
||||||
|
|
@ -15,22 +15,29 @@ 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 {
|
||||||
|
// oxlint-disable typescript/no-base-to-string
|
||||||
return String(payload);
|
return String(payload);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -45,13 +52,19 @@ 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") {
|
||||||
if (s.kind === "every") {return `Every ${formatDurationMs(s.everyMs)}`;}
|
return `At ${formatMs(s.atMs)}`;
|
||||||
|
}
|
||||||
|
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}`;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -36,7 +36,9 @@ 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:
|
||||||
|
|
|
||||||
|
|
@ -18,9 +18,15 @@ type DocumentWithViewTransition = Document & {
|
||||||
};
|
};
|
||||||
|
|
||||||
const clamp01 = (value: number) => {
|
const clamp01 = (value: number) => {
|
||||||
if (Number.isNaN(value)) {return 0.5;}
|
if (Number.isNaN(value)) {
|
||||||
if (value <= 0) {return 0;}
|
return 0.5;
|
||||||
if (value >= 1) {return 1;}
|
}
|
||||||
|
if (value <= 0) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
if (value >= 1) {
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
return value;
|
return value;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -43,7 +49,9 @@ 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) {
|
||||||
|
|
|
||||||
|
|
@ -9,6 +9,8 @@ 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;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -39,7 +39,9 @@ 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 +54,25 @@ 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 +82,9 @@ 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 +92,17 @@ 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) {
|
||||||
if (!current || typeof current !== "object") {return undefined;}
|
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 +113,22 @@ 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 +138,9 @@ 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 +150,9 @@ 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 +176,9 @@ 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 +208,15 @@ 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) {
|
||||||
if (display.detail) {parts.push(display.detail);}
|
parts.push(display.verb);
|
||||||
if (parts.length === 0) {return undefined;}
|
}
|
||||||
|
if (display.detail) {
|
||||||
|
parts.push(display.detail);
|
||||||
|
}
|
||||||
|
if (parts.length === 0) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
return parts.join(" · ");
|
return parts.join(" · ");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -190,6 +226,8 @@ 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, "~");
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -16,7 +16,9 @@ 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;
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -23,7 +23,9 @@ 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 +34,17 @@ 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);
|
||||||
|
|
|
||||||
|
|
@ -18,7 +18,9 @@ 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 ?? {};
|
||||||
|
|
@ -34,7 +36,9 @@ function resolveSchemaNode(
|
||||||
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;
|
||||||
|
|
|
||||||
|
|
@ -140,7 +140,9 @@ 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;">
|
||||||
|
|
|
||||||
|
|
@ -13,8 +13,12 @@ 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) {
|
||||||
if (pubkey.length <= 20) {return pubkey;}
|
return "n/a";
|
||||||
|
}
|
||||||
|
if (pubkey.length <= 20) {
|
||||||
|
return pubkey;
|
||||||
|
}
|
||||||
return `${pubkey.slice(0, 8)}...${pubkey.slice(-8)}`;
|
return `${pubkey.slice(0, 8)}...${pubkey.slice(-8)}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -3,11 +3,17 @@ 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 +21,9 @@ 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 +47,8 @@ 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>`;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -44,7 +44,9 @@ export function renderChannels(props: ChannelsProps) {
|
||||||
order: index,
|
order: index,
|
||||||
}))
|
}))
|
||||||
.toSorted((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;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -236,7 +238,9 @@ 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 +252,34 @@ 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) {
|
||||||
if (account.connected === false) {return "No";}
|
return "Yes";
|
||||||
|
}
|
||||||
|
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";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -75,7 +75,9 @@ 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 +109,9 @@ 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,16 +121,20 @@ 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.addEventListener("load", () => {
|
||||||
const dataUrl = reader.result as string;
|
const dataUrl = reader.result as string;
|
||||||
const newAttachment: ChatAttachment = {
|
const newAttachment: ChatAttachment = {
|
||||||
id: generateAttachmentId(),
|
id: generateAttachmentId(),
|
||||||
|
|
@ -135,14 +143,16 @@ function handlePaste(e: ClipboardEvent, props: ChatProps) {
|
||||||
};
|
};
|
||||||
const current = props.attachments ?? [];
|
const current = props.attachments ?? [];
|
||||||
props.onAttachmentsChange?.([...current, newAttachment]);
|
props.onAttachmentsChange?.([...current, newAttachment]);
|
||||||
};
|
});
|
||||||
reader.readAsDataURL(file);
|
reader.readAsDataURL(file);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
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 +296,9 @@ 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 +350,22 @@ 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") {
|
||||||
if (e.isComposing || e.keyCode === 229) {return;}
|
return;
|
||||||
if (e.shiftKey) {return;} // Allow Shift+Enter for line breaks
|
}
|
||||||
if (!props.connected) {return;}
|
if (e.isComposing || e.keyCode === 229) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (e.shiftKey) {
|
||||||
|
return;
|
||||||
|
} // Allow Shift+Enter for line breaks
|
||||||
|
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 +419,9 @@ 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 +435,9 @@ function groupMessages(items: ChatItem[]): Array<ChatItem | MessageGroup> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (currentGroup) {result.push(currentGroup);}
|
if (currentGroup) {
|
||||||
|
result.push(currentGroup);
|
||||||
|
}
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -475,13 +501,21 @@ 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}`;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -41,7 +41,9 @@ 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 +56,12 @@ 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) {
|
||||||
if (enumValues.length === 0) {unsupported.add(pathLabel);}
|
normalized.nullable = true;
|
||||||
|
}
|
||||||
|
if (enumValues.length === 0) {
|
||||||
|
unsupported.add(pathLabel);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (type === "object") {
|
if (type === "object") {
|
||||||
|
|
@ -63,8 +69,12 @@ 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) {
|
||||||
for (const entry of res.unsupportedPaths) {unsupported.add(entry);}
|
normalizedProps[key] = res.schema;
|
||||||
|
}
|
||||||
|
for (const entry of res.unsupportedPaths) {
|
||||||
|
unsupported.add(entry);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
normalized.properties = normalizedProps;
|
normalized.properties = normalizedProps;
|
||||||
|
|
||||||
|
|
@ -75,8 +85,10 @@ function normalizeSchemaNode(
|
||||||
} else if (schema.additionalProperties && typeof schema.additionalProperties === "object") {
|
} else if (schema.additionalProperties && typeof schema.additionalProperties === "object") {
|
||||||
if (!isAnySchema(schema.additionalProperties)) {
|
if (!isAnySchema(schema.additionalProperties)) {
|
||||||
const res = normalizeSchemaNode(schema.additionalProperties, [...path, "*"]);
|
const res = normalizeSchemaNode(schema.additionalProperties, [...path, "*"]);
|
||||||
normalized.additionalProperties = res.schema ?? (schema.additionalProperties);
|
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 +98,9 @@ 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 +122,28 @@ 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) {
|
||||||
|
|
|
||||||
|
|
@ -18,7 +18,9 @@ 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 +133,12 @@ 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) {
|
||||||
if (v.enum && v.enum.length === 1) {return v.enum[0];}
|
return v.const;
|
||||||
|
}
|
||||||
|
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);
|
||||||
|
|
@ -147,14 +153,20 @@ export function renderNode(params: {
|
||||||
${help ? html`<div class="cfg-field__help">${help}</div>` : nothing}
|
${help ? html`<div class="cfg-field__help">${help}</div>` : nothing}
|
||||||
<div class="cfg-segmented">
|
<div class="cfg-segmented">
|
||||||
${literals.map(
|
${literals.map(
|
||||||
(lit, idx) => html`
|
(lit) => html`
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
class="cfg-segmented__btn ${lit === resolvedValue || String(lit) === String(resolvedValue) ? "active" : ""}"
|
class="cfg-segmented__btn ${
|
||||||
|
// oxlint-disable typescript/no-base-to-string
|
||||||
|
lit === resolvedValue || String(lit) === String(resolvedValue) ? "active" : ""
|
||||||
|
}"
|
||||||
?disabled=${disabled}
|
?disabled=${disabled}
|
||||||
@click=${() => onPatch(path, lit)}
|
@click=${() => onPatch(path, lit)}
|
||||||
>
|
>
|
||||||
${String(lit)}
|
${
|
||||||
|
// oxlint-disable typescript/no-base-to-string
|
||||||
|
String(lit)
|
||||||
|
}
|
||||||
</button>
|
</button>
|
||||||
`,
|
`,
|
||||||
)}
|
)}
|
||||||
|
|
@ -298,7 +310,12 @@ function renderTextInput(params: {
|
||||||
const isSensitive = hint?.sensitive ?? isSensitivePath(path);
|
const isSensitive = hint?.sensitive ?? isSensitivePath(path);
|
||||||
const placeholder =
|
const placeholder =
|
||||||
hint?.placeholder ??
|
hint?.placeholder ??
|
||||||
(isSensitive ? "••••" : schema.default !== undefined ? `Default: ${schema.default}` : "");
|
// oxlint-disable typescript/no-base-to-string
|
||||||
|
(isSensitive
|
||||||
|
? "••••"
|
||||||
|
: schema.default !== undefined
|
||||||
|
? `Default: ${String(schema.default)}`
|
||||||
|
: "");
|
||||||
const displayValue = value ?? "";
|
const displayValue = value ?? "";
|
||||||
|
|
||||||
return html`
|
return html`
|
||||||
|
|
@ -326,7 +343,9 @@ 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());
|
||||||
}}
|
}}
|
||||||
|
|
@ -455,7 +474,6 @@ function renderObject(params: {
|
||||||
onPatch: (path: Array<string | number>, value: unknown) => void;
|
onPatch: (path: Array<string | number>, value: unknown) => void;
|
||||||
}): TemplateResult {
|
}): TemplateResult {
|
||||||
const { schema, value, path, hints, unsupported, disabled, onPatch } = params;
|
const { schema, value, path, hints, unsupported, disabled, onPatch } = params;
|
||||||
const showLabel = params.showLabel ?? true;
|
|
||||||
const hint = hintForPath(path, hints);
|
const hint = hintForPath(path, hints);
|
||||||
const label = hint?.label ?? schema.title ?? humanize(String(path.at(-1)));
|
const label = hint?.label ?? schema.title ?? humanize(String(path.at(-1)));
|
||||||
const help = hint?.help ?? schema.description;
|
const help = hint?.help ?? schema.description;
|
||||||
|
|
@ -472,7 +490,9 @@ function renderObject(params: {
|
||||||
const sorted = entries.toSorted((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]);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -708,9 +728,13 @@ 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);
|
||||||
|
|
|
||||||
|
|
@ -279,49 +279,73 @@ 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)) {
|
||||||
if (meta.description.toLowerCase().includes(q)) {return true;}
|
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)) {
|
||||||
if (schema.description?.toLowerCase().includes(query)) {return true;}
|
return true;
|
||||||
if (schema.enum?.some((value) => String(value).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.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)) {
|
||||||
if (schemaMatches(propSchema, query)) {return true;}
|
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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -350,13 +374,19 @@ export function renderConfigForm(props: ConfigFormProps) {
|
||||||
const entries = Object.entries(properties).toSorted((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) {
|
||||||
if (searchQuery && !matchesSearch(key, node, searchQuery)) {return false;}
|
return false;
|
||||||
|
}
|
||||||
|
if (searchQuery && !matchesSearch(key, node, searchQuery)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
return true;
|
return true;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -398,7 +428,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)[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 +484,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)[key],
|
value: value[key],
|
||||||
path: [key],
|
path: [key],
|
||||||
hints: props.uiHints,
|
hints: props.uiHints,
|
||||||
unsupported,
|
unsupported,
|
||||||
|
|
|
||||||
|
|
@ -17,7 +17,9 @@ 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 +28,12 @@ export function schemaType(schema: JsonSchema): string | undefined {
|
||||||
}
|
}
|
||||||
|
|
||||||
export function defaultValue(schema?: JsonSchema): unknown {
|
export function defaultValue(schema?: JsonSchema): unknown {
|
||||||
if (!schema) {return "";}
|
if (!schema) {
|
||||||
if (schema.default !== undefined) {return schema.default;}
|
return "";
|
||||||
|
}
|
||||||
|
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 +59,18 @@ 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 +78,9 @@ export function hintForPath(path: Array<string | number>, hints: ConfigUiHints)
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (match) {return hint;}
|
if (match) {
|
||||||
|
return hint;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -191,7 +191,9 @@ describe("config view", () => {
|
||||||
|
|
||||||
const input = container.querySelector(".config-search__input");
|
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");
|
||||||
|
|
|
||||||
|
|
@ -299,7 +299,9 @@ 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 +314,9 @@ 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 +332,15 @@ 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 +377,9 @@ 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 +402,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])
|
? analysis.schema.properties?.[props.activeSection]
|
||||||
: undefined;
|
: undefined;
|
||||||
const activeSectionMeta = props.activeSection
|
const activeSectionMeta = props.activeSection
|
||||||
? resolveSectionMeta(props.activeSection, activeSectionSchema)
|
? resolveSectionMeta(props.activeSection, activeSectionSchema)
|
||||||
|
|
|
||||||
|
|
@ -38,16 +38,22 @@ 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;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -212,8 +218,7 @@ export function renderCron(props: CronProps) {
|
||||||
.value=${props.form.channel || "last"}
|
.value=${props.form.channel || "last"}
|
||||||
@change=${(e: Event) =>
|
@change=${(e: Event) =>
|
||||||
props.onFormChange({
|
props.onFormChange({
|
||||||
channel: (e.target as HTMLSelectElement)
|
channel: (e.target as HTMLSelectElement).value,
|
||||||
.value,
|
|
||||||
})}
|
})}
|
||||||
>
|
>
|
||||||
${channelOptions.map(
|
${channelOptions.map(
|
||||||
|
|
|
||||||
|
|
@ -4,21 +4,29 @@ 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";
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,9 @@ 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">
|
||||||
|
|
|
||||||
|
|
@ -21,14 +21,20 @@ 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 +46,9 @@ 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";
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
import { html, nothing } from "lit";
|
import { html } from "lit";
|
||||||
import { unsafeHTML } from "lit/directives/unsafe-html.js";
|
import { unsafeHTML } from "lit/directives/unsafe-html.js";
|
||||||
import { icons } from "../icons";
|
import { icons } from "../icons";
|
||||||
import { toSanitizedMarkdownHtml } from "../markdown";
|
import { toSanitizedMarkdownHtml } from "../markdown";
|
||||||
|
|
|
||||||
|
|
@ -328,12 +328,16 @@ 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 +358,14 @@ 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 +382,9 @@ 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 +392,12 @@ 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) {
|
||||||
if (!a.isDefault && b.isDefault) {return 1;}
|
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 +409,12 @@ 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) {
|
||||||
if (selected && agents.some((agent) => agent.id === selected)) {return selected;}
|
return EXEC_APPROVALS_DEFAULT_SCOPE;
|
||||||
|
}
|
||||||
|
if (selected && agents.some((agent) => agent.id === selected)) {
|
||||||
|
return selected;
|
||||||
|
}
|
||||||
return EXEC_APPROVALS_DEFAULT_SCOPE;
|
return EXEC_APPROVALS_DEFAULT_SCOPE;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -1010,9 +1028,13 @@ 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 +1058,13 @@ 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 +1105,14 @@ 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>;
|
||||||
|
|
|
||||||
|
|
@ -29,10 +29,14 @@ 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 +78,13 @@ 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) {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;
|
||||||
|
|
|
||||||
|
|
@ -42,9 +42,13 @@ 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 +61,25 @@ 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) {
|
||||||
if (!value || value === "off") {return value;}
|
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) {
|
||||||
if (!isBinary) {return value;}
|
return null;
|
||||||
if (value === "on") {return "low";}
|
}
|
||||||
|
if (!isBinary) {
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
if (value === "on") {
|
||||||
|
return "low";
|
||||||
|
}
|
||||||
return value;
|
return value;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -85,8 +85,12 @@ 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) {
|
||||||
if (skill.blockedByAllowlist) {reasons.push("blocked by allowlist");}
|
reasons.push("disabled");
|
||||||
|
}
|
||||||
|
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">
|
||||||
|
|
|
||||||
|
|
@ -6,13 +6,19 @@ 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) {
|
||||||
if (trimmed === "./") {return "./";}
|
return "/";
|
||||||
if (trimmed.endsWith("/")) {return trimmed;}
|
}
|
||||||
|
if (trimmed === "./") {
|
||||||
|
return "./";
|
||||||
|
}
|
||||||
|
if (trimmed.endsWith("/")) {
|
||||||
|
return trimmed;
|
||||||
|
}
|
||||||
return `${trimmed}/`;
|
return `${trimmed}/`;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default defineConfig(({ command }) => {
|
export default defineConfig(() => {
|
||||||
const envBase = process.env.OPENCLAW_CONTROL_UI_BASE_PATH?.trim();
|
const envBase = process.env.OPENCLAW_CONTROL_UI_BASE_PATH?.trim();
|
||||||
const base = envBase ? normalizeBase(envBase) : "./";
|
const base = envBase ? normalizeBase(envBase) : "./";
|
||||||
return {
|
return {
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue