mirror of
https://github.com/farcasclaudiu/openclaw.git
synced 2026-06-28 21:01:43 +03:00
chore: Run pnpm format:fix.
This commit is contained in:
+26
-10
@@ -81,9 +81,11 @@
|
||||
--theme-switch-y: 50%;
|
||||
|
||||
/* Typography - Space Grotesk for personality */
|
||||
--mono: "JetBrains Mono", ui-monospace, SFMono-Regular, "SF Mono", Menlo, Monaco, Consolas, monospace;
|
||||
--mono:
|
||||
"JetBrains Mono", ui-monospace, SFMono-Regular, "SF Mono", Menlo, Monaco, Consolas, monospace;
|
||||
--font-body: "Space Grotesk", -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
|
||||
--font-display: "Space Grotesk", -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
|
||||
--font-display:
|
||||
"Space Grotesk", -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
|
||||
|
||||
/* Shadows - Richer with subtle color */
|
||||
--shadow-sm: 0 1px 2px rgba(0, 0, 0, 0.2);
|
||||
@@ -340,7 +342,8 @@ select {
|
||||
}
|
||||
|
||||
@keyframes pulse-subtle {
|
||||
0%, 100% {
|
||||
0%,
|
||||
100% {
|
||||
opacity: 1;
|
||||
}
|
||||
50% {
|
||||
@@ -349,7 +352,8 @@ select {
|
||||
}
|
||||
|
||||
@keyframes glow-pulse {
|
||||
0%, 100% {
|
||||
0%,
|
||||
100% {
|
||||
box-shadow: 0 0 0 rgba(255, 92, 92, 0);
|
||||
}
|
||||
50% {
|
||||
@@ -358,12 +362,24 @@ select {
|
||||
}
|
||||
|
||||
/* Stagger animation delays for grouped elements */
|
||||
.stagger-1 { animation-delay: 0ms; }
|
||||
.stagger-2 { animation-delay: 50ms; }
|
||||
.stagger-3 { animation-delay: 100ms; }
|
||||
.stagger-4 { animation-delay: 150ms; }
|
||||
.stagger-5 { animation-delay: 200ms; }
|
||||
.stagger-6 { animation-delay: 250ms; }
|
||||
.stagger-1 {
|
||||
animation-delay: 0ms;
|
||||
}
|
||||
.stagger-2 {
|
||||
animation-delay: 50ms;
|
||||
}
|
||||
.stagger-3 {
|
||||
animation-delay: 100ms;
|
||||
}
|
||||
.stagger-4 {
|
||||
animation-delay: 150ms;
|
||||
}
|
||||
.stagger-5 {
|
||||
animation-delay: 200ms;
|
||||
}
|
||||
.stagger-6 {
|
||||
animation-delay: 250ms;
|
||||
}
|
||||
|
||||
/* Focus visible styles */
|
||||
:focus-visible {
|
||||
|
||||
@@ -105,7 +105,9 @@ img.chat-avatar {
|
||||
border-radius: var(--radius-lg);
|
||||
padding: 10px 14px;
|
||||
box-shadow: none;
|
||||
transition: background 150ms ease-out, border-color 150ms ease-out;
|
||||
transition:
|
||||
background 150ms ease-out,
|
||||
border-color 150ms ease-out;
|
||||
max-width: 100%;
|
||||
word-wrap: break-word;
|
||||
}
|
||||
@@ -128,7 +130,9 @@ img.chat-avatar {
|
||||
cursor: pointer;
|
||||
opacity: 0;
|
||||
pointer-events: none;
|
||||
transition: opacity 120ms ease-out, background 120ms ease-out;
|
||||
transition:
|
||||
opacity 120ms ease-out,
|
||||
background 120ms ease-out;
|
||||
}
|
||||
|
||||
.chat-copy-btn__icon {
|
||||
@@ -243,7 +247,8 @@ img.chat-avatar {
|
||||
}
|
||||
|
||||
@keyframes pulsing-border {
|
||||
0%, 100% {
|
||||
0%,
|
||||
100% {
|
||||
border-color: var(--border);
|
||||
}
|
||||
50% {
|
||||
|
||||
@@ -77,7 +77,10 @@
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
transition: background 150ms ease-out, color 150ms ease-out, border-color 150ms ease-out;
|
||||
transition:
|
||||
background 150ms ease-out,
|
||||
color 150ms ease-out,
|
||||
border-color 150ms ease-out;
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.2);
|
||||
}
|
||||
|
||||
|
||||
@@ -6,7 +6,9 @@
|
||||
margin-top: 8px;
|
||||
background: var(--card);
|
||||
box-shadow: inset 0 1px 0 var(--card-highlight);
|
||||
transition: border-color 150ms ease-out, background 150ms ease-out;
|
||||
transition:
|
||||
border-color 150ms ease-out,
|
||||
background 150ms ease-out;
|
||||
/* Fixed max-height to ensure cards don't expand too much */
|
||||
max-height: 120px;
|
||||
overflow: hidden;
|
||||
@@ -187,7 +189,9 @@
|
||||
}
|
||||
|
||||
@keyframes reading-pulse {
|
||||
0%, 60%, 100% {
|
||||
0%,
|
||||
60%,
|
||||
100% {
|
||||
opacity: 0.3;
|
||||
transform: scale(0.8);
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
@import './chat.css';
|
||||
@import "./chat.css";
|
||||
|
||||
/* ===========================================
|
||||
Cards - Refined with depth
|
||||
@@ -14,12 +14,16 @@
|
||||
border-color var(--duration-normal) var(--ease-out),
|
||||
box-shadow var(--duration-normal) var(--ease-out),
|
||||
transform var(--duration-normal) var(--ease-out);
|
||||
box-shadow: var(--shadow-sm), inset 0 1px 0 var(--card-highlight);
|
||||
box-shadow:
|
||||
var(--shadow-sm),
|
||||
inset 0 1px 0 var(--card-highlight);
|
||||
}
|
||||
|
||||
.card:hover {
|
||||
border-color: var(--border-strong);
|
||||
box-shadow: var(--shadow-md), inset 0 1px 0 var(--card-highlight);
|
||||
box-shadow:
|
||||
var(--shadow-md),
|
||||
inset 0 1px 0 var(--card-highlight);
|
||||
}
|
||||
|
||||
.card-title {
|
||||
@@ -53,7 +57,9 @@
|
||||
|
||||
.stat:hover {
|
||||
border-color: var(--border-strong);
|
||||
box-shadow: var(--shadow-sm), inset 0 1px 0 var(--card-highlight);
|
||||
box-shadow:
|
||||
var(--shadow-sm),
|
||||
inset 0 1px 0 var(--card-highlight);
|
||||
}
|
||||
|
||||
.stat-label {
|
||||
@@ -351,7 +357,9 @@
|
||||
.btn.primary:hover {
|
||||
background: var(--accent-hover);
|
||||
border-color: var(--accent-hover);
|
||||
box-shadow: var(--shadow-md), 0 0 20px var(--accent-glow);
|
||||
box-shadow:
|
||||
var(--shadow-md),
|
||||
0 0 20px var(--accent-glow);
|
||||
}
|
||||
|
||||
/* Keyboard shortcut badge (shadcn style) */
|
||||
@@ -571,7 +579,8 @@
|
||||
}
|
||||
|
||||
@keyframes compaction-pulse {
|
||||
0%, 100% {
|
||||
0%,
|
||||
100% {
|
||||
opacity: 0.7;
|
||||
}
|
||||
50% {
|
||||
@@ -1050,7 +1059,8 @@
|
||||
}
|
||||
|
||||
@keyframes chatStreamPulse {
|
||||
0%, 100% {
|
||||
0%,
|
||||
100% {
|
||||
border-color: var(--border);
|
||||
}
|
||||
50% {
|
||||
@@ -1103,7 +1113,9 @@
|
||||
}
|
||||
|
||||
@keyframes chatReadingDot {
|
||||
0%, 80%, 100% {
|
||||
0%,
|
||||
80%,
|
||||
100% {
|
||||
opacity: 0.4;
|
||||
transform: translateY(0);
|
||||
}
|
||||
|
||||
@@ -122,9 +122,12 @@ export async function handleNostrProfileSave(host: OpenClawApp) {
|
||||
},
|
||||
body: JSON.stringify(state.values),
|
||||
});
|
||||
const data = (await response.json().catch(() => null)) as
|
||||
| { ok?: boolean; error?: string; details?: unknown; persisted?: boolean }
|
||||
| null;
|
||||
const data = (await response.json().catch(() => null)) as {
|
||||
ok?: boolean;
|
||||
error?: string;
|
||||
details?: unknown;
|
||||
persisted?: boolean;
|
||||
} | null;
|
||||
|
||||
if (!response.ok || data?.ok === false || !data) {
|
||||
const errorMessage = data?.error ?? `Profile update failed (${response.status})`;
|
||||
@@ -187,9 +190,13 @@ export async function handleNostrProfileImport(host: OpenClawApp) {
|
||||
},
|
||||
body: JSON.stringify({ autoMerge: true }),
|
||||
});
|
||||
const data = (await response.json().catch(() => null)) as
|
||||
| { ok?: boolean; error?: string; imported?: NostrProfile; merged?: NostrProfile; saved?: boolean }
|
||||
| null;
|
||||
const data = (await response.json().catch(() => null)) as {
|
||||
ok?: boolean;
|
||||
error?: string;
|
||||
imported?: NostrProfile;
|
||||
merged?: NostrProfile;
|
||||
saved?: boolean;
|
||||
} | null;
|
||||
|
||||
if (!response.ok || data?.ok === false || !data) {
|
||||
const errorMessage = data?.error ?? `Profile import failed (${response.status})`;
|
||||
|
||||
@@ -101,7 +101,10 @@ async function sendChatMessageNow(
|
||||
host.chatAttachments = opts.previousAttachments;
|
||||
}
|
||||
if (ok) {
|
||||
setLastActiveSessionKey(host as unknown as Parameters<typeof setLastActiveSessionKey>[0], host.sessionKey);
|
||||
setLastActiveSessionKey(
|
||||
host as unknown as Parameters<typeof setLastActiveSessionKey>[0],
|
||||
host.sessionKey,
|
||||
);
|
||||
}
|
||||
if (ok && opts?.restoreDraft && opts.previousDraft?.trim()) {
|
||||
host.chatMessage = opts.previousDraft;
|
||||
@@ -199,7 +202,9 @@ type SessionDefaultsSnapshot = {
|
||||
function resolveAgentIdForSession(host: ChatHost): string | null {
|
||||
const parsed = parseAgentSessionKey(host.sessionKey);
|
||||
if (parsed?.agentId) return parsed.agentId;
|
||||
const snapshot = host.hello?.snapshot as { sessionDefaults?: SessionDefaultsSnapshot } | undefined;
|
||||
const snapshot = host.hello?.snapshot as
|
||||
| { sessionDefaults?: SessionDefaultsSnapshot }
|
||||
| undefined;
|
||||
const fallback = snapshot?.sessionDefaults?.defaultAgentId?.trim();
|
||||
return fallback || "main";
|
||||
}
|
||||
|
||||
@@ -10,12 +10,7 @@ import type { Tab } from "./navigation";
|
||||
import type { UiSettings } from "./storage";
|
||||
import { handleAgentEvent, resetToolStream, type AgentEventPayload } from "./app-tool-stream";
|
||||
import { CHAT_SESSIONS_ACTIVE_MINUTES, flushChatQueueForEvent } from "./app-chat";
|
||||
import {
|
||||
applySettings,
|
||||
loadCron,
|
||||
refreshActiveTab,
|
||||
setLastActiveSessionKey,
|
||||
} from "./app-settings";
|
||||
import { applySettings, loadCron, refreshActiveTab, setLastActiveSessionKey } from "./app-settings";
|
||||
import { handleChatEvent, type ChatEventPayload } from "./controllers/chat";
|
||||
import {
|
||||
addExecApproval,
|
||||
@@ -77,8 +72,7 @@ function normalizeSessionKeyForDefaults(
|
||||
raw === "main" ||
|
||||
raw === mainKey ||
|
||||
(defaultAgentId &&
|
||||
(raw === `agent:${defaultAgentId}:main` ||
|
||||
raw === `agent:${defaultAgentId}:${mainKey}`));
|
||||
(raw === `agent:${defaultAgentId}:main` || raw === `agent:${defaultAgentId}:${mainKey}`));
|
||||
return isAlias ? mainSessionKey : raw;
|
||||
}
|
||||
|
||||
@@ -193,9 +187,7 @@ function handleGatewayEventUnsafe(host: GatewayHost, evt: GatewayEventFrame) {
|
||||
const state = handleChatEvent(host as unknown as OpenClawApp, payload);
|
||||
if (state === "final" || state === "error" || state === "aborted") {
|
||||
resetToolStream(host as unknown as Parameters<typeof resetToolStream>[0]);
|
||||
void flushChatQueueForEvent(
|
||||
host as unknown as Parameters<typeof flushChatQueueForEvent>[0],
|
||||
);
|
||||
void flushChatQueueForEvent(host as unknown as Parameters<typeof flushChatQueueForEvent>[0]);
|
||||
const runId = payload?.runId;
|
||||
if (runId && host.refreshSessionsAfterChat.has(runId)) {
|
||||
host.refreshSessionsAfterChat.delete(runId);
|
||||
|
||||
@@ -35,19 +35,10 @@ type LifecycleHost = {
|
||||
|
||||
export function handleConnected(host: LifecycleHost) {
|
||||
host.basePath = inferBasePath();
|
||||
applySettingsFromUrl(
|
||||
host as unknown as Parameters<typeof applySettingsFromUrl>[0],
|
||||
);
|
||||
syncTabWithLocation(
|
||||
host as unknown as Parameters<typeof syncTabWithLocation>[0],
|
||||
true,
|
||||
);
|
||||
syncThemeWithSettings(
|
||||
host as unknown as Parameters<typeof syncThemeWithSettings>[0],
|
||||
);
|
||||
attachThemeListener(
|
||||
host as unknown as Parameters<typeof attachThemeListener>[0],
|
||||
);
|
||||
applySettingsFromUrl(host as unknown as Parameters<typeof applySettingsFromUrl>[0]);
|
||||
syncTabWithLocation(host as unknown as Parameters<typeof syncTabWithLocation>[0], true);
|
||||
syncThemeWithSettings(host as unknown as Parameters<typeof syncThemeWithSettings>[0]);
|
||||
attachThemeListener(host as unknown as Parameters<typeof attachThemeListener>[0]);
|
||||
window.addEventListener("popstate", host.popStateHandler);
|
||||
connectGateway(host as unknown as Parameters<typeof connectGateway>[0]);
|
||||
startNodesPolling(host as unknown as Parameters<typeof startNodesPolling>[0]);
|
||||
@@ -68,17 +59,12 @@ export function handleDisconnected(host: LifecycleHost) {
|
||||
stopNodesPolling(host as unknown as Parameters<typeof stopNodesPolling>[0]);
|
||||
stopLogsPolling(host as unknown as Parameters<typeof stopLogsPolling>[0]);
|
||||
stopDebugPolling(host as unknown as Parameters<typeof stopDebugPolling>[0]);
|
||||
detachThemeListener(
|
||||
host as unknown as Parameters<typeof detachThemeListener>[0],
|
||||
);
|
||||
detachThemeListener(host as unknown as Parameters<typeof detachThemeListener>[0]);
|
||||
host.topbarObserver?.disconnect();
|
||||
host.topbarObserver = null;
|
||||
}
|
||||
|
||||
export function handleUpdated(
|
||||
host: LifecycleHost,
|
||||
changed: Map<PropertyKey, unknown>,
|
||||
) {
|
||||
export function handleUpdated(host: LifecycleHost, changed: Map<PropertyKey, unknown>) {
|
||||
if (
|
||||
host.tab === "chat" &&
|
||||
(changed.has("chatMessages") ||
|
||||
|
||||
@@ -51,8 +51,39 @@ export function renderChatControls(state: AppViewState) {
|
||||
const showThinking = state.onboarding ? false : state.settings.chatShowThinking;
|
||||
const focusActive = state.onboarding ? true : state.settings.chatFocusMode;
|
||||
// Refresh icon
|
||||
const refreshIcon = html`<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M21 12a9 9 0 1 1-9-9c2.52 0 4.93 1 6.74 2.74L21 8"></path><path d="M21 3v5h-5"></path></svg>`;
|
||||
const focusIcon = html`<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M4 7V4h3"></path><path d="M20 7V4h-3"></path><path d="M4 17v3h3"></path><path d="M20 17v3h-3"></path><circle cx="12" cy="12" r="3"></circle></svg>`;
|
||||
const refreshIcon = html`
|
||||
<svg
|
||||
width="18"
|
||||
height="18"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
>
|
||||
<path d="M21 12a9 9 0 1 1-9-9c2.52 0 4.93 1 6.74 2.74L21 8"></path>
|
||||
<path d="M21 3v5h-5"></path>
|
||||
</svg>
|
||||
`;
|
||||
const focusIcon = html`
|
||||
<svg
|
||||
width="18"
|
||||
height="18"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
>
|
||||
<path d="M4 7V4h3"></path>
|
||||
<path d="M20 7V4h-3"></path>
|
||||
<path d="M4 17v3h3"></path>
|
||||
<path d="M20 17v3h-3"></path>
|
||||
<circle cx="12" cy="12" r="3"></circle>
|
||||
</svg>
|
||||
`;
|
||||
return html`
|
||||
<div class="chat-controls">
|
||||
<label class="field chat-controls__session">
|
||||
@@ -111,9 +142,11 @@ export function renderChatControls(state: AppViewState) {
|
||||
});
|
||||
}}
|
||||
aria-pressed=${showThinking}
|
||||
title=${disableThinkingToggle
|
||||
? "Disabled during onboarding"
|
||||
: "Toggle assistant thinking/working output"}
|
||||
title=${
|
||||
disableThinkingToggle
|
||||
? "Disabled during onboarding"
|
||||
: "Toggle assistant thinking/working output"
|
||||
}
|
||||
>
|
||||
${icons.brain}
|
||||
</button>
|
||||
@@ -128,9 +161,11 @@ export function renderChatControls(state: AppViewState) {
|
||||
});
|
||||
}}
|
||||
aria-pressed=${focusActive}
|
||||
title=${disableFocusToggle
|
||||
? "Disabled during onboarding"
|
||||
: "Toggle focus mode (hide sidebar + page header)"}
|
||||
title=${
|
||||
disableFocusToggle
|
||||
? "Disabled during onboarding"
|
||||
: "Toggle focus mode (hide sidebar + page header)"
|
||||
}
|
||||
>
|
||||
${focusIcon}
|
||||
</button>
|
||||
@@ -156,10 +191,7 @@ function resolveMainSessionKey(
|
||||
return null;
|
||||
}
|
||||
|
||||
function resolveSessionDisplayName(
|
||||
key: string,
|
||||
row?: SessionsListResult["sessions"][number],
|
||||
) {
|
||||
function resolveSessionDisplayName(key: string, row?: SessionsListResult["sessions"][number]) {
|
||||
const label = row?.label?.trim();
|
||||
if (label) return `${label} (${key})`;
|
||||
const displayName = row?.displayName?.trim();
|
||||
@@ -175,8 +207,7 @@ function resolveSessionOptions(
|
||||
const seen = new Set<string>();
|
||||
const options: Array<{ key: string; displayName?: string }> = [];
|
||||
|
||||
const resolvedMain =
|
||||
mainSessionKey && sessions?.sessions?.find((s) => s.key === mainSessionKey);
|
||||
const resolvedMain = mainSessionKey && sessions?.sessions?.find((s) => s.key === mainSessionKey);
|
||||
const resolvedCurrent = sessions?.sessions?.find((s) => s.key === sessionKey);
|
||||
|
||||
// Add main session key first
|
||||
|
||||
+391
-369
@@ -79,7 +79,13 @@ import {
|
||||
saveExecApprovals,
|
||||
updateExecApprovalsFormValue,
|
||||
} from "./controllers/exec-approvals";
|
||||
import { loadCronRuns, toggleCronJob, runCronJob, removeCronJob, addCronJob } from "./controllers/cron";
|
||||
import {
|
||||
loadCronRuns,
|
||||
toggleCronJob,
|
||||
runCronJob,
|
||||
removeCronJob,
|
||||
addCronJob,
|
||||
} from "./controllers/cron";
|
||||
import { loadDebug, callDebugMethod } from "./controllers/debug";
|
||||
import { loadLogs } from "./controllers/logs";
|
||||
|
||||
@@ -89,10 +95,7 @@ const AVATAR_HTTP_RE = /^https?:\/\//i;
|
||||
function resolveAssistantAvatarUrl(state: AppViewState): string | undefined {
|
||||
const list = state.agentsList?.agents ?? [];
|
||||
const parsed = parseAgentSessionKey(state.sessionKey);
|
||||
const agentId =
|
||||
parsed?.agentId ??
|
||||
state.agentsList?.defaultId ??
|
||||
"main";
|
||||
const agentId = parsed?.agentId ?? state.agentsList?.defaultId ?? "main";
|
||||
const agent = list.find((entry) => entry.id === agentId);
|
||||
const identity = agent?.identity;
|
||||
const candidate = identity?.avatarUrl ?? identity?.avatar;
|
||||
@@ -199,384 +202,403 @@ export function renderApp(state: AppViewState) {
|
||||
<div class="page-sub">${subtitleForTab(state.tab)}</div>
|
||||
</div>
|
||||
<div class="page-meta">
|
||||
${state.lastError
|
||||
? html`<div class="pill danger">${state.lastError}</div>`
|
||||
: nothing}
|
||||
${state.lastError ? html`<div class="pill danger">${state.lastError}</div>` : nothing}
|
||||
${isChat ? renderChatControls(state) : nothing}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
${state.tab === "overview"
|
||||
? renderOverview({
|
||||
connected: state.connected,
|
||||
hello: state.hello,
|
||||
settings: state.settings,
|
||||
password: state.password,
|
||||
lastError: state.lastError,
|
||||
presenceCount,
|
||||
sessionsCount,
|
||||
cronEnabled: state.cronStatus?.enabled ?? null,
|
||||
cronNext,
|
||||
lastChannelsRefresh: state.channelsLastSuccess,
|
||||
onSettingsChange: (next) => state.applySettings(next),
|
||||
onPasswordChange: (next) => (state.password = next),
|
||||
onSessionKeyChange: (next) => {
|
||||
state.sessionKey = next;
|
||||
state.chatMessage = "";
|
||||
state.resetToolStream();
|
||||
state.applySettings({
|
||||
...state.settings,
|
||||
sessionKey: next,
|
||||
lastActiveSessionKey: next,
|
||||
});
|
||||
void state.loadAssistantIdentity();
|
||||
},
|
||||
onConnect: () => state.connect(),
|
||||
onRefresh: () => state.loadOverview(),
|
||||
})
|
||||
: nothing}
|
||||
${
|
||||
state.tab === "overview"
|
||||
? renderOverview({
|
||||
connected: state.connected,
|
||||
hello: state.hello,
|
||||
settings: state.settings,
|
||||
password: state.password,
|
||||
lastError: state.lastError,
|
||||
presenceCount,
|
||||
sessionsCount,
|
||||
cronEnabled: state.cronStatus?.enabled ?? null,
|
||||
cronNext,
|
||||
lastChannelsRefresh: state.channelsLastSuccess,
|
||||
onSettingsChange: (next) => state.applySettings(next),
|
||||
onPasswordChange: (next) => (state.password = next),
|
||||
onSessionKeyChange: (next) => {
|
||||
state.sessionKey = next;
|
||||
state.chatMessage = "";
|
||||
state.resetToolStream();
|
||||
state.applySettings({
|
||||
...state.settings,
|
||||
sessionKey: next,
|
||||
lastActiveSessionKey: next,
|
||||
});
|
||||
void state.loadAssistantIdentity();
|
||||
},
|
||||
onConnect: () => state.connect(),
|
||||
onRefresh: () => state.loadOverview(),
|
||||
})
|
||||
: nothing
|
||||
}
|
||||
|
||||
${state.tab === "channels"
|
||||
? renderChannels({
|
||||
connected: state.connected,
|
||||
loading: state.channelsLoading,
|
||||
snapshot: state.channelsSnapshot,
|
||||
lastError: state.channelsError,
|
||||
lastSuccessAt: state.channelsLastSuccess,
|
||||
whatsappMessage: state.whatsappLoginMessage,
|
||||
whatsappQrDataUrl: state.whatsappLoginQrDataUrl,
|
||||
whatsappConnected: state.whatsappLoginConnected,
|
||||
whatsappBusy: state.whatsappBusy,
|
||||
configSchema: state.configSchema,
|
||||
configSchemaLoading: state.configSchemaLoading,
|
||||
configForm: state.configForm,
|
||||
configUiHints: state.configUiHints,
|
||||
configSaving: state.configSaving,
|
||||
configFormDirty: state.configFormDirty,
|
||||
nostrProfileFormState: state.nostrProfileFormState,
|
||||
nostrProfileAccountId: state.nostrProfileAccountId,
|
||||
onRefresh: (probe) => loadChannels(state, probe),
|
||||
onWhatsAppStart: (force) => state.handleWhatsAppStart(force),
|
||||
onWhatsAppWait: () => state.handleWhatsAppWait(),
|
||||
onWhatsAppLogout: () => state.handleWhatsAppLogout(),
|
||||
onConfigPatch: (path, value) => updateConfigFormValue(state, path, value),
|
||||
onConfigSave: () => state.handleChannelConfigSave(),
|
||||
onConfigReload: () => state.handleChannelConfigReload(),
|
||||
onNostrProfileEdit: (accountId, profile) =>
|
||||
state.handleNostrProfileEdit(accountId, profile),
|
||||
onNostrProfileCancel: () => state.handleNostrProfileCancel(),
|
||||
onNostrProfileFieldChange: (field, value) =>
|
||||
state.handleNostrProfileFieldChange(field, value),
|
||||
onNostrProfileSave: () => state.handleNostrProfileSave(),
|
||||
onNostrProfileImport: () => state.handleNostrProfileImport(),
|
||||
onNostrProfileToggleAdvanced: () => state.handleNostrProfileToggleAdvanced(),
|
||||
})
|
||||
: nothing}
|
||||
${
|
||||
state.tab === "channels"
|
||||
? renderChannels({
|
||||
connected: state.connected,
|
||||
loading: state.channelsLoading,
|
||||
snapshot: state.channelsSnapshot,
|
||||
lastError: state.channelsError,
|
||||
lastSuccessAt: state.channelsLastSuccess,
|
||||
whatsappMessage: state.whatsappLoginMessage,
|
||||
whatsappQrDataUrl: state.whatsappLoginQrDataUrl,
|
||||
whatsappConnected: state.whatsappLoginConnected,
|
||||
whatsappBusy: state.whatsappBusy,
|
||||
configSchema: state.configSchema,
|
||||
configSchemaLoading: state.configSchemaLoading,
|
||||
configForm: state.configForm,
|
||||
configUiHints: state.configUiHints,
|
||||
configSaving: state.configSaving,
|
||||
configFormDirty: state.configFormDirty,
|
||||
nostrProfileFormState: state.nostrProfileFormState,
|
||||
nostrProfileAccountId: state.nostrProfileAccountId,
|
||||
onRefresh: (probe) => loadChannels(state, probe),
|
||||
onWhatsAppStart: (force) => state.handleWhatsAppStart(force),
|
||||
onWhatsAppWait: () => state.handleWhatsAppWait(),
|
||||
onWhatsAppLogout: () => state.handleWhatsAppLogout(),
|
||||
onConfigPatch: (path, value) => updateConfigFormValue(state, path, value),
|
||||
onConfigSave: () => state.handleChannelConfigSave(),
|
||||
onConfigReload: () => state.handleChannelConfigReload(),
|
||||
onNostrProfileEdit: (accountId, profile) =>
|
||||
state.handleNostrProfileEdit(accountId, profile),
|
||||
onNostrProfileCancel: () => state.handleNostrProfileCancel(),
|
||||
onNostrProfileFieldChange: (field, value) =>
|
||||
state.handleNostrProfileFieldChange(field, value),
|
||||
onNostrProfileSave: () => state.handleNostrProfileSave(),
|
||||
onNostrProfileImport: () => state.handleNostrProfileImport(),
|
||||
onNostrProfileToggleAdvanced: () => state.handleNostrProfileToggleAdvanced(),
|
||||
})
|
||||
: nothing
|
||||
}
|
||||
|
||||
${state.tab === "instances"
|
||||
? renderInstances({
|
||||
loading: state.presenceLoading,
|
||||
entries: state.presenceEntries,
|
||||
lastError: state.presenceError,
|
||||
statusMessage: state.presenceStatus,
|
||||
onRefresh: () => loadPresence(state),
|
||||
})
|
||||
: nothing}
|
||||
${
|
||||
state.tab === "instances"
|
||||
? renderInstances({
|
||||
loading: state.presenceLoading,
|
||||
entries: state.presenceEntries,
|
||||
lastError: state.presenceError,
|
||||
statusMessage: state.presenceStatus,
|
||||
onRefresh: () => loadPresence(state),
|
||||
})
|
||||
: nothing
|
||||
}
|
||||
|
||||
${state.tab === "sessions"
|
||||
? renderSessions({
|
||||
loading: state.sessionsLoading,
|
||||
result: state.sessionsResult,
|
||||
error: state.sessionsError,
|
||||
activeMinutes: state.sessionsFilterActive,
|
||||
limit: state.sessionsFilterLimit,
|
||||
includeGlobal: state.sessionsIncludeGlobal,
|
||||
includeUnknown: state.sessionsIncludeUnknown,
|
||||
basePath: state.basePath,
|
||||
onFiltersChange: (next) => {
|
||||
state.sessionsFilterActive = next.activeMinutes;
|
||||
state.sessionsFilterLimit = next.limit;
|
||||
state.sessionsIncludeGlobal = next.includeGlobal;
|
||||
state.sessionsIncludeUnknown = next.includeUnknown;
|
||||
},
|
||||
onRefresh: () => loadSessions(state),
|
||||
onPatch: (key, patch) => patchSession(state, key, patch),
|
||||
onDelete: (key) => deleteSession(state, key),
|
||||
})
|
||||
: nothing}
|
||||
${
|
||||
state.tab === "sessions"
|
||||
? renderSessions({
|
||||
loading: state.sessionsLoading,
|
||||
result: state.sessionsResult,
|
||||
error: state.sessionsError,
|
||||
activeMinutes: state.sessionsFilterActive,
|
||||
limit: state.sessionsFilterLimit,
|
||||
includeGlobal: state.sessionsIncludeGlobal,
|
||||
includeUnknown: state.sessionsIncludeUnknown,
|
||||
basePath: state.basePath,
|
||||
onFiltersChange: (next) => {
|
||||
state.sessionsFilterActive = next.activeMinutes;
|
||||
state.sessionsFilterLimit = next.limit;
|
||||
state.sessionsIncludeGlobal = next.includeGlobal;
|
||||
state.sessionsIncludeUnknown = next.includeUnknown;
|
||||
},
|
||||
onRefresh: () => loadSessions(state),
|
||||
onPatch: (key, patch) => patchSession(state, key, patch),
|
||||
onDelete: (key) => deleteSession(state, key),
|
||||
})
|
||||
: nothing
|
||||
}
|
||||
|
||||
${state.tab === "cron"
|
||||
? renderCron({
|
||||
loading: state.cronLoading,
|
||||
status: state.cronStatus,
|
||||
jobs: state.cronJobs,
|
||||
error: state.cronError,
|
||||
busy: state.cronBusy,
|
||||
form: state.cronForm,
|
||||
channels: state.channelsSnapshot?.channelMeta?.length
|
||||
? state.channelsSnapshot.channelMeta.map((entry) => entry.id)
|
||||
: state.channelsSnapshot?.channelOrder ?? [],
|
||||
channelLabels: state.channelsSnapshot?.channelLabels ?? {},
|
||||
channelMeta: state.channelsSnapshot?.channelMeta ?? [],
|
||||
runsJobId: state.cronRunsJobId,
|
||||
runs: state.cronRuns,
|
||||
onFormChange: (patch) => (state.cronForm = { ...state.cronForm, ...patch }),
|
||||
onRefresh: () => state.loadCron(),
|
||||
onAdd: () => addCronJob(state),
|
||||
onToggle: (job, enabled) => toggleCronJob(state, job, enabled),
|
||||
onRun: (job) => runCronJob(state, job),
|
||||
onRemove: (job) => removeCronJob(state, job),
|
||||
onLoadRuns: (jobId) => loadCronRuns(state, jobId),
|
||||
})
|
||||
: nothing}
|
||||
${
|
||||
state.tab === "cron"
|
||||
? renderCron({
|
||||
loading: state.cronLoading,
|
||||
status: state.cronStatus,
|
||||
jobs: state.cronJobs,
|
||||
error: state.cronError,
|
||||
busy: state.cronBusy,
|
||||
form: state.cronForm,
|
||||
channels: state.channelsSnapshot?.channelMeta?.length
|
||||
? state.channelsSnapshot.channelMeta.map((entry) => entry.id)
|
||||
: (state.channelsSnapshot?.channelOrder ?? []),
|
||||
channelLabels: state.channelsSnapshot?.channelLabels ?? {},
|
||||
channelMeta: state.channelsSnapshot?.channelMeta ?? [],
|
||||
runsJobId: state.cronRunsJobId,
|
||||
runs: state.cronRuns,
|
||||
onFormChange: (patch) => (state.cronForm = { ...state.cronForm, ...patch }),
|
||||
onRefresh: () => state.loadCron(),
|
||||
onAdd: () => addCronJob(state),
|
||||
onToggle: (job, enabled) => toggleCronJob(state, job, enabled),
|
||||
onRun: (job) => runCronJob(state, job),
|
||||
onRemove: (job) => removeCronJob(state, job),
|
||||
onLoadRuns: (jobId) => loadCronRuns(state, jobId),
|
||||
})
|
||||
: nothing
|
||||
}
|
||||
|
||||
${state.tab === "skills"
|
||||
? renderSkills({
|
||||
loading: state.skillsLoading,
|
||||
report: state.skillsReport,
|
||||
error: state.skillsError,
|
||||
filter: state.skillsFilter,
|
||||
edits: state.skillEdits,
|
||||
messages: state.skillMessages,
|
||||
busyKey: state.skillsBusyKey,
|
||||
onFilterChange: (next) => (state.skillsFilter = next),
|
||||
onRefresh: () => loadSkills(state, { clearMessages: true }),
|
||||
onToggle: (key, enabled) => updateSkillEnabled(state, key, enabled),
|
||||
onEdit: (key, value) => updateSkillEdit(state, key, value),
|
||||
onSaveKey: (key) => saveSkillApiKey(state, key),
|
||||
onInstall: (skillKey, name, installId) =>
|
||||
installSkill(state, skillKey, name, installId),
|
||||
})
|
||||
: nothing}
|
||||
${
|
||||
state.tab === "skills"
|
||||
? renderSkills({
|
||||
loading: state.skillsLoading,
|
||||
report: state.skillsReport,
|
||||
error: state.skillsError,
|
||||
filter: state.skillsFilter,
|
||||
edits: state.skillEdits,
|
||||
messages: state.skillMessages,
|
||||
busyKey: state.skillsBusyKey,
|
||||
onFilterChange: (next) => (state.skillsFilter = next),
|
||||
onRefresh: () => loadSkills(state, { clearMessages: true }),
|
||||
onToggle: (key, enabled) => updateSkillEnabled(state, key, enabled),
|
||||
onEdit: (key, value) => updateSkillEdit(state, key, value),
|
||||
onSaveKey: (key) => saveSkillApiKey(state, key),
|
||||
onInstall: (skillKey, name, installId) =>
|
||||
installSkill(state, skillKey, name, installId),
|
||||
})
|
||||
: nothing
|
||||
}
|
||||
|
||||
${state.tab === "nodes"
|
||||
? renderNodes({
|
||||
loading: state.nodesLoading,
|
||||
nodes: state.nodes,
|
||||
devicesLoading: state.devicesLoading,
|
||||
devicesError: state.devicesError,
|
||||
devicesList: state.devicesList,
|
||||
configForm: state.configForm ?? (state.configSnapshot?.config as Record<string, unknown> | null),
|
||||
configLoading: state.configLoading,
|
||||
configSaving: state.configSaving,
|
||||
configDirty: state.configFormDirty,
|
||||
configFormMode: state.configFormMode,
|
||||
execApprovalsLoading: state.execApprovalsLoading,
|
||||
execApprovalsSaving: state.execApprovalsSaving,
|
||||
execApprovalsDirty: state.execApprovalsDirty,
|
||||
execApprovalsSnapshot: state.execApprovalsSnapshot,
|
||||
execApprovalsForm: state.execApprovalsForm,
|
||||
execApprovalsSelectedAgent: state.execApprovalsSelectedAgent,
|
||||
execApprovalsTarget: state.execApprovalsTarget,
|
||||
execApprovalsTargetNodeId: state.execApprovalsTargetNodeId,
|
||||
onRefresh: () => loadNodes(state),
|
||||
onDevicesRefresh: () => loadDevices(state),
|
||||
onDeviceApprove: (requestId) => approveDevicePairing(state, requestId),
|
||||
onDeviceReject: (requestId) => rejectDevicePairing(state, requestId),
|
||||
onDeviceRotate: (deviceId, role, scopes) =>
|
||||
rotateDeviceToken(state, { deviceId, role, scopes }),
|
||||
onDeviceRevoke: (deviceId, role) =>
|
||||
revokeDeviceToken(state, { deviceId, role }),
|
||||
onLoadConfig: () => loadConfig(state),
|
||||
onLoadExecApprovals: () => {
|
||||
const target =
|
||||
state.execApprovalsTarget === "node" && state.execApprovalsTargetNodeId
|
||||
? { kind: "node" as const, nodeId: state.execApprovalsTargetNodeId }
|
||||
: { kind: "gateway" as const };
|
||||
return loadExecApprovals(state, target);
|
||||
},
|
||||
onBindDefault: (nodeId) => {
|
||||
if (nodeId) {
|
||||
updateConfigFormValue(state, ["tools", "exec", "node"], nodeId);
|
||||
} else {
|
||||
removeConfigFormValue(state, ["tools", "exec", "node"]);
|
||||
}
|
||||
},
|
||||
onBindAgent: (agentIndex, nodeId) => {
|
||||
const basePath = ["agents", "list", agentIndex, "tools", "exec", "node"];
|
||||
if (nodeId) {
|
||||
updateConfigFormValue(state, basePath, nodeId);
|
||||
} else {
|
||||
removeConfigFormValue(state, basePath);
|
||||
}
|
||||
},
|
||||
onSaveBindings: () => saveConfig(state),
|
||||
onExecApprovalsTargetChange: (kind, nodeId) => {
|
||||
state.execApprovalsTarget = kind;
|
||||
state.execApprovalsTargetNodeId = nodeId;
|
||||
state.execApprovalsSnapshot = null;
|
||||
state.execApprovalsForm = null;
|
||||
state.execApprovalsDirty = false;
|
||||
state.execApprovalsSelectedAgent = null;
|
||||
},
|
||||
onExecApprovalsSelectAgent: (agentId) => {
|
||||
state.execApprovalsSelectedAgent = agentId;
|
||||
},
|
||||
onExecApprovalsPatch: (path, value) =>
|
||||
updateExecApprovalsFormValue(state, path, value),
|
||||
onExecApprovalsRemove: (path) =>
|
||||
removeExecApprovalsFormValue(state, path),
|
||||
onSaveExecApprovals: () => {
|
||||
const target =
|
||||
state.execApprovalsTarget === "node" && state.execApprovalsTargetNodeId
|
||||
? { kind: "node" as const, nodeId: state.execApprovalsTargetNodeId }
|
||||
: { kind: "gateway" as const };
|
||||
return saveExecApprovals(state, target);
|
||||
},
|
||||
})
|
||||
: nothing}
|
||||
${
|
||||
state.tab === "nodes"
|
||||
? renderNodes({
|
||||
loading: state.nodesLoading,
|
||||
nodes: state.nodes,
|
||||
devicesLoading: state.devicesLoading,
|
||||
devicesError: state.devicesError,
|
||||
devicesList: state.devicesList,
|
||||
configForm:
|
||||
state.configForm ??
|
||||
(state.configSnapshot?.config as Record<string, unknown> | null),
|
||||
configLoading: state.configLoading,
|
||||
configSaving: state.configSaving,
|
||||
configDirty: state.configFormDirty,
|
||||
configFormMode: state.configFormMode,
|
||||
execApprovalsLoading: state.execApprovalsLoading,
|
||||
execApprovalsSaving: state.execApprovalsSaving,
|
||||
execApprovalsDirty: state.execApprovalsDirty,
|
||||
execApprovalsSnapshot: state.execApprovalsSnapshot,
|
||||
execApprovalsForm: state.execApprovalsForm,
|
||||
execApprovalsSelectedAgent: state.execApprovalsSelectedAgent,
|
||||
execApprovalsTarget: state.execApprovalsTarget,
|
||||
execApprovalsTargetNodeId: state.execApprovalsTargetNodeId,
|
||||
onRefresh: () => loadNodes(state),
|
||||
onDevicesRefresh: () => loadDevices(state),
|
||||
onDeviceApprove: (requestId) => approveDevicePairing(state, requestId),
|
||||
onDeviceReject: (requestId) => rejectDevicePairing(state, requestId),
|
||||
onDeviceRotate: (deviceId, role, scopes) =>
|
||||
rotateDeviceToken(state, { deviceId, role, scopes }),
|
||||
onDeviceRevoke: (deviceId, role) => revokeDeviceToken(state, { deviceId, role }),
|
||||
onLoadConfig: () => loadConfig(state),
|
||||
onLoadExecApprovals: () => {
|
||||
const target =
|
||||
state.execApprovalsTarget === "node" && state.execApprovalsTargetNodeId
|
||||
? { kind: "node" as const, nodeId: state.execApprovalsTargetNodeId }
|
||||
: { kind: "gateway" as const };
|
||||
return loadExecApprovals(state, target);
|
||||
},
|
||||
onBindDefault: (nodeId) => {
|
||||
if (nodeId) {
|
||||
updateConfigFormValue(state, ["tools", "exec", "node"], nodeId);
|
||||
} else {
|
||||
removeConfigFormValue(state, ["tools", "exec", "node"]);
|
||||
}
|
||||
},
|
||||
onBindAgent: (agentIndex, nodeId) => {
|
||||
const basePath = ["agents", "list", agentIndex, "tools", "exec", "node"];
|
||||
if (nodeId) {
|
||||
updateConfigFormValue(state, basePath, nodeId);
|
||||
} else {
|
||||
removeConfigFormValue(state, basePath);
|
||||
}
|
||||
},
|
||||
onSaveBindings: () => saveConfig(state),
|
||||
onExecApprovalsTargetChange: (kind, nodeId) => {
|
||||
state.execApprovalsTarget = kind;
|
||||
state.execApprovalsTargetNodeId = nodeId;
|
||||
state.execApprovalsSnapshot = null;
|
||||
state.execApprovalsForm = null;
|
||||
state.execApprovalsDirty = false;
|
||||
state.execApprovalsSelectedAgent = null;
|
||||
},
|
||||
onExecApprovalsSelectAgent: (agentId) => {
|
||||
state.execApprovalsSelectedAgent = agentId;
|
||||
},
|
||||
onExecApprovalsPatch: (path, value) =>
|
||||
updateExecApprovalsFormValue(state, path, value),
|
||||
onExecApprovalsRemove: (path) => removeExecApprovalsFormValue(state, path),
|
||||
onSaveExecApprovals: () => {
|
||||
const target =
|
||||
state.execApprovalsTarget === "node" && state.execApprovalsTargetNodeId
|
||||
? { kind: "node" as const, nodeId: state.execApprovalsTargetNodeId }
|
||||
: { kind: "gateway" as const };
|
||||
return saveExecApprovals(state, target);
|
||||
},
|
||||
})
|
||||
: nothing
|
||||
}
|
||||
|
||||
${state.tab === "chat"
|
||||
? renderChat({
|
||||
sessionKey: state.sessionKey,
|
||||
onSessionKeyChange: (next) => {
|
||||
state.sessionKey = next;
|
||||
state.chatMessage = "";
|
||||
state.chatAttachments = [];
|
||||
state.chatStream = null;
|
||||
state.chatStreamStartedAt = null;
|
||||
state.chatRunId = null;
|
||||
state.chatQueue = [];
|
||||
state.resetToolStream();
|
||||
state.resetChatScroll();
|
||||
state.applySettings({
|
||||
...state.settings,
|
||||
sessionKey: next,
|
||||
lastActiveSessionKey: next,
|
||||
});
|
||||
void state.loadAssistantIdentity();
|
||||
void loadChatHistory(state);
|
||||
void refreshChatAvatar(state);
|
||||
},
|
||||
thinkingLevel: state.chatThinkingLevel,
|
||||
showThinking,
|
||||
loading: state.chatLoading,
|
||||
sending: state.chatSending,
|
||||
compactionStatus: state.compactionStatus,
|
||||
assistantAvatarUrl: chatAvatarUrl,
|
||||
messages: state.chatMessages,
|
||||
toolMessages: state.chatToolMessages,
|
||||
stream: state.chatStream,
|
||||
streamStartedAt: state.chatStreamStartedAt,
|
||||
draft: state.chatMessage,
|
||||
queue: state.chatQueue,
|
||||
connected: state.connected,
|
||||
canSend: state.connected,
|
||||
disabledReason: chatDisabledReason,
|
||||
error: state.lastError,
|
||||
sessions: state.sessionsResult,
|
||||
focusMode: chatFocus,
|
||||
onRefresh: () => {
|
||||
state.resetToolStream();
|
||||
return Promise.all([loadChatHistory(state), refreshChatAvatar(state)]);
|
||||
},
|
||||
onToggleFocusMode: () => {
|
||||
if (state.onboarding) return;
|
||||
state.applySettings({
|
||||
...state.settings,
|
||||
chatFocusMode: !state.settings.chatFocusMode,
|
||||
});
|
||||
},
|
||||
onChatScroll: (event) => state.handleChatScroll(event),
|
||||
onDraftChange: (next) => (state.chatMessage = next),
|
||||
attachments: state.chatAttachments,
|
||||
onAttachmentsChange: (next) => (state.chatAttachments = next),
|
||||
onSend: () => state.handleSendChat(),
|
||||
canAbort: Boolean(state.chatRunId),
|
||||
onAbort: () => void state.handleAbortChat(),
|
||||
onQueueRemove: (id) => state.removeQueuedMessage(id),
|
||||
onNewSession: () =>
|
||||
state.handleSendChat("/new", { restoreDraft: true }),
|
||||
// Sidebar props for tool output viewing
|
||||
sidebarOpen: state.sidebarOpen,
|
||||
sidebarContent: state.sidebarContent,
|
||||
sidebarError: state.sidebarError,
|
||||
splitRatio: state.splitRatio,
|
||||
onOpenSidebar: (content: string) => state.handleOpenSidebar(content),
|
||||
onCloseSidebar: () => state.handleCloseSidebar(),
|
||||
onSplitRatioChange: (ratio: number) => state.handleSplitRatioChange(ratio),
|
||||
assistantName: state.assistantName,
|
||||
assistantAvatar: state.assistantAvatar,
|
||||
})
|
||||
: nothing}
|
||||
${
|
||||
state.tab === "chat"
|
||||
? renderChat({
|
||||
sessionKey: state.sessionKey,
|
||||
onSessionKeyChange: (next) => {
|
||||
state.sessionKey = next;
|
||||
state.chatMessage = "";
|
||||
state.chatAttachments = [];
|
||||
state.chatStream = null;
|
||||
state.chatStreamStartedAt = null;
|
||||
state.chatRunId = null;
|
||||
state.chatQueue = [];
|
||||
state.resetToolStream();
|
||||
state.resetChatScroll();
|
||||
state.applySettings({
|
||||
...state.settings,
|
||||
sessionKey: next,
|
||||
lastActiveSessionKey: next,
|
||||
});
|
||||
void state.loadAssistantIdentity();
|
||||
void loadChatHistory(state);
|
||||
void refreshChatAvatar(state);
|
||||
},
|
||||
thinkingLevel: state.chatThinkingLevel,
|
||||
showThinking,
|
||||
loading: state.chatLoading,
|
||||
sending: state.chatSending,
|
||||
compactionStatus: state.compactionStatus,
|
||||
assistantAvatarUrl: chatAvatarUrl,
|
||||
messages: state.chatMessages,
|
||||
toolMessages: state.chatToolMessages,
|
||||
stream: state.chatStream,
|
||||
streamStartedAt: state.chatStreamStartedAt,
|
||||
draft: state.chatMessage,
|
||||
queue: state.chatQueue,
|
||||
connected: state.connected,
|
||||
canSend: state.connected,
|
||||
disabledReason: chatDisabledReason,
|
||||
error: state.lastError,
|
||||
sessions: state.sessionsResult,
|
||||
focusMode: chatFocus,
|
||||
onRefresh: () => {
|
||||
state.resetToolStream();
|
||||
return Promise.all([loadChatHistory(state), refreshChatAvatar(state)]);
|
||||
},
|
||||
onToggleFocusMode: () => {
|
||||
if (state.onboarding) return;
|
||||
state.applySettings({
|
||||
...state.settings,
|
||||
chatFocusMode: !state.settings.chatFocusMode,
|
||||
});
|
||||
},
|
||||
onChatScroll: (event) => state.handleChatScroll(event),
|
||||
onDraftChange: (next) => (state.chatMessage = next),
|
||||
attachments: state.chatAttachments,
|
||||
onAttachmentsChange: (next) => (state.chatAttachments = next),
|
||||
onSend: () => state.handleSendChat(),
|
||||
canAbort: Boolean(state.chatRunId),
|
||||
onAbort: () => void state.handleAbortChat(),
|
||||
onQueueRemove: (id) => state.removeQueuedMessage(id),
|
||||
onNewSession: () => state.handleSendChat("/new", { restoreDraft: true }),
|
||||
// Sidebar props for tool output viewing
|
||||
sidebarOpen: state.sidebarOpen,
|
||||
sidebarContent: state.sidebarContent,
|
||||
sidebarError: state.sidebarError,
|
||||
splitRatio: state.splitRatio,
|
||||
onOpenSidebar: (content: string) => state.handleOpenSidebar(content),
|
||||
onCloseSidebar: () => state.handleCloseSidebar(),
|
||||
onSplitRatioChange: (ratio: number) => state.handleSplitRatioChange(ratio),
|
||||
assistantName: state.assistantName,
|
||||
assistantAvatar: state.assistantAvatar,
|
||||
})
|
||||
: nothing
|
||||
}
|
||||
|
||||
${state.tab === "config"
|
||||
? renderConfig({
|
||||
raw: state.configRaw,
|
||||
originalRaw: state.configRawOriginal,
|
||||
valid: state.configValid,
|
||||
issues: state.configIssues,
|
||||
loading: state.configLoading,
|
||||
saving: state.configSaving,
|
||||
applying: state.configApplying,
|
||||
updating: state.updateRunning,
|
||||
connected: state.connected,
|
||||
schema: state.configSchema,
|
||||
schemaLoading: state.configSchemaLoading,
|
||||
uiHints: state.configUiHints,
|
||||
formMode: state.configFormMode,
|
||||
formValue: state.configForm,
|
||||
originalValue: state.configFormOriginal,
|
||||
searchQuery: state.configSearchQuery,
|
||||
activeSection: state.configActiveSection,
|
||||
activeSubsection: state.configActiveSubsection,
|
||||
onRawChange: (next) => {
|
||||
state.configRaw = next;
|
||||
},
|
||||
onFormModeChange: (mode) => (state.configFormMode = mode),
|
||||
onFormPatch: (path, value) => updateConfigFormValue(state, path, value),
|
||||
onSearchChange: (query) => (state.configSearchQuery = query),
|
||||
onSectionChange: (section) => {
|
||||
state.configActiveSection = section;
|
||||
state.configActiveSubsection = null;
|
||||
},
|
||||
onSubsectionChange: (section) => (state.configActiveSubsection = section),
|
||||
onReload: () => loadConfig(state),
|
||||
onSave: () => saveConfig(state),
|
||||
onApply: () => applyConfig(state),
|
||||
onUpdate: () => runUpdate(state),
|
||||
})
|
||||
: nothing}
|
||||
${
|
||||
state.tab === "config"
|
||||
? renderConfig({
|
||||
raw: state.configRaw,
|
||||
originalRaw: state.configRawOriginal,
|
||||
valid: state.configValid,
|
||||
issues: state.configIssues,
|
||||
loading: state.configLoading,
|
||||
saving: state.configSaving,
|
||||
applying: state.configApplying,
|
||||
updating: state.updateRunning,
|
||||
connected: state.connected,
|
||||
schema: state.configSchema,
|
||||
schemaLoading: state.configSchemaLoading,
|
||||
uiHints: state.configUiHints,
|
||||
formMode: state.configFormMode,
|
||||
formValue: state.configForm,
|
||||
originalValue: state.configFormOriginal,
|
||||
searchQuery: state.configSearchQuery,
|
||||
activeSection: state.configActiveSection,
|
||||
activeSubsection: state.configActiveSubsection,
|
||||
onRawChange: (next) => {
|
||||
state.configRaw = next;
|
||||
},
|
||||
onFormModeChange: (mode) => (state.configFormMode = mode),
|
||||
onFormPatch: (path, value) => updateConfigFormValue(state, path, value),
|
||||
onSearchChange: (query) => (state.configSearchQuery = query),
|
||||
onSectionChange: (section) => {
|
||||
state.configActiveSection = section;
|
||||
state.configActiveSubsection = null;
|
||||
},
|
||||
onSubsectionChange: (section) => (state.configActiveSubsection = section),
|
||||
onReload: () => loadConfig(state),
|
||||
onSave: () => saveConfig(state),
|
||||
onApply: () => applyConfig(state),
|
||||
onUpdate: () => runUpdate(state),
|
||||
})
|
||||
: nothing
|
||||
}
|
||||
|
||||
${state.tab === "debug"
|
||||
? renderDebug({
|
||||
loading: state.debugLoading,
|
||||
status: state.debugStatus,
|
||||
health: state.debugHealth,
|
||||
models: state.debugModels,
|
||||
heartbeat: state.debugHeartbeat,
|
||||
eventLog: state.eventLog,
|
||||
callMethod: state.debugCallMethod,
|
||||
callParams: state.debugCallParams,
|
||||
callResult: state.debugCallResult,
|
||||
callError: state.debugCallError,
|
||||
onCallMethodChange: (next) => (state.debugCallMethod = next),
|
||||
onCallParamsChange: (next) => (state.debugCallParams = next),
|
||||
onRefresh: () => loadDebug(state),
|
||||
onCall: () => callDebugMethod(state),
|
||||
})
|
||||
: nothing}
|
||||
${
|
||||
state.tab === "debug"
|
||||
? renderDebug({
|
||||
loading: state.debugLoading,
|
||||
status: state.debugStatus,
|
||||
health: state.debugHealth,
|
||||
models: state.debugModels,
|
||||
heartbeat: state.debugHeartbeat,
|
||||
eventLog: state.eventLog,
|
||||
callMethod: state.debugCallMethod,
|
||||
callParams: state.debugCallParams,
|
||||
callResult: state.debugCallResult,
|
||||
callError: state.debugCallError,
|
||||
onCallMethodChange: (next) => (state.debugCallMethod = next),
|
||||
onCallParamsChange: (next) => (state.debugCallParams = next),
|
||||
onRefresh: () => loadDebug(state),
|
||||
onCall: () => callDebugMethod(state),
|
||||
})
|
||||
: nothing
|
||||
}
|
||||
|
||||
${state.tab === "logs"
|
||||
? renderLogs({
|
||||
loading: state.logsLoading,
|
||||
error: state.logsError,
|
||||
file: state.logsFile,
|
||||
entries: state.logsEntries,
|
||||
filterText: state.logsFilterText,
|
||||
levelFilters: state.logsLevelFilters,
|
||||
autoFollow: state.logsAutoFollow,
|
||||
truncated: state.logsTruncated,
|
||||
onFilterTextChange: (next) => (state.logsFilterText = next),
|
||||
onLevelToggle: (level, enabled) => {
|
||||
state.logsLevelFilters = { ...state.logsLevelFilters, [level]: enabled };
|
||||
},
|
||||
onToggleAutoFollow: (next) => (state.logsAutoFollow = next),
|
||||
onRefresh: () => loadLogs(state, { reset: true }),
|
||||
onExport: (lines, label) => state.exportLogs(lines, label),
|
||||
onScroll: (event) => state.handleLogsScroll(event),
|
||||
})
|
||||
: nothing}
|
||||
${
|
||||
state.tab === "logs"
|
||||
? renderLogs({
|
||||
loading: state.logsLoading,
|
||||
error: state.logsError,
|
||||
file: state.logsFile,
|
||||
entries: state.logsEntries,
|
||||
filterText: state.logsFilterText,
|
||||
levelFilters: state.logsLevelFilters,
|
||||
autoFollow: state.logsAutoFollow,
|
||||
truncated: state.logsTruncated,
|
||||
onFilterTextChange: (next) => (state.logsFilterText = next),
|
||||
onLevelToggle: (level, enabled) => {
|
||||
state.logsLevelFilters = { ...state.logsLevelFilters, [level]: enabled };
|
||||
},
|
||||
onToggleAutoFollow: (next) => (state.logsAutoFollow = next),
|
||||
onRefresh: () => loadLogs(state, { reset: true }),
|
||||
onExport: (lines, label) => state.exportLogs(lines, label),
|
||||
onScroll: (event) => state.handleLogsScroll(event),
|
||||
})
|
||||
: nothing
|
||||
}
|
||||
</main>
|
||||
${renderExecApprovalPrompt(state)}
|
||||
${renderGatewayUrlConfirmation(state)}
|
||||
|
||||
@@ -35,8 +35,7 @@ export function scheduleChatScroll(host: ScrollHost, force = false) {
|
||||
host.chatScrollFrame = null;
|
||||
const target = pickScrollTarget();
|
||||
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;
|
||||
if (!shouldStick) return;
|
||||
if (force) host.chatHasAutoScrolled = true;
|
||||
@@ -49,8 +48,7 @@ export function scheduleChatScroll(host: ScrollHost, force = false) {
|
||||
if (!latest) return;
|
||||
const latestDistanceFromBottom =
|
||||
latest.scrollHeight - latest.scrollTop - latest.clientHeight;
|
||||
const shouldStickRetry =
|
||||
force || host.chatUserNearBottom || latestDistanceFromBottom < 200;
|
||||
const shouldStickRetry = force || host.chatUserNearBottom || latestDistanceFromBottom < 200;
|
||||
if (!shouldStickRetry) return;
|
||||
latest.scrollTop = latest.scrollHeight;
|
||||
host.chatUserNearBottom = true;
|
||||
@@ -78,16 +76,14 @@ export function scheduleLogsScroll(host: ScrollHost, force = false) {
|
||||
export function handleChatScroll(host: ScrollHost, event: Event) {
|
||||
const container = event.currentTarget as HTMLElement | null;
|
||||
if (!container) return;
|
||||
const distanceFromBottom =
|
||||
container.scrollHeight - container.scrollTop - container.clientHeight;
|
||||
const distanceFromBottom = container.scrollHeight - container.scrollTop - container.clientHeight;
|
||||
host.chatUserNearBottom = distanceFromBottom < 200;
|
||||
}
|
||||
|
||||
export function handleLogsScroll(host: ScrollHost, event: Event) {
|
||||
const container = event.currentTarget as HTMLElement | null;
|
||||
if (!container) return;
|
||||
const distanceFromBottom =
|
||||
container.scrollHeight - container.scrollTop - container.clientHeight;
|
||||
const distanceFromBottom = container.scrollHeight - container.scrollTop - container.clientHeight;
|
||||
host.logsAtBottom = distanceFromBottom < 80;
|
||||
}
|
||||
|
||||
|
||||
+19
-20
@@ -9,12 +9,24 @@ import { loadExecApprovals } from "./controllers/exec-approvals";
|
||||
import { loadPresence } from "./controllers/presence";
|
||||
import { loadSessions } from "./controllers/sessions";
|
||||
import { loadSkills } from "./controllers/skills";
|
||||
import { inferBasePathFromPathname, normalizeBasePath, normalizePath, pathForTab, tabFromPath, type Tab } from "./navigation";
|
||||
import {
|
||||
inferBasePathFromPathname,
|
||||
normalizeBasePath,
|
||||
normalizePath,
|
||||
pathForTab,
|
||||
tabFromPath,
|
||||
type Tab,
|
||||
} from "./navigation";
|
||||
import { saveSettings, type UiSettings } from "./storage";
|
||||
import { resolveTheme, type ResolvedTheme, type ThemeMode } from "./theme";
|
||||
import { startThemeTransition, type ThemeTransitionContext } from "./theme-transition";
|
||||
import { scheduleChatScroll, scheduleLogsScroll } from "./app-scroll";
|
||||
import { startLogsPolling, stopLogsPolling, startDebugPolling, stopDebugPolling } from "./app-polling";
|
||||
import {
|
||||
startLogsPolling,
|
||||
stopLogsPolling,
|
||||
startDebugPolling,
|
||||
stopDebugPolling,
|
||||
} from "./app-polling";
|
||||
import { refreshChat } from "./app-chat";
|
||||
import type { OpenClawApp } from "./app";
|
||||
|
||||
@@ -114,8 +126,7 @@ export function applySettingsFromUrl(host: SettingsHost) {
|
||||
export function setTab(host: SettingsHost, next: Tab) {
|
||||
if (host.tab !== next) host.tab = next;
|
||||
if (next === "chat") host.chatHasAutoScrolled = false;
|
||||
if (next === "logs")
|
||||
startLogsPolling(host as unknown as Parameters<typeof startLogsPolling>[0]);
|
||||
if (next === "logs") startLogsPolling(host as unknown as Parameters<typeof startLogsPolling>[0]);
|
||||
else stopLogsPolling(host as unknown as Parameters<typeof stopLogsPolling>[0]);
|
||||
if (next === "debug")
|
||||
startDebugPolling(host as unknown as Parameters<typeof startDebugPolling>[0]);
|
||||
@@ -124,11 +135,7 @@ export function setTab(host: SettingsHost, next: Tab) {
|
||||
syncUrlWithTab(host, next, false);
|
||||
}
|
||||
|
||||
export function setTheme(
|
||||
host: SettingsHost,
|
||||
next: ThemeMode,
|
||||
context?: ThemeTransitionContext,
|
||||
) {
|
||||
export function setTheme(host: SettingsHost, next: ThemeMode, context?: ThemeTransitionContext) {
|
||||
const applyTheme = () => {
|
||||
host.theme = next;
|
||||
applySettings(host, { ...host.settings, theme: next });
|
||||
@@ -173,10 +180,7 @@ export async function refreshActiveTab(host: SettingsHost) {
|
||||
if (host.tab === "logs") {
|
||||
host.logsAtBottom = true;
|
||||
await loadLogs(host as unknown as OpenClawApp, { reset: true });
|
||||
scheduleLogsScroll(
|
||||
host as unknown as Parameters<typeof scheduleLogsScroll>[0],
|
||||
true,
|
||||
);
|
||||
scheduleLogsScroll(host as unknown as Parameters<typeof scheduleLogsScroll>[0], true);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -262,8 +266,7 @@ export function onPopState(host: SettingsHost) {
|
||||
export function setTabFromRoute(host: SettingsHost, next: Tab) {
|
||||
if (host.tab !== next) host.tab = next;
|
||||
if (next === "chat") host.chatHasAutoScrolled = false;
|
||||
if (next === "logs")
|
||||
startLogsPolling(host as unknown as Parameters<typeof startLogsPolling>[0]);
|
||||
if (next === "logs") startLogsPolling(host as unknown as Parameters<typeof startLogsPolling>[0]);
|
||||
else stopLogsPolling(host as unknown as Parameters<typeof stopLogsPolling>[0]);
|
||||
if (next === "debug")
|
||||
startDebugPolling(host as unknown as Parameters<typeof startDebugPolling>[0]);
|
||||
@@ -294,11 +297,7 @@ 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;
|
||||
const url = new URL(window.location.href);
|
||||
url.searchParams.set("session", sessionKey);
|
||||
|
||||
@@ -191,8 +191,7 @@ export function handleAgentEvent(host: ToolStreamHost, payload?: AgentEventPaylo
|
||||
}
|
||||
|
||||
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;
|
||||
// Fallback: only accept session-less events for the active run.
|
||||
if (!sessionKey && host.chatRunId && payload.runId !== host.chatRunId) return;
|
||||
|
||||
@@ -22,10 +22,7 @@ import type {
|
||||
import type { ChatAttachment, ChatQueueItem, CronFormState } from "./ui-types";
|
||||
import type { EventLogEntry } from "./app-events";
|
||||
import type { SkillMessage } from "./controllers/skills";
|
||||
import type {
|
||||
ExecApprovalsFile,
|
||||
ExecApprovalsSnapshot,
|
||||
} from "./controllers/exec-approvals";
|
||||
import type { ExecApprovalsFile, ExecApprovalsSnapshot } from "./controllers/exec-approvals";
|
||||
import type { DevicePairingList } from "./controllers/devices";
|
||||
import type { ExecApprovalRequest } from "./controllers/exec-approval";
|
||||
import type { NostrProfileFormState } from "./views/channels.nostr-profile-form";
|
||||
|
||||
+15
-42
@@ -27,10 +27,7 @@ import type {
|
||||
import { type ChatAttachment, type ChatQueueItem, type CronFormState } from "./ui-types";
|
||||
import type { EventLogEntry } from "./app-events";
|
||||
import { DEFAULT_CRON_FORM, DEFAULT_LOG_LEVEL_FILTERS } from "./app-defaults";
|
||||
import type {
|
||||
ExecApprovalsFile,
|
||||
ExecApprovalsSnapshot,
|
||||
} from "./controllers/exec-approvals";
|
||||
import type { ExecApprovalsFile, ExecApprovalsSnapshot } from "./controllers/exec-approvals";
|
||||
import type { DevicePairingList } from "./controllers/devices";
|
||||
import type { ExecApprovalRequest } from "./controllers/exec-approval";
|
||||
import {
|
||||
@@ -261,9 +258,7 @@ export class OpenClawApp extends LitElement {
|
||||
refreshSessionsAfterChat = new Set<string>();
|
||||
basePath = "";
|
||||
private popStateHandler = () =>
|
||||
onPopStateInternal(
|
||||
this as unknown as Parameters<typeof onPopStateInternal>[0],
|
||||
);
|
||||
onPopStateInternal(this as unknown as Parameters<typeof onPopStateInternal>[0]);
|
||||
private themeMedia: MediaQueryList | null = null;
|
||||
private themeMediaHandler: ((event: MediaQueryListEvent) => void) | null = null;
|
||||
private topbarObserver: ResizeObserver | null = null;
|
||||
@@ -287,16 +282,11 @@ export class OpenClawApp extends LitElement {
|
||||
}
|
||||
|
||||
protected updated(changed: Map<PropertyKey, unknown>) {
|
||||
handleUpdated(
|
||||
this as unknown as Parameters<typeof handleUpdated>[0],
|
||||
changed,
|
||||
);
|
||||
handleUpdated(this as unknown as Parameters<typeof handleUpdated>[0], changed);
|
||||
}
|
||||
|
||||
connect() {
|
||||
connectGatewayInternal(
|
||||
this as unknown as Parameters<typeof connectGatewayInternal>[0],
|
||||
);
|
||||
connectGatewayInternal(this as unknown as Parameters<typeof connectGatewayInternal>[0]);
|
||||
}
|
||||
|
||||
handleChatScroll(event: Event) {
|
||||
@@ -318,15 +308,11 @@ export class OpenClawApp extends LitElement {
|
||||
}
|
||||
|
||||
resetToolStream() {
|
||||
resetToolStreamInternal(
|
||||
this as unknown as Parameters<typeof resetToolStreamInternal>[0],
|
||||
);
|
||||
resetToolStreamInternal(this as unknown as Parameters<typeof resetToolStreamInternal>[0]);
|
||||
}
|
||||
|
||||
resetChatScroll() {
|
||||
resetChatScrollInternal(
|
||||
this as unknown as Parameters<typeof resetChatScrollInternal>[0],
|
||||
);
|
||||
resetChatScrollInternal(this as unknown as Parameters<typeof resetChatScrollInternal>[0]);
|
||||
}
|
||||
|
||||
async loadAssistantIdentity() {
|
||||
@@ -334,10 +320,7 @@ export class OpenClawApp extends LitElement {
|
||||
}
|
||||
|
||||
applySettings(next: UiSettings) {
|
||||
applySettingsInternal(
|
||||
this as unknown as Parameters<typeof applySettingsInternal>[0],
|
||||
next,
|
||||
);
|
||||
applySettingsInternal(this as unknown as Parameters<typeof applySettingsInternal>[0], next);
|
||||
}
|
||||
|
||||
setTab(next: Tab) {
|
||||
@@ -345,29 +328,19 @@ export class OpenClawApp extends LitElement {
|
||||
}
|
||||
|
||||
setTheme(next: ThemeMode, context?: Parameters<typeof setThemeInternal>[2]) {
|
||||
setThemeInternal(
|
||||
this as unknown as Parameters<typeof setThemeInternal>[0],
|
||||
next,
|
||||
context,
|
||||
);
|
||||
setThemeInternal(this as unknown as Parameters<typeof setThemeInternal>[0], next, context);
|
||||
}
|
||||
|
||||
async loadOverview() {
|
||||
await loadOverviewInternal(
|
||||
this as unknown as Parameters<typeof loadOverviewInternal>[0],
|
||||
);
|
||||
await loadOverviewInternal(this as unknown as Parameters<typeof loadOverviewInternal>[0]);
|
||||
}
|
||||
|
||||
async loadCron() {
|
||||
await loadCronInternal(
|
||||
this as unknown as Parameters<typeof loadCronInternal>[0],
|
||||
);
|
||||
await loadCronInternal(this as unknown as Parameters<typeof loadCronInternal>[0]);
|
||||
}
|
||||
|
||||
async handleAbortChat() {
|
||||
await handleAbortChatInternal(
|
||||
this as unknown as Parameters<typeof handleAbortChatInternal>[0],
|
||||
);
|
||||
await handleAbortChatInternal(this as unknown as Parameters<typeof handleAbortChatInternal>[0]);
|
||||
}
|
||||
|
||||
removeQueuedMessage(id: string) {
|
||||
@@ -454,10 +427,10 @@ export class OpenClawApp extends LitElement {
|
||||
const nextGatewayUrl = this.pendingGatewayUrl;
|
||||
if (!nextGatewayUrl) return;
|
||||
this.pendingGatewayUrl = null;
|
||||
applySettingsInternal(
|
||||
this as unknown as Parameters<typeof applySettingsInternal>[0],
|
||||
{ ...this.settings, gatewayUrl: nextGatewayUrl },
|
||||
);
|
||||
applySettingsInternal(this as unknown as Parameters<typeof applySettingsInternal>[0], {
|
||||
...this.settings,
|
||||
gatewayUrl: nextGatewayUrl,
|
||||
});
|
||||
this.connect();
|
||||
}
|
||||
|
||||
|
||||
@@ -28,13 +28,10 @@ function coerceIdentityValue(value: string | undefined, maxLength: number): stri
|
||||
export function normalizeAssistantIdentity(
|
||||
input?: Partial<AssistantIdentity> | null,
|
||||
): AssistantIdentity {
|
||||
const name =
|
||||
coerceIdentityValue(input?.name, MAX_ASSISTANT_NAME) ?? DEFAULT_ASSISTANT_NAME;
|
||||
const name = coerceIdentityValue(input?.name, MAX_ASSISTANT_NAME) ?? DEFAULT_ASSISTANT_NAME;
|
||||
const avatar = coerceIdentityValue(input?.avatar ?? undefined, MAX_ASSISTANT_AVATAR) ?? null;
|
||||
const agentId =
|
||||
typeof input?.agentId === "string" && input.agentId.trim()
|
||||
? input.agentId.trim()
|
||||
: null;
|
||||
typeof input?.agentId === "string" && input.agentId.trim() ? input.agentId.trim() : null;
|
||||
return { agentId, name, avatar };
|
||||
}
|
||||
|
||||
|
||||
@@ -46,9 +46,7 @@ describe("chat markdown rendering", () => {
|
||||
|
||||
await app.updateComplete;
|
||||
|
||||
const toolCards = Array.from(
|
||||
app.querySelectorAll<HTMLElement>(".chat-tool-card"),
|
||||
);
|
||||
const toolCards = Array.from(app.querySelectorAll<HTMLElement>(".chat-tool-card"));
|
||||
const toolCard = toolCards.find((card) =>
|
||||
card.querySelector(".chat-tool-card__preview, .chat-tool-card__inline"),
|
||||
);
|
||||
|
||||
@@ -38,9 +38,7 @@ function createCopyButton(options: CopyButtonOptions): TemplateResult {
|
||||
aria-label=${idleLabel}
|
||||
@click=${async (e: Event) => {
|
||||
const btn = e.currentTarget as HTMLButtonElement | null;
|
||||
const iconContainer = btn?.querySelector(
|
||||
".chat-copy-btn__icon",
|
||||
) as HTMLElement | null;
|
||||
const iconContainer = btn?.querySelector(".chat-copy-btn__icon") as HTMLElement | null;
|
||||
|
||||
if (!btn || btn.dataset.copying === "1") return;
|
||||
|
||||
|
||||
@@ -35,9 +35,7 @@ function extractImages(message: unknown): ImageBlock[] {
|
||||
const data = source.data as string;
|
||||
const mediaType = (source.media_type as string) || "image/png";
|
||||
// If data is already a data URL, use it directly
|
||||
const url = data.startsWith("data:")
|
||||
? data
|
||||
: `data:${mediaType};base64,${data}`;
|
||||
const url = data.startsWith("data:") ? data : `data:${mediaType};base64,${data}`;
|
||||
images.push({ url });
|
||||
} else if (typeof b.url === "string") {
|
||||
images.push({ url: b.url });
|
||||
@@ -122,11 +120,7 @@ export function renderMessageGroup(
|
||||
? assistantName
|
||||
: normalizedRole;
|
||||
const roleClass =
|
||||
normalizedRole === "user"
|
||||
? "user"
|
||||
: normalizedRole === "assistant"
|
||||
? "assistant"
|
||||
: "other";
|
||||
normalizedRole === "user" ? "user" : normalizedRole === "assistant" ? "assistant" : "other";
|
||||
const timestamp = new Date(group.timestamp).toLocaleTimeString([], {
|
||||
hour: "numeric",
|
||||
minute: "2-digit",
|
||||
@@ -143,8 +137,7 @@ export function renderMessageGroup(
|
||||
renderGroupedMessage(
|
||||
item.message,
|
||||
{
|
||||
isStreaming:
|
||||
group.isStreaming && index === group.messages.length - 1,
|
||||
isStreaming: group.isStreaming && index === group.messages.length - 1,
|
||||
showReasoning: opts.showReasoning,
|
||||
},
|
||||
opts.onOpenSidebar,
|
||||
@@ -159,10 +152,7 @@ export function renderMessageGroup(
|
||||
`;
|
||||
}
|
||||
|
||||
function renderAvatar(
|
||||
role: string,
|
||||
assistant?: Pick<AssistantIdentity, "name" | "avatar">,
|
||||
) {
|
||||
function renderAvatar(role: string, assistant?: Pick<AssistantIdentity, "name" | "avatar">) {
|
||||
const normalized = normalizeRoleForGrouping(role);
|
||||
const assistantName = assistant?.name?.trim() || "Assistant";
|
||||
const assistantAvatar = assistant?.avatar?.trim() || "";
|
||||
@@ -179,7 +169,7 @@ function renderAvatar(
|
||||
? "user"
|
||||
: normalized === "assistant"
|
||||
? "assistant"
|
||||
: normalized === "tool"
|
||||
: normalized === "tool"
|
||||
? "tool"
|
||||
: "other";
|
||||
|
||||
@@ -199,9 +189,7 @@ function renderAvatar(
|
||||
|
||||
function isAvatarUrl(value: string): boolean {
|
||||
return (
|
||||
/^https?:\/\//i.test(value) ||
|
||||
/^data:image\//i.test(value) ||
|
||||
/^\//.test(value) // Relative paths from avatar endpoint
|
||||
/^https?:\/\//i.test(value) || /^data:image\//i.test(value) || /^\//.test(value) // Relative paths from avatar endpoint
|
||||
);
|
||||
}
|
||||
|
||||
@@ -245,13 +233,9 @@ function renderGroupedMessage(
|
||||
|
||||
const extractedText = extractTextCached(message);
|
||||
const extractedThinking =
|
||||
opts.showReasoning && role === "assistant"
|
||||
? extractThinkingCached(message)
|
||||
: null;
|
||||
opts.showReasoning && role === "assistant" ? extractThinkingCached(message) : null;
|
||||
const markdownBase = extractedText?.trim() ? extractedText : null;
|
||||
const reasoningMarkdown = extractedThinking
|
||||
? formatReasoningMarkdown(extractedThinking)
|
||||
: null;
|
||||
const reasoningMarkdown = extractedThinking ? formatReasoningMarkdown(extractedThinking) : null;
|
||||
const markdown = markdownBase;
|
||||
const canCopyMarkdown = role === "assistant" && Boolean(markdown?.trim());
|
||||
|
||||
@@ -265,9 +249,7 @@ function renderGroupedMessage(
|
||||
.join(" ");
|
||||
|
||||
if (!markdown && hasToolCards && isToolResult) {
|
||||
return html`${toolCards.map((card) =>
|
||||
renderToolCardSidebar(card, onOpenSidebar),
|
||||
)}`;
|
||||
return html`${toolCards.map((card) => renderToolCardSidebar(card, onOpenSidebar))}`;
|
||||
}
|
||||
|
||||
if (!markdown && !hasToolCards && !hasImages) return nothing;
|
||||
@@ -276,14 +258,18 @@ function renderGroupedMessage(
|
||||
<div class="${bubbleClasses}">
|
||||
${canCopyMarkdown ? renderCopyAsMarkdownButton(markdown!) : nothing}
|
||||
${renderMessageImages(images)}
|
||||
${reasoningMarkdown
|
||||
? html`<div class="chat-thinking">${unsafeHTML(
|
||||
toSanitizedMarkdownHtml(reasoningMarkdown),
|
||||
)}</div>`
|
||||
: nothing}
|
||||
${markdown
|
||||
? html`<div class="chat-text">${unsafeHTML(toSanitizedMarkdownHtml(markdown))}</div>`
|
||||
: nothing}
|
||||
${
|
||||
reasoningMarkdown
|
||||
? html`<div class="chat-thinking">${unsafeHTML(
|
||||
toSanitizedMarkdownHtml(reasoningMarkdown),
|
||||
)}</div>`
|
||||
: nothing
|
||||
}
|
||||
${
|
||||
markdown
|
||||
? html`<div class="chat-text">${unsafeHTML(toSanitizedMarkdownHtml(markdown))}</div>`
|
||||
: nothing
|
||||
}
|
||||
${toolCards.map((card) => renderToolCardSidebar(card, onOpenSidebar))}
|
||||
</div>
|
||||
`;
|
||||
|
||||
@@ -90,13 +90,9 @@ export function extractThinking(message: unknown): string | null {
|
||||
const rawText = extractRawText(message);
|
||||
if (!rawText) return null;
|
||||
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),
|
||||
];
|
||||
const extracted = matches
|
||||
.map((m) => (m[1] ?? "").trim())
|
||||
.filter(Boolean);
|
||||
const extracted = matches.map((m) => (m[1] ?? "").trim()).filter(Boolean);
|
||||
return extracted.length > 0 ? extracted.join("\n") : null;
|
||||
}
|
||||
|
||||
|
||||
@@ -44,8 +44,18 @@ describe("message-normalizer", () => {
|
||||
|
||||
expect(result.role).toBe("assistant");
|
||||
expect(result.content).toHaveLength(2);
|
||||
expect(result.content[0]).toEqual({ type: "text", text: "Here is the result", name: undefined, args: undefined });
|
||||
expect(result.content[1]).toEqual({ type: "tool_use", text: undefined, name: "bash", args: { command: "ls" } });
|
||||
expect(result.content[0]).toEqual({
|
||||
type: "text",
|
||||
text: "Here is the result",
|
||||
name: undefined,
|
||||
args: undefined,
|
||||
});
|
||||
expect(result.content[1]).toEqual({
|
||||
type: "tool_use",
|
||||
text: undefined,
|
||||
name: "bash",
|
||||
args: { command: "ls" },
|
||||
});
|
||||
});
|
||||
|
||||
it("normalizes message with text field (alternative format)", () => {
|
||||
|
||||
@@ -2,10 +2,7 @@
|
||||
* Message normalization utilities for chat rendering.
|
||||
*/
|
||||
|
||||
import type {
|
||||
NormalizedMessage,
|
||||
MessageContentItem,
|
||||
} from "../types/chat-types";
|
||||
import type { NormalizedMessage, MessageContentItem } from "../types/chat-types";
|
||||
|
||||
/**
|
||||
* Normalize a raw message object into a consistent structure.
|
||||
@@ -16,8 +13,7 @@ export function normalizeMessage(message: unknown): NormalizedMessage {
|
||||
|
||||
// Detect tool messages by common gateway shapes.
|
||||
// Some tool events come through as assistant role with tool_* items in the content array.
|
||||
const hasToolId =
|
||||
typeof m.toolCallId === "string" || typeof m.tool_call_id === "string";
|
||||
const hasToolId = typeof m.toolCallId === "string" || typeof m.tool_call_id === "string";
|
||||
|
||||
const contentRaw = m.content;
|
||||
const contentItems = Array.isArray(contentRaw) ? contentRaw : null;
|
||||
|
||||
@@ -4,10 +4,7 @@ import { formatToolDetail, resolveToolDisplay } from "../tool-display";
|
||||
import { icons } from "../icons";
|
||||
import type { ToolCard } from "../types/chat-types";
|
||||
import { TOOL_INLINE_THRESHOLD } from "./constants";
|
||||
import {
|
||||
formatToolOutputForSidebar,
|
||||
getTruncatedPreview,
|
||||
} from "./tool-helpers";
|
||||
import { formatToolOutputForSidebar, getTruncatedPreview } from "./tool-helpers";
|
||||
import { isToolResultMessage } from "./message-normalizer";
|
||||
import { extractTextCached } from "./message-extract";
|
||||
|
||||
@@ -38,10 +35,7 @@ export function extractToolCards(message: unknown): ToolCard[] {
|
||||
cards.push({ kind: "result", name, text });
|
||||
}
|
||||
|
||||
if (
|
||||
isToolResultMessage(message) &&
|
||||
!cards.some((card) => card.kind === "result")
|
||||
) {
|
||||
if (isToolResultMessage(message) && !cards.some((card) => card.kind === "result")) {
|
||||
const name =
|
||||
(typeof m.toolName === "string" && m.toolName) ||
|
||||
(typeof m.tool_name === "string" && m.tool_name) ||
|
||||
@@ -53,10 +47,7 @@ export function extractToolCards(message: unknown): ToolCard[] {
|
||||
return cards;
|
||||
}
|
||||
|
||||
export function renderToolCardSidebar(
|
||||
card: ToolCard,
|
||||
onOpenSidebar?: (content: string) => void,
|
||||
) {
|
||||
export function renderToolCardSidebar(card: ToolCard, onOpenSidebar?: (content: string) => void) {
|
||||
const display = resolveToolDisplay({ name: card.name, args: card.args });
|
||||
const detail = formatToolDetail(display);
|
||||
const hasText = Boolean(card.text?.trim());
|
||||
@@ -86,36 +77,42 @@ export function renderToolCardSidebar(
|
||||
@click=${handleClick}
|
||||
role=${canClick ? "button" : nothing}
|
||||
tabindex=${canClick ? "0" : nothing}
|
||||
@keydown=${canClick
|
||||
? (e: KeyboardEvent) => {
|
||||
if (e.key !== "Enter" && e.key !== " ") return;
|
||||
e.preventDefault();
|
||||
handleClick?.();
|
||||
}
|
||||
: nothing}
|
||||
@keydown=${
|
||||
canClick
|
||||
? (e: KeyboardEvent) => {
|
||||
if (e.key !== "Enter" && e.key !== " ") return;
|
||||
e.preventDefault();
|
||||
handleClick?.();
|
||||
}
|
||||
: nothing
|
||||
}
|
||||
>
|
||||
<div class="chat-tool-card__header">
|
||||
<div class="chat-tool-card__title">
|
||||
<span class="chat-tool-card__icon">${icons[display.icon]}</span>
|
||||
<span>${display.label}</span>
|
||||
</div>
|
||||
${canClick
|
||||
? html`<span class="chat-tool-card__action">${hasText ? "View" : ""} ${icons.check}</span>`
|
||||
: nothing}
|
||||
${
|
||||
canClick
|
||||
? html`<span class="chat-tool-card__action">${hasText ? "View" : ""} ${icons.check}</span>`
|
||||
: nothing
|
||||
}
|
||||
${isEmpty && !canClick ? html`<span class="chat-tool-card__status">${icons.check}</span>` : nothing}
|
||||
</div>
|
||||
${detail
|
||||
? html`<div class="chat-tool-card__detail">${detail}</div>`
|
||||
: nothing}
|
||||
${isEmpty
|
||||
? html`<div class="chat-tool-card__status-text muted">Completed</div>`
|
||||
: nothing}
|
||||
${showCollapsed
|
||||
? html`<div class="chat-tool-card__preview mono">${getTruncatedPreview(card.text!)}</div>`
|
||||
: nothing}
|
||||
${showInline
|
||||
? html`<div class="chat-tool-card__inline mono">${card.text}</div>`
|
||||
: nothing}
|
||||
${detail ? html`<div class="chat-tool-card__detail">${detail}</div>` : nothing}
|
||||
${
|
||||
isEmpty
|
||||
? html`
|
||||
<div class="chat-tool-card__status-text muted">Completed</div>
|
||||
`
|
||||
: nothing
|
||||
}
|
||||
${
|
||||
showCollapsed
|
||||
? html`<div class="chat-tool-card__preview mono">${getTruncatedPreview(card.text!)}</div>`
|
||||
: nothing
|
||||
}
|
||||
${showInline ? html`<div class="chat-tool-card__inline mono">${card.text}</div>` : nothing}
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
@@ -16,7 +16,7 @@ describe("tool-helpers", () => {
|
||||
});
|
||||
|
||||
it("formats valid JSON array as code block", () => {
|
||||
const input = '[1, 2, 3]';
|
||||
const input = "[1, 2, 3]";
|
||||
const result = formatToolOutputForSidebar(input);
|
||||
|
||||
expect(result).toBe(`\`\`\`json
|
||||
|
||||
@@ -24,7 +24,7 @@ export class ResizableDivider extends LitElement {
|
||||
flex-shrink: 0;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
|
||||
:host::before {
|
||||
content: "";
|
||||
position: absolute;
|
||||
@@ -33,18 +33,20 @@ export class ResizableDivider extends LitElement {
|
||||
right: -4px;
|
||||
bottom: 0;
|
||||
}
|
||||
|
||||
|
||||
:host(:hover) {
|
||||
background: var(--accent, #007bff);
|
||||
}
|
||||
|
||||
|
||||
:host(.dragging) {
|
||||
background: var(--accent, #007bff);
|
||||
}
|
||||
`;
|
||||
|
||||
render() {
|
||||
return html``;
|
||||
return html`
|
||||
|
||||
`;
|
||||
}
|
||||
|
||||
connectedCallback() {
|
||||
@@ -89,7 +91,7 @@ export class ResizableDivider extends LitElement {
|
||||
detail: { splitRatio: newRatio },
|
||||
bubbles: true,
|
||||
composed: true,
|
||||
})
|
||||
}),
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -29,12 +29,7 @@ const rootSchema = {
|
||||
type: "boolean",
|
||||
},
|
||||
bind: {
|
||||
anyOf: [
|
||||
{ const: "auto" },
|
||||
{ const: "lan" },
|
||||
{ const: "tailnet" },
|
||||
{ const: "loopback" },
|
||||
],
|
||||
anyOf: [{ const: "auto" }, { const: "lan" }, { const: "tailnet" }, { const: "loopback" }],
|
||||
},
|
||||
},
|
||||
};
|
||||
@@ -57,17 +52,12 @@ describe("config form renderer", () => {
|
||||
container,
|
||||
);
|
||||
|
||||
const tokenInput = container.querySelector(
|
||||
"input[type='password']",
|
||||
) as HTMLInputElement | null;
|
||||
const tokenInput = container.querySelector("input[type='password']") as HTMLInputElement | null;
|
||||
expect(tokenInput).not.toBeNull();
|
||||
if (!tokenInput) return;
|
||||
tokenInput.value = "abc123";
|
||||
tokenInput.dispatchEvent(new Event("input", { bubbles: true }));
|
||||
expect(onPatch).toHaveBeenCalledWith(
|
||||
["gateway", "auth", "token"],
|
||||
"abc123",
|
||||
);
|
||||
expect(onPatch).toHaveBeenCalledWith(["gateway", "auth", "token"], "abc123");
|
||||
|
||||
const tokenButton = Array.from(
|
||||
container.querySelectorAll<HTMLButtonElement>(".cfg-segmented__btn"),
|
||||
@@ -76,9 +66,7 @@ describe("config form renderer", () => {
|
||||
tokenButton?.dispatchEvent(new MouseEvent("click", { bubbles: true }));
|
||||
expect(onPatch).toHaveBeenCalledWith(["mode"], "token");
|
||||
|
||||
const checkbox = container.querySelector(
|
||||
"input[type='checkbox']",
|
||||
) as HTMLInputElement | null;
|
||||
const checkbox = container.querySelector("input[type='checkbox']") as HTMLInputElement | null;
|
||||
expect(checkbox).not.toBeNull();
|
||||
if (!checkbox) return;
|
||||
checkbox.checked = true;
|
||||
@@ -101,9 +89,7 @@ describe("config form renderer", () => {
|
||||
container,
|
||||
);
|
||||
|
||||
const addButton = container.querySelector(
|
||||
".cfg-array__add",
|
||||
) as HTMLButtonElement | null;
|
||||
const addButton = container.querySelector(".cfg-array__add") as HTMLButtonElement | null;
|
||||
expect(addButton).not.toBeUndefined();
|
||||
addButton?.dispatchEvent(new MouseEvent("click", { bubbles: true }));
|
||||
expect(onPatch).toHaveBeenCalledWith(["allowFrom"], ["+1", ""]);
|
||||
|
||||
@@ -1,8 +1,5 @@
|
||||
import type { GatewayBrowserClient } from "../gateway";
|
||||
import {
|
||||
normalizeAssistantIdentity,
|
||||
type AssistantIdentity,
|
||||
} from "../assistant-identity";
|
||||
import { normalizeAssistantIdentity, type AssistantIdentity } from "../assistant-identity";
|
||||
|
||||
export type AssistantIdentityState = {
|
||||
client: GatewayBrowserClient | null;
|
||||
|
||||
@@ -1,10 +1,6 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
|
||||
import {
|
||||
handleChatEvent,
|
||||
type ChatEventPayload,
|
||||
type ChatState,
|
||||
} from "./chat";
|
||||
import { handleChatEvent, type ChatEventPayload, type ChatState } from "./chat";
|
||||
|
||||
function createState(overrides: Partial<ChatState> = {}): ChatState {
|
||||
return {
|
||||
|
||||
@@ -144,9 +144,7 @@ export async function abortChatRun(state: ChatState): Promise<boolean> {
|
||||
try {
|
||||
await state.client.request(
|
||||
"chat.abort",
|
||||
runId
|
||||
? { sessionKey: state.sessionKey, runId }
|
||||
: { sessionKey: state.sessionKey },
|
||||
runId ? { sessionKey: state.sessionKey, runId } : { sessionKey: state.sessionKey },
|
||||
);
|
||||
return true;
|
||||
} catch (err) {
|
||||
@@ -155,20 +153,13 @@ export async function abortChatRun(state: ChatState): Promise<boolean> {
|
||||
}
|
||||
}
|
||||
|
||||
export function handleChatEvent(
|
||||
state: ChatState,
|
||||
payload?: ChatEventPayload,
|
||||
) {
|
||||
export function handleChatEvent(state: ChatState, payload?: ChatEventPayload) {
|
||||
if (!payload) return null;
|
||||
if (payload.sessionKey !== state.sessionKey) return null;
|
||||
|
||||
// Final from another run (e.g. sub-agent announce): refresh history to show new message.
|
||||
// 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";
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -46,11 +46,11 @@ describe("applyConfigSnapshot", () => {
|
||||
config: { gateway: { mode: "remote", port: 9999 } },
|
||||
valid: true,
|
||||
issues: [],
|
||||
raw: "{\n \"gateway\": { \"mode\": \"remote\", \"port\": 9999 }\n}\n",
|
||||
raw: '{\n "gateway": { "mode": "remote", "port": 9999 }\n}\n',
|
||||
});
|
||||
|
||||
expect(state.configRaw).toBe(
|
||||
"{\n \"gateway\": {\n \"mode\": \"local\",\n \"port\": 18789\n }\n}\n",
|
||||
'{\n "gateway": {\n "mode": "local",\n "port": 18789\n }\n}\n',
|
||||
);
|
||||
});
|
||||
|
||||
@@ -129,7 +129,7 @@ describe("updateConfigFormValue", () => {
|
||||
updateConfigFormValue(state, ["gateway", "port"], 18789);
|
||||
|
||||
expect(state.configRaw).toBe(
|
||||
"{\n \"gateway\": {\n \"mode\": \"local\",\n \"port\": 18789\n }\n}\n",
|
||||
'{\n "gateway": {\n "mode": "local",\n "port": 18789\n }\n}\n',
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -142,7 +142,7 @@ describe("applyConfig", () => {
|
||||
state.client = { request } as unknown as ConfigState["client"];
|
||||
state.applySessionKey = "agent:main:whatsapp:dm:+15555550123";
|
||||
state.configFormMode = "raw";
|
||||
state.configRaw = "{\n agent: { workspace: \"~/openclaw\" }\n}\n";
|
||||
state.configRaw = '{\n agent: { workspace: "~/openclaw" }\n}\n';
|
||||
state.configSnapshot = {
|
||||
hash: "hash-123",
|
||||
};
|
||||
@@ -150,7 +150,7 @@ describe("applyConfig", () => {
|
||||
await applyConfig(state);
|
||||
|
||||
expect(request).toHaveBeenCalledWith("config.apply", {
|
||||
raw: "{\n agent: { workspace: \"~/openclaw\" }\n}\n",
|
||||
raw: '{\n agent: { workspace: "~/openclaw" }\n}\n',
|
||||
baseHash: "hash-123",
|
||||
sessionKey: "agent:main:whatsapp:dm:+15555550123",
|
||||
});
|
||||
|
||||
@@ -1,9 +1,5 @@
|
||||
import type { GatewayBrowserClient } from "../gateway";
|
||||
import type {
|
||||
ConfigSchemaResponse,
|
||||
ConfigSnapshot,
|
||||
ConfigUiHints,
|
||||
} from "../types";
|
||||
import type { ConfigSchemaResponse, ConfigSnapshot, ConfigUiHints } from "../types";
|
||||
import {
|
||||
cloneConfigObject,
|
||||
removePathValue,
|
||||
@@ -57,10 +53,7 @@ export async function loadConfigSchema(state: ConfigState) {
|
||||
if (state.configSchemaLoading) return;
|
||||
state.configSchemaLoading = true;
|
||||
try {
|
||||
const res = (await state.client.request(
|
||||
"config.schema",
|
||||
{},
|
||||
)) as ConfigSchemaResponse;
|
||||
const res = (await state.client.request("config.schema", {})) as ConfigSchemaResponse;
|
||||
applyConfigSchema(state, res);
|
||||
} catch (err) {
|
||||
state.lastError = String(err);
|
||||
@@ -69,10 +62,7 @@ export async function loadConfigSchema(state: ConfigState) {
|
||||
}
|
||||
}
|
||||
|
||||
export function applyConfigSchema(
|
||||
state: ConfigState,
|
||||
res: ConfigSchemaResponse,
|
||||
) {
|
||||
export function applyConfigSchema(state: ConfigState, res: ConfigSchemaResponse) {
|
||||
state.configSchema = res.schema ?? null;
|
||||
state.configUiHints = res.uiHints ?? {};
|
||||
state.configSchemaVersion = res.version ?? null;
|
||||
@@ -175,9 +165,7 @@ export function updateConfigFormValue(
|
||||
path: Array<string | number>,
|
||||
value: unknown,
|
||||
) {
|
||||
const base = cloneConfigObject(
|
||||
state.configForm ?? state.configSnapshot?.config ?? {},
|
||||
);
|
||||
const base = cloneConfigObject(state.configForm ?? state.configSnapshot?.config ?? {});
|
||||
setPathValue(base, path, value);
|
||||
state.configForm = base;
|
||||
state.configFormDirty = true;
|
||||
@@ -186,13 +174,8 @@ export function updateConfigFormValue(
|
||||
}
|
||||
}
|
||||
|
||||
export function removeConfigFormValue(
|
||||
state: ConfigState,
|
||||
path: Array<string | number>,
|
||||
) {
|
||||
const base = cloneConfigObject(
|
||||
state.configForm ?? state.configSnapshot?.config ?? {},
|
||||
);
|
||||
export function removeConfigFormValue(state: ConfigState, path: Array<string | number>) {
|
||||
const base = cloneConfigObject(state.configForm ?? state.configSnapshot?.config ?? {});
|
||||
removePathValue(base, path);
|
||||
state.configForm = base;
|
||||
state.configFormDirty = true;
|
||||
|
||||
@@ -22,16 +22,14 @@ export function setPathValue(
|
||||
if (typeof key === "number") {
|
||||
if (!Array.isArray(current)) return;
|
||||
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[];
|
||||
} else {
|
||||
if (typeof current !== "object" || current == null) return;
|
||||
const record = current as Record<string, unknown>;
|
||||
if (record[key] == null) {
|
||||
record[key] =
|
||||
typeof nextKey === "number" ? [] : ({} as Record<string, unknown>);
|
||||
record[key] = typeof nextKey === "number" ? [] : ({} as Record<string, unknown>);
|
||||
}
|
||||
current = record[key] as Record<string, unknown> | unknown[];
|
||||
}
|
||||
@@ -59,9 +57,7 @@ export function removePathValue(
|
||||
current = current[key] as Record<string, unknown> | unknown[];
|
||||
} else {
|
||||
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;
|
||||
}
|
||||
|
||||
@@ -103,8 +103,7 @@ export async function addCronJob(state: CronState) {
|
||||
wakeMode: state.cronForm.wakeMode,
|
||||
payload,
|
||||
isolation:
|
||||
state.cronForm.postToMainPrefix.trim() &&
|
||||
state.cronForm.sessionTarget === "isolated"
|
||||
state.cronForm.postToMainPrefix.trim() && state.cronForm.sessionTarget === "isolated"
|
||||
? { postToMainPrefix: state.cronForm.postToMainPrefix.trim() }
|
||||
: undefined,
|
||||
};
|
||||
@@ -125,11 +124,7 @@ export async function addCronJob(state: CronState) {
|
||||
}
|
||||
}
|
||||
|
||||
export async function toggleCronJob(
|
||||
state: CronState,
|
||||
job: CronJob,
|
||||
enabled: boolean,
|
||||
) {
|
||||
export async function toggleCronJob(state: CronState, job: CronJob, enabled: boolean) {
|
||||
if (!state.client || !state.connected || state.cronBusy) return;
|
||||
state.cronBusy = true;
|
||||
state.cronError = null;
|
||||
|
||||
@@ -29,9 +29,7 @@ export async function loadDebug(state: DebugState) {
|
||||
state.debugStatus = status as StatusSummary;
|
||||
state.debugHealth = health as HealthSnapshot;
|
||||
const modelPayload = models as { models?: unknown[] } | undefined;
|
||||
state.debugModels = Array.isArray(modelPayload?.models)
|
||||
? modelPayload?.models
|
||||
: [];
|
||||
state.debugModels = Array.isArray(modelPayload?.models) ? modelPayload?.models : [];
|
||||
state.debugHeartbeat = heartbeat as unknown;
|
||||
} catch (err) {
|
||||
state.debugCallError = String(err);
|
||||
|
||||
@@ -118,9 +118,7 @@ export async function revokeDeviceToken(
|
||||
params: { deviceId: string; role: string },
|
||||
) {
|
||||
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;
|
||||
try {
|
||||
await state.client.request("device.token.revoke", params);
|
||||
|
||||
@@ -80,6 +80,9 @@ export function addExecApproval(
|
||||
return next;
|
||||
}
|
||||
|
||||
export function removeExecApproval(queue: ExecApprovalRequest[], id: string): ExecApprovalRequest[] {
|
||||
export function removeExecApproval(
|
||||
queue: ExecApprovalRequest[],
|
||||
id: string,
|
||||
): ExecApprovalRequest[] {
|
||||
return pruneExecApprovalQueue(queue).filter((entry) => entry.id !== id);
|
||||
}
|
||||
|
||||
@@ -34,9 +34,7 @@ export type ExecApprovalsSnapshot = {
|
||||
file: ExecApprovalsFile;
|
||||
};
|
||||
|
||||
export type ExecApprovalsTarget =
|
||||
| { kind: "gateway" }
|
||||
| { kind: "node"; nodeId: string };
|
||||
export type ExecApprovalsTarget = { kind: "gateway" } | { kind: "node"; nodeId: string };
|
||||
|
||||
export type ExecApprovalsState = {
|
||||
client: GatewayBrowserClient | null;
|
||||
@@ -120,10 +118,7 @@ export async function saveExecApprovals(
|
||||
state.lastError = "Exec approvals hash missing; reload and retry.";
|
||||
return;
|
||||
}
|
||||
const file =
|
||||
state.execApprovalsForm ??
|
||||
state.execApprovalsSnapshot?.file ??
|
||||
{};
|
||||
const file = state.execApprovalsForm ?? state.execApprovalsSnapshot?.file ?? {};
|
||||
const rpc = resolveExecApprovalsSaveRpc(target, { file, baseHash });
|
||||
if (!rpc) {
|
||||
state.lastError = "Select a node before saving exec approvals.";
|
||||
|
||||
@@ -16,14 +16,7 @@ export type LogsState = {
|
||||
};
|
||||
|
||||
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) {
|
||||
if (typeof value !== "string") return null;
|
||||
@@ -53,11 +46,7 @@ export function parseLogLine(line: string): LogEntry {
|
||||
? (obj._meta as Record<string, unknown>)
|
||||
: null;
|
||||
const time =
|
||||
typeof obj.time === "string"
|
||||
? obj.time
|
||||
: typeof meta?.date === "string"
|
||||
? meta?.date
|
||||
: null;
|
||||
typeof obj.time === "string" ? obj.time : typeof meta?.date === "string" ? meta?.date : null;
|
||||
const level = normalizeLevel(meta?.logLevelName ?? meta?.level);
|
||||
|
||||
const contextCandidate =
|
||||
@@ -94,17 +83,14 @@ 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.logsLoading && !opts?.quiet) return;
|
||||
if (!opts?.quiet) state.logsLoading = true;
|
||||
state.logsError = null;
|
||||
try {
|
||||
const res = await state.client.request("logs.tail", {
|
||||
cursor: opts?.reset ? undefined : state.logsCursor ?? undefined,
|
||||
cursor: opts?.reset ? undefined : (state.logsCursor ?? undefined),
|
||||
limit: state.logsLimit,
|
||||
maxBytes: state.logsMaxBytes,
|
||||
});
|
||||
|
||||
@@ -8,10 +8,7 @@ export type NodesState = {
|
||||
lastError: string | null;
|
||||
};
|
||||
|
||||
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.nodesLoading) return;
|
||||
state.nodesLoading = true;
|
||||
|
||||
@@ -17,9 +17,7 @@ export async function loadPresence(state: PresenceState) {
|
||||
state.presenceError = null;
|
||||
state.presenceStatus = null;
|
||||
try {
|
||||
const res = (await state.client.request("system-presence", {})) as
|
||||
| PresenceEntry[]
|
||||
| undefined;
|
||||
const res = (await state.client.request("system-presence", {})) as PresenceEntry[] | undefined;
|
||||
if (Array.isArray(res)) {
|
||||
state.presenceEntries = res;
|
||||
state.presenceStatus = res.length === 0 ? "No instances yet." : null;
|
||||
|
||||
@@ -30,8 +30,7 @@ export async function loadSessions(
|
||||
try {
|
||||
const includeGlobal = overrides?.includeGlobal ?? state.sessionsIncludeGlobal;
|
||||
const includeUnknown = overrides?.includeUnknown ?? state.sessionsIncludeUnknown;
|
||||
const activeMinutes =
|
||||
overrides?.activeMinutes ?? toNumber(state.sessionsFilterActive, 0);
|
||||
const activeMinutes = overrides?.activeMinutes ?? toNumber(state.sessionsFilterActive, 0);
|
||||
const limit = overrides?.limit ?? toNumber(state.sessionsFilterLimit, 0);
|
||||
const params: Record<string, unknown> = {
|
||||
includeGlobal,
|
||||
|
||||
@@ -45,9 +45,7 @@ export async function loadSkills(state: SkillsState, options?: LoadSkillsOptions
|
||||
state.skillsLoading = true;
|
||||
state.skillsError = null;
|
||||
try {
|
||||
const res = (await state.client.request("skills.status", {})) as
|
||||
| SkillStatusReport
|
||||
| undefined;
|
||||
const res = (await state.client.request("skills.status", {})) as SkillStatusReport | undefined;
|
||||
if (res) state.skillsReport = res;
|
||||
} catch (err) {
|
||||
state.skillsError = getErrorMessage(err);
|
||||
@@ -56,19 +54,11 @@ export async function loadSkills(state: SkillsState, options?: LoadSkillsOptions
|
||||
}
|
||||
}
|
||||
|
||||
export function updateSkillEdit(
|
||||
state: SkillsState,
|
||||
skillKey: string,
|
||||
value: string,
|
||||
) {
|
||||
export function updateSkillEdit(state: SkillsState, skillKey: string, value: string) {
|
||||
state.skillEdits = { ...state.skillEdits, [skillKey]: value };
|
||||
}
|
||||
|
||||
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;
|
||||
state.skillsBusyKey = skillKey;
|
||||
state.skillsError = null;
|
||||
|
||||
@@ -36,9 +36,7 @@ describe("chat focus mode", () => {
|
||||
expect(shell).not.toBeNull();
|
||||
expect(shell?.classList.contains("shell--chat-focus")).toBe(false);
|
||||
|
||||
const toggle = app.querySelector<HTMLButtonElement>(
|
||||
'button[title^="Toggle focus mode"]',
|
||||
);
|
||||
const toggle = app.querySelector<HTMLButtonElement>('button[title^="Toggle focus mode"]');
|
||||
expect(toggle).not.toBeNull();
|
||||
toggle?.click();
|
||||
|
||||
@@ -47,9 +45,7 @@ describe("chat focus mode", () => {
|
||||
|
||||
const link = app.querySelector<HTMLAnchorElement>('a.nav-item[href="/channels"]');
|
||||
expect(link).not.toBeNull();
|
||||
link?.dispatchEvent(
|
||||
new MouseEvent("click", { bubbles: true, cancelable: true, button: 0 }),
|
||||
);
|
||||
link?.dispatchEvent(new MouseEvent("click", { bubbles: true, cancelable: true, button: 0 }));
|
||||
|
||||
await app.updateComplete;
|
||||
expect(app.tab).toBe("channels");
|
||||
|
||||
+4
-1
@@ -42,7 +42,10 @@ export function clampText(value: string, max = 120): string {
|
||||
return `${value.slice(0, Math.max(0, max - 1))}…`;
|
||||
}
|
||||
|
||||
export function truncateText(value: string, max: number): {
|
||||
export function truncateText(
|
||||
value: string,
|
||||
max: number,
|
||||
): {
|
||||
text: string;
|
||||
truncated: boolean;
|
||||
total: number;
|
||||
|
||||
+217
-31
@@ -5,40 +5,223 @@ import { html, type TemplateResult } from "lit";
|
||||
|
||||
export const icons = {
|
||||
// Navigation icons
|
||||
messageSquare: html`<svg viewBox="0 0 24 24"><path d="M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z"/></svg>`,
|
||||
barChart: html`<svg viewBox="0 0 24 24"><line x1="12" x2="12" y1="20" y2="10"/><line x1="18" x2="18" y1="20" y2="4"/><line x1="6" x2="6" y1="20" y2="16"/></svg>`,
|
||||
link: html`<svg viewBox="0 0 24 24"><path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71"/><path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71"/></svg>`,
|
||||
radio: html`<svg viewBox="0 0 24 24"><circle cx="12" cy="12" r="2"/><path d="M16.24 7.76a6 6 0 0 1 0 8.49m-8.48-.01a6 6 0 0 1 0-8.49m11.31-2.82a10 10 0 0 1 0 14.14m-14.14 0a10 10 0 0 1 0-14.14"/></svg>`,
|
||||
fileText: html`<svg viewBox="0 0 24 24"><path d="M14.5 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V7.5L14.5 2z"/><polyline points="14 2 14 8 20 8"/><line x1="16" x2="8" y1="13" y2="13"/><line x1="16" x2="8" y1="17" y2="17"/><line x1="10" x2="8" y1="9" y2="9"/></svg>`,
|
||||
zap: html`<svg viewBox="0 0 24 24"><polygon points="13 2 3 14 12 14 11 22 21 10 12 10 13 2"/></svg>`,
|
||||
monitor: html`<svg viewBox="0 0 24 24"><rect width="20" height="14" x="2" y="3" rx="2"/><line x1="8" x2="16" y1="21" y2="21"/><line x1="12" x2="12" y1="17" y2="21"/></svg>`,
|
||||
settings: html`<svg viewBox="0 0 24 24"><path d="M12.22 2h-.44a2 2 0 0 0-2 2v.18a2 2 0 0 1-1 1.73l-.43.25a2 2 0 0 1-2 0l-.15-.08a2 2 0 0 0-2.73.73l-.22.38a2 2 0 0 0 .73 2.73l.15.1a2 2 0 0 1 1 1.72v.51a2 2 0 0 1-1 1.74l-.15.09a2 2 0 0 0-.73 2.73l.22.38a2 2 0 0 0 2.73.73l.15-.08a2 2 0 0 1 2 0l.43.25a2 2 0 0 1 1 1.73V20a2 2 0 0 0 2 2h.44a2 2 0 0 0 2-2v-.18a2 2 0 0 1 1-1.73l.43-.25a2 2 0 0 1 2 0l.15.08a2 2 0 0 0 2.73-.73l.22-.39a2 2 0 0 0-.73-2.73l-.15-.08a2 2 0 0 1-1-1.74v-.5a2 2 0 0 1 1-1.74l.15-.09a2 2 0 0 0 .73-2.73l-.22-.38a2 2 0 0 0-2.73-.73l-.15.08a2 2 0 0 1-2 0l-.43-.25a2 2 0 0 1-1-1.73V4a2 2 0 0 0-2-2z"/><circle cx="12" cy="12" r="3"/></svg>`,
|
||||
bug: html`<svg viewBox="0 0 24 24"><path d="m8 2 1.88 1.88"/><path d="M14.12 3.88 16 2"/><path d="M9 7.13v-1a3.003 3.003 0 1 1 6 0v1"/><path d="M12 20c-3.3 0-6-2.7-6-6v-3a4 4 0 0 1 4-4h4a4 4 0 0 1 4 4v3c0 3.3-2.7 6-6 6"/><path d="M12 20v-9"/><path d="M6.53 9C4.6 8.8 3 7.1 3 5"/><path d="M6 13H2"/><path d="M3 21c0-2.1 1.7-3.9 3.8-4"/><path d="M20.97 5c0 2.1-1.6 3.8-3.5 4"/><path d="M22 13h-4"/><path d="M17.2 17c2.1.1 3.8 1.9 3.8 4"/></svg>`,
|
||||
scrollText: html`<svg viewBox="0 0 24 24"><path d="M8 21h12a2 2 0 0 0 2-2v-2H10v2a2 2 0 1 1-4 0V5a2 2 0 1 0-4 0v3h4"/><path d="M19 17V5a2 2 0 0 0-2-2H4"/><path d="M15 8h-5"/><path d="M15 12h-5"/></svg>`,
|
||||
folder: html`<svg viewBox="0 0 24 24"><path d="M20 20a2 2 0 0 0 2-2V8a2 2 0 0 0-2-2h-7.9a2 2 0 0 1-1.69-.9L9.6 3.9A2 2 0 0 0 7.93 3H4a2 2 0 0 0-2 2v13a2 2 0 0 0 2 2Z"/></svg>`,
|
||||
messageSquare: html`
|
||||
<svg viewBox="0 0 24 24">
|
||||
<path d="M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z" />
|
||||
</svg>
|
||||
`,
|
||||
barChart: html`
|
||||
<svg viewBox="0 0 24 24">
|
||||
<line x1="12" x2="12" y1="20" y2="10" />
|
||||
<line x1="18" x2="18" y1="20" y2="4" />
|
||||
<line x1="6" x2="6" y1="20" y2="16" />
|
||||
</svg>
|
||||
`,
|
||||
link: html`
|
||||
<svg viewBox="0 0 24 24">
|
||||
<path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71" />
|
||||
<path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71" />
|
||||
</svg>
|
||||
`,
|
||||
radio: html`
|
||||
<svg viewBox="0 0 24 24">
|
||||
<circle cx="12" cy="12" r="2" />
|
||||
<path
|
||||
d="M16.24 7.76a6 6 0 0 1 0 8.49m-8.48-.01a6 6 0 0 1 0-8.49m11.31-2.82a10 10 0 0 1 0 14.14m-14.14 0a10 10 0 0 1 0-14.14"
|
||||
/>
|
||||
</svg>
|
||||
`,
|
||||
fileText: html`
|
||||
<svg viewBox="0 0 24 24">
|
||||
<path d="M14.5 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V7.5L14.5 2z" />
|
||||
<polyline points="14 2 14 8 20 8" />
|
||||
<line x1="16" x2="8" y1="13" y2="13" />
|
||||
<line x1="16" x2="8" y1="17" y2="17" />
|
||||
<line x1="10" x2="8" y1="9" y2="9" />
|
||||
</svg>
|
||||
`,
|
||||
zap: html`
|
||||
<svg viewBox="0 0 24 24"><polygon points="13 2 3 14 12 14 11 22 21 10 12 10 13 2" /></svg>
|
||||
`,
|
||||
monitor: html`
|
||||
<svg viewBox="0 0 24 24">
|
||||
<rect width="20" height="14" x="2" y="3" rx="2" />
|
||||
<line x1="8" x2="16" y1="21" y2="21" />
|
||||
<line x1="12" x2="12" y1="17" y2="21" />
|
||||
</svg>
|
||||
`,
|
||||
settings: html`
|
||||
<svg viewBox="0 0 24 24">
|
||||
<path
|
||||
d="M12.22 2h-.44a2 2 0 0 0-2 2v.18a2 2 0 0 1-1 1.73l-.43.25a2 2 0 0 1-2 0l-.15-.08a2 2 0 0 0-2.73.73l-.22.38a2 2 0 0 0 .73 2.73l.15.1a2 2 0 0 1 1 1.72v.51a2 2 0 0 1-1 1.74l-.15.09a2 2 0 0 0-.73 2.73l.22.38a2 2 0 0 0 2.73.73l.15-.08a2 2 0 0 1 2 0l.43.25a2 2 0 0 1 1 1.73V20a2 2 0 0 0 2 2h.44a2 2 0 0 0 2-2v-.18a2 2 0 0 1 1-1.73l.43-.25a2 2 0 0 1 2 0l.15.08a2 2 0 0 0 2.73-.73l.22-.39a2 2 0 0 0-.73-2.73l-.15-.08a2 2 0 0 1-1-1.74v-.5a2 2 0 0 1 1-1.74l.15-.09a2 2 0 0 0 .73-2.73l-.22-.38a2 2 0 0 0-2.73-.73l-.15.08a2 2 0 0 1-2 0l-.43-.25a2 2 0 0 1-1-1.73V4a2 2 0 0 0-2-2z"
|
||||
/>
|
||||
<circle cx="12" cy="12" r="3" />
|
||||
</svg>
|
||||
`,
|
||||
bug: html`
|
||||
<svg viewBox="0 0 24 24">
|
||||
<path d="m8 2 1.88 1.88" />
|
||||
<path d="M14.12 3.88 16 2" />
|
||||
<path d="M9 7.13v-1a3.003 3.003 0 1 1 6 0v1" />
|
||||
<path d="M12 20c-3.3 0-6-2.7-6-6v-3a4 4 0 0 1 4-4h4a4 4 0 0 1 4 4v3c0 3.3-2.7 6-6 6" />
|
||||
<path d="M12 20v-9" />
|
||||
<path d="M6.53 9C4.6 8.8 3 7.1 3 5" />
|
||||
<path d="M6 13H2" />
|
||||
<path d="M3 21c0-2.1 1.7-3.9 3.8-4" />
|
||||
<path d="M20.97 5c0 2.1-1.6 3.8-3.5 4" />
|
||||
<path d="M22 13h-4" />
|
||||
<path d="M17.2 17c2.1.1 3.8 1.9 3.8 4" />
|
||||
</svg>
|
||||
`,
|
||||
scrollText: html`
|
||||
<svg viewBox="0 0 24 24">
|
||||
<path d="M8 21h12a2 2 0 0 0 2-2v-2H10v2a2 2 0 1 1-4 0V5a2 2 0 1 0-4 0v3h4" />
|
||||
<path d="M19 17V5a2 2 0 0 0-2-2H4" />
|
||||
<path d="M15 8h-5" />
|
||||
<path d="M15 12h-5" />
|
||||
</svg>
|
||||
`,
|
||||
folder: html`
|
||||
<svg viewBox="0 0 24 24">
|
||||
<path
|
||||
d="M20 20a2 2 0 0 0 2-2V8a2 2 0 0 0-2-2h-7.9a2 2 0 0 1-1.69-.9L9.6 3.9A2 2 0 0 0 7.93 3H4a2 2 0 0 0-2 2v13a2 2 0 0 0 2 2Z"
|
||||
/>
|
||||
</svg>
|
||||
`,
|
||||
|
||||
// UI icons
|
||||
menu: html`<svg viewBox="0 0 24 24"><line x1="4" x2="20" y1="12" y2="12"/><line x1="4" x2="20" y1="6" y2="6"/><line x1="4" x2="20" y1="18" y2="18"/></svg>`,
|
||||
x: html`<svg viewBox="0 0 24 24"><path d="M18 6 6 18"/><path d="m6 6 12 12"/></svg>`,
|
||||
check: html`<svg viewBox="0 0 24 24"><path d="M20 6 9 17l-5-5"/></svg>`,
|
||||
copy: html`<svg viewBox="0 0 24 24"><rect width="14" height="14" x="8" y="8" rx="2" ry="2"/><path d="M4 16c-1.1 0-2-.9-2-2V4c0-1.1.9-2 2-2h10c1.1 0 2 .9 2 2"/></svg>`,
|
||||
search: html`<svg viewBox="0 0 24 24"><circle cx="11" cy="11" r="8"/><path d="m21 21-4.3-4.3"/></svg>`,
|
||||
brain: html`<svg viewBox="0 0 24 24"><path d="M12 5a3 3 0 1 0-5.997.125 4 4 0 0 0-2.526 5.77 4 4 0 0 0 .556 6.588A4 4 0 1 0 12 18Z"/><path d="M12 5a3 3 0 1 1 5.997.125 4 4 0 0 1 2.526 5.77 4 4 0 0 1-.556 6.588A4 4 0 1 1 12 18Z"/><path d="M15 13a4.5 4.5 0 0 1-3-4 4.5 4.5 0 0 1-3 4"/><path d="M17.599 6.5a3 3 0 0 0 .399-1.375"/><path d="M6.003 5.125A3 3 0 0 0 6.401 6.5"/><path d="M3.477 10.896a4 4 0 0 1 .585-.396"/><path d="M19.938 10.5a4 4 0 0 1 .585.396"/><path d="M6 18a4 4 0 0 1-1.967-.516"/><path d="M19.967 17.484A4 4 0 0 1 18 18"/></svg>`,
|
||||
book: html`<svg viewBox="0 0 24 24"><path d="M4 19.5v-15A2.5 2.5 0 0 1 6.5 2H20v20H6.5a2.5 2.5 0 0 1 0-5H20"/></svg>`,
|
||||
loader: html`<svg viewBox="0 0 24 24"><path d="M12 2v4"/><path d="m16.2 7.8 2.9-2.9"/><path d="M18 12h4"/><path d="m16.2 16.2 2.9 2.9"/><path d="M12 18v4"/><path d="m4.9 19.1 2.9-2.9"/><path d="M2 12h4"/><path d="m4.9 4.9 2.9 2.9"/></svg>`,
|
||||
menu: html`
|
||||
<svg viewBox="0 0 24 24">
|
||||
<line x1="4" x2="20" y1="12" y2="12" />
|
||||
<line x1="4" x2="20" y1="6" y2="6" />
|
||||
<line x1="4" x2="20" y1="18" y2="18" />
|
||||
</svg>
|
||||
`,
|
||||
x: html`
|
||||
<svg viewBox="0 0 24 24">
|
||||
<path d="M18 6 6 18" />
|
||||
<path d="m6 6 12 12" />
|
||||
</svg>
|
||||
`,
|
||||
check: html`
|
||||
<svg viewBox="0 0 24 24"><path d="M20 6 9 17l-5-5" /></svg>
|
||||
`,
|
||||
copy: html`
|
||||
<svg viewBox="0 0 24 24">
|
||||
<rect width="14" height="14" x="8" y="8" rx="2" ry="2" />
|
||||
<path d="M4 16c-1.1 0-2-.9-2-2V4c0-1.1.9-2 2-2h10c1.1 0 2 .9 2 2" />
|
||||
</svg>
|
||||
`,
|
||||
search: html`
|
||||
<svg viewBox="0 0 24 24">
|
||||
<circle cx="11" cy="11" r="8" />
|
||||
<path d="m21 21-4.3-4.3" />
|
||||
</svg>
|
||||
`,
|
||||
brain: html`
|
||||
<svg viewBox="0 0 24 24">
|
||||
<path d="M12 5a3 3 0 1 0-5.997.125 4 4 0 0 0-2.526 5.77 4 4 0 0 0 .556 6.588A4 4 0 1 0 12 18Z" />
|
||||
<path d="M12 5a3 3 0 1 1 5.997.125 4 4 0 0 1 2.526 5.77 4 4 0 0 1-.556 6.588A4 4 0 1 1 12 18Z" />
|
||||
<path d="M15 13a4.5 4.5 0 0 1-3-4 4.5 4.5 0 0 1-3 4" />
|
||||
<path d="M17.599 6.5a3 3 0 0 0 .399-1.375" />
|
||||
<path d="M6.003 5.125A3 3 0 0 0 6.401 6.5" />
|
||||
<path d="M3.477 10.896a4 4 0 0 1 .585-.396" />
|
||||
<path d="M19.938 10.5a4 4 0 0 1 .585.396" />
|
||||
<path d="M6 18a4 4 0 0 1-1.967-.516" />
|
||||
<path d="M19.967 17.484A4 4 0 0 1 18 18" />
|
||||
</svg>
|
||||
`,
|
||||
book: html`
|
||||
<svg viewBox="0 0 24 24">
|
||||
<path d="M4 19.5v-15A2.5 2.5 0 0 1 6.5 2H20v20H6.5a2.5 2.5 0 0 1 0-5H20" />
|
||||
</svg>
|
||||
`,
|
||||
loader: html`
|
||||
<svg viewBox="0 0 24 24">
|
||||
<path d="M12 2v4" />
|
||||
<path d="m16.2 7.8 2.9-2.9" />
|
||||
<path d="M18 12h4" />
|
||||
<path d="m16.2 16.2 2.9 2.9" />
|
||||
<path d="M12 18v4" />
|
||||
<path d="m4.9 19.1 2.9-2.9" />
|
||||
<path d="M2 12h4" />
|
||||
<path d="m4.9 4.9 2.9 2.9" />
|
||||
</svg>
|
||||
`,
|
||||
|
||||
// Tool icons
|
||||
wrench: html`<svg viewBox="0 0 24 24"><path d="M14.7 6.3a1 1 0 0 0 0 1.4l1.6 1.6a1 1 0 0 0 1.4 0l3.77-3.77a6 6 0 0 1-7.94 7.94l-6.91 6.91a2.12 2.12 0 0 1-3-3l6.91-6.91a6 6 0 0 1 7.94-7.94l-3.76 3.76z"/></svg>`,
|
||||
fileCode: html`<svg viewBox="0 0 24 24"><path d="M14.5 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V7.5L14.5 2z"/><polyline points="14 2 14 8 20 8"/><path d="m10 13-2 2 2 2"/><path d="m14 17 2-2-2-2"/></svg>`,
|
||||
edit: html`<svg viewBox="0 0 24 24"><path d="M11 4H4a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2v-7"/><path d="M18.5 2.5a2.121 2.121 0 0 1 3 3L12 15l-4 1 1-4 9.5-9.5z"/></svg>`,
|
||||
penLine: html`<svg viewBox="0 0 24 24"><path d="M12 20h9"/><path d="M16.5 3.5a2.121 2.121 0 0 1 3 3L7 19l-4 1 1-4L16.5 3.5z"/></svg>`,
|
||||
paperclip: html`<svg viewBox="0 0 24 24"><path d="m21.44 11.05-9.19 9.19a6 6 0 0 1-8.49-8.49l8.57-8.57A4 4 0 1 1 18 8.84l-8.59 8.57a2 2 0 0 1-2.83-2.83l8.49-8.48"/></svg>`,
|
||||
globe: html`<svg viewBox="0 0 24 24"><circle cx="12" cy="12" r="10"/><path d="M12 2a14.5 14.5 0 0 0 0 20 14.5 14.5 0 0 0 0-20"/><path d="M2 12h20"/></svg>`,
|
||||
image: html`<svg viewBox="0 0 24 24"><rect width="18" height="18" x="3" y="3" rx="2" ry="2"/><circle cx="9" cy="9" r="2"/><path d="m21 15-3.086-3.086a2 2 0 0 0-2.828 0L6 21"/></svg>`,
|
||||
smartphone: html`<svg viewBox="0 0 24 24"><rect width="14" height="20" x="5" y="2" rx="2" ry="2"/><path d="M12 18h.01"/></svg>`,
|
||||
plug: html`<svg viewBox="0 0 24 24"><path d="M12 22v-5"/><path d="M9 8V2"/><path d="M15 8V2"/><path d="M18 8v5a4 4 0 0 1-4 4h-4a4 4 0 0 1-4-4V8Z"/></svg>`,
|
||||
circle: html`<svg viewBox="0 0 24 24"><circle cx="12" cy="12" r="10"/></svg>`,
|
||||
puzzle: html`<svg viewBox="0 0 24 24"><path d="M19.439 7.85c-.049.322.059.648.289.878l1.568 1.568c.47.47.706 1.087.706 1.704s-.235 1.233-.706 1.704l-1.611 1.611a.98.98 0 0 1-.837.276c-.47-.07-.802-.48-.968-.925a2.501 2.501 0 1 0-3.214 3.214c.446.166.855.497.925.968a.979.979 0 0 1-.276.837l-1.61 1.61a2.404 2.404 0 0 1-1.705.707 2.402 2.402 0 0 1-1.704-.706l-1.568-1.568a1.026 1.026 0 0 0-.877-.29c-.493.074-.84.504-1.02.968a2.5 2.5 0 1 1-3.237-3.237c.464-.18.894-.527.967-1.02a1.026 1.026 0 0 0-.289-.877l-1.568-1.568A2.402 2.402 0 0 1 1.998 12c0-.617.236-1.234.706-1.704L4.23 8.77c.24-.24.581-.353.917-.303.515.076.874.54 1.02 1.02a2.5 2.5 0 1 0 3.237-3.237c-.48-.146-.944-.505-1.02-1.02a.98.98 0 0 1 .303-.917l1.526-1.526A2.402 2.402 0 0 1 11.998 2c.617 0 1.234.236 1.704.706l1.568 1.568c.23.23.556.338.877.29.493-.074.84-.504 1.02-.968a2.5 2.5 0 1 1 3.236 3.236c-.464.18-.894.527-.967 1.02Z"/></svg>`,
|
||||
wrench: html`
|
||||
<svg viewBox="0 0 24 24">
|
||||
<path
|
||||
d="M14.7 6.3a1 1 0 0 0 0 1.4l1.6 1.6a1 1 0 0 0 1.4 0l3.77-3.77a6 6 0 0 1-7.94 7.94l-6.91 6.91a2.12 2.12 0 0 1-3-3l6.91-6.91a6 6 0 0 1 7.94-7.94l-3.76 3.76z"
|
||||
/>
|
||||
</svg>
|
||||
`,
|
||||
fileCode: html`
|
||||
<svg viewBox="0 0 24 24">
|
||||
<path d="M14.5 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V7.5L14.5 2z" />
|
||||
<polyline points="14 2 14 8 20 8" />
|
||||
<path d="m10 13-2 2 2 2" />
|
||||
<path d="m14 17 2-2-2-2" />
|
||||
</svg>
|
||||
`,
|
||||
edit: html`
|
||||
<svg viewBox="0 0 24 24">
|
||||
<path d="M11 4H4a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2v-7" />
|
||||
<path d="M18.5 2.5a2.121 2.121 0 0 1 3 3L12 15l-4 1 1-4 9.5-9.5z" />
|
||||
</svg>
|
||||
`,
|
||||
penLine: html`
|
||||
<svg viewBox="0 0 24 24">
|
||||
<path d="M12 20h9" />
|
||||
<path d="M16.5 3.5a2.121 2.121 0 0 1 3 3L7 19l-4 1 1-4L16.5 3.5z" />
|
||||
</svg>
|
||||
`,
|
||||
paperclip: html`
|
||||
<svg viewBox="0 0 24 24">
|
||||
<path
|
||||
d="m21.44 11.05-9.19 9.19a6 6 0 0 1-8.49-8.49l8.57-8.57A4 4 0 1 1 18 8.84l-8.59 8.57a2 2 0 0 1-2.83-2.83l8.49-8.48"
|
||||
/>
|
||||
</svg>
|
||||
`,
|
||||
globe: html`
|
||||
<svg viewBox="0 0 24 24">
|
||||
<circle cx="12" cy="12" r="10" />
|
||||
<path d="M12 2a14.5 14.5 0 0 0 0 20 14.5 14.5 0 0 0 0-20" />
|
||||
<path d="M2 12h20" />
|
||||
</svg>
|
||||
`,
|
||||
image: html`
|
||||
<svg viewBox="0 0 24 24">
|
||||
<rect width="18" height="18" x="3" y="3" rx="2" ry="2" />
|
||||
<circle cx="9" cy="9" r="2" />
|
||||
<path d="m21 15-3.086-3.086a2 2 0 0 0-2.828 0L6 21" />
|
||||
</svg>
|
||||
`,
|
||||
smartphone: html`
|
||||
<svg viewBox="0 0 24 24">
|
||||
<rect width="14" height="20" x="5" y="2" rx="2" ry="2" />
|
||||
<path d="M12 18h.01" />
|
||||
</svg>
|
||||
`,
|
||||
plug: html`
|
||||
<svg viewBox="0 0 24 24">
|
||||
<path d="M12 22v-5" />
|
||||
<path d="M9 8V2" />
|
||||
<path d="M15 8V2" />
|
||||
<path d="M18 8v5a4 4 0 0 1-4 4h-4a4 4 0 0 1-4-4V8Z" />
|
||||
</svg>
|
||||
`,
|
||||
circle: html`
|
||||
<svg viewBox="0 0 24 24"><circle cx="12" cy="12" r="10" /></svg>
|
||||
`,
|
||||
puzzle: html`
|
||||
<svg viewBox="0 0 24 24">
|
||||
<path
|
||||
d="M19.439 7.85c-.049.322.059.648.289.878l1.568 1.568c.47.47.706 1.087.706 1.704s-.235 1.233-.706 1.704l-1.611 1.611a.98.98 0 0 1-.837.276c-.47-.07-.802-.48-.968-.925a2.501 2.501 0 1 0-3.214 3.214c.446.166.855.497.925.968a.979.979 0 0 1-.276.837l-1.61 1.61a2.404 2.404 0 0 1-1.705.707 2.402 2.402 0 0 1-1.704-.706l-1.568-1.568a1.026 1.026 0 0 0-.877-.29c-.493.074-.84.504-1.02.968a2.5 2.5 0 1 1-3.237-3.237c.464-.18.894-.527.967-1.02a1.026 1.026 0 0 0-.289-.877l-1.568-1.568A2.402 2.402 0 0 1 1.998 12c0-.617.236-1.234.706-1.704L4.23 8.77c.24-.24.581-.353.917-.303.515.076.874.54 1.02 1.02a2.5 2.5 0 1 0 3.237-3.237c-.48-.146-.944-.505-1.02-1.02a.98.98 0 0 1 .303-.917l1.526-1.526A2.402 2.402 0 0 1 11.998 2c.617 0 1.234.236 1.704.706l1.568 1.568c.23.23.556.338.877.29.493-.074.84-.504 1.02-.968a2.5 2.5 0 1 1 3.236 3.236c-.464.18-.894.527-.967 1.02Z"
|
||||
/>
|
||||
</svg>
|
||||
`,
|
||||
} as const;
|
||||
|
||||
export type IconName = keyof typeof icons;
|
||||
@@ -52,7 +235,10 @@ export function renderIcon(name: IconName, className = "nav-item__icon"): Templa
|
||||
}
|
||||
|
||||
// Legacy function for compatibility
|
||||
export function renderEmojiIcon(iconContent: string | TemplateResult, className: string): TemplateResult {
|
||||
export function renderEmojiIcon(
|
||||
iconContent: string | TemplateResult,
|
||||
className: string,
|
||||
): TemplateResult {
|
||||
return html`<span class=${className} aria-hidden="true">${iconContent}</span>`;
|
||||
}
|
||||
|
||||
|
||||
@@ -75,13 +75,9 @@ describe("control UI routing", () => {
|
||||
const app = mountApp("/chat");
|
||||
await app.updateComplete;
|
||||
|
||||
const link = app.querySelector<HTMLAnchorElement>(
|
||||
'a.nav-item[href="/channels"]',
|
||||
);
|
||||
const link = app.querySelector<HTMLAnchorElement>('a.nav-item[href="/channels"]');
|
||||
expect(link).not.toBeNull();
|
||||
link?.dispatchEvent(
|
||||
new MouseEvent("click", { bubbles: true, cancelable: true, button: 0 }),
|
||||
);
|
||||
link?.dispatchEvent(new MouseEvent("click", { bubbles: true, cancelable: true, button: 0 }));
|
||||
|
||||
await app.updateComplete;
|
||||
expect(app.tab).toBe("channels");
|
||||
|
||||
@@ -37,9 +37,7 @@ const TAB_PATHS: Record<Tab, string> = {
|
||||
logs: "/logs",
|
||||
};
|
||||
|
||||
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 {
|
||||
if (!basePath) return "";
|
||||
|
||||
+6
-15
@@ -49,22 +49,16 @@ export function loadSettings(): UiSettings {
|
||||
? parsed.sessionKey.trim()
|
||||
: defaults.sessionKey,
|
||||
lastActiveSessionKey:
|
||||
typeof parsed.lastActiveSessionKey === "string" &&
|
||||
parsed.lastActiveSessionKey.trim()
|
||||
typeof parsed.lastActiveSessionKey === "string" && parsed.lastActiveSessionKey.trim()
|
||||
? parsed.lastActiveSessionKey.trim()
|
||||
: (typeof parsed.sessionKey === "string" &&
|
||||
parsed.sessionKey.trim()) ||
|
||||
: (typeof parsed.sessionKey === "string" && parsed.sessionKey.trim()) ||
|
||||
defaults.lastActiveSessionKey,
|
||||
theme:
|
||||
parsed.theme === "light" ||
|
||||
parsed.theme === "dark" ||
|
||||
parsed.theme === "system"
|
||||
parsed.theme === "light" || parsed.theme === "dark" || parsed.theme === "system"
|
||||
? parsed.theme
|
||||
: defaults.theme,
|
||||
chatFocusMode:
|
||||
typeof parsed.chatFocusMode === "boolean"
|
||||
? parsed.chatFocusMode
|
||||
: defaults.chatFocusMode,
|
||||
typeof parsed.chatFocusMode === "boolean" ? parsed.chatFocusMode : defaults.chatFocusMode,
|
||||
chatShowThinking:
|
||||
typeof parsed.chatShowThinking === "boolean"
|
||||
? parsed.chatShowThinking
|
||||
@@ -76,12 +70,9 @@ export function loadSettings(): UiSettings {
|
||||
? parsed.splitRatio
|
||||
: defaults.splitRatio,
|
||||
navCollapsed:
|
||||
typeof parsed.navCollapsed === "boolean"
|
||||
? parsed.navCollapsed
|
||||
: defaults.navCollapsed,
|
||||
typeof parsed.navCollapsed === "boolean" ? parsed.navCollapsed : defaults.navCollapsed,
|
||||
navGroupsCollapsed:
|
||||
typeof parsed.navGroupsCollapsed === "object" &&
|
||||
parsed.navGroupsCollapsed !== null
|
||||
typeof parsed.navGroupsCollapsed === "object" && parsed.navGroupsCollapsed !== null
|
||||
? parsed.navGroupsCollapsed
|
||||
: defaults.navGroupsCollapsed,
|
||||
};
|
||||
|
||||
@@ -55,8 +55,7 @@ export const startThemeTransition = ({
|
||||
const document_ = documentReference as DocumentWithViewTransition;
|
||||
const prefersReducedMotion = hasReducedMotionPreference();
|
||||
|
||||
const canUseViewTransition =
|
||||
Boolean(document_.startViewTransition) && !prefersReducedMotion;
|
||||
const canUseViewTransition = Boolean(document_.startViewTransition) && !prefersReducedMotion;
|
||||
|
||||
if (canUseViewTransition) {
|
||||
let xPercent = 0.5;
|
||||
@@ -71,11 +70,7 @@ export const startThemeTransition = ({
|
||||
yPercent = clamp01(context.pointerClientY / window.innerHeight);
|
||||
} else if (context?.element) {
|
||||
const rect = context.element.getBoundingClientRect();
|
||||
if (
|
||||
rect.width > 0 &&
|
||||
rect.height > 0 &&
|
||||
typeof window !== "undefined"
|
||||
) {
|
||||
if (rect.width > 0 && rect.height > 0 && typeof window !== "undefined") {
|
||||
xPercent = clamp01((rect.left + rect.width / 2) / window.innerWidth);
|
||||
yPercent = clamp01((rect.top + rect.height / 2) / window.innerHeight);
|
||||
}
|
||||
|
||||
+1
-3
@@ -5,9 +5,7 @@ export function getSystemTheme(): ResolvedTheme {
|
||||
if (typeof window === "undefined" || typeof window.matchMedia !== "function") {
|
||||
return "dark";
|
||||
}
|
||||
return window.matchMedia("(prefers-color-scheme: dark)").matches
|
||||
? "dark"
|
||||
: "light";
|
||||
return window.matchMedia("(prefers-color-scheme: dark)").matches ? "dark" : "light";
|
||||
}
|
||||
|
||||
export function resolveTheme(mode: ThemeMode): ResolvedTheme {
|
||||
|
||||
@@ -90,7 +90,13 @@
|
||||
},
|
||||
"act": {
|
||||
"label": "act",
|
||||
"detailKeys": ["request.kind", "request.ref", "request.selector", "request.text", "request.value"]
|
||||
"detailKeys": [
|
||||
"request.kind",
|
||||
"request.ref",
|
||||
"request.selector",
|
||||
"request.text",
|
||||
"request.value"
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -117,9 +123,15 @@
|
||||
"approve": { "label": "approve", "detailKeys": ["requestId"] },
|
||||
"reject": { "label": "reject", "detailKeys": ["requestId"] },
|
||||
"notify": { "label": "notify", "detailKeys": ["node", "nodeId", "title", "body"] },
|
||||
"camera_snap": { "label": "camera snap", "detailKeys": ["node", "nodeId", "facing", "deviceId"] },
|
||||
"camera_snap": {
|
||||
"label": "camera snap",
|
||||
"detailKeys": ["node", "nodeId", "facing", "deviceId"]
|
||||
},
|
||||
"camera_list": { "label": "camera list", "detailKeys": ["node", "nodeId"] },
|
||||
"camera_clip": { "label": "camera clip", "detailKeys": ["node", "nodeId", "facing", "duration", "durationMs"] },
|
||||
"camera_clip": {
|
||||
"label": "camera clip",
|
||||
"detailKeys": ["node", "nodeId", "facing", "duration", "durationMs"]
|
||||
},
|
||||
"screen_record": {
|
||||
"label": "screen record",
|
||||
"detailKeys": ["node", "nodeId", "duration", "durationMs", "fps", "screenIndex"]
|
||||
|
||||
@@ -153,8 +153,7 @@ export function resolveToolDisplay(params: {
|
||||
detail = resolveWriteDetail(params.args);
|
||||
}
|
||||
|
||||
const detailKeys =
|
||||
actionSpec?.detailKeys ?? spec?.detailKeys ?? FALLBACK.detailKeys ?? [];
|
||||
const detailKeys = actionSpec?.detailKeys ?? spec?.detailKeys ?? FALLBACK.detailKeys ?? [];
|
||||
if (!detail && detailKeys.length > 0) {
|
||||
detail = resolveDetailFromKeys(params.args, detailKeys);
|
||||
}
|
||||
@@ -192,7 +191,5 @@ export function formatToolSummary(display: ToolDisplay): string {
|
||||
|
||||
function shortenHomeInString(input: string): string {
|
||||
if (!input) return input;
|
||||
return input
|
||||
.replace(/\/Users\/[^/]+/g, "~")
|
||||
.replace(/\/home\/[^/]+/g, "~");
|
||||
return input.replace(/\/Users\/[^/]+/g, "~").replace(/\/home\/[^/]+/g, "~");
|
||||
}
|
||||
|
||||
+1
-7
@@ -514,13 +514,7 @@ export type StatusSummary = Record<string, unknown>;
|
||||
|
||||
export type HealthSnapshot = Record<string, unknown>;
|
||||
|
||||
export type LogLevel =
|
||||
| "trace"
|
||||
| "debug"
|
||||
| "info"
|
||||
| "warn"
|
||||
| "error"
|
||||
| "fatal";
|
||||
export type LogLevel = "trace" | "debug" | "info" | "warn" | "error" | "fatal";
|
||||
|
||||
export type LogEntry = {
|
||||
raw: string;
|
||||
|
||||
@@ -2,12 +2,7 @@ import { html } from "lit";
|
||||
|
||||
import type { ConfigUiHints } from "../types";
|
||||
import type { ChannelsProps } from "./channels.types";
|
||||
import {
|
||||
analyzeConfigSchema,
|
||||
renderNode,
|
||||
schemaType,
|
||||
type JsonSchema,
|
||||
} from "./config-form";
|
||||
import { analyzeConfigSchema, renderNode, schemaType, type JsonSchema } from "./config-form";
|
||||
|
||||
type ChannelConfigFormProps = {
|
||||
channelId: string;
|
||||
@@ -61,9 +56,7 @@ function resolveChannelValue(
|
||||
(fromChannels && typeof fromChannels === "object"
|
||||
? (fromChannels as Record<string, unknown>)
|
||||
: null) ??
|
||||
(fallback && typeof fallback === "object"
|
||||
? (fallback as Record<string, unknown>)
|
||||
: null);
|
||||
(fallback && typeof fallback === "object" ? (fallback as Record<string, unknown>) : null);
|
||||
return resolved ?? {};
|
||||
}
|
||||
|
||||
@@ -71,11 +64,15 @@ export function renderChannelConfigForm(props: ChannelConfigFormProps) {
|
||||
const analysis = analyzeConfigSchema(props.schema);
|
||||
const normalized = analysis.schema;
|
||||
if (!normalized) {
|
||||
return html`<div class="callout danger">Schema unavailable. Use Raw.</div>`;
|
||||
return html`
|
||||
<div class="callout danger">Schema unavailable. Use Raw.</div>
|
||||
`;
|
||||
}
|
||||
const node = resolveSchemaNode(normalized, ["channels", props.channelId]);
|
||||
if (!node) {
|
||||
return html`<div class="callout danger">Channel config schema unavailable.</div>`;
|
||||
return html`
|
||||
<div class="callout danger">Channel config schema unavailable.</div>
|
||||
`;
|
||||
}
|
||||
const configValue = props.configValue ?? {};
|
||||
const value = resolveChannelValue(configValue, props.channelId);
|
||||
@@ -95,24 +92,25 @@ export function renderChannelConfigForm(props: ChannelConfigFormProps) {
|
||||
`;
|
||||
}
|
||||
|
||||
export function renderChannelConfigSection(params: {
|
||||
channelId: string;
|
||||
props: ChannelsProps;
|
||||
}) {
|
||||
export function renderChannelConfigSection(params: { channelId: string; props: ChannelsProps }) {
|
||||
const { channelId, props } = params;
|
||||
const disabled = props.configSaving || props.configSchemaLoading;
|
||||
return html`
|
||||
<div style="margin-top: 16px;">
|
||||
${props.configSchemaLoading
|
||||
? html`<div class="muted">Loading config schema…</div>`
|
||||
: renderChannelConfigForm({
|
||||
channelId,
|
||||
configValue: props.configForm,
|
||||
schema: props.configSchema,
|
||||
uiHints: props.configUiHints,
|
||||
disabled,
|
||||
onPatch: props.onConfigPatch,
|
||||
})}
|
||||
${
|
||||
props.configSchemaLoading
|
||||
? html`
|
||||
<div class="muted">Loading config schema…</div>
|
||||
`
|
||||
: renderChannelConfigForm({
|
||||
channelId,
|
||||
configValue: props.configForm,
|
||||
schema: props.configSchema,
|
||||
uiHints: props.configUiHints,
|
||||
disabled,
|
||||
onPatch: props.onConfigPatch,
|
||||
})
|
||||
}
|
||||
<div class="row" style="margin-top: 12px;">
|
||||
<button
|
||||
class="btn primary"
|
||||
|
||||
@@ -37,18 +37,22 @@ export function renderDiscordCard(params: {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
${discord?.lastError
|
||||
? html`<div class="callout danger" style="margin-top: 12px;">
|
||||
${
|
||||
discord?.lastError
|
||||
? html`<div class="callout danger" style="margin-top: 12px;">
|
||||
${discord.lastError}
|
||||
</div>`
|
||||
: nothing}
|
||||
: nothing
|
||||
}
|
||||
|
||||
${discord?.probe
|
||||
? html`<div class="callout" style="margin-top: 12px;">
|
||||
${
|
||||
discord?.probe
|
||||
? html`<div class="callout" style="margin-top: 12px;">
|
||||
Probe ${discord.probe.ok ? "ok" : "failed"} ·
|
||||
${discord.probe.status ?? ""} ${discord.probe.error ?? ""}
|
||||
</div>`
|
||||
: nothing}
|
||||
: nothing
|
||||
}
|
||||
|
||||
${renderChannelConfigSection({ channelId: "discord", props })}
|
||||
|
||||
|
||||
@@ -34,9 +34,11 @@ export function renderGoogleChatCard(params: {
|
||||
<div>
|
||||
<span class="label">Audience</span>
|
||||
<span>
|
||||
${googleChat?.audienceType
|
||||
? `${googleChat.audienceType}${googleChat.audience ? ` · ${googleChat.audience}` : ""}`
|
||||
: "n/a"}
|
||||
${
|
||||
googleChat?.audienceType
|
||||
? `${googleChat.audienceType}${googleChat.audience ? ` · ${googleChat.audience}` : ""}`
|
||||
: "n/a"
|
||||
}
|
||||
</span>
|
||||
</div>
|
||||
<div>
|
||||
@@ -49,18 +51,22 @@ export function renderGoogleChatCard(params: {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
${googleChat?.lastError
|
||||
? html`<div class="callout danger" style="margin-top: 12px;">
|
||||
${
|
||||
googleChat?.lastError
|
||||
? html`<div class="callout danger" style="margin-top: 12px;">
|
||||
${googleChat.lastError}
|
||||
</div>`
|
||||
: nothing}
|
||||
: nothing
|
||||
}
|
||||
|
||||
${googleChat?.probe
|
||||
? html`<div class="callout" style="margin-top: 12px;">
|
||||
${
|
||||
googleChat?.probe
|
||||
? html`<div class="callout" style="margin-top: 12px;">
|
||||
Probe ${googleChat.probe.ok ? "ok" : "failed"} ·
|
||||
${googleChat.probe.status ?? ""} ${googleChat.probe.error ?? ""}
|
||||
</div>`
|
||||
: nothing}
|
||||
: nothing
|
||||
}
|
||||
|
||||
${renderChannelConfigSection({ channelId: "googlechat", props })}
|
||||
|
||||
|
||||
@@ -37,18 +37,22 @@ export function renderIMessageCard(params: {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
${imessage?.lastError
|
||||
? html`<div class="callout danger" style="margin-top: 12px;">
|
||||
${
|
||||
imessage?.lastError
|
||||
? html`<div class="callout danger" style="margin-top: 12px;">
|
||||
${imessage.lastError}
|
||||
</div>`
|
||||
: nothing}
|
||||
: nothing
|
||||
}
|
||||
|
||||
${imessage?.probe
|
||||
? html`<div class="callout" style="margin-top: 12px;">
|
||||
${
|
||||
imessage?.probe
|
||||
? html`<div class="callout" style="margin-top: 12px;">
|
||||
Probe ${imessage.probe.ok ? "ok" : "failed"} ·
|
||||
${imessage.probe.error ?? ""}
|
||||
</div>`
|
||||
: nothing}
|
||||
: nothing
|
||||
}
|
||||
|
||||
${renderChannelConfigSection({ channelId: "imessage", props })}
|
||||
|
||||
|
||||
@@ -82,7 +82,7 @@ export function renderNostrProfileForm(params: {
|
||||
placeholder?: string;
|
||||
maxLength?: number;
|
||||
help?: string;
|
||||
} = {}
|
||||
} = {},
|
||||
) => {
|
||||
const { type = "text", placeholder, maxLength, help } = opts;
|
||||
const value = state.values[field] ?? "";
|
||||
@@ -169,13 +169,17 @@ export function renderNostrProfileForm(params: {
|
||||
<div style="font-size: 12px; color: var(--text-muted);">Account: ${accountId}</div>
|
||||
</div>
|
||||
|
||||
${state.error
|
||||
? html`<div class="callout danger" style="margin-bottom: 12px;">${state.error}</div>`
|
||||
: nothing}
|
||||
${
|
||||
state.error
|
||||
? html`<div class="callout danger" style="margin-bottom: 12px;">${state.error}</div>`
|
||||
: nothing
|
||||
}
|
||||
|
||||
${state.success
|
||||
? html`<div class="callout success" style="margin-bottom: 12px;">${state.success}</div>`
|
||||
: nothing}
|
||||
${
|
||||
state.success
|
||||
? html`<div class="callout success" style="margin-bottom: 12px;">${state.success}</div>`
|
||||
: nothing
|
||||
}
|
||||
|
||||
${renderPicturePreview()}
|
||||
|
||||
@@ -204,8 +208,9 @@ export function renderNostrProfileForm(params: {
|
||||
help: "HTTPS URL to your profile picture",
|
||||
})}
|
||||
|
||||
${state.showAdvanced
|
||||
? html`
|
||||
${
|
||||
state.showAdvanced
|
||||
? html`
|
||||
<div style="border-top: 1px solid var(--border-color); padding-top: 12px; margin-top: 12px;">
|
||||
<div style="font-weight: 500; margin-bottom: 12px; color: var(--text-muted);">Advanced</div>
|
||||
|
||||
@@ -232,7 +237,8 @@ export function renderNostrProfileForm(params: {
|
||||
})}
|
||||
</div>
|
||||
`
|
||||
: nothing}
|
||||
: nothing
|
||||
}
|
||||
|
||||
<div style="display: flex; gap: 8px; margin-top: 16px; flex-wrap: wrap;">
|
||||
<button
|
||||
@@ -267,11 +273,15 @@ export function renderNostrProfileForm(params: {
|
||||
</button>
|
||||
</div>
|
||||
|
||||
${isDirty
|
||||
? html`<div style="font-size: 12px; color: var(--warning-color); margin-top: 8px;">
|
||||
You have unsaved changes
|
||||
</div>`
|
||||
: nothing}
|
||||
${
|
||||
isDirty
|
||||
? html`
|
||||
<div style="font-size: 12px; color: var(--warning-color); margin-top: 8px">
|
||||
You have unsaved changes
|
||||
</div>
|
||||
`
|
||||
: nothing
|
||||
}
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
@@ -284,7 +294,7 @@ export function renderNostrProfileForm(params: {
|
||||
* Create initial form state from existing profile
|
||||
*/
|
||||
export function createNostrProfileFormState(
|
||||
profile: NostrProfileType | undefined
|
||||
profile: NostrProfileType | undefined,
|
||||
): NostrProfileFormState {
|
||||
const values: NostrProfileType = {
|
||||
name: profile?.name ?? "",
|
||||
@@ -305,8 +315,6 @@ export function createNostrProfileFormState(
|
||||
error: null,
|
||||
success: null,
|
||||
fieldErrors: {},
|
||||
showAdvanced: Boolean(
|
||||
profile?.banner || profile?.website || profile?.nip05 || profile?.lud16
|
||||
),
|
||||
showAdvanced: Boolean(profile?.banner || profile?.website || profile?.nip05 || profile?.lud16),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -44,8 +44,7 @@ export function renderNostrCard(params: {
|
||||
const summaryConfigured = nostr?.configured ?? primaryAccount?.configured ?? false;
|
||||
const summaryRunning = nostr?.running ?? primaryAccount?.running ?? false;
|
||||
const summaryPublicKey =
|
||||
nostr?.publicKey ??
|
||||
(primaryAccount as { publicKey?: string } | undefined)?.publicKey;
|
||||
nostr?.publicKey ?? (primaryAccount as { publicKey?: string } | undefined)?.publicKey;
|
||||
const summaryLastStartAt = nostr?.lastStartAt ?? primaryAccount?.lastStartAt ?? null;
|
||||
const summaryLastError = nostr?.lastError ?? primaryAccount?.lastError ?? null;
|
||||
const hasMultipleAccounts = nostrAccounts.length > 1;
|
||||
@@ -79,11 +78,13 @@ export function renderNostrCard(params: {
|
||||
<span class="label">Last inbound</span>
|
||||
<span>${account.lastInboundAt ? formatAgo(account.lastInboundAt) : "n/a"}</span>
|
||||
</div>
|
||||
${account.lastError
|
||||
? html`
|
||||
${
|
||||
account.lastError
|
||||
? html`
|
||||
<div class="account-card-error">${account.lastError}</div>
|
||||
`
|
||||
: nothing}
|
||||
: nothing
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
@@ -100,17 +101,19 @@ export function renderNostrCard(params: {
|
||||
}
|
||||
|
||||
const profile =
|
||||
(primaryAccount as
|
||||
| {
|
||||
profile?: {
|
||||
name?: string;
|
||||
displayName?: string;
|
||||
about?: string;
|
||||
picture?: string;
|
||||
nip05?: string;
|
||||
};
|
||||
}
|
||||
| undefined)?.profile ?? nostr?.profile;
|
||||
(
|
||||
primaryAccount as
|
||||
| {
|
||||
profile?: {
|
||||
name?: string;
|
||||
displayName?: string;
|
||||
about?: string;
|
||||
picture?: string;
|
||||
nip05?: string;
|
||||
};
|
||||
}
|
||||
| undefined
|
||||
)?.profile ?? nostr?.profile;
|
||||
const { name, displayName, about, picture, nip05 } = profile ?? {};
|
||||
const hasAnyProfileData = name || displayName || about || picture || nip05;
|
||||
|
||||
@@ -118,8 +121,9 @@ export function renderNostrCard(params: {
|
||||
<div style="margin-top: 16px; padding: 12px; background: var(--bg-secondary); border-radius: 8px;">
|
||||
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 8px;">
|
||||
<div style="font-weight: 500;">Profile</div>
|
||||
${summaryConfigured
|
||||
? html`
|
||||
${
|
||||
summaryConfigured
|
||||
? html`
|
||||
<button
|
||||
class="btn btn-sm"
|
||||
@click=${onEditProfile}
|
||||
@@ -128,13 +132,16 @@ export function renderNostrCard(params: {
|
||||
Edit Profile
|
||||
</button>
|
||||
`
|
||||
: nothing}
|
||||
: nothing
|
||||
}
|
||||
</div>
|
||||
${hasAnyProfileData
|
||||
? html`
|
||||
${
|
||||
hasAnyProfileData
|
||||
? html`
|
||||
<div class="status-list">
|
||||
${picture
|
||||
? html`
|
||||
${
|
||||
picture
|
||||
? html`
|
||||
<div style="margin-bottom: 8px;">
|
||||
<img
|
||||
src=${picture}
|
||||
@@ -146,22 +153,28 @@ export function renderNostrCard(params: {
|
||||
/>
|
||||
</div>
|
||||
`
|
||||
: nothing}
|
||||
: nothing
|
||||
}
|
||||
${name ? html`<div><span class="label">Name</span><span>${name}</span></div>` : nothing}
|
||||
${displayName
|
||||
? html`<div><span class="label">Display Name</span><span>${displayName}</span></div>`
|
||||
: nothing}
|
||||
${about
|
||||
? html`<div><span class="label">About</span><span style="max-width: 300px; overflow: hidden; text-overflow: ellipsis;">${about}</span></div>`
|
||||
: nothing}
|
||||
${
|
||||
displayName
|
||||
? html`<div><span class="label">Display Name</span><span>${displayName}</span></div>`
|
||||
: nothing
|
||||
}
|
||||
${
|
||||
about
|
||||
? html`<div><span class="label">About</span><span style="max-width: 300px; overflow: hidden; text-overflow: ellipsis;">${about}</span></div>`
|
||||
: nothing
|
||||
}
|
||||
${nip05 ? html`<div><span class="label">NIP-05</span><span>${nip05}</span></div>` : nothing}
|
||||
</div>
|
||||
`
|
||||
: html`
|
||||
<div style="color: var(--text-muted); font-size: 13px;">
|
||||
No profile set. Click "Edit Profile" to add your name, bio, and avatar.
|
||||
</div>
|
||||
`}
|
||||
: html`
|
||||
<div style="color: var(--text-muted); font-size: 13px">
|
||||
No profile set. Click "Edit Profile" to add your name, bio, and avatar.
|
||||
</div>
|
||||
`
|
||||
}
|
||||
</div>
|
||||
`;
|
||||
};
|
||||
@@ -172,13 +185,14 @@ export function renderNostrCard(params: {
|
||||
<div class="card-sub">Decentralized DMs via Nostr relays (NIP-04).</div>
|
||||
${accountCountLabel}
|
||||
|
||||
${hasMultipleAccounts
|
||||
? html`
|
||||
${
|
||||
hasMultipleAccounts
|
||||
? html`
|
||||
<div class="account-card-list">
|
||||
${nostrAccounts.map((account) => renderAccountCard(account))}
|
||||
</div>
|
||||
`
|
||||
: html`
|
||||
: html`
|
||||
<div class="status-list" style="margin-top: 16px;">
|
||||
<div>
|
||||
<span class="label">Configured</span>
|
||||
@@ -199,11 +213,14 @@ export function renderNostrCard(params: {
|
||||
<span>${summaryLastStartAt ? formatAgo(summaryLastStartAt) : "n/a"}</span>
|
||||
</div>
|
||||
</div>
|
||||
`}
|
||||
`
|
||||
}
|
||||
|
||||
${summaryLastError
|
||||
? html`<div class="callout danger" style="margin-top: 12px;">${summaryLastError}</div>`
|
||||
: nothing}
|
||||
${
|
||||
summaryLastError
|
||||
? html`<div class="callout danger" style="margin-top: 12px;">${summaryLastError}</div>`
|
||||
: nothing
|
||||
}
|
||||
|
||||
${renderProfileSection()}
|
||||
|
||||
|
||||
@@ -41,18 +41,22 @@ export function renderSignalCard(params: {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
${signal?.lastError
|
||||
? html`<div class="callout danger" style="margin-top: 12px;">
|
||||
${
|
||||
signal?.lastError
|
||||
? html`<div class="callout danger" style="margin-top: 12px;">
|
||||
${signal.lastError}
|
||||
</div>`
|
||||
: nothing}
|
||||
: nothing
|
||||
}
|
||||
|
||||
${signal?.probe
|
||||
? html`<div class="callout" style="margin-top: 12px;">
|
||||
${
|
||||
signal?.probe
|
||||
? html`<div class="callout" style="margin-top: 12px;">
|
||||
Probe ${signal.probe.ok ? "ok" : "failed"} ·
|
||||
${signal.probe.status ?? ""} ${signal.probe.error ?? ""}
|
||||
</div>`
|
||||
: nothing}
|
||||
: nothing
|
||||
}
|
||||
|
||||
${renderChannelConfigSection({ channelId: "signal", props })}
|
||||
|
||||
|
||||
@@ -37,18 +37,22 @@ export function renderSlackCard(params: {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
${slack?.lastError
|
||||
? html`<div class="callout danger" style="margin-top: 12px;">
|
||||
${
|
||||
slack?.lastError
|
||||
? html`<div class="callout danger" style="margin-top: 12px;">
|
||||
${slack.lastError}
|
||||
</div>`
|
||||
: nothing}
|
||||
: nothing
|
||||
}
|
||||
|
||||
${slack?.probe
|
||||
? html`<div class="callout" style="margin-top: 12px;">
|
||||
${
|
||||
slack?.probe
|
||||
? html`<div class="callout" style="margin-top: 12px;">
|
||||
Probe ${slack.probe.ok ? "ok" : "failed"} ·
|
||||
${slack.probe.status ?? ""} ${slack.probe.error ?? ""}
|
||||
</div>`
|
||||
: nothing}
|
||||
: nothing
|
||||
}
|
||||
|
||||
${renderChannelConfigSection({ channelId: "slack", props })}
|
||||
|
||||
|
||||
@@ -39,13 +39,15 @@ export function renderTelegramCard(params: {
|
||||
<span class="label">Last inbound</span>
|
||||
<span>${account.lastInboundAt ? formatAgo(account.lastInboundAt) : "n/a"}</span>
|
||||
</div>
|
||||
${account.lastError
|
||||
? html`
|
||||
${
|
||||
account.lastError
|
||||
? html`
|
||||
<div class="account-card-error">
|
||||
${account.lastError}
|
||||
</div>
|
||||
`
|
||||
: nothing}
|
||||
: nothing
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
@@ -57,13 +59,14 @@ export function renderTelegramCard(params: {
|
||||
<div class="card-sub">Bot status and channel configuration.</div>
|
||||
${accountCountLabel}
|
||||
|
||||
${hasMultipleAccounts
|
||||
? html`
|
||||
${
|
||||
hasMultipleAccounts
|
||||
? html`
|
||||
<div class="account-card-list">
|
||||
${telegramAccounts.map((account) => renderAccountCard(account))}
|
||||
</div>
|
||||
`
|
||||
: html`
|
||||
: html`
|
||||
<div class="status-list" style="margin-top: 16px;">
|
||||
<div>
|
||||
<span class="label">Configured</span>
|
||||
@@ -86,20 +89,25 @@ export function renderTelegramCard(params: {
|
||||
<span>${telegram?.lastProbeAt ? formatAgo(telegram.lastProbeAt) : "n/a"}</span>
|
||||
</div>
|
||||
</div>
|
||||
`}
|
||||
`
|
||||
}
|
||||
|
||||
${telegram?.lastError
|
||||
? html`<div class="callout danger" style="margin-top: 12px;">
|
||||
${
|
||||
telegram?.lastError
|
||||
? html`<div class="callout danger" style="margin-top: 12px;">
|
||||
${telegram.lastError}
|
||||
</div>`
|
||||
: nothing}
|
||||
: nothing
|
||||
}
|
||||
|
||||
${telegram?.probe
|
||||
? html`<div class="callout" style="margin-top: 12px;">
|
||||
${
|
||||
telegram?.probe
|
||||
? html`<div class="callout" style="margin-top: 12px;">
|
||||
Probe ${telegram.probe.ok ? "ok" : "failed"} ·
|
||||
${telegram.probe.status ?? ""} ${telegram.probe.error ?? ""}
|
||||
</div>`
|
||||
: nothing}
|
||||
: nothing
|
||||
}
|
||||
|
||||
${renderChannelConfigSection({ channelId: "telegram", props })}
|
||||
|
||||
|
||||
+28
-47
@@ -15,11 +15,7 @@ import type {
|
||||
TelegramStatus,
|
||||
WhatsAppStatus,
|
||||
} from "../types";
|
||||
import type {
|
||||
ChannelKey,
|
||||
ChannelsChannelData,
|
||||
ChannelsProps,
|
||||
} from "./channels.types";
|
||||
import type { ChannelKey, ChannelsChannelData, ChannelsProps } from "./channels.types";
|
||||
import { channelEnabled, renderChannelAccountCount } from "./channels.shared";
|
||||
import { renderChannelConfigSection } from "./channels.config";
|
||||
import { renderDiscordCard } from "./channels.discord";
|
||||
@@ -33,12 +29,8 @@ import { renderWhatsAppCard } from "./channels.whatsapp";
|
||||
|
||||
export function renderChannels(props: ChannelsProps) {
|
||||
const channels = props.snapshot?.channels as Record<string, unknown> | null;
|
||||
const whatsapp = (channels?.whatsapp ?? undefined) as
|
||||
| WhatsAppStatus
|
||||
| undefined;
|
||||
const telegram = (channels?.telegram ?? undefined) as
|
||||
| TelegramStatus
|
||||
| undefined;
|
||||
const whatsapp = (channels?.whatsapp ?? undefined) as WhatsAppStatus | undefined;
|
||||
const telegram = (channels?.telegram ?? undefined) as TelegramStatus | undefined;
|
||||
const discord = (channels?.discord ?? null) as DiscordStatus | null;
|
||||
const googlechat = (channels?.googlechat ?? null) as GoogleChatStatus | null;
|
||||
const slack = (channels?.slack ?? null) as SlackStatus | null;
|
||||
@@ -82,11 +74,13 @@ export function renderChannels(props: ChannelsProps) {
|
||||
</div>
|
||||
<div class="muted">${props.lastSuccessAt ? formatAgo(props.lastSuccessAt) : "n/a"}</div>
|
||||
</div>
|
||||
${props.lastError
|
||||
? html`<div class="callout danger" style="margin-top: 12px;">
|
||||
${
|
||||
props.lastError
|
||||
? html`<div class="callout danger" style="margin-top: 12px;">
|
||||
${props.lastError}
|
||||
</div>`
|
||||
: nothing}
|
||||
: nothing
|
||||
}
|
||||
<pre class="code-block" style="margin-top: 12px;">
|
||||
${props.snapshot ? JSON.stringify(props.snapshot, null, 2) : "No snapshot yet."}
|
||||
</pre>
|
||||
@@ -101,27 +95,11 @@ function resolveChannelOrder(snapshot: ChannelsStatusSnapshot | null): ChannelKe
|
||||
if (snapshot?.channelOrder?.length) {
|
||||
return snapshot.channelOrder;
|
||||
}
|
||||
return [
|
||||
"whatsapp",
|
||||
"telegram",
|
||||
"discord",
|
||||
"googlechat",
|
||||
"slack",
|
||||
"signal",
|
||||
"imessage",
|
||||
"nostr",
|
||||
];
|
||||
return ["whatsapp", "telegram", "discord", "googlechat", "slack", "signal", "imessage", "nostr"];
|
||||
}
|
||||
|
||||
function renderChannel(
|
||||
key: ChannelKey,
|
||||
props: ChannelsProps,
|
||||
data: ChannelsChannelData,
|
||||
) {
|
||||
const accountCountLabel = renderChannelAccountCount(
|
||||
key,
|
||||
data.channelAccounts,
|
||||
);
|
||||
function renderChannel(key: ChannelKey, props: ChannelsProps, data: ChannelsChannelData) {
|
||||
const accountCountLabel = renderChannelAccountCount(key, data.channelAccounts);
|
||||
switch (key) {
|
||||
case "whatsapp":
|
||||
return renderWhatsAppCard({
|
||||
@@ -218,13 +196,14 @@ function renderGenericChannelCard(
|
||||
<div class="card-sub">Channel status and configuration.</div>
|
||||
${accountCountLabel}
|
||||
|
||||
${accounts.length > 0
|
||||
? html`
|
||||
${
|
||||
accounts.length > 0
|
||||
? html`
|
||||
<div class="account-card-list">
|
||||
${accounts.map((account) => renderGenericAccount(account))}
|
||||
</div>
|
||||
`
|
||||
: html`
|
||||
: html`
|
||||
<div class="status-list" style="margin-top: 16px;">
|
||||
<div>
|
||||
<span class="label">Configured</span>
|
||||
@@ -239,13 +218,16 @@ function renderGenericChannelCard(
|
||||
<span>${connected == null ? "n/a" : connected ? "Yes" : "No"}</span>
|
||||
</div>
|
||||
</div>
|
||||
`}
|
||||
`
|
||||
}
|
||||
|
||||
${lastError
|
||||
? html`<div class="callout danger" style="margin-top: 12px;">
|
||||
${
|
||||
lastError
|
||||
? html`<div class="callout danger" style="margin-top: 12px;">
|
||||
${lastError}
|
||||
</div>`
|
||||
: nothing}
|
||||
: nothing
|
||||
}
|
||||
|
||||
${renderChannelConfigSection({ channelId: key, props })}
|
||||
</div>
|
||||
@@ -259,10 +241,7 @@ function resolveChannelMetaMap(
|
||||
return Object.fromEntries(snapshot.channelMeta.map((entry) => [entry.id, entry]));
|
||||
}
|
||||
|
||||
function resolveChannelLabel(
|
||||
snapshot: ChannelsStatusSnapshot | null,
|
||||
key: string,
|
||||
): string {
|
||||
function resolveChannelLabel(snapshot: ChannelsStatusSnapshot | null, key: string): string {
|
||||
const meta = resolveChannelMetaMap(snapshot)[key];
|
||||
return meta?.label ?? snapshot?.channelLabels?.[key] ?? key;
|
||||
}
|
||||
@@ -316,13 +295,15 @@ function renderGenericAccount(account: ChannelAccountSnapshot) {
|
||||
<span class="label">Last inbound</span>
|
||||
<span>${account.lastInboundAt ? formatAgo(account.lastInboundAt) : "n/a"}</span>
|
||||
</div>
|
||||
${account.lastError
|
||||
? html`
|
||||
${
|
||||
account.lastError
|
||||
? html`
|
||||
<div class="account-card-error">
|
||||
${account.lastError}
|
||||
</div>
|
||||
`
|
||||
: nothing}
|
||||
: nothing
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
@@ -1,16 +1,16 @@
|
||||
import type {
|
||||
ChannelAccountSnapshot,
|
||||
ChannelsStatusSnapshot,
|
||||
ConfigUiHints,
|
||||
DiscordStatus,
|
||||
GoogleChatStatus,
|
||||
IMessageStatus,
|
||||
NostrProfile,
|
||||
NostrStatus,
|
||||
SignalStatus,
|
||||
SlackStatus,
|
||||
TelegramStatus,
|
||||
WhatsAppStatus,
|
||||
ChannelAccountSnapshot,
|
||||
ChannelsStatusSnapshot,
|
||||
ConfigUiHints,
|
||||
DiscordStatus,
|
||||
GoogleChatStatus,
|
||||
IMessageStatus,
|
||||
NostrProfile,
|
||||
NostrStatus,
|
||||
SignalStatus,
|
||||
SlackStatus,
|
||||
TelegramStatus,
|
||||
WhatsAppStatus,
|
||||
} from "../types";
|
||||
import type { NostrProfileFormState } from "./channels.nostr-profile-form";
|
||||
|
||||
|
||||
@@ -39,9 +39,7 @@ export function renderWhatsAppCard(params: {
|
||||
<div>
|
||||
<span class="label">Last connect</span>
|
||||
<span>
|
||||
${whatsapp?.lastConnectedAt
|
||||
? formatAgo(whatsapp.lastConnectedAt)
|
||||
: "n/a"}
|
||||
${whatsapp?.lastConnectedAt ? formatAgo(whatsapp.lastConnectedAt) : "n/a"}
|
||||
</span>
|
||||
</div>
|
||||
<div>
|
||||
@@ -53,30 +51,34 @@ export function renderWhatsAppCard(params: {
|
||||
<div>
|
||||
<span class="label">Auth age</span>
|
||||
<span>
|
||||
${whatsapp?.authAgeMs != null
|
||||
? formatDuration(whatsapp.authAgeMs)
|
||||
: "n/a"}
|
||||
${whatsapp?.authAgeMs != null ? formatDuration(whatsapp.authAgeMs) : "n/a"}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
${whatsapp?.lastError
|
||||
? html`<div class="callout danger" style="margin-top: 12px;">
|
||||
${
|
||||
whatsapp?.lastError
|
||||
? html`<div class="callout danger" style="margin-top: 12px;">
|
||||
${whatsapp.lastError}
|
||||
</div>`
|
||||
: nothing}
|
||||
: nothing
|
||||
}
|
||||
|
||||
${props.whatsappMessage
|
||||
? html`<div class="callout" style="margin-top: 12px;">
|
||||
${
|
||||
props.whatsappMessage
|
||||
? html`<div class="callout" style="margin-top: 12px;">
|
||||
${props.whatsappMessage}
|
||||
</div>`
|
||||
: nothing}
|
||||
: nothing
|
||||
}
|
||||
|
||||
${props.whatsappQrDataUrl
|
||||
? html`<div class="qr-wrap">
|
||||
${
|
||||
props.whatsappQrDataUrl
|
||||
? html`<div class="qr-wrap">
|
||||
<img src=${props.whatsappQrDataUrl} alt="WhatsApp QR" />
|
||||
</div>`
|
||||
: nothing}
|
||||
: nothing
|
||||
}
|
||||
|
||||
<div class="row" style="margin-top: 14px; flex-wrap: wrap;">
|
||||
<button
|
||||
|
||||
+59
-58
@@ -5,10 +5,7 @@ import type { SessionsListResult } from "../types";
|
||||
import type { ChatAttachment, ChatQueueItem } from "../ui-types";
|
||||
import type { ChatItem, MessageGroup } from "../types/chat-types";
|
||||
import { icons } from "../icons";
|
||||
import {
|
||||
normalizeMessage,
|
||||
normalizeRoleForGrouping,
|
||||
} from "../chat/message-normalizer";
|
||||
import { normalizeMessage, normalizeRoleForGrouping } from "../chat/message-normalizer";
|
||||
import {
|
||||
renderMessageGroup,
|
||||
renderReadingIndicatorGroup,
|
||||
@@ -108,10 +105,7 @@ function generateAttachmentId(): string {
|
||||
return `att-${Date.now()}-${Math.random().toString(36).slice(2, 9)}`;
|
||||
}
|
||||
|
||||
function handlePaste(
|
||||
e: ClipboardEvent,
|
||||
props: ChatProps,
|
||||
) {
|
||||
function handlePaste(e: ClipboardEvent, props: ChatProps) {
|
||||
const items = e.clipboardData?.items;
|
||||
if (!items || !props.onAttachmentsChange) return;
|
||||
|
||||
@@ -165,9 +159,7 @@ function renderAttachmentPreview(props: ChatProps) {
|
||||
type="button"
|
||||
aria-label="Remove attachment"
|
||||
@click=${() => {
|
||||
const next = (props.attachments ?? []).filter(
|
||||
(a) => a.id !== att.id,
|
||||
);
|
||||
const next = (props.attachments ?? []).filter((a) => a.id !== att.id);
|
||||
props.onAttachmentsChange?.(next);
|
||||
}}
|
||||
>
|
||||
@@ -184,9 +176,7 @@ export function renderChat(props: ChatProps) {
|
||||
const canCompose = props.connected;
|
||||
const isBusy = props.sending || props.stream !== null;
|
||||
const canAbort = Boolean(props.canAbort && props.onAbort);
|
||||
const activeSession = props.sessions?.sessions?.find(
|
||||
(row) => row.key === props.sessionKey,
|
||||
);
|
||||
const activeSession = props.sessions?.sessions?.find((row) => row.key === props.sessionKey);
|
||||
const reasoningLevel = activeSession?.reasoningLevel ?? "off";
|
||||
const showReasoning = props.showThinking && reasoningLevel !== "off";
|
||||
const assistantIdentity = {
|
||||
@@ -210,49 +200,56 @@ export function renderChat(props: ChatProps) {
|
||||
aria-live="polite"
|
||||
@scroll=${props.onChatScroll}
|
||||
>
|
||||
${props.loading ? html`<div class="muted">Loading chat…</div>` : nothing}
|
||||
${repeat(buildChatItems(props), (item) => item.key, (item) => {
|
||||
if (item.kind === "reading-indicator") {
|
||||
return renderReadingIndicatorGroup(assistantIdentity);
|
||||
}
|
||||
${
|
||||
props.loading
|
||||
? html`
|
||||
<div class="muted">Loading chat…</div>
|
||||
`
|
||||
: nothing
|
||||
}
|
||||
${repeat(
|
||||
buildChatItems(props),
|
||||
(item) => item.key,
|
||||
(item) => {
|
||||
if (item.kind === "reading-indicator") {
|
||||
return renderReadingIndicatorGroup(assistantIdentity);
|
||||
}
|
||||
|
||||
if (item.kind === "stream") {
|
||||
return renderStreamingGroup(
|
||||
item.text,
|
||||
item.startedAt,
|
||||
props.onOpenSidebar,
|
||||
assistantIdentity,
|
||||
);
|
||||
}
|
||||
if (item.kind === "stream") {
|
||||
return renderStreamingGroup(
|
||||
item.text,
|
||||
item.startedAt,
|
||||
props.onOpenSidebar,
|
||||
assistantIdentity,
|
||||
);
|
||||
}
|
||||
|
||||
if (item.kind === "group") {
|
||||
return renderMessageGroup(item, {
|
||||
onOpenSidebar: props.onOpenSidebar,
|
||||
showReasoning,
|
||||
assistantName: props.assistantName,
|
||||
assistantAvatar: assistantIdentity.avatar,
|
||||
});
|
||||
}
|
||||
if (item.kind === "group") {
|
||||
return renderMessageGroup(item, {
|
||||
onOpenSidebar: props.onOpenSidebar,
|
||||
showReasoning,
|
||||
assistantName: props.assistantName,
|
||||
assistantAvatar: assistantIdentity.avatar,
|
||||
});
|
||||
}
|
||||
|
||||
return nothing;
|
||||
})}
|
||||
return nothing;
|
||||
},
|
||||
)}
|
||||
</div>
|
||||
`;
|
||||
|
||||
return html`
|
||||
<section class="card chat">
|
||||
${props.disabledReason
|
||||
? html`<div class="callout">${props.disabledReason}</div>`
|
||||
: nothing}
|
||||
${props.disabledReason ? html`<div class="callout">${props.disabledReason}</div>` : nothing}
|
||||
|
||||
${props.error
|
||||
? html`<div class="callout danger">${props.error}</div>`
|
||||
: nothing}
|
||||
${props.error ? html`<div class="callout danger">${props.error}</div>` : nothing}
|
||||
|
||||
${renderCompactionIndicator(props.compactionStatus)}
|
||||
|
||||
${props.focusMode
|
||||
? html`
|
||||
${
|
||||
props.focusMode
|
||||
? html`
|
||||
<button
|
||||
class="chat-focus-exit"
|
||||
type="button"
|
||||
@@ -263,7 +260,8 @@ export function renderChat(props: ChatProps) {
|
||||
${icons.x}
|
||||
</button>
|
||||
`
|
||||
: nothing}
|
||||
: nothing
|
||||
}
|
||||
|
||||
<div
|
||||
class="chat-split-container ${sidebarOpen ? "chat-split-container--open" : ""}"
|
||||
@@ -275,12 +273,12 @@ export function renderChat(props: ChatProps) {
|
||||
${thread}
|
||||
</div>
|
||||
|
||||
${sidebarOpen
|
||||
? html`
|
||||
${
|
||||
sidebarOpen
|
||||
? html`
|
||||
<resizable-divider
|
||||
.splitRatio=${splitRatio}
|
||||
@resize=${(e: CustomEvent) =>
|
||||
props.onSplitRatioChange?.(e.detail.splitRatio)}
|
||||
@resize=${(e: CustomEvent) => props.onSplitRatioChange?.(e.detail.splitRatio)}
|
||||
></resizable-divider>
|
||||
<div class="chat-sidebar">
|
||||
${renderMarkdownSidebar({
|
||||
@@ -294,11 +292,13 @@ export function renderChat(props: ChatProps) {
|
||||
})}
|
||||
</div>
|
||||
`
|
||||
: nothing}
|
||||
: nothing
|
||||
}
|
||||
</div>
|
||||
|
||||
${props.queue.length
|
||||
? html`
|
||||
${
|
||||
props.queue.length
|
||||
? html`
|
||||
<div class="chat-queue" role="status" aria-live="polite">
|
||||
<div class="chat-queue__title">Queued (${props.queue.length})</div>
|
||||
<div class="chat-queue__list">
|
||||
@@ -306,10 +306,10 @@ export function renderChat(props: ChatProps) {
|
||||
(item) => html`
|
||||
<div class="chat-queue__item">
|
||||
<div class="chat-queue__text">
|
||||
${item.text ||
|
||||
(item.attachments?.length
|
||||
? `Image (${item.attachments.length})`
|
||||
: "")}
|
||||
${
|
||||
item.text ||
|
||||
(item.attachments?.length ? `Image (${item.attachments.length})` : "")
|
||||
}
|
||||
</div>
|
||||
<button
|
||||
class="btn chat-queue__remove"
|
||||
@@ -325,7 +325,8 @@ export function renderChat(props: ChatProps) {
|
||||
</div>
|
||||
</div>
|
||||
`
|
||||
: nothing}
|
||||
: nothing
|
||||
}
|
||||
|
||||
<div class="chat-compose">
|
||||
${renderAttachmentPreview(props)}
|
||||
|
||||
@@ -47,8 +47,7 @@ function normalizeSchemaNode(
|
||||
|
||||
const nullable = Array.isArray(schema.type) && schema.type.includes("null");
|
||||
const type =
|
||||
schemaType(schema) ??
|
||||
(schema.properties || schema.additionalProperties ? "object" : undefined);
|
||||
schemaType(schema) ?? (schema.properties || schema.additionalProperties ? "object" : undefined);
|
||||
normalized.type = type ?? schema.type;
|
||||
normalized.nullable = nullable || schema.nullable;
|
||||
|
||||
@@ -73,24 +72,15 @@ function normalizeSchemaNode(
|
||||
unsupported.add(pathLabel);
|
||||
} else if (schema.additionalProperties === false) {
|
||||
normalized.additionalProperties = false;
|
||||
} else if (
|
||||
schema.additionalProperties &&
|
||||
typeof schema.additionalProperties === "object"
|
||||
) {
|
||||
} else if (schema.additionalProperties && typeof schema.additionalProperties === "object") {
|
||||
if (!isAnySchema(schema.additionalProperties as JsonSchema)) {
|
||||
const res = normalizeSchemaNode(
|
||||
schema.additionalProperties as JsonSchema,
|
||||
[...path, "*"],
|
||||
);
|
||||
normalized.additionalProperties =
|
||||
res.schema ?? (schema.additionalProperties as JsonSchema);
|
||||
const res = normalizeSchemaNode(schema.additionalProperties as JsonSchema, [...path, "*"]);
|
||||
normalized.additionalProperties = res.schema ?? (schema.additionalProperties as JsonSchema);
|
||||
if (res.unsupportedPaths.length > 0) unsupported.add(pathLabel);
|
||||
}
|
||||
}
|
||||
} else if (type === "array") {
|
||||
const itemsSchema = Array.isArray(schema.items)
|
||||
? schema.items[0]
|
||||
: schema.items;
|
||||
const itemsSchema = Array.isArray(schema.items) ? schema.items[0] : schema.items;
|
||||
if (!itemsSchema) {
|
||||
unsupported.add(pathLabel);
|
||||
} else {
|
||||
|
||||
@@ -28,11 +28,69 @@ function jsonValue(value: unknown): string {
|
||||
|
||||
// SVG Icons as template literals
|
||||
const icons = {
|
||||
chevronDown: html`<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="6 9 12 15 18 9"></polyline></svg>`,
|
||||
plus: html`<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><line x1="12" y1="5" x2="12" y2="19"></line><line x1="5" y1="12" x2="19" y2="12"></line></svg>`,
|
||||
minus: html`<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><line x1="5" y1="12" x2="19" y2="12"></line></svg>`,
|
||||
trash: html`<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="3 6 5 6 21 6"></polyline><path d="M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6m3 0V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2"></path></svg>`,
|
||||
edit: html`<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M11 4H4a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2v-7"></path><path d="M18.5 2.5a2.121 2.121 0 0 1 3 3L12 15l-4 1 1-4 9.5-9.5z"></path></svg>`,
|
||||
chevronDown: html`
|
||||
<svg
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
>
|
||||
<polyline points="6 9 12 15 18 9"></polyline>
|
||||
</svg>
|
||||
`,
|
||||
plus: html`
|
||||
<svg
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
>
|
||||
<line x1="12" y1="5" x2="12" y2="19"></line>
|
||||
<line x1="5" y1="12" x2="19" y2="12"></line>
|
||||
</svg>
|
||||
`,
|
||||
minus: html`
|
||||
<svg
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
>
|
||||
<line x1="5" y1="12" x2="19" y2="12"></line>
|
||||
</svg>
|
||||
`,
|
||||
trash: html`
|
||||
<svg
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
>
|
||||
<polyline points="3 6 5 6 21 6"></polyline>
|
||||
<path d="M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6m3 0V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2"></path>
|
||||
</svg>
|
||||
`,
|
||||
edit: html`
|
||||
<svg
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
>
|
||||
<path d="M11 4H4a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2v-7"></path>
|
||||
<path d="M18.5 2.5a2.121 2.121 0 0 1 3 3L12 15l-4 1 1-4 9.5-9.5z"></path>
|
||||
</svg>
|
||||
`,
|
||||
};
|
||||
|
||||
export function renderNode(params: {
|
||||
@@ -64,7 +122,7 @@ export function renderNode(params: {
|
||||
if (schema.anyOf || schema.oneOf) {
|
||||
const variants = schema.anyOf ?? schema.oneOf ?? [];
|
||||
const nonNull = variants.filter(
|
||||
(v) => !(v.type === "null" || (Array.isArray(v.type) && v.type.includes("null")))
|
||||
(v) => !(v.type === "null" || (Array.isArray(v.type) && v.type.includes("null"))),
|
||||
);
|
||||
|
||||
if (nonNull.length === 1) {
|
||||
@@ -88,16 +146,18 @@ export function renderNode(params: {
|
||||
${showLabel ? html`<label class="cfg-field__label">${label}</label>` : nothing}
|
||||
${help ? html`<div class="cfg-field__help">${help}</div>` : nothing}
|
||||
<div class="cfg-segmented">
|
||||
${literals.map((lit, idx) => html`
|
||||
${literals.map(
|
||||
(lit, idx) => html`
|
||||
<button
|
||||
type="button"
|
||||
class="cfg-segmented__btn ${lit === resolvedValue || String(lit) === String(resolvedValue) ? 'active' : ''}"
|
||||
class="cfg-segmented__btn ${lit === resolvedValue || String(lit) === String(resolvedValue) ? "active" : ""}"
|
||||
?disabled=${disabled}
|
||||
@click=${() => onPatch(path, lit)}
|
||||
>
|
||||
${String(lit)}
|
||||
</button>
|
||||
`)}
|
||||
`,
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
@@ -109,11 +169,9 @@ export function renderNode(params: {
|
||||
}
|
||||
|
||||
// Handle mixed primitive types
|
||||
const primitiveTypes = new Set(
|
||||
nonNull.map((variant) => schemaType(variant)).filter(Boolean)
|
||||
);
|
||||
const primitiveTypes = new Set(nonNull.map((variant) => schemaType(variant)).filter(Boolean));
|
||||
const normalizedTypes = new Set(
|
||||
[...primitiveTypes].map((v) => (v === "integer" ? "number" : v))
|
||||
[...primitiveTypes].map((v) => (v === "integer" ? "number" : v)),
|
||||
);
|
||||
|
||||
if ([...normalizedTypes].every((v) => ["string", "number", "boolean"].includes(v as string))) {
|
||||
@@ -147,16 +205,18 @@ export function renderNode(params: {
|
||||
${showLabel ? html`<label class="cfg-field__label">${label}</label>` : nothing}
|
||||
${help ? html`<div class="cfg-field__help">${help}</div>` : nothing}
|
||||
<div class="cfg-segmented">
|
||||
${options.map((opt) => html`
|
||||
${options.map(
|
||||
(opt) => html`
|
||||
<button
|
||||
type="button"
|
||||
class="cfg-segmented__btn ${opt === resolvedValue || String(opt) === String(resolvedValue) ? 'active' : ''}"
|
||||
class="cfg-segmented__btn ${opt === resolvedValue || String(opt) === String(resolvedValue) ? "active" : ""}"
|
||||
?disabled=${disabled}
|
||||
@click=${() => onPatch(path, opt)}
|
||||
>
|
||||
${String(opt)}
|
||||
</button>
|
||||
`)}
|
||||
`,
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
@@ -176,9 +236,14 @@ export function renderNode(params: {
|
||||
|
||||
// Boolean - toggle row
|
||||
if (type === "boolean") {
|
||||
const displayValue = typeof value === "boolean" ? value : typeof schema.default === "boolean" ? schema.default : false;
|
||||
const displayValue =
|
||||
typeof value === "boolean"
|
||||
? value
|
||||
: typeof schema.default === "boolean"
|
||||
? schema.default
|
||||
: false;
|
||||
return html`
|
||||
<label class="cfg-toggle-row ${disabled ? 'disabled' : ''}">
|
||||
<label class="cfg-toggle-row ${disabled ? "disabled" : ""}">
|
||||
<div class="cfg-toggle-row__content">
|
||||
<span class="cfg-toggle-row__label">${label}</span>
|
||||
${help ? html`<span class="cfg-toggle-row__help">${help}</span>` : nothing}
|
||||
@@ -266,7 +331,9 @@ function renderTextInput(params: {
|
||||
onPatch(path, raw.trim());
|
||||
}}
|
||||
/>
|
||||
${schema.default !== undefined ? html`
|
||||
${
|
||||
schema.default !== undefined
|
||||
? html`
|
||||
<button
|
||||
type="button"
|
||||
class="cfg-input__reset"
|
||||
@@ -274,7 +341,9 @@ function renderTextInput(params: {
|
||||
?disabled=${disabled}
|
||||
@click=${() => onPatch(path, schema.default)}
|
||||
>↺</button>
|
||||
` : nothing}
|
||||
`
|
||||
: nothing
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
@@ -365,9 +434,11 @@ function renderSelect(params: {
|
||||
}}
|
||||
>
|
||||
<option value=${unset}>Select...</option>
|
||||
${options.map((opt, idx) => html`
|
||||
${options.map(
|
||||
(opt, idx) => html`
|
||||
<option value=${String(idx)}>${String(opt)}</option>
|
||||
`)}
|
||||
`,
|
||||
)}
|
||||
</select>
|
||||
</div>
|
||||
`;
|
||||
@@ -390,9 +461,10 @@ function renderObject(params: {
|
||||
const help = hint?.help ?? schema.description;
|
||||
|
||||
const fallback = value ?? schema.default;
|
||||
const obj = fallback && typeof fallback === "object" && !Array.isArray(fallback)
|
||||
? (fallback as Record<string, unknown>)
|
||||
: {};
|
||||
const obj =
|
||||
fallback && typeof fallback === "object" && !Array.isArray(fallback)
|
||||
? (fallback as Record<string, unknown>)
|
||||
: {};
|
||||
const props = schema.properties ?? {};
|
||||
const entries = Object.entries(props);
|
||||
|
||||
@@ -421,18 +493,22 @@ function renderObject(params: {
|
||||
unsupported,
|
||||
disabled,
|
||||
onPatch,
|
||||
})
|
||||
}),
|
||||
)}
|
||||
${allowExtra ? renderMapField({
|
||||
schema: additional as JsonSchema,
|
||||
value: obj,
|
||||
path,
|
||||
hints,
|
||||
unsupported,
|
||||
disabled,
|
||||
reservedKeys: reserved,
|
||||
onPatch,
|
||||
}) : nothing}
|
||||
${
|
||||
allowExtra
|
||||
? renderMapField({
|
||||
schema: additional as JsonSchema,
|
||||
value: obj,
|
||||
path,
|
||||
hints,
|
||||
unsupported,
|
||||
disabled,
|
||||
reservedKeys: reserved,
|
||||
onPatch,
|
||||
})
|
||||
: nothing
|
||||
}
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
@@ -455,18 +531,22 @@ function renderObject(params: {
|
||||
unsupported,
|
||||
disabled,
|
||||
onPatch,
|
||||
})
|
||||
}),
|
||||
)}
|
||||
${allowExtra ? renderMapField({
|
||||
schema: additional as JsonSchema,
|
||||
value: obj,
|
||||
path,
|
||||
hints,
|
||||
unsupported,
|
||||
disabled,
|
||||
reservedKeys: reserved,
|
||||
onPatch,
|
||||
}) : nothing}
|
||||
${
|
||||
allowExtra
|
||||
? renderMapField({
|
||||
schema: additional as JsonSchema,
|
||||
value: obj,
|
||||
path,
|
||||
hints,
|
||||
unsupported,
|
||||
disabled,
|
||||
reservedKeys: reserved,
|
||||
onPatch,
|
||||
})
|
||||
: nothing
|
||||
}
|
||||
</div>
|
||||
</details>
|
||||
`;
|
||||
@@ -504,7 +584,7 @@ function renderArray(params: {
|
||||
<div class="cfg-array">
|
||||
<div class="cfg-array__header">
|
||||
${showLabel ? html`<span class="cfg-array__label">${label}</span>` : nothing}
|
||||
<span class="cfg-array__count">${arr.length} item${arr.length !== 1 ? 's' : ''}</span>
|
||||
<span class="cfg-array__count">${arr.length} item${arr.length !== 1 ? "s" : ""}</span>
|
||||
<button
|
||||
type="button"
|
||||
class="cfg-array__add"
|
||||
@@ -520,13 +600,15 @@ function renderArray(params: {
|
||||
</div>
|
||||
${help ? html`<div class="cfg-array__help">${help}</div>` : nothing}
|
||||
|
||||
${arr.length === 0 ? html`
|
||||
<div class="cfg-array__empty">
|
||||
No items yet. Click "Add" to create one.
|
||||
</div>
|
||||
` : html`
|
||||
${
|
||||
arr.length === 0
|
||||
? html`
|
||||
<div class="cfg-array__empty">No items yet. Click "Add" to create one.</div>
|
||||
`
|
||||
: html`
|
||||
<div class="cfg-array__items">
|
||||
${arr.map((item, idx) => html`
|
||||
${arr.map(
|
||||
(item, idx) => html`
|
||||
<div class="cfg-array__item">
|
||||
<div class="cfg-array__item-header">
|
||||
<span class="cfg-array__item-index">#${idx + 1}</span>
|
||||
@@ -557,9 +639,11 @@ function renderArray(params: {
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
`)}
|
||||
`,
|
||||
)}
|
||||
</div>
|
||||
`}
|
||||
`
|
||||
}
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
@@ -603,9 +687,12 @@ function renderMapField(params: {
|
||||
</button>
|
||||
</div>
|
||||
|
||||
${entries.length === 0 ? html`
|
||||
<div class="cfg-map__empty">No custom entries.</div>
|
||||
` : html`
|
||||
${
|
||||
entries.length === 0
|
||||
? html`
|
||||
<div class="cfg-map__empty">No custom entries.</div>
|
||||
`
|
||||
: html`
|
||||
<div class="cfg-map__items">
|
||||
${entries.map(([key, entryValue]) => {
|
||||
const valuePath = [...path, key];
|
||||
@@ -631,8 +718,9 @@ function renderMapField(params: {
|
||||
/>
|
||||
</div>
|
||||
<div class="cfg-map__item-value">
|
||||
${anySchema
|
||||
? html`
|
||||
${
|
||||
anySchema
|
||||
? html`
|
||||
<textarea
|
||||
class="cfg-textarea cfg-textarea--sm"
|
||||
placeholder="JSON value"
|
||||
@@ -654,16 +742,17 @@ function renderMapField(params: {
|
||||
}}
|
||||
></textarea>
|
||||
`
|
||||
: renderNode({
|
||||
schema,
|
||||
value: entryValue,
|
||||
path: valuePath,
|
||||
hints,
|
||||
unsupported,
|
||||
disabled,
|
||||
showLabel: false,
|
||||
onPatch,
|
||||
})}
|
||||
: renderNode({
|
||||
schema,
|
||||
value: entryValue,
|
||||
path: valuePath,
|
||||
hints,
|
||||
unsupported,
|
||||
disabled,
|
||||
showLabel: false,
|
||||
onPatch,
|
||||
})
|
||||
}
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
@@ -682,7 +771,8 @@ function renderMapField(params: {
|
||||
`;
|
||||
})}
|
||||
</div>
|
||||
`}
|
||||
`
|
||||
}
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
@@ -1,12 +1,7 @@
|
||||
import { html, nothing } from "lit";
|
||||
import type { ConfigUiHints } from "../types";
|
||||
import { icons } from "../icons";
|
||||
import {
|
||||
hintForPath,
|
||||
humanize,
|
||||
schemaType,
|
||||
type JsonSchema,
|
||||
} from "./config-form.shared";
|
||||
import { hintForPath, humanize, schemaType, type JsonSchema } from "./config-form.shared";
|
||||
import { renderNode } from "./config-form.node";
|
||||
|
||||
export type ConfigFormProps = {
|
||||
@@ -23,44 +18,237 @@ export type ConfigFormProps = {
|
||||
|
||||
// SVG Icons for section cards (Lucide-style)
|
||||
const sectionIcons = {
|
||||
env: html`<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5"><circle cx="12" cy="12" r="3"></circle><path d="M19.4 15a1.65 1.65 0 0 0 .33 1.82l.06.06a2 2 0 0 1 0 2.83 2 2 0 0 1-2.83 0l-.06-.06a1.65 1.65 0 0 0-1.82-.33 1.65 1.65 0 0 0-1 1.51V21a2 2 0 0 1-2 2 2 2 0 0 1-2-2v-.09A1.65 1.65 0 0 0 9 19.4a1.65 1.65 0 0 0-1.82.33l-.06.06a2 2 0 0 1-2.83 0 2 2 0 0 1 0-2.83l.06-.06a1.65 1.65 0 0 0 .33-1.82 1.65 1.65 0 0 0-1.51-1H3a2 2 0 0 1-2-2 2 2 0 0 1 2-2h.09A1.65 1.65 0 0 0 4.6 9a1.65 1.65 0 0 0-.33-1.82l-.06-.06a2 2 0 0 1 0-2.83 2 2 0 0 1 2.83 0l.06.06a1.65 1.65 0 0 0 1.82.33H9a1.65 1.65 0 0 0 1-1.51V3a2 2 0 0 1 2-2 2 2 0 0 1 2 2v.09a1.65 1.65 0 0 0 1 1.51 1.65 1.65 0 0 0 1.82-.33l.06-.06a2 2 0 0 1 2.83 0 2 2 0 0 1 0 2.83l-.06.06a1.65 1.65 0 0 0-.33 1.82V9a1.65 1.65 0 0 0 1.51 1H21a2 2 0 0 1 2 2 2 2 0 0 1-2 2h-.09a1.65 1.65 0 0 0-1.51 1z"></path></svg>`,
|
||||
update: html`<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5"><path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"></path><polyline points="7 10 12 15 17 10"></polyline><line x1="12" y1="15" x2="12" y2="3"></line></svg>`,
|
||||
agents: html`<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5"><path d="M12 2a2 2 0 0 1 2 2c0 .74-.4 1.39-1 1.73V7h1a7 7 0 0 1 7 7h1a1 1 0 0 1 1 1v3a1 1 0 0 1-1 1h-1v1a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-1H2a1 1 0 0 1-1-1v-3a1 1 0 0 1 1-1h1a7 7 0 0 1 7-7h1V5.73c-.6-.34-1-.99-1-1.73a2 2 0 0 1 2-2z"></path><circle cx="8" cy="14" r="1"></circle><circle cx="16" cy="14" r="1"></circle></svg>`,
|
||||
auth: html`<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5"><rect x="3" y="11" width="18" height="11" rx="2" ry="2"></rect><path d="M7 11V7a5 5 0 0 1 10 0v4"></path></svg>`,
|
||||
channels: html`<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5"><path d="M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z"></path></svg>`,
|
||||
messages: html`<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5"><path d="M4 4h16c1.1 0 2 .9 2 2v12c0 1.1-.9 2-2 2H4c-1.1 0-2-.9-2-2V6c0-1.1.9-2 2-2z"></path><polyline points="22,6 12,13 2,6"></polyline></svg>`,
|
||||
commands: html`<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5"><polyline points="4 17 10 11 4 5"></polyline><line x1="12" y1="19" x2="20" y2="19"></line></svg>`,
|
||||
hooks: html`<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5"><path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71"></path><path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71"></path></svg>`,
|
||||
skills: html`<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5"><polygon points="12 2 15.09 8.26 22 9.27 17 14.14 18.18 21.02 12 17.77 5.82 21.02 7 14.14 2 9.27 8.91 8.26 12 2"></polygon></svg>`,
|
||||
tools: html`<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5"><path d="M14.7 6.3a1 1 0 0 0 0 1.4l1.6 1.6a1 1 0 0 0 1.4 0l3.77-3.77a6 6 0 0 1-7.94 7.94l-6.91 6.91a2.12 2.12 0 0 1-3-3l6.91-6.91a6 6 0 0 1 7.94-7.94l-3.76 3.76z"></path></svg>`,
|
||||
gateway: html`<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5"><circle cx="12" cy="12" r="10"></circle><line x1="2" y1="12" x2="22" y2="12"></line><path d="M12 2a15.3 15.3 0 0 1 4 10 15.3 15.3 0 0 1-4 10 15.3 15.3 0 0 1-4-10 15.3 15.3 0 0 1 4-10z"></path></svg>`,
|
||||
wizard: html`<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5"><path d="M15 4V2"></path><path d="M15 16v-2"></path><path d="M8 9h2"></path><path d="M20 9h2"></path><path d="M17.8 11.8 19 13"></path><path d="M15 9h0"></path><path d="M17.8 6.2 19 5"></path><path d="m3 21 9-9"></path><path d="M12.2 6.2 11 5"></path></svg>`,
|
||||
env: html`
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5">
|
||||
<circle cx="12" cy="12" r="3"></circle>
|
||||
<path
|
||||
d="M19.4 15a1.65 1.65 0 0 0 .33 1.82l.06.06a2 2 0 0 1 0 2.83 2 2 0 0 1-2.83 0l-.06-.06a1.65 1.65 0 0 0-1.82-.33 1.65 1.65 0 0 0-1 1.51V21a2 2 0 0 1-2 2 2 2 0 0 1-2-2v-.09A1.65 1.65 0 0 0 9 19.4a1.65 1.65 0 0 0-1.82.33l-.06.06a2 2 0 0 1-2.83 0 2 2 0 0 1 0-2.83l.06-.06a1.65 1.65 0 0 0 .33-1.82 1.65 1.65 0 0 0-1.51-1H3a2 2 0 0 1-2-2 2 2 0 0 1 2-2h.09A1.65 1.65 0 0 0 4.6 9a1.65 1.65 0 0 0-.33-1.82l-.06-.06a2 2 0 0 1 0-2.83 2 2 0 0 1 2.83 0l.06.06a1.65 1.65 0 0 0 1.82.33H9a1.65 1.65 0 0 0 1-1.51V3a2 2 0 0 1 2-2 2 2 0 0 1 2 2v.09a1.65 1.65 0 0 0 1 1.51 1.65 1.65 0 0 0 1.82-.33l.06-.06a2 2 0 0 1 2.83 0 2 2 0 0 1 0 2.83l-.06.06a1.65 1.65 0 0 0-.33 1.82V9a1.65 1.65 0 0 0 1.51 1H21a2 2 0 0 1 2 2 2 2 0 0 1-2 2h-.09a1.65 1.65 0 0 0-1.51 1z"
|
||||
></path>
|
||||
</svg>
|
||||
`,
|
||||
update: html`
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5">
|
||||
<path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"></path>
|
||||
<polyline points="7 10 12 15 17 10"></polyline>
|
||||
<line x1="12" y1="15" x2="12" y2="3"></line>
|
||||
</svg>
|
||||
`,
|
||||
agents: html`
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5">
|
||||
<path
|
||||
d="M12 2a2 2 0 0 1 2 2c0 .74-.4 1.39-1 1.73V7h1a7 7 0 0 1 7 7h1a1 1 0 0 1 1 1v3a1 1 0 0 1-1 1h-1v1a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-1H2a1 1 0 0 1-1-1v-3a1 1 0 0 1 1-1h1a7 7 0 0 1 7-7h1V5.73c-.6-.34-1-.99-1-1.73a2 2 0 0 1 2-2z"
|
||||
></path>
|
||||
<circle cx="8" cy="14" r="1"></circle>
|
||||
<circle cx="16" cy="14" r="1"></circle>
|
||||
</svg>
|
||||
`,
|
||||
auth: html`
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5">
|
||||
<rect x="3" y="11" width="18" height="11" rx="2" ry="2"></rect>
|
||||
<path d="M7 11V7a5 5 0 0 1 10 0v4"></path>
|
||||
</svg>
|
||||
`,
|
||||
channels: html`
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5">
|
||||
<path d="M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z"></path>
|
||||
</svg>
|
||||
`,
|
||||
messages: html`
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5">
|
||||
<path d="M4 4h16c1.1 0 2 .9 2 2v12c0 1.1-.9 2-2 2H4c-1.1 0-2-.9-2-2V6c0-1.1.9-2 2-2z"></path>
|
||||
<polyline points="22,6 12,13 2,6"></polyline>
|
||||
</svg>
|
||||
`,
|
||||
commands: html`
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5">
|
||||
<polyline points="4 17 10 11 4 5"></polyline>
|
||||
<line x1="12" y1="19" x2="20" y2="19"></line>
|
||||
</svg>
|
||||
`,
|
||||
hooks: html`
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5">
|
||||
<path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71"></path>
|
||||
<path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71"></path>
|
||||
</svg>
|
||||
`,
|
||||
skills: html`
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5">
|
||||
<polygon
|
||||
points="12 2 15.09 8.26 22 9.27 17 14.14 18.18 21.02 12 17.77 5.82 21.02 7 14.14 2 9.27 8.91 8.26 12 2"
|
||||
></polygon>
|
||||
</svg>
|
||||
`,
|
||||
tools: html`
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5">
|
||||
<path
|
||||
d="M14.7 6.3a1 1 0 0 0 0 1.4l1.6 1.6a1 1 0 0 0 1.4 0l3.77-3.77a6 6 0 0 1-7.94 7.94l-6.91 6.91a2.12 2.12 0 0 1-3-3l6.91-6.91a6 6 0 0 1 7.94-7.94l-3.76 3.76z"
|
||||
></path>
|
||||
</svg>
|
||||
`,
|
||||
gateway: html`
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5">
|
||||
<circle cx="12" cy="12" r="10"></circle>
|
||||
<line x1="2" y1="12" x2="22" y2="12"></line>
|
||||
<path
|
||||
d="M12 2a15.3 15.3 0 0 1 4 10 15.3 15.3 0 0 1-4 10 15.3 15.3 0 0 1-4-10 15.3 15.3 0 0 1 4-10z"
|
||||
></path>
|
||||
</svg>
|
||||
`,
|
||||
wizard: html`
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5">
|
||||
<path d="M15 4V2"></path>
|
||||
<path d="M15 16v-2"></path>
|
||||
<path d="M8 9h2"></path>
|
||||
<path d="M20 9h2"></path>
|
||||
<path d="M17.8 11.8 19 13"></path>
|
||||
<path d="M15 9h0"></path>
|
||||
<path d="M17.8 6.2 19 5"></path>
|
||||
<path d="m3 21 9-9"></path>
|
||||
<path d="M12.2 6.2 11 5"></path>
|
||||
</svg>
|
||||
`,
|
||||
// Additional sections
|
||||
meta: html`<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5"><path d="M12 20h9"></path><path d="M16.5 3.5a2.12 2.12 0 0 1 3 3L7 19l-4 1 1-4Z"></path></svg>`,
|
||||
logging: html`<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5"><path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"></path><polyline points="14 2 14 8 20 8"></polyline><line x1="16" y1="13" x2="8" y2="13"></line><line x1="16" y1="17" x2="8" y2="17"></line><polyline points="10 9 9 9 8 9"></polyline></svg>`,
|
||||
browser: html`<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5"><circle cx="12" cy="12" r="10"></circle><circle cx="12" cy="12" r="4"></circle><line x1="21.17" y1="8" x2="12" y2="8"></line><line x1="3.95" y1="6.06" x2="8.54" y2="14"></line><line x1="10.88" y1="21.94" x2="15.46" y2="14"></line></svg>`,
|
||||
ui: html`<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5"><rect x="3" y="3" width="18" height="18" rx="2" ry="2"></rect><line x1="3" y1="9" x2="21" y2="9"></line><line x1="9" y1="21" x2="9" y2="9"></line></svg>`,
|
||||
models: html`<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5"><path d="M21 16V8a2 2 0 0 0-1-1.73l-7-4a2 2 0 0 0-2 0l-7 4A2 2 0 0 0 3 8v8a2 2 0 0 0 1 1.73l7 4a2 2 0 0 0 2 0l7-4A2 2 0 0 0 21 16z"></path><polyline points="3.27 6.96 12 12.01 20.73 6.96"></polyline><line x1="12" y1="22.08" x2="12" y2="12"></line></svg>`,
|
||||
bindings: html`<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5"><rect x="2" y="2" width="20" height="8" rx="2" ry="2"></rect><rect x="2" y="14" width="20" height="8" rx="2" ry="2"></rect><line x1="6" y1="6" x2="6.01" y2="6"></line><line x1="6" y1="18" x2="6.01" y2="18"></line></svg>`,
|
||||
broadcast: html`<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5"><path d="M4.9 19.1C1 15.2 1 8.8 4.9 4.9"></path><path d="M7.8 16.2c-2.3-2.3-2.3-6.1 0-8.5"></path><circle cx="12" cy="12" r="2"></circle><path d="M16.2 7.8c2.3 2.3 2.3 6.1 0 8.5"></path><path d="M19.1 4.9C23 8.8 23 15.1 19.1 19"></path></svg>`,
|
||||
audio: html`<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5"><path d="M9 18V5l12-2v13"></path><circle cx="6" cy="18" r="3"></circle><circle cx="18" cy="16" r="3"></circle></svg>`,
|
||||
session: html`<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5"><path d="M17 21v-2a4 4 0 0 0-4-4H5a4 4 0 0 0-4 4v2"></path><circle cx="9" cy="7" r="4"></circle><path d="M23 21v-2a4 4 0 0 0-3-3.87"></path><path d="M16 3.13a4 4 0 0 1 0 7.75"></path></svg>`,
|
||||
cron: html`<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5"><circle cx="12" cy="12" r="10"></circle><polyline points="12 6 12 12 16 14"></polyline></svg>`,
|
||||
web: html`<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5"><circle cx="12" cy="12" r="10"></circle><line x1="2" y1="12" x2="22" y2="12"></line><path d="M12 2a15.3 15.3 0 0 1 4 10 15.3 15.3 0 0 1-4 10 15.3 15.3 0 0 1-4-10 15.3 15.3 0 0 1 4-10z"></path></svg>`,
|
||||
discovery: html`<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5"><circle cx="11" cy="11" r="8"></circle><line x1="21" y1="21" x2="16.65" y2="16.65"></line></svg>`,
|
||||
canvasHost: html`<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5"><rect x="3" y="3" width="18" height="18" rx="2" ry="2"></rect><circle cx="8.5" cy="8.5" r="1.5"></circle><polyline points="21 15 16 10 5 21"></polyline></svg>`,
|
||||
talk: html`<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5"><path d="M12 1a3 3 0 0 0-3 3v8a3 3 0 0 0 6 0V4a3 3 0 0 0-3-3z"></path><path d="M19 10v2a7 7 0 0 1-14 0v-2"></path><line x1="12" y1="19" x2="12" y2="23"></line><line x1="8" y1="23" x2="16" y2="23"></line></svg>`,
|
||||
plugins: html`<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5"><path d="M12 2v6"></path><path d="m4.93 10.93 4.24 4.24"></path><path d="M2 12h6"></path><path d="m4.93 13.07 4.24-4.24"></path><path d="M12 22v-6"></path><path d="m19.07 13.07-4.24-4.24"></path><path d="M22 12h-6"></path><path d="m19.07 10.93-4.24 4.24"></path></svg>`,
|
||||
default: html`<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5"><path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"></path><polyline points="14 2 14 8 20 8"></polyline></svg>`,
|
||||
meta: html`
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5">
|
||||
<path d="M12 20h9"></path>
|
||||
<path d="M16.5 3.5a2.12 2.12 0 0 1 3 3L7 19l-4 1 1-4Z"></path>
|
||||
</svg>
|
||||
`,
|
||||
logging: html`
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5">
|
||||
<path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"></path>
|
||||
<polyline points="14 2 14 8 20 8"></polyline>
|
||||
<line x1="16" y1="13" x2="8" y2="13"></line>
|
||||
<line x1="16" y1="17" x2="8" y2="17"></line>
|
||||
<polyline points="10 9 9 9 8 9"></polyline>
|
||||
</svg>
|
||||
`,
|
||||
browser: html`
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5">
|
||||
<circle cx="12" cy="12" r="10"></circle>
|
||||
<circle cx="12" cy="12" r="4"></circle>
|
||||
<line x1="21.17" y1="8" x2="12" y2="8"></line>
|
||||
<line x1="3.95" y1="6.06" x2="8.54" y2="14"></line>
|
||||
<line x1="10.88" y1="21.94" x2="15.46" y2="14"></line>
|
||||
</svg>
|
||||
`,
|
||||
ui: html`
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5">
|
||||
<rect x="3" y="3" width="18" height="18" rx="2" ry="2"></rect>
|
||||
<line x1="3" y1="9" x2="21" y2="9"></line>
|
||||
<line x1="9" y1="21" x2="9" y2="9"></line>
|
||||
</svg>
|
||||
`,
|
||||
models: html`
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5">
|
||||
<path
|
||||
d="M21 16V8a2 2 0 0 0-1-1.73l-7-4a2 2 0 0 0-2 0l-7 4A2 2 0 0 0 3 8v8a2 2 0 0 0 1 1.73l7 4a2 2 0 0 0 2 0l7-4A2 2 0 0 0 21 16z"
|
||||
></path>
|
||||
<polyline points="3.27 6.96 12 12.01 20.73 6.96"></polyline>
|
||||
<line x1="12" y1="22.08" x2="12" y2="12"></line>
|
||||
</svg>
|
||||
`,
|
||||
bindings: html`
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5">
|
||||
<rect x="2" y="2" width="20" height="8" rx="2" ry="2"></rect>
|
||||
<rect x="2" y="14" width="20" height="8" rx="2" ry="2"></rect>
|
||||
<line x1="6" y1="6" x2="6.01" y2="6"></line>
|
||||
<line x1="6" y1="18" x2="6.01" y2="18"></line>
|
||||
</svg>
|
||||
`,
|
||||
broadcast: html`
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5">
|
||||
<path d="M4.9 19.1C1 15.2 1 8.8 4.9 4.9"></path>
|
||||
<path d="M7.8 16.2c-2.3-2.3-2.3-6.1 0-8.5"></path>
|
||||
<circle cx="12" cy="12" r="2"></circle>
|
||||
<path d="M16.2 7.8c2.3 2.3 2.3 6.1 0 8.5"></path>
|
||||
<path d="M19.1 4.9C23 8.8 23 15.1 19.1 19"></path>
|
||||
</svg>
|
||||
`,
|
||||
audio: html`
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5">
|
||||
<path d="M9 18V5l12-2v13"></path>
|
||||
<circle cx="6" cy="18" r="3"></circle>
|
||||
<circle cx="18" cy="16" r="3"></circle>
|
||||
</svg>
|
||||
`,
|
||||
session: html`
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5">
|
||||
<path d="M17 21v-2a4 4 0 0 0-4-4H5a4 4 0 0 0-4 4v2"></path>
|
||||
<circle cx="9" cy="7" r="4"></circle>
|
||||
<path d="M23 21v-2a4 4 0 0 0-3-3.87"></path>
|
||||
<path d="M16 3.13a4 4 0 0 1 0 7.75"></path>
|
||||
</svg>
|
||||
`,
|
||||
cron: html`
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5">
|
||||
<circle cx="12" cy="12" r="10"></circle>
|
||||
<polyline points="12 6 12 12 16 14"></polyline>
|
||||
</svg>
|
||||
`,
|
||||
web: html`
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5">
|
||||
<circle cx="12" cy="12" r="10"></circle>
|
||||
<line x1="2" y1="12" x2="22" y2="12"></line>
|
||||
<path
|
||||
d="M12 2a15.3 15.3 0 0 1 4 10 15.3 15.3 0 0 1-4 10 15.3 15.3 0 0 1-4-10 15.3 15.3 0 0 1 4-10z"
|
||||
></path>
|
||||
</svg>
|
||||
`,
|
||||
discovery: html`
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5">
|
||||
<circle cx="11" cy="11" r="8"></circle>
|
||||
<line x1="21" y1="21" x2="16.65" y2="16.65"></line>
|
||||
</svg>
|
||||
`,
|
||||
canvasHost: html`
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5">
|
||||
<rect x="3" y="3" width="18" height="18" rx="2" ry="2"></rect>
|
||||
<circle cx="8.5" cy="8.5" r="1.5"></circle>
|
||||
<polyline points="21 15 16 10 5 21"></polyline>
|
||||
</svg>
|
||||
`,
|
||||
talk: html`
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5">
|
||||
<path d="M12 1a3 3 0 0 0-3 3v8a3 3 0 0 0 6 0V4a3 3 0 0 0-3-3z"></path>
|
||||
<path d="M19 10v2a7 7 0 0 1-14 0v-2"></path>
|
||||
<line x1="12" y1="19" x2="12" y2="23"></line>
|
||||
<line x1="8" y1="23" x2="16" y2="23"></line>
|
||||
</svg>
|
||||
`,
|
||||
plugins: html`
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5">
|
||||
<path d="M12 2v6"></path>
|
||||
<path d="m4.93 10.93 4.24 4.24"></path>
|
||||
<path d="M2 12h6"></path>
|
||||
<path d="m4.93 13.07 4.24-4.24"></path>
|
||||
<path d="M12 22v-6"></path>
|
||||
<path d="m19.07 13.07-4.24-4.24"></path>
|
||||
<path d="M22 12h-6"></path>
|
||||
<path d="m19.07 10.93-4.24 4.24"></path>
|
||||
</svg>
|
||||
`,
|
||||
default: html`
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5">
|
||||
<path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"></path>
|
||||
<polyline points="14 2 14 8 20 8"></polyline>
|
||||
</svg>
|
||||
`,
|
||||
};
|
||||
|
||||
// Section metadata
|
||||
export const SECTION_META: Record<string, { label: string; description: string }> = {
|
||||
env: { label: "Environment Variables", description: "Environment variables passed to the gateway process" },
|
||||
env: {
|
||||
label: "Environment Variables",
|
||||
description: "Environment variables passed to the gateway process",
|
||||
},
|
||||
update: { label: "Updates", description: "Auto-update settings and release channel" },
|
||||
agents: { label: "Agents", description: "Agent configurations, models, and identities" },
|
||||
auth: { label: "Authentication", description: "API keys and authentication profiles" },
|
||||
channels: { label: "Channels", description: "Messaging channels (Telegram, Discord, Slack, etc.)" },
|
||||
channels: {
|
||||
label: "Channels",
|
||||
description: "Messaging channels (Telegram, Discord, Slack, etc.)",
|
||||
},
|
||||
messages: { label: "Messages", description: "Message handling and routing settings" },
|
||||
commands: { label: "Commands", description: "Custom slash commands" },
|
||||
hooks: { label: "Hooks", description: "Webhooks and event hooks" },
|
||||
@@ -142,12 +330,16 @@ function schemaMatches(schema: JsonSchema, query: string): boolean {
|
||||
|
||||
export function renderConfigForm(props: ConfigFormProps) {
|
||||
if (!props.schema) {
|
||||
return html`<div class="muted">Schema unavailable.</div>`;
|
||||
return html`
|
||||
<div class="muted">Schema unavailable.</div>
|
||||
`;
|
||||
}
|
||||
const schema = props.schema;
|
||||
const value = props.value ?? {};
|
||||
if (schemaType(schema) !== "object" || !schema.properties) {
|
||||
return html`<div class="callout danger">Unsupported schema. Use Raw.</div>`;
|
||||
return html`
|
||||
<div class="callout danger">Unsupported schema. Use Raw.</div>
|
||||
`;
|
||||
}
|
||||
const unsupported = new Set(props.unsupportedPaths ?? []);
|
||||
const properties = schema.properties;
|
||||
@@ -168,9 +360,8 @@ export function renderConfigForm(props: ConfigFormProps) {
|
||||
return true;
|
||||
});
|
||||
|
||||
let subsectionContext:
|
||||
| { sectionKey: string; subsectionKey: string; schema: JsonSchema }
|
||||
| null = null;
|
||||
let subsectionContext: { sectionKey: string; subsectionKey: string; schema: JsonSchema } | null =
|
||||
null;
|
||||
if (activeSection && activeSubsection && filteredEntries.length === 1) {
|
||||
const sectionSchema = filteredEntries[0]?.[1];
|
||||
if (
|
||||
@@ -192,9 +383,7 @@ export function renderConfigForm(props: ConfigFormProps) {
|
||||
<div class="config-empty">
|
||||
<div class="config-empty__icon">${icons.search}</div>
|
||||
<div class="config-empty__text">
|
||||
${searchQuery
|
||||
? `No settings match "${searchQuery}"`
|
||||
: "No settings in this section"}
|
||||
${searchQuery ? `No settings match "${searchQuery}"` : "No settings in this section"}
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
@@ -202,27 +391,30 @@ export function renderConfigForm(props: ConfigFormProps) {
|
||||
|
||||
return html`
|
||||
<div class="config-form config-form--modern">
|
||||
${subsectionContext
|
||||
? (() => {
|
||||
const { sectionKey, subsectionKey, schema: node } = subsectionContext;
|
||||
const hint = hintForPath([sectionKey, subsectionKey], props.uiHints);
|
||||
const label = hint?.label ?? node.title ?? humanize(subsectionKey);
|
||||
const description = hint?.help ?? node.description ?? "";
|
||||
const sectionValue = (value as Record<string, unknown>)[sectionKey];
|
||||
const scopedValue =
|
||||
sectionValue && typeof sectionValue === "object"
|
||||
? (sectionValue as Record<string, unknown>)[subsectionKey]
|
||||
: undefined;
|
||||
const id = `config-section-${sectionKey}-${subsectionKey}`;
|
||||
return html`
|
||||
${
|
||||
subsectionContext
|
||||
? (() => {
|
||||
const { sectionKey, subsectionKey, schema: node } = subsectionContext;
|
||||
const hint = hintForPath([sectionKey, subsectionKey], props.uiHints);
|
||||
const label = hint?.label ?? node.title ?? humanize(subsectionKey);
|
||||
const description = hint?.help ?? node.description ?? "";
|
||||
const sectionValue = (value as Record<string, unknown>)[sectionKey];
|
||||
const scopedValue =
|
||||
sectionValue && typeof sectionValue === "object"
|
||||
? (sectionValue as Record<string, unknown>)[subsectionKey]
|
||||
: undefined;
|
||||
const id = `config-section-${sectionKey}-${subsectionKey}`;
|
||||
return html`
|
||||
<section class="config-section-card" id=${id}>
|
||||
<div class="config-section-card__header">
|
||||
<span class="config-section-card__icon">${getSectionIcon(sectionKey)}</span>
|
||||
<div class="config-section-card__titles">
|
||||
<h3 class="config-section-card__title">${label}</h3>
|
||||
${description
|
||||
? html`<p class="config-section-card__desc">${description}</p>`
|
||||
: nothing}
|
||||
${
|
||||
description
|
||||
? html`<p class="config-section-card__desc">${description}</p>`
|
||||
: nothing
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
<div class="config-section-card__content">
|
||||
@@ -239,22 +431,24 @@ export function renderConfigForm(props: ConfigFormProps) {
|
||||
</div>
|
||||
</section>
|
||||
`;
|
||||
})()
|
||||
: filteredEntries.map(([key, node]) => {
|
||||
const meta = SECTION_META[key] ?? {
|
||||
label: key.charAt(0).toUpperCase() + key.slice(1),
|
||||
description: node.description ?? "",
|
||||
};
|
||||
})()
|
||||
: filteredEntries.map(([key, node]) => {
|
||||
const meta = SECTION_META[key] ?? {
|
||||
label: key.charAt(0).toUpperCase() + key.slice(1),
|
||||
description: node.description ?? "",
|
||||
};
|
||||
|
||||
return html`
|
||||
return html`
|
||||
<section class="config-section-card" id="config-section-${key}">
|
||||
<div class="config-section-card__header">
|
||||
<span class="config-section-card__icon">${getSectionIcon(key)}</span>
|
||||
<div class="config-section-card__titles">
|
||||
<h3 class="config-section-card__title">${meta.label}</h3>
|
||||
${meta.description
|
||||
? html`<p class="config-section-card__desc">${meta.description}</p>`
|
||||
: nothing}
|
||||
${
|
||||
meta.description
|
||||
? html`<p class="config-section-card__desc">${meta.description}</p>`
|
||||
: nothing
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
<div class="config-section-card__content">
|
||||
@@ -271,7 +465,8 @@ export function renderConfigForm(props: ConfigFormProps) {
|
||||
</div>
|
||||
</section>
|
||||
`;
|
||||
})}
|
||||
})
|
||||
}
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
@@ -1,7 +1,4 @@
|
||||
export { renderConfigForm, type ConfigFormProps, SECTION_META } from "./config-form.render";
|
||||
export {
|
||||
analyzeConfigSchema,
|
||||
type ConfigSchemaAnalysis,
|
||||
} from "./config-form.analyze";
|
||||
export { analyzeConfigSchema, type ConfigSchemaAnalysis } from "./config-form.analyze";
|
||||
export { renderNode } from "./config-form.node";
|
||||
export { schemaType, type JsonSchema } from "./config-form.shared";
|
||||
|
||||
@@ -59,11 +59,9 @@ describe("config view", () => {
|
||||
container,
|
||||
);
|
||||
|
||||
const saveButton = Array.from(
|
||||
container.querySelectorAll("button"),
|
||||
).find((btn) => btn.textContent?.trim() === "Save") as
|
||||
| HTMLButtonElement
|
||||
| undefined;
|
||||
const saveButton = Array.from(container.querySelectorAll("button")).find(
|
||||
(btn) => btn.textContent?.trim() === "Save",
|
||||
) as HTMLButtonElement | undefined;
|
||||
expect(saveButton).not.toBeUndefined();
|
||||
expect(saveButton?.disabled).toBe(false);
|
||||
});
|
||||
@@ -81,11 +79,9 @@ describe("config view", () => {
|
||||
container,
|
||||
);
|
||||
|
||||
const saveButton = Array.from(
|
||||
container.querySelectorAll("button"),
|
||||
).find((btn) => btn.textContent?.trim() === "Save") as
|
||||
| HTMLButtonElement
|
||||
| undefined;
|
||||
const saveButton = Array.from(container.querySelectorAll("button")).find(
|
||||
(btn) => btn.textContent?.trim() === "Save",
|
||||
) as HTMLButtonElement | undefined;
|
||||
expect(saveButton).not.toBeUndefined();
|
||||
expect(saveButton?.disabled).toBe(true);
|
||||
});
|
||||
@@ -102,16 +98,12 @@ describe("config view", () => {
|
||||
container,
|
||||
);
|
||||
|
||||
const saveButton = Array.from(
|
||||
container.querySelectorAll("button"),
|
||||
).find((btn) => btn.textContent?.trim() === "Save") as
|
||||
| HTMLButtonElement
|
||||
| undefined;
|
||||
const applyButton = Array.from(
|
||||
container.querySelectorAll("button"),
|
||||
).find((btn) => btn.textContent?.trim() === "Apply") as
|
||||
| HTMLButtonElement
|
||||
| undefined;
|
||||
const saveButton = Array.from(container.querySelectorAll("button")).find(
|
||||
(btn) => btn.textContent?.trim() === "Save",
|
||||
) as HTMLButtonElement | undefined;
|
||||
const applyButton = Array.from(container.querySelectorAll("button")).find(
|
||||
(btn) => btn.textContent?.trim() === "Apply",
|
||||
) as HTMLButtonElement | undefined;
|
||||
expect(saveButton).not.toBeUndefined();
|
||||
expect(applyButton).not.toBeUndefined();
|
||||
expect(saveButton?.disabled).toBe(true);
|
||||
@@ -124,22 +116,18 @@ describe("config view", () => {
|
||||
renderConfig({
|
||||
...baseProps(),
|
||||
formMode: "raw",
|
||||
raw: "{\n gateway: { mode: \"local\" }\n}\n",
|
||||
raw: '{\n gateway: { mode: "local" }\n}\n',
|
||||
originalRaw: "{\n}\n",
|
||||
}),
|
||||
container,
|
||||
);
|
||||
|
||||
const saveButton = Array.from(
|
||||
container.querySelectorAll("button"),
|
||||
).find((btn) => btn.textContent?.trim() === "Save") as
|
||||
| HTMLButtonElement
|
||||
| undefined;
|
||||
const applyButton = Array.from(
|
||||
container.querySelectorAll("button"),
|
||||
).find((btn) => btn.textContent?.trim() === "Apply") as
|
||||
| HTMLButtonElement
|
||||
| undefined;
|
||||
const saveButton = Array.from(container.querySelectorAll("button")).find(
|
||||
(btn) => btn.textContent?.trim() === "Save",
|
||||
) as HTMLButtonElement | undefined;
|
||||
const applyButton = Array.from(container.querySelectorAll("button")).find(
|
||||
(btn) => btn.textContent?.trim() === "Apply",
|
||||
) as HTMLButtonElement | undefined;
|
||||
expect(saveButton).not.toBeUndefined();
|
||||
expect(applyButton).not.toBeUndefined();
|
||||
expect(saveButton?.disabled).toBe(false);
|
||||
@@ -157,8 +145,8 @@ describe("config view", () => {
|
||||
container,
|
||||
);
|
||||
|
||||
const btn = Array.from(container.querySelectorAll("button")).find((b) =>
|
||||
b.textContent?.trim() === "Raw",
|
||||
const btn = Array.from(container.querySelectorAll("button")).find(
|
||||
(b) => b.textContent?.trim() === "Raw",
|
||||
) as HTMLButtonElement | undefined;
|
||||
expect(btn).toBeTruthy();
|
||||
btn?.click();
|
||||
@@ -183,8 +171,8 @@ describe("config view", () => {
|
||||
container,
|
||||
);
|
||||
|
||||
const btn = Array.from(container.querySelectorAll("button")).find((b) =>
|
||||
b.textContent?.trim() === "Gateway",
|
||||
const btn = Array.from(container.querySelectorAll("button")).find(
|
||||
(b) => b.textContent?.trim() === "Gateway",
|
||||
) as HTMLButtonElement | undefined;
|
||||
expect(btn).toBeTruthy();
|
||||
btn?.click();
|
||||
@@ -202,9 +190,7 @@ describe("config view", () => {
|
||||
container,
|
||||
);
|
||||
|
||||
const input = container.querySelector(
|
||||
".config-search__input",
|
||||
) as HTMLInputElement | null;
|
||||
const input = container.querySelector(".config-search__input") as HTMLInputElement | null;
|
||||
expect(input).not.toBeNull();
|
||||
if (!input) return;
|
||||
input.value = "gateway";
|
||||
|
||||
+322
-105
@@ -1,12 +1,7 @@
|
||||
import { html, nothing } from "lit";
|
||||
import type { ConfigUiHints } from "../types";
|
||||
import { analyzeConfigSchema, renderConfigForm, SECTION_META } from "./config-form";
|
||||
import {
|
||||
hintForPath,
|
||||
humanize,
|
||||
schemaType,
|
||||
type JsonSchema,
|
||||
} from "./config-form.shared";
|
||||
import { hintForPath, humanize, schemaType, type JsonSchema } from "./config-form.shared";
|
||||
|
||||
export type ConfigProps = {
|
||||
raw: string;
|
||||
@@ -41,36 +36,230 @@ export type ConfigProps = {
|
||||
|
||||
// SVG Icons for sidebar (Lucide-style)
|
||||
const sidebarIcons = {
|
||||
all: html`<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="3" y="3" width="7" height="7"></rect><rect x="14" y="3" width="7" height="7"></rect><rect x="14" y="14" width="7" height="7"></rect><rect x="3" y="14" width="7" height="7"></rect></svg>`,
|
||||
env: html`<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="12" r="3"></circle><path d="M19.4 15a1.65 1.65 0 0 0 .33 1.82l.06.06a2 2 0 0 1 0 2.83 2 2 0 0 1-2.83 0l-.06-.06a1.65 1.65 0 0 0-1.82-.33 1.65 1.65 0 0 0-1 1.51V21a2 2 0 0 1-2 2 2 2 0 0 1-2-2v-.09A1.65 1.65 0 0 0 9 19.4a1.65 1.65 0 0 0-1.82.33l-.06.06a2 2 0 0 1-2.83 0 2 2 0 0 1 0-2.83l.06-.06a1.65 1.65 0 0 0 .33-1.82 1.65 1.65 0 0 0-1.51-1H3a2 2 0 0 1-2-2 2 2 0 0 1 2-2h.09A1.65 1.65 0 0 0 4.6 9a1.65 1.65 0 0 0-.33-1.82l-.06-.06a2 2 0 0 1 0-2.83 2 2 0 0 1 2.83 0l.06.06a1.65 1.65 0 0 0 1.82.33H9a1.65 1.65 0 0 0 1-1.51V3a2 2 0 0 1 2-2 2 2 0 0 1 2 2v.09a1.65 1.65 0 0 0 1 1.51 1.65 1.65 0 0 0 1.82-.33l.06-.06a2 2 0 0 1 2.83 0 2 2 0 0 1 0 2.83l-.06.06a1.65 1.65 0 0 0-.33 1.82V9a1.65 1.65 0 0 0 1.51 1H21a2 2 0 0 1 2 2 2 2 0 0 1-2 2h-.09a1.65 1.65 0 0 0-1.51 1z"></path></svg>`,
|
||||
update: html`<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"></path><polyline points="7 10 12 15 17 10"></polyline><line x1="12" y1="15" x2="12" y2="3"></line></svg>`,
|
||||
agents: html`<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M12 2a2 2 0 0 1 2 2c0 .74-.4 1.39-1 1.73V7h1a7 7 0 0 1 7 7h1a1 1 0 0 1 1 1v3a1 1 0 0 1-1 1h-1v1a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-1H2a1 1 0 0 1-1-1v-3a1 1 0 0 1 1-1h1a7 7 0 0 1 7-7h1V5.73c-.6-.34-1-.99-1-1.73a2 2 0 0 1 2-2z"></path><circle cx="8" cy="14" r="1"></circle><circle cx="16" cy="14" r="1"></circle></svg>`,
|
||||
auth: html`<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="3" y="11" width="18" height="11" rx="2" ry="2"></rect><path d="M7 11V7a5 5 0 0 1 10 0v4"></path></svg>`,
|
||||
channels: html`<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z"></path></svg>`,
|
||||
messages: html`<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M4 4h16c1.1 0 2 .9 2 2v12c0 1.1-.9 2-2 2H4c-1.1 0-2-.9-2-2V6c0-1.1.9-2 2-2z"></path><polyline points="22,6 12,13 2,6"></polyline></svg>`,
|
||||
commands: html`<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="4 17 10 11 4 5"></polyline><line x1="12" y1="19" x2="20" y2="19"></line></svg>`,
|
||||
hooks: html`<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71"></path><path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71"></path></svg>`,
|
||||
skills: html`<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polygon points="12 2 15.09 8.26 22 9.27 17 14.14 18.18 21.02 12 17.77 5.82 21.02 7 14.14 2 9.27 8.91 8.26 12 2"></polygon></svg>`,
|
||||
tools: html`<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M14.7 6.3a1 1 0 0 0 0 1.4l1.6 1.6a1 1 0 0 0 1.4 0l3.77-3.77a6 6 0 0 1-7.94 7.94l-6.91 6.91a2.12 2.12 0 0 1-3-3l6.91-6.91a6 6 0 0 1 7.94-7.94l-3.76 3.76z"></path></svg>`,
|
||||
gateway: html`<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="12" r="10"></circle><line x1="2" y1="12" x2="22" y2="12"></line><path d="M12 2a15.3 15.3 0 0 1 4 10 15.3 15.3 0 0 1-4 10 15.3 15.3 0 0 1-4-10 15.3 15.3 0 0 1 4-10z"></path></svg>`,
|
||||
wizard: html`<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M15 4V2"></path><path d="M15 16v-2"></path><path d="M8 9h2"></path><path d="M20 9h2"></path><path d="M17.8 11.8 19 13"></path><path d="M15 9h0"></path><path d="M17.8 6.2 19 5"></path><path d="m3 21 9-9"></path><path d="M12.2 6.2 11 5"></path></svg>`,
|
||||
all: html`
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<rect x="3" y="3" width="7" height="7"></rect>
|
||||
<rect x="14" y="3" width="7" height="7"></rect>
|
||||
<rect x="14" y="14" width="7" height="7"></rect>
|
||||
<rect x="3" y="14" width="7" height="7"></rect>
|
||||
</svg>
|
||||
`,
|
||||
env: html`
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<circle cx="12" cy="12" r="3"></circle>
|
||||
<path
|
||||
d="M19.4 15a1.65 1.65 0 0 0 .33 1.82l.06.06a2 2 0 0 1 0 2.83 2 2 0 0 1-2.83 0l-.06-.06a1.65 1.65 0 0 0-1.82-.33 1.65 1.65 0 0 0-1 1.51V21a2 2 0 0 1-2 2 2 2 0 0 1-2-2v-.09A1.65 1.65 0 0 0 9 19.4a1.65 1.65 0 0 0-1.82.33l-.06.06a2 2 0 0 1-2.83 0 2 2 0 0 1 0-2.83l.06-.06a1.65 1.65 0 0 0 .33-1.82 1.65 1.65 0 0 0-1.51-1H3a2 2 0 0 1-2-2 2 2 0 0 1 2-2h.09A1.65 1.65 0 0 0 4.6 9a1.65 1.65 0 0 0-.33-1.82l-.06-.06a2 2 0 0 1 0-2.83 2 2 0 0 1 2.83 0l.06.06a1.65 1.65 0 0 0 1.82.33H9a1.65 1.65 0 0 0 1-1.51V3a2 2 0 0 1 2-2 2 2 0 0 1 2 2v.09a1.65 1.65 0 0 0 1 1.51 1.65 1.65 0 0 0 1.82-.33l.06-.06a2 2 0 0 1 2.83 0 2 2 0 0 1 0 2.83l-.06.06a1.65 1.65 0 0 0-.33 1.82V9a1.65 1.65 0 0 0 1.51 1H21a2 2 0 0 1 2 2 2 2 0 0 1-2 2h-.09a1.65 1.65 0 0 0-1.51 1z"
|
||||
></path>
|
||||
</svg>
|
||||
`,
|
||||
update: html`
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"></path>
|
||||
<polyline points="7 10 12 15 17 10"></polyline>
|
||||
<line x1="12" y1="15" x2="12" y2="3"></line>
|
||||
</svg>
|
||||
`,
|
||||
agents: html`
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<path
|
||||
d="M12 2a2 2 0 0 1 2 2c0 .74-.4 1.39-1 1.73V7h1a7 7 0 0 1 7 7h1a1 1 0 0 1 1 1v3a1 1 0 0 1-1 1h-1v1a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-1H2a1 1 0 0 1-1-1v-3a1 1 0 0 1 1-1h1a7 7 0 0 1 7-7h1V5.73c-.6-.34-1-.99-1-1.73a2 2 0 0 1 2-2z"
|
||||
></path>
|
||||
<circle cx="8" cy="14" r="1"></circle>
|
||||
<circle cx="16" cy="14" r="1"></circle>
|
||||
</svg>
|
||||
`,
|
||||
auth: html`
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<rect x="3" y="11" width="18" height="11" rx="2" ry="2"></rect>
|
||||
<path d="M7 11V7a5 5 0 0 1 10 0v4"></path>
|
||||
</svg>
|
||||
`,
|
||||
channels: html`
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<path d="M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z"></path>
|
||||
</svg>
|
||||
`,
|
||||
messages: html`
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<path d="M4 4h16c1.1 0 2 .9 2 2v12c0 1.1-.9 2-2 2H4c-1.1 0-2-.9-2-2V6c0-1.1.9-2 2-2z"></path>
|
||||
<polyline points="22,6 12,13 2,6"></polyline>
|
||||
</svg>
|
||||
`,
|
||||
commands: html`
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<polyline points="4 17 10 11 4 5"></polyline>
|
||||
<line x1="12" y1="19" x2="20" y2="19"></line>
|
||||
</svg>
|
||||
`,
|
||||
hooks: html`
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71"></path>
|
||||
<path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71"></path>
|
||||
</svg>
|
||||
`,
|
||||
skills: html`
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<polygon
|
||||
points="12 2 15.09 8.26 22 9.27 17 14.14 18.18 21.02 12 17.77 5.82 21.02 7 14.14 2 9.27 8.91 8.26 12 2"
|
||||
></polygon>
|
||||
</svg>
|
||||
`,
|
||||
tools: html`
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<path
|
||||
d="M14.7 6.3a1 1 0 0 0 0 1.4l1.6 1.6a1 1 0 0 0 1.4 0l3.77-3.77a6 6 0 0 1-7.94 7.94l-6.91 6.91a2.12 2.12 0 0 1-3-3l6.91-6.91a6 6 0 0 1 7.94-7.94l-3.76 3.76z"
|
||||
></path>
|
||||
</svg>
|
||||
`,
|
||||
gateway: html`
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<circle cx="12" cy="12" r="10"></circle>
|
||||
<line x1="2" y1="12" x2="22" y2="12"></line>
|
||||
<path
|
||||
d="M12 2a15.3 15.3 0 0 1 4 10 15.3 15.3 0 0 1-4 10 15.3 15.3 0 0 1-4-10 15.3 15.3 0 0 1 4-10z"
|
||||
></path>
|
||||
</svg>
|
||||
`,
|
||||
wizard: html`
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<path d="M15 4V2"></path>
|
||||
<path d="M15 16v-2"></path>
|
||||
<path d="M8 9h2"></path>
|
||||
<path d="M20 9h2"></path>
|
||||
<path d="M17.8 11.8 19 13"></path>
|
||||
<path d="M15 9h0"></path>
|
||||
<path d="M17.8 6.2 19 5"></path>
|
||||
<path d="m3 21 9-9"></path>
|
||||
<path d="M12.2 6.2 11 5"></path>
|
||||
</svg>
|
||||
`,
|
||||
// Additional sections
|
||||
meta: html`<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M12 20h9"></path><path d="M16.5 3.5a2.12 2.12 0 0 1 3 3L7 19l-4 1 1-4Z"></path></svg>`,
|
||||
logging: html`<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"></path><polyline points="14 2 14 8 20 8"></polyline><line x1="16" y1="13" x2="8" y2="13"></line><line x1="16" y1="17" x2="8" y2="17"></line><polyline points="10 9 9 9 8 9"></polyline></svg>`,
|
||||
browser: html`<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="12" r="10"></circle><circle cx="12" cy="12" r="4"></circle><line x1="21.17" y1="8" x2="12" y2="8"></line><line x1="3.95" y1="6.06" x2="8.54" y2="14"></line><line x1="10.88" y1="21.94" x2="15.46" y2="14"></line></svg>`,
|
||||
ui: html`<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="3" y="3" width="18" height="18" rx="2" ry="2"></rect><line x1="3" y1="9" x2="21" y2="9"></line><line x1="9" y1="21" x2="9" y2="9"></line></svg>`,
|
||||
models: html`<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M21 16V8a2 2 0 0 0-1-1.73l-7-4a2 2 0 0 0-2 0l-7 4A2 2 0 0 0 3 8v8a2 2 0 0 0 1 1.73l7 4a2 2 0 0 0 2 0l7-4A2 2 0 0 0 21 16z"></path><polyline points="3.27 6.96 12 12.01 20.73 6.96"></polyline><line x1="12" y1="22.08" x2="12" y2="12"></line></svg>`,
|
||||
bindings: html`<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="2" y="2" width="20" height="8" rx="2" ry="2"></rect><rect x="2" y="14" width="20" height="8" rx="2" ry="2"></rect><line x1="6" y1="6" x2="6.01" y2="6"></line><line x1="6" y1="18" x2="6.01" y2="18"></line></svg>`,
|
||||
broadcast: html`<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M4.9 19.1C1 15.2 1 8.8 4.9 4.9"></path><path d="M7.8 16.2c-2.3-2.3-2.3-6.1 0-8.5"></path><circle cx="12" cy="12" r="2"></circle><path d="M16.2 7.8c2.3 2.3 2.3 6.1 0 8.5"></path><path d="M19.1 4.9C23 8.8 23 15.1 19.1 19"></path></svg>`,
|
||||
audio: html`<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M9 18V5l12-2v13"></path><circle cx="6" cy="18" r="3"></circle><circle cx="18" cy="16" r="3"></circle></svg>`,
|
||||
session: html`<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M17 21v-2a4 4 0 0 0-4-4H5a4 4 0 0 0-4 4v2"></path><circle cx="9" cy="7" r="4"></circle><path d="M23 21v-2a4 4 0 0 0-3-3.87"></path><path d="M16 3.13a4 4 0 0 1 0 7.75"></path></svg>`,
|
||||
cron: html`<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="12" r="10"></circle><polyline points="12 6 12 12 16 14"></polyline></svg>`,
|
||||
web: html`<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="12" r="10"></circle><line x1="2" y1="12" x2="22" y2="12"></line><path d="M12 2a15.3 15.3 0 0 1 4 10 15.3 15.3 0 0 1-4 10 15.3 15.3 0 0 1-4-10 15.3 15.3 0 0 1 4-10z"></path></svg>`,
|
||||
discovery: html`<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="11" cy="11" r="8"></circle><line x1="21" y1="21" x2="16.65" y2="16.65"></line></svg>`,
|
||||
canvasHost: html`<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="3" y="3" width="18" height="18" rx="2" ry="2"></rect><circle cx="8.5" cy="8.5" r="1.5"></circle><polyline points="21 15 16 10 5 21"></polyline></svg>`,
|
||||
talk: html`<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M12 1a3 3 0 0 0-3 3v8a3 3 0 0 0 6 0V4a3 3 0 0 0-3-3z"></path><path d="M19 10v2a7 7 0 0 1-14 0v-2"></path><line x1="12" y1="19" x2="12" y2="23"></line><line x1="8" y1="23" x2="16" y2="23"></line></svg>`,
|
||||
plugins: html`<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M12 2v6"></path><path d="m4.93 10.93 4.24 4.24"></path><path d="M2 12h6"></path><path d="m4.93 13.07 4.24-4.24"></path><path d="M12 22v-6"></path><path d="m19.07 13.07-4.24-4.24"></path><path d="M22 12h-6"></path><path d="m19.07 10.93-4.24 4.24"></path></svg>`,
|
||||
default: html`<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"></path><polyline points="14 2 14 8 20 8"></polyline></svg>`,
|
||||
meta: html`
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<path d="M12 20h9"></path>
|
||||
<path d="M16.5 3.5a2.12 2.12 0 0 1 3 3L7 19l-4 1 1-4Z"></path>
|
||||
</svg>
|
||||
`,
|
||||
logging: html`
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"></path>
|
||||
<polyline points="14 2 14 8 20 8"></polyline>
|
||||
<line x1="16" y1="13" x2="8" y2="13"></line>
|
||||
<line x1="16" y1="17" x2="8" y2="17"></line>
|
||||
<polyline points="10 9 9 9 8 9"></polyline>
|
||||
</svg>
|
||||
`,
|
||||
browser: html`
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<circle cx="12" cy="12" r="10"></circle>
|
||||
<circle cx="12" cy="12" r="4"></circle>
|
||||
<line x1="21.17" y1="8" x2="12" y2="8"></line>
|
||||
<line x1="3.95" y1="6.06" x2="8.54" y2="14"></line>
|
||||
<line x1="10.88" y1="21.94" x2="15.46" y2="14"></line>
|
||||
</svg>
|
||||
`,
|
||||
ui: html`
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<rect x="3" y="3" width="18" height="18" rx="2" ry="2"></rect>
|
||||
<line x1="3" y1="9" x2="21" y2="9"></line>
|
||||
<line x1="9" y1="21" x2="9" y2="9"></line>
|
||||
</svg>
|
||||
`,
|
||||
models: html`
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<path
|
||||
d="M21 16V8a2 2 0 0 0-1-1.73l-7-4a2 2 0 0 0-2 0l-7 4A2 2 0 0 0 3 8v8a2 2 0 0 0 1 1.73l7 4a2 2 0 0 0 2 0l7-4A2 2 0 0 0 21 16z"
|
||||
></path>
|
||||
<polyline points="3.27 6.96 12 12.01 20.73 6.96"></polyline>
|
||||
<line x1="12" y1="22.08" x2="12" y2="12"></line>
|
||||
</svg>
|
||||
`,
|
||||
bindings: html`
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<rect x="2" y="2" width="20" height="8" rx="2" ry="2"></rect>
|
||||
<rect x="2" y="14" width="20" height="8" rx="2" ry="2"></rect>
|
||||
<line x1="6" y1="6" x2="6.01" y2="6"></line>
|
||||
<line x1="6" y1="18" x2="6.01" y2="18"></line>
|
||||
</svg>
|
||||
`,
|
||||
broadcast: html`
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<path d="M4.9 19.1C1 15.2 1 8.8 4.9 4.9"></path>
|
||||
<path d="M7.8 16.2c-2.3-2.3-2.3-6.1 0-8.5"></path>
|
||||
<circle cx="12" cy="12" r="2"></circle>
|
||||
<path d="M16.2 7.8c2.3 2.3 2.3 6.1 0 8.5"></path>
|
||||
<path d="M19.1 4.9C23 8.8 23 15.1 19.1 19"></path>
|
||||
</svg>
|
||||
`,
|
||||
audio: html`
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<path d="M9 18V5l12-2v13"></path>
|
||||
<circle cx="6" cy="18" r="3"></circle>
|
||||
<circle cx="18" cy="16" r="3"></circle>
|
||||
</svg>
|
||||
`,
|
||||
session: html`
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<path d="M17 21v-2a4 4 0 0 0-4-4H5a4 4 0 0 0-4 4v2"></path>
|
||||
<circle cx="9" cy="7" r="4"></circle>
|
||||
<path d="M23 21v-2a4 4 0 0 0-3-3.87"></path>
|
||||
<path d="M16 3.13a4 4 0 0 1 0 7.75"></path>
|
||||
</svg>
|
||||
`,
|
||||
cron: html`
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<circle cx="12" cy="12" r="10"></circle>
|
||||
<polyline points="12 6 12 12 16 14"></polyline>
|
||||
</svg>
|
||||
`,
|
||||
web: html`
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<circle cx="12" cy="12" r="10"></circle>
|
||||
<line x1="2" y1="12" x2="22" y2="12"></line>
|
||||
<path
|
||||
d="M12 2a15.3 15.3 0 0 1 4 10 15.3 15.3 0 0 1-4 10 15.3 15.3 0 0 1-4-10 15.3 15.3 0 0 1 4-10z"
|
||||
></path>
|
||||
</svg>
|
||||
`,
|
||||
discovery: html`
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<circle cx="11" cy="11" r="8"></circle>
|
||||
<line x1="21" y1="21" x2="16.65" y2="16.65"></line>
|
||||
</svg>
|
||||
`,
|
||||
canvasHost: html`
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<rect x="3" y="3" width="18" height="18" rx="2" ry="2"></rect>
|
||||
<circle cx="8.5" cy="8.5" r="1.5"></circle>
|
||||
<polyline points="21 15 16 10 5 21"></polyline>
|
||||
</svg>
|
||||
`,
|
||||
talk: html`
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<path d="M12 1a3 3 0 0 0-3 3v8a3 3 0 0 0 6 0V4a3 3 0 0 0-3-3z"></path>
|
||||
<path d="M19 10v2a7 7 0 0 1-14 0v-2"></path>
|
||||
<line x1="12" y1="19" x2="12" y2="23"></line>
|
||||
<line x1="8" y1="23" x2="16" y2="23"></line>
|
||||
</svg>
|
||||
`,
|
||||
plugins: html`
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<path d="M12 2v6"></path>
|
||||
<path d="m4.93 10.93 4.24 4.24"></path>
|
||||
<path d="M2 12h6"></path>
|
||||
<path d="m4.93 13.07 4.24-4.24"></path>
|
||||
<path d="M12 22v-6"></path>
|
||||
<path d="m19.07 13.07-4.24-4.24"></path>
|
||||
<path d="M22 12h-6"></path>
|
||||
<path d="m19.07 10.93-4.24 4.24"></path>
|
||||
</svg>
|
||||
`,
|
||||
default: html`
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"></path>
|
||||
<polyline points="14 2 14 8 20 8"></polyline>
|
||||
</svg>
|
||||
`,
|
||||
};
|
||||
|
||||
// Section definitions
|
||||
@@ -102,7 +291,10 @@ function getSectionIcon(key: string) {
|
||||
return sidebarIcons[key as keyof typeof sidebarIcons] ?? sidebarIcons.default;
|
||||
}
|
||||
|
||||
function resolveSectionMeta(key: string, schema?: JsonSchema): {
|
||||
function resolveSectionMeta(
|
||||
key: string,
|
||||
schema?: JsonSchema,
|
||||
): {
|
||||
label: string;
|
||||
description?: string;
|
||||
} {
|
||||
@@ -134,7 +326,7 @@ function resolveSubsections(params: {
|
||||
|
||||
function computeDiff(
|
||||
original: Record<string, unknown> | null,
|
||||
current: Record<string, unknown> | null
|
||||
current: Record<string, unknown> | null,
|
||||
): Array<{ path: string; from: unknown; to: unknown }> {
|
||||
if (!original || !current) return [];
|
||||
const changes: Array<{ path: string; from: unknown; to: unknown }> = [];
|
||||
@@ -182,22 +374,19 @@ function truncateValue(value: unknown, maxLen = 40): string {
|
||||
}
|
||||
|
||||
export function renderConfig(props: ConfigProps) {
|
||||
const validity =
|
||||
props.valid == null ? "unknown" : props.valid ? "valid" : "invalid";
|
||||
const validity = props.valid == null ? "unknown" : props.valid ? "valid" : "invalid";
|
||||
const analysis = analyzeConfigSchema(props.schema);
|
||||
const formUnsafe = analysis.schema
|
||||
? analysis.unsupportedPaths.length > 0
|
||||
: false;
|
||||
const formUnsafe = analysis.schema ? analysis.unsupportedPaths.length > 0 : false;
|
||||
|
||||
// Get available sections from schema
|
||||
const schemaProps = analysis.schema?.properties ?? {};
|
||||
const availableSections = SECTIONS.filter(s => s.key in schemaProps);
|
||||
const availableSections = SECTIONS.filter((s) => s.key in schemaProps);
|
||||
|
||||
// Add any sections in schema but not in our list
|
||||
const knownKeys = new Set(SECTIONS.map(s => s.key));
|
||||
const knownKeys = new Set(SECTIONS.map((s) => s.key));
|
||||
const extraSections = Object.keys(schemaProps)
|
||||
.filter(k => !knownKeys.has(k))
|
||||
.map(k => ({ key: k, label: k.charAt(0).toUpperCase() + k.slice(1) }));
|
||||
.filter((k) => !knownKeys.has(k))
|
||||
.map((k) => ({ key: k, label: k.charAt(0).toUpperCase() + k.slice(1) }));
|
||||
|
||||
const allSections = [...availableSections, ...extraSections];
|
||||
|
||||
@@ -216,27 +405,22 @@ export function renderConfig(props: ConfigProps) {
|
||||
})
|
||||
: [];
|
||||
const allowSubnav =
|
||||
props.formMode === "form" &&
|
||||
Boolean(props.activeSection) &&
|
||||
subsections.length > 0;
|
||||
props.formMode === "form" && Boolean(props.activeSection) && subsections.length > 0;
|
||||
const isAllSubsection = props.activeSubsection === ALL_SUBSECTION;
|
||||
const effectiveSubsection = props.searchQuery
|
||||
? null
|
||||
: isAllSubsection
|
||||
? null
|
||||
: props.activeSubsection ?? (subsections[0]?.key ?? null);
|
||||
: (props.activeSubsection ?? subsections[0]?.key ?? null);
|
||||
|
||||
// Compute diff for showing changes (works for both form and raw modes)
|
||||
const diff = props.formMode === "form"
|
||||
? computeDiff(props.originalValue, props.formValue)
|
||||
: [];
|
||||
const diff = props.formMode === "form" ? computeDiff(props.originalValue, props.formValue) : [];
|
||||
const hasRawChanges = props.formMode === "raw" && props.raw !== props.originalRaw;
|
||||
const hasChanges = props.formMode === "form" ? diff.length > 0 : hasRawChanges;
|
||||
|
||||
// Save/apply buttons require actual changes to be enabled.
|
||||
// Note: formUnsafe warns about unsupported schema paths but shouldn't block saving.
|
||||
const canSaveForm =
|
||||
Boolean(props.formValue) && !props.loading && Boolean(analysis.schema);
|
||||
const canSaveForm = Boolean(props.formValue) && !props.loading && Boolean(analysis.schema);
|
||||
const canSave =
|
||||
props.connected &&
|
||||
!props.saving &&
|
||||
@@ -272,12 +456,16 @@ export function renderConfig(props: ConfigProps) {
|
||||
.value=${props.searchQuery}
|
||||
@input=${(e: Event) => props.onSearchChange((e.target as HTMLInputElement).value)}
|
||||
/>
|
||||
${props.searchQuery ? html`
|
||||
${
|
||||
props.searchQuery
|
||||
? html`
|
||||
<button
|
||||
class="config-search__clear"
|
||||
@click=${() => props.onSearchChange("")}
|
||||
>×</button>
|
||||
` : nothing}
|
||||
`
|
||||
: nothing
|
||||
}
|
||||
</div>
|
||||
|
||||
<!-- Section nav -->
|
||||
@@ -289,7 +477,8 @@ export function renderConfig(props: ConfigProps) {
|
||||
<span class="config-nav__icon">${sidebarIcons.all}</span>
|
||||
<span class="config-nav__label">All Settings</span>
|
||||
</button>
|
||||
${allSections.map(section => html`
|
||||
${allSections.map(
|
||||
(section) => html`
|
||||
<button
|
||||
class="config-nav__item ${props.activeSection === section.key ? "active" : ""}"
|
||||
@click=${() => props.onSectionChange(section.key)}
|
||||
@@ -297,7 +486,8 @@ export function renderConfig(props: ConfigProps) {
|
||||
<span class="config-nav__icon">${getSectionIcon(section.key)}</span>
|
||||
<span class="config-nav__label">${section.label}</span>
|
||||
</button>
|
||||
`)}
|
||||
`,
|
||||
)}
|
||||
</nav>
|
||||
|
||||
<!-- Mode toggle at bottom -->
|
||||
@@ -325,11 +515,15 @@ export function renderConfig(props: ConfigProps) {
|
||||
<!-- Action bar -->
|
||||
<div class="config-actions">
|
||||
<div class="config-actions__left">
|
||||
${hasChanges ? html`
|
||||
${
|
||||
hasChanges
|
||||
? html`
|
||||
<span class="config-changes-badge">${props.formMode === "raw" ? "Unsaved changes" : `${diff.length} unsaved change${diff.length !== 1 ? "s" : ""}`}</span>
|
||||
` : html`
|
||||
<span class="config-status muted">No changes</span>
|
||||
`}
|
||||
`
|
||||
: html`
|
||||
<span class="config-status muted">No changes</span>
|
||||
`
|
||||
}
|
||||
</div>
|
||||
<div class="config-actions__right">
|
||||
<button class="btn btn--sm" ?disabled=${props.loading} @click=${props.onReload}>
|
||||
@@ -360,7 +554,9 @@ export function renderConfig(props: ConfigProps) {
|
||||
</div>
|
||||
|
||||
<!-- Diff panel (form mode only - raw mode doesn't have granular diff) -->
|
||||
${hasChanges && props.formMode === "form" ? html`
|
||||
${
|
||||
hasChanges && props.formMode === "form"
|
||||
? html`
|
||||
<details class="config-diff">
|
||||
<summary class="config-diff__summary">
|
||||
<span>View ${diff.length} pending change${diff.length !== 1 ? "s" : ""}</span>
|
||||
@@ -369,7 +565,8 @@ export function renderConfig(props: ConfigProps) {
|
||||
</svg>
|
||||
</summary>
|
||||
<div class="config-diff__content">
|
||||
${diff.map(change => html`
|
||||
${diff.map(
|
||||
(change) => html`
|
||||
<div class="config-diff__item">
|
||||
<div class="config-diff__path">${change.path}</div>
|
||||
<div class="config-diff__values">
|
||||
@@ -378,27 +575,35 @@ export function renderConfig(props: ConfigProps) {
|
||||
<span class="config-diff__to">${truncateValue(change.to)}</span>
|
||||
</div>
|
||||
</div>
|
||||
`)}
|
||||
`,
|
||||
)}
|
||||
</div>
|
||||
</details>
|
||||
` : nothing}
|
||||
`
|
||||
: nothing
|
||||
}
|
||||
|
||||
${activeSectionMeta && props.formMode === "form"
|
||||
? html`
|
||||
${
|
||||
activeSectionMeta && props.formMode === "form"
|
||||
? html`
|
||||
<div class="config-section-hero">
|
||||
<div class="config-section-hero__icon">${getSectionIcon(props.activeSection ?? "")}</div>
|
||||
<div class="config-section-hero__text">
|
||||
<div class="config-section-hero__title">${activeSectionMeta.label}</div>
|
||||
${activeSectionMeta.description
|
||||
? html`<div class="config-section-hero__desc">${activeSectionMeta.description}</div>`
|
||||
: nothing}
|
||||
${
|
||||
activeSectionMeta.description
|
||||
? html`<div class="config-section-hero__desc">${activeSectionMeta.description}</div>`
|
||||
: nothing
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
`
|
||||
: nothing}
|
||||
: nothing
|
||||
}
|
||||
|
||||
${allowSubnav
|
||||
? html`
|
||||
${
|
||||
allowSubnav
|
||||
? html`
|
||||
<div class="config-subnav">
|
||||
<button
|
||||
class="config-subnav__item ${effectiveSubsection === null ? "active" : ""}"
|
||||
@@ -421,36 +626,45 @@ export function renderConfig(props: ConfigProps) {
|
||||
)}
|
||||
</div>
|
||||
`
|
||||
: nothing}
|
||||
: nothing
|
||||
}
|
||||
|
||||
<!-- Form content -->
|
||||
<div class="config-content">
|
||||
${props.formMode === "form"
|
||||
? html`
|
||||
${props.schemaLoading
|
||||
? html`<div class="config-loading">
|
||||
<div class="config-loading__spinner"></div>
|
||||
<span>Loading schema…</span>
|
||||
</div>`
|
||||
: renderConfigForm({
|
||||
schema: analysis.schema,
|
||||
uiHints: props.uiHints,
|
||||
value: props.formValue,
|
||||
disabled: props.loading || !props.formValue,
|
||||
unsupportedPaths: analysis.unsupportedPaths,
|
||||
onPatch: props.onFormPatch,
|
||||
searchQuery: props.searchQuery,
|
||||
activeSection: props.activeSection,
|
||||
activeSubsection: effectiveSubsection,
|
||||
})}
|
||||
${formUnsafe
|
||||
? html`<div class="callout danger" style="margin-top: 12px;">
|
||||
Form view can't safely edit some fields.
|
||||
Use Raw to avoid losing config entries.
|
||||
</div>`
|
||||
: nothing}
|
||||
${
|
||||
props.formMode === "form"
|
||||
? html`
|
||||
${
|
||||
props.schemaLoading
|
||||
? html`
|
||||
<div class="config-loading">
|
||||
<div class="config-loading__spinner"></div>
|
||||
<span>Loading schema…</span>
|
||||
</div>
|
||||
`
|
||||
: renderConfigForm({
|
||||
schema: analysis.schema,
|
||||
uiHints: props.uiHints,
|
||||
value: props.formValue,
|
||||
disabled: props.loading || !props.formValue,
|
||||
unsupportedPaths: analysis.unsupportedPaths,
|
||||
onPatch: props.onFormPatch,
|
||||
searchQuery: props.searchQuery,
|
||||
activeSection: props.activeSection,
|
||||
activeSubsection: effectiveSubsection,
|
||||
})
|
||||
}
|
||||
${
|
||||
formUnsafe
|
||||
? html`
|
||||
<div class="callout danger" style="margin-top: 12px">
|
||||
Form view can't safely edit some fields. Use Raw to avoid losing config entries.
|
||||
</div>
|
||||
`
|
||||
: nothing
|
||||
}
|
||||
`
|
||||
: html`
|
||||
: html`
|
||||
<label class="field config-raw-field">
|
||||
<span>Raw JSON5</span>
|
||||
<textarea
|
||||
@@ -459,14 +673,17 @@ export function renderConfig(props: ConfigProps) {
|
||||
props.onRawChange((e.target as HTMLTextAreaElement).value)}
|
||||
></textarea>
|
||||
</label>
|
||||
`}
|
||||
`
|
||||
}
|
||||
</div>
|
||||
|
||||
${props.issues.length > 0
|
||||
? html`<div class="callout danger" style="margin-top: 12px;">
|
||||
${
|
||||
props.issues.length > 0
|
||||
? html`<div class="callout danger" style="margin-top: 12px;">
|
||||
<pre class="code-block">${JSON.stringify(props.issues, null, 2)}</pre>
|
||||
</div>`
|
||||
: nothing}
|
||||
: nothing
|
||||
}
|
||||
</main>
|
||||
</div>
|
||||
`;
|
||||
|
||||
+41
-31
@@ -63,11 +63,7 @@ export function renderCron(props: CronProps) {
|
||||
<div class="stat">
|
||||
<div class="stat-label">Enabled</div>
|
||||
<div class="stat-value">
|
||||
${props.status
|
||||
? props.status.enabled
|
||||
? "Yes"
|
||||
: "No"
|
||||
: "n/a"}
|
||||
${props.status ? (props.status.enabled ? "Yes" : "No") : "n/a"}
|
||||
</div>
|
||||
</div>
|
||||
<div class="stat">
|
||||
@@ -131,7 +127,8 @@ export function renderCron(props: CronProps) {
|
||||
.value=${props.form.scheduleKind}
|
||||
@change=${(e: Event) =>
|
||||
props.onFormChange({
|
||||
scheduleKind: (e.target as HTMLSelectElement).value as CronFormState["scheduleKind"],
|
||||
scheduleKind: (e.target as HTMLSelectElement)
|
||||
.value as CronFormState["scheduleKind"],
|
||||
})}
|
||||
>
|
||||
<option value="every">Every</option>
|
||||
@@ -148,7 +145,8 @@ export function renderCron(props: CronProps) {
|
||||
.value=${props.form.sessionTarget}
|
||||
@change=${(e: Event) =>
|
||||
props.onFormChange({
|
||||
sessionTarget: (e.target as HTMLSelectElement).value as CronFormState["sessionTarget"],
|
||||
sessionTarget: (e.target as HTMLSelectElement)
|
||||
.value as CronFormState["sessionTarget"],
|
||||
})}
|
||||
>
|
||||
<option value="main">Main</option>
|
||||
@@ -174,7 +172,8 @@ export function renderCron(props: CronProps) {
|
||||
.value=${props.form.payloadKind}
|
||||
@change=${(e: Event) =>
|
||||
props.onFormChange({
|
||||
payloadKind: (e.target as HTMLSelectElement).value as CronFormState["payloadKind"],
|
||||
payloadKind: (e.target as HTMLSelectElement)
|
||||
.value as CronFormState["payloadKind"],
|
||||
})}
|
||||
>
|
||||
<option value="systemEvent">System event</option>
|
||||
@@ -193,8 +192,9 @@ export function renderCron(props: CronProps) {
|
||||
rows="4"
|
||||
></textarea>
|
||||
</label>
|
||||
${props.form.payloadKind === "agentTurn"
|
||||
? html`
|
||||
${
|
||||
props.form.payloadKind === "agentTurn"
|
||||
? html`
|
||||
<div class="form-grid" style="margin-top: 12px;">
|
||||
<label class="field checkbox">
|
||||
<span>Deliver</span>
|
||||
@@ -212,9 +212,10 @@ export function renderCron(props: CronProps) {
|
||||
<select
|
||||
.value=${props.form.channel || "last"}
|
||||
@change=${(e: Event) =>
|
||||
props.onFormChange({
|
||||
channel: (e.target as HTMLSelectElement).value as CronFormState["channel"],
|
||||
})}
|
||||
props.onFormChange({
|
||||
channel: (e.target as HTMLSelectElement)
|
||||
.value as CronFormState["channel"],
|
||||
})}
|
||||
>
|
||||
${channelOptions.map(
|
||||
(channel) =>
|
||||
@@ -243,8 +244,9 @@ export function renderCron(props: CronProps) {
|
||||
})}
|
||||
/>
|
||||
</label>
|
||||
${props.form.sessionTarget === "isolated"
|
||||
? html`
|
||||
${
|
||||
props.form.sessionTarget === "isolated"
|
||||
? html`
|
||||
<label class="field">
|
||||
<span>Post to main prefix</span>
|
||||
<input
|
||||
@@ -256,10 +258,12 @@ export function renderCron(props: CronProps) {
|
||||
/>
|
||||
</label>
|
||||
`
|
||||
: nothing}
|
||||
: nothing
|
||||
}
|
||||
</div>
|
||||
`
|
||||
: nothing}
|
||||
: nothing
|
||||
}
|
||||
<div class="row" style="margin-top: 14px;">
|
||||
<button class="btn primary" ?disabled=${props.busy} @click=${props.onAdd}>
|
||||
${props.busy ? "Saving…" : "Add job"}
|
||||
@@ -271,31 +275,37 @@ export function renderCron(props: CronProps) {
|
||||
<section class="card" style="margin-top: 18px;">
|
||||
<div class="card-title">Jobs</div>
|
||||
<div class="card-sub">All scheduled jobs stored in the gateway.</div>
|
||||
${props.jobs.length === 0
|
||||
? html`<div class="muted" style="margin-top: 12px;">No jobs yet.</div>`
|
||||
: html`
|
||||
${
|
||||
props.jobs.length === 0
|
||||
? html`
|
||||
<div class="muted" style="margin-top: 12px">No jobs yet.</div>
|
||||
`
|
||||
: html`
|
||||
<div class="list" style="margin-top: 12px;">
|
||||
${props.jobs.map((job) => renderJob(job, props))}
|
||||
</div>
|
||||
`}
|
||||
`
|
||||
}
|
||||
</section>
|
||||
|
||||
<section class="card" style="margin-top: 18px;">
|
||||
<div class="card-title">Run history</div>
|
||||
<div class="card-sub">Latest runs for ${props.runsJobId ?? "(select a job)"}.</div>
|
||||
${props.runsJobId == null
|
||||
? html`
|
||||
<div class="muted" style="margin-top: 12px;">
|
||||
Select a job to inspect run history.
|
||||
</div>
|
||||
`
|
||||
: props.runs.length === 0
|
||||
? html`<div class="muted" style="margin-top: 12px;">No runs yet.</div>`
|
||||
: html`
|
||||
${
|
||||
props.runsJobId == null
|
||||
? html`
|
||||
<div class="muted" style="margin-top: 12px">Select a job to inspect run history.</div>
|
||||
`
|
||||
: props.runs.length === 0
|
||||
? html`
|
||||
<div class="muted" style="margin-top: 12px">No runs yet.</div>
|
||||
`
|
||||
: html`
|
||||
<div class="list" style="margin-top: 12px;">
|
||||
${props.runs.map((entry) => renderRun(entry))}
|
||||
</div>
|
||||
`}
|
||||
`
|
||||
}
|
||||
</section>
|
||||
`;
|
||||
}
|
||||
|
||||
+25
-20
@@ -31,11 +31,7 @@ export function renderDebug(props: DebugProps) {
|
||||
const info = securitySummary?.info ?? 0;
|
||||
const securityTone = critical > 0 ? "danger" : warn > 0 ? "warn" : "success";
|
||||
const securityLabel =
|
||||
critical > 0
|
||||
? `${critical} critical`
|
||||
: warn > 0
|
||||
? `${warn} warnings`
|
||||
: "No critical issues";
|
||||
critical > 0 ? `${critical} critical` : warn > 0 ? `${warn} warnings` : "No critical issues";
|
||||
|
||||
return html`
|
||||
<section class="grid grid-cols-2">
|
||||
@@ -52,12 +48,14 @@ export function renderDebug(props: DebugProps) {
|
||||
<div class="stack" style="margin-top: 12px;">
|
||||
<div>
|
||||
<div class="muted">Status</div>
|
||||
${securitySummary
|
||||
? html`<div class="callout ${securityTone}" style="margin-top: 8px;">
|
||||
${
|
||||
securitySummary
|
||||
? html`<div class="callout ${securityTone}" style="margin-top: 8px;">
|
||||
Security audit: ${securityLabel}${info > 0 ? ` · ${info} info` : ""}. Run
|
||||
<span class="mono">openclaw security audit --deep</span> for details.
|
||||
</div>`
|
||||
: nothing}
|
||||
: nothing
|
||||
}
|
||||
<pre class="code-block">${JSON.stringify(props.status ?? {}, null, 2)}</pre>
|
||||
</div>
|
||||
<div>
|
||||
@@ -79,8 +77,7 @@ export function renderDebug(props: DebugProps) {
|
||||
<span>Method</span>
|
||||
<input
|
||||
.value=${props.callMethod}
|
||||
@input=${(e: Event) =>
|
||||
props.onCallMethodChange((e.target as HTMLInputElement).value)}
|
||||
@input=${(e: Event) => props.onCallMethodChange((e.target as HTMLInputElement).value)}
|
||||
placeholder="system-presence"
|
||||
/>
|
||||
</label>
|
||||
@@ -97,14 +94,18 @@ export function renderDebug(props: DebugProps) {
|
||||
<div class="row" style="margin-top: 12px;">
|
||||
<button class="btn primary" @click=${props.onCall}>Call</button>
|
||||
</div>
|
||||
${props.callError
|
||||
? html`<div class="callout danger" style="margin-top: 12px;">
|
||||
${
|
||||
props.callError
|
||||
? html`<div class="callout danger" style="margin-top: 12px;">
|
||||
${props.callError}
|
||||
</div>`
|
||||
: nothing}
|
||||
${props.callResult
|
||||
? html`<pre class="code-block" style="margin-top: 12px;">${props.callResult}</pre>`
|
||||
: nothing}
|
||||
: nothing
|
||||
}
|
||||
${
|
||||
props.callResult
|
||||
? html`<pre class="code-block" style="margin-top: 12px;">${props.callResult}</pre>`
|
||||
: nothing
|
||||
}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
@@ -121,9 +122,12 @@ export function renderDebug(props: DebugProps) {
|
||||
<section class="card" style="margin-top: 18px;">
|
||||
<div class="card-title">Event Log</div>
|
||||
<div class="card-sub">Latest gateway events.</div>
|
||||
${props.eventLog.length === 0
|
||||
? html`<div class="muted" style="margin-top: 12px;">No events yet.</div>`
|
||||
: html`
|
||||
${
|
||||
props.eventLog.length === 0
|
||||
? html`
|
||||
<div class="muted" style="margin-top: 12px">No events yet.</div>
|
||||
`
|
||||
: html`
|
||||
<div class="list" style="margin-top: 12px;">
|
||||
${props.eventLog.map(
|
||||
(evt) => html`
|
||||
@@ -139,7 +143,8 @@ export function renderDebug(props: DebugProps) {
|
||||
`,
|
||||
)}
|
||||
</div>
|
||||
`}
|
||||
`
|
||||
}
|
||||
</section>
|
||||
`;
|
||||
}
|
||||
|
||||
@@ -32,9 +32,11 @@ export function renderExecApprovalPrompt(state: AppViewState) {
|
||||
<div class="exec-approval-title">Exec approval needed</div>
|
||||
<div class="exec-approval-sub">${remaining}</div>
|
||||
</div>
|
||||
${queueCount > 1
|
||||
? html`<div class="exec-approval-queue">${queueCount} pending</div>`
|
||||
: nothing}
|
||||
${
|
||||
queueCount > 1
|
||||
? html`<div class="exec-approval-queue">${queueCount} pending</div>`
|
||||
: nothing
|
||||
}
|
||||
</div>
|
||||
<div class="exec-approval-command mono">${request.command}</div>
|
||||
<div class="exec-approval-meta">
|
||||
@@ -46,9 +48,11 @@ export function renderExecApprovalPrompt(state: AppViewState) {
|
||||
${renderMetaRow("Security", request.security)}
|
||||
${renderMetaRow("Ask", request.ask)}
|
||||
</div>
|
||||
${state.execApprovalError
|
||||
? html`<div class="exec-approval-error">${state.execApprovalError}</div>`
|
||||
: nothing}
|
||||
${
|
||||
state.execApprovalError
|
||||
? html`<div class="exec-approval-error">${state.execApprovalError}</div>`
|
||||
: nothing
|
||||
}
|
||||
<div class="exec-approval-actions">
|
||||
<button
|
||||
class="btn primary"
|
||||
|
||||
@@ -23,30 +23,35 @@ export function renderInstances(props: InstancesProps) {
|
||||
${props.loading ? "Loading…" : "Refresh"}
|
||||
</button>
|
||||
</div>
|
||||
${props.lastError
|
||||
? html`<div class="callout danger" style="margin-top: 12px;">
|
||||
${
|
||||
props.lastError
|
||||
? html`<div class="callout danger" style="margin-top: 12px;">
|
||||
${props.lastError}
|
||||
</div>`
|
||||
: nothing}
|
||||
${props.statusMessage
|
||||
? html`<div class="callout" style="margin-top: 12px;">
|
||||
: nothing
|
||||
}
|
||||
${
|
||||
props.statusMessage
|
||||
? html`<div class="callout" style="margin-top: 12px;">
|
||||
${props.statusMessage}
|
||||
</div>`
|
||||
: nothing}
|
||||
: nothing
|
||||
}
|
||||
<div class="list" style="margin-top: 16px;">
|
||||
${props.entries.length === 0
|
||||
? html`<div class="muted">No instances reported yet.</div>`
|
||||
: props.entries.map((entry) => renderEntry(entry))}
|
||||
${
|
||||
props.entries.length === 0
|
||||
? html`
|
||||
<div class="muted">No instances reported yet.</div>
|
||||
`
|
||||
: props.entries.map((entry) => renderEntry(entry))
|
||||
}
|
||||
</div>
|
||||
</section>
|
||||
`;
|
||||
}
|
||||
|
||||
function renderEntry(entry: PresenceEntry) {
|
||||
const lastInput =
|
||||
entry.lastInputSeconds != null
|
||||
? `${entry.lastInputSeconds}s ago`
|
||||
: "n/a";
|
||||
const lastInput = entry.lastInputSeconds != null ? `${entry.lastInputSeconds}s ago` : "n/a";
|
||||
const mode = entry.mode ?? "unknown";
|
||||
const roles = Array.isArray(entry.roles) ? entry.roles.filter(Boolean) : [];
|
||||
const scopes = Array.isArray(entry.scopes) ? entry.scopes.filter(Boolean) : [];
|
||||
@@ -66,12 +71,12 @@ function renderEntry(entry: PresenceEntry) {
|
||||
${roles.map((role) => html`<span class="chip">${role}</span>`)}
|
||||
${scopesLabel ? html`<span class="chip">${scopesLabel}</span>` : nothing}
|
||||
${entry.platform ? html`<span class="chip">${entry.platform}</span>` : nothing}
|
||||
${entry.deviceFamily
|
||||
? html`<span class="chip">${entry.deviceFamily}</span>`
|
||||
: nothing}
|
||||
${entry.modelIdentifier
|
||||
? html`<span class="chip">${entry.modelIdentifier}</span>`
|
||||
: nothing}
|
||||
${entry.deviceFamily ? html`<span class="chip">${entry.deviceFamily}</span>` : nothing}
|
||||
${
|
||||
entry.modelIdentifier
|
||||
? html`<span class="chip">${entry.modelIdentifier}</span>`
|
||||
: nothing
|
||||
}
|
||||
${entry.version ? html`<span class="chip">${entry.version}</span>` : nothing}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
+32
-19
@@ -60,7 +60,11 @@ export function renderLogs(props: LogsProps) {
|
||||
<button
|
||||
class="btn"
|
||||
?disabled=${filtered.length === 0}
|
||||
@click=${() => props.onExport(filtered.map((entry) => entry.raw), exportLabel)}
|
||||
@click=${() =>
|
||||
props.onExport(
|
||||
filtered.map((entry) => entry.raw),
|
||||
exportLabel,
|
||||
)}
|
||||
>
|
||||
Export ${exportLabel}
|
||||
</button>
|
||||
@@ -72,8 +76,7 @@ export function renderLogs(props: LogsProps) {
|
||||
<span>Filter</span>
|
||||
<input
|
||||
.value=${props.filterText}
|
||||
@input=${(e: Event) =>
|
||||
props.onFilterTextChange((e.target as HTMLInputElement).value)}
|
||||
@input=${(e: Event) => props.onFilterTextChange((e.target as HTMLInputElement).value)}
|
||||
placeholder="Search logs"
|
||||
/>
|
||||
</label>
|
||||
@@ -104,23 +107,32 @@ export function renderLogs(props: LogsProps) {
|
||||
)}
|
||||
</div>
|
||||
|
||||
${props.file
|
||||
? html`<div class="muted" style="margin-top: 10px;">File: ${props.file}</div>`
|
||||
: nothing}
|
||||
${props.truncated
|
||||
? html`<div class="callout" style="margin-top: 10px;">
|
||||
Log output truncated; showing latest chunk.
|
||||
</div>`
|
||||
: nothing}
|
||||
${props.error
|
||||
? html`<div class="callout danger" style="margin-top: 10px;">${props.error}</div>`
|
||||
: nothing}
|
||||
${
|
||||
props.file
|
||||
? html`<div class="muted" style="margin-top: 10px;">File: ${props.file}</div>`
|
||||
: nothing
|
||||
}
|
||||
${
|
||||
props.truncated
|
||||
? html`
|
||||
<div class="callout" style="margin-top: 10px">Log output truncated; showing latest chunk.</div>
|
||||
`
|
||||
: nothing
|
||||
}
|
||||
${
|
||||
props.error
|
||||
? html`<div class="callout danger" style="margin-top: 10px;">${props.error}</div>`
|
||||
: nothing
|
||||
}
|
||||
|
||||
<div class="log-stream" style="margin-top: 12px;" @scroll=${props.onScroll}>
|
||||
${filtered.length === 0
|
||||
? html`<div class="muted" style="padding: 12px;">No log entries.</div>`
|
||||
: filtered.map(
|
||||
(entry) => html`
|
||||
${
|
||||
filtered.length === 0
|
||||
? html`
|
||||
<div class="muted" style="padding: 12px">No log entries.</div>
|
||||
`
|
||||
: filtered.map(
|
||||
(entry) => html`
|
||||
<div class="log-row">
|
||||
<div class="log-time mono">${formatTime(entry.time)}</div>
|
||||
<div class="log-level ${entry.level ?? ""}">${entry.level ?? ""}</div>
|
||||
@@ -128,7 +140,8 @@ export function renderLogs(props: LogsProps) {
|
||||
<div class="log-message mono">${entry.message ?? entry.raw}</div>
|
||||
</div>
|
||||
`,
|
||||
)}
|
||||
)
|
||||
}
|
||||
</div>
|
||||
</section>
|
||||
`;
|
||||
|
||||
@@ -21,16 +21,20 @@ export function renderMarkdownSidebar(props: MarkdownSidebarProps) {
|
||||
</button>
|
||||
</div>
|
||||
<div class="sidebar-content">
|
||||
${props.error
|
||||
? html`
|
||||
${
|
||||
props.error
|
||||
? html`
|
||||
<div class="callout danger">${props.error}</div>
|
||||
<button @click=${props.onViewRawText} class="btn" style="margin-top: 12px;">
|
||||
View Raw Text
|
||||
</button>
|
||||
`
|
||||
: props.content
|
||||
? html`<div class="sidebar-markdown">${unsafeHTML(toSanitizedMarkdownHtml(props.content))}</div>`
|
||||
: html`<div class="muted">No content available</div>`}
|
||||
: props.content
|
||||
? html`<div class="sidebar-markdown">${unsafeHTML(toSanitizedMarkdownHtml(props.content))}</div>`
|
||||
: html`
|
||||
<div class="muted">No content available</div>
|
||||
`
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
+164
-114
@@ -68,9 +68,13 @@ export function renderNodes(props: NodesProps) {
|
||||
</button>
|
||||
</div>
|
||||
<div class="list" style="margin-top: 16px;">
|
||||
${props.nodes.length === 0
|
||||
? html`<div class="muted">No nodes found.</div>`
|
||||
: props.nodes.map((n) => renderNode(n))}
|
||||
${
|
||||
props.nodes.length === 0
|
||||
? html`
|
||||
<div class="muted">No nodes found.</div>
|
||||
`
|
||||
: props.nodes.map((n) => renderNode(n))
|
||||
}
|
||||
</div>
|
||||
</section>
|
||||
`;
|
||||
@@ -91,25 +95,35 @@ function renderDevices(props: NodesProps) {
|
||||
${props.devicesLoading ? "Loading…" : "Refresh"}
|
||||
</button>
|
||||
</div>
|
||||
${props.devicesError
|
||||
? html`<div class="callout danger" style="margin-top: 12px;">${props.devicesError}</div>`
|
||||
: nothing}
|
||||
${
|
||||
props.devicesError
|
||||
? html`<div class="callout danger" style="margin-top: 12px;">${props.devicesError}</div>`
|
||||
: nothing
|
||||
}
|
||||
<div class="list" style="margin-top: 16px;">
|
||||
${pending.length > 0
|
||||
? html`
|
||||
${
|
||||
pending.length > 0
|
||||
? html`
|
||||
<div class="muted" style="margin-bottom: 8px;">Pending</div>
|
||||
${pending.map((req) => renderPendingDevice(req, props))}
|
||||
`
|
||||
: nothing}
|
||||
${paired.length > 0
|
||||
? html`
|
||||
: nothing
|
||||
}
|
||||
${
|
||||
paired.length > 0
|
||||
? html`
|
||||
<div class="muted" style="margin-top: 12px; margin-bottom: 8px;">Paired</div>
|
||||
${paired.map((device) => renderPairedDevice(device, props))}
|
||||
`
|
||||
: nothing}
|
||||
${pending.length === 0 && paired.length === 0
|
||||
? html`<div class="muted">No paired devices.</div>`
|
||||
: nothing}
|
||||
: nothing
|
||||
}
|
||||
${
|
||||
pending.length === 0 && paired.length === 0
|
||||
? html`
|
||||
<div class="muted">No paired devices.</div>
|
||||
`
|
||||
: nothing
|
||||
}
|
||||
</div>
|
||||
</section>
|
||||
`;
|
||||
@@ -156,14 +170,18 @@ function renderPairedDevice(device: PairedDevice, props: NodesProps) {
|
||||
<div class="list-title">${name}</div>
|
||||
<div class="list-sub">${device.deviceId}${ip}</div>
|
||||
<div class="muted" style="margin-top: 6px;">${roles} · ${scopes}</div>
|
||||
${tokens.length === 0
|
||||
? html`<div class="muted" style="margin-top: 6px;">Tokens: none</div>`
|
||||
: html`
|
||||
${
|
||||
tokens.length === 0
|
||||
? html`
|
||||
<div class="muted" style="margin-top: 6px">Tokens: none</div>
|
||||
`
|
||||
: html`
|
||||
<div class="muted" style="margin-top: 10px;">Tokens</div>
|
||||
<div style="display: flex; flex-direction: column; gap: 8px; margin-top: 6px;">
|
||||
${tokens.map((token) => renderTokenRow(device.deviceId, token, props))}
|
||||
</div>
|
||||
`}
|
||||
`
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
@@ -183,16 +201,18 @@ function renderTokenRow(deviceId: string, token: DeviceTokenSummary, props: Node
|
||||
>
|
||||
Rotate
|
||||
</button>
|
||||
${token.revokedAtMs
|
||||
? nothing
|
||||
: html`
|
||||
${
|
||||
token.revokedAtMs
|
||||
? nothing
|
||||
: html`
|
||||
<button
|
||||
class="btn btn--sm danger"
|
||||
@click=${() => props.onDeviceRevoke(deviceId, token.role)}
|
||||
>
|
||||
Revoke
|
||||
</button>
|
||||
`}
|
||||
`
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
@@ -389,21 +409,17 @@ function resolveExecApprovalsState(props: NodesProps): ExecApprovalsState {
|
||||
const targetNodes = resolveExecApprovalsNodes(props.nodes);
|
||||
const target = props.execApprovalsTarget;
|
||||
let targetNodeId =
|
||||
target === "node" && props.execApprovalsTargetNodeId
|
||||
? props.execApprovalsTargetNodeId
|
||||
: null;
|
||||
target === "node" && props.execApprovalsTargetNodeId ? props.execApprovalsTargetNodeId : null;
|
||||
if (target === "node" && targetNodeId && !targetNodes.some((node) => node.id === targetNodeId)) {
|
||||
targetNodeId = null;
|
||||
}
|
||||
const selectedScope = resolveExecApprovalsScope(props.execApprovalsSelectedAgent, agents);
|
||||
const selectedAgent =
|
||||
selectedScope !== EXEC_APPROVALS_DEFAULT_SCOPE
|
||||
? ((form?.agents ?? {})[selectedScope] as Record<string, unknown> | undefined) ??
|
||||
null
|
||||
? (((form?.agents ?? {})[selectedScope] as Record<string, unknown> | undefined) ?? null)
|
||||
: null;
|
||||
const allowlist = Array.isArray((selectedAgent as { allowlist?: unknown })?.allowlist)
|
||||
? ((selectedAgent as { allowlist?: ExecApprovalsAllowlistEntry[] }).allowlist ??
|
||||
[])
|
||||
? ((selectedAgent as { allowlist?: ExecApprovalsAllowlistEntry[] }).allowlist ?? [])
|
||||
: [];
|
||||
return {
|
||||
ready,
|
||||
@@ -450,20 +466,25 @@ function renderBindings(state: BindingState) {
|
||||
</button>
|
||||
</div>
|
||||
|
||||
${state.formMode === "raw"
|
||||
? html`<div class="callout warn" style="margin-top: 12px;">
|
||||
Switch the Config tab to <strong>Form</strong> mode to edit bindings here.
|
||||
</div>`
|
||||
: nothing}
|
||||
${
|
||||
state.formMode === "raw"
|
||||
? html`
|
||||
<div class="callout warn" style="margin-top: 12px">
|
||||
Switch the Config tab to <strong>Form</strong> mode to edit bindings here.
|
||||
</div>
|
||||
`
|
||||
: nothing
|
||||
}
|
||||
|
||||
${!state.ready
|
||||
? html`<div class="row" style="margin-top: 12px; gap: 12px;">
|
||||
${
|
||||
!state.ready
|
||||
? html`<div class="row" style="margin-top: 12px; gap: 12px;">
|
||||
<div class="muted">Load config to edit bindings.</div>
|
||||
<button class="btn" ?disabled=${state.configLoading} @click=${state.onLoadConfig}>
|
||||
${state.configLoading ? "Loading…" : "Load config"}
|
||||
</button>
|
||||
</div>`
|
||||
: html`
|
||||
: html`
|
||||
<div class="list" style="margin-top: 16px;">
|
||||
<div class="list-item">
|
||||
<div class="list-main">
|
||||
@@ -493,19 +514,26 @@ function renderBindings(state: BindingState) {
|
||||
)}
|
||||
</select>
|
||||
</label>
|
||||
${!supportsBinding
|
||||
? html`<div class="muted">No nodes with system.run available.</div>`
|
||||
: nothing}
|
||||
${
|
||||
!supportsBinding
|
||||
? html`
|
||||
<div class="muted">No nodes with system.run available.</div>
|
||||
`
|
||||
: nothing
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
${state.agents.length === 0
|
||||
? html`<div class="muted">No agents found.</div>`
|
||||
: state.agents.map((agent) =>
|
||||
renderAgentBinding(agent, state),
|
||||
)}
|
||||
${
|
||||
state.agents.length === 0
|
||||
? html`
|
||||
<div class="muted">No agents found.</div>
|
||||
`
|
||||
: state.agents.map((agent) => renderAgentBinding(agent, state))
|
||||
}
|
||||
</div>
|
||||
`}
|
||||
`
|
||||
}
|
||||
</section>
|
||||
`;
|
||||
}
|
||||
@@ -533,20 +561,24 @@ function renderExecApprovals(state: ExecApprovalsState) {
|
||||
|
||||
${renderExecApprovalsTarget(state)}
|
||||
|
||||
${!ready
|
||||
? html`<div class="row" style="margin-top: 12px; gap: 12px;">
|
||||
${
|
||||
!ready
|
||||
? html`<div class="row" style="margin-top: 12px; gap: 12px;">
|
||||
<div class="muted">Load exec approvals to edit allowlists.</div>
|
||||
<button class="btn" ?disabled=${state.loading || !targetReady} @click=${state.onLoad}>
|
||||
${state.loading ? "Loading…" : "Load approvals"}
|
||||
</button>
|
||||
</div>`
|
||||
: html`
|
||||
: html`
|
||||
${renderExecApprovalsTabs(state)}
|
||||
${renderExecApprovalsPolicy(state)}
|
||||
${state.selectedScope === EXEC_APPROVALS_DEFAULT_SCOPE
|
||||
? nothing
|
||||
: renderExecApprovalsAllowlist(state)}
|
||||
`}
|
||||
${
|
||||
state.selectedScope === EXEC_APPROVALS_DEFAULT_SCOPE
|
||||
? nothing
|
||||
: renderExecApprovalsAllowlist(state)
|
||||
}
|
||||
`
|
||||
}
|
||||
</section>
|
||||
`;
|
||||
}
|
||||
@@ -583,8 +615,9 @@ function renderExecApprovalsTarget(state: ExecApprovalsState) {
|
||||
<option value="node" ?selected=${state.target === "node"}>Node</option>
|
||||
</select>
|
||||
</label>
|
||||
${state.target === "node"
|
||||
? html`
|
||||
${
|
||||
state.target === "node"
|
||||
? html`
|
||||
<label class="field">
|
||||
<span>Node</span>
|
||||
<select
|
||||
@@ -608,12 +641,17 @@ function renderExecApprovalsTarget(state: ExecApprovalsState) {
|
||||
</select>
|
||||
</label>
|
||||
`
|
||||
: nothing}
|
||||
: nothing
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
${state.target === "node" && !hasNodes
|
||||
? html`<div class="muted">No nodes advertise exec approvals yet.</div>`
|
||||
: nothing}
|
||||
${
|
||||
state.target === "node" && !hasNodes
|
||||
? html`
|
||||
<div class="muted">No nodes advertise exec approvals yet.</div>
|
||||
`
|
||||
: nothing
|
||||
}
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
@@ -652,13 +690,10 @@ function renderExecApprovalsPolicy(state: ExecApprovalsState) {
|
||||
const basePath = isDefaults ? ["defaults"] : ["agents", state.selectedScope];
|
||||
const agentSecurity = typeof agent.security === "string" ? agent.security : undefined;
|
||||
const agentAsk = typeof agent.ask === "string" ? agent.ask : undefined;
|
||||
const agentAskFallback =
|
||||
typeof agent.askFallback === "string" ? agent.askFallback : undefined;
|
||||
const securityValue = isDefaults ? defaults.security : agentSecurity ?? "__default__";
|
||||
const askValue = isDefaults ? defaults.ask : agentAsk ?? "__default__";
|
||||
const askFallbackValue = isDefaults
|
||||
? defaults.askFallback
|
||||
: agentAskFallback ?? "__default__";
|
||||
const agentAskFallback = typeof agent.askFallback === "string" ? agent.askFallback : undefined;
|
||||
const securityValue = isDefaults ? defaults.security : (agentSecurity ?? "__default__");
|
||||
const askValue = isDefaults ? defaults.ask : (agentAsk ?? "__default__");
|
||||
const askFallbackValue = isDefaults ? defaults.askFallback : (agentAskFallback ?? "__default__");
|
||||
const autoOverride =
|
||||
typeof agent.autoAllowSkills === "boolean" ? agent.autoAllowSkills : undefined;
|
||||
const autoEffective = autoOverride ?? defaults.autoAllowSkills;
|
||||
@@ -670,9 +705,7 @@ function renderExecApprovalsPolicy(state: ExecApprovalsState) {
|
||||
<div class="list-main">
|
||||
<div class="list-title">Security</div>
|
||||
<div class="list-sub">
|
||||
${isDefaults
|
||||
? "Default security mode."
|
||||
: `Default: ${defaults.security}.`}
|
||||
${isDefaults ? "Default security mode." : `Default: ${defaults.security}.`}
|
||||
</div>
|
||||
</div>
|
||||
<div class="list-meta">
|
||||
@@ -690,11 +723,13 @@ function renderExecApprovalsPolicy(state: ExecApprovalsState) {
|
||||
}
|
||||
}}
|
||||
>
|
||||
${!isDefaults
|
||||
? html`<option value="__default__" ?selected=${securityValue === "__default__"}>
|
||||
${
|
||||
!isDefaults
|
||||
? html`<option value="__default__" ?selected=${securityValue === "__default__"}>
|
||||
Use default (${defaults.security})
|
||||
</option>`
|
||||
: nothing}
|
||||
: nothing
|
||||
}
|
||||
${SECURITY_OPTIONS.map(
|
||||
(option) =>
|
||||
html`<option
|
||||
@@ -731,11 +766,13 @@ function renderExecApprovalsPolicy(state: ExecApprovalsState) {
|
||||
}
|
||||
}}
|
||||
>
|
||||
${!isDefaults
|
||||
? html`<option value="__default__" ?selected=${askValue === "__default__"}>
|
||||
${
|
||||
!isDefaults
|
||||
? html`<option value="__default__" ?selected=${askValue === "__default__"}>
|
||||
Use default (${defaults.ask})
|
||||
</option>`
|
||||
: nothing}
|
||||
: nothing
|
||||
}
|
||||
${ASK_OPTIONS.map(
|
||||
(option) =>
|
||||
html`<option
|
||||
@@ -754,9 +791,11 @@ function renderExecApprovalsPolicy(state: ExecApprovalsState) {
|
||||
<div class="list-main">
|
||||
<div class="list-title">Ask fallback</div>
|
||||
<div class="list-sub">
|
||||
${isDefaults
|
||||
? "Applied when the UI prompt is unavailable."
|
||||
: `Default: ${defaults.askFallback}.`}
|
||||
${
|
||||
isDefaults
|
||||
? "Applied when the UI prompt is unavailable."
|
||||
: `Default: ${defaults.askFallback}.`
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
<div class="list-meta">
|
||||
@@ -774,11 +813,13 @@ function renderExecApprovalsPolicy(state: ExecApprovalsState) {
|
||||
}
|
||||
}}
|
||||
>
|
||||
${!isDefaults
|
||||
? html`<option value="__default__" ?selected=${askFallbackValue === "__default__"}>
|
||||
${
|
||||
!isDefaults
|
||||
? html`<option value="__default__" ?selected=${askFallbackValue === "__default__"}>
|
||||
Use default (${defaults.askFallback})
|
||||
</option>`
|
||||
: nothing}
|
||||
: nothing
|
||||
}
|
||||
${SECURITY_OPTIONS.map(
|
||||
(option) =>
|
||||
html`<option
|
||||
@@ -797,11 +838,13 @@ function renderExecApprovalsPolicy(state: ExecApprovalsState) {
|
||||
<div class="list-main">
|
||||
<div class="list-title">Auto-allow skill CLIs</div>
|
||||
<div class="list-sub">
|
||||
${isDefaults
|
||||
? "Allow skill executables listed by the Gateway."
|
||||
: autoIsDefault
|
||||
? `Using default (${defaults.autoAllowSkills ? "on" : "off"}).`
|
||||
: `Override (${autoEffective ? "on" : "off"}).`}
|
||||
${
|
||||
isDefaults
|
||||
? "Allow skill executables listed by the Gateway."
|
||||
: autoIsDefault
|
||||
? `Using default (${defaults.autoAllowSkills ? "on" : "off"}).`
|
||||
: `Override (${autoEffective ? "on" : "off"}).`
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
<div class="list-meta">
|
||||
@@ -817,15 +860,17 @@ function renderExecApprovalsPolicy(state: ExecApprovalsState) {
|
||||
}}
|
||||
/>
|
||||
</label>
|
||||
${!isDefaults && !autoIsDefault
|
||||
? html`<button
|
||||
${
|
||||
!isDefaults && !autoIsDefault
|
||||
? html`<button
|
||||
class="btn btn--sm"
|
||||
?disabled=${state.disabled}
|
||||
@click=${() => state.onRemove([...basePath, "autoAllowSkills"])}
|
||||
>
|
||||
Use default
|
||||
</button>`
|
||||
: nothing}
|
||||
: nothing
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -853,11 +898,13 @@ function renderExecApprovalsAllowlist(state: ExecApprovalsState) {
|
||||
</button>
|
||||
</div>
|
||||
<div class="list" style="margin-top: 12px;">
|
||||
${entries.length === 0
|
||||
? html`<div class="muted">No allowlist entries yet.</div>`
|
||||
: entries.map((entry, index) =>
|
||||
renderAllowlistEntry(state, entry, index),
|
||||
)}
|
||||
${
|
||||
entries.length === 0
|
||||
? html`
|
||||
<div class="muted">No allowlist entries yet.</div>
|
||||
`
|
||||
: entries.map((entry, index) => renderAllowlistEntry(state, entry, index))
|
||||
}
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
@@ -868,12 +915,8 @@ function renderAllowlistEntry(
|
||||
index: number,
|
||||
) {
|
||||
const lastUsed = entry.lastUsedAt ? formatAgo(entry.lastUsedAt) : "never";
|
||||
const lastCommand = entry.lastUsedCommand
|
||||
? clampText(entry.lastUsedCommand, 120)
|
||||
: null;
|
||||
const lastPath = entry.lastResolvedPath
|
||||
? clampText(entry.lastResolvedPath, 120)
|
||||
: null;
|
||||
const lastCommand = entry.lastUsedCommand ? clampText(entry.lastUsedCommand, 120) : null;
|
||||
const lastPath = entry.lastResolvedPath ? clampText(entry.lastResolvedPath, 120) : null;
|
||||
return html`
|
||||
<div class="list-item">
|
||||
<div class="list-main">
|
||||
@@ -926,9 +969,11 @@ function renderAgentBinding(agent: BindingAgent, state: BindingState) {
|
||||
<div class="list-title">${label}</div>
|
||||
<div class="list-sub">
|
||||
${agent.isDefault ? "default agent" : "agent"} ·
|
||||
${bindingValue === "__default__"
|
||||
? `uses default (${state.defaultBinding ?? "any"})`
|
||||
: `override: ${agent.binding}`}
|
||||
${
|
||||
bindingValue === "__default__"
|
||||
? `uses default (${state.defaultBinding ?? "any"})`
|
||||
: `override: ${agent.binding}`
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
<div class="list-meta">
|
||||
@@ -973,18 +1018,24 @@ function resolveExecNodes(nodes: Array<Record<string, unknown>>): BindingNode[]
|
||||
typeof node.displayName === "string" && node.displayName.trim()
|
||||
? node.displayName.trim()
|
||||
: nodeId;
|
||||
list.push({ id: nodeId, label: displayName === nodeId ? nodeId : `${displayName} · ${nodeId}` });
|
||||
list.push({
|
||||
id: nodeId,
|
||||
label: displayName === nodeId ? nodeId : `${displayName} · ${nodeId}`,
|
||||
});
|
||||
}
|
||||
list.sort((a, b) => a.label.localeCompare(b.label));
|
||||
return list;
|
||||
}
|
||||
|
||||
function resolveExecApprovalsNodes(nodes: Array<Record<string, unknown>>): ExecApprovalsTargetNode[] {
|
||||
function resolveExecApprovalsNodes(
|
||||
nodes: Array<Record<string, unknown>>,
|
||||
): ExecApprovalsTargetNode[] {
|
||||
const list: ExecApprovalsTargetNode[] = [];
|
||||
for (const node of nodes) {
|
||||
const commands = Array.isArray(node.commands) ? node.commands : [];
|
||||
const supports = commands.some(
|
||||
(cmd) => String(cmd) === "system.execApprovals.get" || String(cmd) === "system.execApprovals.set",
|
||||
(cmd) =>
|
||||
String(cmd) === "system.execApprovals.get" || String(cmd) === "system.execApprovals.set",
|
||||
);
|
||||
if (!supports) continue;
|
||||
const nodeId = typeof node.nodeId === "string" ? node.nodeId.trim() : "";
|
||||
@@ -993,7 +1044,10 @@ function resolveExecApprovalsNodes(nodes: Array<Record<string, unknown>>): ExecA
|
||||
typeof node.displayName === "string" && node.displayName.trim()
|
||||
? node.displayName.trim()
|
||||
: nodeId;
|
||||
list.push({ id: nodeId, label: displayName === nodeId ? nodeId : `${displayName} · ${nodeId}` });
|
||||
list.push({
|
||||
id: nodeId,
|
||||
label: displayName === nodeId ? nodeId : `${displayName} · ${nodeId}`,
|
||||
});
|
||||
}
|
||||
list.sort((a, b) => a.label.localeCompare(b.label));
|
||||
return list;
|
||||
@@ -1035,9 +1089,7 @@ function resolveAgentBindings(config: Record<string, unknown> | null): {
|
||||
const toolsEntry = (record.tools ?? {}) as Record<string, unknown>;
|
||||
const execEntry = (toolsEntry.exec ?? {}) as Record<string, unknown>;
|
||||
const binding =
|
||||
typeof execEntry.node === "string" && execEntry.node.trim()
|
||||
? execEntry.node.trim()
|
||||
: null;
|
||||
typeof execEntry.node === "string" && execEntry.node.trim() ? execEntry.node.trim() : null;
|
||||
agents.push({
|
||||
id,
|
||||
name: name || undefined,
|
||||
@@ -1077,9 +1129,7 @@ function renderNode(node: Record<string, unknown>) {
|
||||
${connected ? "connected" : "offline"}
|
||||
</span>
|
||||
${caps.slice(0, 12).map((c) => html`<span class="chip">${String(c)}</span>`)}
|
||||
${commands
|
||||
.slice(0, 8)
|
||||
.map((c) => html`<span class="chip">${String(c)}</span>`)}
|
||||
${commands.slice(0, 8).map((c) => html`<span class="chip">${String(c)}</span>`)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
+23
-28
@@ -28,9 +28,7 @@ export function renderOverview(props: OverviewProps) {
|
||||
| { uptimeMs?: number; policy?: { tickIntervalMs?: number } }
|
||||
| undefined;
|
||||
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 = (() => {
|
||||
if (props.connected || !props.lastError) return null;
|
||||
const lower = props.lastError.toLowerCase();
|
||||
@@ -40,13 +38,13 @@ export function renderOverview(props: OverviewProps) {
|
||||
const hasPassword = Boolean(props.password.trim());
|
||||
if (!hasToken && !hasPassword) {
|
||||
return html`
|
||||
<div class="muted" style="margin-top: 8px;">
|
||||
<div class="muted" style="margin-top: 8px">
|
||||
This gateway requires auth. Add a token or password, then click Connect.
|
||||
<div style="margin-top: 6px;">
|
||||
<div style="margin-top: 6px">
|
||||
<span class="mono">openclaw dashboard --no-open</span> → tokenized URL<br />
|
||||
<span class="mono">openclaw doctor --generate-gateway-token</span> → set token
|
||||
</div>
|
||||
<div style="margin-top: 6px;">
|
||||
<div style="margin-top: 6px">
|
||||
<a
|
||||
class="session-link"
|
||||
href="https://docs.openclaw.ai/web/dashboard"
|
||||
@@ -60,11 +58,10 @@ export function renderOverview(props: OverviewProps) {
|
||||
`;
|
||||
}
|
||||
return html`
|
||||
<div class="muted" style="margin-top: 8px;">
|
||||
<div class="muted" style="margin-top: 8px">
|
||||
Auth failed. Re-copy a tokenized URL with
|
||||
<span class="mono">openclaw dashboard --no-open</span>, or update the token,
|
||||
then click Connect.
|
||||
<div style="margin-top: 6px;">
|
||||
<span class="mono">openclaw dashboard --no-open</span>, or update the token, then click Connect.
|
||||
<div style="margin-top: 6px">
|
||||
<a
|
||||
class="session-link"
|
||||
href="https://docs.openclaw.ai/web/dashboard"
|
||||
@@ -86,14 +83,14 @@ export function renderOverview(props: OverviewProps) {
|
||||
return null;
|
||||
}
|
||||
return html`
|
||||
<div class="muted" style="margin-top: 8px;">
|
||||
This page is HTTP, so the browser blocks device identity. Use HTTPS (Tailscale Serve) or
|
||||
open <span class="mono">http://127.0.0.1:18789</span> on the gateway host.
|
||||
<div style="margin-top: 6px;">
|
||||
<div class="muted" style="margin-top: 8px">
|
||||
This page is HTTP, so the browser blocks device identity. Use HTTPS (Tailscale Serve) or open
|
||||
<span class="mono">http://127.0.0.1:18789</span> on the gateway host.
|
||||
<div style="margin-top: 6px">
|
||||
If you must stay on HTTP, set
|
||||
<span class="mono">gateway.controlUi.allowInsecureAuth: true</span> (token-only).
|
||||
</div>
|
||||
<div style="margin-top: 6px;">
|
||||
<div style="margin-top: 6px">
|
||||
<a
|
||||
class="session-link"
|
||||
href="https://docs.openclaw.ai/gateway/tailscale"
|
||||
@@ -195,21 +192,23 @@ export function renderOverview(props: OverviewProps) {
|
||||
<div class="stat">
|
||||
<div class="stat-label">Last Channels Refresh</div>
|
||||
<div class="stat-value">
|
||||
${props.lastChannelsRefresh
|
||||
? formatAgo(props.lastChannelsRefresh)
|
||||
: "n/a"}
|
||||
${props.lastChannelsRefresh ? formatAgo(props.lastChannelsRefresh) : "n/a"}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
${props.lastError
|
||||
? html`<div class="callout danger" style="margin-top: 14px;">
|
||||
${
|
||||
props.lastError
|
||||
? html`<div class="callout danger" style="margin-top: 14px;">
|
||||
<div>${props.lastError}</div>
|
||||
${authHint ?? ""}
|
||||
${insecureContextHint ?? ""}
|
||||
</div>`
|
||||
: html`<div class="callout" style="margin-top: 14px;">
|
||||
Use Channels to link WhatsApp, Telegram, Discord, Signal, or iMessage.
|
||||
</div>`}
|
||||
: html`
|
||||
<div class="callout" style="margin-top: 14px">
|
||||
Use Channels to link WhatsApp, Telegram, Discord, Signal, or iMessage.
|
||||
</div>
|
||||
`
|
||||
}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
@@ -227,11 +226,7 @@ export function renderOverview(props: OverviewProps) {
|
||||
<div class="card stat-card">
|
||||
<div class="stat-label">Cron</div>
|
||||
<div class="stat-value">
|
||||
${props.cronEnabled == null
|
||||
? "n/a"
|
||||
: props.cronEnabled
|
||||
? "Enabled"
|
||||
: "Disabled"}
|
||||
${props.cronEnabled == null ? "n/a" : props.cronEnabled ? "Enabled" : "Disabled"}
|
||||
</div>
|
||||
<div class="muted">Next wake ${formatNextRun(props.cronNext)}</div>
|
||||
</div>
|
||||
|
||||
+20
-16
@@ -141,9 +141,11 @@ export function renderSessions(props: SessionsProps) {
|
||||
</label>
|
||||
</div>
|
||||
|
||||
${props.error
|
||||
? html`<div class="callout danger" style="margin-top: 12px;">${props.error}</div>`
|
||||
: nothing}
|
||||
${
|
||||
props.error
|
||||
? html`<div class="callout danger" style="margin-top: 12px;">${props.error}</div>`
|
||||
: nothing
|
||||
}
|
||||
|
||||
<div class="muted" style="margin-top: 12px;">
|
||||
${props.result ? `Store: ${props.result.path}` : ""}
|
||||
@@ -161,11 +163,15 @@ export function renderSessions(props: SessionsProps) {
|
||||
<div>Reasoning</div>
|
||||
<div>Actions</div>
|
||||
</div>
|
||||
${rows.length === 0
|
||||
? html`<div class="muted">No sessions found.</div>`
|
||||
: rows.map((row) =>
|
||||
renderRow(row, props.basePath, props.onPatch, props.onDelete, props.loading),
|
||||
)}
|
||||
${
|
||||
rows.length === 0
|
||||
? html`
|
||||
<div class="muted">No sessions found.</div>
|
||||
`
|
||||
: rows.map((row) =>
|
||||
renderRow(row, props.basePath, props.onPatch, props.onDelete, props.loading),
|
||||
)
|
||||
}
|
||||
</div>
|
||||
</section>
|
||||
`;
|
||||
@@ -193,9 +199,9 @@ function renderRow(
|
||||
|
||||
return html`
|
||||
<div class="table-row">
|
||||
<div class="mono">${canLink
|
||||
? html`<a href=${chatUrl} class="session-link">${displayName}</a>`
|
||||
: displayName}</div>
|
||||
<div class="mono">${
|
||||
canLink ? html`<a href=${chatUrl} class="session-link">${displayName}</a>` : displayName
|
||||
}</div>
|
||||
<div>
|
||||
<input
|
||||
.value=${row.label ?? ""}
|
||||
@@ -221,9 +227,7 @@ function renderRow(
|
||||
});
|
||||
}}
|
||||
>
|
||||
${thinkLevels.map((level) =>
|
||||
html`<option value=${level}>${level || "inherit"}</option>`,
|
||||
)}
|
||||
${thinkLevels.map((level) => html`<option value=${level}>${level || "inherit"}</option>`)}
|
||||
</select>
|
||||
</div>
|
||||
<div>
|
||||
@@ -249,8 +253,8 @@ function renderRow(
|
||||
onPatch(row.key, { reasoningLevel: value || null });
|
||||
}}
|
||||
>
|
||||
${REASONING_LEVELS.map((level) =>
|
||||
html`<option value=${level}>${level || "inherit"}</option>`,
|
||||
${REASONING_LEVELS.map(
|
||||
(level) => html`<option value=${level}>${level || "inherit"}</option>`,
|
||||
)}
|
||||
</select>
|
||||
</div>
|
||||
|
||||
+49
-33
@@ -25,10 +25,7 @@ export function renderSkills(props: SkillsProps) {
|
||||
const filter = props.filter.trim().toLowerCase();
|
||||
const filtered = filter
|
||||
? skills.filter((skill) =>
|
||||
[skill.name, skill.description, skill.source]
|
||||
.join(" ")
|
||||
.toLowerCase()
|
||||
.includes(filter),
|
||||
[skill.name, skill.description, skill.source].join(" ").toLowerCase().includes(filter),
|
||||
)
|
||||
: skills;
|
||||
|
||||
@@ -49,25 +46,30 @@ export function renderSkills(props: SkillsProps) {
|
||||
<span>Filter</span>
|
||||
<input
|
||||
.value=${props.filter}
|
||||
@input=${(e: Event) =>
|
||||
props.onFilterChange((e.target as HTMLInputElement).value)}
|
||||
@input=${(e: Event) => props.onFilterChange((e.target as HTMLInputElement).value)}
|
||||
placeholder="Search skills"
|
||||
/>
|
||||
</label>
|
||||
<div class="muted">${filtered.length} shown</div>
|
||||
</div>
|
||||
|
||||
${props.error
|
||||
? html`<div class="callout danger" style="margin-top: 12px;">${props.error}</div>`
|
||||
: nothing}
|
||||
${
|
||||
props.error
|
||||
? html`<div class="callout danger" style="margin-top: 12px;">${props.error}</div>`
|
||||
: nothing
|
||||
}
|
||||
|
||||
${filtered.length === 0
|
||||
? html`<div class="muted" style="margin-top: 16px;">No skills found.</div>`
|
||||
: html`
|
||||
${
|
||||
filtered.length === 0
|
||||
? html`
|
||||
<div class="muted" style="margin-top: 16px">No skills found.</div>
|
||||
`
|
||||
: html`
|
||||
<div class="list" style="margin-top: 16px;">
|
||||
${filtered.map((skill) => renderSkill(skill, props))}
|
||||
</div>
|
||||
`}
|
||||
`
|
||||
}
|
||||
</section>
|
||||
`;
|
||||
}
|
||||
@@ -76,8 +78,7 @@ function renderSkill(skill: SkillStatusEntry, props: SkillsProps) {
|
||||
const busy = props.busyKey === skill.skillKey;
|
||||
const apiKey = props.edits[skill.skillKey] ?? "";
|
||||
const message = props.messages[skill.skillKey] ?? null;
|
||||
const canInstall =
|
||||
skill.install.length > 0 && skill.missing.bins.length > 0;
|
||||
const canInstall = skill.install.length > 0 && skill.missing.bins.length > 0;
|
||||
const missing = [
|
||||
...skill.missing.bins.map((b) => `bin:${b}`),
|
||||
...skill.missing.env.map((e) => `env:${e}`),
|
||||
@@ -99,22 +100,32 @@ function renderSkill(skill: SkillStatusEntry, props: SkillsProps) {
|
||||
<span class="chip ${skill.eligible ? "chip-ok" : "chip-warn"}">
|
||||
${skill.eligible ? "eligible" : "blocked"}
|
||||
</span>
|
||||
${skill.disabled ? html`<span class="chip chip-warn">disabled</span>` : nothing}
|
||||
${
|
||||
skill.disabled
|
||||
? html`
|
||||
<span class="chip chip-warn">disabled</span>
|
||||
`
|
||||
: nothing
|
||||
}
|
||||
</div>
|
||||
${missing.length > 0
|
||||
? html`
|
||||
${
|
||||
missing.length > 0
|
||||
? html`
|
||||
<div class="muted" style="margin-top: 6px;">
|
||||
Missing: ${missing.join(", ")}
|
||||
</div>
|
||||
`
|
||||
: nothing}
|
||||
${reasons.length > 0
|
||||
? html`
|
||||
: nothing
|
||||
}
|
||||
${
|
||||
reasons.length > 0
|
||||
? html`
|
||||
<div class="muted" style="margin-top: 6px;">
|
||||
Reason: ${reasons.join(", ")}
|
||||
</div>
|
||||
`
|
||||
: nothing}
|
||||
: nothing
|
||||
}
|
||||
</div>
|
||||
<div class="list-meta">
|
||||
<div class="row" style="justify-content: flex-end; flex-wrap: wrap;">
|
||||
@@ -125,19 +136,21 @@ function renderSkill(skill: SkillStatusEntry, props: SkillsProps) {
|
||||
>
|
||||
${skill.disabled ? "Enable" : "Disable"}
|
||||
</button>
|
||||
${canInstall
|
||||
? html`<button
|
||||
${
|
||||
canInstall
|
||||
? html`<button
|
||||
class="btn"
|
||||
?disabled=${busy}
|
||||
@click=${() =>
|
||||
props.onInstall(skill.skillKey, skill.name, skill.install[0].id)}
|
||||
@click=${() => props.onInstall(skill.skillKey, skill.name, skill.install[0].id)}
|
||||
>
|
||||
${busy ? "Installing…" : skill.install[0].label}
|
||||
</button>`
|
||||
: nothing}
|
||||
: nothing
|
||||
}
|
||||
</div>
|
||||
${message
|
||||
? html`<div
|
||||
${
|
||||
message
|
||||
? html`<div
|
||||
class="muted"
|
||||
style="margin-top: 8px; color: ${
|
||||
message.kind === "error"
|
||||
@@ -147,9 +160,11 @@ function renderSkill(skill: SkillStatusEntry, props: SkillsProps) {
|
||||
>
|
||||
${message.message}
|
||||
</div>`
|
||||
: nothing}
|
||||
${skill.primaryEnv
|
||||
? html`
|
||||
: nothing
|
||||
}
|
||||
${
|
||||
skill.primaryEnv
|
||||
? html`
|
||||
<div class="field" style="margin-top: 10px;">
|
||||
<span>API key</span>
|
||||
<input
|
||||
@@ -168,7 +183,8 @@ function renderSkill(skill: SkillStatusEntry, props: SkillsProps) {
|
||||
Save key
|
||||
</button>
|
||||
`
|
||||
: nothing}
|
||||
: nothing
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
Reference in New Issue
Block a user