+ View ${R.length} pending change${R.length!==1?"s":""} + +
+${JSON.stringify(e.issues,null,2)}
+ From bf6df6d6b72f2105443d7d5aa648f782e0793ee2 Mon Sep 17 00:00:00 2001
From: Dominic Damoah
- Any OS + WhatsApp/Telegram/Discord/iMessage gateway for AI agents (Pi).
+ Any OS + WhatsApp/Telegram/Discord/Mattermost/iMessage gateway for AI agents (Pi).
Send a message, get an agent response — from your pocket.
` or use allowlists.
diff --git a/docs/web/control-ui.md b/docs/web/control-ui.md
index b1102c47d..5786c696e 100644
--- a/docs/web/control-ui.md
+++ b/docs/web/control-ui.md
@@ -30,7 +30,7 @@ The onboarding wizard generates a gateway token by default, so paste it here on
## What it can do (today)
- Chat with the model via Gateway WS (`chat.history`, `chat.send`, `chat.abort`, `chat.inject`)
- Stream tool calls + live tool output cards in Chat (agent events)
-- Channels: WhatsApp/Telegram status + QR login + per-channel config (`channels.status`, `web.login.*`, `config.patch`)
+- Channels: WhatsApp/Telegram/Discord/Slack/Mattermost status + QR login + per-channel config (`channels.status`, `web.login.*`, `config.patch`)
- Instances: presence list + refresh (`system-presence`)
- Sessions: list + per-session thinking/verbose overrides (`sessions.list`, `sessions.patch`)
- Cron jobs: list/add/run/enable/disable + run history (`cron.*`)
diff --git a/extensions/mattermost/clawdbot.plugin.json b/extensions/mattermost/clawdbot.plugin.json
new file mode 100644
index 000000000..ddb3f8160
--- /dev/null
+++ b/extensions/mattermost/clawdbot.plugin.json
@@ -0,0 +1,11 @@
+{
+ "id": "mattermost",
+ "channels": [
+ "mattermost"
+ ],
+ "configSchema": {
+ "type": "object",
+ "additionalProperties": false,
+ "properties": {}
+ }
+}
diff --git a/extensions/mattermost/index.ts b/extensions/mattermost/index.ts
new file mode 100644
index 000000000..f3bf17ad5
--- /dev/null
+++ b/extensions/mattermost/index.ts
@@ -0,0 +1,18 @@
+import type { ClawdbotPluginApi } from "clawdbot/plugin-sdk";
+import { emptyPluginConfigSchema } from "clawdbot/plugin-sdk";
+
+import { mattermostPlugin } from "./src/channel.js";
+import { setMattermostRuntime } from "./src/runtime.js";
+
+const plugin = {
+ id: "mattermost",
+ name: "Mattermost",
+ description: "Mattermost channel plugin",
+ configSchema: emptyPluginConfigSchema(),
+ register(api: ClawdbotPluginApi) {
+ setMattermostRuntime(api.runtime);
+ api.registerChannel({ plugin: mattermostPlugin });
+ },
+};
+
+export default plugin;
diff --git a/extensions/mattermost/package.json b/extensions/mattermost/package.json
new file mode 100644
index 000000000..8ba462f45
--- /dev/null
+++ b/extensions/mattermost/package.json
@@ -0,0 +1,11 @@
+{
+ "name": "@clawdbot/mattermost",
+ "version": "2026.1.20-2",
+ "type": "module",
+ "description": "Clawdbot Mattermost channel plugin",
+ "clawdbot": {
+ "extensions": [
+ "./index.ts"
+ ]
+ }
+}
diff --git a/extensions/mattermost/src/channel.ts b/extensions/mattermost/src/channel.ts
new file mode 100644
index 000000000..840772a17
--- /dev/null
+++ b/extensions/mattermost/src/channel.ts
@@ -0,0 +1,270 @@
+import {
+ applyAccountNameToChannelSection,
+ buildChannelConfigSchema,
+ DEFAULT_ACCOUNT_ID,
+ deleteAccountFromConfigSection,
+ getChatChannelMeta,
+ listMattermostAccountIds,
+ looksLikeMattermostTargetId,
+ migrateBaseNameToDefaultAccount,
+ normalizeAccountId,
+ normalizeMattermostBaseUrl,
+ normalizeMattermostMessagingTarget,
+ resolveDefaultMattermostAccountId,
+ resolveMattermostAccount,
+ resolveMattermostGroupRequireMention,
+ setAccountEnabledInConfigSection,
+ mattermostOnboardingAdapter,
+ MattermostConfigSchema,
+ type ChannelPlugin,
+ type ResolvedMattermostAccount,
+} from "clawdbot/plugin-sdk";
+
+import { getMattermostRuntime } from "./runtime.js";
+
+const meta = getChatChannelMeta("mattermost");
+
+export const mattermostPlugin: ChannelPlugin = {
+ id: "mattermost",
+ meta: {
+ ...meta,
+ },
+ onboarding: mattermostOnboardingAdapter,
+ capabilities: {
+ chatTypes: ["direct", "channel", "group", "thread"],
+ threads: true,
+ media: true,
+ },
+ streaming: {
+ blockStreamingCoalesceDefaults: { minChars: 1500, idleMs: 1000 },
+ },
+ reload: { configPrefixes: ["channels.mattermost"] },
+ configSchema: buildChannelConfigSchema(MattermostConfigSchema),
+ config: {
+ listAccountIds: (cfg) => listMattermostAccountIds(cfg),
+ resolveAccount: (cfg, accountId) => resolveMattermostAccount({ cfg, accountId }),
+ defaultAccountId: (cfg) => resolveDefaultMattermostAccountId(cfg),
+ setAccountEnabled: ({ cfg, accountId, enabled }) =>
+ setAccountEnabledInConfigSection({
+ cfg,
+ sectionKey: "mattermost",
+ accountId,
+ enabled,
+ allowTopLevel: true,
+ }),
+ deleteAccount: ({ cfg, accountId }) =>
+ deleteAccountFromConfigSection({
+ cfg,
+ sectionKey: "mattermost",
+ accountId,
+ clearBaseFields: ["botToken", "baseUrl", "name"],
+ }),
+ isConfigured: (account) => Boolean(account.botToken && account.baseUrl),
+ describeAccount: (account) => ({
+ accountId: account.accountId,
+ name: account.name,
+ enabled: account.enabled,
+ configured: Boolean(account.botToken && account.baseUrl),
+ botTokenSource: account.botTokenSource,
+ baseUrl: account.baseUrl,
+ }),
+ },
+ groups: {
+ resolveRequireMention: resolveMattermostGroupRequireMention,
+ },
+ messaging: {
+ normalizeTarget: normalizeMattermostMessagingTarget,
+ targetResolver: {
+ looksLikeId: looksLikeMattermostTargetId,
+ hint: "",
+ },
+ },
+ outbound: {
+ deliveryMode: "direct",
+ chunker: (text, limit) => getMattermostRuntime().channel.text.chunkMarkdownText(text, limit),
+ textChunkLimit: 4000,
+ resolveTarget: ({ to }) => {
+ const trimmed = to?.trim();
+ if (!trimmed) {
+ return {
+ ok: false,
+ error: new Error(
+ "Delivering to Mattermost requires --to ",
+ ),
+ };
+ }
+ return { ok: true, to: trimmed };
+ },
+ sendText: async ({ to, text, accountId, deps, replyToId }) => {
+ const send =
+ deps?.sendMattermost ?? getMattermostRuntime().channel.mattermost.sendMessageMattermost;
+ const result = await send(to, text, {
+ accountId: accountId ?? undefined,
+ replyToId: replyToId ?? undefined,
+ });
+ return { channel: "mattermost", ...result };
+ },
+ sendMedia: async ({ to, text, mediaUrl, accountId, deps, replyToId }) => {
+ const send =
+ deps?.sendMattermost ?? getMattermostRuntime().channel.mattermost.sendMessageMattermost;
+ const result = await send(to, text, {
+ accountId: accountId ?? undefined,
+ mediaUrl,
+ replyToId: replyToId ?? undefined,
+ });
+ return { channel: "mattermost", ...result };
+ },
+ },
+ status: {
+ defaultRuntime: {
+ accountId: DEFAULT_ACCOUNT_ID,
+ running: false,
+ connected: false,
+ lastConnectedAt: null,
+ lastDisconnect: null,
+ lastStartAt: null,
+ lastStopAt: null,
+ lastError: null,
+ },
+ buildChannelSummary: ({ snapshot }) => ({
+ configured: snapshot.configured ?? false,
+ botTokenSource: snapshot.botTokenSource ?? "none",
+ running: snapshot.running ?? false,
+ connected: snapshot.connected ?? false,
+ lastStartAt: snapshot.lastStartAt ?? null,
+ lastStopAt: snapshot.lastStopAt ?? null,
+ lastError: snapshot.lastError ?? null,
+ baseUrl: snapshot.baseUrl ?? null,
+ probe: snapshot.probe,
+ lastProbeAt: snapshot.lastProbeAt ?? null,
+ }),
+ probeAccount: async ({ account, timeoutMs }) => {
+ const token = account.botToken?.trim();
+ const baseUrl = account.baseUrl?.trim();
+ if (!token || !baseUrl) {
+ return { ok: false, error: "bot token or baseUrl missing" };
+ }
+ return await getMattermostRuntime().channel.mattermost.probeMattermost(
+ baseUrl,
+ token,
+ timeoutMs,
+ );
+ },
+ buildAccountSnapshot: ({ account, runtime, probe }) => ({
+ accountId: account.accountId,
+ name: account.name,
+ enabled: account.enabled,
+ configured: Boolean(account.botToken && account.baseUrl),
+ botTokenSource: account.botTokenSource,
+ baseUrl: account.baseUrl,
+ running: runtime?.running ?? false,
+ connected: runtime?.connected ?? false,
+ lastConnectedAt: runtime?.lastConnectedAt ?? null,
+ lastDisconnect: runtime?.lastDisconnect ?? null,
+ lastStartAt: runtime?.lastStartAt ?? null,
+ lastStopAt: runtime?.lastStopAt ?? null,
+ lastError: runtime?.lastError ?? null,
+ probe,
+ lastInboundAt: runtime?.lastInboundAt ?? null,
+ lastOutboundAt: runtime?.lastOutboundAt ?? null,
+ }),
+ },
+ setup: {
+ resolveAccountId: ({ accountId }) => normalizeAccountId(accountId),
+ applyAccountName: ({ cfg, accountId, name }) =>
+ applyAccountNameToChannelSection({
+ cfg,
+ channelKey: "mattermost",
+ accountId,
+ name,
+ }),
+ validateInput: ({ accountId, input }) => {
+ if (input.useEnv && accountId !== DEFAULT_ACCOUNT_ID) {
+ return "Mattermost env vars can only be used for the default account.";
+ }
+ const token = input.botToken ?? input.token;
+ const baseUrl = input.httpUrl;
+ if (!input.useEnv && (!token || !baseUrl)) {
+ return "Mattermost requires --bot-token and --http-url (or --use-env).";
+ }
+ if (baseUrl && !normalizeMattermostBaseUrl(baseUrl)) {
+ return "Mattermost --http-url must include a valid base URL.";
+ }
+ return null;
+ },
+ applyAccountConfig: ({ cfg, accountId, input }) => {
+ const token = input.botToken ?? input.token;
+ const baseUrl = input.httpUrl?.trim();
+ const namedConfig = applyAccountNameToChannelSection({
+ cfg,
+ channelKey: "mattermost",
+ accountId,
+ name: input.name,
+ });
+ const next =
+ accountId !== DEFAULT_ACCOUNT_ID
+ ? migrateBaseNameToDefaultAccount({
+ cfg: namedConfig,
+ channelKey: "mattermost",
+ })
+ : namedConfig;
+ if (accountId === DEFAULT_ACCOUNT_ID) {
+ return {
+ ...next,
+ channels: {
+ ...next.channels,
+ mattermost: {
+ ...next.channels?.mattermost,
+ enabled: true,
+ ...(input.useEnv
+ ? {}
+ : {
+ ...(token ? { botToken: token } : {}),
+ ...(baseUrl ? { baseUrl } : {}),
+ }),
+ },
+ },
+ };
+ }
+ return {
+ ...next,
+ channels: {
+ ...next.channels,
+ mattermost: {
+ ...next.channels?.mattermost,
+ enabled: true,
+ accounts: {
+ ...next.channels?.mattermost?.accounts,
+ [accountId]: {
+ ...next.channels?.mattermost?.accounts?.[accountId],
+ enabled: true,
+ ...(token ? { botToken: token } : {}),
+ ...(baseUrl ? { baseUrl } : {}),
+ },
+ },
+ },
+ },
+ };
+ },
+ },
+ gateway: {
+ startAccount: async (ctx) => {
+ const account = ctx.account;
+ ctx.setStatus({
+ accountId: account.accountId,
+ baseUrl: account.baseUrl,
+ botTokenSource: account.botTokenSource,
+ });
+ ctx.log?.info(`[${account.accountId}] starting channel`);
+ return getMattermostRuntime().channel.mattermost.monitorMattermostProvider({
+ botToken: account.botToken ?? undefined,
+ baseUrl: account.baseUrl ?? undefined,
+ accountId: account.accountId,
+ config: ctx.cfg,
+ runtime: ctx.runtime,
+ abortSignal: ctx.abortSignal,
+ statusSink: (patch) => ctx.setStatus({ accountId: ctx.accountId, ...patch }),
+ });
+ },
+ },
+};
diff --git a/extensions/mattermost/src/runtime.ts b/extensions/mattermost/src/runtime.ts
new file mode 100644
index 000000000..3d0ad283f
--- /dev/null
+++ b/extensions/mattermost/src/runtime.ts
@@ -0,0 +1,14 @@
+import type { PluginRuntime } from "clawdbot/plugin-sdk";
+
+let runtime: PluginRuntime | null = null;
+
+export function setMattermostRuntime(next: PluginRuntime) {
+ runtime = next;
+}
+
+export function getMattermostRuntime(): PluginRuntime {
+ if (!runtime) {
+ throw new Error("Mattermost runtime not initialized");
+ }
+ return runtime;
+}
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index c0830488b..7c2c788b2 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -324,6 +324,8 @@ importers:
specifier: ^11.10.6
version: 11.10.6
+ extensions/mattermost: {}
+
extensions/memory-core:
dependencies:
clawdbot:
diff --git a/src/auto-reply/reply/get-reply-run.ts b/src/auto-reply/reply/get-reply-run.ts
index aa2281de6..5a5e1b4e1 100644
--- a/src/auto-reply/reply/get-reply-run.ts
+++ b/src/auto-reply/reply/get-reply-run.ts
@@ -157,7 +157,10 @@ export async function runPreparedReply(
const isFirstTurnInSession = isNewSession || !currentSystemSent;
const isGroupChat = sessionCtx.ChatType === "group";
- const wasMentioned = ctx.WasMentioned === true;
+ const originatingChannel =
+ (ctx.OriginatingChannel ?? ctx.Surface ?? ctx.Provider)?.toString().toLowerCase() ?? "";
+ const wasMentioned =
+ ctx.WasMentioned === true || (originatingChannel === "mattermost" && isGroupChat);
const isHeartbeat = opts?.isHeartbeat === true;
const typingMode = resolveTypingMode({
configured: sessionCfg?.typingMode ?? agentCfg?.typingMode,
diff --git a/src/channels/dock.ts b/src/channels/dock.ts
index 92199a0f2..e6fd3150a 100644
--- a/src/channels/dock.ts
+++ b/src/channels/dock.ts
@@ -11,6 +11,7 @@ import { requireActivePluginRegistry } from "../plugins/runtime.js";
import {
resolveDiscordGroupRequireMention,
resolveIMessageGroupRequireMention,
+ resolveMattermostGroupRequireMention,
resolveSlackGroupRequireMention,
resolveTelegramGroupRequireMention,
resolveWhatsAppGroupRequireMention,
@@ -235,6 +236,30 @@ const DOCKS: Record = {
},
},
},
+ mattermost: {
+ id: "mattermost",
+ capabilities: {
+ chatTypes: ["direct", "channel", "group", "thread"],
+ media: true,
+ threads: true,
+ },
+ outbound: { textChunkLimit: 4000 },
+ streaming: {
+ blockStreamingCoalesceDefaults: { minChars: 1500, idleMs: 1000 },
+ },
+ groups: {
+ resolveRequireMention: resolveMattermostGroupRequireMention,
+ },
+ threading: {
+ buildToolContext: ({ context, hasRepliedRef }) => ({
+ currentChannelId: context.To?.startsWith("channel:")
+ ? context.To.slice("channel:".length)
+ : undefined,
+ currentThreadTs: context.ReplyToId,
+ hasRepliedRef,
+ }),
+ },
+ },
signal: {
id: "signal",
capabilities: {
diff --git a/src/channels/plugins/group-mentions.ts b/src/channels/plugins/group-mentions.ts
index 79dfa0320..bb7a111e8 100644
--- a/src/channels/plugins/group-mentions.ts
+++ b/src/channels/plugins/group-mentions.ts
@@ -1,6 +1,7 @@
import type { ClawdbotConfig } from "../../config/config.js";
import { resolveChannelGroupRequireMention } from "../../config/group-policy.js";
import type { DiscordConfig } from "../../config/types.js";
+import { resolveMattermostAccount } from "../../mattermost/accounts.js";
import { resolveSlackAccount } from "../../slack/accounts.js";
type GroupMentionParams = {
@@ -184,6 +185,15 @@ export function resolveSlackGroupRequireMention(params: GroupMentionParams): boo
return true;
}
+export function resolveMattermostGroupRequireMention(params: GroupMentionParams): boolean {
+ const account = resolveMattermostAccount({
+ cfg: params.cfg,
+ accountId: params.accountId,
+ });
+ if (typeof account.requireMention === "boolean") return account.requireMention;
+ return true;
+}
+
export function resolveBlueBubblesGroupRequireMention(params: GroupMentionParams): boolean {
return resolveChannelGroupRequireMention({
cfg: params.cfg,
diff --git a/src/channels/plugins/normalize/mattermost.ts b/src/channels/plugins/normalize/mattermost.ts
new file mode 100644
index 000000000..80366420f
--- /dev/null
+++ b/src/channels/plugins/normalize/mattermost.ts
@@ -0,0 +1,38 @@
+export function normalizeMattermostMessagingTarget(raw: string): string | undefined {
+ const trimmed = raw.trim();
+ if (!trimmed) return undefined;
+ const lower = trimmed.toLowerCase();
+ if (lower.startsWith("channel:")) {
+ const id = trimmed.slice("channel:".length).trim();
+ return id ? `channel:${id}` : undefined;
+ }
+ if (lower.startsWith("group:")) {
+ const id = trimmed.slice("group:".length).trim();
+ return id ? `channel:${id}` : undefined;
+ }
+ if (lower.startsWith("user:")) {
+ const id = trimmed.slice("user:".length).trim();
+ return id ? `user:${id}` : undefined;
+ }
+ if (lower.startsWith("mattermost:")) {
+ const id = trimmed.slice("mattermost:".length).trim();
+ return id ? `user:${id}` : undefined;
+ }
+ if (trimmed.startsWith("@")) {
+ const id = trimmed.slice(1).trim();
+ return id ? `user:${id}` : undefined;
+ }
+ if (trimmed.startsWith("#")) {
+ const id = trimmed.slice(1).trim();
+ return id ? `channel:${id}` : undefined;
+ }
+ return `channel:${trimmed}`;
+}
+
+export function looksLikeMattermostTargetId(raw: string): boolean {
+ const trimmed = raw.trim();
+ if (!trimmed) return false;
+ if (/^(user|channel|group|mattermost):/i.test(trimmed)) return true;
+ if (/^[@#]/.test(trimmed)) return true;
+ return /^[a-z0-9]{8,}$/i.test(trimmed);
+}
diff --git a/src/channels/plugins/onboarding/mattermost.ts b/src/channels/plugins/onboarding/mattermost.ts
new file mode 100644
index 000000000..3c7ffe2db
--- /dev/null
+++ b/src/channels/plugins/onboarding/mattermost.ts
@@ -0,0 +1,189 @@
+import type { ClawdbotConfig } from "../../../config/config.js";
+import {
+ listMattermostAccountIds,
+ resolveDefaultMattermostAccountId,
+ resolveMattermostAccount,
+} from "../../../mattermost/accounts.js";
+import { DEFAULT_ACCOUNT_ID, normalizeAccountId } from "../../../routing/session-key.js";
+import { formatDocsLink } from "../../../terminal/links.js";
+import type { WizardPrompter } from "../../../wizard/prompts.js";
+import type { ChannelOnboardingAdapter } from "../onboarding-types.js";
+import { promptAccountId } from "./helpers.js";
+
+const channel = "mattermost" as const;
+
+async function noteMattermostSetup(prompter: WizardPrompter): Promise {
+ await prompter.note(
+ [
+ "1) Mattermost System Console -> Integrations -> Bot Accounts",
+ "2) Create a bot + copy its token",
+ "3) Use your server base URL (e.g., https://chat.example.com)",
+ "Tip: the bot must be a member of any channel you want it to monitor.",
+ `Docs: ${formatDocsLink("/channels/mattermost", "mattermost")}`,
+ ].join("\n"),
+ "Mattermost bot token",
+ );
+}
+
+export const mattermostOnboardingAdapter: ChannelOnboardingAdapter = {
+ channel,
+ getStatus: async ({ cfg }) => {
+ const configured = listMattermostAccountIds(cfg).some((accountId) => {
+ const account = resolveMattermostAccount({ cfg, accountId });
+ return Boolean(account.botToken && account.baseUrl);
+ });
+ return {
+ channel,
+ configured,
+ statusLines: [`Mattermost: ${configured ? "configured" : "needs token + url"}`],
+ selectionHint: configured ? "configured" : "needs setup",
+ quickstartScore: configured ? 2 : 1,
+ };
+ },
+ configure: async ({ cfg, prompter, accountOverrides, shouldPromptAccountIds }) => {
+ const override = accountOverrides.mattermost?.trim();
+ const defaultAccountId = resolveDefaultMattermostAccountId(cfg);
+ let accountId = override ? normalizeAccountId(override) : defaultAccountId;
+ if (shouldPromptAccountIds && !override) {
+ accountId = await promptAccountId({
+ cfg,
+ prompter,
+ label: "Mattermost",
+ currentId: accountId,
+ listAccountIds: listMattermostAccountIds,
+ defaultAccountId,
+ });
+ }
+
+ let next = cfg;
+ const resolvedAccount = resolveMattermostAccount({
+ cfg: next,
+ accountId,
+ });
+ const accountConfigured = Boolean(resolvedAccount.botToken && resolvedAccount.baseUrl);
+ const allowEnv = accountId === DEFAULT_ACCOUNT_ID;
+ const canUseEnv =
+ allowEnv &&
+ Boolean(process.env.MATTERMOST_BOT_TOKEN?.trim()) &&
+ Boolean(process.env.MATTERMOST_URL?.trim());
+ const hasConfigValues =
+ Boolean(resolvedAccount.config.botToken) || Boolean(resolvedAccount.config.baseUrl);
+
+ let botToken: string | null = null;
+ let baseUrl: string | null = null;
+
+ if (!accountConfigured) {
+ await noteMattermostSetup(prompter);
+ }
+
+ if (canUseEnv && !hasConfigValues) {
+ const keepEnv = await prompter.confirm({
+ message: "MATTERMOST_BOT_TOKEN + MATTERMOST_URL detected. Use env vars?",
+ initialValue: true,
+ });
+ if (keepEnv) {
+ next = {
+ ...next,
+ channels: {
+ ...next.channels,
+ mattermost: {
+ ...next.channels?.mattermost,
+ enabled: true,
+ },
+ },
+ };
+ } else {
+ botToken = String(
+ await prompter.text({
+ message: "Enter Mattermost bot token",
+ validate: (value) => (value?.trim() ? undefined : "Required"),
+ }),
+ ).trim();
+ baseUrl = String(
+ await prompter.text({
+ message: "Enter Mattermost base URL",
+ validate: (value) => (value?.trim() ? undefined : "Required"),
+ }),
+ ).trim();
+ }
+ } else if (accountConfigured) {
+ const keep = await prompter.confirm({
+ message: "Mattermost credentials already configured. Keep them?",
+ initialValue: true,
+ });
+ if (!keep) {
+ botToken = String(
+ await prompter.text({
+ message: "Enter Mattermost bot token",
+ validate: (value) => (value?.trim() ? undefined : "Required"),
+ }),
+ ).trim();
+ baseUrl = String(
+ await prompter.text({
+ message: "Enter Mattermost base URL",
+ validate: (value) => (value?.trim() ? undefined : "Required"),
+ }),
+ ).trim();
+ }
+ } else {
+ botToken = String(
+ await prompter.text({
+ message: "Enter Mattermost bot token",
+ validate: (value) => (value?.trim() ? undefined : "Required"),
+ }),
+ ).trim();
+ baseUrl = String(
+ await prompter.text({
+ message: "Enter Mattermost base URL",
+ validate: (value) => (value?.trim() ? undefined : "Required"),
+ }),
+ ).trim();
+ }
+
+ if (botToken || baseUrl) {
+ if (accountId === DEFAULT_ACCOUNT_ID) {
+ next = {
+ ...next,
+ channels: {
+ ...next.channels,
+ mattermost: {
+ ...next.channels?.mattermost,
+ enabled: true,
+ ...(botToken ? { botToken } : {}),
+ ...(baseUrl ? { baseUrl } : {}),
+ },
+ },
+ };
+ } else {
+ next = {
+ ...next,
+ channels: {
+ ...next.channels,
+ mattermost: {
+ ...next.channels?.mattermost,
+ enabled: true,
+ accounts: {
+ ...next.channels?.mattermost?.accounts,
+ [accountId]: {
+ ...next.channels?.mattermost?.accounts?.[accountId],
+ enabled: next.channels?.mattermost?.accounts?.[accountId]?.enabled ?? true,
+ ...(botToken ? { botToken } : {}),
+ ...(baseUrl ? { baseUrl } : {}),
+ },
+ },
+ },
+ },
+ };
+ }
+ }
+
+ return { cfg: next, accountId };
+ },
+ disable: (cfg: ClawdbotConfig) => ({
+ ...cfg,
+ channels: {
+ ...cfg.channels,
+ mattermost: { ...cfg.channels?.mattermost, enabled: false },
+ },
+ }),
+};
diff --git a/src/channels/registry.ts b/src/channels/registry.ts
index 52e7a5f01..25fb13502 100644
--- a/src/channels/registry.ts
+++ b/src/channels/registry.ts
@@ -9,6 +9,7 @@ export const CHAT_CHANNEL_ORDER = [
"whatsapp",
"discord",
"slack",
+ "mattermost",
"signal",
"imessage",
] as const;
@@ -67,6 +68,16 @@ const CHAT_CHANNEL_META: Record = {
blurb: "supported (Socket Mode).",
systemImage: "number",
},
+ mattermost: {
+ id: "mattermost",
+ label: "Mattermost",
+ selectionLabel: "Mattermost (Bot Token)",
+ detailLabel: "Mattermost Bot",
+ docsPath: "/channels/mattermost",
+ docsLabel: "mattermost",
+ blurb: "self-hosted Slack-style chat (bot token + URL).",
+ systemImage: "bubble.left.and.bubble.right",
+ },
signal: {
id: "signal",
label: "Signal",
diff --git a/src/commands/channels/resolve.ts b/src/commands/channels/resolve.ts
index 7394fa30f..820b53bf0 100644
--- a/src/commands/channels/resolve.ts
+++ b/src/commands/channels/resolve.ts
@@ -35,7 +35,7 @@ function detectAutoKind(input: string): ChannelResolveKind {
if (!trimmed) return "group";
if (trimmed.startsWith("@")) return "user";
if (/^<@!?/.test(trimmed)) return "user";
- if (/^(user|discord|slack|matrix|msteams|teams|zalo|zalouser):/i.test(trimmed)) {
+ if (/^(user|discord|slack|mattermost|matrix|msteams|teams|zalo|zalouser):/i.test(trimmed)) {
return "user";
}
return "group";
diff --git a/src/config/io.ts b/src/config/io.ts
index d275d3185..34b534285 100644
--- a/src/config/io.ts
+++ b/src/config/io.ts
@@ -52,6 +52,8 @@ const SHELL_ENV_EXPECTED_KEYS = [
"DISCORD_BOT_TOKEN",
"SLACK_BOT_TOKEN",
"SLACK_APP_TOKEN",
+ "MATTERMOST_BOT_TOKEN",
+ "MATTERMOST_URL",
"CLAWDBOT_GATEWAY_TOKEN",
"CLAWDBOT_GATEWAY_PASSWORD",
];
diff --git a/src/config/legacy.migrations.part-1.ts b/src/config/legacy.migrations.part-1.ts
index d1d0a57e7..8658b0ece 100644
--- a/src/config/legacy.migrations.part-1.ts
+++ b/src/config/legacy.migrations.part-1.ts
@@ -124,6 +124,7 @@ export const LEGACY_CONFIG_MIGRATIONS_PART_1: LegacyConfigMigration[] = [
"telegram",
"discord",
"slack",
+ "mattermost",
"signal",
"imessage",
"msteams",
diff --git a/src/config/legacy.rules.ts b/src/config/legacy.rules.ts
index 1ec76bc79..388083ae7 100644
--- a/src/config/legacy.rules.ts
+++ b/src/config/legacy.rules.ts
@@ -17,6 +17,10 @@ export const LEGACY_CONFIG_RULES: LegacyConfigRule[] = [
path: ["slack"],
message: "slack config moved to channels.slack (auto-migrated on load).",
},
+ {
+ path: ["mattermost"],
+ message: "mattermost config moved to channels.mattermost (auto-migrated on load).",
+ },
{
path: ["signal"],
message: "signal config moved to channels.signal (auto-migrated on load).",
diff --git a/src/config/schema.ts b/src/config/schema.ts
index 1ba527439..21f461ec0 100644
--- a/src/config/schema.ts
+++ b/src/config/schema.ts
@@ -272,6 +272,7 @@ const FIELD_LABELS: Record = {
"channels.telegram.customCommands": "Telegram Custom Commands",
"channels.discord": "Discord",
"channels.slack": "Slack",
+ "channels.mattermost": "Mattermost",
"channels.signal": "Signal",
"channels.imessage": "iMessage",
"channels.bluebubbles": "BlueBubbles",
@@ -309,6 +310,11 @@ const FIELD_LABELS: Record = {
"channels.slack.userTokenReadOnly": "Slack User Token Read Only",
"channels.slack.thread.historyScope": "Slack Thread History Scope",
"channels.slack.thread.inheritParent": "Slack Thread Parent Inheritance",
+ "channels.mattermost.botToken": "Mattermost Bot Token",
+ "channels.mattermost.baseUrl": "Mattermost Base URL",
+ "channels.mattermost.chatmode": "Mattermost Chat Mode",
+ "channels.mattermost.oncharPrefixes": "Mattermost Onchar Prefixes",
+ "channels.mattermost.requireMention": "Mattermost Require Mention",
"channels.signal.account": "Signal Account",
"channels.imessage.cliPath": "iMessage CLI Path",
"plugins.enabled": "Enable Plugins",
@@ -415,6 +421,15 @@ const FIELD_HELP: Record = {
'Scope for Slack thread history context ("thread" isolates per thread; "channel" reuses channel history).',
"channels.slack.thread.inheritParent":
"If true, Slack thread sessions inherit the parent channel transcript (default: false).",
+ "channels.mattermost.botToken":
+ "Bot token from Mattermost System Console -> Integrations -> Bot Accounts.",
+ "channels.mattermost.baseUrl":
+ "Base URL for your Mattermost server (e.g., https://chat.example.com).",
+ "channels.mattermost.chatmode":
+ 'Reply to channel messages on mention ("oncall"), on trigger chars (">" or "!") ("onchar"), or on every message ("onmessage").',
+ "channels.mattermost.oncharPrefixes": 'Trigger prefixes for onchar mode (default: [">", "!"]).',
+ "channels.mattermost.requireMention":
+ "Require @mention in channels before responding (default: true).",
"auth.profiles": "Named auth profiles (provider + mode + optional email).",
"auth.order": "Ordered auth profile IDs per provider (used for automatic failover).",
"auth.cooldowns.billingBackoffHours":
@@ -532,6 +547,8 @@ const FIELD_HELP: Record = {
"Allow Telegram to write config in response to channel events/commands (default: true).",
"channels.slack.configWrites":
"Allow Slack to write config in response to channel events/commands (default: true).",
+ "channels.mattermost.configWrites":
+ "Allow Mattermost to write config in response to channel events/commands (default: true).",
"channels.discord.configWrites":
"Allow Discord to write config in response to channel events/commands (default: true).",
"channels.whatsapp.configWrites":
@@ -606,6 +623,7 @@ const FIELD_PLACEHOLDERS: Record = {
"gateway.remote.tlsFingerprint": "sha256:ab12cd34…",
"gateway.remote.sshTarget": "user@host",
"gateway.controlUi.basePath": "/clawdbot",
+ "channels.mattermost.baseUrl": "https://chat.example.com",
};
const SENSITIVE_PATTERNS = [/token/i, /password/i, /secret/i, /api.?key/i];
diff --git a/src/config/types.agent-defaults.ts b/src/config/types.agent-defaults.ts
index 85eff97f2..10066bd75 100644
--- a/src/config/types.agent-defaults.ts
+++ b/src/config/types.agent-defaults.ts
@@ -162,13 +162,14 @@ export type AgentDefaultsConfig = {
every?: string;
/** Heartbeat model override (provider/model). */
model?: string;
- /** Delivery target (last|whatsapp|telegram|discord|slack|msteams|signal|imessage|none). */
+ /** Delivery target (last|whatsapp|telegram|discord|slack|mattermost|msteams|signal|imessage|none). */
target?:
| "last"
| "whatsapp"
| "telegram"
| "discord"
| "slack"
+ | "mattermost"
| "msteams"
| "signal"
| "imessage"
diff --git a/src/config/types.channels.ts b/src/config/types.channels.ts
index ac98e20de..19ac014dd 100644
--- a/src/config/types.channels.ts
+++ b/src/config/types.channels.ts
@@ -1,5 +1,6 @@
import type { DiscordConfig } from "./types.discord.js";
import type { IMessageConfig } from "./types.imessage.js";
+import type { MattermostConfig } from "./types.mattermost.js";
import type { MSTeamsConfig } from "./types.msteams.js";
import type { SignalConfig } from "./types.signal.js";
import type { SlackConfig } from "./types.slack.js";
@@ -17,6 +18,7 @@ export type ChannelsConfig = {
telegram?: TelegramConfig;
discord?: DiscordConfig;
slack?: SlackConfig;
+ mattermost?: MattermostConfig;
signal?: SignalConfig;
imessage?: IMessageConfig;
msteams?: MSTeamsConfig;
diff --git a/src/config/types.hooks.ts b/src/config/types.hooks.ts
index 03e9250b2..2a5bf0f2f 100644
--- a/src/config/types.hooks.ts
+++ b/src/config/types.hooks.ts
@@ -24,6 +24,7 @@ export type HookMappingConfig = {
| "telegram"
| "discord"
| "slack"
+ | "mattermost"
| "signal"
| "imessage"
| "msteams";
diff --git a/src/config/types.mattermost.ts b/src/config/types.mattermost.ts
new file mode 100644
index 000000000..b87bdfabe
--- /dev/null
+++ b/src/config/types.mattermost.ts
@@ -0,0 +1,40 @@
+import type { BlockStreamingCoalesceConfig } from "./types.base.js";
+
+export type MattermostChatMode = "oncall" | "onmessage" | "onchar";
+
+export type MattermostAccountConfig = {
+ /** Optional display name for this account (used in CLI/UI lists). */
+ name?: string;
+ /** Optional provider capability tags used for agent/runtime guidance. */
+ capabilities?: string[];
+ /** Allow channel-initiated config writes (default: true). */
+ configWrites?: boolean;
+ /** If false, do not start this Mattermost account. Default: true. */
+ enabled?: boolean;
+ /** Bot token for Mattermost. */
+ botToken?: string;
+ /** Base URL for the Mattermost server (e.g., https://chat.example.com). */
+ baseUrl?: string;
+ /**
+ * Controls when channel messages trigger replies.
+ * - "oncall": only respond when mentioned
+ * - "onmessage": respond to every channel message
+ * - "onchar": respond when a trigger character prefixes the message
+ */
+ chatmode?: MattermostChatMode;
+ /** Prefix characters that trigger onchar mode (default: [">", "!"]). */
+ oncharPrefixes?: string[];
+ /** Require @mention to respond in channels. Default: true. */
+ requireMention?: boolean;
+ /** Outbound text chunk size (chars). Default: 4000. */
+ textChunkLimit?: number;
+ /** Disable block streaming for this account. */
+ blockStreaming?: boolean;
+ /** Merge streamed block replies before sending. */
+ blockStreamingCoalesce?: BlockStreamingCoalesceConfig;
+};
+
+export type MattermostConfig = {
+ /** Optional per-account Mattermost configuration (multi-account). */
+ accounts?: Record;
+} & MattermostAccountConfig;
diff --git a/src/config/types.messages.ts b/src/config/types.messages.ts
index 691ca617a..fc4146bc7 100644
--- a/src/config/types.messages.ts
+++ b/src/config/types.messages.ts
@@ -22,6 +22,7 @@ export type InboundDebounceByProvider = {
telegram?: number;
discord?: number;
slack?: number;
+ mattermost?: number;
signal?: number;
imessage?: number;
msteams?: number;
diff --git a/src/config/types.queue.ts b/src/config/types.queue.ts
index 0afeb5232..6289e7c56 100644
--- a/src/config/types.queue.ts
+++ b/src/config/types.queue.ts
@@ -13,6 +13,7 @@ export type QueueModeByProvider = {
telegram?: QueueMode;
discord?: QueueMode;
slack?: QueueMode;
+ mattermost?: QueueMode;
signal?: QueueMode;
imessage?: QueueMode;
msteams?: QueueMode;
diff --git a/src/config/types.ts b/src/config/types.ts
index 368618262..46e79eaca 100644
--- a/src/config/types.ts
+++ b/src/config/types.ts
@@ -14,6 +14,7 @@ export * from "./types.hooks.js";
export * from "./types.imessage.js";
export * from "./types.messages.js";
export * from "./types.models.js";
+export * from "./types.mattermost.js";
export * from "./types.msteams.js";
export * from "./types.plugins.js";
export * from "./types.queue.js";
diff --git a/src/config/zod-schema.agent-runtime.ts b/src/config/zod-schema.agent-runtime.ts
index 40dea6eb4..774645a14 100644
--- a/src/config/zod-schema.agent-runtime.ts
+++ b/src/config/zod-schema.agent-runtime.ts
@@ -20,6 +20,7 @@ export const HeartbeatSchema = z
z.literal("telegram"),
z.literal("discord"),
z.literal("slack"),
+ z.literal("mattermost"),
z.literal("msteams"),
z.literal("signal"),
z.literal("imessage"),
diff --git a/src/config/zod-schema.core.ts b/src/config/zod-schema.core.ts
index 6e7b34b0d..1d6612aae 100644
--- a/src/config/zod-schema.core.ts
+++ b/src/config/zod-schema.core.ts
@@ -208,6 +208,7 @@ export const QueueModeBySurfaceSchema = z
telegram: QueueModeSchema.optional(),
discord: QueueModeSchema.optional(),
slack: QueueModeSchema.optional(),
+ mattermost: QueueModeSchema.optional(),
signal: QueueModeSchema.optional(),
imessage: QueueModeSchema.optional(),
msteams: QueueModeSchema.optional(),
@@ -222,6 +223,7 @@ export const DebounceMsBySurfaceSchema = z
telegram: z.number().int().nonnegative().optional(),
discord: z.number().int().nonnegative().optional(),
slack: z.number().int().nonnegative().optional(),
+ mattermost: z.number().int().nonnegative().optional(),
signal: z.number().int().nonnegative().optional(),
imessage: z.number().int().nonnegative().optional(),
msteams: z.number().int().nonnegative().optional(),
diff --git a/src/config/zod-schema.hooks.ts b/src/config/zod-schema.hooks.ts
index 140e861dd..9153aa130 100644
--- a/src/config/zod-schema.hooks.ts
+++ b/src/config/zod-schema.hooks.ts
@@ -23,6 +23,7 @@ export const HookMappingSchema = z
z.literal("telegram"),
z.literal("discord"),
z.literal("slack"),
+ z.literal("mattermost"),
z.literal("signal"),
z.literal("imessage"),
z.literal("msteams"),
diff --git a/src/config/zod-schema.providers-core.ts b/src/config/zod-schema.providers-core.ts
index 906ef5433..200ff18c8 100644
--- a/src/config/zod-schema.providers-core.ts
+++ b/src/config/zod-schema.providers-core.ts
@@ -367,6 +367,27 @@ export const SlackConfigSchema = SlackAccountSchema.extend({
}
});
+export const MattermostAccountSchema = z
+ .object({
+ name: z.string().optional(),
+ capabilities: z.array(z.string()).optional(),
+ enabled: z.boolean().optional(),
+ configWrites: z.boolean().optional(),
+ botToken: z.string().optional(),
+ baseUrl: z.string().optional(),
+ chatmode: z.enum(["oncall", "onmessage", "onchar"]).optional(),
+ oncharPrefixes: z.array(z.string()).optional(),
+ requireMention: z.boolean().optional(),
+ textChunkLimit: z.number().int().positive().optional(),
+ blockStreaming: z.boolean().optional(),
+ blockStreamingCoalesce: BlockStreamingCoalesceSchema.optional(),
+ })
+ .strict();
+
+export const MattermostConfigSchema = MattermostAccountSchema.extend({
+ accounts: z.record(z.string(), MattermostAccountSchema.optional()).optional(),
+});
+
export const SignalAccountSchemaBase = z
.object({
name: z.string().optional(),
diff --git a/src/config/zod-schema.providers.ts b/src/config/zod-schema.providers.ts
index a58119702..aa5eb7737 100644
--- a/src/config/zod-schema.providers.ts
+++ b/src/config/zod-schema.providers.ts
@@ -4,6 +4,7 @@ import {
BlueBubblesConfigSchema,
DiscordConfigSchema,
IMessageConfigSchema,
+ MattermostConfigSchema,
MSTeamsConfigSchema,
SignalConfigSchema,
SlackConfigSchema,
@@ -27,6 +28,7 @@ export const ChannelsSchema = z
telegram: TelegramConfigSchema.optional(),
discord: DiscordConfigSchema.optional(),
slack: SlackConfigSchema.optional(),
+ mattermost: MattermostConfigSchema.optional(),
signal: SignalConfigSchema.optional(),
imessage: IMessageConfigSchema.optional(),
bluebubbles: BlueBubblesConfigSchema.optional(),
diff --git a/src/infra/outbound/deliver.ts b/src/infra/outbound/deliver.ts
index 21fffe807..74caa18c6 100644
--- a/src/infra/outbound/deliver.ts
+++ b/src/infra/outbound/deliver.ts
@@ -6,6 +6,7 @@ import type { ChannelOutboundAdapter } from "../../channels/plugins/types.js";
import type { ClawdbotConfig } from "../../config/config.js";
import type { sendMessageDiscord } from "../../discord/send.js";
import type { sendMessageIMessage } from "../../imessage/send.js";
+import type { sendMessageMattermost } from "../../mattermost/send.js";
import { markdownToSignalTextChunks, type SignalTextStyleRange } from "../../signal/format.js";
import { sendMessageSignal } from "../../signal/send.js";
import type { sendMessageSlack } from "../../slack/send.js";
@@ -33,6 +34,7 @@ export type OutboundSendDeps = {
sendTelegram?: typeof sendMessageTelegram;
sendDiscord?: typeof sendMessageDiscord;
sendSlack?: typeof sendMessageSlack;
+ sendMattermost?: typeof sendMessageMattermost;
sendSignal?: typeof sendMessageSignal;
sendIMessage?: typeof sendMessageIMessage;
sendMatrix?: SendMatrixMessage;
diff --git a/src/mattermost/accounts.ts b/src/mattermost/accounts.ts
new file mode 100644
index 000000000..08ffa2f94
--- /dev/null
+++ b/src/mattermost/accounts.ts
@@ -0,0 +1,114 @@
+import type { ClawdbotConfig } from "../config/config.js";
+import type { MattermostAccountConfig, MattermostChatMode } from "../config/types.js";
+import { DEFAULT_ACCOUNT_ID, normalizeAccountId } from "../routing/session-key.js";
+import { normalizeMattermostBaseUrl } from "./client.js";
+
+export type MattermostTokenSource = "env" | "config" | "none";
+export type MattermostBaseUrlSource = "env" | "config" | "none";
+
+export type ResolvedMattermostAccount = {
+ accountId: string;
+ enabled: boolean;
+ name?: string;
+ botToken?: string;
+ baseUrl?: string;
+ botTokenSource: MattermostTokenSource;
+ baseUrlSource: MattermostBaseUrlSource;
+ config: MattermostAccountConfig;
+ chatmode?: MattermostChatMode;
+ oncharPrefixes?: string[];
+ requireMention?: boolean;
+ textChunkLimit?: number;
+ blockStreaming?: boolean;
+ blockStreamingCoalesce?: MattermostAccountConfig["blockStreamingCoalesce"];
+};
+
+function listConfiguredAccountIds(cfg: ClawdbotConfig): string[] {
+ const accounts = cfg.channels?.mattermost?.accounts;
+ if (!accounts || typeof accounts !== "object") return [];
+ return Object.keys(accounts).filter(Boolean);
+}
+
+export function listMattermostAccountIds(cfg: ClawdbotConfig): string[] {
+ const ids = listConfiguredAccountIds(cfg);
+ if (ids.length === 0) return [DEFAULT_ACCOUNT_ID];
+ return ids.sort((a, b) => a.localeCompare(b));
+}
+
+export function resolveDefaultMattermostAccountId(cfg: ClawdbotConfig): string {
+ const ids = listMattermostAccountIds(cfg);
+ if (ids.includes(DEFAULT_ACCOUNT_ID)) return DEFAULT_ACCOUNT_ID;
+ return ids[0] ?? DEFAULT_ACCOUNT_ID;
+}
+
+function resolveAccountConfig(
+ cfg: ClawdbotConfig,
+ accountId: string,
+): MattermostAccountConfig | undefined {
+ const accounts = cfg.channels?.mattermost?.accounts;
+ if (!accounts || typeof accounts !== "object") return undefined;
+ return accounts[accountId] as MattermostAccountConfig | undefined;
+}
+
+function mergeMattermostAccountConfig(
+ cfg: ClawdbotConfig,
+ accountId: string,
+): MattermostAccountConfig {
+ const { accounts: _ignored, ...base } = (cfg.channels?.mattermost ??
+ {}) as MattermostAccountConfig & { accounts?: unknown };
+ const account = resolveAccountConfig(cfg, accountId) ?? {};
+ return { ...base, ...account };
+}
+
+function resolveMattermostRequireMention(config: MattermostAccountConfig): boolean | undefined {
+ if (config.chatmode === "oncall") return true;
+ if (config.chatmode === "onmessage") return false;
+ if (config.chatmode === "onchar") return true;
+ return config.requireMention;
+}
+
+export function resolveMattermostAccount(params: {
+ cfg: ClawdbotConfig;
+ accountId?: string | null;
+}): ResolvedMattermostAccount {
+ const accountId = normalizeAccountId(params.accountId);
+ const baseEnabled = params.cfg.channels?.mattermost?.enabled !== false;
+ const merged = mergeMattermostAccountConfig(params.cfg, accountId);
+ const accountEnabled = merged.enabled !== false;
+ const enabled = baseEnabled && accountEnabled;
+
+ const allowEnv = accountId === DEFAULT_ACCOUNT_ID;
+ const envToken = allowEnv ? process.env.MATTERMOST_BOT_TOKEN?.trim() : undefined;
+ const envUrl = allowEnv ? process.env.MATTERMOST_URL?.trim() : undefined;
+ const configToken = merged.botToken?.trim();
+ const configUrl = merged.baseUrl?.trim();
+ const botToken = configToken || envToken;
+ const baseUrl = normalizeMattermostBaseUrl(configUrl || envUrl);
+ const requireMention = resolveMattermostRequireMention(merged);
+
+ const botTokenSource: MattermostTokenSource = configToken ? "config" : envToken ? "env" : "none";
+ const baseUrlSource: MattermostBaseUrlSource = configUrl ? "config" : envUrl ? "env" : "none";
+
+ return {
+ accountId,
+ enabled,
+ name: merged.name?.trim() || undefined,
+ botToken,
+ baseUrl,
+ botTokenSource,
+ baseUrlSource,
+ config: merged,
+ chatmode: merged.chatmode,
+ oncharPrefixes: merged.oncharPrefixes,
+ requireMention,
+ textChunkLimit: merged.textChunkLimit,
+ blockStreaming: merged.blockStreaming,
+ blockStreamingCoalesce: merged.blockStreamingCoalesce,
+ };
+}
+
+export function listEnabledMattermostAccounts(cfg: ClawdbotConfig): ResolvedMattermostAccount[] {
+ return listMattermostAccountIds(cfg)
+ .map((accountId) => resolveMattermostAccount({ cfg, accountId }))
+ .filter((account) => account.enabled);
+}
diff --git a/src/mattermost/client.ts b/src/mattermost/client.ts
new file mode 100644
index 000000000..6b63f830f
--- /dev/null
+++ b/src/mattermost/client.ts
@@ -0,0 +1,208 @@
+export type MattermostClient = {
+ baseUrl: string;
+ apiBaseUrl: string;
+ token: string;
+ request: (path: string, init?: RequestInit) => Promise;
+};
+
+export type MattermostUser = {
+ id: string;
+ username?: string | null;
+ nickname?: string | null;
+ first_name?: string | null;
+ last_name?: string | null;
+};
+
+export type MattermostChannel = {
+ id: string;
+ name?: string | null;
+ display_name?: string | null;
+ type?: string | null;
+ team_id?: string | null;
+};
+
+export type MattermostPost = {
+ id: string;
+ user_id?: string | null;
+ channel_id?: string | null;
+ message?: string | null;
+ file_ids?: string[] | null;
+ type?: string | null;
+ root_id?: string | null;
+ create_at?: number | null;
+ props?: Record | null;
+};
+
+export type MattermostFileInfo = {
+ id: string;
+ name?: string | null;
+ mime_type?: string | null;
+ size?: number | null;
+};
+
+export function normalizeMattermostBaseUrl(raw?: string | null): string | undefined {
+ const trimmed = raw?.trim();
+ if (!trimmed) return undefined;
+ const withoutTrailing = trimmed.replace(/\/+$/, "");
+ return withoutTrailing.replace(/\/api\/v4$/i, "");
+}
+
+function buildMattermostApiUrl(baseUrl: string, path: string): string {
+ const normalized = normalizeMattermostBaseUrl(baseUrl);
+ if (!normalized) throw new Error("Mattermost baseUrl is required");
+ const suffix = path.startsWith("/") ? path : `/${path}`;
+ return `${normalized}/api/v4${suffix}`;
+}
+
+async function readMattermostError(res: Response): Promise {
+ const contentType = res.headers.get("content-type") ?? "";
+ if (contentType.includes("application/json")) {
+ const data = (await res.json()) as { message?: string } | undefined;
+ if (data?.message) return data.message;
+ return JSON.stringify(data);
+ }
+ return await res.text();
+}
+
+export function createMattermostClient(params: {
+ baseUrl: string;
+ botToken: string;
+ fetchImpl?: typeof fetch;
+}): MattermostClient {
+ const baseUrl = normalizeMattermostBaseUrl(params.baseUrl);
+ if (!baseUrl) throw new Error("Mattermost baseUrl is required");
+ const apiBaseUrl = `${baseUrl}/api/v4`;
+ const token = params.botToken.trim();
+ const fetchImpl = params.fetchImpl ?? fetch;
+
+ const request = async (path: string, init?: RequestInit): Promise => {
+ const url = buildMattermostApiUrl(baseUrl, path);
+ const headers = new Headers(init?.headers);
+ headers.set("Authorization", `Bearer ${token}`);
+ if (typeof init?.body === "string" && !headers.has("Content-Type")) {
+ headers.set("Content-Type", "application/json");
+ }
+ const res = await fetchImpl(url, { ...init, headers });
+ if (!res.ok) {
+ const detail = await readMattermostError(res);
+ throw new Error(
+ `Mattermost API ${res.status} ${res.statusText}: ${detail || "unknown error"}`,
+ );
+ }
+ return (await res.json()) as T;
+ };
+
+ return { baseUrl, apiBaseUrl, token, request };
+}
+
+export async function fetchMattermostMe(client: MattermostClient): Promise {
+ return await client.request("/users/me");
+}
+
+export async function fetchMattermostUser(
+ client: MattermostClient,
+ userId: string,
+): Promise {
+ return await client.request(`/users/${userId}`);
+}
+
+export async function fetchMattermostUserByUsername(
+ client: MattermostClient,
+ username: string,
+): Promise {
+ return await client.request(`/users/username/${encodeURIComponent(username)}`);
+}
+
+export async function fetchMattermostChannel(
+ client: MattermostClient,
+ channelId: string,
+): Promise {
+ return await client.request(`/channels/${channelId}`);
+}
+
+export async function sendMattermostTyping(
+ client: MattermostClient,
+ params: { channelId: string; parentId?: string },
+): Promise {
+ const payload: Record = {
+ channel_id: params.channelId,
+ };
+ const parentId = params.parentId?.trim();
+ if (parentId) payload.parent_id = parentId;
+ await client.request>("/users/me/typing", {
+ method: "POST",
+ body: JSON.stringify(payload),
+ });
+}
+
+export async function createMattermostDirectChannel(
+ client: MattermostClient,
+ userIds: string[],
+): Promise {
+ return await client.request("/channels/direct", {
+ method: "POST",
+ body: JSON.stringify(userIds),
+ });
+}
+
+export async function createMattermostPost(
+ client: MattermostClient,
+ params: {
+ channelId: string;
+ message: string;
+ rootId?: string;
+ fileIds?: string[];
+ },
+): Promise {
+ const payload: Record = {
+ channel_id: params.channelId,
+ message: params.message,
+ };
+ if (params.rootId) payload.root_id = params.rootId;
+ if (params.fileIds?.length) {
+ (payload as Record).file_ids = params.fileIds;
+ }
+ return await client.request("/posts", {
+ method: "POST",
+ body: JSON.stringify(payload),
+ });
+}
+
+export async function uploadMattermostFile(
+ client: MattermostClient,
+ params: {
+ channelId: string;
+ buffer: Buffer;
+ fileName: string;
+ contentType?: string;
+ },
+): Promise {
+ const form = new FormData();
+ const fileName = params.fileName?.trim() || "upload";
+ const bytes = Uint8Array.from(params.buffer);
+ const blob = params.contentType
+ ? new Blob([bytes], { type: params.contentType })
+ : new Blob([bytes]);
+ form.append("files", blob, fileName);
+ form.append("channel_id", params.channelId);
+
+ const res = await fetch(`${client.apiBaseUrl}/files`, {
+ method: "POST",
+ headers: {
+ Authorization: `Bearer ${client.token}`,
+ },
+ body: form,
+ });
+
+ if (!res.ok) {
+ const detail = await readMattermostError(res);
+ throw new Error(`Mattermost API ${res.status} ${res.statusText}: ${detail || "unknown error"}`);
+ }
+
+ const data = (await res.json()) as { file_infos?: MattermostFileInfo[] };
+ const info = data.file_infos?.[0];
+ if (!info?.id) {
+ throw new Error("Mattermost file upload failed");
+ }
+ return info;
+}
diff --git a/src/mattermost/index.ts b/src/mattermost/index.ts
new file mode 100644
index 000000000..9d09fc402
--- /dev/null
+++ b/src/mattermost/index.ts
@@ -0,0 +1,9 @@
+export {
+ listEnabledMattermostAccounts,
+ listMattermostAccountIds,
+ resolveDefaultMattermostAccountId,
+ resolveMattermostAccount,
+} from "./accounts.js";
+export { monitorMattermostProvider } from "./monitor.js";
+export { probeMattermost } from "./probe.js";
+export { sendMessageMattermost } from "./send.js";
diff --git a/src/mattermost/monitor.ts b/src/mattermost/monitor.ts
new file mode 100644
index 000000000..fb8bd00db
--- /dev/null
+++ b/src/mattermost/monitor.ts
@@ -0,0 +1,774 @@
+import WebSocket from "ws";
+
+import {
+ resolveEffectiveMessagesConfig,
+ resolveHumanDelayConfig,
+ resolveIdentityName,
+} from "../agents/identity.js";
+import { chunkMarkdownText, resolveTextChunkLimit } from "../auto-reply/chunk.js";
+import { hasControlCommand } from "../auto-reply/command-detection.js";
+import { shouldHandleTextCommands } from "../auto-reply/commands-registry.js";
+import { formatInboundEnvelope, formatInboundFromLabel } from "../auto-reply/envelope.js";
+import {
+ createInboundDebouncer,
+ resolveInboundDebounceMs,
+} from "../auto-reply/inbound-debounce.js";
+import { dispatchReplyFromConfig } from "../auto-reply/reply/dispatch-from-config.js";
+import { finalizeInboundContext } from "../auto-reply/reply/inbound-context.js";
+import {
+ buildPendingHistoryContextFromMap,
+ clearHistoryEntries,
+ DEFAULT_GROUP_HISTORY_LIMIT,
+ recordPendingHistoryEntry,
+ type HistoryEntry,
+} from "../auto-reply/reply/history.js";
+import { createReplyDispatcherWithTyping } from "../auto-reply/reply/reply-dispatcher.js";
+import {
+ extractShortModelName,
+ type ResponsePrefixContext,
+} from "../auto-reply/reply/response-prefix-template.js";
+import { buildMentionRegexes, matchesMentionPatterns } from "../auto-reply/reply/mentions.js";
+import type { ReplyPayload } from "../auto-reply/types.js";
+import type { ClawdbotConfig } from "../config/config.js";
+import { loadConfig } from "../config/config.js";
+import { resolveStorePath, updateLastRoute } from "../config/sessions.js";
+import { danger, logVerbose, shouldLogVerbose } from "../globals.js";
+import { createDedupeCache } from "../infra/dedupe.js";
+import { rawDataToString } from "../infra/ws.js";
+import { recordChannelActivity } from "../infra/channel-activity.js";
+import { enqueueSystemEvent } from "../infra/system-events.js";
+import { getChildLogger } from "../logging.js";
+import { mediaKindFromMime, type MediaKind } from "../media/constants.js";
+import { fetchRemoteMedia, type FetchLike } from "../media/fetch.js";
+import { saveMediaBuffer } from "../media/store.js";
+import { resolveAgentRoute } from "../routing/resolve-route.js";
+import { resolveThreadSessionKeys } from "../routing/session-key.js";
+import type { RuntimeEnv } from "../runtime.js";
+import type { ChannelAccountSnapshot } from "../channels/plugins/types.js";
+import { resolveChannelMediaMaxBytes } from "../channels/plugins/media-limits.js";
+import { resolveCommandAuthorizedFromAuthorizers } from "../channels/command-gating.js";
+import { resolveMattermostAccount } from "./accounts.js";
+import {
+ createMattermostClient,
+ fetchMattermostChannel,
+ fetchMattermostMe,
+ fetchMattermostUser,
+ normalizeMattermostBaseUrl,
+ sendMattermostTyping,
+ type MattermostChannel,
+ type MattermostPost,
+ type MattermostUser,
+} from "./client.js";
+import { sendMessageMattermost } from "./send.js";
+
+export type MonitorMattermostOpts = {
+ botToken?: string;
+ baseUrl?: string;
+ accountId?: string;
+ config?: ClawdbotConfig;
+ runtime?: RuntimeEnv;
+ abortSignal?: AbortSignal;
+ statusSink?: (patch: Partial) => void;
+};
+
+type MattermostEventPayload = {
+ event?: string;
+ data?: {
+ post?: string;
+ channel_id?: string;
+ channel_name?: string;
+ channel_display_name?: string;
+ channel_type?: string;
+ sender_name?: string;
+ team_id?: string;
+ };
+ broadcast?: {
+ channel_id?: string;
+ team_id?: string;
+ user_id?: string;
+ };
+};
+
+const RECENT_MATTERMOST_MESSAGE_TTL_MS = 5 * 60_000;
+const RECENT_MATTERMOST_MESSAGE_MAX = 2000;
+const CHANNEL_CACHE_TTL_MS = 5 * 60_000;
+const USER_CACHE_TTL_MS = 10 * 60_000;
+const DEFAULT_ONCHAR_PREFIXES = [">", "!"];
+
+const recentInboundMessages = createDedupeCache({
+ ttlMs: RECENT_MATTERMOST_MESSAGE_TTL_MS,
+ maxSize: RECENT_MATTERMOST_MESSAGE_MAX,
+});
+
+function resolveRuntime(opts: MonitorMattermostOpts): RuntimeEnv {
+ return (
+ opts.runtime ?? {
+ log: console.log,
+ error: console.error,
+ exit: (code: number): never => {
+ throw new Error(`exit ${code}`);
+ },
+ }
+ );
+}
+
+function normalizeMention(text: string, mention: string | undefined): string {
+ if (!mention) return text.trim();
+ const escaped = mention.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
+ const re = new RegExp(`@${escaped}\\b`, "gi");
+ return text.replace(re, " ").replace(/\s+/g, " ").trim();
+}
+
+function resolveOncharPrefixes(prefixes: string[] | undefined): string[] {
+ const cleaned = prefixes?.map((entry) => entry.trim()).filter(Boolean) ?? DEFAULT_ONCHAR_PREFIXES;
+ return cleaned.length > 0 ? cleaned : DEFAULT_ONCHAR_PREFIXES;
+}
+
+function stripOncharPrefix(
+ text: string,
+ prefixes: string[],
+): { triggered: boolean; stripped: string } {
+ const trimmed = text.trimStart();
+ for (const prefix of prefixes) {
+ if (!prefix) continue;
+ if (trimmed.startsWith(prefix)) {
+ return {
+ triggered: true,
+ stripped: trimmed.slice(prefix.length).trimStart(),
+ };
+ }
+ }
+ return { triggered: false, stripped: text };
+}
+
+function isSystemPost(post: MattermostPost): boolean {
+ const type = post.type?.trim();
+ return Boolean(type);
+}
+
+function channelKind(channelType?: string | null): "dm" | "group" | "channel" {
+ if (!channelType) return "channel";
+ const normalized = channelType.trim().toUpperCase();
+ if (normalized === "D") return "dm";
+ if (normalized === "G") return "group";
+ return "channel";
+}
+
+function channelChatType(kind: "dm" | "group" | "channel"): "direct" | "group" | "channel" {
+ if (kind === "dm") return "direct";
+ if (kind === "group") return "group";
+ return "channel";
+}
+
+type MattermostMediaInfo = {
+ path: string;
+ contentType?: string;
+ kind: MediaKind;
+};
+
+function buildMattermostAttachmentPlaceholder(mediaList: MattermostMediaInfo[]): string {
+ if (mediaList.length === 0) return "";
+ if (mediaList.length === 1) {
+ const kind = mediaList[0].kind === "unknown" ? "document" : mediaList[0].kind;
+ return ``;
+ }
+ const allImages = mediaList.every((media) => media.kind === "image");
+ const label = allImages ? "image" : "file";
+ const suffix = mediaList.length === 1 ? label : `${label}s`;
+ const tag = allImages ? "" : "";
+ return `${tag} (${mediaList.length} ${suffix})`;
+}
+
+function buildMattermostMediaPayload(mediaList: MattermostMediaInfo[]): {
+ MediaPath?: string;
+ MediaType?: string;
+ MediaUrl?: string;
+ MediaPaths?: string[];
+ MediaUrls?: string[];
+ MediaTypes?: string[];
+} {
+ const first = mediaList[0];
+ const mediaPaths = mediaList.map((media) => media.path);
+ const mediaTypes = mediaList.map((media) => media.contentType).filter(Boolean) as string[];
+ return {
+ MediaPath: first?.path,
+ MediaType: first?.contentType,
+ MediaUrl: first?.path,
+ MediaPaths: mediaPaths.length > 0 ? mediaPaths : undefined,
+ MediaUrls: mediaPaths.length > 0 ? mediaPaths : undefined,
+ MediaTypes: mediaTypes.length > 0 ? mediaTypes : undefined,
+ };
+}
+
+function buildMattermostWsUrl(baseUrl: string): string {
+ const normalized = normalizeMattermostBaseUrl(baseUrl);
+ if (!normalized) throw new Error("Mattermost baseUrl is required");
+ const wsBase = normalized.replace(/^http/i, "ws");
+ return `${wsBase}/api/v4/websocket`;
+}
+
+export async function monitorMattermostProvider(opts: MonitorMattermostOpts = {}): Promise {
+ const runtime = resolveRuntime(opts);
+ const cfg = opts.config ?? loadConfig();
+ const account = resolveMattermostAccount({
+ cfg,
+ accountId: opts.accountId,
+ });
+ const botToken = opts.botToken?.trim() || account.botToken?.trim();
+ if (!botToken) {
+ throw new Error(
+ `Mattermost bot token missing for account "${account.accountId}" (set channels.mattermost.accounts.${account.accountId}.botToken or MATTERMOST_BOT_TOKEN for default).`,
+ );
+ }
+ const baseUrl = normalizeMattermostBaseUrl(opts.baseUrl ?? account.baseUrl);
+ if (!baseUrl) {
+ throw new Error(
+ `Mattermost baseUrl missing for account "${account.accountId}" (set channels.mattermost.accounts.${account.accountId}.baseUrl or MATTERMOST_URL for default).`,
+ );
+ }
+
+ const client = createMattermostClient({ baseUrl, botToken });
+ const botUser = await fetchMattermostMe(client);
+ const botUserId = botUser.id;
+ const botUsername = botUser.username?.trim() || undefined;
+ runtime.log?.(`mattermost connected as ${botUsername ? `@${botUsername}` : botUserId}`);
+
+ const channelCache = new Map();
+ const userCache = new Map();
+ const logger = getChildLogger({ module: "mattermost" });
+ const mediaMaxBytes =
+ resolveChannelMediaMaxBytes({
+ cfg,
+ resolveChannelLimitMb: () => undefined,
+ accountId: account.accountId,
+ }) ?? 8 * 1024 * 1024;
+ const historyLimit = Math.max(
+ 0,
+ cfg.messages?.groupChat?.historyLimit ?? DEFAULT_GROUP_HISTORY_LIMIT,
+ );
+ const channelHistories = new Map();
+
+ const fetchWithAuth: FetchLike = (input, init) => {
+ const headers = new Headers(init?.headers);
+ headers.set("Authorization", `Bearer ${client.token}`);
+ return fetch(input, { ...init, headers });
+ };
+
+ const resolveMattermostMedia = async (
+ fileIds?: string[] | null,
+ ): Promise => {
+ const ids = (fileIds ?? []).map((id) => id?.trim()).filter(Boolean) as string[];
+ if (ids.length === 0) return [];
+ const out: MattermostMediaInfo[] = [];
+ for (const fileId of ids) {
+ try {
+ const fetched = await fetchRemoteMedia({
+ url: `${client.apiBaseUrl}/files/${fileId}`,
+ fetchImpl: fetchWithAuth,
+ filePathHint: fileId,
+ maxBytes: mediaMaxBytes,
+ });
+ const saved = await saveMediaBuffer(
+ fetched.buffer,
+ fetched.contentType ?? undefined,
+ "inbound",
+ mediaMaxBytes,
+ );
+ const contentType = saved.contentType ?? fetched.contentType ?? undefined;
+ out.push({
+ path: saved.path,
+ contentType,
+ kind: mediaKindFromMime(contentType),
+ });
+ } catch (err) {
+ logger.debug?.(`mattermost: failed to download file ${fileId}: ${String(err)}`);
+ }
+ }
+ return out;
+ };
+
+ const sendTypingIndicator = async (channelId: string, parentId?: string) => {
+ try {
+ await sendMattermostTyping(client, { channelId, parentId });
+ } catch (err) {
+ logger.debug?.(`mattermost typing cue failed for channel ${channelId}: ${String(err)}`);
+ }
+ };
+
+ const resolveChannelInfo = async (channelId: string): Promise => {
+ const cached = channelCache.get(channelId);
+ if (cached && cached.expiresAt > Date.now()) return cached.value;
+ try {
+ const info = await fetchMattermostChannel(client, channelId);
+ channelCache.set(channelId, {
+ value: info,
+ expiresAt: Date.now() + CHANNEL_CACHE_TTL_MS,
+ });
+ return info;
+ } catch (err) {
+ logger.debug?.(`mattermost: channel lookup failed: ${String(err)}`);
+ channelCache.set(channelId, {
+ value: null,
+ expiresAt: Date.now() + CHANNEL_CACHE_TTL_MS,
+ });
+ return null;
+ }
+ };
+
+ const resolveUserInfo = async (userId: string): Promise => {
+ const cached = userCache.get(userId);
+ if (cached && cached.expiresAt > Date.now()) return cached.value;
+ try {
+ const info = await fetchMattermostUser(client, userId);
+ userCache.set(userId, {
+ value: info,
+ expiresAt: Date.now() + USER_CACHE_TTL_MS,
+ });
+ return info;
+ } catch (err) {
+ logger.debug?.(`mattermost: user lookup failed: ${String(err)}`);
+ userCache.set(userId, {
+ value: null,
+ expiresAt: Date.now() + USER_CACHE_TTL_MS,
+ });
+ return null;
+ }
+ };
+
+ const handlePost = async (
+ post: MattermostPost,
+ payload: MattermostEventPayload,
+ messageIds?: string[],
+ ) => {
+ const channelId = post.channel_id ?? payload.data?.channel_id ?? payload.broadcast?.channel_id;
+ if (!channelId) return;
+
+ const allMessageIds = messageIds?.length ? messageIds : post.id ? [post.id] : [];
+ if (allMessageIds.length === 0) return;
+ const dedupeEntries = allMessageIds.map((id) =>
+ recentInboundMessages.check(`${account.accountId}:${id}`),
+ );
+ if (dedupeEntries.length > 0 && dedupeEntries.every(Boolean)) return;
+
+ const senderId = post.user_id ?? payload.broadcast?.user_id;
+ if (!senderId) return;
+ if (senderId === botUserId) return;
+ if (isSystemPost(post)) return;
+
+ const channelInfo = await resolveChannelInfo(channelId);
+ const channelType = payload.data?.channel_type ?? channelInfo?.type ?? undefined;
+ const kind = channelKind(channelType);
+ const chatType = channelChatType(kind);
+
+ const teamId = payload.data?.team_id ?? channelInfo?.team_id ?? undefined;
+ const channelName = payload.data?.channel_name ?? channelInfo?.name ?? "";
+ const channelDisplay =
+ payload.data?.channel_display_name ?? channelInfo?.display_name ?? channelName;
+ const roomLabel = channelName ? `#${channelName}` : channelDisplay || `#${channelId}`;
+
+ const route = resolveAgentRoute({
+ cfg,
+ channel: "mattermost",
+ accountId: account.accountId,
+ teamId,
+ peer: {
+ kind,
+ id: kind === "dm" ? senderId : channelId,
+ },
+ });
+
+ const baseSessionKey = route.sessionKey;
+ const threadRootId = post.root_id?.trim() || undefined;
+ const threadKeys = resolveThreadSessionKeys({
+ baseSessionKey,
+ threadId: threadRootId,
+ parentSessionKey: threadRootId ? baseSessionKey : undefined,
+ });
+ const sessionKey = threadKeys.sessionKey;
+ const historyKey = kind === "dm" ? null : sessionKey;
+
+ const mentionRegexes = buildMentionRegexes(cfg, route.agentId);
+ const rawText = post.message?.trim() || "";
+ const wasMentioned =
+ kind !== "dm" &&
+ ((botUsername ? rawText.toLowerCase().includes(`@${botUsername.toLowerCase()}`) : false) ||
+ matchesMentionPatterns(rawText, mentionRegexes));
+ const pendingBody =
+ rawText ||
+ (post.file_ids?.length
+ ? `[Mattermost ${post.file_ids.length === 1 ? "file" : "files"}]`
+ : "");
+ const pendingSender = payload.data?.sender_name?.trim() || senderId;
+ const recordPendingHistory = () => {
+ if (!historyKey || historyLimit <= 0) return;
+ const trimmed = pendingBody.trim();
+ if (!trimmed) return;
+ recordPendingHistoryEntry({
+ historyMap: channelHistories,
+ historyKey,
+ limit: historyLimit,
+ entry: {
+ sender: pendingSender,
+ body: trimmed,
+ timestamp: typeof post.create_at === "number" ? post.create_at : undefined,
+ messageId: post.id ?? undefined,
+ },
+ });
+ };
+
+ const allowTextCommands = shouldHandleTextCommands({
+ cfg,
+ surface: "mattermost",
+ });
+ const isControlCommand = allowTextCommands && hasControlCommand(rawText, cfg);
+ const oncharEnabled = account.chatmode === "onchar" && kind !== "dm";
+ const oncharPrefixes = oncharEnabled ? resolveOncharPrefixes(account.oncharPrefixes) : [];
+ const oncharResult = oncharEnabled
+ ? stripOncharPrefix(rawText, oncharPrefixes)
+ : { triggered: false, stripped: rawText };
+ const oncharTriggered = oncharResult.triggered;
+
+ const shouldRequireMention = kind === "channel" && (account.requireMention ?? true);
+ const shouldBypassMention = isControlCommand && shouldRequireMention && !wasMentioned;
+ const effectiveWasMentioned = wasMentioned || shouldBypassMention || oncharTriggered;
+ const canDetectMention = Boolean(botUsername) || mentionRegexes.length > 0;
+
+ if (oncharEnabled && !oncharTriggered && !wasMentioned && !isControlCommand) {
+ recordPendingHistory();
+ return;
+ }
+
+ if (kind === "channel" && shouldRequireMention && canDetectMention) {
+ if (!effectiveWasMentioned) {
+ recordPendingHistory();
+ return;
+ }
+ }
+
+ const senderName =
+ payload.data?.sender_name?.trim() ||
+ (await resolveUserInfo(senderId))?.username?.trim() ||
+ senderId;
+ const mediaList = await resolveMattermostMedia(post.file_ids);
+ const mediaPlaceholder = buildMattermostAttachmentPlaceholder(mediaList);
+ const bodySource = oncharTriggered ? oncharResult.stripped : rawText;
+ const baseText = [bodySource, mediaPlaceholder].filter(Boolean).join("\n").trim();
+ const bodyText = normalizeMention(baseText, botUsername);
+ if (!bodyText) return;
+
+ recordChannelActivity({
+ channel: "mattermost",
+ accountId: account.accountId,
+ direction: "inbound",
+ });
+
+ const fromLabel = formatInboundFromLabel({
+ isGroup: kind !== "dm",
+ groupLabel: channelDisplay || roomLabel,
+ groupId: channelId,
+ groupFallback: roomLabel || "Channel",
+ directLabel: senderName,
+ directId: senderId,
+ });
+
+ const preview = bodyText.replace(/\s+/g, " ").slice(0, 160);
+ const inboundLabel =
+ kind === "dm"
+ ? `Mattermost DM from ${senderName}`
+ : `Mattermost message in ${roomLabel} from ${senderName}`;
+ enqueueSystemEvent(`${inboundLabel}: ${preview}`, {
+ sessionKey,
+ contextKey: `mattermost:message:${channelId}:${post.id ?? "unknown"}`,
+ });
+
+ const textWithId = `${bodyText}\n[mattermost message id: ${post.id ?? "unknown"} channel: ${channelId}]`;
+ const body = formatInboundEnvelope({
+ channel: "Mattermost",
+ from: fromLabel,
+ timestamp: typeof post.create_at === "number" ? post.create_at : undefined,
+ body: textWithId,
+ chatType,
+ sender: { name: senderName, id: senderId },
+ });
+ let combinedBody = body;
+ if (historyKey && historyLimit > 0) {
+ combinedBody = buildPendingHistoryContextFromMap({
+ historyMap: channelHistories,
+ historyKey,
+ limit: historyLimit,
+ currentMessage: combinedBody,
+ formatEntry: (entry) =>
+ formatInboundEnvelope({
+ channel: "Mattermost",
+ from: fromLabel,
+ timestamp: entry.timestamp,
+ body: `${entry.body}${
+ entry.messageId ? ` [id:${entry.messageId} channel:${channelId}]` : ""
+ }`,
+ chatType,
+ senderLabel: entry.sender,
+ }),
+ });
+ }
+
+ const to = kind === "dm" ? `user:${senderId}` : `channel:${channelId}`;
+ const mediaPayload = buildMattermostMediaPayload(mediaList);
+ const commandAuthorized = resolveCommandAuthorizedFromAuthorizers({
+ useAccessGroups: cfg.commands?.useAccessGroups ?? false,
+ authorizers: [],
+ });
+ const ctxPayload = finalizeInboundContext({
+ Body: combinedBody,
+ RawBody: bodyText,
+ CommandBody: bodyText,
+ From:
+ kind === "dm"
+ ? `mattermost:${senderId}`
+ : kind === "group"
+ ? `mattermost:group:${channelId}`
+ : `mattermost:channel:${channelId}`,
+ To: to,
+ SessionKey: sessionKey,
+ ParentSessionKey: threadKeys.parentSessionKey,
+ AccountId: route.accountId,
+ ChatType: chatType,
+ ConversationLabel: fromLabel,
+ GroupSubject: kind !== "dm" ? channelDisplay || roomLabel : undefined,
+ GroupChannel: channelName ? `#${channelName}` : undefined,
+ GroupSpace: teamId,
+ SenderName: senderName,
+ SenderId: senderId,
+ Provider: "mattermost" as const,
+ Surface: "mattermost" as const,
+ MessageSid: post.id ?? undefined,
+ MessageSids: allMessageIds.length > 1 ? allMessageIds : undefined,
+ MessageSidFirst: allMessageIds.length > 1 ? allMessageIds[0] : undefined,
+ MessageSidLast:
+ allMessageIds.length > 1 ? allMessageIds[allMessageIds.length - 1] : undefined,
+ ReplyToId: threadRootId,
+ MessageThreadId: threadRootId,
+ Timestamp: typeof post.create_at === "number" ? post.create_at : undefined,
+ WasMentioned: kind !== "dm" ? effectiveWasMentioned : undefined,
+ CommandAuthorized: commandAuthorized,
+ OriginatingChannel: "mattermost" as const,
+ OriginatingTo: to,
+ ...mediaPayload,
+ });
+
+ if (kind === "dm") {
+ const sessionCfg = cfg.session;
+ const storePath = resolveStorePath(sessionCfg?.store, {
+ agentId: route.agentId,
+ });
+ await updateLastRoute({
+ storePath,
+ sessionKey: route.mainSessionKey,
+ deliveryContext: {
+ channel: "mattermost",
+ to,
+ accountId: route.accountId,
+ },
+ });
+ }
+
+ if (shouldLogVerbose()) {
+ const previewLine = bodyText.slice(0, 200).replace(/\n/g, "\\n");
+ logVerbose(
+ `mattermost inbound: from=${ctxPayload.From} len=${bodyText.length} preview="${previewLine}"`,
+ );
+ }
+
+ const textLimit = resolveTextChunkLimit(cfg, "mattermost", account.accountId, {
+ fallbackLimit: account.textChunkLimit ?? 4000,
+ });
+
+ let prefixContext: ResponsePrefixContext = {
+ identityName: resolveIdentityName(cfg, route.agentId),
+ };
+
+ const { dispatcher, replyOptions, markDispatchIdle } = createReplyDispatcherWithTyping({
+ responsePrefix: resolveEffectiveMessagesConfig(cfg, route.agentId).responsePrefix,
+ responsePrefixContextProvider: () => prefixContext,
+ humanDelay: resolveHumanDelayConfig(cfg, route.agentId),
+ deliver: async (payload: ReplyPayload) => {
+ const mediaUrls = payload.mediaUrls ?? (payload.mediaUrl ? [payload.mediaUrl] : []);
+ const text = payload.text ?? "";
+ if (mediaUrls.length === 0) {
+ const chunks = chunkMarkdownText(text, textLimit);
+ for (const chunk of chunks.length > 0 ? chunks : [text]) {
+ if (!chunk) continue;
+ await sendMessageMattermost(to, chunk, {
+ accountId: account.accountId,
+ replyToId: threadRootId,
+ });
+ }
+ } else {
+ let first = true;
+ for (const mediaUrl of mediaUrls) {
+ const caption = first ? text : "";
+ first = false;
+ await sendMessageMattermost(to, caption, {
+ accountId: account.accountId,
+ mediaUrl,
+ replyToId: threadRootId,
+ });
+ }
+ }
+ runtime.log?.(`delivered reply to ${to}`);
+ },
+ onError: (err, info) => {
+ runtime.error?.(danger(`mattermost ${info.kind} reply failed: ${String(err)}`));
+ },
+ onReplyStart: () => sendTypingIndicator(channelId, threadRootId),
+ });
+
+ await dispatchReplyFromConfig({
+ ctx: ctxPayload,
+ cfg,
+ dispatcher,
+ replyOptions: {
+ ...replyOptions,
+ disableBlockStreaming:
+ typeof account.blockStreaming === "boolean" ? !account.blockStreaming : undefined,
+ onModelSelected: (ctx) => {
+ prefixContext.provider = ctx.provider;
+ prefixContext.model = extractShortModelName(ctx.model);
+ prefixContext.modelFull = `${ctx.provider}/${ctx.model}`;
+ prefixContext.thinkingLevel = ctx.thinkLevel ?? "off";
+ },
+ },
+ });
+ markDispatchIdle();
+ if (historyKey && historyLimit > 0) {
+ clearHistoryEntries({ historyMap: channelHistories, historyKey });
+ }
+ };
+
+ const inboundDebounceMs = resolveInboundDebounceMs({ cfg, channel: "mattermost" });
+ const debouncer = createInboundDebouncer<{
+ post: MattermostPost;
+ payload: MattermostEventPayload;
+ }>({
+ debounceMs: inboundDebounceMs,
+ buildKey: (entry) => {
+ const channelId =
+ entry.post.channel_id ??
+ entry.payload.data?.channel_id ??
+ entry.payload.broadcast?.channel_id;
+ if (!channelId) return null;
+ const threadId = entry.post.root_id?.trim();
+ const threadKey = threadId ? `thread:${threadId}` : "channel";
+ return `mattermost:${account.accountId}:${channelId}:${threadKey}`;
+ },
+ shouldDebounce: (entry) => {
+ if (entry.post.file_ids && entry.post.file_ids.length > 0) return false;
+ const text = entry.post.message?.trim() ?? "";
+ if (!text) return false;
+ return !hasControlCommand(text, cfg);
+ },
+ onFlush: async (entries) => {
+ const last = entries.at(-1);
+ if (!last) return;
+ if (entries.length === 1) {
+ await handlePost(last.post, last.payload);
+ return;
+ }
+ const combinedText = entries
+ .map((entry) => entry.post.message?.trim() ?? "")
+ .filter(Boolean)
+ .join("\n");
+ const mergedPost: MattermostPost = {
+ ...last.post,
+ message: combinedText,
+ file_ids: [],
+ };
+ const ids = entries.map((entry) => entry.post.id).filter(Boolean) as string[];
+ await handlePost(mergedPost, last.payload, ids.length > 0 ? ids : undefined);
+ },
+ onError: (err) => {
+ runtime.error?.(danger(`mattermost debounce flush failed: ${String(err)}`));
+ },
+ });
+
+ const wsUrl = buildMattermostWsUrl(baseUrl);
+ let seq = 1;
+
+ const connectOnce = async (): Promise => {
+ const ws = new WebSocket(wsUrl);
+ const onAbort = () => ws.close();
+ opts.abortSignal?.addEventListener("abort", onAbort, { once: true });
+
+ return await new Promise((resolve) => {
+ ws.on("open", () => {
+ opts.statusSink?.({
+ connected: true,
+ lastConnectedAt: Date.now(),
+ lastError: null,
+ });
+ ws.send(
+ JSON.stringify({
+ seq: seq++,
+ action: "authentication_challenge",
+ data: { token: botToken },
+ }),
+ );
+ });
+
+ ws.on("message", async (data) => {
+ const raw = rawDataToString(data);
+ let payload: MattermostEventPayload;
+ try {
+ payload = JSON.parse(raw) as MattermostEventPayload;
+ } catch {
+ return;
+ }
+ if (payload.event !== "posted") return;
+ const postData = payload.data?.post;
+ if (!postData) return;
+ let post: MattermostPost | null = null;
+ if (typeof postData === "string") {
+ try {
+ post = JSON.parse(postData) as MattermostPost;
+ } catch {
+ return;
+ }
+ } else if (typeof postData === "object") {
+ post = postData as MattermostPost;
+ }
+ if (!post) return;
+ try {
+ await debouncer.enqueue({ post, payload });
+ } catch (err) {
+ runtime.error?.(danger(`mattermost handler failed: ${String(err)}`));
+ }
+ });
+
+ ws.on("close", (code, reason) => {
+ const message = reason.length > 0 ? reason.toString("utf8") : "";
+ opts.statusSink?.({
+ connected: false,
+ lastDisconnect: {
+ at: Date.now(),
+ status: code,
+ error: message || undefined,
+ },
+ });
+ opts.abortSignal?.removeEventListener("abort", onAbort);
+ resolve();
+ });
+
+ ws.on("error", (err) => {
+ runtime.error?.(danger(`mattermost websocket error: ${String(err)}`));
+ opts.statusSink?.({
+ lastError: String(err),
+ });
+ });
+ });
+ };
+
+ while (!opts.abortSignal?.aborted) {
+ await connectOnce();
+ if (opts.abortSignal?.aborted) return;
+ await new Promise((resolve) => setTimeout(resolve, 2000));
+ }
+}
diff --git a/src/mattermost/probe.ts b/src/mattermost/probe.ts
new file mode 100644
index 000000000..c0fa8ae63
--- /dev/null
+++ b/src/mattermost/probe.ts
@@ -0,0 +1,70 @@
+import { normalizeMattermostBaseUrl, type MattermostUser } from "./client.js";
+
+export type MattermostProbe = {
+ ok: boolean;
+ status?: number | null;
+ error?: string | null;
+ elapsedMs?: number | null;
+ bot?: MattermostUser;
+};
+
+async function readMattermostError(res: Response): Promise {
+ const contentType = res.headers.get("content-type") ?? "";
+ if (contentType.includes("application/json")) {
+ const data = (await res.json()) as { message?: string } | undefined;
+ if (data?.message) return data.message;
+ return JSON.stringify(data);
+ }
+ return await res.text();
+}
+
+export async function probeMattermost(
+ baseUrl: string,
+ botToken: string,
+ timeoutMs = 2500,
+): Promise {
+ const normalized = normalizeMattermostBaseUrl(baseUrl);
+ if (!normalized) {
+ return { ok: false, error: "baseUrl missing" };
+ }
+ const url = `${normalized}/api/v4/users/me`;
+ const start = Date.now();
+ const controller = timeoutMs > 0 ? new AbortController() : undefined;
+ let timer: NodeJS.Timeout | null = null;
+ if (controller) {
+ timer = setTimeout(() => controller.abort(), timeoutMs);
+ }
+ try {
+ const res = await fetch(url, {
+ headers: { Authorization: `Bearer ${botToken}` },
+ signal: controller?.signal,
+ });
+ const elapsedMs = Date.now() - start;
+ if (!res.ok) {
+ const detail = await readMattermostError(res);
+ return {
+ ok: false,
+ status: res.status,
+ error: detail || res.statusText,
+ elapsedMs,
+ };
+ }
+ const bot = (await res.json()) as MattermostUser;
+ return {
+ ok: true,
+ status: res.status,
+ elapsedMs,
+ bot,
+ };
+ } catch (err) {
+ const message = err instanceof Error ? err.message : String(err);
+ return {
+ ok: false,
+ status: null,
+ error: message,
+ elapsedMs: Date.now() - start,
+ };
+ } finally {
+ if (timer) clearTimeout(timer);
+ }
+}
diff --git a/src/mattermost/send.ts b/src/mattermost/send.ts
new file mode 100644
index 000000000..40f038cc0
--- /dev/null
+++ b/src/mattermost/send.ts
@@ -0,0 +1,207 @@
+import { loadConfig } from "../config/config.js";
+import { logVerbose, shouldLogVerbose } from "../globals.js";
+import { recordChannelActivity } from "../infra/channel-activity.js";
+import { loadWebMedia } from "../web/media.js";
+import { resolveMattermostAccount } from "./accounts.js";
+import {
+ createMattermostClient,
+ createMattermostDirectChannel,
+ createMattermostPost,
+ fetchMattermostMe,
+ fetchMattermostUserByUsername,
+ normalizeMattermostBaseUrl,
+ uploadMattermostFile,
+ type MattermostUser,
+} from "./client.js";
+
+export type MattermostSendOpts = {
+ botToken?: string;
+ baseUrl?: string;
+ accountId?: string;
+ mediaUrl?: string;
+ replyToId?: string;
+};
+
+export type MattermostSendResult = {
+ messageId: string;
+ channelId: string;
+};
+
+type MattermostTarget =
+ | { kind: "channel"; id: string }
+ | { kind: "user"; id?: string; username?: string };
+
+const botUserCache = new Map();
+const userByNameCache = new Map();
+
+function cacheKey(baseUrl: string, token: string): string {
+ return `${baseUrl}::${token}`;
+}
+
+function normalizeMessage(text: string, mediaUrl?: string): string {
+ const trimmed = text.trim();
+ const media = mediaUrl?.trim();
+ return [trimmed, media].filter(Boolean).join("\n");
+}
+
+function isHttpUrl(value: string): boolean {
+ return /^https?:\/\//i.test(value);
+}
+
+function parseMattermostTarget(raw: string): MattermostTarget {
+ const trimmed = raw.trim();
+ if (!trimmed) throw new Error("Recipient is required for Mattermost sends");
+ const lower = trimmed.toLowerCase();
+ if (lower.startsWith("channel:")) {
+ const id = trimmed.slice("channel:".length).trim();
+ if (!id) throw new Error("Channel id is required for Mattermost sends");
+ return { kind: "channel", id };
+ }
+ if (lower.startsWith("user:")) {
+ const id = trimmed.slice("user:".length).trim();
+ if (!id) throw new Error("User id is required for Mattermost sends");
+ return { kind: "user", id };
+ }
+ if (lower.startsWith("mattermost:")) {
+ const id = trimmed.slice("mattermost:".length).trim();
+ if (!id) throw new Error("User id is required for Mattermost sends");
+ return { kind: "user", id };
+ }
+ if (trimmed.startsWith("@")) {
+ const username = trimmed.slice(1).trim();
+ if (!username) {
+ throw new Error("Username is required for Mattermost sends");
+ }
+ return { kind: "user", username };
+ }
+ return { kind: "channel", id: trimmed };
+}
+
+async function resolveBotUser(baseUrl: string, token: string): Promise {
+ const key = cacheKey(baseUrl, token);
+ const cached = botUserCache.get(key);
+ if (cached) return cached;
+ const client = createMattermostClient({ baseUrl, botToken: token });
+ const user = await fetchMattermostMe(client);
+ botUserCache.set(key, user);
+ return user;
+}
+
+async function resolveUserIdByUsername(params: {
+ baseUrl: string;
+ token: string;
+ username: string;
+}): Promise {
+ const { baseUrl, token, username } = params;
+ const key = `${cacheKey(baseUrl, token)}::${username.toLowerCase()}`;
+ const cached = userByNameCache.get(key);
+ if (cached?.id) return cached.id;
+ const client = createMattermostClient({ baseUrl, botToken: token });
+ const user = await fetchMattermostUserByUsername(client, username);
+ userByNameCache.set(key, user);
+ return user.id;
+}
+
+async function resolveTargetChannelId(params: {
+ target: MattermostTarget;
+ baseUrl: string;
+ token: string;
+}): Promise {
+ if (params.target.kind === "channel") return params.target.id;
+ const userId = params.target.id
+ ? params.target.id
+ : await resolveUserIdByUsername({
+ baseUrl: params.baseUrl,
+ token: params.token,
+ username: params.target.username ?? "",
+ });
+ const botUser = await resolveBotUser(params.baseUrl, params.token);
+ const client = createMattermostClient({
+ baseUrl: params.baseUrl,
+ botToken: params.token,
+ });
+ const channel = await createMattermostDirectChannel(client, [botUser.id, userId]);
+ return channel.id;
+}
+
+export async function sendMessageMattermost(
+ to: string,
+ text: string,
+ opts: MattermostSendOpts = {},
+): Promise {
+ const cfg = loadConfig();
+ const account = resolveMattermostAccount({
+ cfg,
+ accountId: opts.accountId,
+ });
+ const token = opts.botToken?.trim() || account.botToken?.trim();
+ if (!token) {
+ throw new Error(
+ `Mattermost bot token missing for account "${account.accountId}" (set channels.mattermost.accounts.${account.accountId}.botToken or MATTERMOST_BOT_TOKEN for default).`,
+ );
+ }
+ const baseUrl = normalizeMattermostBaseUrl(opts.baseUrl ?? account.baseUrl);
+ if (!baseUrl) {
+ throw new Error(
+ `Mattermost baseUrl missing for account "${account.accountId}" (set channels.mattermost.accounts.${account.accountId}.baseUrl or MATTERMOST_URL for default).`,
+ );
+ }
+
+ const target = parseMattermostTarget(to);
+ const channelId = await resolveTargetChannelId({
+ target,
+ baseUrl,
+ token,
+ });
+
+ const client = createMattermostClient({ baseUrl, botToken: token });
+ let message = text?.trim() ?? "";
+ let fileIds: string[] | undefined;
+ let uploadError: Error | undefined;
+ const mediaUrl = opts.mediaUrl?.trim();
+ if (mediaUrl) {
+ try {
+ const media = await loadWebMedia(mediaUrl);
+ const fileInfo = await uploadMattermostFile(client, {
+ channelId,
+ buffer: media.buffer,
+ fileName: media.fileName ?? "upload",
+ contentType: media.contentType ?? undefined,
+ });
+ fileIds = [fileInfo.id];
+ } catch (err) {
+ uploadError = err instanceof Error ? err : new Error(String(err));
+ if (shouldLogVerbose()) {
+ logVerbose(
+ `mattermost send: media upload failed, falling back to URL text: ${String(err)}`,
+ );
+ }
+ message = normalizeMessage(message, isHttpUrl(mediaUrl) ? mediaUrl : "");
+ }
+ }
+
+ if (!message && (!fileIds || fileIds.length === 0)) {
+ if (uploadError) {
+ throw new Error(`Mattermost media upload failed: ${uploadError.message}`);
+ }
+ throw new Error("Mattermost message is empty");
+ }
+
+ const post = await createMattermostPost(client, {
+ channelId,
+ message,
+ rootId: opts.replyToId,
+ fileIds,
+ });
+
+ recordChannelActivity({
+ channel: "mattermost",
+ accountId: account.accountId,
+ direction: "outbound",
+ });
+
+ return {
+ messageId: post.id ?? "unknown",
+ channelId,
+ };
+}
diff --git a/src/plugin-sdk/index.ts b/src/plugin-sdk/index.ts
index 8bef1da37..62979bdd1 100644
--- a/src/plugin-sdk/index.ts
+++ b/src/plugin-sdk/index.ts
@@ -81,6 +81,7 @@ export type {
export {
DiscordConfigSchema,
IMessageConfigSchema,
+ MattermostConfigSchema,
MSTeamsConfigSchema,
SignalConfigSchema,
SlackConfigSchema,
@@ -120,6 +121,7 @@ export {
resolveBlueBubblesGroupRequireMention,
resolveDiscordGroupRequireMention,
resolveIMessageGroupRequireMention,
+ resolveMattermostGroupRequireMention,
resolveSlackGroupRequireMention,
resolveTelegramGroupRequireMention,
resolveWhatsAppGroupRequireMention,
@@ -236,6 +238,21 @@ export {
normalizeSlackMessagingTarget,
} from "../channels/plugins/normalize/slack.js";
+// Channel: Mattermost
+export {
+ listEnabledMattermostAccounts,
+ listMattermostAccountIds,
+ resolveDefaultMattermostAccountId,
+ resolveMattermostAccount,
+ type ResolvedMattermostAccount,
+} from "../mattermost/accounts.js";
+export { normalizeMattermostBaseUrl } from "../mattermost/client.js";
+export { mattermostOnboardingAdapter } from "../channels/plugins/onboarding/mattermost.js";
+export {
+ looksLikeMattermostTargetId,
+ normalizeMattermostMessagingTarget,
+} from "../channels/plugins/normalize/mattermost.js";
+
// Channel: Telegram
export {
listTelegramAccountIds,
diff --git a/src/plugins/runtime/index.ts b/src/plugins/runtime/index.ts
index 4765c71c7..e564ad2f8 100644
--- a/src/plugins/runtime/index.ts
+++ b/src/plugins/runtime/index.ts
@@ -57,6 +57,9 @@ import { enqueueSystemEvent } from "../../infra/system-events.js";
import { monitorIMessageProvider } from "../../imessage/monitor.js";
import { probeIMessage } from "../../imessage/probe.js";
import { sendMessageIMessage } from "../../imessage/send.js";
+import { monitorMattermostProvider } from "../../mattermost/monitor.js";
+import { probeMattermost } from "../../mattermost/probe.js";
+import { sendMessageMattermost } from "../../mattermost/send.js";
import { shouldLogVerbose } from "../../globals.js";
import { getChildLogger } from "../../logging.js";
import { normalizeLogLevel } from "../../logging/levels.js";
@@ -230,6 +233,11 @@ export function createPluginRuntime(): PluginRuntime {
monitorSlackProvider,
handleSlackAction,
},
+ mattermost: {
+ probeMattermost,
+ sendMessageMattermost,
+ monitorMattermostProvider,
+ },
telegram: {
auditGroupMembership: auditTelegramGroupMembership,
collectUnmentionedGroupIds: collectTelegramUnmentionedGroupIds,
diff --git a/src/plugins/runtime/types.ts b/src/plugins/runtime/types.ts
index 089e20c37..31350693c 100644
--- a/src/plugins/runtime/types.ts
+++ b/src/plugins/runtime/types.ts
@@ -98,6 +98,10 @@ type ResolveSlackUserAllowlist =
type SendMessageSlack = typeof import("../../slack/send.js").sendMessageSlack;
type MonitorSlackProvider = typeof import("../../slack/index.js").monitorSlackProvider;
type HandleSlackAction = typeof import("../../agents/tools/slack-actions.js").handleSlackAction;
+type ProbeMattermost = typeof import("../../mattermost/probe.js").probeMattermost;
+type SendMessageMattermost = typeof import("../../mattermost/send.js").sendMessageMattermost;
+type MonitorMattermostProvider =
+ typeof import("../../mattermost/monitor.js").monitorMattermostProvider;
type AuditTelegramGroupMembership =
typeof import("../../telegram/audit.js").auditTelegramGroupMembership;
type CollectTelegramUnmentionedGroupIds =
@@ -242,6 +246,11 @@ export type PluginRuntime = {
monitorSlackProvider: MonitorSlackProvider;
handleSlackAction: HandleSlackAction;
};
+ mattermost: {
+ probeMattermost: ProbeMattermost;
+ sendMessageMattermost: SendMessageMattermost;
+ monitorMattermostProvider: MonitorMattermostProvider;
+ };
telegram: {
auditGroupMembership: AuditTelegramGroupMembership;
collectUnmentionedGroupIds: CollectTelegramUnmentionedGroupIds;
diff --git a/src/utils/message-channel.ts b/src/utils/message-channel.ts
index ecd1f713b..c09436ac8 100644
--- a/src/utils/message-channel.ts
+++ b/src/utils/message-channel.ts
@@ -22,6 +22,7 @@ const MARKDOWN_CAPABLE_CHANNELS = new Set([
"telegram",
"signal",
"discord",
+ "mattermost",
"tui",
INTERNAL_MESSAGE_CHANNEL,
]);
diff --git a/ui/src/ui/types.ts b/ui/src/ui/types.ts
index 6cdbfb029..aaf89b9e9 100644
--- a/ui/src/ui/types.ts
+++ b/ui/src/ui/types.ts
@@ -164,6 +164,39 @@ export type SlackStatus = {
lastProbeAt?: number | null;
};
+export type MattermostBot = {
+ id?: string | null;
+ username?: string | null;
+};
+
+export type MattermostProbe = {
+ ok: boolean;
+ status?: number | null;
+ error?: string | null;
+ elapsedMs?: number | null;
+ bot?: MattermostBot | null;
+};
+
+export type MattermostStatus = {
+ configured: boolean;
+ botTokenSource?: string | null;
+ running: boolean;
+ connected?: boolean | null;
+ lastConnectedAt?: number | null;
+ lastDisconnect?: {
+ at: number;
+ status?: number | null;
+ error?: string | null;
+ loggedOut?: boolean | null;
+ } | null;
+ lastStartAt?: number | null;
+ lastStopAt?: number | null;
+ lastError?: string | null;
+ baseUrl?: string | null;
+ probe?: MattermostProbe | null;
+ lastProbeAt?: number | null;
+};
+
export type SignalProbe = {
ok: boolean;
status?: number | null;
@@ -363,6 +396,7 @@ export type CronPayload =
| "telegram"
| "discord"
| "slack"
+ | "mattermost"
| "signal"
| "imessage"
| "msteams";
diff --git a/ui/src/ui/views/channels.mattermost.ts b/ui/src/ui/views/channels.mattermost.ts
new file mode 100644
index 000000000..c2513ed44
--- /dev/null
+++ b/ui/src/ui/views/channels.mattermost.ts
@@ -0,0 +1,70 @@
+import { html, nothing } from "lit";
+
+import { formatAgo } from "../format";
+import type { MattermostStatus } from "../types";
+import type { ChannelsProps } from "./channels.types";
+import { renderChannelConfigSection } from "./channels.config";
+
+export function renderMattermostCard(params: {
+ props: ChannelsProps;
+ mattermost?: MattermostStatus | null;
+ accountCountLabel: unknown;
+}) {
+ const { props, mattermost, accountCountLabel } = params;
+
+ return html`
+
+ Mattermost
+ Bot token + WebSocket status and configuration.
+ ${accountCountLabel}
+
+
+
+ Configured
+ ${mattermost?.configured ? "Yes" : "No"}
+
+
+ Running
+ ${mattermost?.running ? "Yes" : "No"}
+
+
+ Connected
+ ${mattermost?.connected ? "Yes" : "No"}
+
+
+ Base URL
+ ${mattermost?.baseUrl || "n/a"}
+
+
+ Last start
+ ${mattermost?.lastStartAt ? formatAgo(mattermost.lastStartAt) : "n/a"}
+
+
+ Last probe
+ ${mattermost?.lastProbeAt ? formatAgo(mattermost.lastProbeAt) : "n/a"}
+
+
+
+ ${mattermost?.lastError
+ ? html`
+ ${mattermost.lastError}
+ `
+ : nothing}
+
+ ${mattermost?.probe
+ ? html`
+ Probe ${mattermost.probe.ok ? "ok" : "failed"} -
+ ${mattermost.probe.status ?? ""} ${mattermost.probe.error ?? ""}
+ `
+ : nothing}
+
+ ${renderChannelConfigSection({ channelId: "mattermost", props })}
+
+
+
+
+
+ `;
+}
diff --git a/ui/src/ui/views/channels.ts b/ui/src/ui/views/channels.ts
index 232cf2c85..96a6b8556 100644
--- a/ui/src/ui/views/channels.ts
+++ b/ui/src/ui/views/channels.ts
@@ -7,6 +7,7 @@ import type {
ChannelsStatusSnapshot,
DiscordStatus,
IMessageStatus,
+ MattermostStatus,
NostrProfile,
NostrStatus,
SignalStatus,
@@ -23,6 +24,7 @@ import { channelEnabled, renderChannelAccountCount } from "./channels.shared";
import { renderChannelConfigSection } from "./channels.config";
import { renderDiscordCard } from "./channels.discord";
import { renderIMessageCard } from "./channels.imessage";
+import { renderMattermostCard } from "./channels.mattermost";
import { renderNostrCard } from "./channels.nostr";
import { renderSignalCard } from "./channels.signal";
import { renderSlackCard } from "./channels.slack";
@@ -39,6 +41,7 @@ export function renderChannels(props: ChannelsProps) {
| undefined;
const discord = (channels?.discord ?? null) as DiscordStatus | null;
const slack = (channels?.slack ?? null) as SlackStatus | null;
+ const mattermost = (channels?.mattermost ?? null) as MattermostStatus | null;
const signal = (channels?.signal ?? null) as SignalStatus | null;
const imessage = (channels?.imessage ?? null) as IMessageStatus | null;
const nostr = (channels?.nostr ?? null) as NostrStatus | null;
@@ -62,6 +65,7 @@ export function renderChannels(props: ChannelsProps) {
telegram,
discord,
slack,
+ mattermost,
signal,
imessage,
nostr,
@@ -97,7 +101,7 @@ function resolveChannelOrder(snapshot: ChannelsStatusSnapshot | null): ChannelKe
if (snapshot?.channelOrder?.length) {
return snapshot.channelOrder;
}
- return ["whatsapp", "telegram", "discord", "slack", "signal", "imessage", "nostr"];
+ return ["whatsapp", "telegram", "discord", "slack", "mattermost", "signal", "imessage", "nostr"];
}
function renderChannel(
@@ -135,6 +139,12 @@ function renderChannel(
slack: data.slack,
accountCountLabel,
});
+ case "mattermost":
+ return renderMattermostCard({
+ props,
+ mattermost: data.mattermost,
+ accountCountLabel,
+ });
case "signal":
return renderSignalCard({
props,
diff --git a/ui/src/ui/views/channels.types.ts b/ui/src/ui/views/channels.types.ts
index 43576d54a..d3a98d44e 100644
--- a/ui/src/ui/views/channels.types.ts
+++ b/ui/src/ui/views/channels.types.ts
@@ -4,6 +4,7 @@ import type {
ConfigUiHints,
DiscordStatus,
IMessageStatus,
+ MattermostStatus,
NostrProfile,
NostrStatus,
SignalStatus,
@@ -53,6 +54,7 @@ export type ChannelsChannelData = {
telegram?: TelegramStatus;
discord?: DiscordStatus | null;
slack?: SlackStatus | null;
+ mattermost?: MattermostStatus | null;
signal?: SignalStatus | null;
imessage?: IMessageStatus | null;
nostr?: NostrStatus | null;
From 256fdcb3cf4da93160b0ed231284c0e5b1c7f6f0 Mon Sep 17 00:00:00 2001
From: Robby
Date: Thu, 22 Jan 2026 08:28:55 +0000
Subject: [PATCH 02/97] fix: include skills in minimal prompt mode for
subagents
---
src/agents/system-prompt.ts | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/src/agents/system-prompt.ts b/src/agents/system-prompt.ts
index 9716fed0d..4260c96ab 100644
--- a/src/agents/system-prompt.ts
+++ b/src/agents/system-prompt.ts
@@ -18,7 +18,7 @@ function buildSkillsSection(params: {
readToolName: string;
}) {
const trimmed = params.skillsPrompt?.trim();
- if (!trimmed || params.isMinimal) return [];
+ if (!trimmed) return [];
return [
"## Skills (mandatory)",
"Before replying: scan entries.",
From f2606a17ba61fd70772c6995887a690017fce822 Mon Sep 17 00:00:00 2001
From: Peter Steinberger
Date: Thu, 22 Jan 2026 08:48:09 +0000
Subject: [PATCH 03/97] chore: update a2ui bundle hash
---
src/canvas-host/a2ui/.bundle.hash | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/src/canvas-host/a2ui/.bundle.hash b/src/canvas-host/a2ui/.bundle.hash
index 94549dce8..a4155f132 100644
--- a/src/canvas-host/a2ui/.bundle.hash
+++ b/src/canvas-host/a2ui/.bundle.hash
@@ -1 +1 @@
-70ce2f8889599d5d76bccea69516e3136fd25fd32e43fe055d05faca822b47c7
+a99455ba0c4d0aad0a110bf25440c208b798198d5524b269f0f2d3f984262ae4
From cc8506ae79d7c5b54fbc078bd34db736e96d2524 Mon Sep 17 00:00:00 2001
From: Peter Steinberger
Date: Thu, 22 Jan 2026 08:48:13 +0000
Subject: [PATCH 04/97] fix: refresh menu sessions on reconnect
---
.../Clawdbot/MenuSessionsInjector.swift | 47 +++++++++++++++++++
1 file changed, 47 insertions(+)
diff --git a/apps/macos/Sources/Clawdbot/MenuSessionsInjector.swift b/apps/macos/Sources/Clawdbot/MenuSessionsInjector.swift
index ad0487893..6aab7910e 100644
--- a/apps/macos/Sources/Clawdbot/MenuSessionsInjector.swift
+++ b/apps/macos/Sources/Clawdbot/MenuSessionsInjector.swift
@@ -1,4 +1,5 @@
import AppKit
+import Observation
import SwiftUI
@MainActor
@@ -18,6 +19,7 @@ final class MenuSessionsInjector: NSObject, NSMenuDelegate {
private var isMenuOpen = false
private var lastKnownMenuWidth: CGFloat?
private var menuOpenWidth: CGFloat?
+ private var isObservingControlChannel = false
private var cachedSnapshot: SessionStoreSnapshot?
private var cachedErrorText: String?
@@ -50,6 +52,7 @@ final class MenuSessionsInjector: NSObject, NSMenuDelegate {
self.loadTask = Task { await self.refreshCache(force: true) }
}
+ self.startControlChannelObservation()
self.nodesStore.start()
}
@@ -96,6 +99,50 @@ final class MenuSessionsInjector: NSObject, NSMenuDelegate {
self.cancelPreviewTasks()
}
+ private func startControlChannelObservation() {
+ guard !self.isObservingControlChannel else { return }
+ self.isObservingControlChannel = true
+ self.observeControlChannelState()
+ }
+
+ private func observeControlChannelState() {
+ withObservationTracking {
+ _ = ControlChannel.shared.state
+ } onChange: { [weak self] in
+ Task { @MainActor [weak self] in
+ guard let self else { return }
+ self.handleControlChannelStateChange()
+ self.observeControlChannelState()
+ }
+ }
+ }
+
+ private func handleControlChannelStateChange() {
+ guard self.isMenuOpen, let menu = self.statusItem?.menu else { return }
+ self.loadTask?.cancel()
+ self.loadTask = Task { [weak self, weak menu] in
+ guard let self, let menu else { return }
+ await self.refreshCache(force: true)
+ await self.refreshUsageCache(force: true)
+ await self.refreshCostUsageCache(force: true)
+ await MainActor.run {
+ guard self.isMenuOpen else { return }
+ self.inject(into: menu)
+ self.injectNodes(into: menu)
+ }
+ }
+
+ self.nodesLoadTask?.cancel()
+ self.nodesLoadTask = Task { [weak self, weak menu] in
+ guard let self, let menu else { return }
+ await self.nodesStore.refresh()
+ await MainActor.run {
+ guard self.isMenuOpen else { return }
+ self.injectNodes(into: menu)
+ }
+ }
+ }
+
func menuNeedsUpdate(_ menu: NSMenu) {
self.originalDelegate?.menuNeedsUpdate?(menu)
}
From 54e0fc342e61c9346b96972cea1ee652281fc027 Mon Sep 17 00:00:00 2001
From: Peter Steinberger
Date: Thu, 22 Jan 2026 08:49:51 +0000
Subject: [PATCH 05/97] fix: wrap cli banner tagline
---
src/cli/banner.ts | 24 +++++++++++++++++++++---
1 file changed, 21 insertions(+), 3 deletions(-)
diff --git a/src/cli/banner.ts b/src/cli/banner.ts
index 023f0c85c..d697be8e5 100644
--- a/src/cli/banner.ts
+++ b/src/cli/banner.ts
@@ -1,10 +1,12 @@
import { resolveCommitHash } from "../infra/git-commit.js";
+import { visibleWidth } from "../terminal/ansi.js";
import { isRich, theme } from "../terminal/theme.js";
import { pickTagline, type TaglineOptions } from "./tagline.js";
type BannerOptions = TaglineOptions & {
argv?: string[];
commit?: string | null;
+ columns?: number;
richTty?: boolean;
};
@@ -36,12 +38,28 @@ export function formatCliBannerLine(version: string, options: BannerOptions = {}
const tagline = pickTagline(options);
const rich = options.richTty ?? isRich();
const title = "🦞 Clawdbot";
+ const prefix = "🦞 ";
+ const columns = options.columns ?? process.stdout.columns ?? 120;
+ const plainFullLine = `${title} ${version} (${commitLabel}) — ${tagline}`;
+ const fitsOnOneLine = visibleWidth(plainFullLine) <= columns;
if (rich) {
- return `${theme.heading(title)} ${theme.info(version)} ${theme.muted(
+ if (fitsOnOneLine) {
+ return `${theme.heading(title)} ${theme.info(version)} ${theme.muted(
+ `(${commitLabel})`,
+ )} ${theme.muted("—")} ${theme.accentDim(tagline)}`;
+ }
+ const line1 = `${theme.heading(title)} ${theme.info(version)} ${theme.muted(
`(${commitLabel})`,
- )} ${theme.muted("—")} ${theme.accentDim(tagline)}`;
+ )}`;
+ const line2 = `${" ".repeat(prefix.length)}${theme.muted("—")} ${theme.accentDim(tagline)}`;
+ return `${line1}\n${line2}`;
}
- return `${title} ${version} (${commitLabel}) — ${tagline}`;
+ if (fitsOnOneLine) {
+ return plainFullLine;
+ }
+ const line1 = `${title} ${version} (${commitLabel})`;
+ const line2 = `${" ".repeat(prefix.length)}— ${tagline}`;
+ return `${line1}\n${line2}`;
}
const LOBSTER_ASCII = [
From 0e17e55be9025a341ff18c8fdcb9c79c86a333d1 Mon Sep 17 00:00:00 2001
From: Peter Steinberger
Date: Thu, 22 Jan 2026 08:51:14 +0000
Subject: [PATCH 06/97] fix: cache usage cost summary
---
src/cli/gateway-cli/register.ts | 59 ++++++++++++++++++++++++++
src/gateway/server-methods/usage.ts | 66 ++++++++++++++++++++++++++++-
src/infra/session-cost-usage.ts | 16 +++++--
3 files changed, 136 insertions(+), 5 deletions(-)
diff --git a/src/cli/gateway-cli/register.ts b/src/cli/gateway-cli/register.ts
index 1f094699e..8334cc6f7 100644
--- a/src/cli/gateway-cli/register.ts
+++ b/src/cli/gateway-cli/register.ts
@@ -2,10 +2,12 @@ import type { Command } from "commander";
import { gatewayStatusCommand } from "../../commands/gateway-status.js";
import { formatHealthChannelLines, type HealthSummary } from "../../commands/health.js";
import { discoverGatewayBeacons } from "../../infra/bonjour-discovery.js";
+import type { CostUsageSummary } from "../../infra/session-cost-usage.js";
import { WIDE_AREA_DISCOVERY_DOMAIN } from "../../infra/widearea-dns.js";
import { defaultRuntime } from "../../runtime.js";
import { formatDocsLink } from "../../terminal/links.js";
import { colorize, isRich, theme } from "../../terminal/theme.js";
+import { formatTokenCount, formatUsd } from "../../utils/usage-format.js";
import { withProgress } from "../progress.js";
import { runCommandWithRuntime } from "../cli-utils.js";
import {
@@ -58,6 +60,41 @@ function runGatewayCommand(action: () => Promise, label?: string) {
});
}
+function parseDaysOption(raw: unknown, fallback = 30): number {
+ if (typeof raw === "number" && Number.isFinite(raw)) return Math.max(1, Math.floor(raw));
+ if (typeof raw === "string" && raw.trim() !== "") {
+ const parsed = Number(raw);
+ if (Number.isFinite(parsed)) return Math.max(1, Math.floor(parsed));
+ }
+ return fallback;
+}
+
+function renderCostUsageSummary(summary: CostUsageSummary, days: number, rich: boolean): string[] {
+ const totalCost = formatUsd(summary.totals.totalCost) ?? "$0.00";
+ const totalTokens = formatTokenCount(summary.totals.totalTokens) ?? "0";
+ const lines = [
+ colorize(rich, theme.heading, `Usage cost (${days} days)`),
+ `${colorize(rich, theme.muted, "Total:")} ${totalCost} · ${totalTokens} tokens`,
+ ];
+
+ if (summary.totals.missingCostEntries > 0) {
+ lines.push(
+ `${colorize(rich, theme.muted, "Missing entries:")} ${summary.totals.missingCostEntries}`,
+ );
+ }
+
+ const latest = summary.daily.at(-1);
+ if (latest) {
+ const latestCost = formatUsd(latest.totalCost) ?? "$0.00";
+ const latestTokens = formatTokenCount(latest.totalTokens) ?? "0";
+ lines.push(
+ `${colorize(rich, theme.muted, "Latest day:")} ${latest.date} · ${latestCost} · ${latestTokens} tokens`,
+ );
+ }
+
+ return lines;
+}
+
export function registerGatewayCli(program: Command) {
const gateway = addGatewayRunCommand(
program
@@ -160,6 +197,28 @@ export function registerGatewayCli(program: Command) {
}),
);
+ gatewayCallOpts(
+ gateway
+ .command("usage-cost")
+ .description("Fetch usage cost summary from session logs")
+ .option("--days ", "Number of days to include", "30")
+ .action(async (opts) => {
+ await runGatewayCommand(async () => {
+ const days = parseDaysOption(opts.days);
+ const result = await callGatewayCli("usage.cost", opts, { days });
+ if (opts.json) {
+ defaultRuntime.log(JSON.stringify(result, null, 2));
+ return;
+ }
+ const rich = isRich();
+ const summary = result as CostUsageSummary;
+ for (const line of renderCostUsageSummary(summary, days, rich)) {
+ defaultRuntime.log(line);
+ }
+ }, "Gateway usage cost failed");
+ }),
+ );
+
gatewayCallOpts(
gateway
.command("health")
diff --git a/src/gateway/server-methods/usage.ts b/src/gateway/server-methods/usage.ts
index e6d9b3722..dcdd89742 100644
--- a/src/gateway/server-methods/usage.ts
+++ b/src/gateway/server-methods/usage.ts
@@ -1,16 +1,78 @@
import { loadConfig } from "../../config/config.js";
+import type { CostUsageSummary } from "../../infra/session-cost-usage.js";
import { loadCostUsageSummary } from "../../infra/session-cost-usage.js";
import { loadProviderUsageSummary } from "../../infra/provider-usage.js";
import type { GatewayRequestHandlers } from "./types.js";
+const COST_USAGE_CACHE_TTL_MS = 30_000;
+
+type CostUsageCacheEntry = {
+ summary?: CostUsageSummary;
+ updatedAt?: number;
+ inFlight?: Promise;
+};
+
+const costUsageCache = new Map();
+
+const parseDays = (raw: unknown): number => {
+ if (typeof raw === "number" && Number.isFinite(raw)) return Math.floor(raw);
+ if (typeof raw === "string" && raw.trim() !== "") {
+ const parsed = Number(raw);
+ if (Number.isFinite(parsed)) return Math.floor(parsed);
+ }
+ return 30;
+};
+
+async function loadCostUsageSummaryCached(params: {
+ days: number;
+ config: ReturnType;
+}): Promise {
+ const days = Math.max(1, params.days);
+ const now = Date.now();
+ const cached = costUsageCache.get(days);
+ if (cached?.summary && cached.updatedAt && now - cached.updatedAt < COST_USAGE_CACHE_TTL_MS) {
+ return cached.summary;
+ }
+
+ if (cached?.inFlight) {
+ if (cached.summary) return cached.summary;
+ return await cached.inFlight;
+ }
+
+ const entry: CostUsageCacheEntry = cached ?? {};
+ const inFlight = loadCostUsageSummary({ days, config: params.config })
+ .then((summary) => {
+ costUsageCache.set(days, { summary, updatedAt: Date.now() });
+ return summary;
+ })
+ .catch((err) => {
+ if (entry.summary) return entry.summary;
+ throw err;
+ })
+ .finally(() => {
+ const current = costUsageCache.get(days);
+ if (current?.inFlight === inFlight) {
+ current.inFlight = undefined;
+ costUsageCache.set(days, current);
+ }
+ });
+
+ entry.inFlight = inFlight;
+ costUsageCache.set(days, entry);
+
+ if (entry.summary) return entry.summary;
+ return await inFlight;
+}
+
export const usageHandlers: GatewayRequestHandlers = {
"usage.status": async ({ respond }) => {
const summary = await loadProviderUsageSummary();
respond(true, summary, undefined);
},
- "usage.cost": async ({ respond }) => {
+ "usage.cost": async ({ respond, params }) => {
const config = loadConfig();
- const summary = await loadCostUsageSummary({ days: 30, config });
+ const days = parseDays(params?.days);
+ const summary = await loadCostUsageSummaryCached({ days, config });
respond(true, summary, undefined);
},
};
diff --git a/src/infra/session-cost-usage.ts b/src/infra/session-cost-usage.ts
index 9778f09bb..2b35d637e 100644
--- a/src/infra/session-cost-usage.ts
+++ b/src/infra/session-cost-usage.ts
@@ -184,9 +184,19 @@ export async function loadCostUsageSummary(params?: {
const sessionsDir = resolveSessionTranscriptsDirForAgent(params?.agentId);
const entries = await fs.promises.readdir(sessionsDir, { withFileTypes: true }).catch(() => []);
- const files = entries
- .filter((entry) => entry.isFile() && entry.name.endsWith(".jsonl"))
- .map((entry) => path.join(sessionsDir, entry.name));
+ const files = (
+ await Promise.all(
+ entries
+ .filter((entry) => entry.isFile() && entry.name.endsWith(".jsonl"))
+ .map(async (entry) => {
+ const filePath = path.join(sessionsDir, entry.name);
+ const stats = await fs.promises.stat(filePath).catch(() => null);
+ if (!stats) return null;
+ if (stats.mtimeMs < sinceTime) return null;
+ return filePath;
+ }),
+ )
+ ).filter((filePath): filePath is string => Boolean(filePath));
for (const filePath of files) {
await scanUsageFile({
From 0824bc0236b6105f6d51806a5285fdff98bf6ce4 Mon Sep 17 00:00:00 2001
From: Peter Steinberger
Date: Thu, 22 Jan 2026 08:58:55 +0000
Subject: [PATCH 07/97] test: isolate exec allowlist env
---
.../bash-tools.exec.approval-id.test.ts | 87 +++++++++----------
1 file changed, 39 insertions(+), 48 deletions(-)
diff --git a/src/agents/bash-tools.exec.approval-id.test.ts b/src/agents/bash-tools.exec.approval-id.test.ts
index bd810ab3f..4b42236ad 100644
--- a/src/agents/bash-tools.exec.approval-id.test.ts
+++ b/src/agents/bash-tools.exec.approval-id.test.ts
@@ -80,58 +80,49 @@ describe("exec approvals", () => {
if (process.platform !== "win32") {
await fs.chmod(exePath, 0o755);
}
- const prevPath = process.env.PATH;
- const prevPathExt = process.env.PATHEXT;
- process.env.PATH = binDir;
+ const execEnv: Record = { PATH: binDir };
if (process.platform === "win32") {
- process.env.PATHEXT = ".CMD";
+ execEnv.PATHEXT = ".CMD";
}
-
- try {
- const approvalsFile = {
- version: 1,
- defaults: { security: "allowlist", ask: "on-miss", askFallback: "deny" },
- agents: {
- main: {
- allowlist: [{ pattern: exePath }],
- },
+ const approvalsFile = {
+ version: 1,
+ defaults: { security: "allowlist", ask: "on-miss", askFallback: "deny" },
+ agents: {
+ main: {
+ allowlist: [{ pattern: exePath }],
},
- };
+ },
+ };
- const calls: string[] = [];
- vi.mocked(callGatewayTool).mockImplementation(async (method) => {
- calls.push(method);
- if (method === "exec.approvals.node.get") {
- return { file: approvalsFile };
- }
- if (method === "node.invoke") {
- return { payload: { success: true, stdout: "ok" } };
- }
- if (method === "exec.approval.request") {
- return { decision: "allow-once" };
- }
- return { ok: true };
- });
-
- const { createExecTool } = await import("./bash-tools.exec.js");
- const tool = createExecTool({
- host: "node",
- ask: "on-miss",
- approvalRunningNoticeMs: 0,
- });
-
- const result = await tool.execute("call2", { command: `${exeName} --help` });
- expect(result.details.status).toBe("completed");
- expect(calls).toContain("exec.approvals.node.get");
- expect(calls).toContain("node.invoke");
- expect(calls).not.toContain("exec.approval.request");
- } finally {
- process.env.PATH = prevPath;
- if (prevPathExt === undefined) {
- delete process.env.PATHEXT;
- } else {
- process.env.PATHEXT = prevPathExt;
+ const calls: string[] = [];
+ vi.mocked(callGatewayTool).mockImplementation(async (method) => {
+ calls.push(method);
+ if (method === "exec.approvals.node.get") {
+ return { file: approvalsFile };
}
- }
+ if (method === "node.invoke") {
+ return { payload: { success: true, stdout: "ok" } };
+ }
+ if (method === "exec.approval.request") {
+ return { decision: "allow-once" };
+ }
+ return { ok: true };
+ });
+
+ const { createExecTool } = await import("./bash-tools.exec.js");
+ const tool = createExecTool({
+ host: "node",
+ ask: "on-miss",
+ approvalRunningNoticeMs: 0,
+ });
+
+ const result = await tool.execute("call2", {
+ command: `${exeName} --help`,
+ env: execEnv,
+ });
+ expect(result.details.status).toBe("completed");
+ expect(calls).toContain("exec.approvals.node.get");
+ expect(calls).toContain("node.invoke");
+ expect(calls).not.toContain("exec.approval.request");
});
});
From 646ea6ef0b295489b57a9fb2287a2979208959c2 Mon Sep 17 00:00:00 2001
From: Peter Steinberger
Date: Thu, 22 Jan 2026 09:20:38 +0000
Subject: [PATCH 08/97] test: use absolute exec path for allowlist
---
src/agents/bash-tools.exec.approval-id.test.ts | 7 +------
1 file changed, 1 insertion(+), 6 deletions(-)
diff --git a/src/agents/bash-tools.exec.approval-id.test.ts b/src/agents/bash-tools.exec.approval-id.test.ts
index 4b42236ad..651f1f4d9 100644
--- a/src/agents/bash-tools.exec.approval-id.test.ts
+++ b/src/agents/bash-tools.exec.approval-id.test.ts
@@ -80,10 +80,6 @@ describe("exec approvals", () => {
if (process.platform !== "win32") {
await fs.chmod(exePath, 0o755);
}
- const execEnv: Record = { PATH: binDir };
- if (process.platform === "win32") {
- execEnv.PATHEXT = ".CMD";
- }
const approvalsFile = {
version: 1,
defaults: { security: "allowlist", ask: "on-miss", askFallback: "deny" },
@@ -117,8 +113,7 @@ describe("exec approvals", () => {
});
const result = await tool.execute("call2", {
- command: `${exeName} --help`,
- env: execEnv,
+ command: `"${exePath}" --help`,
});
expect(result.details.status).toBe("completed");
expect(calls).toContain("exec.approvals.node.get");
From 7a283f86a8a9490be75afa1fece59102a9d429b1 Mon Sep 17 00:00:00 2001
From: Peter Steinberger
Date: Thu, 22 Jan 2026 09:32:49 +0000
Subject: [PATCH 09/97] fix: omit skills section in minimal prompt
---
src/agents/system-prompt.ts | 1 +
1 file changed, 1 insertion(+)
diff --git a/src/agents/system-prompt.ts b/src/agents/system-prompt.ts
index 4260c96ab..b952fed4e 100644
--- a/src/agents/system-prompt.ts
+++ b/src/agents/system-prompt.ts
@@ -17,6 +17,7 @@ function buildSkillsSection(params: {
isMinimal: boolean;
readToolName: string;
}) {
+ if (params.isMinimal) return [];
const trimmed = params.skillsPrompt?.trim();
if (!trimmed) return [];
return [
From 486af3f453a13d00b9791559b5563e0ec47b8564 Mon Sep 17 00:00:00 2001
From: Peter Steinberger
Date: Thu, 22 Jan 2026 08:47:31 +0000
Subject: [PATCH 10/97] docs: consolidate 2026.1.21 changelog
---
CHANGELOG.md | 110 ++++++++++++++++++++++-----------------------------
1 file changed, 48 insertions(+), 62 deletions(-)
diff --git a/CHANGELOG.md b/CHANGELOG.md
index b84c324b5..2e836e9e3 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -2,78 +2,64 @@
Docs: https://docs.clawd.bot
-## 2026.1.22
-
-### Changes
-- Highlight: Lobster optional plugin tool for typed workflows + approval gates. https://docs.clawd.bot/tools/lobster
-- Agents: add identity avatar config support and Control UI avatar rendering. (#1329, #1424) Thanks @dlauer.
-- Memory: prevent CLI hangs by deferring vector probes, adding sqlite-vec/embedding timeouts, and showing sync progress early.
-- Docs: add troubleshooting entry for gateway.mode blocking gateway start. https://docs.clawd.bot/gateway/troubleshooting
-- Docs: add /model allowlist troubleshooting note. (#1405)
-- Docs: add per-message Gmail search example for gog. (#1220) Thanks @mbelinky.
-- UI: show per-session assistant identity in the Control UI. (#1420) Thanks @robbyczgw-cla.
-- Onboarding: remove the run setup-token auth option (paste setup-token or reuse CLI creds instead).
-- Signal: add typing indicators and DM read receipts via signal-cli.
-- MSTeams: add file uploads, adaptive cards, and attachment handling improvements. (#1410) Thanks @Evizero.
-- CLI: add `clawdbot update wizard` for interactive channel selection and restart prompts. https://docs.clawd.bot/cli/update
-
-### Breaking
-- **BREAKING:** Envelope and system event timestamps now default to host-local time (was UTC) so agents don’t have to constantly convert.
-
-### Fixes
-- Media: accept MEDIA paths with spaces/tilde and prefer the message tool hint for image replies.
-- Google Antigravity: drop unsigned thinking blocks for Claude models to avoid signature errors.
-- Config: avoid stack traces for invalid configs and log the config path.
-- CLI: read Codex CLI account_id for workspace billing. (#1422) Thanks @aj47.
-- Doctor: avoid recreating WhatsApp config when only legacy routing keys remain. (#900)
-- Doctor: warn when gateway.mode is unset with configure/config guidance.
-- OpenCode Zen: route models to the Zen API shape per family so proxy endpoints are used. (#1416)
-- Browser: suppress Chrome restore prompts for managed profiles. (#1419) Thanks @jamesgroat.
-- Logs: align rolling log filenames with local time and fall back to latest file when today's log is missing. (#1343)
-- Models: inherit session model overrides in thread/topic sessions (Telegram topics, Slack/Discord threads). (#1376)
-- macOS: keep local auto bind loopback-first; only use tailnet when bind=tailnet.
-- macOS: include Textual syntax highlighting resources in packaged app to prevent chat crashes. (#1362)
-- macOS: keep chat pinned to bottom during streaming replies. (#1279)
-- Cron: cap reminder context history to 10 messages and honor `contextMessages`. (#1103) Thanks @mkbehr.
-- Exec approvals: treat main as the default agent + migrate legacy default allowlists. (#1417) Thanks @czekaj.
-- Exec: avoid defaulting to elevated mode when elevated is not allowed.
-- Exec approvals: align node/gateway allowlist prechecks and approval gating; avoid null optional params in approval requests. (#1425) Thanks @czekaj.
-- UI: refresh debug panel on route-driven tab changes. (#1373) Thanks @yazinsai.
-
## 2026.1.21
+### Highlights
+- Lobster optional plugin tool for typed workflows + approval gates. https://docs.clawd.bot/tools/lobster
+- Custom assistant identity + avatars in the Control UI. https://docs.clawd.bot/cli/agents https://docs.clawd.bot/web/control-ui
+- Exec approvals + elevated ask/full modes. https://docs.clawd.bot/tools/exec-approvals https://docs.clawd.bot/tools/elevated
+- Signal typing/read receipts + MSTeams attachments. https://docs.clawd.bot/channels/signal https://docs.clawd.bot/channels/msteams
+- `/models` UX refresh + `clawdbot update wizard`. https://docs.clawd.bot/cli/models https://docs.clawd.bot/cli/update
+
### Changes
-- Heartbeat: allow running heartbeats in an explicit session key. (#1256) Thanks @zknicker.
-- CLI: default exec approvals to the local host, add gateway/node targeting flags, and show target details in allowlist output.
-- CLI: exec approvals mutations render tables instead of raw JSON.
-- Exec approvals: support wildcard agent allowlists (`*`) across all agents.
-- Exec approvals: allowlist matches resolved binary paths only, add safe stdin-only bins, and tighten allowlist shell parsing.
-- Nodes: expose node PATH in status/describe and bootstrap PATH for node-host execution.
-- CLI: flatten node service commands under `clawdbot node` and remove `service node` docs.
-- CLI: move gateway service commands under `clawdbot gateway` and add `gateway probe` for reachability.
-- Sessions: add per-channel reset overrides via `session.resetByChannel`. (#1353) Thanks @cash-echo-bot.
+- Highlight: Lobster optional plugin tool for typed workflows + approval gates. https://docs.clawd.bot/tools/lobster (#1152) Thanks @vignesh07.
+- Agents/UI: add identity avatar config support and Control UI avatar rendering. (#1329, #1424) Thanks @dlauer. https://docs.clawd.bot/gateway/configuration https://docs.clawd.bot/cli/agents
+- Control UI: add custom assistant identity support and per-session identity display. (#1420) Thanks @robbyczgw-cla. https://docs.clawd.bot/web/control-ui
+- CLI: add `clawdbot update wizard` with interactive channel selection + restart prompts, plus preflight checks before rebasing. https://docs.clawd.bot/cli/update
+- Models/Commands: add `/models`, improve `/model` listing UX, and expand `clawdbot models` paging. (#1398) Thanks @vignesh07. https://docs.clawd.bot/cli/models
+- CLI: move gateway service commands under `clawdbot gateway`, flatten node service commands under `clawdbot node`, and add `gateway probe` for reachability. https://docs.clawd.bot/cli/gateway https://docs.clawd.bot/cli/node
+- Exec: add elevated ask/full modes, tighten allowlist gating, and render approvals tables on write. https://docs.clawd.bot/tools/elevated https://docs.clawd.bot/tools/exec-approvals
+- Exec approvals: default to local host, add gateway/node targeting + target details, support wildcard agent allowlists, and tighten allowlist parsing/safe bins. https://docs.clawd.bot/cli/approvals https://docs.clawd.bot/tools/exec-approvals
+- Heartbeat: allow explicit session keys and active hours. (#1256) Thanks @zknicker. https://docs.clawd.bot/gateway/heartbeat
+- Sessions: add per-channel idle durations via `sessions.channelIdleMinutes`. (#1353) Thanks @cash-echo-bot.
+- Nodes: run exec-style, expose PATH in status/describe, and bootstrap PATH for node-host execution. https://docs.clawd.bot/cli/node
+- Cache: add `cache.ttlPrune` mode and auth-aware defaults for cache TTL behavior.
+- Queue: add per-channel debounce overrides for auto-reply. https://docs.clawd.bot/concepts/queue
+- Discord: add wildcard channel config support. (#1334) Thanks @pvoo. https://docs.clawd.bot/channels/discord
+- Signal: add typing indicators and DM read receipts via signal-cli. https://docs.clawd.bot/channels/signal
+- MSTeams: add file uploads, adaptive cards, and attachment handling improvements. (#1410) Thanks @Evizero. https://docs.clawd.bot/channels/msteams
+- Onboarding: remove the run setup-token auth option (paste setup-token or reuse CLI creds instead).
+- macOS: refresh Settings (location access in Permissions, connection mode in menu, remove CLI install UI).
+- Diagnostics: add cache trace config for debugging. (#1370) Thanks @parubets.
+- Docs: Lobster guides + org URL updates, /model allowlist troubleshooting, Gmail message search examples, gateway.mode troubleshooting, prompt injection guidance, npm prefix/node CLI notes, control UI dev gatewayUrl note, tool_use FAQ, showcase video, and sharp/node-gyp workaround. (#1427, #1220, #1405) Thanks @vignesh07, @mbelinky.
### Breaking
- **BREAKING:** Control UI now rejects insecure HTTP without device identity by default. Use HTTPS (Tailscale Serve) or set `gateway.controlUi.allowInsecureAuth: true` to allow token-only auth. https://docs.clawd.bot/web/control-ui#insecure-http
+- **BREAKING:** Envelope and system event timestamps now default to host-local time (was UTC) so agents don’t have to constantly convert.
### Fixes
+- Streaming/Typing/Media: keep reply tags across streamed chunks, start typing indicators at run start, and accept MEDIA paths with spaces/tilde while preferring the message tool hint for image replies.
+- Agents/Providers: drop unsigned thinking blocks for Claude models (Google Antigravity) and enforce alphanumeric tool call ids for strict providers (Mistral/OpenRouter). (#1372) Thanks @zerone0x.
+- Exec approvals: treat main as the default agent, align node/gateway allowlist prechecks, validate resolved paths, avoid allowlist resolve races, and avoid null optional params. (#1417, #1414, #1425) Thanks @czekaj.
+- Exec/Windows: resolve Windows exec paths with extensions and handle safe-bin exe names.
- Nodes/macOS: prompt on allowlist miss for node exec approvals, persist allowlist decisions, and flatten node invoke errors. (#1394) Thanks @ngutman.
-- Gateway: keep auto bind loopback-first and add explicit tailnet binding to avoid Tailscale taking over local UI. (#1380)
-- Agents: enforce 9-char alphanumeric tool call ids for Mistral providers. (#1372) Thanks @zerone0x.
+- Gateway: prevent multiple gateways from sharing the same config/state (singleton lock), keep auto bind loopback-first with explicit tailnet binding, and improve SSH auth handling. (#1380)
+- Control UI: remove the chat stop button, keep the composer aligned to the bottom edge, stabilize session previews, and refresh the debug panel on route-driven tab changes. (#1373) Thanks @yazinsai.
+- UI/config: export `SECTION_META` for config form modules. (#1418) Thanks @MaudeBot.
+- macOS: keep chat pinned during streaming replies, include Textual resources, respect wildcard exec approvals, allow SSH agent auth, and default distribution builds to universal binaries. (#1279, #1362, #1384, #1396) Thanks @ameno-, @JustYannicc.
+- BlueBubbles: resolve short message IDs safely, expose full IDs in templates, and harden short-id fetch wrappers. (#1369, #1387) Thanks @tyler6204.
+- Models/Configure: inherit session model overrides in threads/topics, map OpenCode Zen models to the correct APIs, narrow Anthropic OAuth allowlist handling, seed allowlist fallbacks, list the full catalog when no allowlist is set, and limit `/model` list output. (#1376, #1416)
+- Memory: prevent CLI hangs by deferring vector probes, add sqlite-vec/embedding timeouts, and make session memory indexing async.
+- Cron: cap reminder context history to 10 messages and honor `contextMessages`. (#1103) Thanks @mkbehr.
+- Cache: restore the 1h cache TTL option and reset the pruning window.
+- Zalo Personal: tolerate ANSI/log-prefixed JSON output from `zca`. (#1379) Thanks @ptn1411.
+- Browser: suppress Chrome restore prompts for managed profiles. (#1419) Thanks @jamesgroat.
+- Infra: preserve fetch helper methods/preconnect when wrapping abort signals and normalize Telegram fetch aborts.
+- Config/Doctor: avoid stack traces for invalid configs, log the config path, avoid WhatsApp config resurrection, and warn when `gateway.mode` is unset. (#900)
+- CLI: read Codex CLI account_id for workspace billing. (#1422) Thanks @aj47.
+- Logs/Status: align rolling log filenames with local time and report sandboxed runtime in `clawdbot status`. (#1343)
- Embedded runner: persist injected history images so attachments aren’t reloaded each turn. (#1374) Thanks @Nicell.
-- Nodes tool: include agent/node/gateway context in tool failure logs to speed approval debugging.
-- macOS: exec approvals now respect wildcard agent allowlists (`*`).
-- macOS: allow SSH agent auth when no identity file is set. (#1384) Thanks @ameno-.
-- Gateway: prevent multiple gateways from sharing the same config/state at once (singleton lock).
-- UI: remove the chat stop button and keep the composer aligned to the bottom edge.
-- Typing: start instant typing indicators at run start so DMs and mentions show immediately.
-- Configure: restrict the model allowlist picker to OAuth-compatible Anthropic models and preselect Opus 4.5.
-- Configure: seed model fallbacks from the allowlist selection when multiple models are chosen.
-- Model picker: list the full catalog when no model allowlist is configured.
-- Discord: honor wildcard channel configs via shared match helpers. (#1334) Thanks @pvoo.
-- BlueBubbles: resolve short message IDs safely and expose full IDs in templates. (#1387) Thanks @tyler6204.
-- Infra: preserve fetch helper methods when wrapping abort signals. (#1387)
-- macOS: default distribution packaging to universal binaries. (#1396) Thanks @JustYannicc.
+- Nodes/Subagents: include agent/node/gateway context in tool failure logs and ensure subagent list uses the command session.
## 2026.1.20
From 9f588d91f42cb9d0fd58edd54f2285d61afcad87 Mon Sep 17 00:00:00 2001
From: Peter Steinberger
Date: Thu, 22 Jan 2026 09:35:03 +0000
Subject: [PATCH 11/97] docs: add cache optimization highlight
---
CHANGELOG.md | 1 +
1 file changed, 1 insertion(+)
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 2e836e9e3..87356dd15 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -7,6 +7,7 @@ Docs: https://docs.clawd.bot
### Highlights
- Lobster optional plugin tool for typed workflows + approval gates. https://docs.clawd.bot/tools/lobster
- Custom assistant identity + avatars in the Control UI. https://docs.clawd.bot/cli/agents https://docs.clawd.bot/web/control-ui
+- Cache optimizations: cache-ttl pruning + defaults reduce token spend on cold requests. https://docs.clawd.bot/concepts/session-pruning
- Exec approvals + elevated ask/full modes. https://docs.clawd.bot/tools/exec-approvals https://docs.clawd.bot/tools/elevated
- Signal typing/read receipts + MSTeams attachments. https://docs.clawd.bot/channels/signal https://docs.clawd.bot/channels/msteams
- `/models` UX refresh + `clawdbot update wizard`. https://docs.clawd.bot/cli/models https://docs.clawd.bot/cli/update
From 6822d509d7f10c00ae3d3c539481e6493d25780d Mon Sep 17 00:00:00 2001
From: Peter Steinberger
Date: Thu, 22 Jan 2026 09:38:43 +0000
Subject: [PATCH 12/97] docs: explain unpinning model auth profiles
---
docs/start/faq.md | 11 +++++++++++
1 file changed, 11 insertions(+)
diff --git a/docs/start/faq.md b/docs/start/faq.md
index 915890c99..286662cb6 100644
--- a/docs/start/faq.md
+++ b/docs/start/faq.md
@@ -1067,6 +1067,17 @@ You can also force a specific auth profile for the provider (per session):
Tip: `/model status` shows which agent is active, which `auth-profiles.json` file is being used, and which auth profile will be tried next.
It also shows the configured provider endpoint (`baseUrl`) and API mode (`api`) when available.
+### How do I unpin a profile I set with `@profile`?
+
+Re-run `/model` **without** the `@profile` suffix:
+
+```
+/model anthropic/claude-opus-4-5
+```
+
+If you want to return to the default, pick it from `/model` (or send `/model `).
+Use `/model status` to confirm which auth profile is active.
+
### Why do I see “Model … is not allowed” and then no reply?
If `agents.defaults.models` is set, it becomes the **allowlist** for `/model` and any
From ced9efd964d64db17e1bef7bb5dd0620fb719062 Mon Sep 17 00:00:00 2001
From: Peter Steinberger
Date: Thu, 22 Jan 2026 09:53:36 +0000
Subject: [PATCH 13/97] fix: avoid duplicate exec approval prompts
---
.../ExecApprovalsGatewayPrompter.swift | 33 +++++++++++++++++++
.../Clawdbot/ExecApprovalsSocket.swift | 4 ++-
.../Clawdbot/NodeMode/MacNodeRuntime.swift | 3 +-
3 files changed, 38 insertions(+), 2 deletions(-)
diff --git a/apps/macos/Sources/Clawdbot/ExecApprovalsGatewayPrompter.swift b/apps/macos/Sources/Clawdbot/ExecApprovalsGatewayPrompter.swift
index 4cd79d5f6..4b8389625 100644
--- a/apps/macos/Sources/Clawdbot/ExecApprovalsGatewayPrompter.swift
+++ b/apps/macos/Sources/Clawdbot/ExecApprovalsGatewayPrompter.swift
@@ -1,5 +1,6 @@
import ClawdbotKit
import ClawdbotProtocol
+import CoreGraphics
import Foundation
import OSLog
@@ -44,6 +45,7 @@ final class ExecApprovalsGatewayPrompter {
do {
let data = try JSONEncoder().encode(payload)
let request = try JSONDecoder().decode(GatewayApprovalRequest.self, from: data)
+ guard self.shouldPresent(request: request) else { return }
let decision = ExecApprovalsPromptPresenter.prompt(request.request)
try await GatewayConnection.shared.requestVoid(
method: .execApprovalResolve,
@@ -56,4 +58,35 @@ final class ExecApprovalsGatewayPrompter {
self.logger.error("exec approval handling failed \(error.localizedDescription, privacy: .public)")
}
}
+
+ private func shouldPresent(request: GatewayApprovalRequest) -> Bool {
+ let mode = AppStateStore.shared.connectionMode
+ let activeSession = WebChatManager.shared.activeSessionKey?.trimmingCharacters(in: .whitespacesAndNewlines)
+ let requestSession = request.request.sessionKey?.trimmingCharacters(in: .whitespacesAndNewlines)
+ let recentlyActive = self.isRecentlyActive(mode: mode, thresholdSeconds: 120)
+
+ if let session = requestSession, !session.isEmpty {
+ if let active = activeSession, !active.isEmpty {
+ return active == session
+ }
+ return recentlyActive
+ }
+
+ if let active = activeSession, !active.isEmpty {
+ return true
+ }
+ return mode == .local
+ }
+
+ private func isRecentlyActive(mode: AppState.ConnectionMode, thresholdSeconds: Int) -> Bool {
+ guard let seconds = Self.lastInputSeconds() else { return mode == .local }
+ return seconds <= thresholdSeconds
+ }
+
+ private static func lastInputSeconds() -> Int? {
+ let anyEvent = CGEventType(rawValue: UInt32.max) ?? .null
+ let seconds = CGEventSource.secondsSinceLastEventType(.combinedSessionState, eventType: anyEvent)
+ if seconds.isNaN || seconds.isInfinite || seconds < 0 { return nil }
+ return Int(seconds.rounded())
+ }
}
diff --git a/apps/macos/Sources/Clawdbot/ExecApprovalsSocket.swift b/apps/macos/Sources/Clawdbot/ExecApprovalsSocket.swift
index b5b74bec8..68f8e906d 100644
--- a/apps/macos/Sources/Clawdbot/ExecApprovalsSocket.swift
+++ b/apps/macos/Sources/Clawdbot/ExecApprovalsSocket.swift
@@ -13,6 +13,7 @@ struct ExecApprovalPromptRequest: Codable, Sendable {
var ask: String?
var agentId: String?
var resolvedPath: String?
+ var sessionKey: String?
}
private struct ExecApprovalSocketRequest: Codable {
@@ -412,7 +413,8 @@ private enum ExecHostExecutor {
security: context.security.rawValue,
ask: context.ask.rawValue,
agentId: context.trimmedAgent,
- resolvedPath: context.resolution?.resolvedPath))
+ resolvedPath: context.resolution?.resolvedPath,
+ sessionKey: request.sessionKey))
switch decision {
case .deny:
diff --git a/apps/macos/Sources/Clawdbot/NodeMode/MacNodeRuntime.swift b/apps/macos/Sources/Clawdbot/NodeMode/MacNodeRuntime.swift
index 184164262..c3eacb8a1 100644
--- a/apps/macos/Sources/Clawdbot/NodeMode/MacNodeRuntime.swift
+++ b/apps/macos/Sources/Clawdbot/NodeMode/MacNodeRuntime.swift
@@ -679,7 +679,8 @@ actor MacNodeRuntime {
security: context.security.rawValue,
ask: context.ask.rawValue,
agentId: context.agentId,
- resolvedPath: context.resolution?.resolvedPath))
+ resolvedPath: context.resolution?.resolvedPath,
+ sessionKey: context.sessionKey))
}
switch decision {
case .deny:
From 0873351401bb9406dc621a69bd78206da62da211 Mon Sep 17 00:00:00 2001
From: Robby
Date: Thu, 22 Jan 2026 09:58:07 +0000
Subject: [PATCH 14/97] fix: update token count display after compaction
(#1299)
---
src/agents/pi-embedded-runner/compact.ts | 23 ++++++++++++++++++++-
src/agents/pi-embedded-runner/types.ts | 1 +
src/auto-reply/reply/commands-compact.ts | 26 +++++++++++++++---------
src/auto-reply/reply/session-updates.ts | 22 +++++++++++++++-----
4 files changed, 56 insertions(+), 16 deletions(-)
diff --git a/src/agents/pi-embedded-runner/compact.ts b/src/agents/pi-embedded-runner/compact.ts
index 2aad2431a..53b0ae8c0 100644
--- a/src/agents/pi-embedded-runner/compact.ts
+++ b/src/agents/pi-embedded-runner/compact.ts
@@ -1,7 +1,12 @@
import fs from "node:fs/promises";
import os from "node:os";
-import { createAgentSession, SessionManager, SettingsManager } from "@mariozechner/pi-coding-agent";
+import {
+ createAgentSession,
+ estimateTokens,
+ SessionManager,
+ SettingsManager,
+} from "@mariozechner/pi-coding-agent";
import { resolveHeartbeatPrompt } from "../../auto-reply/heartbeat.js";
import type { ReasoningLevel, ThinkLevel } from "../../auto-reply/thinking.js";
@@ -370,6 +375,21 @@ export async function compactEmbeddedPiSession(params: {
session.agent.replaceMessages(limited);
}
const result = await session.compact(params.customInstructions);
+ // Estimate tokens after compaction by summing token estimates for remaining messages
+ let tokensAfter: number | undefined;
+ try {
+ tokensAfter = 0;
+ for (const message of session.messages) {
+ tokensAfter += estimateTokens(message);
+ }
+ // Sanity check: tokensAfter should be less than tokensBefore
+ if (tokensAfter > result.tokensBefore) {
+ tokensAfter = undefined; // Don't trust the estimate
+ }
+ } catch {
+ // If estimation fails, leave tokensAfter undefined
+ tokensAfter = undefined;
+ }
return {
ok: true,
compacted: true,
@@ -377,6 +397,7 @@ export async function compactEmbeddedPiSession(params: {
summary: result.summary,
firstKeptEntryId: result.firstKeptEntryId,
tokensBefore: result.tokensBefore,
+ tokensAfter,
details: result.details,
},
};
diff --git a/src/agents/pi-embedded-runner/types.ts b/src/agents/pi-embedded-runner/types.ts
index 56380cd1d..6a1ee1128 100644
--- a/src/agents/pi-embedded-runner/types.ts
+++ b/src/agents/pi-embedded-runner/types.ts
@@ -59,6 +59,7 @@ export type EmbeddedPiCompactResult = {
summary: string;
firstKeptEntryId: string;
tokensBefore: number;
+ tokensAfter?: number;
details?: unknown;
};
};
diff --git a/src/auto-reply/reply/commands-compact.ts b/src/auto-reply/reply/commands-compact.ts
index da048ec65..3fd47172f 100644
--- a/src/auto-reply/reply/commands-compact.ts
+++ b/src/auto-reply/reply/commands-compact.ts
@@ -83,18 +83,13 @@ export const handleCompactCommand: CommandHandler = async (params) => {
ownerNumbers: params.command.ownerList.length > 0 ? params.command.ownerList : undefined,
});
- const totalTokens =
- params.sessionEntry.totalTokens ??
- (params.sessionEntry.inputTokens ?? 0) + (params.sessionEntry.outputTokens ?? 0);
- const contextSummary = formatContextUsageShort(
- totalTokens > 0 ? totalTokens : null,
- params.contextTokens ?? params.sessionEntry.contextTokens ?? null,
- );
const compactLabel = result.ok
? result.compacted
- ? result.result?.tokensBefore
- ? `Compacted (${formatTokenCount(result.result.tokensBefore)} before)`
- : "Compacted"
+ ? result.result?.tokensBefore != null && result.result?.tokensAfter != null
+ ? `Compacted (${formatTokenCount(result.result.tokensBefore)} → ${formatTokenCount(result.result.tokensAfter)})`
+ : result.result?.tokensBefore
+ ? `Compacted (${formatTokenCount(result.result.tokensBefore)} before)`
+ : "Compacted"
: "Compaction skipped"
: "Compaction failed";
if (result.ok && result.compacted) {
@@ -103,8 +98,19 @@ export const handleCompactCommand: CommandHandler = async (params) => {
sessionStore: params.sessionStore,
sessionKey: params.sessionKey,
storePath: params.storePath,
+ // Update token counts after compaction
+ tokensAfter: result.result?.tokensAfter,
});
}
+ // Use the post-compaction token count for context summary if available
+ const tokensAfterCompaction = result.result?.tokensAfter;
+ const totalTokens = tokensAfterCompaction ??
+ params.sessionEntry.totalTokens ??
+ (params.sessionEntry.inputTokens ?? 0) + (params.sessionEntry.outputTokens ?? 0);
+ const contextSummary = formatContextUsageShort(
+ totalTokens > 0 ? totalTokens : null,
+ params.contextTokens ?? params.sessionEntry.contextTokens ?? null,
+ );
const reason = result.reason?.trim();
const line = reason
? `${compactLabel}: ${reason} • ${contextSummary}`
diff --git a/src/auto-reply/reply/session-updates.ts b/src/auto-reply/reply/session-updates.ts
index e5ad81d8e..acdadc39c 100644
--- a/src/auto-reply/reply/session-updates.ts
+++ b/src/auto-reply/reply/session-updates.ts
@@ -237,23 +237,35 @@ export async function incrementCompactionCount(params: {
sessionKey?: string;
storePath?: string;
now?: number;
+ /** Token count after compaction - if provided, updates session token counts */
+ tokensAfter?: number;
}): Promise {
- const { sessionEntry, sessionStore, sessionKey, storePath, now = Date.now() } = params;
+ const { sessionEntry, sessionStore, sessionKey, storePath, now = Date.now(), tokensAfter } = params;
if (!sessionStore || !sessionKey) return undefined;
const entry = sessionStore[sessionKey] ?? sessionEntry;
if (!entry) return undefined;
const nextCount = (entry.compactionCount ?? 0) + 1;
- sessionStore[sessionKey] = {
- ...entry,
+ // Build update payload with compaction count and optionally updated token counts
+ const updates: Partial = {
compactionCount: nextCount,
updatedAt: now,
};
+ // If tokensAfter is provided, update the cached token counts to reflect post-compaction state
+ if (tokensAfter != null && tokensAfter > 0) {
+ updates.totalTokens = tokensAfter;
+ // Clear input/output breakdown since we only have the total estimate after compaction
+ updates.inputTokens = undefined;
+ updates.outputTokens = undefined;
+ }
+ sessionStore[sessionKey] = {
+ ...entry,
+ ...updates,
+ };
if (storePath) {
await updateSessionStore(storePath, (store) => {
store[sessionKey] = {
...store[sessionKey],
- compactionCount: nextCount,
- updatedAt: now,
+ ...updates,
};
});
}
From e389bd478bb4325a36ace2c539d53af438abbfd7 Mon Sep 17 00:00:00 2001
From: Peter Steinberger
Date: Thu, 22 Jan 2026 09:58:15 +0000
Subject: [PATCH 15/97] fix: keep backslashes in quoted exec paths
---
src/infra/exec-approvals.ts | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/src/infra/exec-approvals.ts b/src/infra/exec-approvals.ts
index af0142ebc..5c9e4a826 100644
--- a/src/infra/exec-approvals.ts
+++ b/src/infra/exec-approvals.ts
@@ -519,7 +519,7 @@ function splitShellPipeline(command: string): { ok: boolean; reason?: string; se
escaped = false;
continue;
}
- if (!inSingle && ch === "\\") {
+ if (!inSingle && !inDouble && ch === "\\") {
escaped = true;
buf += ch;
continue;
@@ -595,7 +595,7 @@ function tokenizeShellSegment(segment: string): string[] | null {
escaped = false;
continue;
}
- if (!inSingle && ch === "\\") {
+ if (!inSingle && !inDouble && ch === "\\") {
escaped = true;
continue;
}
From 72455b902fdf7a8f48a83b06f847e70621c15019 Mon Sep 17 00:00:00 2001
From: Peter Steinberger
Date: Thu, 22 Jan 2026 10:00:55 +0000
Subject: [PATCH 16/97] test: cover exec approval prompt gating
---
.../Sources/Clawdbot/ExecApprovals.swift | 12 ++--
.../ExecApprovalsGatewayPrompter.swift | 49 +++++++++++++---
.../Clawdbot/GatewayEndpointStore.swift | 8 +--
.../Clawdbot/MenuSessionsInjector.swift | 3 +-
.../ExecApprovalsGatewayPrompterTests.swift | 56 +++++++++++++++++++
.../GatewayProcessManagerTests.swift | 1 +
6 files changed, 111 insertions(+), 18 deletions(-)
create mode 100644 apps/macos/Tests/ClawdbotIPCTests/ExecApprovalsGatewayPrompterTests.swift
diff --git a/apps/macos/Sources/Clawdbot/ExecApprovals.swift b/apps/macos/Sources/Clawdbot/ExecApprovals.swift
index 53e0b10a8..537ceeaad 100644
--- a/apps/macos/Sources/Clawdbot/ExecApprovals.swift
+++ b/apps/macos/Sources/Clawdbot/ExecApprovals.swift
@@ -475,8 +475,8 @@ enum ExecApprovalsStore {
private static func mergeAgents(
current: ExecApprovalsAgent,
- legacy: ExecApprovalsAgent
- ) -> ExecApprovalsAgent {
+ legacy: ExecApprovalsAgent) -> ExecApprovalsAgent
+ {
var seen = Set()
var allowlist: [ExecAllowlistEntry] = []
func append(_ entry: ExecAllowlistEntry) {
@@ -486,8 +486,12 @@ enum ExecApprovalsStore {
seen.insert(key)
allowlist.append(entry)
}
- for entry in current.allowlist ?? [] { append(entry) }
- for entry in legacy.allowlist ?? [] { append(entry) }
+ for entry in current.allowlist ?? [] {
+ append(entry)
+ }
+ for entry in legacy.allowlist ?? [] {
+ append(entry)
+ }
return ExecApprovalsAgent(
security: current.security ?? legacy.security,
diff --git a/apps/macos/Sources/Clawdbot/ExecApprovalsGatewayPrompter.swift b/apps/macos/Sources/Clawdbot/ExecApprovalsGatewayPrompter.swift
index 4b8389625..dbe80ecfe 100644
--- a/apps/macos/Sources/Clawdbot/ExecApprovalsGatewayPrompter.swift
+++ b/apps/macos/Sources/Clawdbot/ExecApprovalsGatewayPrompter.swift
@@ -63,26 +63,38 @@ final class ExecApprovalsGatewayPrompter {
let mode = AppStateStore.shared.connectionMode
let activeSession = WebChatManager.shared.activeSessionKey?.trimmingCharacters(in: .whitespacesAndNewlines)
let requestSession = request.request.sessionKey?.trimmingCharacters(in: .whitespacesAndNewlines)
- let recentlyActive = self.isRecentlyActive(mode: mode, thresholdSeconds: 120)
+ return Self.shouldPresent(
+ mode: mode,
+ activeSession: activeSession,
+ requestSession: requestSession,
+ lastInputSeconds: Self.lastInputSeconds(),
+ thresholdSeconds: 120)
+ }
- if let session = requestSession, !session.isEmpty {
- if let active = activeSession, !active.isEmpty {
+ private static func shouldPresent(
+ mode: AppState.ConnectionMode,
+ activeSession: String?,
+ requestSession: String?,
+ lastInputSeconds: Int?,
+ thresholdSeconds: Int) -> Bool
+ {
+ let active = activeSession?.trimmingCharacters(in: .whitespacesAndNewlines)
+ let requested = requestSession?.trimmingCharacters(in: .whitespacesAndNewlines)
+ let recentlyActive = lastInputSeconds.map { $0 <= thresholdSeconds } ?? (mode == .local)
+
+ if let session = requested, !session.isEmpty {
+ if let active, !active.isEmpty {
return active == session
}
return recentlyActive
}
- if let active = activeSession, !active.isEmpty {
+ if let active, !active.isEmpty {
return true
}
return mode == .local
}
- private func isRecentlyActive(mode: AppState.ConnectionMode, thresholdSeconds: Int) -> Bool {
- guard let seconds = Self.lastInputSeconds() else { return mode == .local }
- return seconds <= thresholdSeconds
- }
-
private static func lastInputSeconds() -> Int? {
let anyEvent = CGEventType(rawValue: UInt32.max) ?? .null
let seconds = CGEventSource.secondsSinceLastEventType(.combinedSessionState, eventType: anyEvent)
@@ -90,3 +102,22 @@ final class ExecApprovalsGatewayPrompter {
return Int(seconds.rounded())
}
}
+
+#if DEBUG
+extension ExecApprovalsGatewayPrompter {
+ static func _testShouldPresent(
+ mode: AppState.ConnectionMode,
+ activeSession: String?,
+ requestSession: String?,
+ lastInputSeconds: Int?,
+ thresholdSeconds: Int = 120) -> Bool
+ {
+ self.shouldPresent(
+ mode: mode,
+ activeSession: activeSession,
+ requestSession: requestSession,
+ lastInputSeconds: lastInputSeconds,
+ thresholdSeconds: thresholdSeconds)
+ }
+}
+#endif
diff --git a/apps/macos/Sources/Clawdbot/GatewayEndpointStore.swift b/apps/macos/Sources/Clawdbot/GatewayEndpointStore.swift
index 418d0b810..633b7d872 100644
--- a/apps/macos/Sources/Clawdbot/GatewayEndpointStore.swift
+++ b/apps/macos/Sources/Clawdbot/GatewayEndpointStore.swift
@@ -560,13 +560,13 @@ actor GatewayEndpointStore {
{
switch bindMode {
case "tailnet":
- return tailscaleIP ?? "127.0.0.1"
+ tailscaleIP ?? "127.0.0.1"
case "auto":
- return "127.0.0.1"
+ "127.0.0.1"
case "custom":
- return customBindHost ?? "127.0.0.1"
+ customBindHost ?? "127.0.0.1"
default:
- return "127.0.0.1"
+ "127.0.0.1"
}
}
}
diff --git a/apps/macos/Sources/Clawdbot/MenuSessionsInjector.swift b/apps/macos/Sources/Clawdbot/MenuSessionsInjector.swift
index 6aab7910e..b39138277 100644
--- a/apps/macos/Sources/Clawdbot/MenuSessionsInjector.swift
+++ b/apps/macos/Sources/Clawdbot/MenuSessionsInjector.swift
@@ -192,7 +192,8 @@ extension MenuSessionsInjector {
let headerItem = NSMenuItem()
headerItem.tag = self.tag
headerItem.isEnabled = false
- let statusText = self.cachedErrorText ?? (isConnected ? nil : self.controlChannelStatusText(for: channelState))
+ let statusText = self
+ .cachedErrorText ?? (isConnected ? nil : self.controlChannelStatusText(for: channelState))
let hosted = self.makeHostedView(
rootView: AnyView(MenuSessionsHeaderView(
count: rows.count,
diff --git a/apps/macos/Tests/ClawdbotIPCTests/ExecApprovalsGatewayPrompterTests.swift b/apps/macos/Tests/ClawdbotIPCTests/ExecApprovalsGatewayPrompterTests.swift
new file mode 100644
index 000000000..88fb02f2b
--- /dev/null
+++ b/apps/macos/Tests/ClawdbotIPCTests/ExecApprovalsGatewayPrompterTests.swift
@@ -0,0 +1,56 @@
+import Testing
+@testable import Clawdbot
+
+@Suite
+@MainActor
+struct ExecApprovalsGatewayPrompterTests {
+ @Test func sessionMatchPrefersActiveSession() {
+ let matches = ExecApprovalsGatewayPrompter._testShouldPresent(
+ mode: .remote,
+ activeSession: " main ",
+ requestSession: "main",
+ lastInputSeconds: nil)
+ #expect(matches)
+
+ let mismatched = ExecApprovalsGatewayPrompter._testShouldPresent(
+ mode: .remote,
+ activeSession: "other",
+ requestSession: "main",
+ lastInputSeconds: 0)
+ #expect(!mismatched)
+ }
+
+ @Test func sessionFallbackUsesRecentActivity() {
+ let recent = ExecApprovalsGatewayPrompter._testShouldPresent(
+ mode: .remote,
+ activeSession: nil,
+ requestSession: "main",
+ lastInputSeconds: 10,
+ thresholdSeconds: 120)
+ #expect(recent)
+
+ let stale = ExecApprovalsGatewayPrompter._testShouldPresent(
+ mode: .remote,
+ activeSession: nil,
+ requestSession: "main",
+ lastInputSeconds: 200,
+ thresholdSeconds: 120)
+ #expect(!stale)
+ }
+
+ @Test func defaultBehaviorMatchesMode() {
+ let local = ExecApprovalsGatewayPrompter._testShouldPresent(
+ mode: .local,
+ activeSession: nil,
+ requestSession: nil,
+ lastInputSeconds: 400)
+ #expect(local)
+
+ let remote = ExecApprovalsGatewayPrompter._testShouldPresent(
+ mode: .remote,
+ activeSession: nil,
+ requestSession: nil,
+ lastInputSeconds: 400)
+ #expect(!remote)
+ }
+}
diff --git a/apps/macos/Tests/ClawdbotIPCTests/GatewayProcessManagerTests.swift b/apps/macos/Tests/ClawdbotIPCTests/GatewayProcessManagerTests.swift
index 18e529389..05c96f8be 100644
--- a/apps/macos/Tests/ClawdbotIPCTests/GatewayProcessManagerTests.swift
+++ b/apps/macos/Tests/ClawdbotIPCTests/GatewayProcessManagerTests.swift
@@ -1,3 +1,4 @@
+import ClawdbotKit
import Foundation
import os
import Testing
From cadaf2c835f908b4227df98ddbf5e6775b0f99ba Mon Sep 17 00:00:00 2001
From: Peter Steinberger
Date: Thu, 22 Jan 2026 10:21:50 +0000
Subject: [PATCH 17/97] feat: add sessions preview rpc and menu prewarm
---
.../Sources/Clawdbot/GatewayConnection.swift | 25 ++
.../Clawdbot/MenuSessionsInjector.swift | 7 +
.../Clawdbot/SessionMenuPreviewView.swift | 213 ++++++++++++++----
.../SessionMenuPreviewTests.swift | 20 +-
.../Sources/ClawdbotChatUI/ChatModels.swift | 16 ++
src/gateway/protocol/index.ts | 7 +
.../protocol/schema/protocol-schemas.ts | 2 +
src/gateway/protocol/schema/sessions.ts | 9 +
src/gateway/protocol/schema/types.ts | 2 +
src/gateway/server-methods-list.ts | 1 +
src/gateway/server-methods.ts | 1 +
src/gateway/server-methods/sessions.ts | 73 ++++++
...sessions.gateway-server-sessions-a.test.ts | 48 ++++
src/gateway/session-utils.fs.test.ts | 63 ++++++
src/gateway/session-utils.fs.ts | 201 +++++++++++++++++
src/gateway/session-utils.ts | 3 +
src/gateway/session-utils.types.ts | 16 ++
17 files changed, 650 insertions(+), 57 deletions(-)
diff --git a/apps/macos/Sources/Clawdbot/GatewayConnection.swift b/apps/macos/Sources/Clawdbot/GatewayConnection.swift
index 4a0234748..9feb98ba9 100644
--- a/apps/macos/Sources/Clawdbot/GatewayConnection.swift
+++ b/apps/macos/Sources/Clawdbot/GatewayConnection.swift
@@ -69,6 +69,7 @@ actor GatewayConnection {
case channelsLogout = "channels.logout"
case modelsList = "models.list"
case chatHistory = "chat.history"
+ case sessionsPreview = "sessions.preview"
case chatSend = "chat.send"
case chatAbort = "chat.abort"
case skillsStatus = "skills.status"
@@ -540,6 +541,30 @@ extension GatewayConnection {
return try await self.requestDecoded(method: .skillsUpdate, params: params)
}
+ // MARK: - Sessions
+
+ func sessionsPreview(
+ keys: [String],
+ limit: Int? = nil,
+ maxChars: Int? = nil,
+ timeoutMs: Int? = nil) async throws -> ClawdbotSessionsPreviewPayload
+ {
+ let resolvedKeys = keys
+ .map { self.canonicalizeSessionKey($0) }
+ .filter { !$0.isEmpty }
+ if resolvedKeys.isEmpty {
+ return ClawdbotSessionsPreviewPayload(ts: 0, previews: [])
+ }
+ var params: [String: AnyCodable] = ["keys": AnyCodable(resolvedKeys)]
+ if let limit { params["limit"] = AnyCodable(limit) }
+ if let maxChars { params["maxChars"] = AnyCodable(maxChars) }
+ let timeout = timeoutMs.map { Double($0) }
+ return try await self.requestDecoded(
+ method: .sessionsPreview,
+ params: params,
+ timeoutMs: timeout)
+ }
+
// MARK: - Chat
func chatHistory(
diff --git a/apps/macos/Sources/Clawdbot/MenuSessionsInjector.swift b/apps/macos/Sources/Clawdbot/MenuSessionsInjector.swift
index b39138277..4b8854cda 100644
--- a/apps/macos/Sources/Clawdbot/MenuSessionsInjector.swift
+++ b/apps/macos/Sources/Clawdbot/MenuSessionsInjector.swift
@@ -188,6 +188,13 @@ extension MenuSessionsInjector {
if rhs.key == mainKey { return false }
return (lhs.updatedAt ?? .distantPast) > (rhs.updatedAt ?? .distantPast)
}
+ if !rows.isEmpty {
+ let previewKeys = rows.prefix(20).map(\.key)
+ let task = Task {
+ await SessionMenuPreviewLoader.prewarm(sessionKeys: previewKeys, maxItems: 10)
+ }
+ self.previewTasks.append(task)
+ }
let headerItem = NSMenuItem()
headerItem.tag = self.tag
diff --git a/apps/macos/Sources/Clawdbot/SessionMenuPreviewView.swift b/apps/macos/Sources/Clawdbot/SessionMenuPreviewView.swift
index 88c75160e..e7d81659e 100644
--- a/apps/macos/Sources/Clawdbot/SessionMenuPreviewView.swift
+++ b/apps/macos/Sources/Clawdbot/SessionMenuPreviewView.swift
@@ -1,5 +1,6 @@
import ClawdbotChatUI
import ClawdbotKit
+import ClawdbotProtocol
import OSLog
import SwiftUI
@@ -31,24 +32,24 @@ actor SessionPreviewCache {
static let shared = SessionPreviewCache()
private struct CacheEntry {
- let items: [SessionPreviewItem]
+ let snapshot: SessionMenuPreviewSnapshot
let updatedAt: Date
}
private var entries: [String: CacheEntry] = [:]
- func cachedItems(for sessionKey: String, maxAge: TimeInterval) -> [SessionPreviewItem]? {
+ func cachedSnapshot(for sessionKey: String, maxAge: TimeInterval) -> SessionMenuPreviewSnapshot? {
guard let entry = self.entries[sessionKey] else { return nil }
guard Date().timeIntervalSince(entry.updatedAt) < maxAge else { return nil }
- return entry.items
+ return entry.snapshot
}
- func store(items: [SessionPreviewItem], for sessionKey: String) {
- self.entries[sessionKey] = CacheEntry(items: items, updatedAt: Date())
+ func store(snapshot: SessionMenuPreviewSnapshot, for sessionKey: String) {
+ self.entries[sessionKey] = CacheEntry(snapshot: snapshot, updatedAt: Date())
}
- func lastItems(for sessionKey: String) -> [SessionPreviewItem]? {
- self.entries[sessionKey]?.items
+ func lastSnapshot(for sessionKey: String) -> SessionMenuPreviewSnapshot? {
+ self.entries[sessionKey]?.snapshot
}
}
@@ -99,8 +100,12 @@ actor SessionPreviewLimiter {
#if DEBUG
extension SessionPreviewCache {
- func _testSet(items: [SessionPreviewItem], for sessionKey: String, updatedAt: Date = Date()) {
- self.entries[sessionKey] = CacheEntry(items: items, updatedAt: updatedAt)
+ func _testSet(
+ snapshot: SessionMenuPreviewSnapshot,
+ for sessionKey: String,
+ updatedAt: Date = Date())
+ {
+ self.entries[sessionKey] = CacheEntry(snapshot: snapshot, updatedAt: updatedAt)
}
func _testReset() {
@@ -219,50 +224,44 @@ enum SessionMenuPreviewLoader {
private static let logger = Logger(subsystem: "com.clawdbot", category: "SessionPreview")
private static let previewTimeoutSeconds: Double = 4
private static let cacheMaxAgeSeconds: TimeInterval = 30
+ private static let previewMaxChars = 240
private struct PreviewTimeoutError: LocalizedError {
var errorDescription: String? { "preview timeout" }
}
+ static func prewarm(sessionKeys: [String], maxItems: Int) async {
+ let keys = self.uniqueKeys(sessionKeys)
+ guard !keys.isEmpty else { return }
+ do {
+ let payload = try await self.requestPreview(keys: keys, maxItems: maxItems)
+ await self.cache(payload: payload, maxItems: maxItems)
+ } catch {
+ if self.isUnknownMethodError(error) { return }
+ let errorDescription = String(describing: error)
+ Self.logger.debug(
+ "Session preview prewarm failed count=\(keys.count, privacy: .public) " +
+ "error=\(errorDescription, privacy: .public)")
+ }
+ }
+
static func load(sessionKey: String, maxItems: Int) async -> SessionMenuPreviewSnapshot {
- if let cached = await SessionPreviewCache.shared.cachedItems(for: sessionKey, maxAge: cacheMaxAgeSeconds) {
- return self.snapshot(from: cached)
- }
-
- let isConnected = await MainActor.run {
- if case .connected = ControlChannel.shared.state { return true }
- return false
- }
-
- guard isConnected else {
- if let fallback = await SessionPreviewCache.shared.lastItems(for: sessionKey) {
- return Self.snapshot(from: fallback)
- }
- return SessionMenuPreviewSnapshot(items: [], status: .error("Gateway disconnected"))
+ if let cached = await SessionPreviewCache.shared.cachedSnapshot(
+ for: sessionKey,
+ maxAge: cacheMaxAgeSeconds)
+ {
+ return cached
}
do {
- let timeoutMs = Int(self.previewTimeoutSeconds * 1000)
- let payload = try await SessionPreviewLimiter.shared.withPermit {
- try await AsyncTimeout.withTimeout(
- seconds: self.previewTimeoutSeconds,
- onTimeout: { PreviewTimeoutError() },
- operation: {
- try await GatewayConnection.shared.chatHistory(
- sessionKey: sessionKey,
- limit: self.previewLimit(for: maxItems),
- timeoutMs: timeoutMs)
- })
- }
- let built = Self.previewItems(from: payload, maxItems: maxItems)
- await SessionPreviewCache.shared.store(items: built, for: sessionKey)
- return Self.snapshot(from: built)
+ let snapshot = try await self.fetchSnapshot(sessionKey: sessionKey, maxItems: maxItems)
+ await SessionPreviewCache.shared.store(snapshot: snapshot, for: sessionKey)
+ return snapshot
} catch is CancellationError {
return SessionMenuPreviewSnapshot(items: [], status: .loading)
} catch {
- let fallback = await SessionPreviewCache.shared.lastItems(for: sessionKey)
- if let fallback {
- return Self.snapshot(from: fallback)
+ if let fallback = await SessionPreviewCache.shared.lastSnapshot(for: sessionKey) {
+ return fallback
}
let errorDescription = String(describing: error)
Self.logger.warning(
@@ -272,18 +271,120 @@ enum SessionMenuPreviewLoader {
}
}
+ private static func fetchSnapshot(sessionKey: String, maxItems: Int) async throws -> SessionMenuPreviewSnapshot {
+ do {
+ let payload = try await self.requestPreview(keys: [sessionKey], maxItems: maxItems)
+ if let entry = payload.previews.first(where: { $0.key == sessionKey }) ?? payload.previews.first {
+ return self.snapshot(from: entry, maxItems: maxItems)
+ }
+ return SessionMenuPreviewSnapshot(items: [], status: .error("Preview unavailable"))
+ } catch {
+ if self.isUnknownMethodError(error) {
+ return try await self.fetchHistorySnapshot(sessionKey: sessionKey, maxItems: maxItems)
+ }
+ throw error
+ }
+ }
+
+ private static func requestPreview(
+ keys: [String],
+ maxItems: Int) async throws -> ClawdbotSessionsPreviewPayload
+ {
+ let boundedItems = self.normalizeMaxItems(maxItems)
+ let timeoutMs = Int(self.previewTimeoutSeconds * 1000)
+ return try await SessionPreviewLimiter.shared.withPermit {
+ try await AsyncTimeout.withTimeout(
+ seconds: self.previewTimeoutSeconds,
+ onTimeout: { PreviewTimeoutError() },
+ operation: {
+ try await GatewayConnection.shared.sessionsPreview(
+ keys: keys,
+ limit: boundedItems,
+ maxChars: self.previewMaxChars,
+ timeoutMs: timeoutMs)
+ })
+ }
+ }
+
+ private static func fetchHistorySnapshot(
+ sessionKey: String,
+ maxItems: Int) async throws -> SessionMenuPreviewSnapshot
+ {
+ let timeoutMs = Int(self.previewTimeoutSeconds * 1000)
+ let payload = try await SessionPreviewLimiter.shared.withPermit {
+ try await AsyncTimeout.withTimeout(
+ seconds: self.previewTimeoutSeconds,
+ onTimeout: { PreviewTimeoutError() },
+ operation: {
+ try await GatewayConnection.shared.chatHistory(
+ sessionKey: sessionKey,
+ limit: self.previewLimit(for: maxItems),
+ timeoutMs: timeoutMs)
+ })
+ }
+ let built = Self.previewItems(from: payload, maxItems: maxItems)
+ return Self.snapshot(from: built)
+ }
+
private static func snapshot(from items: [SessionPreviewItem]) -> SessionMenuPreviewSnapshot {
SessionMenuPreviewSnapshot(items: items, status: items.isEmpty ? .empty : .ready)
}
+ private static func snapshot(
+ from entry: ClawdbotSessionPreviewEntry,
+ maxItems: Int) -> SessionMenuPreviewSnapshot
+ {
+ let items = self.previewItems(from: entry, maxItems: maxItems)
+ let normalized = entry.status.trimmingCharacters(in: .whitespacesAndNewlines).lowercased()
+ switch normalized {
+ case "ok":
+ return SessionMenuPreviewSnapshot(items: items, status: items.isEmpty ? .empty : .ready)
+ case "empty":
+ return SessionMenuPreviewSnapshot(items: items, status: .empty)
+ case "missing":
+ return SessionMenuPreviewSnapshot(items: items, status: .error("Session missing"))
+ default:
+ return SessionMenuPreviewSnapshot(items: items, status: .error("Preview unavailable"))
+ }
+ }
+
+ private static func cache(payload: ClawdbotSessionsPreviewPayload, maxItems: Int) async {
+ for entry in payload.previews {
+ let snapshot = self.snapshot(from: entry, maxItems: maxItems)
+ await SessionPreviewCache.shared.store(snapshot: snapshot, for: entry.key)
+ }
+ }
+
private static func previewLimit(for maxItems: Int) -> Int {
- min(max(maxItems * 3, 20), 120)
+ let boundedItems = self.normalizeMaxItems(maxItems)
+ return min(max(boundedItems * 3, 20), 120)
+ }
+
+ private static func normalizeMaxItems(_ maxItems: Int) -> Int {
+ max(1, min(maxItems, 50))
+ }
+
+ private static func previewItems(
+ from entry: ClawdbotSessionPreviewEntry,
+ maxItems: Int) -> [SessionPreviewItem]
+ {
+ let boundedItems = self.normalizeMaxItems(maxItems)
+ let built: [SessionPreviewItem] = entry.items.enumerated().compactMap { index, item in
+ let text = item.text.trimmingCharacters(in: .whitespacesAndNewlines)
+ guard !text.isEmpty else { return nil }
+ let role = self.previewRoleFromRaw(item.role)
+ return SessionPreviewItem(id: "\(entry.key)-\(index)", role: role, text: text)
+ }
+
+ let trimmed = built.suffix(boundedItems)
+ return Array(trimmed.reversed())
}
private static func previewItems(
from payload: ClawdbotChatHistoryPayload,
maxItems: Int) -> [SessionPreviewItem]
{
+ let boundedItems = self.normalizeMaxItems(maxItems)
let raw: [ClawdbotKit.AnyCodable] = payload.messages ?? []
let messages = self.decodeMessages(raw)
let built = messages.compactMap { message -> SessionPreviewItem? in
@@ -294,7 +395,7 @@ enum SessionMenuPreviewLoader {
return SessionPreviewItem(id: id, role: role, text: text)
}
- let trimmed = built.suffix(maxItems)
+ let trimmed = built.suffix(boundedItems)
return Array(trimmed.reversed())
}
@@ -307,12 +408,16 @@ enum SessionMenuPreviewLoader {
private static func previewRole(_ raw: String, isTool: Bool) -> PreviewRole {
if isTool { return .tool }
+ return self.previewRoleFromRaw(raw)
+ }
+
+ private static func previewRoleFromRaw(_ raw: String) -> PreviewRole {
switch raw.lowercased() {
- case "user": return .user
- case "assistant": return .assistant
- case "system": return .system
- case "tool": return .tool
- default: return .other
+ case "user": .user
+ case "assistant": .assistant
+ case "system": .system
+ case "tool": .tool
+ default: .other
}
}
@@ -375,4 +480,16 @@ enum SessionMenuPreviewLoader {
}
return result
}
+
+ private static func uniqueKeys(_ keys: [String]) -> [String] {
+ let trimmed = keys.map { $0.trimmingCharacters(in: .whitespacesAndNewlines) }
+ return self.dedupePreservingOrder(trimmed.filter { !$0.isEmpty })
+ }
+
+ private static func isUnknownMethodError(_ error: Error) -> Bool {
+ guard let response = error as? GatewayResponseError else { return false }
+ guard response.code == ErrorCode.invalidRequest.rawValue else { return false }
+ let message = response.message.lowercased()
+ return message.contains("unknown method")
+ }
}
diff --git a/apps/macos/Tests/ClawdbotIPCTests/SessionMenuPreviewTests.swift b/apps/macos/Tests/ClawdbotIPCTests/SessionMenuPreviewTests.swift
index b1d7b462c..af25d5246 100644
--- a/apps/macos/Tests/ClawdbotIPCTests/SessionMenuPreviewTests.swift
+++ b/apps/macos/Tests/ClawdbotIPCTests/SessionMenuPreviewTests.swift
@@ -7,20 +7,22 @@ struct SessionMenuPreviewTests {
@Test func loaderReturnsCachedItems() async {
await SessionPreviewCache.shared._testReset()
let items = [SessionPreviewItem(id: "1", role: .user, text: "Hi")]
- await SessionPreviewCache.shared._testSet(items: items, for: "main")
+ let snapshot = SessionMenuPreviewSnapshot(items: items, status: .ready)
+ await SessionPreviewCache.shared._testSet(snapshot: snapshot, for: "main")
- let snapshot = await SessionMenuPreviewLoader.load(sessionKey: "main", maxItems: 10)
- #expect(snapshot.status == .ready)
- #expect(snapshot.items.count == 1)
- #expect(snapshot.items.first?.text == "Hi")
+ let loaded = await SessionMenuPreviewLoader.load(sessionKey: "main", maxItems: 10)
+ #expect(loaded.status == .ready)
+ #expect(loaded.items.count == 1)
+ #expect(loaded.items.first?.text == "Hi")
}
@Test func loaderReturnsEmptyWhenCachedEmpty() async {
await SessionPreviewCache.shared._testReset()
- await SessionPreviewCache.shared._testSet(items: [], for: "main")
+ let snapshot = SessionMenuPreviewSnapshot(items: [], status: .empty)
+ await SessionPreviewCache.shared._testSet(snapshot: snapshot, for: "main")
- let snapshot = await SessionMenuPreviewLoader.load(sessionKey: "main", maxItems: 10)
- #expect(snapshot.status == .empty)
- #expect(snapshot.items.isEmpty)
+ let loaded = await SessionMenuPreviewLoader.load(sessionKey: "main", maxItems: 10)
+ #expect(loaded.status == .empty)
+ #expect(loaded.items.isEmpty)
}
}
diff --git a/apps/shared/ClawdbotKit/Sources/ClawdbotChatUI/ChatModels.swift b/apps/shared/ClawdbotKit/Sources/ClawdbotChatUI/ChatModels.swift
index ab86f6b53..7b1c693a6 100644
--- a/apps/shared/ClawdbotKit/Sources/ClawdbotChatUI/ChatModels.swift
+++ b/apps/shared/ClawdbotKit/Sources/ClawdbotChatUI/ChatModels.swift
@@ -235,6 +235,22 @@ public struct ClawdbotChatHistoryPayload: Codable, Sendable {
public let thinkingLevel: String?
}
+public struct ClawdbotSessionPreviewItem: Codable, Hashable, Sendable {
+ public let role: String
+ public let text: String
+}
+
+public struct ClawdbotSessionPreviewEntry: Codable, Sendable {
+ public let key: String
+ public let status: String
+ public let items: [ClawdbotSessionPreviewItem]
+}
+
+public struct ClawdbotSessionsPreviewPayload: Codable, Sendable {
+ public let ts: Int
+ public let previews: [ClawdbotSessionPreviewEntry]
+}
+
public struct ClawdbotChatSendResponse: Codable, Sendable {
public let runId: String
public let status: String
diff --git a/src/gateway/protocol/index.ts b/src/gateway/protocol/index.ts
index d1656135c..6e5a862d1 100644
--- a/src/gateway/protocol/index.ts
+++ b/src/gateway/protocol/index.ts
@@ -140,6 +140,8 @@ import {
SessionsListParamsSchema,
type SessionsPatchParams,
SessionsPatchParamsSchema,
+ type SessionsPreviewParams,
+ SessionsPreviewParamsSchema,
type SessionsResetParams,
SessionsResetParamsSchema,
type SessionsResolveParams,
@@ -229,6 +231,9 @@ export const validateNodeInvokeResultParams = ajv.compile(NodeEventParamsSchema);
export const validateSessionsListParams = ajv.compile(SessionsListParamsSchema);
+export const validateSessionsPreviewParams = ajv.compile(
+ SessionsPreviewParamsSchema,
+);
export const validateSessionsResolveParams = ajv.compile(
SessionsResolveParamsSchema,
);
@@ -376,6 +381,7 @@ export {
NodeListParamsSchema,
NodeInvokeParamsSchema,
SessionsListParamsSchema,
+ SessionsPreviewParamsSchema,
SessionsPatchParamsSchema,
SessionsResetParamsSchema,
SessionsDeleteParamsSchema,
@@ -488,6 +494,7 @@ export type {
NodeInvokeResultParams,
NodeEventParams,
SessionsListParams,
+ SessionsPreviewParams,
SessionsResolveParams,
SessionsPatchParams,
SessionsResetParams,
diff --git a/src/gateway/protocol/schema/protocol-schemas.ts b/src/gateway/protocol/schema/protocol-schemas.ts
index 7e55d2075..e92f114e2 100644
--- a/src/gateway/protocol/schema/protocol-schemas.ts
+++ b/src/gateway/protocol/schema/protocol-schemas.ts
@@ -108,6 +108,7 @@ import {
SessionsDeleteParamsSchema,
SessionsListParamsSchema,
SessionsPatchParamsSchema,
+ SessionsPreviewParamsSchema,
SessionsResetParamsSchema,
SessionsResolveParamsSchema,
} from "./sessions.js";
@@ -155,6 +156,7 @@ export const ProtocolSchemas: Record = {
NodeEventParams: NodeEventParamsSchema,
NodeInvokeRequestEvent: NodeInvokeRequestEventSchema,
SessionsListParams: SessionsListParamsSchema,
+ SessionsPreviewParams: SessionsPreviewParamsSchema,
SessionsResolveParams: SessionsResolveParamsSchema,
SessionsPatchParams: SessionsPatchParamsSchema,
SessionsResetParams: SessionsResetParamsSchema,
diff --git a/src/gateway/protocol/schema/sessions.ts b/src/gateway/protocol/schema/sessions.ts
index 42fa83ff6..4b7e895c7 100644
--- a/src/gateway/protocol/schema/sessions.ts
+++ b/src/gateway/protocol/schema/sessions.ts
@@ -26,6 +26,15 @@ export const SessionsListParamsSchema = Type.Object(
{ additionalProperties: false },
);
+export const SessionsPreviewParamsSchema = Type.Object(
+ {
+ keys: Type.Array(NonEmptyString, { minItems: 1 }),
+ limit: Type.Optional(Type.Integer({ minimum: 1 })),
+ maxChars: Type.Optional(Type.Integer({ minimum: 20 })),
+ },
+ { additionalProperties: false },
+);
+
export const SessionsResolveParamsSchema = Type.Object(
{
key: Type.Optional(NonEmptyString),
diff --git a/src/gateway/protocol/schema/types.ts b/src/gateway/protocol/schema/types.ts
index 164d6b902..696503721 100644
--- a/src/gateway/protocol/schema/types.ts
+++ b/src/gateway/protocol/schema/types.ts
@@ -101,6 +101,7 @@ import type {
SessionsDeleteParamsSchema,
SessionsListParamsSchema,
SessionsPatchParamsSchema,
+ SessionsPreviewParamsSchema,
SessionsResetParamsSchema,
SessionsResolveParamsSchema,
} from "./sessions.js";
@@ -144,6 +145,7 @@ export type NodeInvokeParams = Static;
export type NodeInvokeResultParams = Static;
export type NodeEventParams = Static;
export type SessionsListParams = Static;
+export type SessionsPreviewParams = Static;
export type SessionsResolveParams = Static;
export type SessionsPatchParams = Static;
export type SessionsResetParams = Static;
diff --git a/src/gateway/server-methods-list.ts b/src/gateway/server-methods-list.ts
index b90643df8..b02902b5e 100644
--- a/src/gateway/server-methods-list.ts
+++ b/src/gateway/server-methods-list.ts
@@ -34,6 +34,7 @@ const BASE_METHODS = [
"voicewake.get",
"voicewake.set",
"sessions.list",
+ "sessions.preview",
"sessions.patch",
"sessions.reset",
"sessions.delete",
diff --git a/src/gateway/server-methods.ts b/src/gateway/server-methods.ts
index 486bd249c..9651add19 100644
--- a/src/gateway/server-methods.ts
+++ b/src/gateway/server-methods.ts
@@ -59,6 +59,7 @@ const READ_METHODS = new Set([
"skills.status",
"voicewake.get",
"sessions.list",
+ "sessions.preview",
"cron.list",
"cron.status",
"cron.runs",
diff --git a/src/gateway/server-methods/sessions.ts b/src/gateway/server-methods/sessions.ts
index 9d3752627..f31c726bb 100644
--- a/src/gateway/server-methods/sessions.ts
+++ b/src/gateway/server-methods/sessions.ts
@@ -6,6 +6,7 @@ import { stopSubagentsForRequester } from "../../auto-reply/reply/abort.js";
import { clearSessionQueues } from "../../auto-reply/reply/queue.js";
import { loadConfig } from "../../config/config.js";
import {
+ loadSessionStore,
snapshotSessionOrigin,
resolveMainSessionKey,
type SessionEntry,
@@ -19,6 +20,7 @@ import {
validateSessionsDeleteParams,
validateSessionsListParams,
validateSessionsPatchParams,
+ validateSessionsPreviewParams,
validateSessionsResetParams,
validateSessionsResolveParams,
} from "../protocol/index.js";
@@ -27,9 +29,12 @@ import {
listSessionsFromStore,
loadCombinedSessionStoreForGateway,
loadSessionEntry,
+ readSessionPreviewItemsFromTranscript,
resolveGatewaySessionStoreTarget,
resolveSessionTranscriptCandidates,
type SessionsPatchResult,
+ type SessionsPreviewEntry,
+ type SessionsPreviewResult,
} from "../session-utils.js";
import { applySessionsPatchToStore } from "../sessions-patch.js";
import { resolveSessionKeyFromResolveParams } from "../sessions-resolve.js";
@@ -59,6 +64,74 @@ export const sessionsHandlers: GatewayRequestHandlers = {
});
respond(true, result, undefined);
},
+ "sessions.preview": ({ params, respond }) => {
+ if (!validateSessionsPreviewParams(params)) {
+ respond(
+ false,
+ undefined,
+ errorShape(
+ ErrorCodes.INVALID_REQUEST,
+ `invalid sessions.preview params: ${formatValidationErrors(
+ validateSessionsPreviewParams.errors,
+ )}`,
+ ),
+ );
+ return;
+ }
+ const p = params as import("../protocol/index.js").SessionsPreviewParams;
+ const keysRaw = Array.isArray(p.keys) ? p.keys : [];
+ const keys = keysRaw
+ .map((key) => String(key ?? "").trim())
+ .filter(Boolean)
+ .slice(0, 64);
+ const limit =
+ typeof p.limit === "number" && Number.isFinite(p.limit) ? Math.max(1, p.limit) : 12;
+ const maxChars =
+ typeof p.maxChars === "number" && Number.isFinite(p.maxChars)
+ ? Math.max(20, p.maxChars)
+ : 240;
+
+ if (keys.length === 0) {
+ respond(true, { ts: Date.now(), previews: [] } satisfies SessionsPreviewResult, undefined);
+ return;
+ }
+
+ const cfg = loadConfig();
+ const storeCache = new Map>();
+ const previews: SessionsPreviewEntry[] = [];
+
+ for (const key of keys) {
+ try {
+ const target = resolveGatewaySessionStoreTarget({ cfg, key });
+ const store = storeCache.get(target.storePath) ?? loadSessionStore(target.storePath);
+ storeCache.set(target.storePath, store);
+ const entry =
+ target.storeKeys.map((candidate) => store[candidate]).find(Boolean) ??
+ store[target.canonicalKey];
+ if (!entry?.sessionId) {
+ previews.push({ key, status: "missing", items: [] });
+ continue;
+ }
+ const items = readSessionPreviewItemsFromTranscript(
+ entry.sessionId,
+ target.storePath,
+ entry.sessionFile,
+ target.agentId,
+ limit,
+ maxChars,
+ );
+ previews.push({
+ key,
+ status: items.length > 0 ? "ok" : "empty",
+ items,
+ });
+ } catch {
+ previews.push({ key, status: "error", items: [] });
+ }
+ }
+
+ respond(true, { ts: Date.now(), previews } satisfies SessionsPreviewResult, undefined);
+ },
"sessions.resolve": ({ params, respond }) => {
if (!validateSessionsResolveParams(params)) {
respond(
diff --git a/src/gateway/server.sessions.gateway-server-sessions-a.test.ts b/src/gateway/server.sessions.gateway-server-sessions-a.test.ts
index abfcb44e7..95e91ef50 100644
--- a/src/gateway/server.sessions.gateway-server-sessions-a.test.ts
+++ b/src/gateway/server.sessions.gateway-server-sessions-a.test.ts
@@ -103,6 +103,7 @@ describe("gateway server sessions", () => {
expect((hello as unknown as { features?: { methods?: string[] } }).features?.methods).toEqual(
expect.arrayContaining([
"sessions.list",
+ "sessions.preview",
"sessions.patch",
"sessions.reset",
"sessions.delete",
@@ -338,6 +339,53 @@ describe("gateway server sessions", () => {
await server.close();
});
+ test("sessions.preview returns transcript previews", async () => {
+ const dir = await fs.mkdtemp(path.join(os.tmpdir(), "clawdbot-sessions-preview-"));
+ const storePath = path.join(dir, "sessions.json");
+ testState.sessionStorePath = storePath;
+ const sessionId = "sess-preview";
+ const transcriptPath = path.join(dir, `${sessionId}.jsonl`);
+ const lines = [
+ JSON.stringify({ type: "session", version: 1, id: sessionId }),
+ JSON.stringify({ message: { role: "user", content: "Hello" } }),
+ JSON.stringify({ message: { role: "assistant", content: "Hi" } }),
+ JSON.stringify({
+ message: { role: "assistant", content: [{ type: "toolcall", name: "weather" }] },
+ }),
+ JSON.stringify({ message: { role: "assistant", content: "Forecast ready" } }),
+ ];
+ await fs.writeFile(transcriptPath, lines.join("\n"), "utf-8");
+
+ await writeSessionStore({
+ entries: {
+ main: {
+ sessionId,
+ updatedAt: Date.now(),
+ },
+ },
+ });
+
+ const { server, ws } = await startServerWithClient();
+ await connectOk(ws);
+ const preview = await rpcReq<{
+ previews: Array<{
+ key: string;
+ status: string;
+ items: Array<{ role: string; text: string }>;
+ }>;
+ }>(ws, "sessions.preview", { keys: ["main"], limit: 3, maxChars: 120 });
+
+ expect(preview.ok).toBe(true);
+ const entry = preview.payload?.previews[0];
+ expect(entry?.key).toBe("main");
+ expect(entry?.status).toBe("ok");
+ expect(entry?.items.map((item) => item.role)).toEqual(["assistant", "tool", "assistant"]);
+ expect(entry?.items[1]?.text).toContain("call weather");
+
+ ws.close();
+ await server.close();
+ });
+
test("sessions.delete rejects main and aborts active runs", async () => {
const dir = await fs.mkdtemp(path.join(os.tmpdir(), "clawdbot-sessions-"));
const storePath = path.join(dir, "sessions.json");
diff --git a/src/gateway/session-utils.fs.test.ts b/src/gateway/session-utils.fs.test.ts
index 8e2ad67c6..a85dc9f69 100644
--- a/src/gateway/session-utils.fs.test.ts
+++ b/src/gateway/session-utils.fs.test.ts
@@ -5,6 +5,7 @@ import { afterEach, beforeEach, describe, expect, test } from "vitest";
import {
readFirstUserMessageFromTranscript,
readLastMessagePreviewFromTranscript,
+ readSessionPreviewItemsFromTranscript,
} from "./session-utils.fs.js";
describe("readFirstUserMessageFromTranscript", () => {
@@ -341,3 +342,65 @@ describe("readLastMessagePreviewFromTranscript", () => {
expect(result).toBe("Valid UTF-8: 你好世界 🌍");
});
});
+
+describe("readSessionPreviewItemsFromTranscript", () => {
+ let tmpDir: string;
+ let storePath: string;
+
+ beforeEach(() => {
+ tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "clawdbot-session-preview-test-"));
+ storePath = path.join(tmpDir, "sessions.json");
+ });
+
+ afterEach(() => {
+ fs.rmSync(tmpDir, { recursive: true, force: true });
+ });
+
+ test("returns recent preview items with tool summary", () => {
+ const sessionId = "preview-session";
+ const transcriptPath = path.join(tmpDir, `${sessionId}.jsonl`);
+ const lines = [
+ JSON.stringify({ type: "session", version: 1, id: sessionId }),
+ JSON.stringify({ message: { role: "user", content: "Hello" } }),
+ JSON.stringify({ message: { role: "assistant", content: "Hi" } }),
+ JSON.stringify({
+ message: { role: "assistant", content: [{ type: "toolcall", name: "weather" }] },
+ }),
+ JSON.stringify({ message: { role: "assistant", content: "Forecast ready" } }),
+ ];
+ fs.writeFileSync(transcriptPath, lines.join("\n"), "utf-8");
+
+ const result = readSessionPreviewItemsFromTranscript(
+ sessionId,
+ storePath,
+ undefined,
+ undefined,
+ 3,
+ 120,
+ );
+
+ expect(result.map((item) => item.role)).toEqual(["assistant", "tool", "assistant"]);
+ expect(result[1]?.text).toContain("call weather");
+ });
+
+ test("truncates preview text to max chars", () => {
+ const sessionId = "preview-truncate";
+ const transcriptPath = path.join(tmpDir, `${sessionId}.jsonl`);
+ const longText = "a".repeat(60);
+ const lines = [JSON.stringify({ message: { role: "assistant", content: longText } })];
+ fs.writeFileSync(transcriptPath, lines.join("\n"), "utf-8");
+
+ const result = readSessionPreviewItemsFromTranscript(
+ sessionId,
+ storePath,
+ undefined,
+ undefined,
+ 1,
+ 24,
+ );
+
+ expect(result).toHaveLength(1);
+ expect(result[0]?.text.length).toBe(24);
+ expect(result[0]?.text.endsWith("...")).toBe(true);
+ });
+});
diff --git a/src/gateway/session-utils.fs.ts b/src/gateway/session-utils.fs.ts
index eb8912359..d6453ace6 100644
--- a/src/gateway/session-utils.fs.ts
+++ b/src/gateway/session-utils.fs.ts
@@ -3,6 +3,8 @@ import os from "node:os";
import path from "node:path";
import { resolveSessionTranscriptPath } from "../config/sessions.js";
+import { stripEnvelope } from "./chat-sanitize.js";
+import type { SessionPreviewItem } from "./session-utils.types.js";
export function readSessionMessages(
sessionId: string,
@@ -189,3 +191,202 @@ export function readLastMessagePreviewFromTranscript(
}
return null;
}
+
+const PREVIEW_READ_SIZES = [64 * 1024, 256 * 1024, 1024 * 1024];
+const PREVIEW_MAX_LINES = 200;
+
+type TranscriptContentEntry = {
+ type?: string;
+ text?: string;
+ name?: string;
+};
+
+type TranscriptPreviewMessage = {
+ role?: string;
+ content?: string | TranscriptContentEntry[];
+ text?: string;
+ toolName?: string;
+ tool_name?: string;
+};
+
+function normalizeRole(role: string | undefined, isTool: boolean): SessionPreviewItem["role"] {
+ if (isTool) return "tool";
+ switch ((role ?? "").toLowerCase()) {
+ case "user":
+ return "user";
+ case "assistant":
+ return "assistant";
+ case "system":
+ return "system";
+ case "tool":
+ return "tool";
+ default:
+ return "other";
+ }
+}
+
+function truncatePreviewText(text: string, maxChars: number): string {
+ if (maxChars <= 0 || text.length <= maxChars) return text;
+ if (maxChars <= 3) return text.slice(0, maxChars);
+ return `${text.slice(0, maxChars - 3)}...`;
+}
+
+function extractPreviewText(message: TranscriptPreviewMessage): string | null {
+ if (typeof message.content === "string") {
+ const trimmed = message.content.trim();
+ return trimmed ? trimmed : null;
+ }
+ if (Array.isArray(message.content)) {
+ const parts = message.content
+ .map((entry) => (typeof entry?.text === "string" ? entry.text : ""))
+ .filter((text) => text.trim().length > 0);
+ if (parts.length > 0) {
+ return parts.join("\n").trim();
+ }
+ }
+ if (typeof message.text === "string") {
+ const trimmed = message.text.trim();
+ return trimmed ? trimmed : null;
+ }
+ return null;
+}
+
+function isToolCall(message: TranscriptPreviewMessage): boolean {
+ if (message.toolName || message.tool_name) return true;
+ if (!Array.isArray(message.content)) return false;
+ return message.content.some((entry) => {
+ if (entry?.name) return true;
+ const raw = typeof entry?.type === "string" ? entry.type.toLowerCase() : "";
+ return raw === "toolcall" || raw === "tool_call";
+ });
+}
+
+function extractToolNames(message: TranscriptPreviewMessage): string[] {
+ const names: string[] = [];
+ if (Array.isArray(message.content)) {
+ for (const entry of message.content) {
+ if (typeof entry?.name === "string" && entry.name.trim()) {
+ names.push(entry.name.trim());
+ }
+ }
+ }
+ const toolName = typeof message.toolName === "string" ? message.toolName : message.tool_name;
+ if (typeof toolName === "string" && toolName.trim()) {
+ names.push(toolName.trim());
+ }
+ return names;
+}
+
+function extractMediaSummary(message: TranscriptPreviewMessage): string | null {
+ if (!Array.isArray(message.content)) return null;
+ for (const entry of message.content) {
+ const raw = typeof entry?.type === "string" ? entry.type.trim().toLowerCase() : "";
+ if (!raw || raw === "text" || raw === "toolcall" || raw === "tool_call") continue;
+ return `[${raw}]`;
+ }
+ return null;
+}
+
+function buildPreviewItems(
+ messages: TranscriptPreviewMessage[],
+ maxItems: number,
+ maxChars: number,
+): SessionPreviewItem[] {
+ const items: SessionPreviewItem[] = [];
+ for (const message of messages) {
+ const toolCall = isToolCall(message);
+ const role = normalizeRole(message.role, toolCall);
+ let text = extractPreviewText(message);
+ if (!text) {
+ const toolNames = extractToolNames(message);
+ if (toolNames.length > 0) {
+ const shown = toolNames.slice(0, 2);
+ const overflow = toolNames.length - shown.length;
+ text = `call ${shown.join(", ")}`;
+ if (overflow > 0) text += ` +${overflow}`;
+ }
+ }
+ if (!text) {
+ text = extractMediaSummary(message);
+ }
+ if (!text) continue;
+ let trimmed = text.trim();
+ if (!trimmed) continue;
+ if (role === "user") {
+ trimmed = stripEnvelope(trimmed);
+ }
+ trimmed = truncatePreviewText(trimmed, maxChars);
+ items.push({ role, text: trimmed });
+ }
+
+ if (items.length <= maxItems) return items;
+ return items.slice(-maxItems);
+}
+
+function readRecentMessagesFromTranscript(
+ filePath: string,
+ maxMessages: number,
+ readBytes: number,
+): TranscriptPreviewMessage[] {
+ let fd: number | null = null;
+ try {
+ fd = fs.openSync(filePath, "r");
+ const stat = fs.fstatSync(fd);
+ const size = stat.size;
+ if (size === 0) return [];
+
+ const readStart = Math.max(0, size - readBytes);
+ const readLen = Math.min(size, readBytes);
+ const buf = Buffer.alloc(readLen);
+ fs.readSync(fd, buf, 0, readLen, readStart);
+
+ const chunk = buf.toString("utf-8");
+ const lines = chunk.split(/\r?\n/).filter((l) => l.trim());
+ const tailLines = lines.slice(-PREVIEW_MAX_LINES);
+
+ const collected: TranscriptPreviewMessage[] = [];
+ for (let i = tailLines.length - 1; i >= 0; i--) {
+ const line = tailLines[i];
+ try {
+ const parsed = JSON.parse(line);
+ const msg = parsed?.message as TranscriptPreviewMessage | undefined;
+ if (msg && typeof msg === "object") {
+ collected.push(msg);
+ if (collected.length >= maxMessages) break;
+ }
+ } catch {
+ // skip malformed lines
+ }
+ }
+ return collected.reverse();
+ } catch {
+ return [];
+ } finally {
+ if (fd !== null) fs.closeSync(fd);
+ }
+}
+
+export function readSessionPreviewItemsFromTranscript(
+ sessionId: string,
+ storePath: string | undefined,
+ sessionFile: string | undefined,
+ agentId: string | undefined,
+ maxItems: number,
+ maxChars: number,
+): SessionPreviewItem[] {
+ const candidates = resolveSessionTranscriptCandidates(sessionId, storePath, sessionFile, agentId);
+ const filePath = candidates.find((p) => fs.existsSync(p));
+ if (!filePath) return [];
+
+ const boundedItems = Math.max(1, Math.min(maxItems, 50));
+ const boundedChars = Math.max(20, Math.min(maxChars, 2000));
+
+ for (const readSize of PREVIEW_READ_SIZES) {
+ const messages = readRecentMessagesFromTranscript(filePath, boundedItems, readSize);
+ if (messages.length > 0 || readSize === PREVIEW_READ_SIZES[PREVIEW_READ_SIZES.length - 1]) {
+ return buildPreviewItems(messages, boundedItems, boundedChars);
+ }
+ }
+
+ return [];
+}
diff --git a/src/gateway/session-utils.ts b/src/gateway/session-utils.ts
index 1c5934aa5..c4046a08e 100644
--- a/src/gateway/session-utils.ts
+++ b/src/gateway/session-utils.ts
@@ -38,6 +38,7 @@ export {
capArrayByJsonBytes,
readFirstUserMessageFromTranscript,
readLastMessagePreviewFromTranscript,
+ readSessionPreviewItemsFromTranscript,
readSessionMessages,
resolveSessionTranscriptCandidates,
} from "./session-utils.fs.js";
@@ -47,6 +48,8 @@ export type {
GatewaySessionsDefaults,
SessionsListResult,
SessionsPatchResult,
+ SessionsPreviewEntry,
+ SessionsPreviewResult,
} from "./session-utils.types.js";
const DERIVED_TITLE_MAX_LEN = 60;
diff --git a/src/gateway/session-utils.types.ts b/src/gateway/session-utils.types.ts
index 99491a7f9..074e9eaa8 100644
--- a/src/gateway/session-utils.types.ts
+++ b/src/gateway/session-utils.types.ts
@@ -55,6 +55,22 @@ export type GatewayAgentRow = {
};
};
+export type SessionPreviewItem = {
+ role: "user" | "assistant" | "tool" | "system" | "other";
+ text: string;
+};
+
+export type SessionsPreviewEntry = {
+ key: string;
+ status: "ok" | "empty" | "missing" | "error";
+ items: SessionPreviewItem[];
+};
+
+export type SessionsPreviewResult = {
+ ts: number;
+ previews: SessionsPreviewEntry[];
+};
+
export type SessionsListResult = {
ts: number;
path: string;
From 36a2584ac777646261c73f09799239157932e480 Mon Sep 17 00:00:00 2001
From: Peter Steinberger
Date: Thu, 22 Jan 2026 10:29:36 +0000
Subject: [PATCH 18/97] fix: allowlist match without local exec resolution
---
src/infra/exec-approvals.ts | 22 +++++++++++++++++++++-
1 file changed, 21 insertions(+), 1 deletion(-)
diff --git a/src/infra/exec-approvals.ts b/src/infra/exec-approvals.ts
index 5c9e4a826..b2c53fc80 100644
--- a/src/infra/exec-approvals.ts
+++ b/src/infra/exec-approvals.ts
@@ -465,6 +465,21 @@ function matchesPattern(pattern: string, target: string): boolean {
return regex.test(normalizedTarget);
}
+function resolveAllowlistCandidatePath(
+ resolution: CommandResolution | null,
+ cwd?: string,
+): string | undefined {
+ if (!resolution) return undefined;
+ if (resolution.resolvedPath) return resolution.resolvedPath;
+ const raw = resolution.rawExecutable?.trim();
+ if (!raw) return undefined;
+ const expanded = raw.startsWith("~") ? expandHome(raw) : raw;
+ if (!expanded.includes("/") && !expanded.includes("\\")) return undefined;
+ if (path.isAbsolute(expanded)) return expanded;
+ const base = cwd && cwd.trim() ? cwd.trim() : process.cwd();
+ return path.resolve(base, expanded);
+}
+
export function matchAllowlist(
entries: ExecAllowlistEntry[],
resolution: CommandResolution | null,
@@ -770,7 +785,12 @@ export function evaluateExecAllowlist(params: {
}
const allowSkills = params.autoAllowSkills === true && (params.skillBins?.size ?? 0) > 0;
const allowlistSatisfied = params.analysis.segments.every((segment) => {
- const match = matchAllowlist(params.allowlist, segment.resolution);
+ const candidatePath = resolveAllowlistCandidatePath(segment.resolution, params.cwd);
+ const candidateResolution =
+ candidatePath && segment.resolution
+ ? { ...segment.resolution, resolvedPath: candidatePath }
+ : segment.resolution;
+ const match = matchAllowlist(params.allowlist, candidateResolution);
if (match) allowlistMatches.push(match);
const safe = isSafeBinUsage({
argv: segment.argv,
From 5045a9a00dd2a3e24261961dc393188fff662402 Mon Sep 17 00:00:00 2001
From: Peter Steinberger
Date: Thu, 22 Jan 2026 10:29:44 +0000
Subject: [PATCH 19/97] test: relax Windows vitest limits
---
vitest.config.ts | 8 +++++---
1 file changed, 5 insertions(+), 3 deletions(-)
diff --git a/vitest.config.ts b/vitest.config.ts
index 886ca5402..2bf5b8aac 100644
--- a/vitest.config.ts
+++ b/vitest.config.ts
@@ -4,7 +4,9 @@ import { defineConfig } from "vitest/config";
const repoRoot = path.dirname(fileURLToPath(import.meta.url));
const isCI = process.env.CI === "true" || process.env.GITHUB_ACTIONS === "true";
+const isWindows = process.platform === "win32";
const localWorkers = 4;
+const ciWorkers = isWindows ? 2 : 3;
export default defineConfig({
resolve: {
@@ -13,10 +15,10 @@ export default defineConfig({
},
},
test: {
- testTimeout: 60_000,
- hookTimeout: 120_000,
+ testTimeout: isWindows ? 120_000 : 60_000,
+ hookTimeout: isWindows ? 180_000 : 120_000,
pool: "forks",
- maxWorkers: isCI ? 3 : localWorkers,
+ maxWorkers: isCI ? ciWorkers : localWorkers,
include: [
"src/**/*.test.ts",
"extensions/**/*.test.ts",
From d47db551063fc2c49ba5fd45c4d859c27e7c9661 Mon Sep 17 00:00:00 2001
From: Peter Steinberger
Date: Thu, 22 Jan 2026 09:39:47 +0000
Subject: [PATCH 20/97] chore: sync plugin versions
---
extensions/lobster/package.json | 6 ++++--
1 file changed, 4 insertions(+), 2 deletions(-)
diff --git a/extensions/lobster/package.json b/extensions/lobster/package.json
index 606975434..3e62dc2f9 100644
--- a/extensions/lobster/package.json
+++ b/extensions/lobster/package.json
@@ -1,9 +1,11 @@
{
"name": "@clawdbot/lobster",
- "version": "2026.1.17-1",
+ "version": "2026.1.21",
"type": "module",
"description": "Lobster workflow tool plugin (typed pipelines + resumable approvals)",
"clawdbot": {
- "extensions": ["./index.ts"]
+ "extensions": [
+ "./index.ts"
+ ]
}
}
From fb85cb327145ee517a8963a758e6e50c91c08122 Mon Sep 17 00:00:00 2001
From: Peter Steinberger
Date: Thu, 22 Jan 2026 10:47:58 +0000
Subject: [PATCH 21/97] docs: clarify bootstrap memory absence
---
docs/reference/templates/BOOTSTRAP.md | 2 ++
1 file changed, 2 insertions(+)
diff --git a/docs/reference/templates/BOOTSTRAP.md b/docs/reference/templates/BOOTSTRAP.md
index 7b1cad59d..f0d4f7ac8 100644
--- a/docs/reference/templates/BOOTSTRAP.md
+++ b/docs/reference/templates/BOOTSTRAP.md
@@ -7,6 +7,8 @@ read_when:
*You just woke up. Time to figure out who you are.*
+There is no memory yet. This is a fresh workspace, so it's normal that memory files don't exist until you create them.
+
## The Conversation
Don't interrogate. Don't be robotic. Just... talk.
From 058f00ba0b7c34bbaaa506de24c8dff600e73ce2 Mon Sep 17 00:00:00 2001
From: Peter Steinberger
Date: Thu, 22 Jan 2026 11:02:03 +0000
Subject: [PATCH 22/97] chore: update protocol Swift models
---
.../ClawdbotProtocol/GatewayModels.swift | 21 +++++++++++++++++++
.../ClawdbotProtocol/GatewayModels.swift | 21 +++++++++++++++++++
2 files changed, 42 insertions(+)
diff --git a/apps/macos/Sources/ClawdbotProtocol/GatewayModels.swift b/apps/macos/Sources/ClawdbotProtocol/GatewayModels.swift
index aefbdb572..4b80bf3f6 100644
--- a/apps/macos/Sources/ClawdbotProtocol/GatewayModels.swift
+++ b/apps/macos/Sources/ClawdbotProtocol/GatewayModels.swift
@@ -925,6 +925,27 @@ public struct SessionsListParams: Codable, Sendable {
}
}
+public struct SessionsPreviewParams: Codable, Sendable {
+ public let keys: [String]
+ public let limit: Int?
+ public let maxchars: Int?
+
+ public init(
+ keys: [String],
+ limit: Int?,
+ maxchars: Int?
+ ) {
+ self.keys = keys
+ self.limit = limit
+ self.maxchars = maxchars
+ }
+ private enum CodingKeys: String, CodingKey {
+ case keys
+ case limit
+ case maxchars = "maxChars"
+ }
+}
+
public struct SessionsResolveParams: Codable, Sendable {
public let key: String?
public let label: String?
diff --git a/apps/shared/ClawdbotKit/Sources/ClawdbotProtocol/GatewayModels.swift b/apps/shared/ClawdbotKit/Sources/ClawdbotProtocol/GatewayModels.swift
index aefbdb572..4b80bf3f6 100644
--- a/apps/shared/ClawdbotKit/Sources/ClawdbotProtocol/GatewayModels.swift
+++ b/apps/shared/ClawdbotKit/Sources/ClawdbotProtocol/GatewayModels.swift
@@ -925,6 +925,27 @@ public struct SessionsListParams: Codable, Sendable {
}
}
+public struct SessionsPreviewParams: Codable, Sendable {
+ public let keys: [String]
+ public let limit: Int?
+ public let maxchars: Int?
+
+ public init(
+ keys: [String],
+ limit: Int?,
+ maxchars: Int?
+ ) {
+ self.keys = keys
+ self.limit = limit
+ self.maxchars = maxchars
+ }
+ private enum CodingKeys: String, CodingKey {
+ case keys
+ case limit
+ case maxchars = "maxChars"
+ }
+}
+
public struct SessionsResolveParams: Codable, Sendable {
public let key: String?
public let label: String?
From 3be7ac8524c8674d71d4e1188bdd7038f11d5d93 Mon Sep 17 00:00:00 2001
From: Peter Steinberger
Date: Thu, 22 Jan 2026 11:08:58 +0000
Subject: [PATCH 23/97] fix: build control ui during prepack
---
package.json | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/package.json b/package.json
index 0bc9e7236..bd001bbbe 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
{
"name": "clawdbot",
- "version": "2026.1.21",
+ "version": "2026.1.21-1",
"description": "WhatsApp gateway CLI (Baileys web) with Pi RPC agent",
"type": "module",
"main": "dist/index.js",
@@ -73,7 +73,7 @@
"scripts": {
"dev": "node scripts/run-node.mjs",
"postinstall": "node scripts/postinstall.js",
- "prepack": "pnpm build",
+ "prepack": "pnpm build && pnpm ui:build",
"docs:list": "node scripts/docs-list.js",
"docs:bin": "node scripts/build-docs-list.mjs",
"docs:dev": "cd docs && mint dev",
From 019726f2d1c0007cb5491411160bd5dd0bfa0309 Mon Sep 17 00:00:00 2001
From: Peter Steinberger
Date: Thu, 22 Jan 2026 11:37:29 +0000
Subject: [PATCH 24/97] fix: guard invalid avatar bootstrap text
---
src/gateway/assistant-identity.test.ts | 42 ++++++++++++++++++++++++++
src/gateway/assistant-identity.ts | 32 +++++++++++++++++---
2 files changed, 69 insertions(+), 5 deletions(-)
create mode 100644 src/gateway/assistant-identity.test.ts
diff --git a/src/gateway/assistant-identity.test.ts b/src/gateway/assistant-identity.test.ts
new file mode 100644
index 000000000..5085708e5
--- /dev/null
+++ b/src/gateway/assistant-identity.test.ts
@@ -0,0 +1,42 @@
+import { describe, expect, it } from "vitest";
+
+import type { ClawdbotConfig } from "../config/config.js";
+import { DEFAULT_ASSISTANT_IDENTITY, resolveAssistantIdentity } from "./assistant-identity.js";
+
+describe("resolveAssistantIdentity avatar normalization", () => {
+ it("drops sentence-like avatar placeholders", () => {
+ const cfg: ClawdbotConfig = {
+ ui: {
+ assistant: {
+ avatar: "workspace-relative path, http(s) URL, or data URI",
+ },
+ },
+ };
+
+ expect(resolveAssistantIdentity({ cfg }).avatar).toBe(DEFAULT_ASSISTANT_IDENTITY.avatar);
+ });
+
+ it("keeps short text avatars", () => {
+ const cfg: ClawdbotConfig = {
+ ui: {
+ assistant: {
+ avatar: "PS",
+ },
+ },
+ };
+
+ expect(resolveAssistantIdentity({ cfg }).avatar).toBe("PS");
+ });
+
+ it("keeps path avatars", () => {
+ const cfg: ClawdbotConfig = {
+ ui: {
+ assistant: {
+ avatar: "avatars/clawd.png",
+ },
+ },
+ };
+
+ expect(resolveAssistantIdentity({ cfg }).avatar).toBe("avatars/clawd.png");
+ });
+});
diff --git a/src/gateway/assistant-identity.ts b/src/gateway/assistant-identity.ts
index 72f521d12..35ff43490 100644
--- a/src/gateway/assistant-identity.ts
+++ b/src/gateway/assistant-identity.ts
@@ -26,6 +26,25 @@ function coerceIdentityValue(value: string | undefined, maxLength: number): stri
return trimmed.slice(0, maxLength);
}
+function isAvatarUrl(value: string): boolean {
+ return /^https?:\/\//i.test(value) || /^data:image\//i.test(value);
+}
+
+function looksLikeAvatarPath(value: string): boolean {
+ if (/[\\/]/.test(value)) return true;
+ return /\.(png|jpe?g|gif|webp|svg|ico)$/i.test(value);
+}
+
+function normalizeAvatarValue(value: string | undefined): string | undefined {
+ if (!value) return undefined;
+ const trimmed = value.trim();
+ if (!trimmed) return undefined;
+ if (isAvatarUrl(trimmed)) return trimmed;
+ if (looksLikeAvatarPath(trimmed)) return trimmed;
+ if (!/\s/.test(trimmed) && trimmed.length <= 4) return trimmed;
+ return undefined;
+}
+
export function resolveAssistantIdentity(params: {
cfg: ClawdbotConfig;
agentId?: string | null;
@@ -43,12 +62,15 @@ export function resolveAssistantIdentity(params: {
coerceIdentityValue(fileIdentity?.name, MAX_ASSISTANT_NAME) ??
DEFAULT_ASSISTANT_IDENTITY.name;
+ const avatarCandidates = [
+ coerceIdentityValue(configAssistant?.avatar, MAX_ASSISTANT_AVATAR),
+ coerceIdentityValue(agentIdentity?.avatar, MAX_ASSISTANT_AVATAR),
+ coerceIdentityValue(agentIdentity?.emoji, MAX_ASSISTANT_AVATAR),
+ coerceIdentityValue(fileIdentity?.avatar, MAX_ASSISTANT_AVATAR),
+ coerceIdentityValue(fileIdentity?.emoji, MAX_ASSISTANT_AVATAR),
+ ];
const avatar =
- coerceIdentityValue(configAssistant?.avatar, MAX_ASSISTANT_AVATAR) ??
- coerceIdentityValue(agentIdentity?.avatar, MAX_ASSISTANT_AVATAR) ??
- coerceIdentityValue(agentIdentity?.emoji, MAX_ASSISTANT_AVATAR) ??
- coerceIdentityValue(fileIdentity?.avatar, MAX_ASSISTANT_AVATAR) ??
- coerceIdentityValue(fileIdentity?.emoji, MAX_ASSISTANT_AVATAR) ??
+ avatarCandidates.map((candidate) => normalizeAvatarValue(candidate)).find(Boolean) ??
DEFAULT_ASSISTANT_IDENTITY.avatar;
return { agentId, name, avatar };
From 447db67b181255ff98b7f5790d7d149572acb863 Mon Sep 17 00:00:00 2001
From: Peter Steinberger
Date: Thu, 22 Jan 2026 11:40:24 +0000
Subject: [PATCH 25/97] ui: add onboarding mode for control ui
---
dist/control-ui/assets/index-BPDeGGxb.css | 1 +
dist/control-ui/assets/index-bYQnHP3a.js | 3047 ++++++++++++++++++
dist/control-ui/assets/index-bYQnHP3a.js.map | 1 +
dist/control-ui/index.html | 15 +
ui/src/styles/layout.css | 12 +
ui/src/ui/app-gateway.ts | 2 +
ui/src/ui/app-render.helpers.ts | 34 +-
ui/src/ui/app-render.ts | 15 +-
ui/src/ui/app-view-state.ts | 1 +
ui/src/ui/app.ts | 10 +
10 files changed, 3122 insertions(+), 16 deletions(-)
create mode 100644 dist/control-ui/assets/index-BPDeGGxb.css
create mode 100644 dist/control-ui/assets/index-bYQnHP3a.js
create mode 100644 dist/control-ui/assets/index-bYQnHP3a.js.map
create mode 100644 dist/control-ui/index.html
diff --git a/dist/control-ui/assets/index-BPDeGGxb.css b/dist/control-ui/assets/index-BPDeGGxb.css
new file mode 100644
index 000000000..4fa216699
--- /dev/null
+++ b/dist/control-ui/assets/index-BPDeGGxb.css
@@ -0,0 +1 @@
+@import"https://fonts.googleapis.com/css2?family=IBM+Plex+Mono:wght@400;500&family=Unbounded:wght@400;500;600&family=Work+Sans:wght@400;500;600;700&display=swap";:root{--bg: #0a0f14;--bg-accent: #111826;--bg-grad-1: #162031;--bg-grad-2: #1f2a22;--bg-overlay: rgba(255, 255, 255, .05);--bg-glow: rgba(245, 159, 74, .12);--panel: rgba(14, 20, 30, .88);--panel-strong: rgba(18, 26, 38, .96);--chrome: rgba(9, 14, 20, .72);--chrome-strong: rgba(9, 14, 20, .86);--text: rgba(244, 246, 251, .96);--chat-text: rgba(231, 237, 244, .92);--muted: rgba(156, 169, 189, .72);--border: rgba(255, 255, 255, .09);--border-strong: rgba(255, 255, 255, .16);--accent: #f59f4a;--accent-2: #34c7b7;--ok: #2bd97f;--warn: #f2c94c;--danger: #ff6b6b;--focus: rgba(245, 159, 74, .35);--grid-line: rgba(255, 255, 255, .04);--theme-switch-x: 50%;--theme-switch-y: 50%;--mono: "IBM Plex Mono", ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;--font-body: "Work Sans", system-ui, sans-serif;--font-display: "Unbounded", "Times New Roman", serif;color-scheme:dark}:root[data-theme=light]{--bg: #f5f1ea;--bg-accent: #ffffff;--bg-grad-1: #f1e6d6;--bg-grad-2: #e5eef4;--bg-overlay: rgba(28, 32, 46, .05);--bg-glow: rgba(52, 199, 183, .14);--panel: rgba(255, 255, 255, .9);--panel-strong: rgba(255, 255, 255, .97);--chrome: rgba(255, 255, 255, .75);--chrome-strong: rgba(255, 255, 255, .88);--text: rgba(27, 36, 50, .98);--chat-text: rgba(36, 48, 66, .9);--muted: rgba(80, 94, 114, .7);--border: rgba(18, 24, 40, .12);--border-strong: rgba(18, 24, 40, .2);--accent: #e28a3f;--accent-2: #1ba99d;--ok: #1aa86c;--warn: #b3771c;--danger: #d44848;--focus: rgba(226, 138, 63, .35);--grid-line: rgba(18, 24, 40, .06);color-scheme:light}*{box-sizing:border-box}html,body{height:100%}body{margin:0;font:15px/1.5 var(--font-body);background:radial-gradient(1200px 900px at 15% -10%,var(--bg-grad-1) 0%,transparent 55%) fixed,radial-gradient(900px 700px at 80% 10%,var(--bg-grad-2) 0%,transparent 60%) fixed,linear-gradient(160deg,var(--bg) 0%,var(--bg-accent) 100%) fixed;color:var(--text)}body:before{content:"";position:fixed;inset:0;background:linear-gradient(140deg,var(--bg-overlay) 0%,rgba(255,255,255,0) 40%),radial-gradient(620px 420px at 75% 75%,var(--bg-glow),transparent 60%);pointer-events:none;z-index:0}@keyframes theme-circle-transition{0%{clip-path:circle(0% at var(--theme-switch-x, 50%) var(--theme-switch-y, 50%))}to{clip-path:circle(150% at var(--theme-switch-x, 50%) var(--theme-switch-y, 50%))}}html.theme-transition{view-transition-name:theme}html.theme-transition::view-transition-old(theme){mix-blend-mode:normal;animation:none;z-index:1}html.theme-transition::view-transition-new(theme){mix-blend-mode:normal;z-index:2;animation:theme-circle-transition .45s ease-out forwards}@media(prefers-reduced-motion:reduce){html.theme-transition::view-transition-old(theme),html.theme-transition::view-transition-new(theme){animation:none!important}}clawdbot-app{display:block;position:relative;z-index:1;min-height:100vh}a{color:inherit}button,input,textarea,select{font:inherit;color:inherit}@keyframes rise{0%{opacity:0;transform:translateY(6px)}to{opacity:1;transform:translateY(0)}}@keyframes dashboard-enter{0%{opacity:0;transform:translateY(12px)}to{opacity:1;transform:translateY(0)}}.shell{--shell-pad: 16px;--shell-gap: 16px;--shell-nav-width: 220px;--shell-topbar-height: 56px;--shell-focus-duration: .22s;--shell-focus-ease: cubic-bezier(.2, .85, .25, 1);min-height:100vh;display:grid;grid-template-columns:var(--shell-nav-width) minmax(0,1fr);grid-template-rows:var(--shell-topbar-height) 1fr;grid-template-areas:"topbar topbar" "nav content";gap:0;animation:dashboard-enter .6s ease-out;transition:grid-template-columns var(--shell-focus-duration) var(--shell-focus-ease)}.shell--chat{min-height:100vh;height:100vh;overflow:hidden}@supports (height: 100dvh){.shell--chat{height:100dvh}}.shell--nav-collapsed,.shell--chat-focus{grid-template-columns:0px minmax(0,1fr)}.shell--onboarding{grid-template-rows:0 1fr}.shell--onboarding .topbar{display:none}.shell--onboarding .content{padding-top:0}.shell--chat-focus .content{padding-top:0;gap:0}.topbar{grid-area:topbar;position:sticky;top:0;z-index:40;display:flex;justify-content:space-between;align-items:center;gap:16px;padding:0 20px;height:var(--shell-topbar-height);border-bottom:1px solid var(--border);background:var(--panel);-webkit-backdrop-filter:blur(18px);backdrop-filter:blur(18px)}.topbar-left{display:flex;align-items:center;gap:12px}.topbar .nav-collapse-toggle{width:44px;height:44px;margin-bottom:0}.topbar .nav-collapse-toggle__icon{font-size:22px}.brand{display:flex;flex-direction:column;gap:2px}.brand-title{font-family:var(--font-display);font-size:16px;letter-spacing:1px;text-transform:uppercase;font-weight:600;line-height:1.1}.brand-sub{font-size:10px;color:var(--muted);letter-spacing:.8px;text-transform:uppercase;line-height:1}.topbar-status{display:flex;align-items:center;gap:8px}.topbar-status .pill{padding:4px 10px;gap:6px;font-size:11px}.topbar-status .statusDot{width:6px;height:6px}.topbar-status .theme-toggle{--theme-item: 22px;--theme-gap: 4px;--theme-pad: 4px}.topbar-status .theme-icon{width:12px;height:12px}.nav{grid-area:nav;overflow-y:auto;overflow-x:hidden;padding:16px;border-right:1px solid var(--border);background:var(--panel);-webkit-backdrop-filter:blur(18px);backdrop-filter:blur(18px);transition:width var(--shell-focus-duration) var(--shell-focus-ease),padding var(--shell-focus-duration) var(--shell-focus-ease)}.shell--chat-focus .nav{width:0;padding:0;border-width:0;overflow:hidden;pointer-events:none}.nav--collapsed{width:0;min-width:0;padding:0;overflow:hidden;border:none;opacity:0;pointer-events:none}.nav-collapse-toggle{width:32px;height:32px;display:flex;align-items:center;justify-content:center;background:transparent;border:1px solid transparent;border-radius:6px;cursor:pointer;transition:background .15s ease,border-color .15s ease;margin-bottom:16px}.nav-collapse-toggle:hover{background:#ffffff14;border-color:var(--border)}:root[data-theme=light] .nav-collapse-toggle:hover{background:#0000000f}.nav-collapse-toggle__icon{font-size:16px;color:var(--muted)}.nav-group{margin-bottom:18px;display:grid;gap:6px;padding-bottom:12px;border-bottom:1px dashed rgba(255,255,255,.08)}.nav-group:last-child{margin-bottom:0;padding-bottom:0;border-bottom:none}.nav-group__items{display:grid;gap:4px}.nav-group--collapsed .nav-group__items{display:none}.nav-label{display:flex;align-items:center;justify-content:space-between;gap:8px;width:100%;padding:4px 0;font-size:11px;font-weight:500;text-transform:uppercase;letter-spacing:1.4px;color:var(--text);opacity:.7;margin-bottom:4px;background:transparent;border:none;cursor:pointer;text-align:left}.nav-label:hover{opacity:1}.nav-label--static{cursor:default}.nav-label--static:hover{opacity:.7}.nav-label__text{flex:1}.nav-label__chevron{font-size:12px;opacity:.6}.nav-item{position:relative;display:flex;align-items:center;justify-content:flex-start;gap:8px;padding:10px 12px 10px 14px;border-radius:12px;border:1px solid transparent;background:transparent;color:var(--muted);cursor:pointer;text-decoration:none;transition:border-color .16s ease,background .16s ease,color .16s ease}.nav-item__icon{font-size:16px;width:18px;height:18px;display:flex;align-items:center;justify-content:center;flex-shrink:0}.nav-item__text{font-size:13px;white-space:nowrap}.nav-item:hover{color:var(--text);border-color:#ffffff1f;background:#ffffff0f}.nav-item:before{content:"";position:absolute;left:0;top:50%;width:4px;height:60%;border-radius:0 999px 999px 0;transform:translateY(-50%);background:transparent}.nav-item.active{color:var(--text);border-color:#f59f4a73;background:#f59f4a1f}.nav-item.active:before{background:var(--accent);box-shadow:0 0 12px #f59f4a66}.content{grid-area:content;padding:8px 6px 20px;display:flex;flex-direction:column;gap:20px;min-height:0;overflow-y:auto;overflow-x:hidden}.content--chat{overflow:hidden}.content-header{display:flex;align-items:flex-end;justify-content:space-between;gap:12px;padding:0 6px;overflow:hidden;transform-origin:top center;transition:opacity var(--shell-focus-duration) var(--shell-focus-ease),transform var(--shell-focus-duration) var(--shell-focus-ease),max-height var(--shell-focus-duration) var(--shell-focus-ease),padding var(--shell-focus-duration) var(--shell-focus-ease);max-height:90px}.shell--chat-focus .content-header{opacity:0;transform:translateY(-10px);max-height:0px;padding:0;pointer-events:none}.page-title{font-family:var(--font-display);font-size:26px;letter-spacing:.6px}.page-sub{color:var(--muted);font-size:12px;letter-spacing:.4px}.page-meta{display:flex;gap:10px}.content--chat .content-header{flex-direction:row;align-items:center;justify-content:space-between;gap:16px}.content--chat .content-header>div:first-child{text-align:left}.content--chat .page-meta{justify-content:flex-start}.content--chat .chat-controls{flex-shrink:0}.grid{display:grid;gap:18px}.grid-cols-2{grid-template-columns:repeat(2,minmax(0,1fr))}.grid-cols-3{grid-template-columns:repeat(3,minmax(0,1fr))}.stat-grid{display:grid;gap:14px;grid-template-columns:repeat(auto-fit,minmax(140px,1fr))}.note-grid{display:grid;gap:14px;grid-template-columns:repeat(auto-fit,minmax(200px,1fr))}.row{display:flex;gap:12px;align-items:center}.stack{display:grid;gap:14px}.filters{display:flex;flex-wrap:wrap;gap:10px;align-items:center}@media(max-width:1100px){.shell{--shell-pad: 12px;--shell-gap: 12px;--shell-nav-col: 1fr;grid-template-columns:1fr;grid-template-rows:auto auto 1fr;grid-template-areas:"topbar" "nav" "content"}.nav{position:static;max-height:none;display:flex;gap:16px;overflow-x:auto;border-right:none;padding:12px}.nav-group{grid-auto-flow:column;grid-template-columns:repeat(auto-fit,minmax(120px,1fr));border-bottom:none;padding-bottom:0}.grid-cols-2,.grid-cols-3{grid-template-columns:1fr}.topbar{position:static;flex-direction:column;align-items:flex-start;gap:12px}.topbar-status{width:100%;flex-wrap:wrap}.table-head,.table-row,.list-item{grid-template-columns:1fr}}@media(max-width:1100px){.nav{display:flex;flex-direction:row;flex-wrap:nowrap;gap:6px;padding:10px 12px;overflow-x:auto;-webkit-overflow-scrolling:touch;scrollbar-width:none}.nav::-webkit-scrollbar{display:none}.nav-group,.nav-group__items{display:contents}.nav-label{display:none}.nav-group--collapsed .nav-group__items{display:contents}.nav-item{padding:8px 14px;font-size:13px;border-radius:10px;white-space:nowrap;flex-shrink:0}.nav-item:before{display:none}}@media(max-width:600px){.shell{--shell-pad: 8px;--shell-gap: 8px}.topbar{padding:10px 12px;border-radius:12px;gap:8px;flex-direction:row;flex-wrap:wrap;justify-content:space-between;align-items:center}.brand{flex:1;min-width:0}.brand-title{font-size:15px;letter-spacing:.3px}.brand-sub{display:none}.topbar-status{gap:6px;width:auto;flex-wrap:nowrap}.topbar-status .pill{padding:4px 8px;font-size:11px;gap:4px}.topbar-status .pill .mono{display:none}.topbar-status .pill span:nth-child(2){display:none}.nav{padding:8px;border-radius:12px;gap:8px;-webkit-overflow-scrolling:touch;scrollbar-width:none}.nav::-webkit-scrollbar{display:none}.nav-group{display:contents}.nav-label{display:none}.nav-item{padding:7px 10px;font-size:12px;border-radius:8px;white-space:nowrap;flex-shrink:0}.nav-item:before{display:none}.content-header{display:none}.content{padding:4px 4px 16px;gap:12px}.card{padding:12px;border-radius:12px}.card-title{font-size:14px}.stat-grid{gap:8px;grid-template-columns:repeat(2,1fr)}.stat{padding:10px;border-radius:10px}.stat-label{font-size:10px}.stat-value{font-size:16px}.note-grid,.form-grid{grid-template-columns:1fr;gap:10px}.field input,.field textarea,.field select{padding:8px 10px;border-radius:10px;font-size:14px}.btn{padding:8px 12px;font-size:13px}.pill{padding:4px 10px;font-size:12px}.chat-header{flex-direction:column;align-items:stretch;gap:8px}.chat-header__left{flex-direction:column;align-items:stretch}.chat-header__right{justify-content:space-between}.chat-session{min-width:unset;width:100%}.chat-thread{margin-top:8px;padding:10px 8px;border-radius:12px}.chat-msg{max-width:92%}.chat-bubble{padding:8px 10px;border-radius:12px}.chat-compose{gap:8px}.chat-compose__field textarea{min-height:60px;padding:8px 10px;border-radius:12px;font-size:14px}.log-stream{border-radius:10px;max-height:400px}.log-row{grid-template-columns:1fr;gap:4px;padding:8px}.log-time{font-size:10px}.log-level{font-size:9px}.log-subsystem{font-size:11px}.log-message{font-size:12px}.list-item{padding:10px;border-radius:10px}.list-title{font-size:14px}.list-sub{font-size:11px}.code-block{padding:8px;border-radius:10px;font-size:11px}.theme-toggle{--theme-item: 24px;--theme-gap: 4px;--theme-pad: 4px}.theme-icon{width:14px;height:14px}}.chat{position:relative;display:flex;flex-direction:column;flex:1 1 0;height:100%;min-height:0;overflow:hidden;background:transparent!important;border:none!important;box-shadow:none!important}.chat-header{display:flex;justify-content:space-between;align-items:center;gap:12px;flex-wrap:nowrap;flex-shrink:0;padding-bottom:12px;margin-bottom:12px;background:transparent}.chat-header__left{display:flex;align-items:center;gap:12px;flex-wrap:wrap;min-width:0}.chat-header__right{display:flex;align-items:center;gap:8px}.chat-session{min-width:180px}.chat-thread{flex:1 1 0;overflow-y:auto;overflow-x:hidden;padding:12px;margin:0 -12px;min-height:0;border-radius:12px;background:transparent}.chat-focus-exit{position:absolute;top:12px;right:12px;z-index:100;width:32px;height:32px;border-radius:50%;border:1px solid var(--border);background:var(--panel);color:var(--muted);font-size:20px;line-height:1;cursor:pointer;display:flex;align-items:center;justify-content:center;transition:background .15s ease-out,color .15s ease-out,border-color .15s ease-out;box-shadow:0 4px 12px #0003}.chat-focus-exit:hover{background:var(--panel-strong);color:var(--text);border-color:var(--accent)}.chat-compose{position:sticky;bottom:0;flex-shrink:0;display:flex;align-items:flex-end;gap:12px;margin-top:auto;padding:16px 0 4px;background:linear-gradient(to bottom,transparent,var(--bg) 20%);z-index:10}.chat-compose__field{flex:1 1 auto;min-width:0}.chat-compose__field>span{display:none}.chat-compose .chat-compose__field textarea{width:100%;min-height:36px;max-height:150px;padding:8px 12px;border-radius:10px;resize:vertical;white-space:pre-wrap;font-family:var(--font-body);font-size:14px;line-height:1.45}.chat-compose__actions{flex-shrink:0;display:flex;align-items:stretch}.chat-compose .chat-compose__actions .btn{padding:8px 16px;font-size:13px;min-height:36px;white-space:nowrap}.chat-controls{display:flex;align-items:center;justify-content:flex-start;gap:12px;flex-wrap:wrap}.chat-controls__session{min-width:140px}.chat-controls__thinking{display:flex;align-items:center;gap:6px;font-size:13px}.btn--icon{padding:8px!important;min-width:36px;height:36px;display:inline-flex;align-items:center;justify-content:center;border:1px solid var(--border);background:#ffffff0f}.chat-controls__separator{color:#fff6;font-size:18px;margin:0 8px;font-weight:300}:root[data-theme=light] .chat-controls__separator{color:#1018284d}.btn--icon:hover{background:#ffffff1f;border-color:#fff3}:root[data-theme=light] .btn--icon{background:#ffffffe6;border-color:#10182833;box-shadow:0 1px 2px #1018280d;color:#101828b3}:root[data-theme=light] .btn--icon:hover{background:#fff;border-color:#1018284d;color:#101828e6}.btn--icon svg{display:block}.chat-controls__session select{padding:6px 10px;font-size:13px}.chat-controls__thinking{display:flex;align-items:center;gap:4px;font-size:12px;padding:4px 10px;background:#ffffff0a;border-radius:6px;border:1px solid var(--border)}:root[data-theme=light] .chat-controls__thinking{background:#ffffffe6;border-color:#10182826}@media(max-width:640px){.chat-session{min-width:140px}.chat-compose{grid-template-columns:1fr}.chat-controls{flex-wrap:wrap;gap:8px}.chat-controls__session{min-width:120px}}.chat-thinking{margin-bottom:10px;padding:10px 12px;border-radius:10px;border:1px dashed rgba(255,255,255,.18);background:#ffffff0a;color:var(--muted);font-size:12px;line-height:1.4}:root[data-theme=light] .chat-thinking{border-color:#1018282e;background:#10182808}.chat-text{font-size:14px;line-height:1.5;word-wrap:break-word;overflow-wrap:break-word}.chat-text :where(p+p,p+ul,p+ol,p+pre,p+blockquote){margin-top:.75em}.chat-text :where(ul,ol){padding-left:1.5em}.chat-text :where(a){color:var(--accent);text-decoration:underline;text-underline-offset:2px}.chat-text :where(a:hover){opacity:.8}.chat-text :where(code){font-family:var(--mono);font-size:.9em}.chat-text :where(:not(pre)>code){background:#00000026;padding:.15em .4em;border-radius:4px}.chat-text :where(pre){background:#00000026;border-radius:6px;padding:10px 12px;overflow-x:auto}.chat-text :where(pre code){background:none;padding:0}.chat-text :where(blockquote){border-left:3px solid var(--border);padding-left:12px;color:var(--muted)}.chat-text :where(hr){border:none;border-top:1px solid var(--border);margin:1em 0}.chat-group{display:flex;gap:12px;align-items:flex-start;margin-bottom:16px;margin-left:16px;margin-right:16px}.chat-group.user{flex-direction:row-reverse;justify-content:flex-start}.chat-group-messages{display:flex;flex-direction:column;gap:2px;max-width:min(900px,calc(100% - 60px))}.chat-group.user .chat-group-messages{align-items:flex-end}.chat-group.user .chat-group-footer{justify-content:flex-end}.chat-group-footer{display:flex;gap:8px;align-items:baseline;margin-top:6px}.chat-sender-name{font-weight:500;font-size:12px;color:var(--muted)}.chat-group-timestamp{font-size:11px;color:var(--muted);opacity:.7}.chat-avatar{width:40px;height:40px;border-radius:8px;background:var(--panel-strong);display:grid;place-items:center;font-weight:600;font-size:14px;flex-shrink:0;align-self:flex-end;margin-bottom:4px}.chat-avatar.user{background:#f59f4a33;color:#f59f4a}.chat-avatar.assistant{background:#34c7b733;color:#34c7b7}.chat-avatar.other{background:#96969633;color:#969696}.chat-avatar.tool{background:#868e9633;color:#868e96}img.chat-avatar{display:block;object-fit:cover;object-position:center}.chat-bubble{position:relative;display:inline-block;border:1px solid var(--border);background:#0000001f;border-radius:12px;padding:10px 14px;box-shadow:none;transition:background .15s ease-out,border-color .15s ease-out;max-width:100%;word-wrap:break-word}.chat-bubble.has-copy{padding-right:36px}.chat-copy-btn{position:absolute;top:6px;right:8px;border:1px solid var(--border);background:#00000038;color:var(--muted);border-radius:8px;padding:4px 6px;font-size:14px;line-height:1;cursor:pointer;opacity:0;pointer-events:none;transition:opacity .12s ease-out,background .12s ease-out}.chat-copy-btn__icon{display:inline-block;width:1em;text-align:center}.chat-bubble:hover .chat-copy-btn{opacity:1;pointer-events:auto}.chat-copy-btn:hover{background:#0000004d}.chat-copy-btn[data-copying="1"]{opacity:0;pointer-events:none}.chat-copy-btn[data-error="1"]{opacity:1;pointer-events:auto;border-color:#ff453acc;background:#ff453a2e;color:#ff453a}.chat-copy-btn[data-copied="1"]{opacity:1;pointer-events:auto;border-color:#34c7b7cc;background:#34c7b72e;color:#34c7b7}.chat-copy-btn:focus-visible{opacity:1;pointer-events:auto;outline:2px solid var(--accent);outline-offset:2px}@media(hover:none){.chat-copy-btn{opacity:1;pointer-events:auto}}.chat-bubble:hover{background:#0000002e}.chat-group.user .chat-bubble{background:#f59f4a26;border-color:#f59f4a4d}.chat-group.user .chat-bubble:hover{background:#f59f4a38}.chat-bubble.streaming{animation:pulsing-border 1.5s ease-out infinite}@keyframes pulsing-border{0%,to{border-color:var(--border)}50%{border-color:var(--accent)}}.chat-bubble.fade-in{animation:fade-in .2s ease-out}@keyframes fade-in{0%{opacity:0;transform:translateY(4px)}to{opacity:1;transform:translateY(0)}}.chat-tool-card{border:1px solid var(--border);border-radius:8px;padding:12px;margin-top:8px;transition:border-color .15s ease-out,background .15s ease-out;max-height:120px;overflow:hidden}.chat-tool-card:hover{border-color:var(--accent);background:#0000000f}.chat-tool-card:first-child{margin-top:0}.chat-tool-card--clickable{cursor:pointer}.chat-tool-card--clickable:focus{outline:2px solid var(--accent);outline-offset:2px}.chat-tool-card__header{display:flex;justify-content:space-between;align-items:center;gap:8px}.chat-tool-card__title{display:inline-flex;align-items:center;gap:6px;font-weight:600;font-size:13px;line-height:1.2}.chat-tool-card__icon{display:inline-flex;align-items:center;justify-content:center;width:18px;height:18px;font-size:14px;line-height:1;font-family:"Apple Color Emoji","Segoe UI Emoji","Noto Color Emoji",sans-serif;vertical-align:middle;flex-shrink:0}.chat-tool-card__action{font-size:12px;color:var(--accent);opacity:.8;transition:opacity .15s ease-out}.chat-tool-card--clickable:hover .chat-tool-card__action{opacity:1}.chat-tool-card__status{font-size:14px;color:var(--ok)}.chat-tool-card__status-text{font-size:11px;margin-top:4px}.chat-tool-card__detail{font-size:12px;color:var(--muted);margin-top:4px}.chat-tool-card__preview{font-size:11px;color:var(--muted);margin-top:8px;padding:8px 10px;background:#00000014;border-radius:6px;white-space:pre-wrap;overflow:hidden;max-height:44px;line-height:1.4;border:1px solid rgba(255,255,255,.04)}.chat-tool-card--clickable:hover .chat-tool-card__preview{background:#0000001f;border-color:#ffffff14}.chat-tool-card__inline{font-size:11px;color:var(--text);margin-top:6px;padding:6px 8px;background:#0000000f;border-radius:4px;white-space:pre-wrap;word-break:break-word}.chat-reading-indicator{background:transparent;border:1px solid var(--border);padding:12px;display:inline-flex}.chat-reading-indicator__dots{display:flex;gap:6px;align-items:center}.chat-reading-indicator__dots span{width:6px;height:6px;border-radius:50%;background:var(--muted);animation:reading-pulse 1.4s ease-in-out infinite}.chat-reading-indicator__dots span:nth-child(1){animation-delay:0s}.chat-reading-indicator__dots span:nth-child(2){animation-delay:.2s}.chat-reading-indicator__dots span:nth-child(3){animation-delay:.4s}@keyframes reading-pulse{0%,60%,to{opacity:.3;transform:scale(.8)}30%{opacity:1;transform:scale(1)}}.chat-split-container{display:flex;gap:0;flex:1;min-height:0;height:100%}.chat-main{min-width:400px;display:flex;flex-direction:column;overflow:hidden;transition:flex .25s ease-out}.chat-sidebar{flex:1;min-width:300px;border-left:1px solid var(--border);display:flex;flex-direction:column;overflow:hidden;animation:slide-in .2s ease-out}@keyframes slide-in{0%{opacity:0;transform:translate(20px)}to{opacity:1;transform:translate(0)}}.sidebar-panel{display:flex;flex-direction:column;height:100%;background:var(--panel)}.sidebar-header{display:flex;justify-content:space-between;align-items:center;padding:12px 16px;border-bottom:1px solid var(--border);flex-shrink:0;position:sticky;top:0;z-index:10;background:var(--panel)}.sidebar-header .btn{padding:4px 8px;font-size:14px;min-width:auto;line-height:1}.sidebar-title{font-weight:600;font-size:14px}.sidebar-content{flex:1;overflow:auto;padding:16px}.sidebar-markdown{font-size:14px;line-height:1.5}.sidebar-markdown pre{background:#0000001f;border-radius:4px;padding:12px;overflow-x:auto}.sidebar-markdown code{font-family:var(--mono);font-size:13px}@media(max-width:768px){.chat-split-container--open{position:fixed;inset:0;z-index:1000}.chat-split-container--open .chat-main{display:none}.chat-split-container--open .chat-sidebar{width:100%;min-width:0;border-left:none}}.card{border:1px solid var(--border);background:linear-gradient(160deg,rgba(255,255,255,.04),transparent 65%),var(--panel);border-radius:16px;padding:16px;box-shadow:0 18px 36px #00000047;animation:rise .4s ease}.card-title{font-family:var(--font-display);font-size:16px;letter-spacing:.6px;text-transform:uppercase}.card-sub{color:var(--muted);font-size:12px}.stat{background:linear-gradient(140deg,rgba(255,255,255,.04),transparent 70%),var(--panel-strong);border-radius:14px;padding:12px;border:1px solid var(--border-strong)}.stat-label{color:var(--muted);font-size:11px;text-transform:uppercase;letter-spacing:1px}.stat-value{font-size:18px;margin-top:6px}.stat-value.ok{color:var(--ok)}.stat-value.warn{color:var(--warn)}.stat-card{display:grid;gap:6px}.note-title{font-weight:600;letter-spacing:.2px}.status-list{display:grid;gap:8px}.status-list div{display:flex;justify-content:space-between;gap:12px;padding:6px 0;border-bottom:1px dashed rgba(255,255,255,.06)}.status-list div:last-child{border-bottom:none}.account-count{margin-top:8px;font-size:12px;font-weight:600;letter-spacing:.4px;color:var(--muted)}.account-card-list{margin-top:16px;display:grid;gap:10px}.account-card{border:1px solid var(--border);border-radius:10px;padding:12px;background:linear-gradient(160deg,rgba(255,255,255,.06),transparent),#ffffff08}.account-card-header{display:flex;justify-content:space-between;align-items:baseline;gap:12px}.account-card-title{font-weight:600}.account-card-id{font-family:var(--mono);font-size:12px;color:var(--muted)}.account-card-status{margin-top:8px;font-size:13px}.account-card-status div{padding:4px 0}.account-card-error{margin-top:6px;color:var(--danger);font-size:12px}.label{color:var(--muted);font-size:11px;text-transform:uppercase;letter-spacing:.9px}.pill{display:inline-flex;align-items:center;gap:8px;border:1px solid var(--border-strong);padding:6px 12px;border-radius:999px;background:linear-gradient(160deg,rgba(255,255,255,.06),transparent),var(--panel)}.theme-toggle{--theme-item: 28px;--theme-gap: 6px;--theme-pad: 6px;position:relative}.theme-toggle__track{position:relative;display:grid;grid-template-columns:repeat(3,var(--theme-item));gap:var(--theme-gap);padding:var(--theme-pad);border-radius:999px;border:1px solid var(--border-strong);background:#ffffff0a}.theme-toggle__indicator{position:absolute;top:50%;left:var(--theme-pad);width:var(--theme-item);height:var(--theme-item);border-radius:999px;transform:translateY(-50%) translate(calc(var(--theme-index, 0) * (var(--theme-item) + var(--theme-gap))));background:linear-gradient(160deg,rgba(255,255,255,.12),transparent),var(--panel-strong);border:1px solid var(--border-strong);box-shadow:0 8px 16px #00000040;transition:transform .18s ease-out,background .18s ease-out,box-shadow .18s ease-out;z-index:0}.theme-toggle__button{height:var(--theme-item);width:var(--theme-item);display:grid;place-items:center;border:0;border-radius:999px;background:transparent;color:var(--muted);cursor:pointer;position:relative;z-index:1;transition:color .15s ease-out,background .15s ease-out}.theme-toggle__button:hover{color:var(--text);background:#ffffff14}.theme-toggle__button.active{color:var(--text)}.theme-icon{width:16px;height:16px;stroke:currentColor;fill:none;stroke-width:1.75px;stroke-linecap:round;stroke-linejoin:round}.pill.danger{border-color:#ff5c5c80;color:var(--danger)}.statusDot{width:8px;height:8px;border-radius:999px;background:var(--danger);box-shadow:0 0 0 2px #00000040}.statusDot.ok{background:var(--ok);box-shadow:0 0 0 2px #00000040,0 0 10px #2bd97f66}.btn{border:1px solid var(--border-strong);background:#ffffff0a;padding:8px 14px;border-radius:999px;cursor:pointer;transition:transform .15s ease,border-color .15s ease,background .15s ease}.btn:hover{background:#ffffff1a;transform:translateY(-1px)}.btn.primary{border-color:#f59f4a73;background:#f59f4a33}.btn.active{border-color:#f59f4a8c;background:#f59f4a29}.btn.danger{border-color:#ff6b6b73;background:#ff6b6b2e}.btn--sm{padding:5px 10px;font-size:12px}.btn:disabled{opacity:.5;cursor:not-allowed;transform:none}.field{display:grid;gap:6px}.field.full{grid-column:1 / -1}.field span{color:var(--muted);font-size:11px;letter-spacing:.4px}.field input,.field textarea,.field select{border:1px solid var(--border-strong);background:#00000038;border-radius:12px;padding:9px 11px;outline:none;transition:border-color .15s ease,box-shadow .15s ease,background .15s ease}.field input:focus,.field textarea:focus,.field select:focus{border-color:var(--accent);box-shadow:0 0 0 3px var(--focus);background:#00000047}.field select{appearance:none;padding-right:38px;background-color:var(--panel-strong);background-image:linear-gradient(45deg,transparent 50%,var(--muted) 50%),linear-gradient(135deg,var(--muted) 50%,transparent 50%),linear-gradient(to right,transparent,transparent);background-position:calc(100% - 18px) 50%,calc(100% - 12px) 50%,calc(100% - 38px) 50%;background-size:6px 6px,6px 6px,1px 60%;background-repeat:no-repeat;box-shadow:inset 0 1px #ffffff0a}.field textarea{font-family:var(--mono);min-height:180px;resize:vertical;white-space:pre}.field textarea:focus{background:#00000052}.field.checkbox{grid-template-columns:auto 1fr;align-items:center}.config-form .field.checkbox{grid-template-columns:18px minmax(0,1fr);column-gap:10px}.config-form .field.checkbox input[type=checkbox]{margin:0}.form-grid{display:grid;gap:12px;grid-template-columns:repeat(auto-fit,minmax(200px,1fr))}:root[data-theme=light] .field input,:root[data-theme=light] .field textarea,:root[data-theme=light] .field select{background:#fff;border-color:#10182840;box-shadow:0 1px 2px #1018280f}:root[data-theme=light] .field input:focus,:root[data-theme=light] .field textarea:focus,:root[data-theme=light] .field select:focus{background:#fff}:root[data-theme=light] .btn{background:#ffffffe6;border-color:#10182833;box-shadow:0 1px 2px #1018280d}:root[data-theme=light] .btn:hover{background:#fff;border-color:#1018284d}:root[data-theme=light] .btn.primary{background:#f59f4a26}:root[data-theme=light] .btn.active{background:#f59f4a1f}.muted{color:var(--muted)}.mono{font-family:var(--mono)}.callout{padding:10px 12px;border-radius:14px;background:linear-gradient(160deg,rgba(255,255,255,.06),transparent),#ffffff08;border:1px solid var(--border)}.callout.danger{border-color:#ff5c5c66;color:var(--danger)}.code-block{font-family:var(--mono);font-size:12px;background:#00000059;padding:10px;border-radius:12px;border:1px solid var(--border);max-height:360px;overflow:auto}:root[data-theme=light] .code-block,:root[data-theme=light] .list-item,:root[data-theme=light] .table-row,:root[data-theme=light] .chip{background:#ffffffd9}.list{display:grid;gap:12px;container-type:inline-size}.list-item{display:grid;grid-template-columns:minmax(0,1fr) minmax(220px,260px);gap:14px;align-items:start;border:1px solid var(--border);border-radius:14px;padding:12px;background:#0003}.list-item-clickable{cursor:pointer;transition:border-color .15s ease,box-shadow .15s ease}.list-item-clickable:hover{border-color:var(--border-strong)}.list-item-selected{border-color:var(--accent);box-shadow:0 0 0 1px var(--focus)}.list-main{display:grid;gap:6px;min-width:0}.list-title{font-weight:600}.list-sub{color:var(--muted);font-size:12px}.list-meta{text-align:right;color:var(--muted);font-size:11px;display:grid;gap:4px;min-width:220px}.list-meta .btn{padding:6px 10px}.list-meta .field input,.list-meta .field textarea,.list-meta .field select{width:100%}@container (max-width: 560px){.list-item{grid-template-columns:1fr}.list-meta{min-width:0;text-align:left}}.chip-row{display:flex;flex-wrap:wrap;gap:6px}.chip{font-size:11px;border:1px solid var(--border);border-radius:999px;padding:4px 8px;color:var(--muted);background:#0003}.chip input{margin-right:6px}.chip-ok{color:var(--ok);border-color:#1bd98a66}.chip-warn{color:var(--warn);border-color:#f2c94c66}.table{display:grid;gap:8px}.table-head,.table-row{display:grid;grid-template-columns:1.4fr 1fr .8fr .7fr .8fr .8fr .8fr .8fr .6fr;gap:12px;align-items:center}.table-head{font-size:11px;text-transform:uppercase;letter-spacing:.8px;color:var(--muted)}.table-row{border:1px solid var(--border);padding:10px;border-radius:12px;background:#0003}.session-link{text-decoration:none;color:var(--accent)}.session-link:hover{text-decoration:underline}.log-stream{border:1px solid var(--border);border-radius:14px;background:#0003;max-height:520px;overflow:auto;container-type:inline-size}.log-row{display:grid;grid-template-columns:90px 70px minmax(140px,200px) minmax(0,1fr);gap:12px;align-items:start;padding:6px 10px;border-bottom:1px solid var(--border);font-size:12px}.log-row:last-child{border-bottom:none}.log-time{color:var(--muted)}.log-level{text-transform:uppercase;font-size:10px;font-weight:600;border:1px solid var(--border);border-radius:999px;padding:2px 6px;width:fit-content}.log-level.trace,.log-level.debug{color:var(--muted)}.log-level.info{color:var(--info);border-color:#4c96f266}.log-level.warn{color:var(--warn);border-color:#f2c94c66}.log-level.error,.log-level.fatal{color:var(--danger);border-color:#ff5c5c66}.log-chip.trace,.log-chip.debug{color:var(--muted)}.log-chip.info{color:var(--info);border-color:#4c96f266}.log-chip.warn{color:var(--warn);border-color:#f2c94c66}.log-chip.error,.log-chip.fatal{color:var(--danger);border-color:#ff5c5c66}.log-subsystem{color:var(--muted)}.log-message{white-space:pre-wrap;word-break:break-word}@container (max-width: 620px){.log-row{grid-template-columns:70px 60px minmax(0,1fr)}.log-subsystem{display:none}}.chat{display:flex;flex-direction:column;min-height:0}.shell--chat .chat{flex:1}.chat-header{display:flex;justify-content:space-between;align-items:flex-end;gap:12px;flex-wrap:wrap}.chat-header__left{display:flex;align-items:flex-end;gap:12px;flex-wrap:wrap;min-width:0}.chat-header__right{display:flex;align-items:center;gap:10px}.chat-session{min-width:240px}.chat-thread{margin-top:12px;display:flex;flex-direction:column;gap:12px;flex:1;min-height:0;overflow-y:auto;overflow-x:hidden;padding:14px 12px;min-width:0;border-radius:0;border:none;background:transparent}:root[data-theme=light] .chat-thread{background:transparent}.chat-queue{margin-top:12px;padding:10px 12px;border-radius:16px;border:1px solid var(--border);background:#0000002e;display:grid;gap:8px}:root[data-theme=light] .chat-queue{background:#1018280a}.chat-queue__title{font-family:var(--font-mono);font-size:12px;color:var(--muted)}.chat-queue__list{display:grid;gap:8px}.chat-queue__item{display:grid;grid-template-columns:minmax(0,1fr) auto;align-items:start;gap:10px;padding:8px 10px;border-radius:12px;border:1px dashed var(--border);background:#0003}:root[data-theme=light] .chat-queue__item{background:#1018280d}.chat-queue__text{color:var(--chat-text);font-size:13px;line-height:1.4;white-space:pre-wrap;overflow:hidden;display:-webkit-box;-webkit-line-clamp:3;-webkit-box-orient:vertical}.chat-queue__remove{align-self:start;padding:4px 10px;font-size:12px;line-height:1}.chat-line{display:flex}.chat-line.user{justify-content:flex-end}.chat-line.assistant,.chat-line.other{justify-content:flex-start}.chat-msg{display:grid;gap:6px;max-width:min(720px,82%)}.chat-line.user .chat-msg{justify-items:end}.chat-bubble{border:1px solid var(--border);background:#0000003d;border-radius:16px;padding:10px 12px;min-width:0;box-shadow:0 12px 22px #0000003d}:root[data-theme=light] .chat-bubble{background:#ffffffd9;box-shadow:0 12px 26px #10182814}.chat-line.user .chat-bubble{border-color:#f59f4a73;background:linear-gradient(135deg,#f59f4a42,#f59f4a1f)}.chat-line.assistant .chat-bubble{border-color:#34c7b733;background:linear-gradient(135deg,#34c7b71f,#0000003d)}:root[data-theme=light] .chat-line.assistant .chat-bubble{background:linear-gradient(135deg,#1bb9b11f,#ffffffd9)}@keyframes chatStreamPulse{0%{box-shadow:0 12px 22px #0000003d,0 0 #34c7b700}60%{box-shadow:0 12px 22px #0000003d,0 0 0 6px #34c7b714}to{box-shadow:0 12px 22px #0000003d,0 0 #34c7b700}}.chat-bubble.streaming{border-color:#34c7b766;animation:chatStreamPulse 1.6s ease-in-out infinite}@media(prefers-reduced-motion:reduce){.chat-bubble.streaming{animation:none}}.chat-bubble.chat-reading-indicator{width:fit-content;padding:10px 14px}.chat-reading-indicator__dots{display:inline-flex;align-items:center;gap:6px;height:10px}.chat-reading-indicator__dots>span{display:inline-block;width:6px;height:6px;border-radius:999px;background:var(--chat-text);opacity:.55;transform:translateY(0);animation:chatReadingDot 1.1s ease-in-out infinite;will-change:transform,opacity}.chat-reading-indicator__dots>span:nth-child(2){animation-delay:.12s}.chat-reading-indicator__dots>span:nth-child(3){animation-delay:.24s}@keyframes chatReadingDot{0%,80%,to{opacity:.38;transform:translateY(0) scale(.92)}40%{opacity:1;transform:translateY(-3px) scale(1.18)}}@media(prefers-reduced-motion:reduce){.chat-reading-indicator__dots>span{animation:none;opacity:.75}}.chat-text{overflow-wrap:anywhere;word-break:break-word;color:var(--chat-text);line-height:1.5}.chat-text :where(p,ul,ol,pre,blockquote,table){margin:0}.chat-text :where(p+p,p+ul,p+ol,p+pre,p+blockquote,p+table){margin-top:.75em}.chat-text :where(ul,ol){padding-left:1.1em}.chat-text :where(li+li){margin-top:.25em}.chat-text :where(a){color:var(--accent);text-decoration-thickness:2px;text-underline-offset:2px}.chat-text :where(a:hover){text-decoration-thickness:3px}.chat-text :where(blockquote){border-left:2px solid rgba(255,255,255,.14);padding-left:12px;color:var(--muted)}:root[data-theme=light] .chat-text :where(blockquote){border-left-color:#10182829}.chat-text :where(hr){border:0;border-top:1px solid var(--border);opacity:.6;margin:.9em 0}.chat-text :where(code){font-family:var(--font-mono);font-size:.92em}.chat-text :where(:not(pre)>code){padding:.15em .35em;border-radius:8px;border:1px solid var(--border);background:#0003}:root[data-theme=light] .chat-text :where(:not(pre)>code){background:#1018280d}.chat-text :where(pre){margin-top:.75em;padding:10px 12px;border-radius:14px;border:1px solid var(--border);background:#00000038;overflow:auto}:root[data-theme=light] .chat-text :where(pre){background:#1018280a}.chat-text :where(pre code){font-size:12px;white-space:pre}.chat-text :where(table){margin-top:.75em;border-collapse:collapse;width:100%;font-size:12px}.chat-text :where(th,td){border:1px solid var(--border);padding:6px 8px;vertical-align:top}.chat-text :where(th){font-family:var(--font-mono);font-weight:600;color:var(--muted)}.chat-tool-card{margin-top:8px;padding:8px 10px;border-radius:12px;border:1px solid var(--border);background:#00000038;display:grid;gap:4px}:root[data-theme=light] .chat-tool-card{background:#ffffffb3}.chat-tool-card__title{font-family:var(--font-mono);font-size:12px;color:var(--chat-text)}.chat-tool-card__detail{font-family:var(--font-mono);font-size:11px;color:var(--muted)}.chat-tool-card__details{margin-top:6px}.chat-tool-card__summary{font-family:var(--font-mono);font-size:11px;color:var(--muted);cursor:pointer;list-style:none;display:inline-flex;align-items:center;gap:6px}.chat-tool-card__summary::-webkit-details-marker{display:none}.chat-tool-card__summary-meta{color:var(--muted);opacity:.8}.chat-tool-card__details[open] .chat-tool-card__summary{color:var(--chat-text)}.chat-tool-card__output{margin-top:6px;font-family:var(--font-mono);font-size:11px;line-height:1.45;white-space:pre-wrap;color:var(--chat-text);padding:8px;border-radius:10px;border:1px solid var(--border);background:#0003}:root[data-theme=light] .chat-tool-card__output{background:#1018280d}.chat-stamp{font-size:11px;color:var(--muted)}.chat-line.user .chat-stamp{text-align:right}.chat-compose{margin-top:12px;display:grid;grid-template-columns:minmax(0,1fr) auto;align-items:end;gap:10px}.shell--chat .chat-compose{position:sticky;bottom:0;z-index:5;margin-top:0;padding-top:12px;background:linear-gradient(180deg,rgba(0,0,0,0) 0%,var(--panel) 35%)}.shell--chat-focus .chat-compose{bottom:calc(var(--shell-pad) + 8px);padding-bottom:calc(14px + env(safe-area-inset-bottom,0px));border-bottom-left-radius:18px;border-bottom-right-radius:18px}.chat-compose__field{gap:4px}.chat-compose__field textarea{min-height:72px;padding:10px 12px;border-radius:16px;resize:vertical;white-space:pre-wrap;font-family:var(--font-body);line-height:1.45}.chat-compose__field textarea:disabled{opacity:.7;cursor:not-allowed}.chat-compose__actions{justify-content:flex-end;align-self:end}@media(max-width:900px){.chat-session{min-width:200px}.chat-compose{grid-template-columns:1fr}}.qr-wrap{margin-top:12px;border-radius:14px;background:#0003;border:1px dashed rgba(255,255,255,.18);padding:12px;display:inline-flex}.qr-wrap img{width:180px;height:180px;border-radius:10px;image-rendering:pixelated}.exec-approval-overlay{position:fixed;inset:0;background:#080c12b3;-webkit-backdrop-filter:blur(6px);backdrop-filter:blur(6px);display:flex;align-items:center;justify-content:center;padding:24px;z-index:200}.exec-approval-card{width:min(560px,100%);background:var(--panel-strong);border:1px solid var(--border-strong);border-radius:18px;padding:20px;box-shadow:0 28px 60px #00000059;animation:rise .25s ease}.exec-approval-header{display:flex;align-items:center;justify-content:space-between;gap:12px}.exec-approval-title{font-family:var(--font-display);font-size:14px;letter-spacing:.8px;text-transform:uppercase}.exec-approval-sub{color:var(--muted);font-size:12px}.exec-approval-queue{font-size:11px;text-transform:uppercase;letter-spacing:1px;color:var(--muted);border:1px solid var(--border);border-radius:999px;padding:4px 10px}.exec-approval-command{margin-top:12px;padding:10px 12px;background:#00000040;border:1px solid var(--border);border-radius:12px;word-break:break-word;white-space:pre-wrap}.exec-approval-meta{margin-top:12px;display:grid;gap:6px;font-size:12px;color:var(--muted)}.exec-approval-meta-row{display:flex;justify-content:space-between;gap:12px}.exec-approval-meta-row span:last-child{color:var(--text);font-family:var(--mono)}.exec-approval-error{margin-top:10px;font-size:12px;color:var(--danger)}.exec-approval-actions{margin-top:16px;display:flex;flex-wrap:wrap;gap:10px}.config-layout{display:grid;grid-template-columns:240px minmax(0,1fr);gap:0;min-height:calc(100vh - 140px);margin:-16px;border-radius:16px;overflow:hidden;border:1px solid var(--border);background:var(--panel)}.config-sidebar{display:flex;flex-direction:column;background:#0003;border-right:1px solid var(--border)}:root[data-theme=light] .config-sidebar{background:#00000008}.config-sidebar__header{display:flex;align-items:center;justify-content:space-between;padding:16px;border-bottom:1px solid var(--border)}.config-sidebar__title{font-weight:600;font-size:14px;letter-spacing:.3px}.config-sidebar__footer{margin-top:auto;padding:12px;border-top:1px solid var(--border)}.config-search{position:relative;padding:12px;border-bottom:1px solid var(--border)}.config-search__icon{position:absolute;left:24px;top:50%;transform:translateY(-50%);width:16px;height:16px;color:var(--muted);pointer-events:none}.config-search__input{width:100%;padding:10px 32px 10px 40px;border:1px solid var(--border);border-radius:8px;background:#00000026;font-size:13px;outline:none;transition:border-color .15s ease,box-shadow .15s ease,background .15s ease}.config-search__input::placeholder{color:var(--muted)}.config-search__input:focus{border-color:var(--accent);box-shadow:0 0 0 3px var(--focus);background:#0003}:root[data-theme=light] .config-search__input{background:#fffc}:root[data-theme=light] .config-search__input:focus{background:#fff}.config-search__clear{position:absolute;right:20px;top:50%;transform:translateY(-50%);width:20px;height:20px;border:none;border-radius:50%;background:#ffffff1a;color:var(--muted);font-size:16px;line-height:1;cursor:pointer;display:flex;align-items:center;justify-content:center;transition:background .15s ease,color .15s ease}.config-search__clear:hover{background:#fff3;color:var(--text)}.config-nav{flex:1;overflow-y:auto;padding:8px}.config-nav__item{display:flex;align-items:center;gap:12px;width:100%;padding:10px 12px;border:none;border-radius:8px;background:transparent;color:var(--muted);font-size:13px;font-weight:500;text-align:left;cursor:pointer;transition:background .15s ease,color .15s ease}.config-nav__item:hover{background:#ffffff0d;color:var(--text)}:root[data-theme=light] .config-nav__item:hover{background:#0000000d}.config-nav__item.active{background:#f59f4a1f;color:var(--accent)}.config-nav__icon{width:20px;height:20px;display:flex;align-items:center;justify-content:center;font-size:14px}.config-nav__icon svg{width:18px;height:18px;stroke:currentColor;fill:none}.config-nav__label{flex:1;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.config-mode-toggle{display:flex;padding:3px;background:#0003;border-radius:8px;border:1px solid var(--border)}:root[data-theme=light] .config-mode-toggle{background:#0000000f}.config-mode-toggle__btn{flex:1;padding:8px 12px;border:none;border-radius:6px;background:transparent;color:var(--muted);font-size:12px;font-weight:600;cursor:pointer;transition:background .15s ease,color .15s ease,box-shadow .15s ease}.config-mode-toggle__btn:hover{color:var(--text)}.config-mode-toggle__btn.active{background:#ffffff1a;color:var(--text);box-shadow:0 1px 3px #0003}:root[data-theme=light] .config-mode-toggle__btn.active{background:#fff;box-shadow:0 1px 3px #0000001a}.config-main{display:flex;flex-direction:column;min-width:0;background:var(--panel)}.config-actions{display:flex;align-items:center;justify-content:space-between;gap:12px;padding:12px 20px;background:#00000014;border-bottom:1px solid var(--border)}:root[data-theme=light] .config-actions{background:#00000005}.config-actions__left,.config-actions__right{display:flex;align-items:center;gap:8px}.config-changes-badge{padding:5px 12px;border-radius:999px;background:#f59f4a26;border:1px solid rgba(245,159,74,.3);color:var(--accent);font-size:12px;font-weight:600}.config-status{font-size:13px;color:var(--muted)}.config-diff{margin:16px 20px 0;border:1px solid rgba(245,159,74,.3);border-radius:10px;background:#f59f4a0d;overflow:hidden}.config-diff__summary{display:flex;align-items:center;justify-content:space-between;padding:12px 16px;cursor:pointer;font-size:13px;font-weight:600;color:var(--accent);list-style:none}.config-diff__summary::-webkit-details-marker{display:none}.config-diff__chevron{width:16px;height:16px;transition:transform .2s ease}.config-diff__chevron svg{width:100%;height:100%}.config-diff[open] .config-diff__chevron{transform:rotate(180deg)}.config-diff__content{padding:0 16px 16px;display:grid;gap:8px}.config-diff__item{display:flex;align-items:baseline;gap:12px;padding:8px 12px;border-radius:6px;background:#0000001a;font-size:12px;font-family:var(--mono)}:root[data-theme=light] .config-diff__item{background:#fff9}.config-diff__path{font-weight:600;color:var(--text);flex-shrink:0}.config-diff__values{display:flex;align-items:baseline;gap:8px;min-width:0;flex-wrap:wrap}.config-diff__from{color:var(--danger);opacity:.8}.config-diff__arrow{color:var(--muted)}.config-diff__to{color:var(--ok)}.config-section-hero{display:flex;align-items:center;gap:14px;padding:14px 20px;border-bottom:1px solid var(--border);background:#0000000a}:root[data-theme=light] .config-section-hero{background:#00000004}.config-section-hero__icon{width:28px;height:28px;color:var(--accent);display:flex;align-items:center;justify-content:center}.config-section-hero__icon svg{width:100%;height:100%;stroke:currentColor;fill:none}.config-section-hero__text{display:grid;gap:2px;min-width:0}.config-section-hero__title{font-size:15px;font-weight:600}.config-section-hero__desc{font-size:12px;color:var(--muted)}.config-subnav{display:flex;gap:8px;padding:10px 20px 12px;border-bottom:1px solid var(--border);background:#00000008;overflow-x:auto}:root[data-theme=light] .config-subnav{background:#00000005}.config-subnav__item{border:1px solid transparent;border-radius:999px;padding:6px 12px;font-size:12px;font-weight:600;color:var(--muted);background:#0000001f;cursor:pointer;transition:background .15s ease,color .15s ease,border-color .15s ease;white-space:nowrap}:root[data-theme=light] .config-subnav__item{background:#0000000f}.config-subnav__item:hover{color:var(--text);background:#ffffff14}:root[data-theme=light] .config-subnav__item:hover{background:#00000014}.config-subnav__item.active{color:var(--accent);border-color:#f59f4a66;background:#f59f4a1f}.config-content{flex:1;overflow-y:auto;padding:20px}.config-raw-field textarea{min-height:500px;font-family:var(--mono);font-size:13px;line-height:1.5}.config-loading{display:flex;flex-direction:column;align-items:center;justify-content:center;gap:16px;padding:80px 20px;color:var(--muted)}.config-loading__spinner{width:36px;height:36px;border:3px solid var(--border);border-top-color:var(--accent);border-radius:50%;animation:spin .8s linear infinite}@keyframes spin{to{transform:rotate(360deg)}}.config-empty{display:flex;flex-direction:column;align-items:center;justify-content:center;gap:16px;padding:80px 20px;text-align:center}.config-empty__icon{font-size:56px;opacity:.4}.config-empty__text{color:var(--muted);font-size:15px}.config-form--modern{display:grid;gap:24px}.config-section-card{border:1px solid var(--border);border-radius:12px;background:#ffffff05;overflow:hidden}:root[data-theme=light] .config-section-card{background:#ffffff80}.config-section-card__header{display:flex;align-items:flex-start;gap:14px;padding:18px 20px;background:#0000000f;border-bottom:1px solid var(--border)}:root[data-theme=light] .config-section-card__header{background:#00000005}.config-section-card__icon{width:32px;height:32px;color:var(--accent);flex-shrink:0}.config-section-card__icon svg{width:100%;height:100%}.config-section-card__titles{flex:1;min-width:0}.config-section-card__title{margin:0;font-size:17px;font-weight:600}.config-section-card__desc{margin:4px 0 0;font-size:13px;color:var(--muted);line-height:1.4}.config-section-card__content{padding:20px}.cfg-fields{display:grid;gap:20px}.cfg-field{display:grid;gap:6px}.cfg-field--error{padding:12px;border-radius:8px;background:#ff5c5c1a;border:1px solid rgba(255,92,92,.3)}.cfg-field__label{font-size:13px;font-weight:600;color:var(--text)}.cfg-field__help{font-size:12px;color:var(--muted);line-height:1.4}.cfg-field__error{font-size:12px;color:var(--danger)}.cfg-input-wrap{display:flex;gap:8px}.cfg-input{flex:1;padding:10px 12px;border:1px solid var(--border);border-radius:8px;background:#0000001f;font-size:14px;outline:none;transition:border-color .15s ease,box-shadow .15s ease,background .15s ease}.cfg-input::placeholder{color:var(--muted);opacity:.7}.cfg-input:focus{border-color:var(--accent);box-shadow:0 0 0 3px var(--focus);background:#0000002e}:root[data-theme=light] .cfg-input{background:#fff}:root[data-theme=light] .cfg-input:focus{background:#fff}.cfg-input--sm{padding:8px 10px;font-size:13px}.cfg-input__reset{padding:8px 12px;border:1px solid var(--border);border-radius:8px;background:#ffffff0d;color:var(--muted);font-size:14px;cursor:pointer;transition:background .15s ease,color .15s ease}.cfg-input__reset:hover:not(:disabled){background:#ffffff1a;color:var(--text)}.cfg-input__reset:disabled{opacity:.5;cursor:not-allowed}.cfg-textarea{width:100%;padding:10px 12px;border:1px solid var(--border);border-radius:8px;background:#0000001f;font-family:var(--mono);font-size:13px;line-height:1.5;resize:vertical;outline:none;transition:border-color .15s ease,box-shadow .15s ease}.cfg-textarea:focus{border-color:var(--accent);box-shadow:0 0 0 3px var(--focus)}:root[data-theme=light] .cfg-textarea{background:#fff}.cfg-textarea--sm{padding:8px 10px;font-size:12px}.cfg-number{display:inline-flex;border:1px solid var(--border);border-radius:8px;overflow:hidden;background:#0000001f}:root[data-theme=light] .cfg-number{background:#fff}.cfg-number__btn{width:40px;border:none;background:#ffffff0d;color:var(--text);font-size:18px;font-weight:300;cursor:pointer;transition:background .15s ease}.cfg-number__btn:hover:not(:disabled){background:#ffffff1a}.cfg-number__btn:disabled{opacity:.4;cursor:not-allowed}:root[data-theme=light] .cfg-number__btn{background:#00000008}:root[data-theme=light] .cfg-number__btn:hover:not(:disabled){background:#0000000f}.cfg-number__input{width:80px;padding:10px;border:none;border-left:1px solid var(--border);border-right:1px solid var(--border);background:transparent;font-size:14px;text-align:center;outline:none;-moz-appearance:textfield}.cfg-number__input::-webkit-outer-spin-button,.cfg-number__input::-webkit-inner-spin-button{-webkit-appearance:none;margin:0}.cfg-select{padding:10px 36px 10px 12px;border:1px solid var(--border);border-radius:8px;background-color:#0000001f;background-image:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' viewBox='0 0 24 24' fill='none' stroke='%23888' stroke-width='2'%3E%3Cpolyline points='6 9 12 15 18 9'%3E%3C/polyline%3E%3C/svg%3E");background-repeat:no-repeat;background-position:right 10px center;font-size:14px;cursor:pointer;outline:none;appearance:none;transition:border-color .15s ease,box-shadow .15s ease}.cfg-select:focus{border-color:var(--accent);box-shadow:0 0 0 3px var(--focus)}:root[data-theme=light] .cfg-select{background-color:#fff}.cfg-segmented{display:inline-flex;padding:3px;border:1px solid var(--border);border-radius:8px;background:#0000001f}:root[data-theme=light] .cfg-segmented{background:#0000000a}.cfg-segmented__btn{padding:8px 16px;border:none;border-radius:6px;background:transparent;color:var(--muted);font-size:13px;font-weight:500;cursor:pointer;transition:background .15s ease,color .15s ease,box-shadow .15s ease}.cfg-segmented__btn:hover:not(:disabled):not(.active){color:var(--text)}.cfg-segmented__btn.active{background:#ffffff1f;color:var(--text);box-shadow:0 1px 3px #0003}:root[data-theme=light] .cfg-segmented__btn.active{background:#fff;box-shadow:0 1px 3px #0000001a}.cfg-segmented__btn:disabled{opacity:.5;cursor:not-allowed}.cfg-toggle-row{display:flex;align-items:center;justify-content:space-between;gap:16px;padding:14px 16px;border:1px solid var(--border);border-radius:10px;background:#0000000f;cursor:pointer;transition:background .15s ease,border-color .15s ease}.cfg-toggle-row:hover:not(.disabled){background:#0000001a;border-color:var(--border-strong)}.cfg-toggle-row.disabled{opacity:.6;cursor:not-allowed}:root[data-theme=light] .cfg-toggle-row{background:#ffffff80}:root[data-theme=light] .cfg-toggle-row:hover:not(.disabled){background:#fffc}.cfg-toggle-row__content{flex:1;min-width:0}.cfg-toggle-row__label{display:block;font-size:14px;font-weight:500;color:var(--text)}.cfg-toggle-row__help{display:block;margin-top:2px;font-size:12px;color:var(--muted);line-height:1.4}.cfg-toggle{position:relative;flex-shrink:0}.cfg-toggle input{position:absolute;opacity:0;width:0;height:0}.cfg-toggle__track{display:block;width:48px;height:28px;background:#ffffff1f;border:1px solid var(--border);border-radius:999px;position:relative;transition:background .2s ease,border-color .2s ease}:root[data-theme=light] .cfg-toggle__track{background:#0000001a}.cfg-toggle__track:after{content:"";position:absolute;top:3px;left:3px;width:20px;height:20px;background:var(--text);border-radius:50%;box-shadow:0 2px 4px #0000004d;transition:transform .2s ease,background .2s ease}.cfg-toggle input:checked+.cfg-toggle__track{background:#2bd97f40;border-color:#2bd97f80}.cfg-toggle input:checked+.cfg-toggle__track:after{transform:translate(20px);background:var(--ok)}.cfg-toggle input:focus+.cfg-toggle__track{box-shadow:0 0 0 3px var(--focus)}.cfg-object{border:1px solid var(--border);border-radius:10px;background:#0000000a;overflow:hidden}:root[data-theme=light] .cfg-object{background:#fff6}.cfg-object__header{display:flex;align-items:center;justify-content:space-between;padding:12px 16px;cursor:pointer;list-style:none;transition:background .15s ease}.cfg-object__header:hover{background:#ffffff08}:root[data-theme=light] .cfg-object__header:hover{background:#00000005}.cfg-object__header::-webkit-details-marker{display:none}.cfg-object__title{font-size:14px;font-weight:600;color:var(--text)}.cfg-object__chevron{width:18px;height:18px;color:var(--muted);transition:transform .2s ease}.cfg-object__chevron svg{width:100%;height:100%}.cfg-object[open] .cfg-object__chevron{transform:rotate(180deg)}.cfg-object__help{padding:0 16px 12px;font-size:12px;color:var(--muted);border-bottom:1px solid var(--border)}.cfg-object__content{padding:16px;display:grid;gap:16px}.cfg-array{border:1px solid var(--border);border-radius:10px;overflow:hidden}.cfg-array__header{display:flex;align-items:center;gap:12px;padding:12px 16px;background:#0000000f;border-bottom:1px solid var(--border)}:root[data-theme=light] .cfg-array__header{background:#00000005}.cfg-array__label{flex:1;font-size:14px;font-weight:600;color:var(--text)}.cfg-array__count{font-size:12px;color:var(--muted);padding:3px 8px;background:#ffffff0f;border-radius:999px}:root[data-theme=light] .cfg-array__count{background:#0000000f}.cfg-array__add{display:inline-flex;align-items:center;gap:6px;padding:6px 12px;border:1px solid var(--border);border-radius:6px;background:#ffffff0d;color:var(--text);font-size:12px;font-weight:500;cursor:pointer;transition:background .15s ease}.cfg-array__add:hover:not(:disabled){background:#ffffff1a}.cfg-array__add:disabled{opacity:.5;cursor:not-allowed}.cfg-array__add-icon{width:14px;height:14px}.cfg-array__add-icon svg{width:100%;height:100%}.cfg-array__help{padding:10px 16px;font-size:12px;color:var(--muted);border-bottom:1px solid var(--border)}.cfg-array__empty{padding:32px 16px;text-align:center;color:var(--muted);font-size:13px}.cfg-array__items{display:grid;gap:1px;background:var(--border)}.cfg-array__item{background:var(--panel)}.cfg-array__item-header{display:flex;align-items:center;justify-content:space-between;padding:10px 16px;background:#0000000a;border-bottom:1px solid var(--border)}:root[data-theme=light] .cfg-array__item-header{background:#00000005}.cfg-array__item-index{font-size:11px;font-weight:600;color:var(--muted);text-transform:uppercase;letter-spacing:.5px}.cfg-array__item-remove{width:28px;height:28px;display:flex;align-items:center;justify-content:center;border:none;border-radius:6px;background:transparent;color:var(--muted);cursor:pointer;transition:background .15s ease,color .15s ease}.cfg-array__item-remove svg{width:16px;height:16px}.cfg-array__item-remove:hover:not(:disabled){background:#ff5c5c26;color:var(--danger)}.cfg-array__item-remove:disabled{opacity:.4;cursor:not-allowed}.cfg-array__item-content{padding:16px}.cfg-map{border:1px solid var(--border);border-radius:10px;overflow:hidden}.cfg-map__header{display:flex;align-items:center;justify-content:space-between;gap:12px;padding:12px 16px;background:#0000000f;border-bottom:1px solid var(--border)}:root[data-theme=light] .cfg-map__header{background:#00000005}.cfg-map__label{font-size:13px;font-weight:600;color:var(--muted)}.cfg-map__add{display:inline-flex;align-items:center;gap:6px;padding:6px 12px;border:1px solid var(--border);border-radius:6px;background:#ffffff0d;color:var(--text);font-size:12px;font-weight:500;cursor:pointer;transition:background .15s ease}.cfg-map__add:hover:not(:disabled){background:#ffffff1a}.cfg-map__add-icon{width:14px;height:14px}.cfg-map__add-icon svg{width:100%;height:100%}.cfg-map__empty{padding:24px 16px;text-align:center;color:var(--muted);font-size:13px}.cfg-map__items{display:grid;gap:8px;padding:12px}.cfg-map__item{display:grid;grid-template-columns:140px 1fr auto;gap:8px;align-items:start}.cfg-map__item-key,.cfg-map__item-value{min-width:0}.cfg-map__item-remove{width:32px;height:32px;display:flex;align-items:center;justify-content:center;border:none;border-radius:6px;background:transparent;color:var(--muted);cursor:pointer;transition:background .15s ease,color .15s ease}.cfg-map__item-remove svg{width:16px;height:16px}.cfg-map__item-remove:hover:not(:disabled){background:#ff5c5c26;color:var(--danger)}.pill--sm{padding:4px 10px;font-size:11px}.pill--ok{border-color:#2bd97f66;color:var(--ok)}.pill--danger{border-color:#ff5c5c66;color:var(--danger)}@media(max-width:768px){.config-layout{grid-template-columns:1fr}.config-sidebar{border-right:none;border-bottom:1px solid var(--border)}.config-sidebar__header{padding:12px 16px}.config-nav{display:flex;flex-wrap:nowrap;gap:4px;padding:8px 12px;overflow-x:auto;-webkit-overflow-scrolling:touch}.config-nav__item{flex:0 0 auto;padding:8px 12px;white-space:nowrap}.config-nav__label{display:inline}.config-sidebar__footer{display:none}.config-actions{flex-wrap:wrap;padding:12px 16px}.config-actions__left,.config-actions__right{width:100%;justify-content:center}.config-section-hero{padding:12px 16px}.config-subnav{padding:8px 16px 10px}.config-content{padding:16px}.config-section-card__header{padding:14px 16px}.config-section-card__content{padding:16px}.cfg-toggle-row{padding:12px 14px}.cfg-map__item{grid-template-columns:1fr;gap:8px}.cfg-map__item-remove{justify-self:end}}@media(max-width:480px){.config-nav__icon{width:24px;height:24px;font-size:16px}.config-nav__label{display:none}.config-section-card__icon{width:28px;height:28px}.config-section-card__title{font-size:15px}.cfg-segmented{flex-wrap:wrap}.cfg-segmented__btn{flex:1 0 auto;min-width:60px}}
diff --git a/dist/control-ui/assets/index-bYQnHP3a.js b/dist/control-ui/assets/index-bYQnHP3a.js
new file mode 100644
index 000000000..ef15341cf
--- /dev/null
+++ b/dist/control-ui/assets/index-bYQnHP3a.js
@@ -0,0 +1,3047 @@
+(function(){const t=document.createElement("link").relList;if(t&&t.supports&&t.supports("modulepreload"))return;for(const i of document.querySelectorAll('link[rel="modulepreload"]'))s(i);new MutationObserver(i=>{for(const o of i)if(o.type==="childList")for(const a of o.addedNodes)a.tagName==="LINK"&&a.rel==="modulepreload"&&s(a)}).observe(document,{childList:!0,subtree:!0});function n(i){const o={};return i.integrity&&(o.integrity=i.integrity),i.referrerPolicy&&(o.referrerPolicy=i.referrerPolicy),i.crossOrigin==="use-credentials"?o.credentials="include":i.crossOrigin==="anonymous"?o.credentials="omit":o.credentials="same-origin",o}function s(i){if(i.ep)return;i.ep=!0;const o=n(i);fetch(i.href,o)}})();const zt=globalThis,As=zt.ShadowRoot&&(zt.ShadyCSS===void 0||zt.ShadyCSS.nativeShadow)&&"adoptedStyleSheets"in Document.prototype&&"replace"in CSSStyleSheet.prototype,Ss=Symbol(),Li=new WeakMap;let Ho=class{constructor(t,n,s){if(this._$cssResult$=!0,s!==Ss)throw Error("CSSResult is not constructable. Use `unsafeCSS` or `css` instead.");this.cssText=t,this.t=n}get styleSheet(){let t=this.o;const n=this.t;if(As&&t===void 0){const s=n!==void 0&&n.length===1;s&&(t=Li.get(n)),t===void 0&&((this.o=t=new CSSStyleSheet).replaceSync(this.cssText),s&&Li.set(n,t))}return t}toString(){return this.cssText}};const Lr=e=>new Ho(typeof e=="string"?e:e+"",void 0,Ss),Rr=(e,...t)=>{const n=e.length===1?e[0]:t.reduce((s,i,o)=>s+(a=>{if(a._$cssResult$===!0)return a.cssText;if(typeof a=="number")return a;throw Error("Value passed to 'css' function must be a 'css' function result: "+a+". Use 'unsafeCSS' to pass non-literal values, but take care to ensure page security.")})(i)+e[o+1],e[0]);return new Ho(n,e,Ss)},Mr=(e,t)=>{if(As)e.adoptedStyleSheets=t.map(n=>n instanceof CSSStyleSheet?n:n.styleSheet);else for(const n of t){const s=document.createElement("style"),i=zt.litNonce;i!==void 0&&s.setAttribute("nonce",i),s.textContent=n.cssText,e.appendChild(s)}},Ri=As?e=>e:e=>e instanceof CSSStyleSheet?(t=>{let n="";for(const s of t.cssRules)n+=s.cssText;return Lr(n)})(e):e;const{is:Pr,defineProperty:Nr,getOwnPropertyDescriptor:Or,getOwnPropertyNames:Dr,getOwnPropertySymbols:Br,getPrototypeOf:Fr}=Object,en=globalThis,Mi=en.trustedTypes,Ur=Mi?Mi.emptyScript:"",Kr=en.reactiveElementPolyfillSupport,vt=(e,t)=>e,Wt={toAttribute(e,t){switch(t){case Boolean:e=e?Ur:null;break;case Object:case Array:e=e==null?e:JSON.stringify(e)}return e},fromAttribute(e,t){let n=e;switch(t){case Boolean:n=e!==null;break;case Number:n=e===null?null:Number(e);break;case Object:case Array:try{n=JSON.parse(e)}catch{n=null}}return n}},_s=(e,t)=>!Pr(e,t),Pi={attribute:!0,type:String,converter:Wt,reflect:!1,useDefault:!1,hasChanged:_s};Symbol.metadata??=Symbol("metadata"),en.litPropertyMetadata??=new WeakMap;let Ve=class extends HTMLElement{static addInitializer(t){this._$Ei(),(this.l??=[]).push(t)}static get observedAttributes(){return this.finalize(),this._$Eh&&[...this._$Eh.keys()]}static createProperty(t,n=Pi){if(n.state&&(n.attribute=!1),this._$Ei(),this.prototype.hasOwnProperty(t)&&((n=Object.create(n)).wrapped=!0),this.elementProperties.set(t,n),!n.noAccessor){const s=Symbol(),i=this.getPropertyDescriptor(t,s,n);i!==void 0&&Nr(this.prototype,t,i)}}static getPropertyDescriptor(t,n,s){const{get:i,set:o}=Or(this.prototype,t)??{get(){return this[n]},set(a){this[n]=a}};return{get:i,set(a){const c=i?.call(this);o?.call(this,a),this.requestUpdate(t,c,s)},configurable:!0,enumerable:!0}}static getPropertyOptions(t){return this.elementProperties.get(t)??Pi}static _$Ei(){if(this.hasOwnProperty(vt("elementProperties")))return;const t=Fr(this);t.finalize(),t.l!==void 0&&(this.l=[...t.l]),this.elementProperties=new Map(t.elementProperties)}static finalize(){if(this.hasOwnProperty(vt("finalized")))return;if(this.finalized=!0,this._$Ei(),this.hasOwnProperty(vt("properties"))){const n=this.properties,s=[...Dr(n),...Br(n)];for(const i of s)this.createProperty(i,n[i])}const t=this[Symbol.metadata];if(t!==null){const n=litPropertyMetadata.get(t);if(n!==void 0)for(const[s,i]of n)this.elementProperties.set(s,i)}this._$Eh=new Map;for(const[n,s]of this.elementProperties){const i=this._$Eu(n,s);i!==void 0&&this._$Eh.set(i,n)}this.elementStyles=this.finalizeStyles(this.styles)}static finalizeStyles(t){const n=[];if(Array.isArray(t)){const s=new Set(t.flat(1/0).reverse());for(const i of s)n.unshift(Ri(i))}else t!==void 0&&n.push(Ri(t));return n}static _$Eu(t,n){const s=n.attribute;return s===!1?void 0:typeof s=="string"?s:typeof t=="string"?t.toLowerCase():void 0}constructor(){super(),this._$Ep=void 0,this.isUpdatePending=!1,this.hasUpdated=!1,this._$Em=null,this._$Ev()}_$Ev(){this._$ES=new Promise(t=>this.enableUpdating=t),this._$AL=new Map,this._$E_(),this.requestUpdate(),this.constructor.l?.forEach(t=>t(this))}addController(t){(this._$EO??=new Set).add(t),this.renderRoot!==void 0&&this.isConnected&&t.hostConnected?.()}removeController(t){this._$EO?.delete(t)}_$E_(){const t=new Map,n=this.constructor.elementProperties;for(const s of n.keys())this.hasOwnProperty(s)&&(t.set(s,this[s]),delete this[s]);t.size>0&&(this._$Ep=t)}createRenderRoot(){const t=this.shadowRoot??this.attachShadow(this.constructor.shadowRootOptions);return Mr(t,this.constructor.elementStyles),t}connectedCallback(){this.renderRoot??=this.createRenderRoot(),this.enableUpdating(!0),this._$EO?.forEach(t=>t.hostConnected?.())}enableUpdating(t){}disconnectedCallback(){this._$EO?.forEach(t=>t.hostDisconnected?.())}attributeChangedCallback(t,n,s){this._$AK(t,s)}_$ET(t,n){const s=this.constructor.elementProperties.get(t),i=this.constructor._$Eu(t,s);if(i!==void 0&&s.reflect===!0){const o=(s.converter?.toAttribute!==void 0?s.converter:Wt).toAttribute(n,s.type);this._$Em=t,o==null?this.removeAttribute(i):this.setAttribute(i,o),this._$Em=null}}_$AK(t,n){const s=this.constructor,i=s._$Eh.get(t);if(i!==void 0&&this._$Em!==i){const o=s.getPropertyOptions(i),a=typeof o.converter=="function"?{fromAttribute:o.converter}:o.converter?.fromAttribute!==void 0?o.converter:Wt;this._$Em=i;const c=a.fromAttribute(n,o.type);this[i]=c??this._$Ej?.get(i)??c,this._$Em=null}}requestUpdate(t,n,s,i=!1,o){if(t!==void 0){const a=this.constructor;if(i===!1&&(o=this[t]),s??=a.getPropertyOptions(t),!((s.hasChanged??_s)(o,n)||s.useDefault&&s.reflect&&o===this._$Ej?.get(t)&&!this.hasAttribute(a._$Eu(t,s))))return;this.C(t,n,s)}this.isUpdatePending===!1&&(this._$ES=this._$EP())}C(t,n,{useDefault:s,reflect:i,wrapped:o},a){s&&!(this._$Ej??=new Map).has(t)&&(this._$Ej.set(t,a??n??this[t]),o!==!0||a!==void 0)||(this._$AL.has(t)||(this.hasUpdated||s||(n=void 0),this._$AL.set(t,n)),i===!0&&this._$Em!==t&&(this._$Eq??=new Set).add(t))}async _$EP(){this.isUpdatePending=!0;try{await this._$ES}catch(n){Promise.reject(n)}const t=this.scheduleUpdate();return t!=null&&await t,!this.isUpdatePending}scheduleUpdate(){return this.performUpdate()}performUpdate(){if(!this.isUpdatePending)return;if(!this.hasUpdated){if(this.renderRoot??=this.createRenderRoot(),this._$Ep){for(const[i,o]of this._$Ep)this[i]=o;this._$Ep=void 0}const s=this.constructor.elementProperties;if(s.size>0)for(const[i,o]of s){const{wrapped:a}=o,c=this[i];a!==!0||this._$AL.has(i)||c===void 0||this.C(i,void 0,o,c)}}let t=!1;const n=this._$AL;try{t=this.shouldUpdate(n),t?(this.willUpdate(n),this._$EO?.forEach(s=>s.hostUpdate?.()),this.update(n)):this._$EM()}catch(s){throw t=!1,this._$EM(),s}t&&this._$AE(n)}willUpdate(t){}_$AE(t){this._$EO?.forEach(n=>n.hostUpdated?.()),this.hasUpdated||(this.hasUpdated=!0,this.firstUpdated(t)),this.updated(t)}_$EM(){this._$AL=new Map,this.isUpdatePending=!1}get updateComplete(){return this.getUpdateComplete()}getUpdateComplete(){return this._$ES}shouldUpdate(t){return!0}update(t){this._$Eq&&=this._$Eq.forEach(n=>this._$ET(n,this[n])),this._$EM()}updated(t){}firstUpdated(t){}};Ve.elementStyles=[],Ve.shadowRootOptions={mode:"open"},Ve[vt("elementProperties")]=new Map,Ve[vt("finalized")]=new Map,Kr?.({ReactiveElement:Ve}),(en.reactiveElementVersions??=[]).push("2.1.2");const Ts=globalThis,Ni=e=>e,Vt=Ts.trustedTypes,Oi=Vt?Vt.createPolicy("lit-html",{createHTML:e=>e}):void 0,zo="$lit$",we=`lit$${Math.random().toFixed(9).slice(2)}$`,jo="?"+we,Hr=`<${jo}>`,Pe=document,yt=()=>Pe.createComment(""),wt=e=>e===null||typeof e!="object"&&typeof e!="function",Es=Array.isArray,zr=e=>Es(e)||typeof e?.[Symbol.iterator]=="function",Nn=`[
+\f\r]`,ot=/<(?:(!--|\/[^a-zA-Z])|(\/?[a-zA-Z][^>\s]*)|(\/?$))/g,Di=/-->/g,Bi=/>/g,Ce=RegExp(`>|${Nn}(?:([^\\s"'>=/]+)(${Nn}*=${Nn}*(?:[^
+\f\r"'\`<>=]|("|')|))|$)`,"g"),Fi=/'/g,Ui=/"/g,qo=/^(?:script|style|textarea|title)$/i,jr=e=>(t,...n)=>({_$litType$:e,strings:t,values:n}),d=jr(1),xe=Symbol.for("lit-noChange"),g=Symbol.for("lit-nothing"),Ki=new WeakMap,Me=Pe.createTreeWalker(Pe,129);function Wo(e,t){if(!Es(e)||!e.hasOwnProperty("raw"))throw Error("invalid template strings array");return Oi!==void 0?Oi.createHTML(t):t}const qr=(e,t)=>{const n=e.length-1,s=[];let i,o=t===2?"":t===3?"":"")),s]};let Xn=class Vo{constructor({strings:t,_$litType$:n},s){let i;this.parts=[];let o=0,a=0;const c=t.length-1,r=this.parts,[p,l]=qr(t,n);if(this.el=Vo.createElement(p,s),Me.currentNode=this.el.content,n===2||n===3){const u=this.el.content.firstChild;u.replaceWith(...u.childNodes)}for(;(i=Me.nextNode())!==null&&r.length0){i.textContent=Vt?Vt.emptyScript:"";for(let v=0;v2||s[0]!==""||s[1]!==""?(this._$AH=Array(s.length-1).fill(new String),this.strings=s):this._$AH=g}_$AI(t,n=this,s,i){const o=this.strings;let a=!1;if(o===void 0)t=Qe(this,t,n,0),a=!wt(t)||t!==this._$AH&&t!==xe,a&&(this._$AH=t);else{const c=t;let r,p;for(t=o[0],r=0;r{const s=n?.renderBefore??t;let i=s._$litPart$;if(i===void 0){const o=n?.renderBefore??null;s._$litPart$=i=new tn(t.insertBefore(yt(),o),o,void 0,n??{})}return i._$AI(e),i};const Cs=globalThis;let Ye=class extends Ve{constructor(){super(...arguments),this.renderOptions={host:this},this._$Do=void 0}createRenderRoot(){const t=super.createRenderRoot();return this.renderOptions.renderBefore??=t.firstChild,t}update(t){const n=this.render();this.hasUpdated||(this.renderOptions.isConnected=this.isConnected),super.update(t),this._$Do=Xr(n,this.renderRoot,this.renderOptions)}connectedCallback(){super.connectedCallback(),this._$Do?.setConnected(!0)}disconnectedCallback(){super.disconnectedCallback(),this._$Do?.setConnected(!1)}render(){return xe}};Ye._$litElement$=!0,Ye.finalized=!0,Cs.litElementHydrateSupport?.({LitElement:Ye});const el=Cs.litElementPolyfillSupport;el?.({LitElement:Ye});(Cs.litElementVersions??=[]).push("4.2.2");const Yo=e=>(t,n)=>{n!==void 0?n.addInitializer(()=>{customElements.define(e,t)}):customElements.define(e,t)};const tl={attribute:!0,type:String,converter:Wt,reflect:!1,hasChanged:_s},nl=(e=tl,t,n)=>{const{kind:s,metadata:i}=n;let o=globalThis.litPropertyMetadata.get(i);if(o===void 0&&globalThis.litPropertyMetadata.set(i,o=new Map),s==="setter"&&((e=Object.create(e)).wrapped=!0),o.set(n.name,e),s==="accessor"){const{name:a}=n;return{set(c){const r=t.get.call(this);t.set.call(this,c),this.requestUpdate(a,r,e,!0,c)},init(c){return c!==void 0&&this.C(a,void 0,e,c),c}}}if(s==="setter"){const{name:a}=n;return function(c){const r=this[a];t.call(this,c),this.requestUpdate(a,r,e,!0,c)}}throw Error("Unsupported decorator location: "+s)};function sn(e){return(t,n)=>typeof n=="object"?nl(e,t,n):((s,i,o)=>{const a=i.hasOwnProperty(o);return i.constructor.createProperty(o,s),a?Object.getOwnPropertyDescriptor(i,o):void 0})(e,t,n)}function y(e){return sn({...e,state:!0,attribute:!1})}const sl=50,il=200,ol="Assistant";function Hi(e,t){if(typeof e!="string")return;const n=e.trim();if(n)return n.length<=t?n:n.slice(0,t)}function es(e){const t=Hi(e?.name,sl)??ol,n=Hi(e?.avatar??void 0,il)??null;return{agentId:typeof e?.agentId=="string"&&e.agentId.trim()?e.agentId.trim():null,name:t,avatar:n}}function al(){return es(typeof window>"u"?{}:{name:window.__CLAWDBOT_ASSISTANT_NAME__,avatar:window.__CLAWDBOT_ASSISTANT_AVATAR__})}const Qo="clawdbot.control.settings.v1";function rl(){const t={gatewayUrl:`${location.protocol==="https:"?"wss":"ws"}://${location.host}`,token:"",sessionKey:"main",lastActiveSessionKey:"main",theme:"system",chatFocusMode:!1,chatShowThinking:!0,splitRatio:.6,navCollapsed:!1,navGroupsCollapsed:{}};try{const n=localStorage.getItem(Qo);if(!n)return t;const s=JSON.parse(n);return{gatewayUrl:typeof s.gatewayUrl=="string"&&s.gatewayUrl.trim()?s.gatewayUrl.trim():t.gatewayUrl,token:typeof s.token=="string"?s.token:t.token,sessionKey:typeof s.sessionKey=="string"&&s.sessionKey.trim()?s.sessionKey.trim():t.sessionKey,lastActiveSessionKey:typeof s.lastActiveSessionKey=="string"&&s.lastActiveSessionKey.trim()?s.lastActiveSessionKey.trim():typeof s.sessionKey=="string"&&s.sessionKey.trim()||t.lastActiveSessionKey,theme:s.theme==="light"||s.theme==="dark"||s.theme==="system"?s.theme:t.theme,chatFocusMode:typeof s.chatFocusMode=="boolean"?s.chatFocusMode:t.chatFocusMode,chatShowThinking:typeof s.chatShowThinking=="boolean"?s.chatShowThinking:t.chatShowThinking,splitRatio:typeof s.splitRatio=="number"&&s.splitRatio>=.4&&s.splitRatio<=.7?s.splitRatio:t.splitRatio,navCollapsed:typeof s.navCollapsed=="boolean"?s.navCollapsed:t.navCollapsed,navGroupsCollapsed:typeof s.navGroupsCollapsed=="object"&&s.navGroupsCollapsed!==null?s.navGroupsCollapsed:t.navGroupsCollapsed}}catch{return t}}function ll(e){localStorage.setItem(Qo,JSON.stringify(e))}function Jo(e){const t=(e??"").trim();if(!t)return null;const n=t.split(":").filter(Boolean);if(n.length<3||n[0]!=="agent")return null;const s=n[1]?.trim(),i=n.slice(2).join(":");return!s||!i?null:{agentId:s,rest:i}}const cl=[{label:"Chat",tabs:["chat"]},{label:"Control",tabs:["overview","channels","instances","sessions","cron"]},{label:"Agent",tabs:["skills","nodes"]},{label:"Settings",tabs:["config","debug","logs"]}],Zo={overview:"/overview",channels:"/channels",instances:"/instances",sessions:"/sessions",cron:"/cron",skills:"/skills",nodes:"/nodes",chat:"/chat",config:"/config",debug:"/debug",logs:"/logs"},Xo=new Map(Object.entries(Zo).map(([e,t])=>[t,e]));function on(e){if(!e)return"";let t=e.trim();return t.startsWith("/")||(t=`/${t}`),t==="/"?"":(t.endsWith("/")&&(t=t.slice(0,-1)),t)}function $t(e){if(!e)return"/";let t=e.trim();return t.startsWith("/")||(t=`/${t}`),t.length>1&&t.endsWith("/")&&(t=t.slice(0,-1)),t}function Is(e,t=""){const n=on(t),s=Zo[e];return n?`${n}${s}`:s}function ea(e,t=""){const n=on(t);let s=e||"/";n&&(s===n?s="/":s.startsWith(`${n}/`)&&(s=s.slice(n.length)));let i=$t(s).toLowerCase();return i.endsWith("/index.html")&&(i="/"),i==="/"?"chat":Xo.get(i)??null}function dl(e){let t=$t(e);if(t.endsWith("/index.html")&&(t=$t(t.slice(0,-11))),t==="/")return"";const n=t.split("/").filter(Boolean);if(n.length===0)return"";for(let s=0;s!!(t&&t.trim())).join(", ")}function ss(e,t=120){return e.length<=t?e:`${e.slice(0,Math.max(0,t-1))}…`}function na(e,t){return e.length<=t?{text:e,truncated:!1,total:e.length}:{text:e.slice(0,Math.max(0,t)),truncated:!0,total:e.length}}function Gt(e,t){const n=Number(e);return Number.isFinite(n)?n:t}const On=/<\s*\/?\s*think(?:ing)?\s*>/gi,zi=/<\s*think(?:ing)?\s*>/i,ji=/<\s*\/\s*think(?:ing)?\s*>/i;function Dn(e){if(!e)return e;const t=zi.test(e),n=ji.test(e);if(!t&&!n)return e;if(t!==n)return t?e.replace(zi,"").trimStart():e.replace(ji,"").trimStart();if(!On.test(e))return e;On.lastIndex=0;let s="",i=0,o=!1;for(const a of e.matchAll(On)){const c=a.index??0;o||(s+=e.slice(i,c)),o=!a[0].toLowerCase().includes("/"),i=c+a[0].length}return o||(s+=e.slice(i)),s.trimStart()}const fl=/^\[([^\]]+)\]\s*/,hl=["WebChat","WhatsApp","Telegram","Signal","Slack","Discord","iMessage","Teams","Matrix","Zalo","Zalo Personal","BlueBubbles"];function gl(e){return/\d{4}-\d{2}-\d{2}T\d{2}:\d{2}Z\b/.test(e)||/\d{4}-\d{2}-\d{2} \d{2}:\d{2}\b/.test(e)?!0:hl.some(t=>e.startsWith(`${t} `))}function Bn(e){const t=e.match(fl);if(!t)return e;const n=t[1]??"";return gl(n)?e.slice(t[0].length):e}function an(e){const t=e,n=typeof t.role=="string"?t.role:"",s=t.content;if(typeof s=="string")return n==="assistant"?Dn(s):Bn(s);if(Array.isArray(s)){const i=s.map(o=>{const a=o;return a.type==="text"&&typeof a.text=="string"?a.text:null}).filter(o=>typeof o=="string");if(i.length>0){const o=i.join(`
+`);return n==="assistant"?Dn(o):Bn(o)}}return typeof t.text=="string"?n==="assistant"?Dn(t.text):Bn(t.text):null}function vl(e){const n=e.content,s=[];if(Array.isArray(n))for(const c of n){const r=c;if(r.type==="thinking"&&typeof r.thinking=="string"){const p=r.thinking.trim();p&&s.push(p)}}if(s.length>0)return s.join(`
+`);const i=ml(e);if(!i)return null;const a=[...i.matchAll(/<\s*think(?:ing)?\s*>([\s\S]*?)<\s*\/\s*think(?:ing)?\s*>/gi)].map(c=>(c[1]??"").trim()).filter(Boolean);return a.length>0?a.join(`
+`):null}function ml(e){const t=e,n=t.content;if(typeof n=="string")return n;if(Array.isArray(n)){const s=n.map(i=>{const o=i;return o.type==="text"&&typeof o.text=="string"?o.text:null}).filter(i=>typeof i=="string");if(s.length>0)return s.join(`
+`)}return typeof t.text=="string"?t.text:null}function bl(e){const t=e.trim();if(!t)return"";const n=t.split(/\r?\n/).map(s=>s.trim()).filter(Boolean).map(s=>`_${s}_`);return n.length?["_Reasoning:_",...n].join(`
+`):""}function qi(e){e[6]=e[6]&15|64,e[8]=e[8]&63|128;let t="";for(let n=0;n>>8&255,e[2]^=t>>>16&255,e[3]^=t>>>24&255,e}function Ls(e=globalThis.crypto){if(e&&typeof e.randomUUID=="function")return e.randomUUID();if(e&&typeof e.getRandomValues=="function"){const t=new Uint8Array(16);return e.getRandomValues(t),qi(t)}return qi(yl())}async function Je(e){if(!(!e.client||!e.connected)){e.chatLoading=!0,e.lastError=null;try{const t=await e.client.request("chat.history",{sessionKey:e.sessionKey,limit:200});e.chatMessages=Array.isArray(t.messages)?t.messages:[],e.chatThinkingLevel=t.thinkingLevel??null}catch(t){e.lastError=String(t)}finally{e.chatLoading=!1}}}async function wl(e,t){if(!e.client||!e.connected)return!1;const n=t.trim();if(!n)return!1;const s=Date.now();e.chatMessages=[...e.chatMessages,{role:"user",content:[{type:"text",text:n}],timestamp:s}],e.chatSending=!0,e.lastError=null;const i=Ls();e.chatRunId=i,e.chatStream="",e.chatStreamStartedAt=s;try{return await e.client.request("chat.send",{sessionKey:e.sessionKey,message:n,deliver:!1,idempotencyKey:i}),!0}catch(o){const a=String(o);return e.chatRunId=null,e.chatStream=null,e.chatStreamStartedAt=null,e.lastError=a,e.chatMessages=[...e.chatMessages,{role:"assistant",content:[{type:"text",text:"Error: "+a}],timestamp:Date.now()}],!1}finally{e.chatSending=!1}}async function $l(e){if(!e.client||!e.connected)return!1;const t=e.chatRunId;try{return await e.client.request("chat.abort",t?{sessionKey:e.sessionKey,runId:t}:{sessionKey:e.sessionKey}),!0}catch(n){return e.lastError=String(n),!1}}function kl(e,t){if(!t||t.sessionKey!==e.sessionKey||t.runId&&e.chatRunId&&t.runId!==e.chatRunId)return null;if(t.state==="delta"){const n=an(t.message);if(typeof n=="string"){const s=e.chatStream??"";(!s||n.length>=s.length)&&(e.chatStream=n)}}else t.state==="final"||t.state==="aborted"?(e.chatStream=null,e.chatRunId=null,e.chatStreamStartedAt=null):t.state==="error"&&(e.chatStream=null,e.chatRunId=null,e.chatStreamStartedAt=null,e.lastError=t.errorMessage??"chat error");return t.state}async function tt(e){if(!(!e.client||!e.connected)&&!e.sessionsLoading){e.sessionsLoading=!0,e.sessionsError=null;try{const t={includeGlobal:e.sessionsIncludeGlobal,includeUnknown:e.sessionsIncludeUnknown},n=Gt(e.sessionsFilterActive,0),s=Gt(e.sessionsFilterLimit,0);n>0&&(t.activeMinutes=n),s>0&&(t.limit=s);const i=await e.client.request("sessions.list",t);i&&(e.sessionsResult=i)}catch(t){e.sessionsError=String(t)}finally{e.sessionsLoading=!1}}}async function xl(e,t,n){if(!e.client||!e.connected)return;const s={key:t};"label"in n&&(s.label=n.label),"thinkingLevel"in n&&(s.thinkingLevel=n.thinkingLevel),"verboseLevel"in n&&(s.verboseLevel=n.verboseLevel),"reasoningLevel"in n&&(s.reasoningLevel=n.reasoningLevel);try{await e.client.request("sessions.patch",s),await tt(e)}catch(i){e.sessionsError=String(i)}}async function Al(e,t){if(!(!e.client||!e.connected||e.sessionsLoading||!window.confirm(`Delete session "${t}"?
+
+Deletes the session entry and archives its transcript.`))){e.sessionsLoading=!0,e.sessionsError=null;try{await e.client.request("sessions.delete",{key:t,deleteTranscript:!0}),await tt(e)}catch(s){e.sessionsError=String(s)}finally{e.sessionsLoading=!1}}}const Wi=50,Sl=80,_l=12e4;function Tl(e){if(!e||typeof e!="object")return null;const t=e;if(typeof t.text=="string")return t.text;const n=t.content;if(!Array.isArray(n))return null;const s=n.map(i=>{if(!i||typeof i!="object")return null;const o=i;return o.type==="text"&&typeof o.text=="string"?o.text:null}).filter(i=>!!i);return s.length===0?null:s.join(`
+`)}function Vi(e){if(e==null)return null;if(typeof e=="number"||typeof e=="boolean")return String(e);const t=Tl(e);let n;if(typeof e=="string")n=e;else if(t)n=t;else try{n=JSON.stringify(e,null,2)}catch{n=String(e)}const s=na(n,_l);return s.truncated?`${s.text}
+
+… truncated (${s.total} chars, showing first ${s.text.length}).`:s.text}function El(e){const t=[];return t.push({type:"toolcall",name:e.name,arguments:e.args??{}}),e.output&&t.push({type:"toolresult",name:e.name,text:e.output}),{role:"assistant",toolCallId:e.toolCallId,runId:e.runId,content:t,timestamp:e.startedAt}}function Cl(e){if(e.toolStreamOrder.length<=Wi)return;const t=e.toolStreamOrder.length-Wi,n=e.toolStreamOrder.splice(0,t);for(const s of n)e.toolStreamById.delete(s)}function Il(e){e.chatToolMessages=e.toolStreamOrder.map(t=>e.toolStreamById.get(t)?.message).filter(t=>!!t)}function is(e){e.toolStreamSyncTimer!=null&&(clearTimeout(e.toolStreamSyncTimer),e.toolStreamSyncTimer=null),Il(e)}function Ll(e,t=!1){if(t){is(e);return}e.toolStreamSyncTimer==null&&(e.toolStreamSyncTimer=window.setTimeout(()=>is(e),Sl))}function Rs(e){e.toolStreamById.clear(),e.toolStreamOrder=[],e.chatToolMessages=[],is(e)}function Rl(e,t){if(!t||t.stream!=="tool")return;const n=typeof t.sessionKey=="string"?t.sessionKey:void 0;if(n&&n!==e.sessionKey||!n&&e.chatRunId&&t.runId!==e.chatRunId||e.chatRunId&&t.runId!==e.chatRunId||!e.chatRunId)return;const s=t.data??{},i=typeof s.toolCallId=="string"?s.toolCallId:"";if(!i)return;const o=typeof s.name=="string"?s.name:"tool",a=typeof s.phase=="string"?s.phase:"",c=a==="start"?s.args:void 0,r=a==="update"?Vi(s.partialResult):a==="result"?Vi(s.result):void 0,p=Date.now();let l=e.toolStreamById.get(i);l?(l.name=o,c!==void 0&&(l.args=c),r!==void 0&&(l.output=r),l.updatedAt=p):(l={toolCallId:i,runId:t.runId,sessionKey:n,name:o,args:c,output:r,startedAt:typeof t.ts=="number"?t.ts:p,updatedAt:p,message:{}},e.toolStreamById.set(i,l),e.toolStreamOrder.push(i)),l.message=El(l),Cl(e),Ll(e,a==="result")}function rn(e,t=!1){e.chatScrollFrame&&cancelAnimationFrame(e.chatScrollFrame),e.chatScrollTimeout!=null&&(clearTimeout(e.chatScrollTimeout),e.chatScrollTimeout=null);const n=()=>{const s=e.querySelector(".chat-thread");if(s){const i=getComputedStyle(s).overflowY;if(i==="auto"||i==="scroll"||s.scrollHeight-s.clientHeight>1)return s}return document.scrollingElement??document.documentElement};e.updateComplete.then(()=>{e.chatScrollFrame=requestAnimationFrame(()=>{e.chatScrollFrame=null;const s=n();if(!s)return;const i=s.scrollHeight-s.scrollTop-s.clientHeight;if(!(t||e.chatUserNearBottom||i<200))return;t&&(e.chatHasAutoScrolled=!0),s.scrollTop=s.scrollHeight,e.chatUserNearBottom=!0;const a=t?150:120;e.chatScrollTimeout=window.setTimeout(()=>{e.chatScrollTimeout=null;const c=n();if(!c)return;const r=c.scrollHeight-c.scrollTop-c.clientHeight;(t||e.chatUserNearBottom||r<200)&&(c.scrollTop=c.scrollHeight,e.chatUserNearBottom=!0)},a)})})}function sa(e,t=!1){e.logsScrollFrame&&cancelAnimationFrame(e.logsScrollFrame),e.updateComplete.then(()=>{e.logsScrollFrame=requestAnimationFrame(()=>{e.logsScrollFrame=null;const n=e.querySelector(".log-stream");if(!n)return;const s=n.scrollHeight-n.scrollTop-n.clientHeight;(t||s<80)&&(n.scrollTop=n.scrollHeight)})})}function Ml(e,t){const n=t.currentTarget;if(!n)return;const s=n.scrollHeight-n.scrollTop-n.clientHeight;e.chatUserNearBottom=s<200}function Pl(e,t){const n=t.currentTarget;if(!n)return;const s=n.scrollHeight-n.scrollTop-n.clientHeight;e.logsAtBottom=s<80}function Nl(e){e.chatHasAutoScrolled=!1,e.chatUserNearBottom=!0}function Ol(e,t){if(e.length===0)return;const n=new Blob([`${e.join(`
+`)}
+`],{type:"text/plain"}),s=URL.createObjectURL(n),i=document.createElement("a"),o=new Date().toISOString().slice(0,19).replace(/[:T]/g,"-");i.href=s,i.download=`clawdbot-logs-${t}-${o}.log`,i.click(),URL.revokeObjectURL(s)}function Dl(e){if(typeof ResizeObserver>"u")return;const t=e.querySelector(".topbar");if(!t)return;const n=()=>{const{height:s}=t.getBoundingClientRect();e.style.setProperty("--topbar-height",`${s}px`)};n(),e.topbarObserver=new ResizeObserver(()=>n()),e.topbarObserver.observe(t)}function Ne(e){return typeof structuredClone=="function"?structuredClone(e):JSON.parse(JSON.stringify(e))}function Ze(e){return`${JSON.stringify(e,null,2).trimEnd()}
+`}function ia(e,t,n){if(t.length===0)return;let s=e;for(let o=0;o0&&(n.timeoutSeconds=s),n}async function jl(e){if(!(!e.client||!e.connected||e.cronBusy)){e.cronBusy=!0,e.cronError=null;try{const t=Hl(e.cronForm),n=zl(e.cronForm),s=e.cronForm.agentId.trim(),i={name:e.cronForm.name.trim(),description:e.cronForm.description.trim()||void 0,agentId:s||void 0,enabled:e.cronForm.enabled,schedule:t,sessionTarget:e.cronForm.sessionTarget,wakeMode:e.cronForm.wakeMode,payload:n,isolation:e.cronForm.postToMainPrefix.trim()&&e.cronForm.sessionTarget==="isolated"?{postToMainPrefix:e.cronForm.postToMainPrefix.trim()}:void 0};if(!i.name)throw new Error("Name required.");await e.client.request("cron.add",i),e.cronForm={...e.cronForm,name:"",description:"",payloadText:""},await ln(e),await St(e)}catch(t){e.cronError=String(t)}finally{e.cronBusy=!1}}}async function ql(e,t,n){if(!(!e.client||!e.connected||e.cronBusy)){e.cronBusy=!0,e.cronError=null;try{await e.client.request("cron.update",{id:t.id,patch:{enabled:n}}),await ln(e),await St(e)}catch(s){e.cronError=String(s)}finally{e.cronBusy=!1}}}async function Wl(e,t){if(!(!e.client||!e.connected||e.cronBusy)){e.cronBusy=!0,e.cronError=null;try{await e.client.request("cron.run",{id:t.id,mode:"force"}),await ra(e,t.id)}catch(n){e.cronError=String(n)}finally{e.cronBusy=!1}}}async function Vl(e,t){if(!(!e.client||!e.connected||e.cronBusy)){e.cronBusy=!0,e.cronError=null;try{await e.client.request("cron.remove",{id:t.id}),e.cronRunsJobId===t.id&&(e.cronRunsJobId=null,e.cronRuns=[]),await ln(e),await St(e)}catch(n){e.cronError=String(n)}finally{e.cronBusy=!1}}}async function ra(e,t){if(!(!e.client||!e.connected))try{const n=await e.client.request("cron.runs",{id:t,limit:50});e.cronRunsJobId=t,e.cronRuns=Array.isArray(n.entries)?n.entries:[]}catch(n){e.cronError=String(n)}}async function oe(e,t){if(!(!e.client||!e.connected)&&!e.channelsLoading){e.channelsLoading=!0,e.channelsError=null;try{const n=await e.client.request("channels.status",{probe:t,timeoutMs:8e3});e.channelsSnapshot=n,e.channelsLastSuccess=Date.now()}catch(n){e.channelsError=String(n)}finally{e.channelsLoading=!1}}}async function Gl(e,t){if(!(!e.client||!e.connected||e.whatsappBusy)){e.whatsappBusy=!0;try{const n=await e.client.request("web.login.start",{force:t,timeoutMs:3e4});e.whatsappLoginMessage=n.message??null,e.whatsappLoginQrDataUrl=n.qrDataUrl??null,e.whatsappLoginConnected=null}catch(n){e.whatsappLoginMessage=String(n),e.whatsappLoginQrDataUrl=null,e.whatsappLoginConnected=null}finally{e.whatsappBusy=!1}}}async function Yl(e){if(!(!e.client||!e.connected||e.whatsappBusy)){e.whatsappBusy=!0;try{const t=await e.client.request("web.login.wait",{timeoutMs:12e4});e.whatsappLoginMessage=t.message??null,e.whatsappLoginConnected=t.connected??null,t.connected&&(e.whatsappLoginQrDataUrl=null)}catch(t){e.whatsappLoginMessage=String(t),e.whatsappLoginConnected=null}finally{e.whatsappBusy=!1}}}async function Ql(e){if(!(!e.client||!e.connected||e.whatsappBusy)){e.whatsappBusy=!0;try{await e.client.request("channels.logout",{channel:"whatsapp"}),e.whatsappLoginMessage="Logged out.",e.whatsappLoginQrDataUrl=null,e.whatsappLoginConnected=null}catch(t){e.whatsappLoginMessage=String(t)}finally{e.whatsappBusy=!1}}}async function cn(e){if(!(!e.client||!e.connected)&&!e.debugLoading){e.debugLoading=!0;try{const[t,n,s,i]=await Promise.all([e.client.request("status",{}),e.client.request("health",{}),e.client.request("models.list",{}),e.client.request("last-heartbeat",{})]);e.debugStatus=t,e.debugHealth=n;const o=s;e.debugModels=Array.isArray(o?.models)?o?.models:[],e.debugHeartbeat=i}catch(t){e.debugCallError=String(t)}finally{e.debugLoading=!1}}}async function Jl(e){if(!(!e.client||!e.connected)){e.debugCallError=null,e.debugCallResult=null;try{const t=e.debugCallParams.trim()?JSON.parse(e.debugCallParams):{},n=await e.client.request(e.debugCallMethod.trim(),t);e.debugCallResult=JSON.stringify(n,null,2)}catch(t){e.debugCallError=String(t)}}}const Zl=2e3,Xl=new Set(["trace","debug","info","warn","error","fatal"]);function ec(e){if(typeof e!="string")return null;const t=e.trim();if(!t.startsWith("{")||!t.endsWith("}"))return null;try{const n=JSON.parse(t);return!n||typeof n!="object"?null:n}catch{return null}}function tc(e){if(typeof e!="string")return null;const t=e.toLowerCase();return Xl.has(t)?t:null}function nc(e){if(!e.trim())return{raw:e,message:e};try{const t=JSON.parse(e),n=t&&typeof t._meta=="object"&&t._meta!==null?t._meta:null,s=typeof t.time=="string"?t.time:typeof n?.date=="string"?n?.date:null,i=tc(n?.logLevelName??n?.level),o=typeof t[0]=="string"?t[0]:typeof n?.name=="string"?n?.name:null,a=ec(o);let c=null;a&&(typeof a.subsystem=="string"?c=a.subsystem:typeof a.module=="string"&&(c=a.module)),!c&&o&&o.length<120&&(c=o);let r=null;return typeof t[1]=="string"?r=t[1]:!a&&typeof t[0]=="string"?r=t[0]:typeof t.message=="string"&&(r=t.message),{raw:e,time:s,level:i,subsystem:c,message:r??e,meta:n??void 0}}catch{return{raw:e,message:e}}}async function Ms(e,t){if(!(!e.client||!e.connected)&&!(e.logsLoading&&!t?.quiet)){t?.quiet||(e.logsLoading=!0),e.logsError=null;try{const s=await e.client.request("logs.tail",{cursor:t?.reset?void 0:e.logsCursor??void 0,limit:e.logsLimit,maxBytes:e.logsMaxBytes}),o=(Array.isArray(s.lines)?s.lines.filter(c=>typeof c=="string"):[]).map(nc),a=!!(t?.reset||s.reset||e.logsCursor==null);e.logsEntries=a?o:[...e.logsEntries,...o].slice(-Zl),typeof s.cursor=="number"&&(e.logsCursor=s.cursor),typeof s.file=="string"&&(e.logsFile=s.file),e.logsTruncated=!!s.truncated,e.logsLastFetchAt=Date.now()}catch(n){e.logsError=String(n)}finally{t?.quiet||(e.logsLoading=!1)}}}const la={p:0x7fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffedn,n:0x1000000000000000000000000000000014def9dea2f79cd65812631a5cf5d3edn,h:8n,a:0x7fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffecn,d:0x52036cee2b6ffe738cc740797779e89800700a4d4141d8ab75eb4dca135978a3n,Gx:0x216936d3cd6e53fec0a4e231fdd6dc5c692cc7609525a7b2c9562d608f25d51an,Gy:0x6666666666666666666666666666666666666666666666666666666666666658n},{p:W,n:jt,Gx:Yi,Gy:Qi,a:Fn,d:Un,h:sc}=la,Oe=32,Ps=64,ic=(...e)=>{"captureStackTrace"in Error&&typeof Error.captureStackTrace=="function"&&Error.captureStackTrace(...e)},H=(e="")=>{const t=new Error(e);throw ic(t,H),t},oc=e=>typeof e=="bigint",ac=e=>typeof e=="string",rc=e=>e instanceof Uint8Array||ArrayBuffer.isView(e)&&e.constructor.name==="Uint8Array",Ae=(e,t,n="")=>{const s=rc(e),i=e?.length,o=t!==void 0;if(!s||o&&i!==t){const a=n&&`"${n}" `,c=o?` of length ${t}`:"",r=s?`length=${i}`:`type=${typeof e}`;H(a+"expected Uint8Array"+c+", got "+r)}return e},dn=e=>new Uint8Array(e),ca=e=>Uint8Array.from(e),da=(e,t)=>e.toString(16).padStart(t,"0"),ua=e=>Array.from(Ae(e)).map(t=>da(t,2)).join(""),ge={_0:48,_9:57,A:65,F:70,a:97,f:102},Ji=e=>{if(e>=ge._0&&e<=ge._9)return e-ge._0;if(e>=ge.A&&e<=ge.F)return e-(ge.A-10);if(e>=ge.a&&e<=ge.f)return e-(ge.a-10)},pa=e=>{const t="hex invalid";if(!ac(e))return H(t);const n=e.length,s=n/2;if(n%2)return H(t);const i=dn(s);for(let o=0,a=0;oglobalThis?.crypto,lc=()=>fa()?.subtle??H("crypto.subtle must be defined, consider polyfill"),xt=(...e)=>{const t=dn(e.reduce((s,i)=>s+Ae(i).length,0));let n=0;return e.forEach(s=>{t.set(s,n),n+=s.length}),t},cc=(e=Oe)=>fa().getRandomValues(dn(e)),Yt=BigInt,Re=(e,t,n,s="bad number: out of range")=>oc(e)&&t<=e&&e{const n=e%t;return n>=0n?n:t+n},ha=e=>S(e,jt),dc=(e,t)=>{(e===0n||t<=0n)&&H("no inverse n="+e+" mod="+t);let n=S(e,t),s=t,i=0n,o=1n;for(;n!==0n;){const a=s/n,c=s%n,r=i-o*a;s=n,n=c,i=o,o=r}return s===1n?S(i,t):H("no inverse")},uc=e=>{const t=ba[e];return typeof t!="function"&&H("hashes."+e+" not set"),t},Kn=e=>e instanceof X?e:H("Point expected"),as=2n**256n;class X{static BASE;static ZERO;X;Y;Z;T;constructor(t,n,s,i){const o=as;this.X=Re(t,0n,o),this.Y=Re(n,0n,o),this.Z=Re(s,1n,o),this.T=Re(i,0n,o),Object.freeze(this)}static CURVE(){return la}static fromAffine(t){return new X(t.x,t.y,1n,S(t.x*t.y))}static fromBytes(t,n=!1){const s=Un,i=ca(Ae(t,Oe)),o=t[31];i[31]=o&-129;const a=va(i);Re(a,0n,n?as:W);const r=S(a*a),p=S(r-1n),l=S(s*r+1n);let{isValid:u,value:h}=fc(p,l);u||H("bad point: y not sqrt");const v=(h&1n)===1n,w=(o&128)!==0;return!n&&h===0n&&w&&H("bad point: x==0, isLastByteOdd"),w!==v&&(h=S(-h)),new X(h,a,1n,S(h*a))}static fromHex(t,n){return X.fromBytes(pa(t),n)}get x(){return this.toAffine().x}get y(){return this.toAffine().y}assertValidity(){const t=Fn,n=Un,s=this;if(s.is0())return H("bad point: ZERO");const{X:i,Y:o,Z:a,T:c}=s,r=S(i*i),p=S(o*o),l=S(a*a),u=S(l*l),h=S(r*t),v=S(l*S(h+p)),w=S(u+S(n*S(r*p)));if(v!==w)return H("bad point: equation left != right (1)");const $=S(i*o),x=S(a*c);return $!==x?H("bad point: equation left != right (2)"):this}equals(t){const{X:n,Y:s,Z:i}=this,{X:o,Y:a,Z:c}=Kn(t),r=S(n*c),p=S(o*i),l=S(s*c),u=S(a*i);return r===p&&l===u}is0(){return this.equals(Ge)}negate(){return new X(S(-this.X),this.Y,this.Z,S(-this.T))}double(){const{X:t,Y:n,Z:s}=this,i=Fn,o=S(t*t),a=S(n*n),c=S(2n*S(s*s)),r=S(i*o),p=t+n,l=S(S(p*p)-o-a),u=r+a,h=u-c,v=r-a,w=S(l*h),$=S(u*v),x=S(l*v),E=S(h*u);return new X(w,$,E,x)}add(t){const{X:n,Y:s,Z:i,T:o}=this,{X:a,Y:c,Z:r,T:p}=Kn(t),l=Fn,u=Un,h=S(n*a),v=S(s*c),w=S(o*u*p),$=S(i*r),x=S((n+s)*(a+c)-h-v),E=S($-w),I=S($+w),R=S(v-l*h),C=S(x*E),A=S(I*R),B=S(x*R),ue=S(E*I);return new X(C,A,ue,B)}subtract(t){return this.add(Kn(t).negate())}multiply(t,n=!0){if(!n&&(t===0n||this.is0()))return Ge;if(Re(t,1n,jt),t===1n)return this;if(this.equals(De))return Ac(t).p;let s=Ge,i=De;for(let o=this;t>0n;o=o.double(),t>>=1n)t&1n?s=s.add(o):n&&(i=i.add(o));return s}multiplyUnsafe(t){return this.multiply(t,!1)}toAffine(){const{X:t,Y:n,Z:s}=this;if(this.equals(Ge))return{x:0n,y:1n};const i=dc(s,W);S(s*i)!==1n&&H("invalid inverse");const o=S(t*i),a=S(n*i);return{x:o,y:a}}toBytes(){const{x:t,y:n}=this.assertValidity().toAffine(),s=ga(n);return s[31]|=t&1n?128:0,s}toHex(){return ua(this.toBytes())}clearCofactor(){return this.multiply(Yt(sc),!1)}isSmallOrder(){return this.clearCofactor().is0()}isTorsionFree(){let t=this.multiply(jt/2n,!1).double();return jt%2n&&(t=t.add(this)),t.is0()}}const De=new X(Yi,Qi,1n,S(Yi*Qi)),Ge=new X(0n,1n,1n,0n);X.BASE=De;X.ZERO=Ge;const ga=e=>pa(da(Re(e,0n,as),Ps)).reverse(),va=e=>Yt("0x"+ua(ca(Ae(e)).reverse())),le=(e,t)=>{let n=e;for(;t-- >0n;)n*=n,n%=W;return n},pc=e=>{const n=e*e%W*e%W,s=le(n,2n)*n%W,i=le(s,1n)*e%W,o=le(i,5n)*i%W,a=le(o,10n)*o%W,c=le(a,20n)*a%W,r=le(c,40n)*c%W,p=le(r,80n)*r%W,l=le(p,80n)*r%W,u=le(l,10n)*o%W;return{pow_p_5_8:le(u,2n)*e%W,b2:n}},Zi=0x2b8324804fc1df0b2b4d00993dfbd7a72f431806ad2fe478c4ee1b274a0ea0b0n,fc=(e,t)=>{const n=S(t*t*t),s=S(n*n*t),i=pc(e*s).pow_p_5_8;let o=S(e*n*i);const a=S(t*o*o),c=o,r=S(o*Zi),p=a===e,l=a===S(-e),u=a===S(-e*Zi);return p&&(o=c),(l||u)&&(o=r),(S(o)&1n)===1n&&(o=S(-o)),{isValid:p||l,value:o}},rs=e=>ha(va(e)),Ns=(...e)=>ba.sha512Async(xt(...e)),hc=(...e)=>uc("sha512")(xt(...e)),ma=e=>{const t=e.slice(0,Oe);t[0]&=248,t[31]&=127,t[31]|=64;const n=e.slice(Oe,Ps),s=rs(t),i=De.multiply(s),o=i.toBytes();return{head:t,prefix:n,scalar:s,point:i,pointBytes:o}},Os=e=>Ns(Ae(e,Oe)).then(ma),gc=e=>ma(hc(Ae(e,Oe))),vc=e=>Os(e).then(t=>t.pointBytes),mc=e=>Ns(e.hashable).then(e.finish),bc=(e,t,n)=>{const{pointBytes:s,scalar:i}=e,o=rs(t),a=De.multiply(o).toBytes();return{hashable:xt(a,s,n),finish:p=>{const l=ha(o+rs(p)*i);return Ae(xt(a,ga(l)),Ps)}}},yc=async(e,t)=>{const n=Ae(e),s=await Os(t),i=await Ns(s.prefix,n);return mc(bc(s,i,n))},ba={sha512Async:async e=>{const t=lc(),n=xt(e);return dn(await t.digest("SHA-512",n.buffer))},sha512:void 0},wc=(e=cc(Oe))=>e,$c={getExtendedPublicKeyAsync:Os,getExtendedPublicKey:gc,randomSecretKey:wc},Qt=8,kc=256,ya=Math.ceil(kc/Qt)+1,ls=2**(Qt-1),xc=()=>{const e=[];let t=De,n=t;for(let s=0;s{const n=t.negate();return e?n:t},Ac=e=>{const t=Xi||(Xi=xc());let n=Ge,s=De;const i=2**Qt,o=i,a=Yt(i-1),c=Yt(Qt);for(let r=0;r>=c,p>ls&&(p-=o,e+=1n);const l=r*ls,u=l,h=l+Math.abs(p)-1,v=r%2!==0,w=p<0;p===0?s=s.add(eo(v,t[u])):n=n.add(eo(w,t[h]))}return e!==0n&&H("invalid wnaf"),{p:n,f:s}},Hn="clawdbot-device-identity-v1";function cs(e){let t="";for(const n of e)t+=String.fromCharCode(n);return btoa(t).replaceAll("+","-").replaceAll("/","_").replace(/=+$/g,"")}function wa(e){const t=e.replaceAll("-","+").replaceAll("_","/"),n=t+"=".repeat((4-t.length%4)%4),s=atob(n),i=new Uint8Array(s.length);for(let o=0;ot.toString(16).padStart(2,"0")).join("")}async function $a(e){const t=await crypto.subtle.digest("SHA-256",e);return Sc(new Uint8Array(t))}async function _c(){const e=$c.randomSecretKey(),t=await vc(e);return{deviceId:await $a(t),publicKey:cs(t),privateKey:cs(e)}}async function Ds(){try{const n=localStorage.getItem(Hn);if(n){const s=JSON.parse(n);if(s?.version===1&&typeof s.deviceId=="string"&&typeof s.publicKey=="string"&&typeof s.privateKey=="string"){const i=await $a(wa(s.publicKey));if(i!==s.deviceId){const o={...s,deviceId:i};return localStorage.setItem(Hn,JSON.stringify(o)),{deviceId:i,publicKey:s.publicKey,privateKey:s.privateKey}}return{deviceId:s.deviceId,publicKey:s.publicKey,privateKey:s.privateKey}}}}catch{}const e=await _c(),t={version:1,deviceId:e.deviceId,publicKey:e.publicKey,privateKey:e.privateKey,createdAtMs:Date.now()};return localStorage.setItem(Hn,JSON.stringify(t)),e}async function Tc(e,t){const n=wa(e),s=new TextEncoder().encode(t),i=await yc(s,n);return cs(i)}const ka="clawdbot.device.auth.v1";function Bs(e){return e.trim()}function Ec(e){if(!Array.isArray(e))return[];const t=new Set;for(const n of e){const s=n.trim();s&&t.add(s)}return[...t].sort()}function Fs(){try{const e=window.localStorage.getItem(ka);if(!e)return null;const t=JSON.parse(e);return!t||t.version!==1||!t.deviceId||typeof t.deviceId!="string"||!t.tokens||typeof t.tokens!="object"?null:t}catch{return null}}function xa(e){try{window.localStorage.setItem(ka,JSON.stringify(e))}catch{}}function Cc(e){const t=Fs();if(!t||t.deviceId!==e.deviceId)return null;const n=Bs(e.role),s=t.tokens[n];return!s||typeof s.token!="string"?null:s}function Aa(e){const t=Bs(e.role),n={version:1,deviceId:e.deviceId,tokens:{}},s=Fs();s&&s.deviceId===e.deviceId&&(n.tokens={...s.tokens});const i={token:e.token,role:t,scopes:Ec(e.scopes),updatedAtMs:Date.now()};return n.tokens[t]=i,xa(n),i}function Sa(e){const t=Fs();if(!t||t.deviceId!==e.deviceId)return;const n=Bs(e.role);if(!t.tokens[n])return;const s={...t,tokens:{...t.tokens}};delete s.tokens[n],xa(s)}async function Se(e,t){if(!(!e.client||!e.connected)&&!e.devicesLoading){e.devicesLoading=!0,t?.quiet||(e.devicesError=null);try{const n=await e.client.request("device.pair.list",{});e.devicesList={pending:Array.isArray(n?.pending)?n.pending:[],paired:Array.isArray(n?.paired)?n.paired:[]}}catch(n){t?.quiet||(e.devicesError=String(n))}finally{e.devicesLoading=!1}}}async function Ic(e,t){if(!(!e.client||!e.connected))try{await e.client.request("device.pair.approve",{requestId:t}),await Se(e)}catch(n){e.devicesError=String(n)}}async function Lc(e,t){if(!(!e.client||!e.connected||!window.confirm("Reject this device pairing request?")))try{await e.client.request("device.pair.reject",{requestId:t}),await Se(e)}catch(s){e.devicesError=String(s)}}async function Rc(e,t){if(!(!e.client||!e.connected))try{const n=await e.client.request("device.token.rotate",t);if(n?.token){const s=await Ds(),i=n.role??t.role;(n.deviceId===s.deviceId||t.deviceId===s.deviceId)&&Aa({deviceId:s.deviceId,role:i,token:n.token,scopes:n.scopes??t.scopes??[]}),window.prompt("New device token (copy and store securely):",n.token)}await Se(e)}catch(n){e.devicesError=String(n)}}async function Mc(e,t){if(!(!e.client||!e.connected||!window.confirm(`Revoke token for ${t.deviceId} (${t.role})?`)))try{await e.client.request("device.token.revoke",t);const s=await Ds();t.deviceId===s.deviceId&&Sa({deviceId:s.deviceId,role:t.role}),await Se(e)}catch(s){e.devicesError=String(s)}}async function un(e,t){if(!(!e.client||!e.connected)&&!e.nodesLoading){e.nodesLoading=!0,t?.quiet||(e.lastError=null);try{const n=await e.client.request("node.list",{});e.nodes=Array.isArray(n.nodes)?n.nodes:[]}catch(n){t?.quiet||(e.lastError=String(n))}finally{e.nodesLoading=!1}}}function Pc(e){if(!e||e.kind==="gateway")return{method:"exec.approvals.get",params:{}};const t=e.nodeId.trim();return t?{method:"exec.approvals.node.get",params:{nodeId:t}}:null}function Nc(e,t){if(!e||e.kind==="gateway")return{method:"exec.approvals.set",params:t};const n=e.nodeId.trim();return n?{method:"exec.approvals.node.set",params:{...t,nodeId:n}}:null}async function Us(e,t){if(!(!e.client||!e.connected)&&!e.execApprovalsLoading){e.execApprovalsLoading=!0,e.lastError=null;try{const n=Pc(t);if(!n){e.lastError="Select a node before loading exec approvals.";return}const s=await e.client.request(n.method,n.params);Oc(e,s)}catch(n){e.lastError=String(n)}finally{e.execApprovalsLoading=!1}}}function Oc(e,t){e.execApprovalsSnapshot=t,e.execApprovalsDirty||(e.execApprovalsForm=Ne(t.file??{}))}async function Dc(e,t){if(!(!e.client||!e.connected)){e.execApprovalsSaving=!0,e.lastError=null;try{const n=e.execApprovalsSnapshot?.hash;if(!n){e.lastError="Exec approvals hash missing; reload and retry.";return}const s=e.execApprovalsForm??e.execApprovalsSnapshot?.file??{},i=Nc(t,{file:s,baseHash:n});if(!i){e.lastError="Select a node before saving exec approvals.";return}await e.client.request(i.method,i.params),e.execApprovalsDirty=!1,await Us(e,t)}catch(n){e.lastError=String(n)}finally{e.execApprovalsSaving=!1}}}function Bc(e,t,n){const s=Ne(e.execApprovalsForm??e.execApprovalsSnapshot?.file??{});ia(s,t,n),e.execApprovalsForm=s,e.execApprovalsDirty=!0}function Fc(e,t){const n=Ne(e.execApprovalsForm??e.execApprovalsSnapshot?.file??{});oa(n,t),e.execApprovalsForm=n,e.execApprovalsDirty=!0}async function Ks(e){if(!(!e.client||!e.connected)&&!e.presenceLoading){e.presenceLoading=!0,e.presenceError=null,e.presenceStatus=null;try{const t=await e.client.request("system-presence",{});Array.isArray(t)?(e.presenceEntries=t,e.presenceStatus=t.length===0?"No instances yet.":null):(e.presenceEntries=[],e.presenceStatus="No presence payload.")}catch(t){e.presenceError=String(t)}finally{e.presenceLoading=!1}}}function Xe(e,t,n){if(!t.trim())return;const s={...e.skillMessages};n?s[t]=n:delete s[t],e.skillMessages=s}function pn(e){return e instanceof Error?e.message:String(e)}async function _t(e,t){if(t?.clearMessages&&Object.keys(e.skillMessages).length>0&&(e.skillMessages={}),!(!e.client||!e.connected)&&!e.skillsLoading){e.skillsLoading=!0,e.skillsError=null;try{const n=await e.client.request("skills.status",{});n&&(e.skillsReport=n)}catch(n){e.skillsError=pn(n)}finally{e.skillsLoading=!1}}}function Uc(e,t,n){e.skillEdits={...e.skillEdits,[t]:n}}async function Kc(e,t,n){if(!(!e.client||!e.connected)){e.skillsBusyKey=t,e.skillsError=null;try{await e.client.request("skills.update",{skillKey:t,enabled:n}),await _t(e),Xe(e,t,{kind:"success",message:n?"Skill enabled":"Skill disabled"})}catch(s){const i=pn(s);e.skillsError=i,Xe(e,t,{kind:"error",message:i})}finally{e.skillsBusyKey=null}}}async function Hc(e,t){if(!(!e.client||!e.connected)){e.skillsBusyKey=t,e.skillsError=null;try{const n=e.skillEdits[t]??"";await e.client.request("skills.update",{skillKey:t,apiKey:n}),await _t(e),Xe(e,t,{kind:"success",message:"API key saved"})}catch(n){const s=pn(n);e.skillsError=s,Xe(e,t,{kind:"error",message:s})}finally{e.skillsBusyKey=null}}}async function zc(e,t,n,s){if(!(!e.client||!e.connected)){e.skillsBusyKey=t,e.skillsError=null;try{const i=await e.client.request("skills.install",{name:n,installId:s,timeoutMs:12e4});await _t(e),Xe(e,t,{kind:"success",message:i?.message??"Installed"})}catch(i){const o=pn(i);e.skillsError=o,Xe(e,t,{kind:"error",message:o})}finally{e.skillsBusyKey=null}}}function jc(){return typeof window>"u"||typeof window.matchMedia!="function"||window.matchMedia("(prefers-color-scheme: dark)").matches?"dark":"light"}function Hs(e){return e==="system"?jc():e}const Ot=e=>Number.isNaN(e)?.5:e<=0?0:e>=1?1:e,qc=()=>typeof window>"u"||typeof window.matchMedia!="function"?!1:window.matchMedia("(prefers-reduced-motion: reduce)").matches??!1,Dt=e=>{e.classList.remove("theme-transition"),e.style.removeProperty("--theme-switch-x"),e.style.removeProperty("--theme-switch-y")},Wc=({nextTheme:e,applyTheme:t,context:n,currentTheme:s})=>{if(s===e)return;const i=globalThis.document??null;if(!i){t();return}const o=i.documentElement,a=i,c=qc();if(!!a.startViewTransition&&!c){let p=.5,l=.5;if(n?.pointerClientX!==void 0&&n?.pointerClientY!==void 0&&typeof window<"u")p=Ot(n.pointerClientX/window.innerWidth),l=Ot(n.pointerClientY/window.innerHeight);else if(n?.element){const u=n.element.getBoundingClientRect();u.width>0&&u.height>0&&typeof window<"u"&&(p=Ot((u.left+u.width/2)/window.innerWidth),l=Ot((u.top+u.height/2)/window.innerHeight))}o.style.setProperty("--theme-switch-x",`${p*100}%`),o.style.setProperty("--theme-switch-y",`${l*100}%`),o.classList.add("theme-transition");try{const u=a.startViewTransition?.(()=>{t()});u?.finished?u.finished.finally(()=>Dt(o)):Dt(o)}catch{Dt(o),t()}return}t(),Dt(o)};function Vc(e){e.nodesPollInterval==null&&(e.nodesPollInterval=window.setInterval(()=>{un(e,{quiet:!0})},5e3))}function Gc(e){e.nodesPollInterval!=null&&(clearInterval(e.nodesPollInterval),e.nodesPollInterval=null)}function zs(e){e.logsPollInterval==null&&(e.logsPollInterval=window.setInterval(()=>{e.tab==="logs"&&Ms(e,{quiet:!0})},2e3))}function js(e){e.logsPollInterval!=null&&(clearInterval(e.logsPollInterval),e.logsPollInterval=null)}function qs(e){e.debugPollInterval==null&&(e.debugPollInterval=window.setInterval(()=>{e.tab==="debug"&&cn(e)},3e3))}function Ws(e){e.debugPollInterval!=null&&(clearInterval(e.debugPollInterval),e.debugPollInterval=null)}function $e(e,t){const n={...t,lastActiveSessionKey:t.lastActiveSessionKey?.trim()||t.sessionKey.trim()||"main"};e.settings=n,ll(n),t.theme!==e.theme&&(e.theme=t.theme,fn(e,Hs(t.theme))),e.applySessionKey=e.settings.lastActiveSessionKey}function _a(e,t){const n=t.trim();n&&e.settings.lastActiveSessionKey!==n&&$e(e,{...e.settings,lastActiveSessionKey:n})}function Yc(e){if(!window.location.search)return;const t=new URLSearchParams(window.location.search),n=t.get("token"),s=t.get("password"),i=t.get("session"),o=t.get("gatewayUrl");let a=!1;if(n!=null){const r=n.trim();r&&r!==e.settings.token&&$e(e,{...e.settings,token:r}),t.delete("token"),a=!0}if(s!=null){const r=s.trim();r&&(e.password=r),t.delete("password"),a=!0}if(i!=null){const r=i.trim();r&&(e.sessionKey=r,$e(e,{...e.settings,sessionKey:r,lastActiveSessionKey:r}))}if(o!=null){const r=o.trim();r&&r!==e.settings.gatewayUrl&&$e(e,{...e.settings,gatewayUrl:r}),t.delete("gatewayUrl"),a=!0}if(!a)return;const c=new URL(window.location.href);c.search=t.toString(),window.history.replaceState({},"",c.toString())}function Qc(e,t){e.tab!==t&&(e.tab=t),t==="chat"&&(e.chatHasAutoScrolled=!1),t==="logs"?zs(e):js(e),t==="debug"?qs(e):Ws(e),Vs(e),Ea(e,t,!1)}function Jc(e,t,n){Wc({nextTheme:t,applyTheme:()=>{e.theme=t,$e(e,{...e.settings,theme:t}),fn(e,Hs(t))},context:n,currentTheme:e.theme})}async function Vs(e){e.tab==="overview"&&await Ca(e),e.tab==="channels"&&await od(e),e.tab==="instances"&&await Ks(e),e.tab==="sessions"&&await tt(e),e.tab==="cron"&&await Gs(e),e.tab==="skills"&&await _t(e),e.tab==="nodes"&&(await un(e),await Se(e),await me(e),await Us(e)),e.tab==="chat"&&(await dd(e),rn(e,!e.chatHasAutoScrolled)),e.tab==="config"&&(await aa(e),await me(e)),e.tab==="debug"&&(await cn(e),e.eventLog=e.eventLogBuffer),e.tab==="logs"&&(e.logsAtBottom=!0,await Ms(e,{reset:!0}),sa(e,!0))}function Zc(){if(typeof window>"u")return"";const e=window.__CLAWDBOT_CONTROL_UI_BASE_PATH__;return typeof e=="string"&&e.trim()?on(e):dl(window.location.pathname)}function Xc(e){e.theme=e.settings.theme??"system",fn(e,Hs(e.theme))}function fn(e,t){if(e.themeResolved=t,typeof document>"u")return;const n=document.documentElement;n.dataset.theme=t,n.style.colorScheme=t}function ed(e){if(typeof window>"u"||typeof window.matchMedia!="function")return;if(e.themeMedia=window.matchMedia("(prefers-color-scheme: dark)"),e.themeMediaHandler=n=>{e.theme==="system"&&fn(e,n.matches?"dark":"light")},typeof e.themeMedia.addEventListener=="function"){e.themeMedia.addEventListener("change",e.themeMediaHandler);return}e.themeMedia.addListener(e.themeMediaHandler)}function td(e){if(!e.themeMedia||!e.themeMediaHandler)return;if(typeof e.themeMedia.removeEventListener=="function"){e.themeMedia.removeEventListener("change",e.themeMediaHandler);return}e.themeMedia.removeListener(e.themeMediaHandler),e.themeMedia=null,e.themeMediaHandler=null}function nd(e,t){if(typeof window>"u")return;const n=ea(window.location.pathname,e.basePath)??"chat";Ta(e,n),Ea(e,n,t)}function sd(e){if(typeof window>"u")return;const t=ea(window.location.pathname,e.basePath);if(!t)return;const s=new URL(window.location.href).searchParams.get("session")?.trim();s&&(e.sessionKey=s,$e(e,{...e.settings,sessionKey:s,lastActiveSessionKey:s})),Ta(e,t)}function Ta(e,t){e.tab!==t&&(e.tab=t),t==="chat"&&(e.chatHasAutoScrolled=!1),t==="logs"?zs(e):js(e),t==="debug"?qs(e):Ws(e),e.connected&&Vs(e)}function Ea(e,t,n){if(typeof window>"u")return;const s=$t(Is(t,e.basePath)),i=$t(window.location.pathname),o=new URL(window.location.href);t==="chat"&&e.sessionKey?o.searchParams.set("session",e.sessionKey):o.searchParams.delete("session"),i!==s&&(o.pathname=s),n?window.history.replaceState({},"",o.toString()):window.history.pushState({},"",o.toString())}function id(e,t,n){if(typeof window>"u")return;const s=new URL(window.location.href);s.searchParams.set("session",t),window.history.replaceState({},"",s.toString())}async function Ca(e){await Promise.all([oe(e,!1),Ks(e),tt(e),St(e),cn(e)])}async function od(e){await Promise.all([oe(e,!0),aa(e),me(e)])}async function Gs(e){await Promise.all([oe(e,!1),St(e),ln(e)])}function Ia(e){return e.chatSending||!!e.chatRunId}function ad(e){const t=e.trim();if(!t)return!1;const n=t.toLowerCase();return n==="/stop"?!0:n==="stop"||n==="esc"||n==="abort"||n==="wait"||n==="exit"}async function La(e){e.connected&&(e.chatMessage="",await $l(e))}function rd(e,t){const n=t.trim();n&&(e.chatQueue=[...e.chatQueue,{id:Ls(),text:n,createdAt:Date.now()}])}async function Ra(e,t,n){Rs(e);const s=await wl(e,t);return!s&&n?.previousDraft!=null&&(e.chatMessage=n.previousDraft),s&&_a(e,e.sessionKey),s&&n?.restoreDraft&&n.previousDraft?.trim()&&(e.chatMessage=n.previousDraft),rn(e),s&&!e.chatRunId&&Ma(e),s}async function Ma(e){if(!e.connected||Ia(e))return;const[t,...n]=e.chatQueue;if(!t)return;e.chatQueue=n,await Ra(e,t.text)||(e.chatQueue=[t,...e.chatQueue])}function ld(e,t){e.chatQueue=e.chatQueue.filter(n=>n.id!==t)}async function cd(e,t,n){if(!e.connected)return;const s=e.chatMessage,i=(t??e.chatMessage).trim();if(i){if(ad(i)){await La(e);return}if(t==null&&(e.chatMessage=""),Ia(e)){rd(e,i);return}await Ra(e,i,{previousDraft:t==null?s:void 0,restoreDraft:!!(t&&n?.restoreDraft)})}}async function dd(e){await Promise.all([Je(e),tt(e),ds(e)]),rn(e,!0)}const ud=Ma;function pd(e){const t=Jo(e.sessionKey);return t?.agentId?t.agentId:e.hello?.snapshot?.sessionDefaults?.defaultAgentId?.trim()||"main"}function fd(e,t){const n=on(e),s=encodeURIComponent(t);return n?`${n}/avatar/${s}?meta=1`:`/avatar/${s}?meta=1`}async function ds(e){if(!e.connected){e.chatAvatarUrl=null;return}const t=pd(e);if(!t){e.chatAvatarUrl=null;return}e.chatAvatarUrl=null;const n=fd(e.basePath,t);try{const s=await fetch(n,{method:"GET"});if(!s.ok){e.chatAvatarUrl=null;return}const i=await s.json(),o=typeof i.avatarUrl=="string"?i.avatarUrl.trim():"";e.chatAvatarUrl=o||null}catch{e.chatAvatarUrl=null}}const Pa={CHILD:2},Na=e=>(...t)=>({_$litDirective$:e,values:t});let Oa=class{constructor(t){}get _$AU(){return this._$AM._$AU}_$AT(t,n,s){this._$Ct=t,this._$AM=n,this._$Ci=s}_$AS(t,n){return this.update(t,n)}update(t,n){return this.render(...n)}};const{I:hd}=Jr,to=e=>e,no=()=>document.createComment(""),at=(e,t,n)=>{const s=e._$AA.parentNode,i=t===void 0?e._$AB:t._$AA;if(n===void 0){const o=s.insertBefore(no(),i),a=s.insertBefore(no(),i);n=new hd(o,a,e,e.options)}else{const o=n._$AB.nextSibling,a=n._$AM,c=a!==e;if(c){let r;n._$AQ?.(e),n._$AM=e,n._$AP!==void 0&&(r=e._$AU)!==a._$AU&&n._$AP(r)}if(o!==i||c){let r=n._$AA;for(;r!==o;){const p=to(r).nextSibling;to(s).insertBefore(r,i),r=p}}}return n},Ie=(e,t,n=e)=>(e._$AI(t,n),e),gd={},vd=(e,t=gd)=>e._$AH=t,md=e=>e._$AH,zn=e=>{e._$AR(),e._$AA.remove()};const so=(e,t,n)=>{const s=new Map;for(let i=t;i<=n;i++)s.set(e[i],i);return s},Da=Na(class extends Oa{constructor(e){if(super(e),e.type!==Pa.CHILD)throw Error("repeat() can only be used in text expressions")}dt(e,t,n){let s;n===void 0?n=t:t!==void 0&&(s=t);const i=[],o=[];let a=0;for(const c of e)i[a]=s?s(c,a):a,o[a]=n(c,a),a++;return{values:o,keys:i}}render(e,t,n){return this.dt(e,t,n).values}update(e,[t,n,s]){const i=md(e),{values:o,keys:a}=this.dt(t,n,s);if(!Array.isArray(i))return this.ut=a,o;const c=this.ut??=[],r=[];let p,l,u=0,h=i.length-1,v=0,w=o.length-1;for(;u<=h&&v<=w;)if(i[u]===null)u++;else if(i[h]===null)h--;else if(c[u]===a[v])r[v]=Ie(i[u],o[v]),u++,v++;else if(c[h]===a[w])r[w]=Ie(i[h],o[w]),h--,w--;else if(c[u]===a[w])r[w]=Ie(i[u],o[w]),at(e,r[w+1],i[u]),u++,w--;else if(c[h]===a[v])r[v]=Ie(i[h],o[v]),at(e,i[u],i[h]),h--,v++;else if(p===void 0&&(p=so(a,v,w),l=so(c,u,h)),p.has(c[u]))if(p.has(c[h])){const $=l.get(a[v]),x=$!==void 0?i[$]:null;if(x===null){const E=at(e,i[u]);Ie(E,o[v]),r[v]=E}else r[v]=Ie(x,o[v]),at(e,i[u],x),i[$]=null;v++}else zn(i[h]),h--;else zn(i[u]),u++;for(;v<=w;){const $=at(e,r[w+1]);Ie($,o[v]),r[v++]=$}for(;u<=h;){const $=i[u++];$!==null&&zn($)}return this.ut=a,vd(e,r),xe}});function Ba(e){const t=e;let n=typeof t.role=="string"?t.role:"unknown";const s=typeof t.toolCallId=="string"||typeof t.tool_call_id=="string",i=t.content,o=Array.isArray(i)?i:null,a=Array.isArray(o)&&o.some(u=>{const h=u,v=String(h.type??"").toLowerCase();return v==="toolcall"||v==="tool_call"||v==="tooluse"||v==="tool_use"||v==="toolresult"||v==="tool_result"||v==="tool_call"||v==="tool_result"||typeof h.name=="string"&&h.arguments!=null}),c=typeof t.toolName=="string"||typeof t.tool_name=="string";(s||a||c)&&(n="toolResult");let r=[];typeof t.content=="string"?r=[{type:"text",text:t.content}]:Array.isArray(t.content)?r=t.content.map(u=>({type:u.type||"text",text:u.text,name:u.name,args:u.args||u.arguments})):typeof t.text=="string"&&(r=[{type:"text",text:t.text}]);const p=typeof t.timestamp=="number"?t.timestamp:Date.now(),l=typeof t.id=="string"?t.id:void 0;return{role:n,content:r,timestamp:p,id:l}}function Ys(e){const t=e.toLowerCase();return t==="toolresult"||t==="tool_result"||t==="tool"||t==="function"||t==="toolresult"?"tool":t==="assistant"?"assistant":t==="user"?"user":t==="system"?"system":e}function Fa(e){const t=e,n=typeof t.role=="string"?t.role.toLowerCase():"";return n==="toolresult"||n==="tool_result"}class us extends Oa{constructor(t){if(super(t),this.it=g,t.type!==Pa.CHILD)throw Error(this.constructor.directiveName+"() can only be used in child bindings")}render(t){if(t===g||t==null)return this._t=void 0,this.it=t;if(t===xe)return t;if(typeof t!="string")throw Error(this.constructor.directiveName+"() called with a non-string value");if(t===this.it)return this._t;this.it=t;const n=[t];return n.raw=n,this._t={_$litType$:this.constructor.resultType,strings:n,values:[]}}}us.directiveName="unsafeHTML",us.resultType=1;const ps=Na(us);const{entries:Ua,setPrototypeOf:io,isFrozen:bd,getPrototypeOf:yd,getOwnPropertyDescriptor:wd}=Object;let{freeze:Q,seal:te,create:fs}=Object,{apply:hs,construct:gs}=typeof Reflect<"u"&&Reflect;Q||(Q=function(t){return t});te||(te=function(t){return t});hs||(hs=function(t,n){for(var s=arguments.length,i=new Array(s>2?s-2:0),o=2;o1?n-1:0),i=1;i1?n-1:0),i=1;i2&&arguments[2]!==void 0?arguments[2]:qt;io&&io(e,null);let s=t.length;for(;s--;){let i=t[s];if(typeof i=="string"){const o=n(i);o!==i&&(bd(t)||(t[s]=o),i=o)}e[i]=!0}return e}function _d(e){for(let t=0;t/gm),Ld=te(/\$\{[\w\W]*/gm),Rd=te(/^data-[\-\w.\u00B7-\uFFFF]+$/),Md=te(/^aria-[\-\w]+$/),Ka=te(/^(?:(?:(?:f|ht)tps?|mailto|tel|callto|sms|cid|xmpp|matrix):|[^a-z]|[a-z+.\-]+(?:[^a-z+.\-:]|$))/i),Pd=te(/^(?:\w+script|data):/i),Nd=te(/[\u0000-\u0020\u00A0\u1680\u180E\u2000-\u2029\u205F\u3000]/g),Ha=te(/^html$/i),Od=te(/^[a-z][.\w]*(-[.\w]+)+$/i);var uo=Object.freeze({__proto__:null,ARIA_ATTR:Md,ATTR_WHITESPACE:Nd,CUSTOM_ELEMENT:Od,DATA_ATTR:Rd,DOCTYPE_NAME:Ha,ERB_EXPR:Id,IS_ALLOWED_URI:Ka,IS_SCRIPT_OR_DATA:Pd,MUSTACHE_EXPR:Cd,TMPLIT_EXPR:Ld});const ut={element:1,text:3,progressingInstruction:7,comment:8,document:9},Dd=function(){return typeof window>"u"?null:window},Bd=function(t,n){if(typeof t!="object"||typeof t.createPolicy!="function")return null;let s=null;const i="data-tt-policy-suffix";n&&n.hasAttribute(i)&&(s=n.getAttribute(i));const o="dompurify"+(s?"#"+s:"");try{return t.createPolicy(o,{createHTML(a){return a},createScriptURL(a){return a}})}catch{return console.warn("TrustedTypes policy "+o+" could not be created."),null}},po=function(){return{afterSanitizeAttributes:[],afterSanitizeElements:[],afterSanitizeShadowDOM:[],beforeSanitizeAttributes:[],beforeSanitizeElements:[],beforeSanitizeShadowDOM:[],uponSanitizeAttribute:[],uponSanitizeElement:[],uponSanitizeShadowNode:[]}};function za(){let e=arguments.length>0&&arguments[0]!==void 0?arguments[0]:Dd();const t=T=>za(T);if(t.version="3.3.1",t.removed=[],!e||!e.document||e.document.nodeType!==ut.document||!e.Element)return t.isSupported=!1,t;let{document:n}=e;const s=n,i=s.currentScript,{DocumentFragment:o,HTMLTemplateElement:a,Node:c,Element:r,NodeFilter:p,NamedNodeMap:l=e.NamedNodeMap||e.MozNamedAttrMap,HTMLFormElement:u,DOMParser:h,trustedTypes:v}=e,w=r.prototype,$=dt(w,"cloneNode"),x=dt(w,"remove"),E=dt(w,"nextSibling"),I=dt(w,"childNodes"),R=dt(w,"parentNode");if(typeof a=="function"){const T=n.createElement("template");T.content&&T.content.ownerDocument&&(n=T.content.ownerDocument)}let C,A="";const{implementation:B,createNodeIterator:ue,createDocumentFragment:bn,getElementsByTagName:yn}=n,{importNode:br}=s;let V=po();t.isSupported=typeof Ua=="function"&&typeof R=="function"&&B&&B.createHTMLDocument!==void 0;const{MUSTACHE_EXPR:wn,ERB_EXPR:$n,TMPLIT_EXPR:kn,DATA_ATTR:yr,ARIA_ATTR:wr,IS_SCRIPT_OR_DATA:$r,ATTR_WHITESPACE:ri,CUSTOM_ELEMENT:kr}=uo;let{IS_ALLOWED_URI:li}=uo,K=null;const ci=L({},[...ao,...Wn,...Vn,...Gn,...ro]);let z=null;const di=L({},[...lo,...Yn,...co,...Ft]);let D=Object.seal(fs(null,{tagNameCheck:{writable:!0,configurable:!1,enumerable:!0,value:null},attributeNameCheck:{writable:!0,configurable:!1,enumerable:!0,value:null},allowCustomizedBuiltInElements:{writable:!0,configurable:!1,enumerable:!0,value:!1}})),nt=null,xn=null;const Ue=Object.seal(fs(null,{tagCheck:{writable:!0,configurable:!1,enumerable:!0,value:null},attributeCheck:{writable:!0,configurable:!1,enumerable:!0,value:null}}));let ui=!0,An=!0,pi=!1,fi=!0,Ke=!1,Et=!0,Te=!1,Sn=!1,_n=!1,He=!1,Ct=!1,It=!1,hi=!0,gi=!1;const xr="user-content-";let Tn=!0,st=!1,ze={},ae=null;const En=L({},["annotation-xml","audio","colgroup","desc","foreignobject","head","iframe","math","mi","mn","mo","ms","mtext","noembed","noframes","noscript","plaintext","script","style","svg","template","thead","title","video","xmp"]);let vi=null;const mi=L({},["audio","video","img","source","image","track"]);let Cn=null;const bi=L({},["alt","class","for","id","label","name","pattern","placeholder","role","summary","title","value","style","xmlns"]),Lt="http://www.w3.org/1998/Math/MathML",Rt="http://www.w3.org/2000/svg",pe="http://www.w3.org/1999/xhtml";let je=pe,In=!1,Ln=null;const Ar=L({},[Lt,Rt,pe],jn);let Mt=L({},["mi","mo","mn","ms","mtext"]),Pt=L({},["annotation-xml"]);const Sr=L({},["title","style","font","a","script"]);let it=null;const _r=["application/xhtml+xml","text/html"],Tr="text/html";let U=null,qe=null;const Er=n.createElement("form"),yi=function(f){return f instanceof RegExp||f instanceof Function},Rn=function(){let f=arguments.length>0&&arguments[0]!==void 0?arguments[0]:{};if(!(qe&&qe===f)){if((!f||typeof f!="object")&&(f={}),f=ce(f),it=_r.indexOf(f.PARSER_MEDIA_TYPE)===-1?Tr:f.PARSER_MEDIA_TYPE,U=it==="application/xhtml+xml"?jn:qt,K=ne(f,"ALLOWED_TAGS")?L({},f.ALLOWED_TAGS,U):ci,z=ne(f,"ALLOWED_ATTR")?L({},f.ALLOWED_ATTR,U):di,Ln=ne(f,"ALLOWED_NAMESPACES")?L({},f.ALLOWED_NAMESPACES,jn):Ar,Cn=ne(f,"ADD_URI_SAFE_ATTR")?L(ce(bi),f.ADD_URI_SAFE_ATTR,U):bi,vi=ne(f,"ADD_DATA_URI_TAGS")?L(ce(mi),f.ADD_DATA_URI_TAGS,U):mi,ae=ne(f,"FORBID_CONTENTS")?L({},f.FORBID_CONTENTS,U):En,nt=ne(f,"FORBID_TAGS")?L({},f.FORBID_TAGS,U):ce({}),xn=ne(f,"FORBID_ATTR")?L({},f.FORBID_ATTR,U):ce({}),ze=ne(f,"USE_PROFILES")?f.USE_PROFILES:!1,ui=f.ALLOW_ARIA_ATTR!==!1,An=f.ALLOW_DATA_ATTR!==!1,pi=f.ALLOW_UNKNOWN_PROTOCOLS||!1,fi=f.ALLOW_SELF_CLOSE_IN_ATTR!==!1,Ke=f.SAFE_FOR_TEMPLATES||!1,Et=f.SAFE_FOR_XML!==!1,Te=f.WHOLE_DOCUMENT||!1,He=f.RETURN_DOM||!1,Ct=f.RETURN_DOM_FRAGMENT||!1,It=f.RETURN_TRUSTED_TYPE||!1,_n=f.FORCE_BODY||!1,hi=f.SANITIZE_DOM!==!1,gi=f.SANITIZE_NAMED_PROPS||!1,Tn=f.KEEP_CONTENT!==!1,st=f.IN_PLACE||!1,li=f.ALLOWED_URI_REGEXP||Ka,je=f.NAMESPACE||pe,Mt=f.MATHML_TEXT_INTEGRATION_POINTS||Mt,Pt=f.HTML_INTEGRATION_POINTS||Pt,D=f.CUSTOM_ELEMENT_HANDLING||{},f.CUSTOM_ELEMENT_HANDLING&&yi(f.CUSTOM_ELEMENT_HANDLING.tagNameCheck)&&(D.tagNameCheck=f.CUSTOM_ELEMENT_HANDLING.tagNameCheck),f.CUSTOM_ELEMENT_HANDLING&&yi(f.CUSTOM_ELEMENT_HANDLING.attributeNameCheck)&&(D.attributeNameCheck=f.CUSTOM_ELEMENT_HANDLING.attributeNameCheck),f.CUSTOM_ELEMENT_HANDLING&&typeof f.CUSTOM_ELEMENT_HANDLING.allowCustomizedBuiltInElements=="boolean"&&(D.allowCustomizedBuiltInElements=f.CUSTOM_ELEMENT_HANDLING.allowCustomizedBuiltInElements),Ke&&(An=!1),Ct&&(He=!0),ze&&(K=L({},ro),z=[],ze.html===!0&&(L(K,ao),L(z,lo)),ze.svg===!0&&(L(K,Wn),L(z,Yn),L(z,Ft)),ze.svgFilters===!0&&(L(K,Vn),L(z,Yn),L(z,Ft)),ze.mathMl===!0&&(L(K,Gn),L(z,co),L(z,Ft))),f.ADD_TAGS&&(typeof f.ADD_TAGS=="function"?Ue.tagCheck=f.ADD_TAGS:(K===ci&&(K=ce(K)),L(K,f.ADD_TAGS,U))),f.ADD_ATTR&&(typeof f.ADD_ATTR=="function"?Ue.attributeCheck=f.ADD_ATTR:(z===di&&(z=ce(z)),L(z,f.ADD_ATTR,U))),f.ADD_URI_SAFE_ATTR&&L(Cn,f.ADD_URI_SAFE_ATTR,U),f.FORBID_CONTENTS&&(ae===En&&(ae=ce(ae)),L(ae,f.FORBID_CONTENTS,U)),f.ADD_FORBID_CONTENTS&&(ae===En&&(ae=ce(ae)),L(ae,f.ADD_FORBID_CONTENTS,U)),Tn&&(K["#text"]=!0),Te&&L(K,["html","head","body"]),K.table&&(L(K,["tbody"]),delete nt.tbody),f.TRUSTED_TYPES_POLICY){if(typeof f.TRUSTED_TYPES_POLICY.createHTML!="function")throw ct('TRUSTED_TYPES_POLICY configuration option must provide a "createHTML" hook.');if(typeof f.TRUSTED_TYPES_POLICY.createScriptURL!="function")throw ct('TRUSTED_TYPES_POLICY configuration option must provide a "createScriptURL" hook.');C=f.TRUSTED_TYPES_POLICY,A=C.createHTML("")}else C===void 0&&(C=Bd(v,i)),C!==null&&typeof A=="string"&&(A=C.createHTML(""));Q&&Q(f),qe=f}},wi=L({},[...Wn,...Vn,...Td]),$i=L({},[...Gn,...Ed]),Cr=function(f){let k=R(f);(!k||!k.tagName)&&(k={namespaceURI:je,tagName:"template"});const _=qt(f.tagName),N=qt(k.tagName);return Ln[f.namespaceURI]?f.namespaceURI===Rt?k.namespaceURI===pe?_==="svg":k.namespaceURI===Lt?_==="svg"&&(N==="annotation-xml"||Mt[N]):!!wi[_]:f.namespaceURI===Lt?k.namespaceURI===pe?_==="math":k.namespaceURI===Rt?_==="math"&&Pt[N]:!!$i[_]:f.namespaceURI===pe?k.namespaceURI===Rt&&!Pt[N]||k.namespaceURI===Lt&&!Mt[N]?!1:!$i[_]&&(Sr[_]||!wi[_]):!!(it==="application/xhtml+xml"&&Ln[f.namespaceURI]):!1},re=function(f){rt(t.removed,{element:f});try{R(f).removeChild(f)}catch{x(f)}},Ee=function(f,k){try{rt(t.removed,{attribute:k.getAttributeNode(f),from:k})}catch{rt(t.removed,{attribute:null,from:k})}if(k.removeAttribute(f),f==="is")if(He||Ct)try{re(k)}catch{}else try{k.setAttribute(f,"")}catch{}},ki=function(f){let k=null,_=null;if(_n)f=" "+f;else{const F=qn(f,/^[\r\n\t ]+/);_=F&&F[0]}it==="application/xhtml+xml"&&je===pe&&(f=''+f+"");const N=C?C.createHTML(f):f;if(je===pe)try{k=new h().parseFromString(N,it)}catch{}if(!k||!k.documentElement){k=B.createDocument(je,"template",null);try{k.documentElement.innerHTML=In?A:N}catch{}}const q=k.body||k.documentElement;return f&&_&&q.insertBefore(n.createTextNode(_),q.childNodes[0]||null),je===pe?yn.call(k,Te?"html":"body")[0]:Te?k.documentElement:q},xi=function(f){return ue.call(f.ownerDocument||f,f,p.SHOW_ELEMENT|p.SHOW_COMMENT|p.SHOW_TEXT|p.SHOW_PROCESSING_INSTRUCTION|p.SHOW_CDATA_SECTION,null)},Mn=function(f){return f instanceof u&&(typeof f.nodeName!="string"||typeof f.textContent!="string"||typeof f.removeChild!="function"||!(f.attributes instanceof l)||typeof f.removeAttribute!="function"||typeof f.setAttribute!="function"||typeof f.namespaceURI!="string"||typeof f.insertBefore!="function"||typeof f.hasChildNodes!="function")},Ai=function(f){return typeof c=="function"&&f instanceof c};function fe(T,f,k){Bt(T,_=>{_.call(t,f,k,qe)})}const Si=function(f){let k=null;if(fe(V.beforeSanitizeElements,f,null),Mn(f))return re(f),!0;const _=U(f.nodeName);if(fe(V.uponSanitizeElement,f,{tagName:_,allowedTags:K}),Et&&f.hasChildNodes()&&!Ai(f.firstElementChild)&&G(/<[/\w!]/g,f.innerHTML)&&G(/<[/\w!]/g,f.textContent)||f.nodeType===ut.progressingInstruction||Et&&f.nodeType===ut.comment&&G(/<[/\w]/g,f.data))return re(f),!0;if(!(Ue.tagCheck instanceof Function&&Ue.tagCheck(_))&&(!K[_]||nt[_])){if(!nt[_]&&Ti(_)&&(D.tagNameCheck instanceof RegExp&&G(D.tagNameCheck,_)||D.tagNameCheck instanceof Function&&D.tagNameCheck(_)))return!1;if(Tn&&!ae[_]){const N=R(f)||f.parentNode,q=I(f)||f.childNodes;if(q&&N){const F=q.length;for(let Z=F-1;Z>=0;--Z){const he=$(q[Z],!0);he.__removalCount=(f.__removalCount||0)+1,N.insertBefore(he,E(f))}}}return re(f),!0}return f instanceof r&&!Cr(f)||(_==="noscript"||_==="noembed"||_==="noframes")&&G(/<\/no(script|embed|frames)/i,f.innerHTML)?(re(f),!0):(Ke&&f.nodeType===ut.text&&(k=f.textContent,Bt([wn,$n,kn],N=>{k=lt(k,N," ")}),f.textContent!==k&&(rt(t.removed,{element:f.cloneNode()}),f.textContent=k)),fe(V.afterSanitizeElements,f,null),!1)},_i=function(f,k,_){if(hi&&(k==="id"||k==="name")&&(_ in n||_ in Er))return!1;if(!(An&&!xn[k]&&G(yr,k))){if(!(ui&&G(wr,k))){if(!(Ue.attributeCheck instanceof Function&&Ue.attributeCheck(k,f))){if(!z[k]||xn[k]){if(!(Ti(f)&&(D.tagNameCheck instanceof RegExp&&G(D.tagNameCheck,f)||D.tagNameCheck instanceof Function&&D.tagNameCheck(f))&&(D.attributeNameCheck instanceof RegExp&&G(D.attributeNameCheck,k)||D.attributeNameCheck instanceof Function&&D.attributeNameCheck(k,f))||k==="is"&&D.allowCustomizedBuiltInElements&&(D.tagNameCheck instanceof RegExp&&G(D.tagNameCheck,_)||D.tagNameCheck instanceof Function&&D.tagNameCheck(_))))return!1}else if(!Cn[k]){if(!G(li,lt(_,ri,""))){if(!((k==="src"||k==="xlink:href"||k==="href")&&f!=="script"&&xd(_,"data:")===0&&vi[f])){if(!(pi&&!G($r,lt(_,ri,"")))){if(_)return!1}}}}}}}return!0},Ti=function(f){return f!=="annotation-xml"&&qn(f,kr)},Ei=function(f){fe(V.beforeSanitizeAttributes,f,null);const{attributes:k}=f;if(!k||Mn(f))return;const _={attrName:"",attrValue:"",keepAttr:!0,allowedAttributes:z,forceKeepAttr:void 0};let N=k.length;for(;N--;){const q=k[N],{name:F,namespaceURI:Z,value:he}=q,We=U(F),Pn=he;let j=F==="value"?Pn:Ad(Pn);if(_.attrName=We,_.attrValue=j,_.keepAttr=!0,_.forceKeepAttr=void 0,fe(V.uponSanitizeAttribute,f,_),j=_.attrValue,gi&&(We==="id"||We==="name")&&(Ee(F,f),j=xr+j),Et&&G(/((--!?|])>)|<\/(style|title|textarea)/i,j)){Ee(F,f);continue}if(We==="attributename"&&qn(j,"href")){Ee(F,f);continue}if(_.forceKeepAttr)continue;if(!_.keepAttr){Ee(F,f);continue}if(!fi&&G(/\/>/i,j)){Ee(F,f);continue}Ke&&Bt([wn,$n,kn],Ii=>{j=lt(j,Ii," ")});const Ci=U(f.nodeName);if(!_i(Ci,We,j)){Ee(F,f);continue}if(C&&typeof v=="object"&&typeof v.getAttributeType=="function"&&!Z)switch(v.getAttributeType(Ci,We)){case"TrustedHTML":{j=C.createHTML(j);break}case"TrustedScriptURL":{j=C.createScriptURL(j);break}}if(j!==Pn)try{Z?f.setAttributeNS(Z,F,j):f.setAttribute(F,j),Mn(f)?re(f):oo(t.removed)}catch{Ee(F,f)}}fe(V.afterSanitizeAttributes,f,null)},Ir=function T(f){let k=null;const _=xi(f);for(fe(V.beforeSanitizeShadowDOM,f,null);k=_.nextNode();)fe(V.uponSanitizeShadowNode,k,null),Si(k),Ei(k),k.content instanceof o&&T(k.content);fe(V.afterSanitizeShadowDOM,f,null)};return t.sanitize=function(T){let f=arguments.length>1&&arguments[1]!==void 0?arguments[1]:{},k=null,_=null,N=null,q=null;if(In=!T,In&&(T=""),typeof T!="string"&&!Ai(T))if(typeof T.toString=="function"){if(T=T.toString(),typeof T!="string")throw ct("dirty is not a string, aborting")}else throw ct("toString is not a function");if(!t.isSupported)return T;if(Sn||Rn(f),t.removed=[],typeof T=="string"&&(st=!1),st){if(T.nodeName){const he=U(T.nodeName);if(!K[he]||nt[he])throw ct("root node is forbidden and cannot be sanitized in-place")}}else if(T instanceof c)k=ki(""),_=k.ownerDocument.importNode(T,!0),_.nodeType===ut.element&&_.nodeName==="BODY"||_.nodeName==="HTML"?k=_:k.appendChild(_);else{if(!He&&!Ke&&!Te&&T.indexOf("<")===-1)return C&&It?C.createHTML(T):T;if(k=ki(T),!k)return He?null:It?A:""}k&&_n&&re(k.firstChild);const F=xi(st?T:k);for(;N=F.nextNode();)Si(N),Ei(N),N.content instanceof o&&Ir(N.content);if(st)return T;if(He){if(Ct)for(q=bn.call(k.ownerDocument);k.firstChild;)q.appendChild(k.firstChild);else q=k;return(z.shadowroot||z.shadowrootmode)&&(q=br.call(s,q,!0)),q}let Z=Te?k.outerHTML:k.innerHTML;return Te&&K["!doctype"]&&k.ownerDocument&&k.ownerDocument.doctype&&k.ownerDocument.doctype.name&&G(Ha,k.ownerDocument.doctype.name)&&(Z="
+`+Z),Ke&&Bt([wn,$n,kn],he=>{Z=lt(Z,he," ")}),C&&It?C.createHTML(Z):Z},t.setConfig=function(){let T=arguments.length>0&&arguments[0]!==void 0?arguments[0]:{};Rn(T),Sn=!0},t.clearConfig=function(){qe=null,Sn=!1},t.isValidAttribute=function(T,f,k){qe||Rn({});const _=U(T),N=U(f);return _i(_,N,k)},t.addHook=function(T,f){typeof f=="function"&&rt(V[T],f)},t.removeHook=function(T,f){if(f!==void 0){const k=$d(V[T],f);return k===-1?void 0:kd(V[T],k,1)[0]}return oo(V[T])},t.removeHooks=function(T){V[T]=[]},t.removeAllHooks=function(){V=po()},t}var vs=za();function Qs(){return{async:!1,breaks:!1,extensions:null,gfm:!0,hooks:null,pedantic:!1,renderer:null,silent:!1,tokenizer:null,walkTokens:null}}var Fe=Qs();function ja(e){Fe=e}var mt={exec:()=>null};function M(e,t=""){let n=typeof e=="string"?e:e.source,s={replace:(i,o)=>{let a=typeof o=="string"?o:o.source;return a=a.replace(Y.caret,"$1"),n=n.replace(i,a),s},getRegex:()=>new RegExp(n,t)};return s}var Fd=(()=>{try{return!!new RegExp("(?<=1)(?/,blockquoteSetextReplace:/\n {0,3}((?:=+|-+) *)(?=\n|$)/g,blockquoteSetextReplace2:/^ {0,3}>[ \t]?/gm,listReplaceTabs:/^\t+/,listReplaceNesting:/^ {1,4}(?=( {4})*[^ ])/g,listIsTask:/^\[[ xX]\] +\S/,listReplaceTask:/^\[[ xX]\] +/,listTaskCheckbox:/\[[ xX]\]/,anyLine:/\n.*\n/,hrefBrackets:/^<(.*)>$/,tableDelimiter:/[:|]/,tableAlignChars:/^\||\| *$/g,tableRowBlankLine:/\n[ \t]*$/,tableAlignRight:/^ *-+: *$/,tableAlignCenter:/^ *:-+: *$/,tableAlignLeft:/^ *:-+ *$/,startATag:/^/i,startPreScriptTag:/^<(pre|code|kbd|script)(\s|>)/i,endPreScriptTag:/^<\/(pre|code|kbd|script)(\s|>)/i,startAngleBracket:/^,endAngleBracket:/>$/,pedanticHrefTitle:/^([^'"]*[^\s])\s+(['"])(.*)\2/,unicodeAlphaNumeric:/[\p{L}\p{N}]/u,escapeTest:/[&<>"']/,escapeReplace:/[&<>"']/g,escapeTestNoEncode:/[<>"']|&(?!(#\d{1,7}|#[Xx][a-fA-F0-9]{1,6}|\w+);)/,escapeReplaceNoEncode:/[<>"']|&(?!(#\d{1,7}|#[Xx][a-fA-F0-9]{1,6}|\w+);)/g,unescapeTest:/&(#(?:\d+)|(?:#x[0-9A-Fa-f]+)|(?:\w+));?/ig,caret:/(^|[^\[])\^/g,percentDecode:/%25/g,findPipe:/\|/g,splitPipe:/ \|/,slashPipe:/\\\|/g,carriageReturn:/\r\n|\r/g,spaceLine:/^ +$/gm,notSpaceStart:/^\S*/,endingNewline:/\n$/,listItemRegex:e=>new RegExp(`^( {0,3}${e})((?:[ ][^\\n]*)?(?:\\n|$))`),nextBulletRegex:e=>new RegExp(`^ {0,${Math.min(3,e-1)}}(?:[*+-]|\\d{1,9}[.)])((?:[ ][^\\n]*)?(?:\\n|$))`),hrRegex:e=>new RegExp(`^ {0,${Math.min(3,e-1)}}((?:- *){3,}|(?:_ *){3,}|(?:\\* *){3,})(?:\\n+|$)`),fencesBeginRegex:e=>new RegExp(`^ {0,${Math.min(3,e-1)}}(?:\`\`\`|~~~)`),headingBeginRegex:e=>new RegExp(`^ {0,${Math.min(3,e-1)}}#`),htmlBeginRegex:e=>new RegExp(`^ {0,${Math.min(3,e-1)}}<(?:[a-z].*>|!--)`,"i")},Ud=/^(?:[ \t]*(?:\n|$))+/,Kd=/^((?: {4}| {0,3}\t)[^\n]+(?:\n(?:[ \t]*(?:\n|$))*)?)+/,Hd=/^ {0,3}(`{3,}(?=[^`\n]*(?:\n|$))|~{3,})([^\n]*)(?:\n|$)(?:|([\s\S]*?)(?:\n|$))(?: {0,3}\1[~`]* *(?=\n|$)|$)/,Tt=/^ {0,3}((?:-[\t ]*){3,}|(?:_[ \t]*){3,}|(?:\*[ \t]*){3,})(?:\n+|$)/,zd=/^ {0,3}(#{1,6})(?=\s|$)(.*)(?:\n+|$)/,Js=/(?:[*+-]|\d{1,9}[.)])/,qa=/^(?!bull |blockCode|fences|blockquote|heading|html|table)((?:.|\n(?!\s*?\n|bull |blockCode|fences|blockquote|heading|html|table))+?)\n {0,3}(=+|-+) *(?:\n+|$)/,Wa=M(qa).replace(/bull/g,Js).replace(/blockCode/g,/(?: {4}| {0,3}\t)/).replace(/fences/g,/ {0,3}(?:`{3,}|~{3,})/).replace(/blockquote/g,/ {0,3}>/).replace(/heading/g,/ {0,3}#{1,6}/).replace(/html/g,/ {0,3}<[^\n>]+>\n/).replace(/\|table/g,"").getRegex(),jd=M(qa).replace(/bull/g,Js).replace(/blockCode/g,/(?: {4}| {0,3}\t)/).replace(/fences/g,/ {0,3}(?:`{3,}|~{3,})/).replace(/blockquote/g,/ {0,3}>/).replace(/heading/g,/ {0,3}#{1,6}/).replace(/html/g,/ {0,3}<[^\n>]+>\n/).replace(/table/g,/ {0,3}\|?(?:[:\- ]*\|)+[\:\- ]*\n/).getRegex(),Zs=/^([^\n]+(?:\n(?!hr|heading|lheading|blockquote|fences|list|html|table| +\n)[^\n]+)*)/,qd=/^[^\n]+/,Xs=/(?!\s*\])(?:\\[\s\S]|[^\[\]\\])+/,Wd=M(/^ {0,3}\[(label)\]: *(?:\n[ \t]*)?([^<\s][^\s]*|<.*?>)(?:(?: +(?:\n[ \t]*)?| *\n[ \t]*)(title))? *(?:\n+|$)/).replace("label",Xs).replace("title",/(?:"(?:\\"?|[^"\\])*"|'[^'\n]*(?:\n[^'\n]+)*\n?'|\([^()]*\))/).getRegex(),Vd=M(/^( {0,3}bull)([ \t][^\n]+?)?(?:\n|$)/).replace(/bull/g,Js).getRegex(),hn="address|article|aside|base|basefont|blockquote|body|caption|center|col|colgroup|dd|details|dialog|dir|div|dl|dt|fieldset|figcaption|figure|footer|form|frame|frameset|h[1-6]|head|header|hr|html|iframe|legend|li|link|main|menu|menuitem|meta|nav|noframes|ol|optgroup|option|p|param|search|section|summary|table|tbody|td|tfoot|th|thead|title|tr|track|ul",ei=/|$))/,Gd=M("^ {0,3}(?:<(script|pre|style|textarea)[\\s>][\\s\\S]*?(?:\\1>[^\\n]*\\n+|$)|comment[^\\n]*(\\n+|$)|<\\?[\\s\\S]*?(?:\\?>\\n*|$)|\\n*|$)|\\n*|$)|?(tag)(?: +|\\n|/?>)[\\s\\S]*?(?:(?:\\n[ ]*)+\\n|$)|<(?!script|pre|style|textarea)([a-z][\\w-]*)(?:attribute)*? */?>(?=[ \\t]*(?:\\n|$))[\\s\\S]*?(?:(?:\\n[ ]*)+\\n|$)|(?!script|pre|style|textarea)[a-z][\\w-]*\\s*>(?=[ \\t]*(?:\\n|$))[\\s\\S]*?(?:(?:\\n[ ]*)+\\n|$))","i").replace("comment",ei).replace("tag",hn).replace("attribute",/ +[a-zA-Z:_][\w.:-]*(?: *= *"[^"\n]*"| *= *'[^'\n]*'| *= *[^\s"'=<>`]+)?/).getRegex(),Va=M(Zs).replace("hr",Tt).replace("heading"," {0,3}#{1,6}(?:\\s|$)").replace("|lheading","").replace("|table","").replace("blockquote"," {0,3}>").replace("fences"," {0,3}(?:`{3,}(?=[^`\\n]*\\n)|~{3,})[^\\n]*\\n").replace("list"," {0,3}(?:[*+-]|1[.)]) ").replace("html","?(?:tag)(?: +|\\n|/?>)|<(?:script|pre|style|textarea|!--)").replace("tag",hn).getRegex(),Yd=M(/^( {0,3}> ?(paragraph|[^\n]*)(?:\n|$))+/).replace("paragraph",Va).getRegex(),ti={blockquote:Yd,code:Kd,def:Wd,fences:Hd,heading:zd,hr:Tt,html:Gd,lheading:Wa,list:Vd,newline:Ud,paragraph:Va,table:mt,text:qd},fo=M("^ *([^\\n ].*)\\n {0,3}((?:\\| *)?:?-+:? *(?:\\| *:?-+:? *)*(?:\\| *)?)(?:\\n((?:(?! *\\n|hr|heading|blockquote|code|fences|list|html).*(?:\\n|$))*)\\n*|$)").replace("hr",Tt).replace("heading"," {0,3}#{1,6}(?:\\s|$)").replace("blockquote"," {0,3}>").replace("code","(?: {4}| {0,3} )[^\\n]").replace("fences"," {0,3}(?:`{3,}(?=[^`\\n]*\\n)|~{3,})[^\\n]*\\n").replace("list"," {0,3}(?:[*+-]|1[.)]) ").replace("html","?(?:tag)(?: +|\\n|/?>)|<(?:script|pre|style|textarea|!--)").replace("tag",hn).getRegex(),Qd={...ti,lheading:jd,table:fo,paragraph:M(Zs).replace("hr",Tt).replace("heading"," {0,3}#{1,6}(?:\\s|$)").replace("|lheading","").replace("table",fo).replace("blockquote"," {0,3}>").replace("fences"," {0,3}(?:`{3,}(?=[^`\\n]*\\n)|~{3,})[^\\n]*\\n").replace("list"," {0,3}(?:[*+-]|1[.)]) ").replace("html","?(?:tag)(?: +|\\n|/?>)|<(?:script|pre|style|textarea|!--)").replace("tag",hn).getRegex()},Jd={...ti,html:M(`^ *(?:comment *(?:\\n|\\s*$)|<(tag)[\\s\\S]+?\\1> *(?:\\n{2,}|\\s*$)| \\s]*)*?/?> *(?:\\n{2,}|\\s*$))`).replace("comment",ei).replace(/tag/g,"(?!(?:a|em|strong|small|s|cite|q|dfn|abbr|data|time|code|var|samp|kbd|sub|sup|i|b|u|mark|ruby|rt|rp|bdi|bdo|span|br|wbr|ins|del|img)\\b)\\w+(?!:|[^\\w\\s@]*@)\\b").getRegex(),def:/^ *\[([^\]]+)\]: *([^\s>]+)>?(?: +(["(][^\n]+[")]))? *(?:\n+|$)/,heading:/^(#{1,6})(.*)(?:\n+|$)/,fences:mt,lheading:/^(.+?)\n {0,3}(=+|-+) *(?:\n+|$)/,paragraph:M(Zs).replace("hr",Tt).replace("heading",` *#{1,6} *[^
+]`).replace("lheading",Wa).replace("|table","").replace("blockquote"," {0,3}>").replace("|fences","").replace("|list","").replace("|html","").replace("|tag","").getRegex()},Zd=/^\\([!"#$%&'()*+,\-./:;<=>?@\[\]\\^_`{|}~])/,Xd=/^(`+)([^`]|[^`][\s\S]*?[^`])\1(?!`)/,Ga=/^( {2,}|\\)\n(?!\s*$)/,eu=/^(`+|[^`])(?:(?= {2,}\n)|[\s\S]*?(?:(?=[\\`+)[^`]+\k(?!`))*?\]\((?:\\[\s\S]|[^\\\(\)]|\((?:\\[\s\S]|[^\\\(\)])*\))*\)/).replace("precode-",Fd?"(?`+)[^`]+\k(?!`)/).replace("html",/<(?! )[^<>]*?>/).getRegex(),Ja=/^(?:\*+(?:((?!\*)punct)|[^\s*]))|^_+(?:((?!_)punct)|([^\s_]))/,ou=M(Ja,"u").replace(/punct/g,gn).getRegex(),au=M(Ja,"u").replace(/punct/g,Qa).getRegex(),Za="^[^_*]*?__[^_*]*?\\*[^_*]*?(?=__)|[^*]+(?=[^*])|(?!\\*)punct(\\*+)(?=[\\s]|$)|notPunctSpace(\\*+)(?!\\*)(?=punctSpace|$)|(?!\\*)punctSpace(\\*+)(?=notPunctSpace)|[\\s](\\*+)(?!\\*)(?=punct)|(?!\\*)punct(\\*+)(?!\\*)(?=punct)|notPunctSpace(\\*+)(?=notPunctSpace)",ru=M(Za,"gu").replace(/notPunctSpace/g,Ya).replace(/punctSpace/g,ni).replace(/punct/g,gn).getRegex(),lu=M(Za,"gu").replace(/notPunctSpace/g,su).replace(/punctSpace/g,nu).replace(/punct/g,Qa).getRegex(),cu=M("^[^_*]*?\\*\\*[^_*]*?_[^_*]*?(?=\\*\\*)|[^_]+(?=[^_])|(?!_)punct(_+)(?=[\\s]|$)|notPunctSpace(_+)(?!_)(?=punctSpace|$)|(?!_)punctSpace(_+)(?=notPunctSpace)|[\\s](_+)(?!_)(?=punct)|(?!_)punct(_+)(?!_)(?=punct)","gu").replace(/notPunctSpace/g,Ya).replace(/punctSpace/g,ni).replace(/punct/g,gn).getRegex(),du=M(/\\(punct)/,"gu").replace(/punct/g,gn).getRegex(),uu=M(/^<(scheme:[^\s\x00-\x1f<>]*|email)>/).replace("scheme",/[a-zA-Z][a-zA-Z0-9+.-]{1,31}/).replace("email",/[a-zA-Z0-9.!#$%&'*+/=?^_`{|}~-]+(@)[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)+(?![-_])/).getRegex(),pu=M(ei).replace("(?:-->|$)","-->").getRegex(),fu=M("^comment|^[a-zA-Z][\\w:-]*\\s*>|^<[a-zA-Z][\\w-]*(?:attribute)*?\\s*/?>|^<\\?[\\s\\S]*?\\?>|^|^").replace("comment",pu).replace("attribute",/\s+[a-zA-Z:_][\w.:-]*(?:\s*=\s*"[^"]*"|\s*=\s*'[^']*'|\s*=\s*[^\s"'=<>`]+)?/).getRegex(),Jt=/(?:\[(?:\\[\s\S]|[^\[\]\\])*\]|\\[\s\S]|`+[^`]*?`+(?!`)|[^\[\]\\`])*?/,hu=M(/^!?\[(label)\]\(\s*(href)(?:(?:[ \t]*(?:\n[ \t]*)?)(title))?\s*\)/).replace("label",Jt).replace("href",/<(?:\\.|[^\n<>\\])+>|[^ \t\n\x00-\x1f]*/).replace("title",/"(?:\\"?|[^"\\])*"|'(?:\\'?|[^'\\])*'|\((?:\\\)?|[^)\\])*\)/).getRegex(),Xa=M(/^!?\[(label)\]\[(ref)\]/).replace("label",Jt).replace("ref",Xs).getRegex(),er=M(/^!?\[(ref)\](?:\[\])?/).replace("ref",Xs).getRegex(),gu=M("reflink|nolink(?!\\()","g").replace("reflink",Xa).replace("nolink",er).getRegex(),ho=/[hH][tT][tT][pP][sS]?|[fF][tT][pP]/,si={_backpedal:mt,anyPunctuation:du,autolink:uu,blockSkip:iu,br:Ga,code:Xd,del:mt,emStrongLDelim:ou,emStrongRDelimAst:ru,emStrongRDelimUnd:cu,escape:Zd,link:hu,nolink:er,punctuation:tu,reflink:Xa,reflinkSearch:gu,tag:fu,text:eu,url:mt},vu={...si,link:M(/^!?\[(label)\]\((.*?)\)/).replace("label",Jt).getRegex(),reflink:M(/^!?\[(label)\]\s*\[([^\]]*)\]/).replace("label",Jt).getRegex()},ms={...si,emStrongRDelimAst:lu,emStrongLDelim:au,url:M(/^((?:protocol):\/\/|www\.)(?:[a-zA-Z0-9\-]+\.?)+[^\s<]*|^email/).replace("protocol",ho).replace("email",/[A-Za-z0-9._+-]+(@)[a-zA-Z0-9-_]+(?:\.[a-zA-Z0-9-_]*[a-zA-Z0-9])+(?![-_])/).getRegex(),_backpedal:/(?:[^?!.,:;*_'"~()&]+|\([^)]*\)|&(?![a-zA-Z0-9]+;$)|[?!.,:;*_'"~)]+(?!$))+/,del:/^(~~?)(?=[^\s~])((?:\\[\s\S]|[^\\])*?(?:\\[\s\S]|[^\s~\\]))\1(?=[^~]|$)/,text:M(/^([`~]+|[^`~])(?:(?= {2,}\n)|(?=[a-zA-Z0-9.!#$%&'*+\/=?_`{\|}~-]+@)|[\s\S]*?(?:(?=[\\":">",'"':""","'":"'"},go=e=>bu[e];function ve(e,t){if(t){if(Y.escapeTest.test(e))return e.replace(Y.escapeReplace,go)}else if(Y.escapeTestNoEncode.test(e))return e.replace(Y.escapeReplaceNoEncode,go);return e}function vo(e){try{e=encodeURI(e).replace(Y.percentDecode,"%")}catch{return null}return e}function mo(e,t){let n=e.replace(Y.findPipe,(o,a,c)=>{let r=!1,p=a;for(;--p>=0&&c[p]==="\\";)r=!r;return r?"|":" |"}),s=n.split(Y.splitPipe),i=0;if(s[0].trim()||s.shift(),s.length>0&&!s.at(-1)?.trim()&&s.pop(),t)if(s.length>t)s.splice(t);else for(;s.length0?-2:-1}function bo(e,t,n,s,i){let o=t.href,a=t.title||null,c=e[1].replace(i.other.outputLinkReplace,"$1");s.state.inLink=!0;let r={type:e[0].charAt(0)==="!"?"image":"link",raw:n,href:o,title:a,text:c,tokens:s.inlineTokens(c)};return s.state.inLink=!1,r}function wu(e,t,n){let s=e.match(n.other.indentCodeCompensation);if(s===null)return t;let i=s[1];return t.split(`
+`).map(o=>{let a=o.match(n.other.beginningSpace);if(a===null)return o;let[c]=a;return c.length>=i.length?o.slice(i.length):o}).join(`
+`)}var Zt=class{options;rules;lexer;constructor(e){this.options=e||Fe}space(e){let t=this.rules.block.newline.exec(e);if(t&&t[0].length>0)return{type:"space",raw:t[0]}}code(e){let t=this.rules.block.code.exec(e);if(t){let n=t[0].replace(this.rules.other.codeRemoveIndent,"");return{type:"code",raw:t[0],codeBlockStyle:"indented",text:this.options.pedantic?n:ft(n,`
+`)}}}fences(e){let t=this.rules.block.fences.exec(e);if(t){let n=t[0],s=wu(n,t[3]||"",this.rules);return{type:"code",raw:n,lang:t[2]?t[2].trim().replace(this.rules.inline.anyPunctuation,"$1"):t[2],text:s}}}heading(e){let t=this.rules.block.heading.exec(e);if(t){let n=t[2].trim();if(this.rules.other.endingHash.test(n)){let s=ft(n,"#");(this.options.pedantic||!s||this.rules.other.endingSpaceChar.test(s))&&(n=s.trim())}return{type:"heading",raw:t[0],depth:t[1].length,text:n,tokens:this.lexer.inline(n)}}}hr(e){let t=this.rules.block.hr.exec(e);if(t)return{type:"hr",raw:ft(t[0],`
+`)}}blockquote(e){let t=this.rules.block.blockquote.exec(e);if(t){let n=ft(t[0],`
+`).split(`
+`),s="",i="",o=[];for(;n.length>0;){let a=!1,c=[],r;for(r=0;r1,i={type:"list",raw:"",ordered:s,start:s?+n.slice(0,-1):"",loose:!1,items:[]};n=s?`\\d{1,9}\\${n.slice(-1)}`:`\\${n}`,this.options.pedantic&&(n=s?n:"[*+-]");let o=this.rules.other.listItemRegex(n),a=!1;for(;e;){let r=!1,p="",l="";if(!(t=o.exec(e))||this.rules.block.hr.test(e))break;p=t[0],e=e.substring(p.length);let u=t[2].split(`
+`,1)[0].replace(this.rules.other.listReplaceTabs,$=>" ".repeat(3*$.length)),h=e.split(`
+`,1)[0],v=!u.trim(),w=0;if(this.options.pedantic?(w=2,l=u.trimStart()):v?w=t[1].length+1:(w=t[2].search(this.rules.other.nonSpaceChar),w=w>4?1:w,l=u.slice(w),w+=t[1].length),v&&this.rules.other.blankLine.test(h)&&(p+=h+`
+`,e=e.substring(h.length+1),r=!0),!r){let $=this.rules.other.nextBulletRegex(w),x=this.rules.other.hrRegex(w),E=this.rules.other.fencesBeginRegex(w),I=this.rules.other.headingBeginRegex(w),R=this.rules.other.htmlBeginRegex(w);for(;e;){let C=e.split(`
+`,1)[0],A;if(h=C,this.options.pedantic?(h=h.replace(this.rules.other.listReplaceNesting," "),A=h):A=h.replace(this.rules.other.tabCharGlobal," "),E.test(h)||I.test(h)||R.test(h)||$.test(h)||x.test(h))break;if(A.search(this.rules.other.nonSpaceChar)>=w||!h.trim())l+=`
+`+A.slice(w);else{if(v||u.replace(this.rules.other.tabCharGlobal," ").search(this.rules.other.nonSpaceChar)>=4||E.test(u)||I.test(u)||x.test(u))break;l+=`
+`+h}!v&&!h.trim()&&(v=!0),p+=C+`
+`,e=e.substring(C.length+1),u=A.slice(w)}}i.loose||(a?i.loose=!0:this.rules.other.doubleBlankLine.test(p)&&(a=!0)),i.items.push({type:"list_item",raw:p,task:!!this.options.gfm&&this.rules.other.listIsTask.test(l),loose:!1,text:l,tokens:[]}),i.raw+=p}let c=i.items.at(-1);if(c)c.raw=c.raw.trimEnd(),c.text=c.text.trimEnd();else return;i.raw=i.raw.trimEnd();for(let r of i.items){if(this.lexer.state.top=!1,r.tokens=this.lexer.blockTokens(r.text,[]),r.task){if(r.text=r.text.replace(this.rules.other.listReplaceTask,""),r.tokens[0]?.type==="text"||r.tokens[0]?.type==="paragraph"){r.tokens[0].raw=r.tokens[0].raw.replace(this.rules.other.listReplaceTask,""),r.tokens[0].text=r.tokens[0].text.replace(this.rules.other.listReplaceTask,"");for(let l=this.lexer.inlineQueue.length-1;l>=0;l--)if(this.rules.other.listIsTask.test(this.lexer.inlineQueue[l].src)){this.lexer.inlineQueue[l].src=this.lexer.inlineQueue[l].src.replace(this.rules.other.listReplaceTask,"");break}}let p=this.rules.other.listTaskCheckbox.exec(r.raw);if(p){let l={type:"checkbox",raw:p[0]+" ",checked:p[0]!=="[ ]"};r.checked=l.checked,i.loose?r.tokens[0]&&["paragraph","text"].includes(r.tokens[0].type)&&"tokens"in r.tokens[0]&&r.tokens[0].tokens?(r.tokens[0].raw=l.raw+r.tokens[0].raw,r.tokens[0].text=l.raw+r.tokens[0].text,r.tokens[0].tokens.unshift(l)):r.tokens.unshift({type:"paragraph",raw:l.raw,text:l.raw,tokens:[l]}):r.tokens.unshift(l)}}if(!i.loose){let p=r.tokens.filter(u=>u.type==="space"),l=p.length>0&&p.some(u=>this.rules.other.anyLine.test(u.raw));i.loose=l}}if(i.loose)for(let r of i.items){r.loose=!0;for(let p of r.tokens)p.type==="text"&&(p.type="paragraph")}return i}}html(e){let t=this.rules.block.html.exec(e);if(t)return{type:"html",block:!0,raw:t[0],pre:t[1]==="pre"||t[1]==="script"||t[1]==="style",text:t[0]}}def(e){let t=this.rules.block.def.exec(e);if(t){let n=t[1].toLowerCase().replace(this.rules.other.multipleSpaceGlobal," "),s=t[2]?t[2].replace(this.rules.other.hrefBrackets,"$1").replace(this.rules.inline.anyPunctuation,"$1"):"",i=t[3]?t[3].substring(1,t[3].length-1).replace(this.rules.inline.anyPunctuation,"$1"):t[3];return{type:"def",tag:n,raw:t[0],href:s,title:i}}}table(e){let t=this.rules.block.table.exec(e);if(!t||!this.rules.other.tableDelimiter.test(t[2]))return;let n=mo(t[1]),s=t[2].replace(this.rules.other.tableAlignChars,"").split("|"),i=t[3]?.trim()?t[3].replace(this.rules.other.tableRowBlankLine,"").split(`
+`):[],o={type:"table",raw:t[0],header:[],align:[],rows:[]};if(n.length===s.length){for(let a of s)this.rules.other.tableAlignRight.test(a)?o.align.push("right"):this.rules.other.tableAlignCenter.test(a)?o.align.push("center"):this.rules.other.tableAlignLeft.test(a)?o.align.push("left"):o.align.push(null);for(let a=0;a({text:c,tokens:this.lexer.inline(c),header:!1,align:o.align[r]})));return o}}lheading(e){let t=this.rules.block.lheading.exec(e);if(t)return{type:"heading",raw:t[0],depth:t[2].charAt(0)==="="?1:2,text:t[1],tokens:this.lexer.inline(t[1])}}paragraph(e){let t=this.rules.block.paragraph.exec(e);if(t){let n=t[1].charAt(t[1].length-1)===`
+`?t[1].slice(0,-1):t[1];return{type:"paragraph",raw:t[0],text:n,tokens:this.lexer.inline(n)}}}text(e){let t=this.rules.block.text.exec(e);if(t)return{type:"text",raw:t[0],text:t[0],tokens:this.lexer.inline(t[0])}}escape(e){let t=this.rules.inline.escape.exec(e);if(t)return{type:"escape",raw:t[0],text:t[1]}}tag(e){let t=this.rules.inline.tag.exec(e);if(t)return!this.lexer.state.inLink&&this.rules.other.startATag.test(t[0])?this.lexer.state.inLink=!0:this.lexer.state.inLink&&this.rules.other.endATag.test(t[0])&&(this.lexer.state.inLink=!1),!this.lexer.state.inRawBlock&&this.rules.other.startPreScriptTag.test(t[0])?this.lexer.state.inRawBlock=!0:this.lexer.state.inRawBlock&&this.rules.other.endPreScriptTag.test(t[0])&&(this.lexer.state.inRawBlock=!1),{type:"html",raw:t[0],inLink:this.lexer.state.inLink,inRawBlock:this.lexer.state.inRawBlock,block:!1,text:t[0]}}link(e){let t=this.rules.inline.link.exec(e);if(t){let n=t[2].trim();if(!this.options.pedantic&&this.rules.other.startAngleBracket.test(n)){if(!this.rules.other.endAngleBracket.test(n))return;let o=ft(n.slice(0,-1),"\\");if((n.length-o.length)%2===0)return}else{let o=yu(t[2],"()");if(o===-2)return;if(o>-1){let a=(t[0].indexOf("!")===0?5:4)+t[1].length+o;t[2]=t[2].substring(0,o),t[0]=t[0].substring(0,a).trim(),t[3]=""}}let s=t[2],i="";if(this.options.pedantic){let o=this.rules.other.pedanticHrefTitle.exec(s);o&&(s=o[1],i=o[3])}else i=t[3]?t[3].slice(1,-1):"";return s=s.trim(),this.rules.other.startAngleBracket.test(s)&&(this.options.pedantic&&!this.rules.other.endAngleBracket.test(n)?s=s.slice(1):s=s.slice(1,-1)),bo(t,{href:s&&s.replace(this.rules.inline.anyPunctuation,"$1"),title:i&&i.replace(this.rules.inline.anyPunctuation,"$1")},t[0],this.lexer,this.rules)}}reflink(e,t){let n;if((n=this.rules.inline.reflink.exec(e))||(n=this.rules.inline.nolink.exec(e))){let s=(n[2]||n[1]).replace(this.rules.other.multipleSpaceGlobal," "),i=t[s.toLowerCase()];if(!i){let o=n[0].charAt(0);return{type:"text",raw:o,text:o}}return bo(n,i,n[0],this.lexer,this.rules)}}emStrong(e,t,n=""){let s=this.rules.inline.emStrongLDelim.exec(e);if(!(!s||s[3]&&n.match(this.rules.other.unicodeAlphaNumeric))&&(!(s[1]||s[2])||!n||this.rules.inline.punctuation.exec(n))){let i=[...s[0]].length-1,o,a,c=i,r=0,p=s[0][0]==="*"?this.rules.inline.emStrongRDelimAst:this.rules.inline.emStrongRDelimUnd;for(p.lastIndex=0,t=t.slice(-1*e.length+i);(s=p.exec(t))!=null;){if(o=s[1]||s[2]||s[3]||s[4]||s[5]||s[6],!o)continue;if(a=[...o].length,s[3]||s[4]){c+=a;continue}else if((s[5]||s[6])&&i%3&&!((i+a)%3)){r+=a;continue}if(c-=a,c>0)continue;a=Math.min(a,a+c+r);let l=[...s[0]][0].length,u=e.slice(0,i+s.index+l+a);if(Math.min(i,a)%2){let v=u.slice(1,-1);return{type:"em",raw:u,text:v,tokens:this.lexer.inlineTokens(v)}}let h=u.slice(2,-2);return{type:"strong",raw:u,text:h,tokens:this.lexer.inlineTokens(h)}}}}codespan(e){let t=this.rules.inline.code.exec(e);if(t){let n=t[2].replace(this.rules.other.newLineCharGlobal," "),s=this.rules.other.nonSpaceChar.test(n),i=this.rules.other.startingSpaceChar.test(n)&&this.rules.other.endingSpaceChar.test(n);return s&&i&&(n=n.substring(1,n.length-1)),{type:"codespan",raw:t[0],text:n}}}br(e){let t=this.rules.inline.br.exec(e);if(t)return{type:"br",raw:t[0]}}del(e){let t=this.rules.inline.del.exec(e);if(t)return{type:"del",raw:t[0],text:t[2],tokens:this.lexer.inlineTokens(t[2])}}autolink(e){let t=this.rules.inline.autolink.exec(e);if(t){let n,s;return t[2]==="@"?(n=t[1],s="mailto:"+n):(n=t[1],s=n),{type:"link",raw:t[0],text:n,href:s,tokens:[{type:"text",raw:n,text:n}]}}}url(e){let t;if(t=this.rules.inline.url.exec(e)){let n,s;if(t[2]==="@")n=t[0],s="mailto:"+n;else{let i;do i=t[0],t[0]=this.rules.inline._backpedal.exec(t[0])?.[0]??"";while(i!==t[0]);n=t[0],t[1]==="www."?s="http://"+t[0]:s=t[0]}return{type:"link",raw:t[0],text:n,href:s,tokens:[{type:"text",raw:n,text:n}]}}}inlineText(e){let t=this.rules.inline.text.exec(e);if(t){let n=this.lexer.state.inRawBlock;return{type:"text",raw:t[0],text:t[0],escaped:n}}}},se=class bs{tokens;options;state;inlineQueue;tokenizer;constructor(t){this.tokens=[],this.tokens.links=Object.create(null),this.options=t||Fe,this.options.tokenizer=this.options.tokenizer||new Zt,this.tokenizer=this.options.tokenizer,this.tokenizer.options=this.options,this.tokenizer.lexer=this,this.inlineQueue=[],this.state={inLink:!1,inRawBlock:!1,top:!0};let n={other:Y,block:Ut.normal,inline:pt.normal};this.options.pedantic?(n.block=Ut.pedantic,n.inline=pt.pedantic):this.options.gfm&&(n.block=Ut.gfm,this.options.breaks?n.inline=pt.breaks:n.inline=pt.gfm),this.tokenizer.rules=n}static get rules(){return{block:Ut,inline:pt}}static lex(t,n){return new bs(n).lex(t)}static lexInline(t,n){return new bs(n).inlineTokens(t)}lex(t){t=t.replace(Y.carriageReturn,`
+`),this.blockTokens(t,this.tokens);for(let n=0;n(i=a.call({lexer:this},t,n))?(t=t.substring(i.raw.length),n.push(i),!0):!1))continue;if(i=this.tokenizer.space(t)){t=t.substring(i.raw.length);let a=n.at(-1);i.raw.length===1&&a!==void 0?a.raw+=`
+`:n.push(i);continue}if(i=this.tokenizer.code(t)){t=t.substring(i.raw.length);let a=n.at(-1);a?.type==="paragraph"||a?.type==="text"?(a.raw+=(a.raw.endsWith(`
+`)?"":`
+`)+i.raw,a.text+=`
+`+i.text,this.inlineQueue.at(-1).src=a.text):n.push(i);continue}if(i=this.tokenizer.fences(t)){t=t.substring(i.raw.length),n.push(i);continue}if(i=this.tokenizer.heading(t)){t=t.substring(i.raw.length),n.push(i);continue}if(i=this.tokenizer.hr(t)){t=t.substring(i.raw.length),n.push(i);continue}if(i=this.tokenizer.blockquote(t)){t=t.substring(i.raw.length),n.push(i);continue}if(i=this.tokenizer.list(t)){t=t.substring(i.raw.length),n.push(i);continue}if(i=this.tokenizer.html(t)){t=t.substring(i.raw.length),n.push(i);continue}if(i=this.tokenizer.def(t)){t=t.substring(i.raw.length);let a=n.at(-1);a?.type==="paragraph"||a?.type==="text"?(a.raw+=(a.raw.endsWith(`
+`)?"":`
+`)+i.raw,a.text+=`
+`+i.raw,this.inlineQueue.at(-1).src=a.text):this.tokens.links[i.tag]||(this.tokens.links[i.tag]={href:i.href,title:i.title},n.push(i));continue}if(i=this.tokenizer.table(t)){t=t.substring(i.raw.length),n.push(i);continue}if(i=this.tokenizer.lheading(t)){t=t.substring(i.raw.length),n.push(i);continue}let o=t;if(this.options.extensions?.startBlock){let a=1/0,c=t.slice(1),r;this.options.extensions.startBlock.forEach(p=>{r=p.call({lexer:this},c),typeof r=="number"&&r>=0&&(a=Math.min(a,r))}),a<1/0&&a>=0&&(o=t.substring(0,a+1))}if(this.state.top&&(i=this.tokenizer.paragraph(o))){let a=n.at(-1);s&&a?.type==="paragraph"?(a.raw+=(a.raw.endsWith(`
+`)?"":`
+`)+i.raw,a.text+=`
+`+i.text,this.inlineQueue.pop(),this.inlineQueue.at(-1).src=a.text):n.push(i),s=o.length!==t.length,t=t.substring(i.raw.length);continue}if(i=this.tokenizer.text(t)){t=t.substring(i.raw.length);let a=n.at(-1);a?.type==="text"?(a.raw+=(a.raw.endsWith(`
+`)?"":`
+`)+i.raw,a.text+=`
+`+i.text,this.inlineQueue.pop(),this.inlineQueue.at(-1).src=a.text):n.push(i);continue}if(t){let a="Infinite loop on byte: "+t.charCodeAt(0);if(this.options.silent){console.error(a);break}else throw new Error(a)}}return this.state.top=!0,n}inline(t,n=[]){return this.inlineQueue.push({src:t,tokens:n}),n}inlineTokens(t,n=[]){let s=t,i=null;if(this.tokens.links){let r=Object.keys(this.tokens.links);if(r.length>0)for(;(i=this.tokenizer.rules.inline.reflinkSearch.exec(s))!=null;)r.includes(i[0].slice(i[0].lastIndexOf("[")+1,-1))&&(s=s.slice(0,i.index)+"["+"a".repeat(i[0].length-2)+"]"+s.slice(this.tokenizer.rules.inline.reflinkSearch.lastIndex))}for(;(i=this.tokenizer.rules.inline.anyPunctuation.exec(s))!=null;)s=s.slice(0,i.index)+"++"+s.slice(this.tokenizer.rules.inline.anyPunctuation.lastIndex);let o;for(;(i=this.tokenizer.rules.inline.blockSkip.exec(s))!=null;)o=i[2]?i[2].length:0,s=s.slice(0,i.index+o)+"["+"a".repeat(i[0].length-o-2)+"]"+s.slice(this.tokenizer.rules.inline.blockSkip.lastIndex);s=this.options.hooks?.emStrongMask?.call({lexer:this},s)??s;let a=!1,c="";for(;t;){a||(c=""),a=!1;let r;if(this.options.extensions?.inline?.some(l=>(r=l.call({lexer:this},t,n))?(t=t.substring(r.raw.length),n.push(r),!0):!1))continue;if(r=this.tokenizer.escape(t)){t=t.substring(r.raw.length),n.push(r);continue}if(r=this.tokenizer.tag(t)){t=t.substring(r.raw.length),n.push(r);continue}if(r=this.tokenizer.link(t)){t=t.substring(r.raw.length),n.push(r);continue}if(r=this.tokenizer.reflink(t,this.tokens.links)){t=t.substring(r.raw.length);let l=n.at(-1);r.type==="text"&&l?.type==="text"?(l.raw+=r.raw,l.text+=r.text):n.push(r);continue}if(r=this.tokenizer.emStrong(t,s,c)){t=t.substring(r.raw.length),n.push(r);continue}if(r=this.tokenizer.codespan(t)){t=t.substring(r.raw.length),n.push(r);continue}if(r=this.tokenizer.br(t)){t=t.substring(r.raw.length),n.push(r);continue}if(r=this.tokenizer.del(t)){t=t.substring(r.raw.length),n.push(r);continue}if(r=this.tokenizer.autolink(t)){t=t.substring(r.raw.length),n.push(r);continue}if(!this.state.inLink&&(r=this.tokenizer.url(t))){t=t.substring(r.raw.length),n.push(r);continue}let p=t;if(this.options.extensions?.startInline){let l=1/0,u=t.slice(1),h;this.options.extensions.startInline.forEach(v=>{h=v.call({lexer:this},u),typeof h=="number"&&h>=0&&(l=Math.min(l,h))}),l<1/0&&l>=0&&(p=t.substring(0,l+1))}if(r=this.tokenizer.inlineText(p)){t=t.substring(r.raw.length),r.raw.slice(-1)!=="_"&&(c=r.raw.slice(-1)),a=!0;let l=n.at(-1);l?.type==="text"?(l.raw+=r.raw,l.text+=r.text):n.push(r);continue}if(t){let l="Infinite loop on byte: "+t.charCodeAt(0);if(this.options.silent){console.error(l);break}else throw new Error(l)}}return n}},Xt=class{options;parser;constructor(e){this.options=e||Fe}space(e){return""}code({text:e,lang:t,escaped:n}){let s=(t||"").match(Y.notSpaceStart)?.[0],i=e.replace(Y.endingNewline,"")+`
+`;return s?''+(n?i:ve(i,!0))+`
+`:""+(n?i:ve(i,!0))+`
+`}blockquote({tokens:e}){return`
+${this.parser.parse(e)}
+`}html({text:e}){return e}def(e){return""}heading({tokens:e,depth:t}){return`${this.parser.parseInline(e)}
+`}hr(e){return`
+`}list(e){let t=e.ordered,n=e.start,s="";for(let a=0;a
+`+s+""+i+`>
+`}listitem(e){return`${this.parser.parse(e.tokens)}
+`}checkbox({checked:e}){return" '}paragraph({tokens:e}){return`${this.parser.parseInline(e)}
+`}table(e){let t="",n="";for(let i=0;i${s}`),`
+
+`+t+`
+`+s+`
+`}tablerow({text:e}){return`
+${e}
+`}tablecell(e){let t=this.parser.parseInline(e.tokens),n=e.header?"th":"td";return(e.align?`<${n} align="${e.align}">`:`<${n}>`)+t+`${n}>
+`}strong({tokens:e}){return`${this.parser.parseInline(e)}`}em({tokens:e}){return`${this.parser.parseInline(e)}`}codespan({text:e}){return`${ve(e,!0)}`}br(e){return"
"}del({tokens:e}){return`${this.parser.parseInline(e)}`}link({href:e,title:t,tokens:n}){let s=this.parser.parseInline(n),i=vo(e);if(i===null)return s;e=i;let o='"+s+"",o}image({href:e,title:t,text:n,tokens:s}){s&&(n=this.parser.parseInline(s,this.parser.textRenderer));let i=vo(e);if(i===null)return ve(n);e=i;let o=`
",o}text(e){return"tokens"in e&&e.tokens?this.parser.parseInline(e.tokens):"escaped"in e&&e.escaped?e.text:ve(e.text)}},ii=class{strong({text:e}){return e}em({text:e}){return e}codespan({text:e}){return e}del({text:e}){return e}html({text:e}){return e}text({text:e}){return e}link({text:e}){return""+e}image({text:e}){return""+e}br(){return""}checkbox({raw:e}){return e}},ie=class ys{options;renderer;textRenderer;constructor(t){this.options=t||Fe,this.options.renderer=this.options.renderer||new Xt,this.renderer=this.options.renderer,this.renderer.options=this.options,this.renderer.parser=this,this.textRenderer=new ii}static parse(t,n){return new ys(n).parse(t)}static parseInline(t,n){return new ys(n).parseInline(t)}parse(t){let n="";for(let s=0;s{let a=i[o].flat(1/0);n=n.concat(this.walkTokens(a,t))}):i.tokens&&(n=n.concat(this.walkTokens(i.tokens,t)))}}return n}use(...e){let t=this.defaults.extensions||{renderers:{},childTokens:{}};return e.forEach(n=>{let s={...n};if(s.async=this.defaults.async||s.async||!1,n.extensions&&(n.extensions.forEach(i=>{if(!i.name)throw new Error("extension name required");if("renderer"in i){let o=t.renderers[i.name];o?t.renderers[i.name]=function(...a){let c=i.renderer.apply(this,a);return c===!1&&(c=o.apply(this,a)),c}:t.renderers[i.name]=i.renderer}if("tokenizer"in i){if(!i.level||i.level!=="block"&&i.level!=="inline")throw new Error("extension level must be 'block' or 'inline'");let o=t[i.level];o?o.unshift(i.tokenizer):t[i.level]=[i.tokenizer],i.start&&(i.level==="block"?t.startBlock?t.startBlock.push(i.start):t.startBlock=[i.start]:i.level==="inline"&&(t.startInline?t.startInline.push(i.start):t.startInline=[i.start]))}"childTokens"in i&&i.childTokens&&(t.childTokens[i.name]=i.childTokens)}),s.extensions=t),n.renderer){let i=this.defaults.renderer||new Xt(this.defaults);for(let o in n.renderer){if(!(o in i))throw new Error(`renderer '${o}' does not exist`);if(["options","parser"].includes(o))continue;let a=o,c=n.renderer[a],r=i[a];i[a]=(...p)=>{let l=c.apply(i,p);return l===!1&&(l=r.apply(i,p)),l||""}}s.renderer=i}if(n.tokenizer){let i=this.defaults.tokenizer||new Zt(this.defaults);for(let o in n.tokenizer){if(!(o in i))throw new Error(`tokenizer '${o}' does not exist`);if(["options","rules","lexer"].includes(o))continue;let a=o,c=n.tokenizer[a],r=i[a];i[a]=(...p)=>{let l=c.apply(i,p);return l===!1&&(l=r.apply(i,p)),l}}s.tokenizer=i}if(n.hooks){let i=this.defaults.hooks||new ht;for(let o in n.hooks){if(!(o in i))throw new Error(`hook '${o}' does not exist`);if(["options","block"].includes(o))continue;let a=o,c=n.hooks[a],r=i[a];ht.passThroughHooks.has(o)?i[a]=p=>{if(this.defaults.async&&ht.passThroughHooksRespectAsync.has(o))return(async()=>{let u=await c.call(i,p);return r.call(i,u)})();let l=c.call(i,p);return r.call(i,l)}:i[a]=(...p)=>{if(this.defaults.async)return(async()=>{let u=await c.apply(i,p);return u===!1&&(u=await r.apply(i,p)),u})();let l=c.apply(i,p);return l===!1&&(l=r.apply(i,p)),l}}s.hooks=i}if(n.walkTokens){let i=this.defaults.walkTokens,o=n.walkTokens;s.walkTokens=function(a){let c=[];return c.push(o.call(this,a)),i&&(c=c.concat(i.call(this,a))),c}}this.defaults={...this.defaults,...s}}),this}setOptions(e){return this.defaults={...this.defaults,...e},this}lexer(e,t){return se.lex(e,t??this.defaults)}parser(e,t){return ie.parse(e,t??this.defaults)}parseMarkdown(e){return(t,n)=>{let s={...n},i={...this.defaults,...s},o=this.onError(!!i.silent,!!i.async);if(this.defaults.async===!0&&s.async===!1)return o(new Error("marked(): The async option was set to true by an extension. Remove async: false from the parse options object to return a Promise."));if(typeof t>"u"||t===null)return o(new Error("marked(): input parameter is undefined or null"));if(typeof t!="string")return o(new Error("marked(): input parameter is of type "+Object.prototype.toString.call(t)+", string expected"));if(i.hooks&&(i.hooks.options=i,i.hooks.block=e),i.async)return(async()=>{let a=i.hooks?await i.hooks.preprocess(t):t,c=await(i.hooks?await i.hooks.provideLexer():e?se.lex:se.lexInline)(a,i),r=i.hooks?await i.hooks.processAllTokens(c):c;i.walkTokens&&await Promise.all(this.walkTokens(r,i.walkTokens));let p=await(i.hooks?await i.hooks.provideParser():e?ie.parse:ie.parseInline)(r,i);return i.hooks?await i.hooks.postprocess(p):p})().catch(o);try{i.hooks&&(t=i.hooks.preprocess(t));let a=(i.hooks?i.hooks.provideLexer():e?se.lex:se.lexInline)(t,i);i.hooks&&(a=i.hooks.processAllTokens(a)),i.walkTokens&&this.walkTokens(a,i.walkTokens);let c=(i.hooks?i.hooks.provideParser():e?ie.parse:ie.parseInline)(a,i);return i.hooks&&(c=i.hooks.postprocess(c)),c}catch(a){return o(a)}}}onError(e,t){return n=>{if(n.message+=`
+Please report this to https://github.com/markedjs/marked.`,e){let s="An error occurred:
"+ve(n.message+"",!0)+"
";return t?Promise.resolve(s):s}if(t)return Promise.reject(n);throw n}}},Be=new $u;function P(e,t){return Be.parse(e,t)}P.options=P.setOptions=function(e){return Be.setOptions(e),P.defaults=Be.defaults,ja(P.defaults),P};P.getDefaults=Qs;P.defaults=Fe;P.use=function(...e){return Be.use(...e),P.defaults=Be.defaults,ja(P.defaults),P};P.walkTokens=function(e,t){return Be.walkTokens(e,t)};P.parseInline=Be.parseInline;P.Parser=ie;P.parser=ie.parse;P.Renderer=Xt;P.TextRenderer=ii;P.Lexer=se;P.lexer=se.lex;P.Tokenizer=Zt;P.Hooks=ht;P.parse=P;P.options;P.setOptions;P.use;P.walkTokens;P.parseInline;ie.parse;se.lex;P.setOptions({gfm:!0,breaks:!0,mangle:!1});const yo=["a","b","blockquote","br","code","del","em","h1","h2","h3","h4","hr","i","li","ol","p","pre","strong","table","tbody","td","th","thead","tr","ul"],wo=["class","href","rel","target","title","start"];let $o=!1;const ku=14e4,xu=4e4;function Au(){$o||($o=!0,vs.addHook("afterSanitizeAttributes",e=>{!(e instanceof HTMLAnchorElement)||!e.getAttribute("href")||(e.setAttribute("rel","noreferrer noopener"),e.setAttribute("target","_blank"))}))}function ws(e){const t=e.trim();if(!t)return"";Au();const n=na(t,ku),s=n.truncated?`
+
+… truncated (${n.total} chars, showing first ${n.text.length}).`:"";if(n.text.length>xu){const a=`${Su(`${n.text}${s}`)}`;return vs.sanitize(a,{ALLOWED_TAGS:yo,ALLOWED_ATTR:wo})}const i=P.parse(`${n.text}${s}`);return vs.sanitize(i,{ALLOWED_TAGS:yo,ALLOWED_ATTR:wo})}function Su(e){return e.replace(/&/g,"&").replace(//g,">").replace(/"/g,""").replace(/'/g,"'")}function _u(e,t){return d``}function Kt(e,t){e&&(e.textContent=t)}const Tu=1500,Eu=2e3,tr="Copy as markdown",Cu="Copied",Iu="Copy failed",Qn="📋",Lu="✓",Ru="!";async function Mu(e){if(!e)return!1;try{return await navigator.clipboard.writeText(e),!0}catch{return!1}}function Ht(e,t){e.title=t,e.setAttribute("aria-label",t)}function Pu(e){const t=e.label??tr;return d`
+
+ `}function Nu(e){return Pu({text:()=>e,label:tr})}const Ou={emoji:"🧩",detailKeys:["command","path","url","targetUrl","targetId","ref","element","node","nodeId","id","requestId","to","channelId","guildId","userId","name","query","pattern","messageId"]},Du={bash:{emoji:"🛠️",title:"Bash",detailKeys:["command"]},process:{emoji:"🧰",title:"Process",detailKeys:["sessionId"]},read:{emoji:"📖",title:"Read",detailKeys:["path"]},write:{emoji:"✍️",title:"Write",detailKeys:["path"]},edit:{emoji:"📝",title:"Edit",detailKeys:["path"]},attach:{emoji:"📎",title:"Attach",detailKeys:["path","url","fileName"]},browser:{emoji:"🌐",title:"Browser",actions:{status:{label:"status"},start:{label:"start"},stop:{label:"stop"},tabs:{label:"tabs"},open:{label:"open",detailKeys:["targetUrl"]},focus:{label:"focus",detailKeys:["targetId"]},close:{label:"close",detailKeys:["targetId"]},snapshot:{label:"snapshot",detailKeys:["targetUrl","targetId","ref","element","format"]},screenshot:{label:"screenshot",detailKeys:["targetUrl","targetId","ref","element"]},navigate:{label:"navigate",detailKeys:["targetUrl","targetId"]},console:{label:"console",detailKeys:["level","targetId"]},pdf:{label:"pdf",detailKeys:["targetId"]},upload:{label:"upload",detailKeys:["paths","ref","inputRef","element","targetId"]},dialog:{label:"dialog",detailKeys:["accept","promptText","targetId"]},act:{label:"act",detailKeys:["request.kind","request.ref","request.selector","request.text","request.value"]}}},canvas:{emoji:"🖼️",title:"Canvas",actions:{present:{label:"present",detailKeys:["target","node","nodeId"]},hide:{label:"hide",detailKeys:["node","nodeId"]},navigate:{label:"navigate",detailKeys:["url","node","nodeId"]},eval:{label:"eval",detailKeys:["javaScript","node","nodeId"]},snapshot:{label:"snapshot",detailKeys:["format","node","nodeId"]},a2ui_push:{label:"A2UI push",detailKeys:["jsonlPath","node","nodeId"]},a2ui_reset:{label:"A2UI reset",detailKeys:["node","nodeId"]}}},nodes:{emoji:"📱",title:"Nodes",actions:{status:{label:"status"},describe:{label:"describe",detailKeys:["node","nodeId"]},pending:{label:"pending"},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_list:{label:"camera list",detailKeys:["node","nodeId"]},camera_clip:{label:"camera clip",detailKeys:["node","nodeId","facing","duration","durationMs"]},screen_record:{label:"screen record",detailKeys:["node","nodeId","duration","durationMs","fps","screenIndex"]}}},cron:{emoji:"⏰",title:"Cron",actions:{status:{label:"status"},list:{label:"list"},add:{label:"add",detailKeys:["job.name","job.id","job.schedule","job.cron"]},update:{label:"update",detailKeys:["id"]},remove:{label:"remove",detailKeys:["id"]},run:{label:"run",detailKeys:["id"]},runs:{label:"runs",detailKeys:["id"]},wake:{label:"wake",detailKeys:["text","mode"]}}},gateway:{emoji:"🔌",title:"Gateway",actions:{restart:{label:"restart",detailKeys:["reason","delayMs"]},"config.get":{label:"config get"},"config.schema":{label:"config schema"},"config.apply":{label:"config apply",detailKeys:["restartDelayMs"]},"update.run":{label:"update run",detailKeys:["restartDelayMs"]}}},whatsapp_login:{emoji:"🟢",title:"WhatsApp Login",actions:{start:{label:"start"},wait:{label:"wait"}}},discord:{emoji:"💬",title:"Discord",actions:{react:{label:"react",detailKeys:["channelId","messageId","emoji"]},reactions:{label:"reactions",detailKeys:["channelId","messageId"]},sticker:{label:"sticker",detailKeys:["to","stickerIds"]},poll:{label:"poll",detailKeys:["question","to"]},permissions:{label:"permissions",detailKeys:["channelId"]},readMessages:{label:"read messages",detailKeys:["channelId","limit"]},sendMessage:{label:"send",detailKeys:["to","content"]},editMessage:{label:"edit",detailKeys:["channelId","messageId"]},deleteMessage:{label:"delete",detailKeys:["channelId","messageId"]},threadCreate:{label:"thread create",detailKeys:["channelId","name"]},threadList:{label:"thread list",detailKeys:["guildId","channelId"]},threadReply:{label:"thread reply",detailKeys:["channelId","content"]},pinMessage:{label:"pin",detailKeys:["channelId","messageId"]},unpinMessage:{label:"unpin",detailKeys:["channelId","messageId"]},listPins:{label:"list pins",detailKeys:["channelId"]},searchMessages:{label:"search",detailKeys:["guildId","content"]},memberInfo:{label:"member",detailKeys:["guildId","userId"]},roleInfo:{label:"roles",detailKeys:["guildId"]},emojiList:{label:"emoji list",detailKeys:["guildId"]},roleAdd:{label:"role add",detailKeys:["guildId","userId","roleId"]},roleRemove:{label:"role remove",detailKeys:["guildId","userId","roleId"]},channelInfo:{label:"channel",detailKeys:["channelId"]},channelList:{label:"channels",detailKeys:["guildId"]},voiceStatus:{label:"voice",detailKeys:["guildId","userId"]},eventList:{label:"events",detailKeys:["guildId"]},eventCreate:{label:"event create",detailKeys:["guildId","name"]},timeout:{label:"timeout",detailKeys:["guildId","userId"]},kick:{label:"kick",detailKeys:["guildId","userId"]},ban:{label:"ban",detailKeys:["guildId","userId"]}}},slack:{emoji:"💬",title:"Slack",actions:{react:{label:"react",detailKeys:["channelId","messageId","emoji"]},reactions:{label:"reactions",detailKeys:["channelId","messageId"]},sendMessage:{label:"send",detailKeys:["to","content"]},editMessage:{label:"edit",detailKeys:["channelId","messageId"]},deleteMessage:{label:"delete",detailKeys:["channelId","messageId"]},readMessages:{label:"read messages",detailKeys:["channelId","limit"]},pinMessage:{label:"pin",detailKeys:["channelId","messageId"]},unpinMessage:{label:"unpin",detailKeys:["channelId","messageId"]},listPins:{label:"list pins",detailKeys:["channelId"]},memberInfo:{label:"member",detailKeys:["userId"]},emojiList:{label:"emoji list"}}}},Bu={fallback:Ou,tools:Du},nr=Bu,ko=nr.fallback??{emoji:"🧩"},Fu=nr.tools??{};function Uu(e){return(e??"tool").trim()}function Ku(e){const t=e.replace(/_/g," ").trim();return t?t.split(/\s+/).map(n=>n.length<=2&&n.toUpperCase()===n?n:`${n.at(0)?.toUpperCase()??""}${n.slice(1)}`).join(" "):"Tool"}function Hu(e){const t=e?.trim();if(t)return t.replace(/_/g," ")}function sr(e){if(e!=null){if(typeof e=="string"){const t=e.trim();if(!t)return;const n=t.split(/\r?\n/)[0]?.trim()??"";return n?n.length>160?`${n.slice(0,157)}…`:n:void 0}if(typeof e=="number"||typeof e=="boolean")return String(e);if(Array.isArray(e)){const t=e.map(s=>sr(s)).filter(s=>!!s);if(t.length===0)return;const n=t.slice(0,3).join(", ");return t.length>3?`${n}…`:n}}}function zu(e,t){if(!e||typeof e!="object")return;let n=e;for(const s of t.split(".")){if(!s||!n||typeof n!="object")return;n=n[s]}return n}function ju(e,t){for(const n of t){const s=zu(e,n),i=sr(s);if(i)return i}}function qu(e){if(!e||typeof e!="object")return;const t=e,n=typeof t.path=="string"?t.path:void 0;if(!n)return;const s=typeof t.offset=="number"?t.offset:void 0,i=typeof t.limit=="number"?t.limit:void 0;return s!==void 0&&i!==void 0?`${n}:${s}-${s+i}`:n}function Wu(e){if(!e||typeof e!="object")return;const t=e;return typeof t.path=="string"?t.path:void 0}function Vu(e,t){if(!(!e||!t))return e.actions?.[t]??void 0}function Gu(e){const t=Uu(e.name),n=t.toLowerCase(),s=Fu[n],i=s?.emoji??ko.emoji??"🧩",o=s?.title??Ku(t),a=s?.label??t,c=e.args&&typeof e.args=="object"?e.args.action:void 0,r=typeof c=="string"?c.trim():void 0,p=Vu(s,r),l=Hu(p?.label??r);let u;n==="read"&&(u=qu(e.args)),!u&&(n==="write"||n==="edit"||n==="attach")&&(u=Wu(e.args));const h=p?.detailKeys??s?.detailKeys??ko.detailKeys??[];return!u&&h.length>0&&(u=ju(e.args,h)),!u&&e.meta&&(u=e.meta),u&&(u=Qu(u)),{name:t,emoji:i,title:o,label:a,verb:l,detail:u}}function Yu(e){const t=[];if(e.verb&&t.push(e.verb),e.detail&&t.push(e.detail),t.length!==0)return t.join(" · ")}function Qu(e){return e&&e.replace(/\/Users\/[^/]+/g,"~").replace(/\/home\/[^/]+/g,"~")}const Ju=80,Zu=2,xo=100;function Xu(e){const t=e.trim();if(t.startsWith("{")||t.startsWith("["))try{const n=JSON.parse(t);return"```json\n"+JSON.stringify(n,null,2)+"\n```"}catch{}return e}function ep(e){const t=e.split(`
+`),n=t.slice(0,Zu),s=n.join(`
+`);return s.length>xo?s.slice(0,xo)+"…":n.lengthi.kind==="result")){const i=typeof t.toolName=="string"&&t.toolName||typeof t.tool_name=="string"&&t.tool_name||"tool",o=an(e)??void 0;s.push({kind:"result",name:i,text:o})}return s}function Ao(e,t){const n=Gu({name:e.name,args:e.args}),s=Yu(n),i=!!e.text?.trim(),o=!!t,a=o?()=>{if(i){t(Xu(e.text));return}const u=`## ${n.label}
+
+${s?`**Command:** \`${s}\`
+
+`:""}*No output — tool completed successfully.*`;t(u)}:void 0,c=i&&(e.text?.length??0)<=Ju,r=i&&!c,p=i&&c,l=!i;return d`
+ {u.key!=="Enter"&&u.key!==" "||(u.preventDefault(),a?.())}:g}
+ >
+
+
+ ${n.emoji}
+ ${n.label}
+
+ ${o?d`${i?"View ›":"›"}`:g}
+ ${l&&!o?d`✓`:g}
+
+ ${s?d`${s}`:g}
+ ${l?d`Completed`:g}
+ ${r?d`${ep(e.text)}`:g}
+ ${p?d`${e.text}`:g}
+
+ `}function np(e){return Array.isArray(e)?e.filter(Boolean):[]}function sp(e){if(typeof e!="string")return e;const t=e.trim();if(!t||!t.startsWith("{")&&!t.startsWith("["))return e;try{return JSON.parse(t)}catch{return e}}function ip(e){if(typeof e.text=="string")return e.text;if(typeof e.content=="string")return e.content}function op(e){return d`
+
+ ${oi("assistant",e)}
+
+
+ `}function ap(e,t,n,s){const i=new Date(t).toLocaleTimeString([],{hour:"numeric",minute:"2-digit"}),o=s?.name??"Assistant";return d`
+
+ ${oi("assistant",s)}
+
+
+ `}function rp(e,t){const n=Ys(e.role),s=t.assistantName??"Assistant",i=n==="user"?"You":n==="assistant"?s:n,o=n==="user"?"user":n==="assistant"?"assistant":"other",a=new Date(e.timestamp).toLocaleTimeString([],{hour:"numeric",minute:"2-digit"});return d`
+
+ ${oi(e.role,{name:s,avatar:t.assistantAvatar??null})}
+
+
+ `}function oi(e,t){const n=Ys(e),s=t?.name?.trim()||"Assistant",i=t?.avatar?.trim()||"",o=n==="user"?"U":n==="assistant"?s.charAt(0).toUpperCase()||"A":n==="tool"?"⚙":"?",a=n==="user"?"user":n==="assistant"?"assistant":n==="tool"?"tool":"other";return i&&n==="assistant"?lp(i)?d`
`:d`${i}`:d`${o}`}function lp(e){return/^https?:\/\//i.test(e)||/^data:image\//i.test(e)}function ir(e,t,n){const s=e,i=typeof s.role=="string"?s.role:"unknown",o=Fa(e)||i.toLowerCase()==="toolresult"||i.toLowerCase()==="tool_result"||typeof s.toolCallId=="string"||typeof s.tool_call_id=="string",a=tp(e),c=a.length>0,r=an(e),p=t.showReasoning&&i==="assistant"?vl(e):null,l=r?.trim()?r:null,u=p?bl(p):null,h=l,v=i==="assistant"&&!!h?.trim(),w=["chat-bubble",v?"has-copy":"",t.isStreaming?"streaming":"","fade-in"].filter(Boolean).join(" ");return!h&&c&&o?d`${a.map($=>Ao($,n))}`:!h&&!c?g:d`
+
+ ${v?Nu(h):g}
+ ${u?d`${ps(ws(u))}`:g}
+ ${h?d`${ps(ws(h))}`:g}
+ ${a.map($=>Ao($,n))}
+
+ `}function cp(e){return d`
+
+ `}var dp=Object.defineProperty,up=Object.getOwnPropertyDescriptor,vn=(e,t,n,s)=>{for(var i=s>1?void 0:s?up(t,n):t,o=e.length-1,a;o>=0;o--)(a=e[o])&&(i=(s?a(t,n,i):a(i))||i);return s&&i&&dp(t,n,i),i};let et=class extends Ye{constructor(){super(...arguments),this.splitRatio=.6,this.minRatio=.4,this.maxRatio=.7,this.isDragging=!1,this.startX=0,this.startRatio=0,this.handleMouseDown=e=>{this.isDragging=!0,this.startX=e.clientX,this.startRatio=this.splitRatio,this.classList.add("dragging"),document.addEventListener("mousemove",this.handleMouseMove),document.addEventListener("mouseup",this.handleMouseUp),e.preventDefault()},this.handleMouseMove=e=>{if(!this.isDragging)return;const t=this.parentElement;if(!t)return;const n=t.getBoundingClientRect().width,i=(e.clientX-this.startX)/n;let o=this.startRatio+i;o=Math.max(this.minRatio,Math.min(this.maxRatio,o)),this.dispatchEvent(new CustomEvent("resize",{detail:{splitRatio:o},bubbles:!0,composed:!0}))},this.handleMouseUp=()=>{this.isDragging=!1,this.classList.remove("dragging"),document.removeEventListener("mousemove",this.handleMouseMove),document.removeEventListener("mouseup",this.handleMouseUp)}}render(){return d``}connectedCallback(){super.connectedCallback(),this.addEventListener("mousedown",this.handleMouseDown)}disconnectedCallback(){super.disconnectedCallback(),this.removeEventListener("mousedown",this.handleMouseDown),document.removeEventListener("mousemove",this.handleMouseMove),document.removeEventListener("mouseup",this.handleMouseUp)}};et.styles=Rr`
+ :host {
+ width: 4px;
+ cursor: col-resize;
+ background: var(--border, #333);
+ transition: background 150ms ease-out;
+ flex-shrink: 0;
+ position: relative;
+ }
+
+ :host::before {
+ content: "";
+ position: absolute;
+ top: 0;
+ left: -4px;
+ right: -4px;
+ bottom: 0;
+ }
+
+ :host(:hover) {
+ background: var(--accent, #007bff);
+ }
+
+ :host(.dragging) {
+ background: var(--accent, #007bff);
+ }
+ `;vn([sn({type:Number})],et.prototype,"splitRatio",2);vn([sn({type:Number})],et.prototype,"minRatio",2);vn([sn({type:Number})],et.prototype,"maxRatio",2);et=vn([Yo("resizable-divider")],et);function pp(e){const t=e.connected,n=e.sending||e.stream!==null,i=e.sessions?.sessions?.find(l=>l.key===e.sessionKey)?.reasoningLevel??"off",o=e.showThinking&&i!=="off",a={name:e.assistantName,avatar:e.assistantAvatar??e.assistantAvatarUrl??null},c=e.connected?"Message (↩ to send, Shift+↩ for line breaks)":"Connect to the gateway to start chatting…",r=e.splitRatio??.6,p=!!(e.sidebarOpen&&e.onCloseSidebar);return d`
+
+ ${e.disabledReason?d`${e.disabledReason}`:g}
+
+ ${e.error?d`${e.error}`:g}
+
+ ${e.focusMode?d`
+
+ `:g}
+
+
+
+
+ ${e.loading?d`Loading chat…`:g}
+ ${Da(hp(e),l=>l.key,l=>l.kind==="reading-indicator"?op(a):l.kind==="stream"?ap(l.text,l.startedAt,e.onOpenSidebar,a):l.kind==="group"?rp(l,{onOpenSidebar:e.onOpenSidebar,showReasoning:o,assistantName:e.assistantName,assistantAvatar:a.avatar}):g)}
+
+
+
+ ${p?d`
+ e.onSplitRatioChange?.(l.detail.splitRatio)}
+ >
+
+ `:g}
+
+
+ ${e.queue.length?d`
+
+ Queued (${e.queue.length})
+
+ ${e.queue.map(l=>d`
+
+ ${l.text}
+
+
+ `)}
+
+
+ `:g}
+
+
+
+
+
+
+
+
+
+ `}const So=200;function fp(e){const t=[];let n=null;for(const s of e){if(s.kind!=="message"){n&&(t.push(n),n=null),t.push(s);continue}const i=Ba(s.message),o=Ys(i.role),a=i.timestamp||Date.now();!n||n.role!==o?(n&&t.push(n),n={kind:"group",key:`group:${o}:${s.key}`,role:o,messages:[{message:s.message,key:s.key}],timestamp:a,isStreaming:!1}):n.messages.push({message:s.message,key:s.key})}return n&&t.push(n),t}function hp(e){const t=[],n=Array.isArray(e.messages)?e.messages:[],s=Array.isArray(e.toolMessages)?e.toolMessages:[],i=Math.max(0,n.length-So);i>0&&t.push({kind:"message",key:"chat:history:notice",message:{role:"system",content:`Showing last ${So} messages (${i} hidden).`,timestamp:Date.now()}});for(let o=i;o0?t.push({kind:"stream",key:o,text:e.stream,startedAt:e.streamStartedAt??Date.now()}):t.push({kind:"reading-indicator",key:o})}return fp(t)}function _o(e,t){const n=e,s=typeof n.toolCallId=="string"?n.toolCallId:"";if(s)return`tool:${s}`;const i=typeof n.id=="string"?n.id:"";if(i)return`msg:${i}`;const o=typeof n.messageId=="string"?n.messageId:"";if(o)return`msg:${o}`;const a=typeof n.timestamp=="number"?n.timestamp:null,c=typeof n.role=="string"?n.role:"unknown",p=an(e)??(typeof n.content=="string"?n.content:null)??gp(e)??String(t),l=vp(p);return a?`msg:${c}:${a}:${l}`:`msg:${c}:${l}`}function gp(e){try{return JSON.stringify(e)}catch{return null}}function vp(e){let t=2166136261;for(let n=0;n>>0).toString(36)}function de(e){if(e)return Array.isArray(e.type)?e.type.filter(n=>n!=="null")[0]??e.type[0]:e.type}function or(e){if(!e)return"";if(e.default!==void 0)return e.default;switch(de(e)){case"object":return{};case"array":return[];case"boolean":return!1;case"number":case"integer":return 0;case"string":return"";default:return""}}function mn(e){return e.filter(t=>typeof t=="string").join(".")}function ee(e,t){const n=mn(e),s=t[n];if(s)return s;const i=n.split(".");for(const[o,a]of Object.entries(t)){if(!o.includes("*"))continue;const c=o.split(".");if(c.length!==i.length)continue;let r=!0;for(let p=0;pt.toUpperCase())}function mp(e){const t=mn(e).toLowerCase();return t.includes("token")||t.includes("password")||t.includes("secret")||t.includes("apikey")||t.endsWith("key")}const bp=new Set(["title","description","default","nullable"]);function yp(e){return Object.keys(e??{}).filter(n=>!bp.has(n)).length===0}function wp(e){if(e===void 0)return"";try{return JSON.stringify(e,null,2)??""}catch{return""}}const At={chevronDown:d``,plus:d``,minus:d``,trash:d``,edit:d``};function be(e){const{schema:t,value:n,path:s,hints:i,unsupported:o,disabled:a,onPatch:c}=e,r=e.showLabel??!0,p=de(t),l=ee(s,i),u=l?.label??t.title??ye(String(s.at(-1))),h=l?.help??t.description,v=mn(s);if(o.has(v))return d`
+ ${u}
+ Unsupported schema node. Use Raw mode.
+ `;if(t.anyOf||t.oneOf){const $=(t.anyOf??t.oneOf??[]).filter(A=>!(A.type==="null"||Array.isArray(A.type)&&A.type.includes("null")));if($.length===1)return be({...e,schema:$[0]});const x=A=>{if(A.const!==void 0)return A.const;if(A.enum&&A.enum.length===1)return A.enum[0]},E=$.map(x),I=E.every(A=>A!==void 0);if(I&&E.length>0&&E.length<=5){const A=n??t.default;return d`
+
+ ${r?d``:g}
+ ${h?d`${h}`:g}
+
+ ${E.map((B,ue)=>d`
+
+ `)}
+
+
+ `}if(I&&E.length>5)return Eo({...e,options:E,value:n??t.default});const R=new Set($.map(A=>de(A)).filter(Boolean)),C=new Set([...R].map(A=>A==="integer"?"number":A));if([...C].every(A=>["string","number","boolean"].includes(A))){const A=C.has("string"),B=C.has("number");if(C.has("boolean")&&C.size===1)return be({...e,schema:{...t,type:"boolean",anyOf:void 0,oneOf:void 0}});if(A||B)return To({...e,inputType:B&&!A?"number":"text"})}}if(t.enum){const w=t.enum;if(w.length<=5){const $=n??t.default;return d`
+
+ ${r?d``:g}
+ ${h?d`${h}`:g}
+
+ ${w.map(x=>d`
+
+ `)}
+
+
+ `}return Eo({...e,options:w,value:n??t.default})}if(p==="object")return kp(e);if(p==="array")return xp(e);if(p==="boolean"){const w=typeof n=="boolean"?n:typeof t.default=="boolean"?t.default:!1;return d`
+
+ `}return p==="number"||p==="integer"?$p(e):p==="string"?To({...e,inputType:"text"}):d`
+
+ ${u}
+ Unsupported type: ${p}. Use Raw mode.
+
+ `}function To(e){const{schema:t,value:n,path:s,hints:i,disabled:o,onPatch:a,inputType:c}=e,r=e.showLabel??!0,p=ee(s,i),l=p?.label??t.title??ye(String(s.at(-1))),u=p?.help??t.description,h=p?.sensitive??mp(s),v=p?.placeholder??(h?"••••":t.default!==void 0?`Default: ${t.default}`:""),w=n??"";return d`
+
+ ${r?d``:g}
+ ${u?d`${u}`:g}
+
+ {const x=$.target.value;if(c==="number"){if(x.trim()===""){a(s,void 0);return}const E=Number(x);a(s,Number.isNaN(E)?x:E);return}a(s,x)}}
+ />
+ ${t.default!==void 0?d`
+
+ `:g}
+
+
+ `}function $p(e){const{schema:t,value:n,path:s,hints:i,disabled:o,onPatch:a}=e,c=e.showLabel??!0,r=ee(s,i),p=r?.label??t.title??ye(String(s.at(-1))),l=r?.help??t.description,u=n??t.default??"",h=typeof u=="number"?u:0;return d`
+
+ ${c?d``:g}
+ ${l?d`${l}`:g}
+
+
+ {const w=v.target.value,$=w===""?void 0:Number(w);a(s,$)}}
+ />
+
+
+
+ `}function Eo(e){const{schema:t,value:n,path:s,hints:i,disabled:o,options:a,onPatch:c}=e,r=e.showLabel??!0,p=ee(s,i),l=p?.label??t.title??ye(String(s.at(-1))),u=p?.help??t.description,h=n??t.default,v=a.findIndex($=>$===h||String($)===String(h)),w="__unset__";return d`
+
+ ${r?d``:g}
+ ${u?d`${u}`:g}
+
+
+ `}function kp(e){const{schema:t,value:n,path:s,hints:i,unsupported:o,disabled:a,onPatch:c}=e;e.showLabel;const r=ee(s,i),p=r?.label??t.title??ye(String(s.at(-1))),l=r?.help??t.description,u=n??t.default,h=u&&typeof u=="object"&&!Array.isArray(u)?u:{},v=t.properties??{},$=Object.entries(v).sort((R,C)=>{const A=ee([...s,R[0]],i)?.order??0,B=ee([...s,C[0]],i)?.order??0;return A!==B?A-B:R[0].localeCompare(C[0])}),x=new Set(Object.keys(v)),E=t.additionalProperties,I=!!E&&typeof E=="object";return s.length===1?d`
+
+ ${$.map(([R,C])=>be({schema:C,value:h[R],path:[...s,R],hints:i,unsupported:o,disabled:a,onPatch:c}))}
+ ${I?Co({schema:E,value:h,path:s,hints:i,unsupported:o,disabled:a,reservedKeys:x,onPatch:c}):g}
+
+ `:d`
+
+
+ ${p}
+ ${At.chevronDown}
+
+ ${l?d`${l}`:g}
+
+ ${$.map(([R,C])=>be({schema:C,value:h[R],path:[...s,R],hints:i,unsupported:o,disabled:a,onPatch:c}))}
+ ${I?Co({schema:E,value:h,path:s,hints:i,unsupported:o,disabled:a,reservedKeys:x,onPatch:c}):g}
+
+
+ `}function xp(e){const{schema:t,value:n,path:s,hints:i,unsupported:o,disabled:a,onPatch:c}=e,r=e.showLabel??!0,p=ee(s,i),l=p?.label??t.title??ye(String(s.at(-1))),u=p?.help??t.description,h=Array.isArray(t.items)?t.items[0]:t.items;if(!h)return d`
+
+ ${l}
+ Unsupported array schema. Use Raw mode.
+
+ `;const v=Array.isArray(n)?n:Array.isArray(t.default)?t.default:[];return d`
+
+
+ ${r?d`${l}`:g}
+ ${v.length} item${v.length!==1?"s":""}
+
+
+ ${u?d`${u}`:g}
+
+ ${v.length===0?d`
+
+ No items yet. Click "Add" to create one.
+
+ `:d`
+
+ ${v.map((w,$)=>d`
+
+
+ #${$+1}
+
+
+
+ ${be({schema:h,value:w,path:[...s,$],hints:i,unsupported:o,disabled:a,showLabel:!1,onPatch:c})}
+
+
+ `)}
+
+ `}
+
+ `}function Co(e){const{schema:t,value:n,path:s,hints:i,unsupported:o,disabled:a,reservedKeys:c,onPatch:r}=e,p=yp(t),l=Object.entries(n??{}).filter(([u])=>!c.has(u));return d`
+
+
+ Custom entries
+
+
+
+ ${l.length===0?d`
+ No custom entries.
+ `:d`
+
+ ${l.map(([u,h])=>{const v=[...s,u],w=wp(h);return d`
+
+
+ {const x=$.target.value.trim();if(!x||x===u)return;const E={...n??{}};x in E||(E[x]=E[u],delete E[u],r(s,E))}}
+ />
+
+
+ ${p?d`
+
+ `:be({schema:t,value:h,path:v,hints:i,unsupported:o,disabled:a,showLabel:!1,onPatch:r})}
+
+
+
+ `})}
+
+ `}
+
+ `}const Io={env:d``,update:d``,agents:d``,auth:d``,channels:d``,messages:d``,commands:d``,hooks:d``,skills:d``,tools:d``,gateway:d``,wizard:d``,meta:d``,logging:d``,browser:d``,ui:d``,models:d``,bindings:d``,broadcast:d``,audio:d``,session:d``,cron:d``,web:d``,discovery:d``,canvasHost:d``,talk:d``,plugins:d``,default:d``},ai={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.)"},messages:{label:"Messages",description:"Message handling and routing settings"},commands:{label:"Commands",description:"Custom slash commands"},hooks:{label:"Hooks",description:"Webhooks and event hooks"},skills:{label:"Skills",description:"Skill packs and capabilities"},tools:{label:"Tools",description:"Tool configurations (browser, search, etc.)"},gateway:{label:"Gateway",description:"Gateway server settings (port, auth, binding)"},wizard:{label:"Setup Wizard",description:"Setup wizard state and history"},meta:{label:"Metadata",description:"Gateway metadata and version information"},logging:{label:"Logging",description:"Log levels and output configuration"},browser:{label:"Browser",description:"Browser automation settings"},ui:{label:"UI",description:"User interface preferences"},models:{label:"Models",description:"AI model configurations and providers"},bindings:{label:"Bindings",description:"Key bindings and shortcuts"},broadcast:{label:"Broadcast",description:"Broadcast and notification settings"},audio:{label:"Audio",description:"Audio input/output settings"},session:{label:"Session",description:"Session management and persistence"},cron:{label:"Cron",description:"Scheduled tasks and automation"},web:{label:"Web",description:"Web server and API settings"},discovery:{label:"Discovery",description:"Service discovery and networking"},canvasHost:{label:"Canvas Host",description:"Canvas rendering and display"},talk:{label:"Talk",description:"Voice and speech settings"},plugins:{label:"Plugins",description:"Plugin management and extensions"}};function Lo(e){return Io[e]??Io.default}function Ap(e,t,n){if(!n)return!0;const s=n.toLowerCase(),i=ai[e];return e.toLowerCase().includes(s)||i&&(i.label.toLowerCase().includes(s)||i.description.toLowerCase().includes(s))?!0:gt(t,s)}function gt(e,t){if(e.title?.toLowerCase().includes(t)||e.description?.toLowerCase().includes(t)||e.enum?.some(s=>String(s).toLowerCase().includes(t)))return!0;if(e.properties){for(const[s,i]of Object.entries(e.properties))if(s.toLowerCase().includes(t)||gt(i,t))return!0}if(e.items){const s=Array.isArray(e.items)?e.items:[e.items];for(const i of s)if(i&>(i,t))return!0}if(e.additionalProperties&&typeof e.additionalProperties=="object"&>(e.additionalProperties,t))return!0;const n=e.anyOf??e.oneOf??e.allOf;if(n){for(const s of n)if(s&>(s,t))return!0}return!1}function Sp(e){if(!e.schema)return d`Schema unavailable.`;const t=e.schema,n=e.value??{};if(de(t)!=="object"||!t.properties)return d`Unsupported schema. Use Raw.`;const s=new Set(e.unsupportedPaths??[]),i=t.properties,o=e.searchQuery??"",a=e.activeSection,c=e.activeSubsection??null;let r=Object.entries(i);a&&(r=r.filter(([l])=>l===a)),o&&(r=r.filter(([l,u])=>Ap(l,u,o))),r.sort((l,u)=>{const h=ee([l[0]],e.uiHints)?.order??50,v=ee([u[0]],e.uiHints)?.order??50;return h!==v?h-v:l[0].localeCompare(u[0])});let p=null;if(a&&c&&r.length===1){const l=r[0]?.[1];l&&de(l)==="object"&&l.properties&&l.properties[c]&&(p={sectionKey:a,subsectionKey:c,schema:l.properties[c]})}return r.length===0?d`
+
+ 🔍
+
+ ${o?`No settings match "${o}"`:"No settings in this section"}
+
+
+ `:d`
+
+ ${p?(()=>{const{sectionKey:l,subsectionKey:u,schema:h}=p,v=ee([l,u],e.uiHints),w=v?.label??h.title??ye(u),$=v?.help??h.description??"",x=n[l],E=x&&typeof x=="object"?x[u]:void 0,I=`config-section-${l}-${u}`;return d`
+
+
+ ${Lo(l)}
+
+ ${w}
+ ${$?d`${$}
`:g}
+
+
+
+ ${be({schema:h,value:E,path:[l,u],hints:e.uiHints,unsupported:s,disabled:e.disabled??!1,showLabel:!1,onPatch:e.onPatch})}
+
+
+ `})():r.map(([l,u])=>{const h=ai[l]??{label:l.charAt(0).toUpperCase()+l.slice(1),description:u.description??""};return d`
+
+
+ ${Lo(l)}
+
+ ${h.label}
+ ${h.description?d`${h.description}
`:g}
+
+
+
+ ${be({schema:u,value:n[l],path:[l],hints:e.uiHints,unsupported:s,disabled:e.disabled??!1,showLabel:!1,onPatch:e.onPatch})}
+
+
+ `})}
+
+ `}const _p=new Set(["title","description","default","nullable"]);function Tp(e){return Object.keys(e??{}).filter(n=>!_p.has(n)).length===0}function ar(e){const t=e.filter(i=>i!=null),n=t.length!==e.length,s=[];for(const i of t)s.some(o=>Object.is(o,i))||s.push(i);return{enumValues:s,nullable:n}}function rr(e){return!e||typeof e!="object"?{schema:null,unsupportedPaths:[""]}:bt(e,[])}function bt(e,t){const n=new Set,s={...e},i=mn(t)||"";if(e.anyOf||e.oneOf||e.allOf){const c=Ep(e,t);return c||{schema:e,unsupportedPaths:[i]}}const o=Array.isArray(e.type)&&e.type.includes("null"),a=de(e)??(e.properties||e.additionalProperties?"object":void 0);if(s.type=a??e.type,s.nullable=o||e.nullable,s.enum){const{enumValues:c,nullable:r}=ar(s.enum);s.enum=c,r&&(s.nullable=!0),c.length===0&&n.add(i)}if(a==="object"){const c=e.properties??{},r={};for(const[p,l]of Object.entries(c)){const u=bt(l,[...t,p]);u.schema&&(r[p]=u.schema);for(const h of u.unsupportedPaths)n.add(h)}if(s.properties=r,e.additionalProperties===!0)n.add(i);else if(e.additionalProperties===!1)s.additionalProperties=!1;else if(e.additionalProperties&&typeof e.additionalProperties=="object"&&!Tp(e.additionalProperties)){const p=bt(e.additionalProperties,[...t,"*"]);s.additionalProperties=p.schema??e.additionalProperties,p.unsupportedPaths.length>0&&n.add(i)}}else if(a==="array"){const c=Array.isArray(e.items)?e.items[0]:e.items;if(!c)n.add(i);else{const r=bt(c,[...t,"*"]);s.items=r.schema??c,r.unsupportedPaths.length>0&&n.add(i)}}else a!=="string"&&a!=="number"&&a!=="integer"&&a!=="boolean"&&!s.enum&&n.add(i);return{schema:s,unsupportedPaths:Array.from(n)}}function Ep(e,t){if(e.allOf)return null;const n=e.anyOf??e.oneOf;if(!n)return null;const s=[],i=[];let o=!1;for(const c of n){if(!c||typeof c!="object")return null;if(Array.isArray(c.enum)){const{enumValues:r,nullable:p}=ar(c.enum);s.push(...r),p&&(o=!0);continue}if("const"in c){if(c.const==null){o=!0;continue}s.push(c.const);continue}if(de(c)==="null"){o=!0;continue}i.push(c)}if(s.length>0&&i.length===0){const c=[];for(const r of s)c.some(p=>Object.is(p,r))||c.push(r);return{schema:{...e,enum:c,nullable:o,anyOf:void 0,oneOf:void 0,allOf:void 0},unsupportedPaths:[]}}if(i.length===1){const c=bt(i[0],t);return c.schema&&(c.schema.nullable=o||c.schema.nullable),c}const a=["string","number","integer","boolean"];return i.length>0&&s.length===0&&i.every(c=>c.type&&a.includes(String(c.type)))?{schema:{...e,nullable:o},unsupportedPaths:[]}:null}const $s={all:d``,env:d``,update:d``,agents:d``,auth:d``,channels:d``,messages:d``,commands:d``,hooks:d``,skills:d``,tools:d``,gateway:d``,wizard:d``,meta:d``,logging:d``,browser:d``,ui:d``,models:d``,bindings:d``,broadcast:d``,audio:d``,session:d``,cron:d``,web:d``,discovery:d``,canvasHost:d``,talk:d``,plugins:d``,default:d``},Ro=[{key:"env",label:"Environment"},{key:"update",label:"Updates"},{key:"agents",label:"Agents"},{key:"auth",label:"Authentication"},{key:"channels",label:"Channels"},{key:"messages",label:"Messages"},{key:"commands",label:"Commands"},{key:"hooks",label:"Hooks"},{key:"skills",label:"Skills"},{key:"tools",label:"Tools"},{key:"gateway",label:"Gateway"},{key:"wizard",label:"Setup Wizard"}],Mo="__all__";function Po(e){return $s[e]??$s.default}function Cp(e,t){const n=ai[e];return n||{label:t?.title??ye(e),description:t?.description??""}}function Ip(e){const{key:t,schema:n,uiHints:s}=e;if(!n||de(n)!=="object"||!n.properties)return[];const i=Object.entries(n.properties).map(([o,a])=>{const c=ee([t,o],s),r=c?.label??a.title??ye(o),p=c?.help??a.description??"",l=c?.order??50;return{key:o,label:r,description:p,order:l}});return i.sort((o,a)=>o.order!==a.order?o.order-a.order:o.key.localeCompare(a.key)),i}function Lp(e,t){if(!e||!t)return[];const n=[];function s(i,o,a){if(i===o)return;if(typeof i!=typeof o){n.push({path:a,from:i,to:o});return}if(typeof i!="object"||i===null||o===null){i!==o&&n.push({path:a,from:i,to:o});return}if(Array.isArray(i)&&Array.isArray(o)){JSON.stringify(i)!==JSON.stringify(o)&&n.push({path:a,from:i,to:o});return}const c=i,r=o,p=new Set([...Object.keys(c),...Object.keys(r)]);for(const l of p)s(c[l],r[l],a?`${a}.${l}`:l)}return s(e,t,""),n}function No(e,t=40){let n;try{n=JSON.stringify(e)??String(e)}catch{n=String(e)}return n.length<=t?n:n.slice(0,t-3)+"..."}function Rp(e){const t=e.valid==null?"unknown":e.valid?"valid":"invalid",n=rr(e.schema),s=n.schema?n.unsupportedPaths.length>0:!1,i=!!e.formValue&&!e.loading&&!s,o=e.connected&&!e.saving&&(e.formMode==="raw"?!0:i),a=e.connected&&!e.applying&&!e.updating&&(e.formMode==="raw"?!0:i),c=e.connected&&!e.applying&&!e.updating,r=n.schema?.properties??{},p=Ro.filter(A=>A.key in r),l=new Set(Ro.map(A=>A.key)),u=Object.keys(r).filter(A=>!l.has(A)).map(A=>({key:A,label:A.charAt(0).toUpperCase()+A.slice(1)})),h=[...p,...u],v=e.activeSection&&n.schema&&de(n.schema)==="object"?n.schema.properties?.[e.activeSection]:void 0,w=e.activeSection?Cp(e.activeSection,v):null,$=e.activeSection?Ip({key:e.activeSection,schema:v,uiHints:e.uiHints}):[],x=e.formMode==="form"&&!!e.activeSection&&$.length>0,E=e.activeSubsection===Mo,I=e.searchQuery||E?null:e.activeSubsection??$[0]?.key??null,R=e.formMode==="form"?Lp(e.originalValue,e.formValue):[],C=R.length>0;return d`
+
+
+
+
+
+
+
+
+
+ ${C?d`
+ ${R.length} unsaved change${R.length!==1?"s":""}
+ `:d`
+ No changes
+ `}
+
+
+
+
+
+
+
+
+
+
+ ${C?d`
+
+
+ View ${R.length} pending change${R.length!==1?"s":""}
+
+
+
+ ${R.map(A=>d`
+
+ ${A.path}
+
+ ${No(A.from)}
+ →
+ ${No(A.to)}
+
+
+ `)}
+
+
+ `:g}
+
+ ${w&&e.formMode==="form"?d`
+
+ ${Po(e.activeSection??"")}
+
+ ${w.label}
+ ${w.description?d`${w.description}`:g}
+
+
+ `:g}
+
+ ${x?d`
+
+ `:g}
+
+
+
+ ${e.formMode==="form"?d`
+ ${e.schemaLoading?d`
+
+ Loading schema…
+ `:Sp({schema:n.schema,uiHints:e.uiHints,value:e.formValue,disabled:e.loading||!e.formValue,unsupportedPaths:n.unsupportedPaths,onPatch:e.onFormPatch,searchQuery:e.searchQuery,activeSection:e.activeSection,activeSubsection:I})}
+ ${s?d`
+ Form view can't safely edit some fields.
+ Use Raw to avoid losing config entries.
+ `:g}
+ `:d`
+
+ `}
+
+
+ ${e.issues.length>0?d`
+ ${JSON.stringify(e.issues,null,2)}
+ `:g}
+
+
+ `}function Mp(e){if(!e&&e!==0)return"n/a";const t=Math.round(e/1e3);if(t<60)return`${t}s`;const n=Math.round(t/60);return n<60?`${n}m`:`${Math.round(n/60)}h`}function Pp(e,t){const n=t.snapshot,s=n?.channels;if(!n||!s)return!1;const i=s[e],o=typeof i?.configured=="boolean"&&i.configured,a=typeof i?.running=="boolean"&&i.running,c=typeof i?.connected=="boolean"&&i.connected,p=(n.channelAccounts?.[e]??[]).some(l=>l.configured||l.running||l.connected);return o||a||c||p}function Np(e,t){return t?.[e]?.length??0}function lr(e,t){const n=Np(e,t);return n<2?g:d`Accounts (${n})`}function Op(e,t){let n=e;for(const s of t){if(!n)return null;const i=de(n);if(i==="object"){const o=n.properties??{};if(typeof s=="string"&&o[s]){n=o[s];continue}const a=n.additionalProperties;if(typeof s=="string"&&a&&typeof a=="object"){n=a;continue}return null}if(i==="array"){if(typeof s!="number")return null;n=(Array.isArray(n.items)?n.items[0]:n.items)??null;continue}return null}return n}function Dp(e,t){const s=(e.channels??{})[t],i=e[t];return(s&&typeof s=="object"?s:null)??(i&&typeof i=="object"?i:null)??{}}function Bp(e){const t=rr(e.schema),n=t.schema;if(!n)return d`Schema unavailable. Use Raw.`;const s=Op(n,["channels",e.channelId]);if(!s)return d`Channel config schema unavailable.`;const i=e.configValue??{},o=Dp(i,e.channelId);return d`
+
+ ${be({schema:s,value:o,path:["channels",e.channelId],hints:e.uiHints,unsupported:new Set(t.unsupportedPaths),disabled:e.disabled,showLabel:!1,onPatch:e.onPatch})}
+
+ `}function _e(e){const{channelId:t,props:n}=e,s=n.configSaving||n.configSchemaLoading;return d`
+
+ ${n.configSchemaLoading?d`Loading config schema…`:Bp({channelId:t,configValue:n.configForm,schema:n.configSchema,uiHints:n.configUiHints,disabled:s,onPatch:n.onConfigPatch})}
+
+
+
+
+
+ `}function Fp(e){const{props:t,discord:n,accountCountLabel:s}=e;return d`
+
+ Discord
+ Bot status and channel configuration.
+ ${s}
+
+
+
+ Configured
+ ${n?.configured?"Yes":"No"}
+
+
+ Running
+ ${n?.running?"Yes":"No"}
+
+
+ Last start
+ ${n?.lastStartAt?O(n.lastStartAt):"n/a"}
+
+
+ Last probe
+ ${n?.lastProbeAt?O(n.lastProbeAt):"n/a"}
+
+
+
+ ${n?.lastError?d`
+ ${n.lastError}
+ `:g}
+
+ ${n?.probe?d`
+ Probe ${n.probe.ok?"ok":"failed"} ·
+ ${n.probe.status??""} ${n.probe.error??""}
+ `:g}
+
+ ${_e({channelId:"discord",props:t})}
+
+
+
+
+
+ `}function Up(e){const{props:t,imessage:n,accountCountLabel:s}=e;return d`
+
+ iMessage
+ macOS bridge status and channel configuration.
+ ${s}
+
+
+
+ Configured
+ ${n?.configured?"Yes":"No"}
+
+
+ Running
+ ${n?.running?"Yes":"No"}
+
+
+ Last start
+ ${n?.lastStartAt?O(n.lastStartAt):"n/a"}
+
+
+ Last probe
+ ${n?.lastProbeAt?O(n.lastProbeAt):"n/a"}
+
+
+
+ ${n?.lastError?d`
+ ${n.lastError}
+ `:g}
+
+ ${n?.probe?d`
+ Probe ${n.probe.ok?"ok":"failed"} ·
+ ${n.probe.error??""}
+ `:g}
+
+ ${_e({channelId:"imessage",props:t})}
+
+
+
+
+
+ `}function Kp(e){const{values:t,original:n}=e;return t.name!==n.name||t.displayName!==n.displayName||t.about!==n.about||t.picture!==n.picture||t.banner!==n.banner||t.website!==n.website||t.nip05!==n.nip05||t.lud16!==n.lud16}function Hp(e){const{state:t,callbacks:n,accountId:s}=e,i=Kp(t),o=(c,r,p={})=>{const{type:l="text",placeholder:u,maxLength:h,help:v}=p,w=t.values[c]??"",$=t.fieldErrors[c],x=`nostr-profile-${c}`;return l==="textarea"?d`
+
+
+
+ ${v?d`${v}`:g}
+ ${$?d`${$}`:g}
+
+ `:d`
+
+
+ {const I=E.target;n.onFieldChange(c,I.value)}}
+ ?disabled=${t.saving}
+ />
+ ${v?d`${v}`:g}
+ ${$?d`${$}`:g}
+
+ `},a=()=>{const c=t.values.picture;return c?d`
+
+
{const p=r.target;p.style.display="none"}}
+ @load=${r=>{const p=r.target;p.style.display="block"}}
+ />
+
+ `:g};return d`
+
+
+ Edit Profile
+ Account: ${s}
+
+
+ ${t.error?d`${t.error}`:g}
+
+ ${t.success?d`${t.success}`:g}
+
+ ${a()}
+
+ ${o("name","Username",{placeholder:"satoshi",maxLength:256,help:"Short username (e.g., satoshi)"})}
+
+ ${o("displayName","Display Name",{placeholder:"Satoshi Nakamoto",maxLength:256,help:"Your full display name"})}
+
+ ${o("about","Bio",{type:"textarea",placeholder:"Tell people about yourself...",maxLength:2e3,help:"A brief bio or description"})}
+
+ ${o("picture","Avatar URL",{type:"url",placeholder:"https://example.com/avatar.jpg",help:"HTTPS URL to your profile picture"})}
+
+ ${t.showAdvanced?d`
+
+ Advanced
+
+ ${o("banner","Banner URL",{type:"url",placeholder:"https://example.com/banner.jpg",help:"HTTPS URL to a banner image"})}
+
+ ${o("website","Website",{type:"url",placeholder:"https://example.com",help:"Your personal website"})}
+
+ ${o("nip05","NIP-05 Identifier",{placeholder:"you@example.com",help:"Verifiable identifier (e.g., you@domain.com)"})}
+
+ ${o("lud16","Lightning Address",{placeholder:"you@getalby.com",help:"Lightning address for tips (LUD-16)"})}
+
+ `:g}
+
+
+
+
+
+
+
+
+
+
+
+ ${i?d`
+ You have unsaved changes
+ `:g}
+
+ `}function zp(e){const t={name:e?.name??"",displayName:e?.displayName??"",about:e?.about??"",picture:e?.picture??"",banner:e?.banner??"",website:e?.website??"",nip05:e?.nip05??"",lud16:e?.lud16??""};return{values:t,original:{...t},saving:!1,importing:!1,error:null,success:null,fieldErrors:{},showAdvanced:!!(e?.banner||e?.website||e?.nip05||e?.lud16)}}function Oo(e){return e?e.length<=20?e:`${e.slice(0,8)}...${e.slice(-8)}`:"n/a"}function jp(e){const{props:t,nostr:n,nostrAccounts:s,accountCountLabel:i,profileFormState:o,profileFormCallbacks:a,onEditProfile:c}=e,r=s[0],p=n?.configured??r?.configured??!1,l=n?.running??r?.running??!1,u=n?.publicKey??r?.publicKey,h=n?.lastStartAt??r?.lastStartAt??null,v=n?.lastError??r?.lastError??null,w=s.length>1,$=o!=null,x=I=>{const R=I.publicKey,C=I.profile,A=C?.displayName??C?.name??I.name??I.accountId;return d`
+
+
+ ${A}
+ ${I.accountId}
+
+
+
+ Running
+ ${I.running?"Yes":"No"}
+
+
+ Configured
+ ${I.configured?"Yes":"No"}
+
+
+ Public Key
+ ${Oo(R)}
+
+
+ Last inbound
+ ${I.lastInboundAt?O(I.lastInboundAt):"n/a"}
+
+ ${I.lastError?d`
+ ${I.lastError}
+ `:g}
+
+
+ `},E=()=>{if($&&a)return Hp({state:o,callbacks:a,accountId:s[0]?.accountId??"default"});const I=r?.profile??n?.profile,{name:R,displayName:C,about:A,picture:B,nip05:ue}=I??{},bn=R||C||A||B||ue;return d`
+
+
+ Profile
+ ${p?d`
+
+ `:g}
+
+ ${bn?d`
+
+ ${B?d`
+
+
{yn.target.style.display="none"}}
+ />
+
+ `:g}
+ ${R?d`Name${R}`:g}
+ ${C?d`Display Name${C}`:g}
+ ${A?d`About`:g}
+ ${ue?d`NIP-05${ue}`:g}
+
+ `:d`
+
+ No profile set. Click "Edit Profile" to add your name, bio, and avatar.
+
+ `}
+
+ `};return d`
+
+ Nostr
+ Decentralized DMs via Nostr relays (NIP-04).
+ ${i}
+
+ ${w?d`
+
+ ${s.map(I=>x(I))}
+
+ `:d`
+
+
+ Configured
+ ${p?"Yes":"No"}
+
+
+ Running
+ ${l?"Yes":"No"}
+
+
+ Public Key
+ ${Oo(u)}
+
+
+ Last start
+ ${h?O(h):"n/a"}
+
+
+ `}
+
+ ${v?d`${v}`:g}
+
+ ${E()}
+
+ ${_e({channelId:"nostr",props:t})}
+
+
+
+
+
+ `}function qp(e){const{props:t,signal:n,accountCountLabel:s}=e;return d`
+
+ Signal
+ signal-cli status and channel configuration.
+ ${s}
+
+
+
+ Configured
+ ${n?.configured?"Yes":"No"}
+
+
+ Running
+ ${n?.running?"Yes":"No"}
+
+
+ Base URL
+ ${n?.baseUrl??"n/a"}
+
+
+ Last start
+ ${n?.lastStartAt?O(n.lastStartAt):"n/a"}
+
+
+ Last probe
+ ${n?.lastProbeAt?O(n.lastProbeAt):"n/a"}
+
+
+
+ ${n?.lastError?d`
+ ${n.lastError}
+ `:g}
+
+ ${n?.probe?d`
+ Probe ${n.probe.ok?"ok":"failed"} ·
+ ${n.probe.status??""} ${n.probe.error??""}
+ `:g}
+
+ ${_e({channelId:"signal",props:t})}
+
+
+
+
+
+ `}function Wp(e){const{props:t,slack:n,accountCountLabel:s}=e;return d`
+
+ Slack
+ Socket mode status and channel configuration.
+ ${s}
+
+
+
+ Configured
+ ${n?.configured?"Yes":"No"}
+
+
+ Running
+ ${n?.running?"Yes":"No"}
+
+
+ Last start
+ ${n?.lastStartAt?O(n.lastStartAt):"n/a"}
+
+
+ Last probe
+ ${n?.lastProbeAt?O(n.lastProbeAt):"n/a"}
+
+
+
+ ${n?.lastError?d`
+ ${n.lastError}
+ `:g}
+
+ ${n?.probe?d`
+ Probe ${n.probe.ok?"ok":"failed"} ·
+ ${n.probe.status??""} ${n.probe.error??""}
+ `:g}
+
+ ${_e({channelId:"slack",props:t})}
+
+
+
+
+
+ `}function Vp(e){const{props:t,telegram:n,telegramAccounts:s,accountCountLabel:i}=e,o=s.length>1,a=c=>{const p=c.probe?.bot?.username,l=c.name||c.accountId;return d`
+
+
+
+ ${p?`@${p}`:l}
+
+ ${c.accountId}
+
+
+
+ Running
+ ${c.running?"Yes":"No"}
+
+
+ Configured
+ ${c.configured?"Yes":"No"}
+
+
+ Last inbound
+ ${c.lastInboundAt?O(c.lastInboundAt):"n/a"}
+
+ ${c.lastError?d`
+
+ ${c.lastError}
+
+ `:g}
+
+
+ `};return d`
+
+ Telegram
+ Bot status and channel configuration.
+ ${i}
+
+ ${o?d`
+
+ ${s.map(c=>a(c))}
+
+ `:d`
+
+
+ Configured
+ ${n?.configured?"Yes":"No"}
+
+
+ Running
+ ${n?.running?"Yes":"No"}
+
+
+ Mode
+ ${n?.mode??"n/a"}
+
+
+ Last start
+ ${n?.lastStartAt?O(n.lastStartAt):"n/a"}
+
+
+ Last probe
+ ${n?.lastProbeAt?O(n.lastProbeAt):"n/a"}
+
+
+ `}
+
+ ${n?.lastError?d`
+ ${n.lastError}
+ `:g}
+
+ ${n?.probe?d`
+ Probe ${n.probe.ok?"ok":"failed"} ·
+ ${n.probe.status??""} ${n.probe.error??""}
+ `:g}
+
+ ${_e({channelId:"telegram",props:t})}
+
+
+
+
+
+ `}function Gp(e){const{props:t,whatsapp:n,accountCountLabel:s}=e;return d`
+
+ WhatsApp
+ Link WhatsApp Web and monitor connection health.
+ ${s}
+
+
+
+ Configured
+ ${n?.configured?"Yes":"No"}
+
+
+ Linked
+ ${n?.linked?"Yes":"No"}
+
+
+ Running
+ ${n?.running?"Yes":"No"}
+
+
+ Connected
+ ${n?.connected?"Yes":"No"}
+
+
+ Last connect
+
+ ${n?.lastConnectedAt?O(n.lastConnectedAt):"n/a"}
+
+
+
+ Last message
+
+ ${n?.lastMessageAt?O(n.lastMessageAt):"n/a"}
+
+
+
+ Auth age
+
+ ${n?.authAgeMs!=null?Mp(n.authAgeMs):"n/a"}
+
+
+
+
+ ${n?.lastError?d`
+ ${n.lastError}
+ `:g}
+
+ ${t.whatsappMessage?d`
+ ${t.whatsappMessage}
+ `:g}
+
+ ${t.whatsappQrDataUrl?d`
+
+ `:g}
+
+
+
+
+
+
+
+
+
+ ${_e({channelId:"whatsapp",props:t})}
+
+ `}function Yp(e){const t=e.snapshot?.channels,n=t?.whatsapp??void 0,s=t?.telegram??void 0,i=t?.discord??null,o=t?.slack??null,a=t?.signal??null,c=t?.imessage??null,r=t?.nostr??null,l=Qp(e.snapshot).map((u,h)=>({key:u,enabled:Pp(u,e),order:h})).sort((u,h)=>u.enabled!==h.enabled?u.enabled?-1:1:u.order-h.order);return d`
+
+ ${l.map(u=>Jp(u.key,e,{whatsapp:n,telegram:s,discord:i,slack:o,signal:a,imessage:c,nostr:r,channelAccounts:e.snapshot?.channelAccounts??null}))}
+
+
+
+
+
+ Channel health
+ Channel status snapshots from the gateway.
+
+ ${e.lastSuccessAt?O(e.lastSuccessAt):"n/a"}
+
+ ${e.lastError?d`
+ ${e.lastError}
+ `:g}
+
+${e.snapshot?JSON.stringify(e.snapshot,null,2):"No snapshot yet."}
+
+
+ `}function Qp(e){return e?.channelMeta?.length?e.channelMeta.map(t=>t.id):e?.channelOrder?.length?e.channelOrder:["whatsapp","telegram","discord","slack","signal","imessage","nostr"]}function Jp(e,t,n){const s=lr(e,n.channelAccounts);switch(e){case"whatsapp":return Gp({props:t,whatsapp:n.whatsapp,accountCountLabel:s});case"telegram":return Vp({props:t,telegram:n.telegram,telegramAccounts:n.channelAccounts?.telegram??[],accountCountLabel:s});case"discord":return Fp({props:t,discord:n.discord,accountCountLabel:s});case"slack":return Wp({props:t,slack:n.slack,accountCountLabel:s});case"signal":return qp({props:t,signal:n.signal,accountCountLabel:s});case"imessage":return Up({props:t,imessage:n.imessage,accountCountLabel:s});case"nostr":{const i=n.channelAccounts?.nostr??[],o=i[0],a=o?.accountId??"default",c=o?.profile??null,r=t.nostrProfileAccountId===a?t.nostrProfileFormState:null,p=r?{onFieldChange:t.onNostrProfileFieldChange,onSave:t.onNostrProfileSave,onImport:t.onNostrProfileImport,onCancel:t.onNostrProfileCancel,onToggleAdvanced:t.onNostrProfileToggleAdvanced}:null;return jp({props:t,nostr:n.nostr,nostrAccounts:i,accountCountLabel:s,profileFormState:r,profileFormCallbacks:p,onEditProfile:()=>t.onNostrProfileEdit(a,c)})}default:return Zp(e,t,n.channelAccounts??{})}}function Zp(e,t,n){const s=ef(t.snapshot,e),i=t.snapshot?.channels?.[e],o=typeof i?.configured=="boolean"?i.configured:void 0,a=typeof i?.running=="boolean"?i.running:void 0,c=typeof i?.connected=="boolean"?i.connected:void 0,r=typeof i?.lastError=="string"?i.lastError:void 0,p=n[e]??[],l=lr(e,n);return d`
+
+ ${s}
+ Channel status and configuration.
+ ${l}
+
+ ${p.length>0?d`
+
+ ${p.map(u=>of(u))}
+
+ `:d`
+
+
+ Configured
+ ${o==null?"n/a":o?"Yes":"No"}
+
+
+ Running
+ ${a==null?"n/a":a?"Yes":"No"}
+
+
+ Connected
+ ${c==null?"n/a":c?"Yes":"No"}
+
+
+ `}
+
+ ${r?d`
+ ${r}
+ `:g}
+
+ ${_e({channelId:e,props:t})}
+
+ `}function Xp(e){return e?.channelMeta?.length?Object.fromEntries(e.channelMeta.map(t=>[t.id,t])):{}}function ef(e,t){return Xp(e)[t]?.label??e?.channelLabels?.[t]??t}const tf=600*1e3;function cr(e){return e.lastInboundAt?Date.now()-e.lastInboundAt
+
+ ${e.name||e.accountId}
+ ${e.accountId}
+
+
+
+ Running
+ ${t}
+
+
+ Configured
+ ${e.configured?"Yes":"No"}
+
+
+ Connected
+ ${n}
+
+
+ Last inbound
+ ${e.lastInboundAt?O(e.lastInboundAt):"n/a"}
+
+ ${e.lastError?d`
+
+ ${e.lastError}
+
+ `:g}
+
+
+ `}function af(e){const t=e.host??"unknown",n=e.ip?`(${e.ip})`:"",s=e.mode??"",i=e.version??"";return`${t} ${n} ${s} ${i}`.trim()}function rf(e){const t=e.ts??null;return t?O(t):"n/a"}function dr(e){return e?`${kt(e)} (${O(e)})`:"n/a"}function lf(e){if(e.totalTokens==null)return"n/a";const t=e.totalTokens??0,n=e.contextTokens??0;return n?`${t} / ${n}`:String(t)}function cf(e){if(e==null)return"";try{return JSON.stringify(e,null,2)}catch{return String(e)}}function df(e){const t=e.state??{},n=t.nextRunAtMs?kt(t.nextRunAtMs):"n/a",s=t.lastRunAtMs?kt(t.lastRunAtMs):"n/a";return`${t.lastStatus??"n/a"} · next ${n} · last ${s}`}function uf(e){const t=e.schedule;return t.kind==="at"?`At ${kt(t.atMs)}`:t.kind==="every"?`Every ${ta(t.everyMs)}`:`Cron ${t.expr}${t.tz?` (${t.tz})`:""}`}function pf(e){const t=e.payload;return t.kind==="systemEvent"?`System: ${t.text}`:`Agent: ${t.message}`}function ff(e){const t=["last",...e.channels.filter(Boolean)],n=e.form.channel?.trim();n&&!t.includes(n)&&t.push(n);const s=new Set;return t.filter(i=>s.has(i)?!1:(s.add(i),!0))}function hf(e,t){if(t==="last")return"last";const n=e.channelMeta?.find(s=>s.id===t);return n?.label?n.label:e.channelLabels?.[t]??t}function gf(e){const t=ff(e);return d`
+
+
+ Scheduler
+ Gateway-owned cron scheduler status.
+
+
+ Enabled
+
+ ${e.status?e.status.enabled?"Yes":"No":"n/a"}
+
+
+
+ Jobs
+ ${e.status?.jobs??"n/a"}
+
+
+ Next wake
+ ${dr(e.status?.nextWakeAtMs??null)}
+
+
+
+
+ ${e.error?d`${e.error}`:g}
+
+
+
+
+ New Job
+ Create a scheduled wakeup or agent run.
+
+
+
+
+
+
+
+ ${vf(e)}
+
+
+
+
+
+
+ ${e.form.payloadKind==="agentTurn"?d`
+
+
+
+
+
+ ${e.form.sessionTarget==="isolated"?d`
+
+ `:g}
+
+ `:g}
+
+
+
+
+
+
+
+ Jobs
+ All scheduled jobs stored in the gateway.
+ ${e.jobs.length===0?d`No jobs yet.`:d`
+
+ ${e.jobs.map(n=>mf(n,e))}
+
+ `}
+
+
+
+ Run history
+ Latest runs for ${e.runsJobId??"(select a job)"}.
+ ${e.runsJobId==null?d`
+
+ Select a job to inspect run history.
+
+ `:e.runs.length===0?d`No runs yet.`:d`
+
+ ${e.runs.map(n=>bf(n))}
+
+ `}
+
+ `}function vf(e){const t=e.form;return t.scheduleKind==="at"?d`
+
+ `:t.scheduleKind==="every"?d`
+
+
+
+
+ `:d`
+
+
+
+
+ `}function mf(e,t){const s=`list-item list-item-clickable${t.runsJobId===e.id?" list-item-selected":""}`;return d`
+ t.onLoadRuns(e.id)}>
+
+ ${e.name}
+ ${uf(e)}
+ ${pf(e)}
+ ${e.agentId?d`Agent: ${e.agentId}`:g}
+
+ ${e.enabled?"enabled":"disabled"}
+ ${e.sessionTarget}
+ ${e.wakeMode}
+
+
+
+
+ `}function bf(e){return d`
+
+
+ ${e.status}
+ ${e.summary??""}
+
+
+
+ `}function yf(e){return d`
+
+
+
+
+ Snapshots
+ Status, health, and heartbeat data.
+
+
+
+
+
+ Status
+ ${JSON.stringify(e.status??{},null,2)}
+
+
+ Health
+ ${JSON.stringify(e.health??{},null,2)}
+
+
+ Last heartbeat
+ ${JSON.stringify(e.heartbeat??{},null,2)}
+
+
+
+
+
+ Manual RPC
+ Send a raw gateway method with JSON params.
+
+
+
+
+
+
+
+ ${e.callError?d`
+ ${e.callError}
+ `:g}
+ ${e.callResult?d`${e.callResult}`:g}
+
+
+
+
+ Models
+ Catalog from models.list.
+ ${JSON.stringify(e.models??[],null,2)}
+
+
+
+ Event Log
+ Latest gateway events.
+ ${e.eventLog.length===0?d`No events yet.`:d`
+
+ ${e.eventLog.map(t=>d`
+
+
+ ${t.event}
+ ${new Date(t.ts).toLocaleTimeString()}
+
+
+
+ `)}
+
+ `}
+
+ `}function wf(e){return d`
+
+
+
+ Connected Instances
+ Presence beacons from the gateway and clients.
+
+
+
+ ${e.lastError?d`
+ ${e.lastError}
+ `:g}
+ ${e.statusMessage?d`
+ ${e.statusMessage}
+ `:g}
+
+ ${e.entries.length===0?d`No instances reported yet.`:e.entries.map(t=>$f(t))}
+
+
+ `}function $f(e){const t=e.lastInputSeconds!=null?`${e.lastInputSeconds}s ago`:"n/a",n=e.mode??"unknown",s=Array.isArray(e.roles)?e.roles.filter(Boolean):[],i=Array.isArray(e.scopes)?e.scopes.filter(Boolean):[],o=i.length>0?i.length>3?`${i.length} scopes`:`scopes: ${i.join(", ")}`:null;return d`
+
+
+ ${e.host??"unknown host"}
+ ${af(e)}
+
+ ${n}
+ ${s.map(a=>d`${a}`)}
+ ${o?d`${o}`:g}
+ ${e.platform?d`${e.platform}`:g}
+ ${e.deviceFamily?d`${e.deviceFamily}`:g}
+ ${e.modelIdentifier?d`${e.modelIdentifier}`:g}
+ ${e.version?d`${e.version}`:g}
+
+
+
+
+ `}const Do=["trace","debug","info","warn","error","fatal"];function kf(e){if(!e)return"";const t=new Date(e);return Number.isNaN(t.getTime())?e:t.toLocaleTimeString()}function xf(e,t){return t?[e.message,e.subsystem,e.raw].filter(Boolean).join(" ").toLowerCase().includes(t):!0}function Af(e){const t=e.filterText.trim().toLowerCase(),n=Do.some(o=>!e.levelFilters[o]),s=e.entries.filter(o=>o.level&&!e.levelFilters[o.level]?!1:xf(o,t)),i=t||n?"filtered":"visible";return d`
+
+
+
+ Logs
+ Gateway file logs (JSONL).
+
+
+
+
+
+
+
+
+
+
+
+
+
+ ${Do.map(o=>d`
+
+ `)}
+
+
+ ${e.file?d`File: ${e.file}`:g}
+ ${e.truncated?d`
+ Log output truncated; showing latest chunk.
+ `:g}
+ ${e.error?d`${e.error}`:g}
+
+
+ ${s.length===0?d`No log entries.`:s.map(o=>d`
+
+ ${kf(o.time)}
+ ${o.level??""}
+ ${o.subsystem??""}
+
+
+ `)}
+
+
+ `}function Sf(e){const t=Lf(e),n=Df(e);return d`
+ ${Ff(n)}
+ ${Bf(t)}
+ ${_f(e)}
+
+
+
+ Nodes
+ Paired devices and live links.
+
+
+
+
+ ${e.nodes.length===0?d`No nodes found.`:e.nodes.map(s=>Yf(s))}
+
+
+ `}function _f(e){const t=e.devicesList??{pending:[],paired:[]},n=Array.isArray(t.pending)?t.pending:[],s=Array.isArray(t.paired)?t.paired:[];return d`
+
+
+
+ Devices
+ Pairing requests + role tokens.
+
+
+
+ ${e.devicesError?d`${e.devicesError}`:g}
+
+ ${n.length>0?d`
+ Pending
+ ${n.map(i=>Tf(i,e))}
+ `:g}
+ ${s.length>0?d`
+ Paired
+ ${s.map(i=>Ef(i,e))}
+ `:g}
+ ${n.length===0&&s.length===0?d`No paired devices.`:g}
+
+
+ `}function Tf(e,t){const n=e.displayName?.trim()||e.deviceId,s=typeof e.ts=="number"?O(e.ts):"n/a",i=e.role?.trim()?`role: ${e.role}`:"role: -",o=e.isRepair?" · repair":"",a=e.remoteIp?` · ${e.remoteIp}`:"";return d`
+
+
+ ${n}
+ ${e.deviceId}${a}
+
+ ${i} · requested ${s}${o}
+
+
+
+
+ `}function Ef(e,t){const n=e.displayName?.trim()||e.deviceId,s=e.remoteIp?` · ${e.remoteIp}`:"",i=`roles: ${ns(e.roles)}`,o=`scopes: ${ns(e.scopes)}`,a=Array.isArray(e.tokens)?e.tokens:[];return d`
+
+
+ ${n}
+ ${e.deviceId}${s}
+ ${i} · ${o}
+ ${a.length===0?d`Tokens: none`:d`
+ Tokens
+
+ ${a.map(c=>Cf(e.deviceId,c,t))}
+
+ `}
+
+
+ `}function Cf(e,t,n){const s=t.revokedAtMs?"revoked":"active",i=`scopes: ${ns(t.scopes)}`,o=O(t.rotatedAtMs??t.createdAtMs??t.lastUsedAtMs??null);return d`
+
+ ${t.role} · ${s} · ${i} · ${o}
+
+
+ ${t.revokedAtMs?g:d`
+
+ `}
+
+
+ `}const ke="__defaults__",Bo=[{value:"deny",label:"Deny"},{value:"allowlist",label:"Allowlist"},{value:"full",label:"Full"}],If=[{value:"off",label:"Off"},{value:"on-miss",label:"On miss"},{value:"always",label:"Always"}];function Lf(e){const t=e.configForm,n=Wf(e.nodes),{defaultBinding:s,agents:i}=Gf(t),o=!!t,a=e.configSaving||e.configFormMode==="raw";return{ready:o,disabled:a,configDirty:e.configDirty,configLoading:e.configLoading,configSaving:e.configSaving,defaultBinding:s,agents:i,nodes:n,onBindDefault:e.onBindDefault,onBindAgent:e.onBindAgent,onSave:e.onSaveBindings,onLoadConfig:e.onLoadConfig,formMode:e.configFormMode}}function Fo(e){return e==="allowlist"||e==="full"||e==="deny"?e:"deny"}function Rf(e){return e==="always"||e==="off"||e==="on-miss"?e:"on-miss"}function Mf(e){const t=e?.defaults??{};return{security:Fo(t.security),ask:Rf(t.ask),askFallback:Fo(t.askFallback??"deny"),autoAllowSkills:!!(t.autoAllowSkills??!1)}}function Pf(e){const t=e?.agents??{},n=Array.isArray(t.list)?t.list:[],s=[];return n.forEach(i=>{if(!i||typeof i!="object")return;const o=i,a=typeof o.id=="string"?o.id.trim():"";if(!a)return;const c=typeof o.name=="string"?o.name.trim():void 0,r=o.default===!0;s.push({id:a,name:c||void 0,isDefault:r})}),s}function Nf(e,t){const n=Pf(e),s=Object.keys(t?.agents??{}),i=new Map;n.forEach(a=>i.set(a.id,a)),s.forEach(a=>{i.has(a)||i.set(a,{id:a})});const o=Array.from(i.values());return o.length===0&&o.push({id:"main",isDefault:!0}),o.sort((a,c)=>{if(a.isDefault&&!c.isDefault)return-1;if(!a.isDefault&&c.isDefault)return 1;const r=a.name?.trim()?a.name:a.id,p=c.name?.trim()?c.name:c.id;return r.localeCompare(p)}),o}function Of(e,t){return e===ke?ke:e&&t.some(n=>n.id===e)?e:ke}function Df(e){const t=e.execApprovalsForm??e.execApprovalsSnapshot?.file??null,n=!!t,s=Mf(t),i=Nf(e.configForm,t),o=Vf(e.nodes),a=e.execApprovalsTarget;let c=a==="node"&&e.execApprovalsTargetNodeId?e.execApprovalsTargetNodeId:null;a==="node"&&c&&!o.some(u=>u.id===c)&&(c=null);const r=Of(e.execApprovalsSelectedAgent,i),p=r!==ke?(t?.agents??{})[r]??null:null,l=Array.isArray(p?.allowlist)?p.allowlist??[]:[];return{ready:n,disabled:e.execApprovalsSaving||e.execApprovalsLoading,dirty:e.execApprovalsDirty,loading:e.execApprovalsLoading,saving:e.execApprovalsSaving,form:t,defaults:s,selectedScope:r,selectedAgent:p,agents:i,allowlist:l,target:a,targetNodeId:c,targetNodes:o,onSelectScope:e.onExecApprovalsSelectAgent,onSelectTarget:e.onExecApprovalsTargetChange,onPatch:e.onExecApprovalsPatch,onRemove:e.onExecApprovalsRemove,onLoad:e.onLoadExecApprovals,onSave:e.onSaveExecApprovals}}function Bf(e){const t=e.nodes.length>0,n=e.defaultBinding??"";return d`
+
+
+
+ Exec node binding
+
+ Pin agents to a specific node when using exec host=node.
+
+
+
+
+
+ ${e.formMode==="raw"?d`
+ Switch the Config tab to Form mode to edit bindings here.
+ `:g}
+
+ ${e.ready?d`
+
+
+
+ Default binding
+ Used when agents do not override a node binding.
+
+
+
+
+ ${e.agents.length===0?d`No agents found.`:e.agents.map(s=>qf(s,e))}
+
+ `:d`
+ Load config to edit bindings.
+
+ `}
+
+ `}function Ff(e){const t=e.ready,n=e.target!=="node"||!!e.targetNodeId;return d`
+
+
+
+ Exec approvals
+
+ Allowlist and approval policy for exec host=gateway/node.
+
+
+
+
+
+ ${Uf(e)}
+
+ ${t?d`
+ ${Kf(e)}
+ ${Hf(e)}
+ ${e.selectedScope===ke?g:zf(e)}
+ `:d`
+ Load exec approvals to edit allowlists.
+
+ `}
+
+ `}function Uf(e){const t=e.targetNodes.length>0,n=e.targetNodeId??"";return d`
+
+
+
+ Target
+
+ Gateway edits local approvals; node edits the selected node.
+
+
+
+
+ ${e.target==="node"&&!t?d`No nodes advertise exec approvals yet.`:g}
+
+ `}function Kf(e){return d`
+
+ Scope
+
+
+ ${e.agents.map(t=>{const n=t.name?.trim()?`${t.name} (${t.id})`:t.id;return d`
+
+ `})}
+
+
+ `}function Hf(e){const t=e.selectedScope===ke,n=e.defaults,s=e.selectedAgent??{},i=t?["defaults"]:["agents",e.selectedScope],o=typeof s.security=="string"?s.security:void 0,a=typeof s.ask=="string"?s.ask:void 0,c=typeof s.askFallback=="string"?s.askFallback:void 0,r=t?n.security:o??"__default__",p=t?n.ask:a??"__default__",l=t?n.askFallback:c??"__default__",u=typeof s.autoAllowSkills=="boolean"?s.autoAllowSkills:void 0,h=u??n.autoAllowSkills,v=u==null;return d`
+
+
+
+ Security
+
+ ${t?"Default security mode.":`Default: ${n.security}.`}
+
+
+
+
+
+
+
+ Ask
+
+ ${t?"Default prompt policy.":`Default: ${n.ask}.`}
+
+
+
+
+
+
+
+ Ask fallback
+
+ ${t?"Applied when the UI prompt is unavailable.":`Default: ${n.askFallback}.`}
+
+
+
+
+
+
+
+ Auto-allow skill CLIs
+
+ ${t?"Allow skill executables listed by the Gateway.":v?`Using default (${n.autoAllowSkills?"on":"off"}).`:`Override (${h?"on":"off"}).`}
+
+
+
+
+
+ `}function zf(e){const t=["agents",e.selectedScope,"allowlist"],n=e.allowlist;return d`
+
+
+ Allowlist
+ Case-insensitive glob patterns.
+
+
+
+
+ ${n.length===0?d`No allowlist entries yet.`:n.map((s,i)=>jf(e,s,i))}
+
+ `}function jf(e,t,n){const s=t.lastUsedAt?O(t.lastUsedAt):"never",i=t.lastUsedCommand?ss(t.lastUsedCommand,120):null,o=t.lastResolvedPath?ss(t.lastResolvedPath,120):null;return d`
+
+
+ ${t.pattern?.trim()?t.pattern:"New pattern"}
+ Last used: ${s}
+ ${i?d`${i}`:g}
+ ${o?d`${o}`:g}
+
+
+
+ `}function qf(e,t){const n=e.binding??"__default__",s=e.name?.trim()?`${e.name} (${e.id})`:e.id,i=t.nodes.length>0;return d`
+
+
+ ${s}
+
+ ${e.isDefault?"default agent":"agent"} ·
+ ${n==="__default__"?`uses default (${t.defaultBinding??"any"})`:`override: ${e.binding}`}
+
+
+
+
+ `}function Wf(e){const t=[];for(const n of e){if(!(Array.isArray(n.commands)?n.commands:[]).some(c=>String(c)==="system.run"))continue;const o=typeof n.nodeId=="string"?n.nodeId.trim():"";if(!o)continue;const a=typeof n.displayName=="string"&&n.displayName.trim()?n.displayName.trim():o;t.push({id:o,label:a===o?o:`${a} · ${o}`})}return t.sort((n,s)=>n.label.localeCompare(s.label)),t}function Vf(e){const t=[];for(const n of e){if(!(Array.isArray(n.commands)?n.commands:[]).some(c=>String(c)==="system.execApprovals.get"||String(c)==="system.execApprovals.set"))continue;const o=typeof n.nodeId=="string"?n.nodeId.trim():"";if(!o)continue;const a=typeof n.displayName=="string"&&n.displayName.trim()?n.displayName.trim():o;t.push({id:o,label:a===o?o:`${a} · ${o}`})}return t.sort((n,s)=>n.label.localeCompare(s.label)),t}function Gf(e){const t={id:"main",name:void 0,index:0,isDefault:!0,binding:null};if(!e||typeof e!="object")return{defaultBinding:null,agents:[t]};const s=(e.tools??{}).exec??{},i=typeof s.node=="string"&&s.node.trim()?s.node.trim():null,o=e.agents??{},a=Array.isArray(o.list)?o.list:[];if(a.length===0)return{defaultBinding:i,agents:[t]};const c=[];return a.forEach((r,p)=>{if(!r||typeof r!="object")return;const l=r,u=typeof l.id=="string"?l.id.trim():"";if(!u)return;const h=typeof l.name=="string"?l.name.trim():void 0,v=l.default===!0,$=(l.tools??{}).exec??{},x=typeof $.node=="string"&&$.node.trim()?$.node.trim():null;c.push({id:u,name:h||void 0,index:p,isDefault:v,binding:x})}),c.length===0&&c.push(t),{defaultBinding:i,agents:c}}function Yf(e){const t=!!e.connected,n=!!e.paired,s=typeof e.displayName=="string"&&e.displayName.trim()||(typeof e.nodeId=="string"?e.nodeId:"unknown"),i=Array.isArray(e.caps)?e.caps:[],o=Array.isArray(e.commands)?e.commands:[];return d`
+
+
+ ${s}
+
+ ${typeof e.nodeId=="string"?e.nodeId:""}
+ ${typeof e.remoteIp=="string"?` · ${e.remoteIp}`:""}
+ ${typeof e.version=="string"?` · ${e.version}`:""}
+
+
+ ${n?"paired":"unpaired"}
+
+ ${t?"connected":"offline"}
+
+ ${i.slice(0,12).map(a=>d`${String(a)}`)}
+ ${o.slice(0,8).map(a=>d`${String(a)}`)}
+
+
+
+ `}function Qf(e){const t=e.hello?.snapshot,n=t?.uptimeMs?ta(t.uptimeMs):"n/a",s=t?.policy?.tickIntervalMs?`${t.policy.tickIntervalMs}ms`:"n/a",i=(()=>{if(e.connected||!e.lastError)return null;const a=e.lastError.toLowerCase();if(!(a.includes("unauthorized")||a.includes("connect failed")))return null;const r=!!e.settings.token.trim(),p=!!e.password.trim();return!r&&!p?d`
+
+ This gateway requires auth. Add a token or password, then click Connect.
+
+ clawdbot dashboard --no-open → tokenized URL
+ clawdbot doctor --generate-gateway-token → set token
+
+
+
+ `:d`
+
+ Auth failed. Re-copy a tokenized URL with
+ clawdbot dashboard --no-open, or update the token,
+ then click Connect.
+
+
+ `})(),o=(()=>{if(e.connected||!e.lastError||(typeof window<"u"?window.isSecureContext:!0)!==!1)return null;const c=e.lastError.toLowerCase();return!c.includes("secure context")&&!c.includes("device identity required")?null:d`
+
+ This page is HTTP, so the browser blocks device identity. Use HTTPS (Tailscale Serve) or
+ open http://127.0.0.1:18789 on the gateway host.
+
+ If you must stay on HTTP, set
+ gateway.controlUi.allowInsecureAuth: true (token-only).
+
+
+
+ `})();return d`
+
+
+ Gateway Access
+ Where the dashboard connects and how it authenticates.
+
+
+
+
+
+
+
+
+
+ Click Connect to apply connection changes.
+
+
+
+
+ Snapshot
+ Latest gateway handshake information.
+
+
+ Status
+
+ ${e.connected?"Connected":"Disconnected"}
+
+
+
+ Uptime
+ ${n}
+
+
+ Tick Interval
+ ${s}
+
+
+ Last Channels Refresh
+
+ ${e.lastChannelsRefresh?O(e.lastChannelsRefresh):"n/a"}
+
+
+
+ ${e.lastError?d`
+ ${e.lastError}
+ ${i??""}
+ ${o??""}
+ `:d`
+ Use Channels to link WhatsApp, Telegram, Discord, Signal, or iMessage.
+ `}
+
+
+
+
+
+ Instances
+ ${e.presenceCount}
+ Presence beacons in the last 5 minutes.
+
+
+ Sessions
+ ${e.sessionsCount??"n/a"}
+ Recent session keys tracked by the gateway.
+
+
+ Cron
+
+ ${e.cronEnabled==null?"n/a":e.cronEnabled?"Enabled":"Disabled"}
+
+ Next wake ${dr(e.cronNext)}
+
+
+
+
+ Notes
+ Quick reminders for remote control setups.
+
+
+ Tailscale serve
+
+ Prefer serve mode to keep the gateway on loopback with tailnet auth.
+
+
+
+ Session hygiene
+ Use /new or sessions.patch to reset context.
+
+
+ Cron reminders
+ Use isolated sessions for recurring runs.
+
+
+
+ `}const Jf=["","off","minimal","low","medium","high"],Zf=["","off","on"],Xf=[{value:"",label:"inherit"},{value:"off",label:"off (explicit)"},{value:"on",label:"on"}],eh=["","off","on","stream"];function th(e){if(!e)return"";const t=e.trim().toLowerCase();return t==="z.ai"||t==="z-ai"?"zai":t}function ur(e){return th(e)==="zai"}function nh(e){return ur(e)?Zf:Jf}function sh(e,t){return!t||!e||e==="off"?e:"on"}function ih(e,t){return e?t&&e==="on"?"low":e:null}function oh(e){const t=e.result?.sessions??[];return d`
+
+
+
+ Sessions
+ Active session keys and per-session overrides.
+
+
+
+
+
+
+
+
+
+
+
+ ${e.error?d`${e.error}`:g}
+
+
+ ${e.result?`Store: ${e.result.path}`:""}
+
+
+
+
+ Key
+ Label
+ Kind
+ Updated
+ Tokens
+ Thinking
+ Verbose
+ Reasoning
+ Actions
+
+ ${t.length===0?d`No sessions found.`:t.map(n=>ah(n,e.basePath,e.onPatch,e.onDelete,e.loading))}
+
+
+ `}function ah(e,t,n,s,i){const o=e.updatedAt?O(e.updatedAt):"n/a",a=e.thinkingLevel??"",c=ur(e.modelProvider),r=sh(a,c),p=nh(e.modelProvider),l=e.verboseLevel??"",u=e.reasoningLevel??"",h=e.displayName??e.key,v=e.kind!=="global",w=v?`${Is("chat",t)}?session=${encodeURIComponent(e.key)}`:null;return d`
+
+ ${v?d`${h}`:h}
+
+ {const x=$.target.value.trim();n(e.key,{label:x||null})}}
+ />
+
+ ${e.kind}
+ ${o}
+ ${lf(e)}
+
+
+
+
+
+
+
+
+
+
+
+
+
+ `}function rh(e){const t=Math.max(0,e),n=Math.floor(t/1e3);if(n<60)return`${n}s`;const s=Math.floor(n/60);return s<60?`${s}m`:`${Math.floor(s/60)}h`}function Le(e,t){return t?d``:g}function lh(e){const t=e.execApprovalQueue[0];if(!t)return g;const n=t.request,s=t.expiresAtMs-Date.now(),i=s>0?`expires in ${rh(s)}`:"expired",o=e.execApprovalQueue.length;return d`
+
+ `}function ch(e){const t=e.report?.skills??[],n=e.filter.trim().toLowerCase(),s=n?t.filter(i=>[i.name,i.description,i.source].join(" ").toLowerCase().includes(n)):t;return d`
+
+
+
+ Skills
+ Bundled, managed, and workspace skills.
+
+
+
+
+
+
+ ${s.length} shown
+
+
+ ${e.error?d`${e.error}`:g}
+
+ ${s.length===0?d`No skills found.`:d`
+
+ ${s.map(i=>dh(i,e))}
+
+ `}
+
+ `}function dh(e,t){const n=t.busyKey===e.skillKey,s=t.edits[e.skillKey]??"",i=t.messages[e.skillKey]??null,o=e.install.length>0&&e.missing.bins.length>0,a=[...e.missing.bins.map(r=>`bin:${r}`),...e.missing.env.map(r=>`env:${r}`),...e.missing.config.map(r=>`config:${r}`),...e.missing.os.map(r=>`os:${r}`)],c=[];return e.disabled&&c.push("disabled"),e.blockedByAllowlist&&c.push("blocked by allowlist"),d`
+
+
+
+ ${e.emoji?`${e.emoji} `:""}${e.name}
+
+ ${ss(e.description,140)}
+
+ ${e.source}
+
+ ${e.eligible?"eligible":"blocked"}
+
+ ${e.disabled?d`disabled`:g}
+
+ ${a.length>0?d`
+
+ Missing: ${a.join(", ")}
+
+ `:g}
+ ${c.length>0?d`
+
+ Reason: ${c.join(", ")}
+
+ `:g}
+
+
+
+ `}function uh(e,t){const n=Is(t,e.basePath);return d`
+ {s.defaultPrevented||s.button!==0||s.metaKey||s.ctrlKey||s.shiftKey||s.altKey||(s.preventDefault(),e.setTab(t))}}
+ title=${ts(t)}
+ >
+
+
+
+ `}function ph(e){const t=fh(e.sessionKey,e.sessionsResult),n=e.onboarding,s=e.onboarding,i=e.onboarding?!1:e.settings.chatShowThinking,o=e.onboarding?!0:e.settings.chatFocusMode,a=d``,c=d``;return d`
+
+
+
+ |
+
+
+
+ `}function fh(e,t){const n=new Set,s=[],i=t?.sessions?.find(o=>o.key===e);if(n.add(e),s.push({key:e,displayName:i?.displayName}),t?.sessions)for(const o of t.sessions)n.has(o.key)||(n.add(o.key),s.push({key:o.key,displayName:o.displayName}));return s}const hh=["system","light","dark"];function gh(e){const t=Math.max(0,hh.indexOf(e.theme)),n=s=>i=>{const a={element:i.currentTarget};(i.clientX||i.clientY)&&(a.pointerClientX=i.clientX,a.pointerClientY=i.clientY),e.setTheme(s,a)};return d`
+
+
+
+
+
+
+
+
+ `}function vh(){return d`
+
+ `}function mh(){return d`
+
+ `}function bh(){return d`
+
+ `}const yh=/^data:/i,wh=/^https?:\/\//i;function $h(e){const t=e.agentsList?.agents??[],s=Jo(e.sessionKey)?.agentId??e.agentsList?.defaultId??"main",o=t.find(c=>c.id===s)?.identity,a=o?.avatarUrl??o?.avatar;if(a)return yh.test(a)||wh.test(a)?a:o?.avatarUrl}function kh(e){const t=e.presenceEntries.length,n=e.sessionsResult?.count??null,s=e.cronStatus?.nextWakeAtMs??null,i=e.connected?null:"Disconnected from gateway.",o=e.tab==="chat",a=o&&(e.settings.chatFocusMode||e.onboarding),c=e.onboarding?!1:e.settings.chatShowThinking,r=$h(e),p=e.chatAvatarUrl??r??null;return d`
+
+
+
+
+
+
+
+
+
+ ${ts(e.tab)}
+ ${pl(e.tab)}
+
+
+
+
+ ${e.tab==="overview"?Qf({connected:e.connected,hello:e.hello,settings:e.settings,password:e.password,lastError:e.lastError,presenceCount:t,sessionsCount:n,cronEnabled:e.cronStatus?.enabled??null,cronNext:s,lastChannelsRefresh:e.channelsLastSuccess,onSettingsChange:l=>e.applySettings(l),onPasswordChange:l=>e.password=l,onSessionKeyChange:l=>{e.sessionKey=l,e.chatMessage="",e.resetToolStream(),e.applySettings({...e.settings,sessionKey:l,lastActiveSessionKey:l}),e.loadAssistantIdentity()},onConnect:()=>e.connect(),onRefresh:()=>e.loadOverview()}):g}
+
+ ${e.tab==="channels"?Yp({connected:e.connected,loading:e.channelsLoading,snapshot:e.channelsSnapshot,lastError:e.channelsError,lastSuccessAt:e.channelsLastSuccess,whatsappMessage:e.whatsappLoginMessage,whatsappQrDataUrl:e.whatsappLoginQrDataUrl,whatsappConnected:e.whatsappLoginConnected,whatsappBusy:e.whatsappBusy,configSchema:e.configSchema,configSchemaLoading:e.configSchemaLoading,configForm:e.configForm,configUiHints:e.configUiHints,configSaving:e.configSaving,configFormDirty:e.configFormDirty,nostrProfileFormState:e.nostrProfileFormState,nostrProfileAccountId:e.nostrProfileAccountId,onRefresh:l=>oe(e,l),onWhatsAppStart:l=>e.handleWhatsAppStart(l),onWhatsAppWait:()=>e.handleWhatsAppWait(),onWhatsAppLogout:()=>e.handleWhatsAppLogout(),onConfigPatch:(l,u)=>Nt(e,l,u),onConfigSave:()=>e.handleChannelConfigSave(),onConfigReload:()=>e.handleChannelConfigReload(),onNostrProfileEdit:(l,u)=>e.handleNostrProfileEdit(l,u),onNostrProfileCancel:()=>e.handleNostrProfileCancel(),onNostrProfileFieldChange:(l,u)=>e.handleNostrProfileFieldChange(l,u),onNostrProfileSave:()=>e.handleNostrProfileSave(),onNostrProfileImport:()=>e.handleNostrProfileImport(),onNostrProfileToggleAdvanced:()=>e.handleNostrProfileToggleAdvanced()}):g}
+
+ ${e.tab==="instances"?wf({loading:e.presenceLoading,entries:e.presenceEntries,lastError:e.presenceError,statusMessage:e.presenceStatus,onRefresh:()=>Ks(e)}):g}
+
+ ${e.tab==="sessions"?oh({loading:e.sessionsLoading,result:e.sessionsResult,error:e.sessionsError,activeMinutes:e.sessionsFilterActive,limit:e.sessionsFilterLimit,includeGlobal:e.sessionsIncludeGlobal,includeUnknown:e.sessionsIncludeUnknown,basePath:e.basePath,onFiltersChange:l=>{e.sessionsFilterActive=l.activeMinutes,e.sessionsFilterLimit=l.limit,e.sessionsIncludeGlobal=l.includeGlobal,e.sessionsIncludeUnknown=l.includeUnknown},onRefresh:()=>tt(e),onPatch:(l,u)=>xl(e,l,u),onDelete:l=>Al(e,l)}):g}
+
+ ${e.tab==="cron"?gf({loading:e.cronLoading,status:e.cronStatus,jobs:e.cronJobs,error:e.cronError,busy:e.cronBusy,form:e.cronForm,channels:e.channelsSnapshot?.channelMeta?.length?e.channelsSnapshot.channelMeta.map(l=>l.id):e.channelsSnapshot?.channelOrder??[],channelLabels:e.channelsSnapshot?.channelLabels??{},channelMeta:e.channelsSnapshot?.channelMeta??[],runsJobId:e.cronRunsJobId,runs:e.cronRuns,onFormChange:l=>e.cronForm={...e.cronForm,...l},onRefresh:()=>e.loadCron(),onAdd:()=>jl(e),onToggle:(l,u)=>ql(e,l,u),onRun:l=>Wl(e,l),onRemove:l=>Vl(e,l),onLoadRuns:l=>ra(e,l)}):g}
+
+ ${e.tab==="skills"?ch({loading:e.skillsLoading,report:e.skillsReport,error:e.skillsError,filter:e.skillsFilter,edits:e.skillEdits,messages:e.skillMessages,busyKey:e.skillsBusyKey,onFilterChange:l=>e.skillsFilter=l,onRefresh:()=>_t(e,{clearMessages:!0}),onToggle:(l,u)=>Kc(e,l,u),onEdit:(l,u)=>Uc(e,l,u),onSaveKey:l=>Hc(e,l),onInstall:(l,u,h)=>zc(e,l,u,h)}):g}
+
+ ${e.tab==="nodes"?Sf({loading:e.nodesLoading,nodes:e.nodes,devicesLoading:e.devicesLoading,devicesError:e.devicesError,devicesList:e.devicesList,configForm:e.configForm??e.configSnapshot?.config,configLoading:e.configLoading,configSaving:e.configSaving,configDirty:e.configFormDirty,configFormMode:e.configFormMode,execApprovalsLoading:e.execApprovalsLoading,execApprovalsSaving:e.execApprovalsSaving,execApprovalsDirty:e.execApprovalsDirty,execApprovalsSnapshot:e.execApprovalsSnapshot,execApprovalsForm:e.execApprovalsForm,execApprovalsSelectedAgent:e.execApprovalsSelectedAgent,execApprovalsTarget:e.execApprovalsTarget,execApprovalsTargetNodeId:e.execApprovalsTargetNodeId,onRefresh:()=>un(e),onDevicesRefresh:()=>Se(e),onDeviceApprove:l=>Ic(e,l),onDeviceReject:l=>Lc(e,l),onDeviceRotate:(l,u,h)=>Rc(e,{deviceId:l,role:u,scopes:h}),onDeviceRevoke:(l,u)=>Mc(e,{deviceId:l,role:u}),onLoadConfig:()=>me(e),onLoadExecApprovals:()=>{const l=e.execApprovalsTarget==="node"&&e.execApprovalsTargetNodeId?{kind:"node",nodeId:e.execApprovalsTargetNodeId}:{kind:"gateway"};return Us(e,l)},onBindDefault:l=>{l?Nt(e,["tools","exec","node"],l):Gi(e,["tools","exec","node"])},onBindAgent:(l,u)=>{const h=["agents","list",l,"tools","exec","node"];u?Nt(e,h,u):Gi(e,h)},onSaveBindings:()=>os(e),onExecApprovalsTargetChange:(l,u)=>{e.execApprovalsTarget=l,e.execApprovalsTargetNodeId=u,e.execApprovalsSnapshot=null,e.execApprovalsForm=null,e.execApprovalsDirty=!1,e.execApprovalsSelectedAgent=null},onExecApprovalsSelectAgent:l=>{e.execApprovalsSelectedAgent=l},onExecApprovalsPatch:(l,u)=>Bc(e,l,u),onExecApprovalsRemove:l=>Fc(e,l),onSaveExecApprovals:()=>{const l=e.execApprovalsTarget==="node"&&e.execApprovalsTargetNodeId?{kind:"node",nodeId:e.execApprovalsTargetNodeId}:{kind:"gateway"};return Dc(e,l)}}):g}
+
+ ${e.tab==="chat"?pp({sessionKey:e.sessionKey,onSessionKeyChange:l=>{e.sessionKey=l,e.chatMessage="",e.chatStream=null,e.chatStreamStartedAt=null,e.chatRunId=null,e.chatQueue=[],e.resetToolStream(),e.resetChatScroll(),e.applySettings({...e.settings,sessionKey:l,lastActiveSessionKey:l}),e.loadAssistantIdentity(),Je(e),ds(e)},thinkingLevel:e.chatThinkingLevel,showThinking:c,loading:e.chatLoading,sending:e.chatSending,assistantAvatarUrl:p,messages:e.chatMessages,toolMessages:e.chatToolMessages,stream:e.chatStream,streamStartedAt:e.chatStreamStartedAt,draft:e.chatMessage,queue:e.chatQueue,connected:e.connected,canSend:e.connected,disabledReason:i,error:e.lastError,sessions:e.sessionsResult,focusMode:a,onRefresh:()=>(e.resetToolStream(),Promise.all([Je(e),ds(e)])),onToggleFocusMode:()=>{e.onboarding||e.applySettings({...e.settings,chatFocusMode:!e.settings.chatFocusMode})},onChatScroll:l=>e.handleChatScroll(l),onDraftChange:l=>e.chatMessage=l,onSend:()=>e.handleSendChat(),canAbort:!!e.chatRunId,onAbort:()=>{e.handleAbortChat()},onQueueRemove:l=>e.removeQueuedMessage(l),onNewSession:()=>e.handleSendChat("/new",{restoreDraft:!0}),sidebarOpen:e.sidebarOpen,sidebarContent:e.sidebarContent,sidebarError:e.sidebarError,splitRatio:e.splitRatio,onOpenSidebar:l=>e.handleOpenSidebar(l),onCloseSidebar:()=>e.handleCloseSidebar(),onSplitRatioChange:l=>e.handleSplitRatioChange(l),assistantName:e.assistantName,assistantAvatar:e.assistantAvatar}):g}
+
+ ${e.tab==="config"?Rp({raw:e.configRaw,valid:e.configValid,issues:e.configIssues,loading:e.configLoading,saving:e.configSaving,applying:e.configApplying,updating:e.updateRunning,connected:e.connected,schema:e.configSchema,schemaLoading:e.configSchemaLoading,uiHints:e.configUiHints,formMode:e.configFormMode,formValue:e.configForm,originalValue:e.configFormOriginal,searchQuery:e.configSearchQuery,activeSection:e.configActiveSection,activeSubsection:e.configActiveSubsection,onRawChange:l=>e.configRaw=l,onFormModeChange:l=>e.configFormMode=l,onFormPatch:(l,u)=>Nt(e,l,u),onSearchChange:l=>e.configSearchQuery=l,onSectionChange:l=>{e.configActiveSection=l,e.configActiveSubsection=null},onSubsectionChange:l=>e.configActiveSubsection=l,onReload:()=>me(e),onSave:()=>os(e),onApply:()=>Ul(e),onUpdate:()=>Kl(e)}):g}
+
+ ${e.tab==="debug"?yf({loading:e.debugLoading,status:e.debugStatus,health:e.debugHealth,models:e.debugModels,heartbeat:e.debugHeartbeat,eventLog:e.eventLog,callMethod:e.debugCallMethod,callParams:e.debugCallParams,callResult:e.debugCallResult,callError:e.debugCallError,onCallMethodChange:l=>e.debugCallMethod=l,onCallParamsChange:l=>e.debugCallParams=l,onRefresh:()=>cn(e),onCall:()=>Jl(e)}):g}
+
+ ${e.tab==="logs"?Af({loading:e.logsLoading,error:e.logsError,file:e.logsFile,entries:e.logsEntries,filterText:e.logsFilterText,levelFilters:e.logsLevelFilters,autoFollow:e.logsAutoFollow,truncated:e.logsTruncated,onFilterTextChange:l=>e.logsFilterText=l,onLevelToggle:(l,u)=>{e.logsLevelFilters={...e.logsLevelFilters,[l]:u}},onToggleAutoFollow:l=>e.logsAutoFollow=l,onRefresh:()=>Ms(e,{reset:!0}),onExport:(l,u)=>e.exportLogs(l,u),onScroll:l=>e.handleLogsScroll(l)}):g}
+
+ ${lh(e)}
+
+ `}const xh={trace:!0,debug:!0,info:!0,warn:!0,error:!0,fatal:!0},Ah={name:"",description:"",agentId:"",enabled:!0,scheduleKind:"every",scheduleAt:"",everyAmount:"30",everyUnit:"minutes",cronExpr:"0 7 * * *",cronTz:"",sessionTarget:"main",wakeMode:"next-heartbeat",payloadKind:"systemEvent",payloadText:"",deliver:!1,channel:"last",to:"",timeoutSeconds:"",postToMainPrefix:""};async function Sh(e){if(!(!e.client||!e.connected)&&!e.agentsLoading){e.agentsLoading=!0,e.agentsError=null;try{const t=await e.client.request("agents.list",{});t&&(e.agentsList=t)}catch(t){e.agentsError=String(t)}finally{e.agentsLoading=!1}}}const pr={WEBCHAT_UI:"webchat-ui",CONTROL_UI:"clawdbot-control-ui",WEBCHAT:"webchat",CLI:"cli",GATEWAY_CLIENT:"gateway-client",MACOS_APP:"clawdbot-macos",IOS_APP:"clawdbot-ios",ANDROID_APP:"clawdbot-android",NODE_HOST:"node-host",TEST:"test",FINGERPRINT:"fingerprint",PROBE:"clawdbot-probe"},Uo=pr,ks={WEBCHAT:"webchat",CLI:"cli",UI:"ui",BACKEND:"backend",NODE:"node",PROBE:"probe",TEST:"test"};new Set(Object.values(pr));new Set(Object.values(ks));function _h(e){const t=e.version??(e.nonce?"v2":"v1"),n=e.scopes.join(","),s=e.token??"",i=[t,e.deviceId,e.clientId,e.clientMode,e.role,n,String(e.signedAtMs),s];return t==="v2"&&i.push(e.nonce??""),i.join("|")}const Th=4008;class Eh{constructor(t){this.opts=t,this.ws=null,this.pending=new Map,this.closed=!1,this.lastSeq=null,this.connectNonce=null,this.connectSent=!1,this.connectTimer=null,this.backoffMs=800}start(){this.closed=!1,this.connect()}stop(){this.closed=!0,this.ws?.close(),this.ws=null,this.flushPending(new Error("gateway client stopped"))}get connected(){return this.ws?.readyState===WebSocket.OPEN}connect(){this.closed||(this.ws=new WebSocket(this.opts.url),this.ws.onopen=()=>this.queueConnect(),this.ws.onmessage=t=>this.handleMessage(String(t.data??"")),this.ws.onclose=t=>{const n=String(t.reason??"");this.ws=null,this.flushPending(new Error(`gateway closed (${t.code}): ${n}`)),this.opts.onClose?.({code:t.code,reason:n}),this.scheduleReconnect()},this.ws.onerror=()=>{})}scheduleReconnect(){if(this.closed)return;const t=this.backoffMs;this.backoffMs=Math.min(this.backoffMs*1.7,15e3),window.setTimeout(()=>this.connect(),t)}flushPending(t){for(const[,n]of this.pending)n.reject(t);this.pending.clear()}async sendConnect(){if(this.connectSent)return;this.connectSent=!0,this.connectTimer!==null&&(window.clearTimeout(this.connectTimer),this.connectTimer=null);const t=typeof crypto<"u"&&!!crypto.subtle,n=["operator.admin","operator.approvals","operator.pairing"],s="operator";let i=null,o=!1,a=this.opts.token;if(t){i=await Ds();const l=Cc({deviceId:i.deviceId,role:s})?.token;a=l??this.opts.token,o=!!(l&&this.opts.token)}const c=a||this.opts.password?{token:a,password:this.opts.password}:void 0;let r;if(t&&i){const l=Date.now(),u=this.connectNonce??void 0,h=_h({deviceId:i.deviceId,clientId:this.opts.clientName??Uo.CONTROL_UI,clientMode:this.opts.mode??ks.WEBCHAT,role:s,scopes:n,signedAtMs:l,token:a??null,nonce:u}),v=await Tc(i.privateKey,h);r={id:i.deviceId,publicKey:i.publicKey,signature:v,signedAt:l,nonce:u}}const p={minProtocol:3,maxProtocol:3,client:{id:this.opts.clientName??Uo.CONTROL_UI,version:this.opts.clientVersion??"dev",platform:this.opts.platform??navigator.platform??"web",mode:this.opts.mode??ks.WEBCHAT,instanceId:this.opts.instanceId},role:s,scopes:n,device:r,caps:[],auth:c,userAgent:navigator.userAgent,locale:navigator.language};this.request("connect",p).then(l=>{l?.auth?.deviceToken&&i&&Aa({deviceId:i.deviceId,role:l.auth.role??s,token:l.auth.deviceToken,scopes:l.auth.scopes??[]}),this.backoffMs=800,this.opts.onHello?.(l)}).catch(()=>{o&&i&&Sa({deviceId:i.deviceId,role:s}),this.ws?.close(Th,"connect failed")})}handleMessage(t){let n;try{n=JSON.parse(t)}catch{return}const s=n;if(s.type==="event"){const i=n;if(i.event==="connect.challenge"){const a=i.payload,c=a&&typeof a.nonce=="string"?a.nonce:null;c&&(this.connectNonce=c,this.sendConnect());return}const o=typeof i.seq=="number"?i.seq:null;o!==null&&(this.lastSeq!==null&&o>this.lastSeq+1&&this.opts.onGap?.({expected:this.lastSeq+1,received:o}),this.lastSeq=o),this.opts.onEvent?.(i);return}if(s.type==="res"){const i=n,o=this.pending.get(i.id);if(!o)return;this.pending.delete(i.id),i.ok?o.resolve(i.payload):o.reject(new Error(i.error?.message??"request failed"));return}}request(t,n){if(!this.ws||this.ws.readyState!==WebSocket.OPEN)return Promise.reject(new Error("gateway not connected"));const s=Ls(),i={type:"req",id:s,method:t,params:n},o=new Promise((a,c)=>{this.pending.set(s,{resolve:r=>a(r),reject:c})});return this.ws.send(JSON.stringify(i)),o}queueConnect(){this.connectNonce=null,this.connectSent=!1,this.connectTimer!==null&&window.clearTimeout(this.connectTimer),this.connectTimer=window.setTimeout(()=>{this.sendConnect()},750)}}function xs(e){return typeof e=="object"&&e!==null}function Ch(e){if(!xs(e))return null;const t=typeof e.id=="string"?e.id.trim():"",n=e.request;if(!t||!xs(n))return null;const s=typeof n.command=="string"?n.command.trim():"";if(!s)return null;const i=typeof e.createdAtMs=="number"?e.createdAtMs:0,o=typeof e.expiresAtMs=="number"?e.expiresAtMs:0;return!i||!o?null:{id:t,request:{command:s,cwd:typeof n.cwd=="string"?n.cwd:null,host:typeof n.host=="string"?n.host:null,security:typeof n.security=="string"?n.security:null,ask:typeof n.ask=="string"?n.ask:null,agentId:typeof n.agentId=="string"?n.agentId:null,resolvedPath:typeof n.resolvedPath=="string"?n.resolvedPath:null,sessionKey:typeof n.sessionKey=="string"?n.sessionKey:null},createdAtMs:i,expiresAtMs:o}}function Ih(e){if(!xs(e))return null;const t=typeof e.id=="string"?e.id.trim():"";return t?{id:t,decision:typeof e.decision=="string"?e.decision:null,resolvedBy:typeof e.resolvedBy=="string"?e.resolvedBy:null,ts:typeof e.ts=="number"?e.ts:null}:null}function fr(e){const t=Date.now();return e.filter(n=>n.expiresAtMs>t)}function Lh(e,t){const n=fr(e).filter(s=>s.id!==t.id);return n.push(t),n}function Ko(e,t){return fr(e).filter(n=>n.id!==t)}async function hr(e,t){if(!e.client||!e.connected)return;const n=e.sessionKey.trim(),s=n?{sessionKey:n}:{};try{const i=await e.client.request("agent.identity.get",s);if(!i)return;const o=es(i);e.assistantName=o.name,e.assistantAvatar=o.avatar,e.assistantAgentId=o.agentId??null}catch{}}function Jn(e,t){const n=(e??"").trim(),s=t.mainSessionKey?.trim();if(!s)return n;if(!n)return s;const i=t.mainKey?.trim()||"main",o=t.defaultAgentId?.trim();return n==="main"||n===i||o&&(n===`agent:${o}:main`||n===`agent:${o}:${i}`)?s:n}function Rh(e,t){if(!t?.mainSessionKey)return;const n=Jn(e.sessionKey,t),s=Jn(e.settings.sessionKey,t),i=Jn(e.settings.lastActiveSessionKey,t),o=n||s||e.sessionKey,a={...e.settings,sessionKey:s||o,lastActiveSessionKey:i||o},c=a.sessionKey!==e.settings.sessionKey||a.lastActiveSessionKey!==e.settings.lastActiveSessionKey;o!==e.sessionKey&&(e.sessionKey=o),c&&$e(e,a)}function gr(e){e.lastError=null,e.hello=null,e.connected=!1,e.execApprovalQueue=[],e.execApprovalError=null,e.client?.stop(),e.client=new Eh({url:e.settings.gatewayUrl,token:e.settings.token.trim()?e.settings.token:void 0,password:e.password.trim()?e.password:void 0,clientName:"clawdbot-control-ui",mode:"webchat",onHello:t=>{e.connected=!0,e.hello=t,Ph(e,t),hr(e),Sh(e),un(e,{quiet:!0}),Se(e,{quiet:!0}),Vs(e)},onClose:({code:t,reason:n})=>{e.connected=!1,e.lastError=`disconnected (${t}): ${n||"no reason"}`},onEvent:t=>Mh(e,t),onGap:({expected:t,received:n})=>{e.lastError=`event gap detected (expected seq ${t}, got ${n}); refresh recommended`}}),e.client.start()}function Mh(e,t){if(e.eventLogBuffer=[{ts:Date.now(),event:t.event,payload:t.payload},...e.eventLogBuffer].slice(0,250),e.tab==="debug"&&(e.eventLog=e.eventLogBuffer),t.event==="agent"){if(e.onboarding)return;Rl(e,t.payload);return}if(t.event==="chat"){const n=t.payload;n?.sessionKey&&_a(e,n.sessionKey);const s=kl(e,n);(s==="final"||s==="error"||s==="aborted")&&(Rs(e),ud(e)),s==="final"&&Je(e);return}if(t.event==="presence"){const n=t.payload;n?.presence&&Array.isArray(n.presence)&&(e.presenceEntries=n.presence,e.presenceError=null,e.presenceStatus=null);return}if(t.event==="cron"&&e.tab==="cron"&&Gs(e),(t.event==="device.pair.requested"||t.event==="device.pair.resolved")&&Se(e,{quiet:!0}),t.event==="exec.approval.requested"){const n=Ch(t.payload);if(n){e.execApprovalQueue=Lh(e.execApprovalQueue,n),e.execApprovalError=null;const s=Math.max(0,n.expiresAtMs-Date.now()+500);window.setTimeout(()=>{e.execApprovalQueue=Ko(e.execApprovalQueue,n.id)},s)}return}if(t.event==="exec.approval.resolved"){const n=Ih(t.payload);n&&(e.execApprovalQueue=Ko(e.execApprovalQueue,n.id))}}function Ph(e,t){const n=t.snapshot;n?.presence&&Array.isArray(n.presence)&&(e.presenceEntries=n.presence),n?.health&&(e.debugHealth=n.health),n?.sessionDefaults&&Rh(e,n.sessionDefaults)}function Nh(e){e.basePath=Zc(),nd(e,!0),Xc(e),ed(e),window.addEventListener("popstate",e.popStateHandler),Yc(e),gr(e),Vc(e),e.tab==="logs"&&zs(e),e.tab==="debug"&&qs(e)}function Oh(e){Dl(e)}function Dh(e){window.removeEventListener("popstate",e.popStateHandler),Gc(e),js(e),Ws(e),td(e),e.topbarObserver?.disconnect(),e.topbarObserver=null}function Bh(e,t){if(e.tab==="chat"&&(t.has("chatMessages")||t.has("chatToolMessages")||t.has("chatStream")||t.has("chatLoading")||t.has("tab"))){const n=t.has("tab"),s=t.has("chatLoading")&&t.get("chatLoading")===!0&&e.chatLoading===!1;rn(e,n||s||!e.chatHasAutoScrolled)}e.tab==="logs"&&(t.has("logsEntries")||t.has("logsAutoFollow")||t.has("tab"))&&e.logsAutoFollow&&e.logsAtBottom&&sa(e,t.has("tab")||t.has("logsAutoFollow"))}async function Fh(e,t){await Gl(e,t),await oe(e,!0)}async function Uh(e){await Yl(e),await oe(e,!0)}async function Kh(e){await Ql(e),await oe(e,!0)}async function Hh(e){await os(e),await me(e),await oe(e,!0)}async function zh(e){await me(e),await oe(e,!0)}function jh(e){if(!Array.isArray(e))return{};const t={};for(const n of e){if(typeof n!="string")continue;const[s,...i]=n.split(":");if(!s||i.length===0)continue;const o=s.trim(),a=i.join(":").trim();o&&a&&(t[o]=a)}return t}function vr(e){return(e.channelsSnapshot?.channelAccounts?.nostr??[])[0]?.accountId??e.nostrProfileAccountId??"default"}function mr(e,t=""){return`/api/channels/nostr/${encodeURIComponent(e)}/profile${t}`}function qh(e,t,n){e.nostrProfileAccountId=t,e.nostrProfileFormState=zp(n??void 0)}function Wh(e){e.nostrProfileFormState=null,e.nostrProfileAccountId=null}function Vh(e,t,n){const s=e.nostrProfileFormState;s&&(e.nostrProfileFormState={...s,values:{...s.values,[t]:n},fieldErrors:{...s.fieldErrors,[t]:""}})}function Gh(e){const t=e.nostrProfileFormState;t&&(e.nostrProfileFormState={...t,showAdvanced:!t.showAdvanced})}async function Yh(e){const t=e.nostrProfileFormState;if(!t||t.saving)return;const n=vr(e);e.nostrProfileFormState={...t,saving:!0,error:null,success:null,fieldErrors:{}};try{const s=await fetch(mr(n),{method:"PUT",headers:{"Content-Type":"application/json"},body:JSON.stringify(t.values)}),i=await s.json().catch(()=>null);if(!s.ok||i?.ok===!1||!i){const o=i?.error??`Profile update failed (${s.status})`;e.nostrProfileFormState={...t,saving:!1,error:o,success:null,fieldErrors:jh(i?.details)};return}if(!i.persisted){e.nostrProfileFormState={...t,saving:!1,error:"Profile publish failed on all relays.",success:null};return}e.nostrProfileFormState={...t,saving:!1,error:null,success:"Profile published to relays.",fieldErrors:{},original:{...t.values}},await oe(e,!0)}catch(s){e.nostrProfileFormState={...t,saving:!1,error:`Profile update failed: ${String(s)}`,success:null}}}async function Qh(e){const t=e.nostrProfileFormState;if(!t||t.importing)return;const n=vr(e);e.nostrProfileFormState={...t,importing:!0,error:null,success:null};try{const s=await fetch(mr(n,"/import"),{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({autoMerge:!0})}),i=await s.json().catch(()=>null);if(!s.ok||i?.ok===!1||!i){const r=i?.error??`Profile import failed (${s.status})`;e.nostrProfileFormState={...t,importing:!1,error:r,success:null};return}const o=i.merged??i.imported??null,a=o?{...t.values,...o}:t.values,c=!!(a.banner||a.website||a.nip05||a.lud16);e.nostrProfileFormState={...t,importing:!1,values:a,error:null,success:i.saved?"Profile imported from relays. Review and publish.":"Profile imported. Review and publish.",showAdvanced:c},i.saved&&await oe(e,!0)}catch(s){e.nostrProfileFormState={...t,importing:!1,error:`Profile import failed: ${String(s)}`,success:null}}}var Jh=Object.defineProperty,Zh=Object.getOwnPropertyDescriptor,b=(e,t,n,s)=>{for(var i=s>1?void 0:s?Zh(t,n):t,o=e.length-1,a;o>=0;o--)(a=e[o])&&(i=(s?a(t,n,i):a(i))||i);return s&&i&&Jh(t,n,i),i};const Zn=al();function Xh(){if(!window.location.search)return!1;const t=new URLSearchParams(window.location.search).get("onboarding");if(!t)return!1;const n=t.trim().toLowerCase();return n==="1"||n==="true"||n==="yes"||n==="on"}let m=class extends Ye{constructor(){super(...arguments),this.settings=rl(),this.password="",this.tab="chat",this.onboarding=Xh(),this.connected=!1,this.theme=this.settings.theme??"system",this.themeResolved="dark",this.hello=null,this.lastError=null,this.eventLog=[],this.eventLogBuffer=[],this.toolStreamSyncTimer=null,this.sidebarCloseTimer=null,this.assistantName=Zn.name,this.assistantAvatar=Zn.avatar,this.assistantAgentId=Zn.agentId??null,this.sessionKey=this.settings.sessionKey,this.chatLoading=!1,this.chatSending=!1,this.chatMessage="",this.chatMessages=[],this.chatToolMessages=[],this.chatStream=null,this.chatStreamStartedAt=null,this.chatRunId=null,this.chatAvatarUrl=null,this.chatThinkingLevel=null,this.chatQueue=[],this.sidebarOpen=!1,this.sidebarContent=null,this.sidebarError=null,this.splitRatio=this.settings.splitRatio,this.nodesLoading=!1,this.nodes=[],this.devicesLoading=!1,this.devicesError=null,this.devicesList=null,this.execApprovalsLoading=!1,this.execApprovalsSaving=!1,this.execApprovalsDirty=!1,this.execApprovalsSnapshot=null,this.execApprovalsForm=null,this.execApprovalsSelectedAgent=null,this.execApprovalsTarget="gateway",this.execApprovalsTargetNodeId=null,this.execApprovalQueue=[],this.execApprovalBusy=!1,this.execApprovalError=null,this.configLoading=!1,this.configRaw=`{
+}
+`,this.configValid=null,this.configIssues=[],this.configSaving=!1,this.configApplying=!1,this.updateRunning=!1,this.applySessionKey=this.settings.lastActiveSessionKey,this.configSnapshot=null,this.configSchema=null,this.configSchemaVersion=null,this.configSchemaLoading=!1,this.configUiHints={},this.configForm=null,this.configFormOriginal=null,this.configFormDirty=!1,this.configFormMode="form",this.configSearchQuery="",this.configActiveSection=null,this.configActiveSubsection=null,this.channelsLoading=!1,this.channelsSnapshot=null,this.channelsError=null,this.channelsLastSuccess=null,this.whatsappLoginMessage=null,this.whatsappLoginQrDataUrl=null,this.whatsappLoginConnected=null,this.whatsappBusy=!1,this.nostrProfileFormState=null,this.nostrProfileAccountId=null,this.presenceLoading=!1,this.presenceEntries=[],this.presenceError=null,this.presenceStatus=null,this.agentsLoading=!1,this.agentsList=null,this.agentsError=null,this.sessionsLoading=!1,this.sessionsResult=null,this.sessionsError=null,this.sessionsFilterActive="",this.sessionsFilterLimit="120",this.sessionsIncludeGlobal=!0,this.sessionsIncludeUnknown=!1,this.cronLoading=!1,this.cronJobs=[],this.cronStatus=null,this.cronError=null,this.cronForm={...Ah},this.cronRunsJobId=null,this.cronRuns=[],this.cronBusy=!1,this.skillsLoading=!1,this.skillsReport=null,this.skillsError=null,this.skillsFilter="",this.skillEdits={},this.skillsBusyKey=null,this.skillMessages={},this.debugLoading=!1,this.debugStatus=null,this.debugHealth=null,this.debugModels=[],this.debugHeartbeat=null,this.debugCallMethod="",this.debugCallParams="{}",this.debugCallResult=null,this.debugCallError=null,this.logsLoading=!1,this.logsError=null,this.logsFile=null,this.logsEntries=[],this.logsFilterText="",this.logsLevelFilters={...xh},this.logsAutoFollow=!0,this.logsTruncated=!1,this.logsCursor=null,this.logsLastFetchAt=null,this.logsLimit=500,this.logsMaxBytes=25e4,this.logsAtBottom=!0,this.client=null,this.chatScrollFrame=null,this.chatScrollTimeout=null,this.chatHasAutoScrolled=!1,this.chatUserNearBottom=!0,this.nodesPollInterval=null,this.logsPollInterval=null,this.debugPollInterval=null,this.logsScrollFrame=null,this.toolStreamById=new Map,this.toolStreamOrder=[],this.basePath="",this.popStateHandler=()=>sd(this),this.themeMedia=null,this.themeMediaHandler=null,this.topbarObserver=null}createRenderRoot(){return this}connectedCallback(){super.connectedCallback(),Nh(this)}firstUpdated(){Oh(this)}disconnectedCallback(){Dh(this),super.disconnectedCallback()}updated(e){Bh(this,e)}connect(){gr(this)}handleChatScroll(e){Ml(this,e)}handleLogsScroll(e){Pl(this,e)}exportLogs(e,t){Ol(e,t)}resetToolStream(){Rs(this)}resetChatScroll(){Nl(this)}async loadAssistantIdentity(){await hr(this)}applySettings(e){$e(this,e)}setTab(e){Qc(this,e)}setTheme(e,t){Jc(this,e,t)}async loadOverview(){await Ca(this)}async loadCron(){await Gs(this)}async handleAbortChat(){await La(this)}removeQueuedMessage(e){ld(this,e)}async handleSendChat(e,t){await cd(this,e,t)}async handleWhatsAppStart(e){await Fh(this,e)}async handleWhatsAppWait(){await Uh(this)}async handleWhatsAppLogout(){await Kh(this)}async handleChannelConfigSave(){await Hh(this)}async handleChannelConfigReload(){await zh(this)}handleNostrProfileEdit(e,t){qh(this,e,t)}handleNostrProfileCancel(){Wh(this)}handleNostrProfileFieldChange(e,t){Vh(this,e,t)}async handleNostrProfileSave(){await Yh(this)}async handleNostrProfileImport(){await Qh(this)}handleNostrProfileToggleAdvanced(){Gh(this)}async handleExecApprovalDecision(e){const t=this.execApprovalQueue[0];if(!(!t||!this.client||this.execApprovalBusy)){this.execApprovalBusy=!0,this.execApprovalError=null;try{await this.client.request("exec.approval.resolve",{id:t.id,decision:e}),this.execApprovalQueue=this.execApprovalQueue.filter(n=>n.id!==t.id)}catch(n){this.execApprovalError=`Exec approval failed: ${String(n)}`}finally{this.execApprovalBusy=!1}}}handleOpenSidebar(e){this.sidebarCloseTimer!=null&&(window.clearTimeout(this.sidebarCloseTimer),this.sidebarCloseTimer=null),this.sidebarContent=e,this.sidebarError=null,this.sidebarOpen=!0}handleCloseSidebar(){this.sidebarOpen=!1,this.sidebarCloseTimer!=null&&window.clearTimeout(this.sidebarCloseTimer),this.sidebarCloseTimer=window.setTimeout(()=>{this.sidebarOpen||(this.sidebarContent=null,this.sidebarError=null,this.sidebarCloseTimer=null)},200)}handleSplitRatioChange(e){const t=Math.max(.4,Math.min(.7,e));this.splitRatio=t,this.applySettings({...this.settings,splitRatio:t})}render(){return kh(this)}};b([y()],m.prototype,"settings",2);b([y()],m.prototype,"password",2);b([y()],m.prototype,"tab",2);b([y()],m.prototype,"onboarding",2);b([y()],m.prototype,"connected",2);b([y()],m.prototype,"theme",2);b([y()],m.prototype,"themeResolved",2);b([y()],m.prototype,"hello",2);b([y()],m.prototype,"lastError",2);b([y()],m.prototype,"eventLog",2);b([y()],m.prototype,"assistantName",2);b([y()],m.prototype,"assistantAvatar",2);b([y()],m.prototype,"assistantAgentId",2);b([y()],m.prototype,"sessionKey",2);b([y()],m.prototype,"chatLoading",2);b([y()],m.prototype,"chatSending",2);b([y()],m.prototype,"chatMessage",2);b([y()],m.prototype,"chatMessages",2);b([y()],m.prototype,"chatToolMessages",2);b([y()],m.prototype,"chatStream",2);b([y()],m.prototype,"chatStreamStartedAt",2);b([y()],m.prototype,"chatRunId",2);b([y()],m.prototype,"chatAvatarUrl",2);b([y()],m.prototype,"chatThinkingLevel",2);b([y()],m.prototype,"chatQueue",2);b([y()],m.prototype,"sidebarOpen",2);b([y()],m.prototype,"sidebarContent",2);b([y()],m.prototype,"sidebarError",2);b([y()],m.prototype,"splitRatio",2);b([y()],m.prototype,"nodesLoading",2);b([y()],m.prototype,"nodes",2);b([y()],m.prototype,"devicesLoading",2);b([y()],m.prototype,"devicesError",2);b([y()],m.prototype,"devicesList",2);b([y()],m.prototype,"execApprovalsLoading",2);b([y()],m.prototype,"execApprovalsSaving",2);b([y()],m.prototype,"execApprovalsDirty",2);b([y()],m.prototype,"execApprovalsSnapshot",2);b([y()],m.prototype,"execApprovalsForm",2);b([y()],m.prototype,"execApprovalsSelectedAgent",2);b([y()],m.prototype,"execApprovalsTarget",2);b([y()],m.prototype,"execApprovalsTargetNodeId",2);b([y()],m.prototype,"execApprovalQueue",2);b([y()],m.prototype,"execApprovalBusy",2);b([y()],m.prototype,"execApprovalError",2);b([y()],m.prototype,"configLoading",2);b([y()],m.prototype,"configRaw",2);b([y()],m.prototype,"configValid",2);b([y()],m.prototype,"configIssues",2);b([y()],m.prototype,"configSaving",2);b([y()],m.prototype,"configApplying",2);b([y()],m.prototype,"updateRunning",2);b([y()],m.prototype,"applySessionKey",2);b([y()],m.prototype,"configSnapshot",2);b([y()],m.prototype,"configSchema",2);b([y()],m.prototype,"configSchemaVersion",2);b([y()],m.prototype,"configSchemaLoading",2);b([y()],m.prototype,"configUiHints",2);b([y()],m.prototype,"configForm",2);b([y()],m.prototype,"configFormOriginal",2);b([y()],m.prototype,"configFormDirty",2);b([y()],m.prototype,"configFormMode",2);b([y()],m.prototype,"configSearchQuery",2);b([y()],m.prototype,"configActiveSection",2);b([y()],m.prototype,"configActiveSubsection",2);b([y()],m.prototype,"channelsLoading",2);b([y()],m.prototype,"channelsSnapshot",2);b([y()],m.prototype,"channelsError",2);b([y()],m.prototype,"channelsLastSuccess",2);b([y()],m.prototype,"whatsappLoginMessage",2);b([y()],m.prototype,"whatsappLoginQrDataUrl",2);b([y()],m.prototype,"whatsappLoginConnected",2);b([y()],m.prototype,"whatsappBusy",2);b([y()],m.prototype,"nostrProfileFormState",2);b([y()],m.prototype,"nostrProfileAccountId",2);b([y()],m.prototype,"presenceLoading",2);b([y()],m.prototype,"presenceEntries",2);b([y()],m.prototype,"presenceError",2);b([y()],m.prototype,"presenceStatus",2);b([y()],m.prototype,"agentsLoading",2);b([y()],m.prototype,"agentsList",2);b([y()],m.prototype,"agentsError",2);b([y()],m.prototype,"sessionsLoading",2);b([y()],m.prototype,"sessionsResult",2);b([y()],m.prototype,"sessionsError",2);b([y()],m.prototype,"sessionsFilterActive",2);b([y()],m.prototype,"sessionsFilterLimit",2);b([y()],m.prototype,"sessionsIncludeGlobal",2);b([y()],m.prototype,"sessionsIncludeUnknown",2);b([y()],m.prototype,"cronLoading",2);b([y()],m.prototype,"cronJobs",2);b([y()],m.prototype,"cronStatus",2);b([y()],m.prototype,"cronError",2);b([y()],m.prototype,"cronForm",2);b([y()],m.prototype,"cronRunsJobId",2);b([y()],m.prototype,"cronRuns",2);b([y()],m.prototype,"cronBusy",2);b([y()],m.prototype,"skillsLoading",2);b([y()],m.prototype,"skillsReport",2);b([y()],m.prototype,"skillsError",2);b([y()],m.prototype,"skillsFilter",2);b([y()],m.prototype,"skillEdits",2);b([y()],m.prototype,"skillsBusyKey",2);b([y()],m.prototype,"skillMessages",2);b([y()],m.prototype,"debugLoading",2);b([y()],m.prototype,"debugStatus",2);b([y()],m.prototype,"debugHealth",2);b([y()],m.prototype,"debugModels",2);b([y()],m.prototype,"debugHeartbeat",2);b([y()],m.prototype,"debugCallMethod",2);b([y()],m.prototype,"debugCallParams",2);b([y()],m.prototype,"debugCallResult",2);b([y()],m.prototype,"debugCallError",2);b([y()],m.prototype,"logsLoading",2);b([y()],m.prototype,"logsError",2);b([y()],m.prototype,"logsFile",2);b([y()],m.prototype,"logsEntries",2);b([y()],m.prototype,"logsFilterText",2);b([y()],m.prototype,"logsLevelFilters",2);b([y()],m.prototype,"logsAutoFollow",2);b([y()],m.prototype,"logsTruncated",2);b([y()],m.prototype,"logsCursor",2);b([y()],m.prototype,"logsLastFetchAt",2);b([y()],m.prototype,"logsLimit",2);b([y()],m.prototype,"logsMaxBytes",2);b([y()],m.prototype,"logsAtBottom",2);m=b([Yo("clawdbot-app")],m);
+//# sourceMappingURL=index-bYQnHP3a.js.map
diff --git a/dist/control-ui/assets/index-bYQnHP3a.js.map b/dist/control-ui/assets/index-bYQnHP3a.js.map
new file mode 100644
index 000000000..1df80bade
--- /dev/null
+++ b/dist/control-ui/assets/index-bYQnHP3a.js.map
@@ -0,0 +1 @@
+{"version":3,"file":"index-bYQnHP3a.js","sources":["../../../node_modules/.pnpm/@lit+reactive-element@2.1.2/node_modules/@lit/reactive-element/css-tag.js","../../../node_modules/.pnpm/@lit+reactive-element@2.1.2/node_modules/@lit/reactive-element/reactive-element.js","../../../node_modules/.pnpm/lit-html@3.3.2/node_modules/lit-html/lit-html.js","../../../node_modules/.pnpm/lit-element@4.2.2/node_modules/lit-element/lit-element.js","../../../node_modules/.pnpm/@lit+reactive-element@2.1.2/node_modules/@lit/reactive-element/decorators/custom-element.js","../../../node_modules/.pnpm/@lit+reactive-element@2.1.2/node_modules/@lit/reactive-element/decorators/property.js","../../../node_modules/.pnpm/@lit+reactive-element@2.1.2/node_modules/@lit/reactive-element/decorators/state.js","../../../ui/src/ui/assistant-identity.ts","../../../ui/src/ui/storage.ts","../../../src/sessions/session-key-utils.ts","../../../ui/src/ui/navigation.ts","../../../ui/src/ui/format.ts","../../../ui/src/ui/chat/message-extract.ts","../../../ui/src/ui/uuid.ts","../../../ui/src/ui/controllers/chat.ts","../../../ui/src/ui/controllers/sessions.ts","../../../ui/src/ui/app-tool-stream.ts","../../../ui/src/ui/app-scroll.ts","../../../ui/src/ui/controllers/config/form-utils.ts","../../../ui/src/ui/controllers/config.ts","../../../ui/src/ui/controllers/cron.ts","../../../ui/src/ui/controllers/channels.ts","../../../ui/src/ui/controllers/debug.ts","../../../ui/src/ui/controllers/logs.ts","../../../node_modules/.pnpm/@noble+ed25519@3.0.0/node_modules/@noble/ed25519/index.js","../../../ui/src/ui/device-identity.ts","../../../ui/src/ui/device-auth.ts","../../../ui/src/ui/controllers/devices.ts","../../../ui/src/ui/controllers/nodes.ts","../../../ui/src/ui/controllers/exec-approvals.ts","../../../ui/src/ui/controllers/presence.ts","../../../ui/src/ui/controllers/skills.ts","../../../ui/src/ui/theme.ts","../../../ui/src/ui/theme-transition.ts","../../../ui/src/ui/app-polling.ts","../../../ui/src/ui/app-settings.ts","../../../ui/src/ui/app-chat.ts","../../../node_modules/.pnpm/lit-html@3.3.2/node_modules/lit-html/directive.js","../../../node_modules/.pnpm/lit-html@3.3.2/node_modules/lit-html/directive-helpers.js","../../../node_modules/.pnpm/lit-html@3.3.2/node_modules/lit-html/directives/repeat.js","../../../ui/src/ui/chat/message-normalizer.ts","../../../node_modules/.pnpm/lit-html@3.3.2/node_modules/lit-html/directives/unsafe-html.js","../../../node_modules/.pnpm/dompurify@3.3.1/node_modules/dompurify/dist/purify.es.mjs","../../../node_modules/.pnpm/marked@17.0.1/node_modules/marked/lib/marked.esm.js","../../../ui/src/ui/markdown.ts","../../../ui/src/ui/icons.ts","../../../ui/src/ui/chat/copy-as-markdown.ts","../../../ui/src/ui/tool-display.ts","../../../ui/src/ui/chat/constants.ts","../../../ui/src/ui/chat/tool-helpers.ts","../../../ui/src/ui/chat/tool-cards.ts","../../../ui/src/ui/chat/grouped-render.ts","../../../ui/src/ui/views/markdown-sidebar.ts","../../../ui/src/ui/components/resizable-divider.ts","../../../ui/src/ui/views/chat.ts","../../../ui/src/ui/views/config-form.shared.ts","../../../ui/src/ui/views/config-form.node.ts","../../../ui/src/ui/views/config-form.render.ts","../../../ui/src/ui/views/config-form.analyze.ts","../../../ui/src/ui/views/config.ts","../../../ui/src/ui/views/channels.shared.ts","../../../ui/src/ui/views/channels.config.ts","../../../ui/src/ui/views/channels.discord.ts","../../../ui/src/ui/views/channels.imessage.ts","../../../ui/src/ui/views/channels.nostr-profile-form.ts","../../../ui/src/ui/views/channels.nostr.ts","../../../ui/src/ui/views/channels.signal.ts","../../../ui/src/ui/views/channels.slack.ts","../../../ui/src/ui/views/channels.telegram.ts","../../../ui/src/ui/views/channels.whatsapp.ts","../../../ui/src/ui/views/channels.ts","../../../ui/src/ui/presenter.ts","../../../ui/src/ui/views/cron.ts","../../../ui/src/ui/views/debug.ts","../../../ui/src/ui/views/instances.ts","../../../ui/src/ui/views/logs.ts","../../../ui/src/ui/views/nodes.ts","../../../ui/src/ui/views/overview.ts","../../../ui/src/ui/views/sessions.ts","../../../ui/src/ui/views/exec-approval.ts","../../../ui/src/ui/views/skills.ts","../../../ui/src/ui/app-render.helpers.ts","../../../ui/src/ui/app-render.ts","../../../ui/src/ui/app-defaults.ts","../../../ui/src/ui/controllers/agents.ts","../../../src/gateway/protocol/client-info.ts","../../../src/gateway/device-auth.ts","../../../ui/src/ui/gateway.ts","../../../ui/src/ui/controllers/exec-approval.ts","../../../ui/src/ui/controllers/assistant-identity.ts","../../../ui/src/ui/app-gateway.ts","../../../ui/src/ui/app-lifecycle.ts","../../../ui/src/ui/app-channels.ts","../../../ui/src/ui/app.ts"],"sourcesContent":["/**\n * @license\n * Copyright 2019 Google LLC\n * SPDX-License-Identifier: BSD-3-Clause\n */\nconst t=globalThis,e=t.ShadowRoot&&(void 0===t.ShadyCSS||t.ShadyCSS.nativeShadow)&&\"adoptedStyleSheets\"in Document.prototype&&\"replace\"in CSSStyleSheet.prototype,s=Symbol(),o=new WeakMap;class n{constructor(t,e,o){if(this._$cssResult$=!0,o!==s)throw Error(\"CSSResult is not constructable. Use `unsafeCSS` or `css` instead.\");this.cssText=t,this.t=e}get styleSheet(){let t=this.o;const s=this.t;if(e&&void 0===t){const e=void 0!==s&&1===s.length;e&&(t=o.get(s)),void 0===t&&((this.o=t=new CSSStyleSheet).replaceSync(this.cssText),e&&o.set(s,t))}return t}toString(){return this.cssText}}const r=t=>new n(\"string\"==typeof t?t:t+\"\",void 0,s),i=(t,...e)=>{const o=1===t.length?t[0]:e.reduce((e,s,o)=>e+(t=>{if(!0===t._$cssResult$)return t.cssText;if(\"number\"==typeof t)return t;throw Error(\"Value passed to 'css' function must be a 'css' function result: \"+t+\". Use 'unsafeCSS' to pass non-literal values, but take care to ensure page security.\")})(s)+t[o+1],t[0]);return new n(o,t,s)},S=(s,o)=>{if(e)s.adoptedStyleSheets=o.map(t=>t instanceof CSSStyleSheet?t:t.styleSheet);else for(const e of o){const o=document.createElement(\"style\"),n=t.litNonce;void 0!==n&&o.setAttribute(\"nonce\",n),o.textContent=e.cssText,s.appendChild(o)}},c=e?t=>t:t=>t instanceof CSSStyleSheet?(t=>{let e=\"\";for(const s of t.cssRules)e+=s.cssText;return r(e)})(t):t;export{n as CSSResult,S as adoptStyles,i as css,c as getCompatibleStyle,e as supportsAdoptingStyleSheets,r as unsafeCSS};\n//# sourceMappingURL=css-tag.js.map\n","import{getCompatibleStyle as t,adoptStyles as s}from\"./css-tag.js\";export{CSSResult,css,supportsAdoptingStyleSheets,unsafeCSS}from\"./css-tag.js\";\n/**\n * @license\n * Copyright 2017 Google LLC\n * SPDX-License-Identifier: BSD-3-Clause\n */const{is:i,defineProperty:e,getOwnPropertyDescriptor:h,getOwnPropertyNames:r,getOwnPropertySymbols:o,getPrototypeOf:n}=Object,a=globalThis,c=a.trustedTypes,l=c?c.emptyScript:\"\",p=a.reactiveElementPolyfillSupport,d=(t,s)=>t,u={toAttribute(t,s){switch(s){case Boolean:t=t?l:null;break;case Object:case Array:t=null==t?t:JSON.stringify(t)}return t},fromAttribute(t,s){let i=t;switch(s){case Boolean:i=null!==t;break;case Number:i=null===t?null:Number(t);break;case Object:case Array:try{i=JSON.parse(t)}catch(t){i=null}}return i}},f=(t,s)=>!i(t,s),b={attribute:!0,type:String,converter:u,reflect:!1,useDefault:!1,hasChanged:f};Symbol.metadata??=Symbol(\"metadata\"),a.litPropertyMetadata??=new WeakMap;class y extends HTMLElement{static addInitializer(t){this._$Ei(),(this.l??=[]).push(t)}static get observedAttributes(){return this.finalize(),this._$Eh&&[...this._$Eh.keys()]}static createProperty(t,s=b){if(s.state&&(s.attribute=!1),this._$Ei(),this.prototype.hasOwnProperty(t)&&((s=Object.create(s)).wrapped=!0),this.elementProperties.set(t,s),!s.noAccessor){const i=Symbol(),h=this.getPropertyDescriptor(t,i,s);void 0!==h&&e(this.prototype,t,h)}}static getPropertyDescriptor(t,s,i){const{get:e,set:r}=h(this.prototype,t)??{get(){return this[s]},set(t){this[s]=t}};return{get:e,set(s){const h=e?.call(this);r?.call(this,s),this.requestUpdate(t,h,i)},configurable:!0,enumerable:!0}}static getPropertyOptions(t){return this.elementProperties.get(t)??b}static _$Ei(){if(this.hasOwnProperty(d(\"elementProperties\")))return;const t=n(this);t.finalize(),void 0!==t.l&&(this.l=[...t.l]),this.elementProperties=new Map(t.elementProperties)}static finalize(){if(this.hasOwnProperty(d(\"finalized\")))return;if(this.finalized=!0,this._$Ei(),this.hasOwnProperty(d(\"properties\"))){const t=this.properties,s=[...r(t),...o(t)];for(const i of s)this.createProperty(i,t[i])}const t=this[Symbol.metadata];if(null!==t){const s=litPropertyMetadata.get(t);if(void 0!==s)for(const[t,i]of s)this.elementProperties.set(t,i)}this._$Eh=new Map;for(const[t,s]of this.elementProperties){const i=this._$Eu(t,s);void 0!==i&&this._$Eh.set(i,t)}this.elementStyles=this.finalizeStyles(this.styles)}static finalizeStyles(s){const i=[];if(Array.isArray(s)){const e=new Set(s.flat(1/0).reverse());for(const s of e)i.unshift(t(s))}else void 0!==s&&i.push(t(s));return i}static _$Eu(t,s){const i=s.attribute;return!1===i?void 0:\"string\"==typeof i?i:\"string\"==typeof t?t.toLowerCase():void 0}constructor(){super(),this._$Ep=void 0,this.isUpdatePending=!1,this.hasUpdated=!1,this._$Em=null,this._$Ev()}_$Ev(){this._$ES=new Promise(t=>this.enableUpdating=t),this._$AL=new Map,this._$E_(),this.requestUpdate(),this.constructor.l?.forEach(t=>t(this))}addController(t){(this._$EO??=new Set).add(t),void 0!==this.renderRoot&&this.isConnected&&t.hostConnected?.()}removeController(t){this._$EO?.delete(t)}_$E_(){const t=new Map,s=this.constructor.elementProperties;for(const i of s.keys())this.hasOwnProperty(i)&&(t.set(i,this[i]),delete this[i]);t.size>0&&(this._$Ep=t)}createRenderRoot(){const t=this.shadowRoot??this.attachShadow(this.constructor.shadowRootOptions);return s(t,this.constructor.elementStyles),t}connectedCallback(){this.renderRoot??=this.createRenderRoot(),this.enableUpdating(!0),this._$EO?.forEach(t=>t.hostConnected?.())}enableUpdating(t){}disconnectedCallback(){this._$EO?.forEach(t=>t.hostDisconnected?.())}attributeChangedCallback(t,s,i){this._$AK(t,i)}_$ET(t,s){const i=this.constructor.elementProperties.get(t),e=this.constructor._$Eu(t,i);if(void 0!==e&&!0===i.reflect){const h=(void 0!==i.converter?.toAttribute?i.converter:u).toAttribute(s,i.type);this._$Em=t,null==h?this.removeAttribute(e):this.setAttribute(e,h),this._$Em=null}}_$AK(t,s){const i=this.constructor,e=i._$Eh.get(t);if(void 0!==e&&this._$Em!==e){const t=i.getPropertyOptions(e),h=\"function\"==typeof t.converter?{fromAttribute:t.converter}:void 0!==t.converter?.fromAttribute?t.converter:u;this._$Em=e;const r=h.fromAttribute(s,t.type);this[e]=r??this._$Ej?.get(e)??r,this._$Em=null}}requestUpdate(t,s,i,e=!1,h){if(void 0!==t){const r=this.constructor;if(!1===e&&(h=this[t]),i??=r.getPropertyOptions(t),!((i.hasChanged??f)(h,s)||i.useDefault&&i.reflect&&h===this._$Ej?.get(t)&&!this.hasAttribute(r._$Eu(t,i))))return;this.C(t,s,i)}!1===this.isUpdatePending&&(this._$ES=this._$EP())}C(t,s,{useDefault:i,reflect:e,wrapped:h},r){i&&!(this._$Ej??=new Map).has(t)&&(this._$Ej.set(t,r??s??this[t]),!0!==h||void 0!==r)||(this._$AL.has(t)||(this.hasUpdated||i||(s=void 0),this._$AL.set(t,s)),!0===e&&this._$Em!==t&&(this._$Eq??=new Set).add(t))}async _$EP(){this.isUpdatePending=!0;try{await this._$ES}catch(t){Promise.reject(t)}const t=this.scheduleUpdate();return null!=t&&await t,!this.isUpdatePending}scheduleUpdate(){return this.performUpdate()}performUpdate(){if(!this.isUpdatePending)return;if(!this.hasUpdated){if(this.renderRoot??=this.createRenderRoot(),this._$Ep){for(const[t,s]of this._$Ep)this[t]=s;this._$Ep=void 0}const t=this.constructor.elementProperties;if(t.size>0)for(const[s,i]of t){const{wrapped:t}=i,e=this[s];!0!==t||this._$AL.has(s)||void 0===e||this.C(s,void 0,i,e)}}let t=!1;const s=this._$AL;try{t=this.shouldUpdate(s),t?(this.willUpdate(s),this._$EO?.forEach(t=>t.hostUpdate?.()),this.update(s)):this._$EM()}catch(s){throw t=!1,this._$EM(),s}t&&this._$AE(s)}willUpdate(t){}_$AE(t){this._$EO?.forEach(t=>t.hostUpdated?.()),this.hasUpdated||(this.hasUpdated=!0,this.firstUpdated(t)),this.updated(t)}_$EM(){this._$AL=new Map,this.isUpdatePending=!1}get updateComplete(){return this.getUpdateComplete()}getUpdateComplete(){return this._$ES}shouldUpdate(t){return!0}update(t){this._$Eq&&=this._$Eq.forEach(t=>this._$ET(t,this[t])),this._$EM()}updated(t){}firstUpdated(t){}}y.elementStyles=[],y.shadowRootOptions={mode:\"open\"},y[d(\"elementProperties\")]=new Map,y[d(\"finalized\")]=new Map,p?.({ReactiveElement:y}),(a.reactiveElementVersions??=[]).push(\"2.1.2\");export{y as ReactiveElement,s as adoptStyles,u as defaultConverter,t as getCompatibleStyle,f as notEqual};\n//# sourceMappingURL=reactive-element.js.map\n","/**\n * @license\n * Copyright 2017 Google LLC\n * SPDX-License-Identifier: BSD-3-Clause\n */\nconst t=globalThis,i=t=>t,s=t.trustedTypes,e=s?s.createPolicy(\"lit-html\",{createHTML:t=>t}):void 0,h=\"$lit$\",o=`lit$${Math.random().toFixed(9).slice(2)}$`,n=\"?\"+o,r=`<${n}>`,l=document,c=()=>l.createComment(\"\"),a=t=>null===t||\"object\"!=typeof t&&\"function\"!=typeof t,u=Array.isArray,d=t=>u(t)||\"function\"==typeof t?.[Symbol.iterator],f=\"[ \\t\\n\\f\\r]\",v=/<(?:(!--|\\/[^a-zA-Z])|(\\/?[a-zA-Z][^>\\s]*)|(\\/?$))/g,_=/-->/g,m=/>/g,p=RegExp(`>|${f}(?:([^\\\\s\"'>=/]+)(${f}*=${f}*(?:[^ \\t\\n\\f\\r\"'\\`<>=]|(\"|')|))|$)`,\"g\"),g=/'/g,$=/\"/g,y=/^(?:script|style|textarea|title)$/i,x=t=>(i,...s)=>({_$litType$:t,strings:i,values:s}),b=x(1),w=x(2),T=x(3),E=Symbol.for(\"lit-noChange\"),A=Symbol.for(\"lit-nothing\"),C=new WeakMap,P=l.createTreeWalker(l,129);function V(t,i){if(!u(t)||!t.hasOwnProperty(\"raw\"))throw Error(\"invalid template strings array\");return void 0!==e?e.createHTML(i):i}const N=(t,i)=>{const s=t.length-1,e=[];let n,l=2===i?\"\":3===i?\"\":\"\")),e]};class S{constructor({strings:t,_$litType$:i},e){let r;this.parts=[];let l=0,a=0;const u=t.length-1,d=this.parts,[f,v]=N(t,i);if(this.el=S.createElement(f,e),P.currentNode=this.el.content,2===i||3===i){const t=this.el.content.firstChild;t.replaceWith(...t.childNodes)}for(;null!==(r=P.nextNode())&&d.length0){r.textContent=s?s.emptyScript:\"\";for(let s=0;s2||\"\"!==s[0]||\"\"!==s[1]?(this._$AH=Array(s.length-1).fill(new String),this.strings=s):this._$AH=A}_$AI(t,i=this,s,e){const h=this.strings;let o=!1;if(void 0===h)t=M(this,t,i,0),o=!a(t)||t!==this._$AH&&t!==E,o&&(this._$AH=t);else{const e=t;let n,r;for(t=h[0],n=0;n{const e=s?.renderBefore??i;let h=e._$litPart$;if(void 0===h){const t=s?.renderBefore??null;e._$litPart$=h=new k(i.insertBefore(c(),t),t,void 0,s??{})}return h._$AI(t),h};export{j as _$LH,b as html,T as mathml,E as noChange,A as nothing,D as render,w as svg};\n//# sourceMappingURL=lit-html.js.map\n","import{ReactiveElement as t}from\"@lit/reactive-element\";export*from\"@lit/reactive-element\";import{render as e,noChange as r}from\"lit-html\";export*from\"lit-html\";\n/**\n * @license\n * Copyright 2017 Google LLC\n * SPDX-License-Identifier: BSD-3-Clause\n */const s=globalThis;class i extends t{constructor(){super(...arguments),this.renderOptions={host:this},this._$Do=void 0}createRenderRoot(){const t=super.createRenderRoot();return this.renderOptions.renderBefore??=t.firstChild,t}update(t){const r=this.render();this.hasUpdated||(this.renderOptions.isConnected=this.isConnected),super.update(t),this._$Do=e(r,this.renderRoot,this.renderOptions)}connectedCallback(){super.connectedCallback(),this._$Do?.setConnected(!0)}disconnectedCallback(){super.disconnectedCallback(),this._$Do?.setConnected(!1)}render(){return r}}i._$litElement$=!0,i[\"finalized\"]=!0,s.litElementHydrateSupport?.({LitElement:i});const o=s.litElementPolyfillSupport;o?.({LitElement:i});const n={_$AK:(t,e,r)=>{t._$AK(e,r)},_$AL:t=>t._$AL};(s.litElementVersions??=[]).push(\"4.2.2\");export{i as LitElement,n as _$LE};\n//# sourceMappingURL=lit-element.js.map\n","/**\n * @license\n * Copyright 2017 Google LLC\n * SPDX-License-Identifier: BSD-3-Clause\n */\nconst t=t=>(e,o)=>{void 0!==o?o.addInitializer(()=>{customElements.define(t,e)}):customElements.define(t,e)};export{t as customElement};\n//# sourceMappingURL=custom-element.js.map\n","import{notEqual as t,defaultConverter as e}from\"../reactive-element.js\";\n/**\n * @license\n * Copyright 2017 Google LLC\n * SPDX-License-Identifier: BSD-3-Clause\n */const o={attribute:!0,type:String,converter:e,reflect:!1,hasChanged:t},r=(t=o,e,r)=>{const{kind:n,metadata:i}=r;let s=globalThis.litPropertyMetadata.get(i);if(void 0===s&&globalThis.litPropertyMetadata.set(i,s=new Map),\"setter\"===n&&((t=Object.create(t)).wrapped=!0),s.set(r.name,t),\"accessor\"===n){const{name:o}=r;return{set(r){const n=e.get.call(this);e.set.call(this,r),this.requestUpdate(o,n,t,!0,r)},init(e){return void 0!==e&&this.C(o,void 0,t,e),e}}}if(\"setter\"===n){const{name:o}=r;return function(r){const n=this[o];e.call(this,r),this.requestUpdate(o,n,t,!0,r)}}throw Error(\"Unsupported decorator location: \"+n)};function n(t){return(e,o)=>\"object\"==typeof o?r(t,e,o):((t,e,o)=>{const r=e.hasOwnProperty(o);return e.constructor.createProperty(o,t),r?Object.getOwnPropertyDescriptor(e,o):void 0})(t,e,o)}export{n as property,r as standardProperty};\n//# sourceMappingURL=property.js.map\n","import{property as t}from\"./property.js\";\n/**\n * @license\n * Copyright 2017 Google LLC\n * SPDX-License-Identifier: BSD-3-Clause\n */function r(r){return t({...r,state:!0,attribute:!1})}export{r as state};\n//# sourceMappingURL=state.js.map\n","const MAX_ASSISTANT_NAME = 50;\nconst MAX_ASSISTANT_AVATAR = 200;\n\nexport const DEFAULT_ASSISTANT_NAME = \"Assistant\";\nexport const DEFAULT_ASSISTANT_AVATAR = \"A\";\n\nexport type AssistantIdentity = {\n agentId?: string | null;\n name: string;\n avatar: string | null;\n};\n\ndeclare global {\n interface Window {\n __CLAWDBOT_ASSISTANT_NAME__?: string;\n __CLAWDBOT_ASSISTANT_AVATAR__?: string;\n }\n}\n\nfunction coerceIdentityValue(value: string | undefined, maxLength: number): string | undefined {\n if (typeof value !== \"string\") return undefined;\n const trimmed = value.trim();\n if (!trimmed) return undefined;\n if (trimmed.length <= maxLength) return trimmed;\n return trimmed.slice(0, maxLength);\n}\n\nexport function normalizeAssistantIdentity(\n input?: Partial | null,\n): AssistantIdentity {\n const name =\n coerceIdentityValue(input?.name, MAX_ASSISTANT_NAME) ?? DEFAULT_ASSISTANT_NAME;\n const avatar = coerceIdentityValue(input?.avatar ?? undefined, MAX_ASSISTANT_AVATAR) ?? null;\n const agentId =\n typeof input?.agentId === \"string\" && input.agentId.trim()\n ? input.agentId.trim()\n : null;\n return { agentId, name, avatar };\n}\n\nexport function resolveInjectedAssistantIdentity(): AssistantIdentity {\n if (typeof window === \"undefined\") {\n return normalizeAssistantIdentity({});\n }\n return normalizeAssistantIdentity({\n name: window.__CLAWDBOT_ASSISTANT_NAME__,\n avatar: window.__CLAWDBOT_ASSISTANT_AVATAR__,\n });\n}\n","const KEY = \"clawdbot.control.settings.v1\";\n\nimport type { ThemeMode } from \"./theme\";\n\nexport type UiSettings = {\n gatewayUrl: string;\n token: string;\n sessionKey: string;\n lastActiveSessionKey: string;\n theme: ThemeMode;\n chatFocusMode: boolean;\n chatShowThinking: boolean;\n splitRatio: number; // Sidebar split ratio (0.4 to 0.7, default 0.6)\n navCollapsed: boolean; // Collapsible sidebar state\n navGroupsCollapsed: Record; // Which nav groups are collapsed\n};\n\nexport function loadSettings(): UiSettings {\n const defaultUrl = (() => {\n const proto = location.protocol === \"https:\" ? \"wss\" : \"ws\";\n return `${proto}://${location.host}`;\n })();\n\n const defaults: UiSettings = {\n gatewayUrl: defaultUrl,\n token: \"\",\n sessionKey: \"main\",\n lastActiveSessionKey: \"main\",\n theme: \"system\",\n chatFocusMode: false,\n chatShowThinking: true,\n splitRatio: 0.6,\n navCollapsed: false,\n navGroupsCollapsed: {},\n };\n\n try {\n const raw = localStorage.getItem(KEY);\n if (!raw) return defaults;\n const parsed = JSON.parse(raw) as Partial;\n return {\n gatewayUrl:\n typeof parsed.gatewayUrl === \"string\" && parsed.gatewayUrl.trim()\n ? parsed.gatewayUrl.trim()\n : defaults.gatewayUrl,\n token: typeof parsed.token === \"string\" ? parsed.token : defaults.token,\n sessionKey:\n typeof parsed.sessionKey === \"string\" && parsed.sessionKey.trim()\n ? parsed.sessionKey.trim()\n : defaults.sessionKey,\n lastActiveSessionKey:\n typeof parsed.lastActiveSessionKey === \"string\" &&\n parsed.lastActiveSessionKey.trim()\n ? parsed.lastActiveSessionKey.trim()\n : (typeof parsed.sessionKey === \"string\" &&\n parsed.sessionKey.trim()) ||\n defaults.lastActiveSessionKey,\n theme:\n parsed.theme === \"light\" ||\n parsed.theme === \"dark\" ||\n parsed.theme === \"system\"\n ? parsed.theme\n : defaults.theme,\n chatFocusMode:\n typeof parsed.chatFocusMode === \"boolean\"\n ? parsed.chatFocusMode\n : defaults.chatFocusMode,\n chatShowThinking:\n typeof parsed.chatShowThinking === \"boolean\"\n ? parsed.chatShowThinking\n : defaults.chatShowThinking,\n splitRatio:\n typeof parsed.splitRatio === \"number\" &&\n parsed.splitRatio >= 0.4 &&\n parsed.splitRatio <= 0.7\n ? parsed.splitRatio\n : defaults.splitRatio,\n navCollapsed:\n typeof parsed.navCollapsed === \"boolean\"\n ? parsed.navCollapsed\n : defaults.navCollapsed,\n navGroupsCollapsed:\n typeof parsed.navGroupsCollapsed === \"object\" &&\n parsed.navGroupsCollapsed !== null\n ? parsed.navGroupsCollapsed\n : defaults.navGroupsCollapsed,\n };\n } catch {\n return defaults;\n }\n}\n\nexport function saveSettings(next: UiSettings) {\n localStorage.setItem(KEY, JSON.stringify(next));\n}\n","export type ParsedAgentSessionKey = {\n agentId: string;\n rest: string;\n};\n\nexport function parseAgentSessionKey(\n sessionKey: string | undefined | null,\n): ParsedAgentSessionKey | null {\n const raw = (sessionKey ?? \"\").trim();\n if (!raw) return null;\n const parts = raw.split(\":\").filter(Boolean);\n if (parts.length < 3) return null;\n if (parts[0] !== \"agent\") return null;\n const agentId = parts[1]?.trim();\n const rest = parts.slice(2).join(\":\");\n if (!agentId || !rest) return null;\n return { agentId, rest };\n}\n\nexport function isSubagentSessionKey(sessionKey: string | undefined | null): boolean {\n const raw = (sessionKey ?? \"\").trim();\n if (!raw) return false;\n if (raw.toLowerCase().startsWith(\"subagent:\")) return true;\n const parsed = parseAgentSessionKey(raw);\n return Boolean((parsed?.rest ?? \"\").toLowerCase().startsWith(\"subagent:\"));\n}\n\nexport function isAcpSessionKey(sessionKey: string | undefined | null): boolean {\n const raw = (sessionKey ?? \"\").trim();\n if (!raw) return false;\n const normalized = raw.toLowerCase();\n if (normalized.startsWith(\"acp:\")) return true;\n const parsed = parseAgentSessionKey(raw);\n return Boolean((parsed?.rest ?? \"\").toLowerCase().startsWith(\"acp:\"));\n}\n\nconst THREAD_SESSION_MARKERS = [\":thread:\", \":topic:\"];\n\nexport function resolveThreadParentSessionKey(\n sessionKey: string | undefined | null,\n): string | null {\n const raw = (sessionKey ?? \"\").trim();\n if (!raw) return null;\n const normalized = raw.toLowerCase();\n let idx = -1;\n for (const marker of THREAD_SESSION_MARKERS) {\n const candidate = normalized.lastIndexOf(marker);\n if (candidate > idx) idx = candidate;\n }\n if (idx <= 0) return null;\n const parent = raw.slice(0, idx).trim();\n return parent ? parent : null;\n}\n","export const TAB_GROUPS = [\n { label: \"Chat\", tabs: [\"chat\"] },\n {\n label: \"Control\",\n tabs: [\"overview\", \"channels\", \"instances\", \"sessions\", \"cron\"],\n },\n { label: \"Agent\", tabs: [\"skills\", \"nodes\"] },\n { label: \"Settings\", tabs: [\"config\", \"debug\", \"logs\"] },\n] as const;\n\nexport type Tab =\n | \"overview\"\n | \"channels\"\n | \"instances\"\n | \"sessions\"\n | \"cron\"\n | \"skills\"\n | \"nodes\"\n | \"chat\"\n | \"config\"\n | \"debug\"\n | \"logs\";\n\nconst TAB_PATHS: Record = {\n overview: \"/overview\",\n channels: \"/channels\",\n instances: \"/instances\",\n sessions: \"/sessions\",\n cron: \"/cron\",\n skills: \"/skills\",\n nodes: \"/nodes\",\n chat: \"/chat\",\n config: \"/config\",\n debug: \"/debug\",\n logs: \"/logs\",\n};\n\nconst PATH_TO_TAB = new Map(\n Object.entries(TAB_PATHS).map(([tab, path]) => [path, tab as Tab]),\n);\n\nexport function normalizeBasePath(basePath: string): string {\n if (!basePath) return \"\";\n let base = basePath.trim();\n if (!base.startsWith(\"/\")) base = `/${base}`;\n if (base === \"/\") return \"\";\n if (base.endsWith(\"/\")) base = base.slice(0, -1);\n return base;\n}\n\nexport function normalizePath(path: string): string {\n if (!path) return \"/\";\n let normalized = path.trim();\n if (!normalized.startsWith(\"/\")) normalized = `/${normalized}`;\n if (normalized.length > 1 && normalized.endsWith(\"/\")) {\n normalized = normalized.slice(0, -1);\n }\n return normalized;\n}\n\nexport function pathForTab(tab: Tab, basePath = \"\"): string {\n const base = normalizeBasePath(basePath);\n const path = TAB_PATHS[tab];\n return base ? `${base}${path}` : path;\n}\n\nexport function tabFromPath(pathname: string, basePath = \"\"): Tab | null {\n const base = normalizeBasePath(basePath);\n let path = pathname || \"/\";\n if (base) {\n if (path === base) {\n path = \"/\";\n } else if (path.startsWith(`${base}/`)) {\n path = path.slice(base.length);\n }\n }\n let normalized = normalizePath(path).toLowerCase();\n if (normalized.endsWith(\"/index.html\")) normalized = \"/\";\n if (normalized === \"/\") return \"chat\";\n return PATH_TO_TAB.get(normalized) ?? null;\n}\n\nexport function inferBasePathFromPathname(pathname: string): string {\n let normalized = normalizePath(pathname);\n if (normalized.endsWith(\"/index.html\")) {\n normalized = normalizePath(normalized.slice(0, -\"/index.html\".length));\n }\n if (normalized === \"/\") return \"\";\n const segments = normalized.split(\"/\").filter(Boolean);\n if (segments.length === 0) return \"\";\n for (let i = 0; i < segments.length; i++) {\n const candidate = `/${segments.slice(i).join(\"/\")}`.toLowerCase();\n if (PATH_TO_TAB.has(candidate)) {\n const prefix = segments.slice(0, i);\n return prefix.length ? `/${prefix.join(\"/\")}` : \"\";\n }\n }\n return `/${segments.join(\"/\")}`;\n}\n\nexport function iconForTab(tab: Tab): string {\n switch (tab) {\n case \"chat\":\n return \"💬\";\n case \"overview\":\n return \"📊\";\n case \"channels\":\n return \"🔗\";\n case \"instances\":\n return \"📡\";\n case \"sessions\":\n return \"📄\";\n case \"cron\":\n return \"⏰\";\n case \"skills\":\n return \"⚡️\";\n case \"nodes\":\n return \"🖥️\";\n case \"config\":\n return \"⚙️\";\n case \"debug\":\n return \"🐞\";\n case \"logs\":\n return \"🧾\";\n default:\n return \"📁\";\n }\n}\n\nexport function titleForTab(tab: Tab) {\n switch (tab) {\n case \"overview\":\n return \"Overview\";\n case \"channels\":\n return \"Channels\";\n case \"instances\":\n return \"Instances\";\n case \"sessions\":\n return \"Sessions\";\n case \"cron\":\n return \"Cron Jobs\";\n case \"skills\":\n return \"Skills\";\n case \"nodes\":\n return \"Nodes\";\n case \"chat\":\n return \"Chat\";\n case \"config\":\n return \"Config\";\n case \"debug\":\n return \"Debug\";\n case \"logs\":\n return \"Logs\";\n default:\n return \"Control\";\n }\n}\n\nexport function subtitleForTab(tab: Tab) {\n switch (tab) {\n case \"overview\":\n return \"Gateway status, entry points, and a fast health read.\";\n case \"channels\":\n return \"Manage channels and settings.\";\n case \"instances\":\n return \"Presence beacons from connected clients and nodes.\";\n case \"sessions\":\n return \"Inspect active sessions and adjust per-session defaults.\";\n case \"cron\":\n return \"Schedule wakeups and recurring agent runs.\";\n case \"skills\":\n return \"Manage skill availability and API key injection.\";\n case \"nodes\":\n return \"Paired devices, capabilities, and command exposure.\";\n case \"chat\":\n return \"Direct gateway chat session for quick interventions.\";\n case \"config\":\n return \"Edit ~/.clawdbot/clawdbot.json safely.\";\n case \"debug\":\n return \"Gateway snapshots, events, and manual RPC calls.\";\n case \"logs\":\n return \"Live tail of the gateway file logs.\";\n default:\n return \"\";\n }\n}\n","export function formatMs(ms?: number | null): string {\n if (!ms && ms !== 0) return \"n/a\";\n return new Date(ms).toLocaleString();\n}\n\nexport function formatAgo(ms?: number | null): string {\n if (!ms && ms !== 0) return \"n/a\";\n const diff = Date.now() - ms;\n if (diff < 0) return \"just now\";\n const sec = Math.round(diff / 1000);\n if (sec < 60) return `${sec}s ago`;\n const min = Math.round(sec / 60);\n if (min < 60) return `${min}m ago`;\n const hr = Math.round(min / 60);\n if (hr < 48) return `${hr}h ago`;\n const day = Math.round(hr / 24);\n return `${day}d ago`;\n}\n\nexport function formatDurationMs(ms?: number | null): string {\n if (!ms && ms !== 0) return \"n/a\";\n if (ms < 1000) return `${ms}ms`;\n const sec = Math.round(ms / 1000);\n if (sec < 60) return `${sec}s`;\n const min = Math.round(sec / 60);\n if (min < 60) return `${min}m`;\n const hr = Math.round(min / 60);\n if (hr < 48) return `${hr}h`;\n const day = Math.round(hr / 24);\n return `${day}d`;\n}\n\nexport function formatList(values?: Array): string {\n if (!values || values.length === 0) return \"none\";\n return values.filter((v): v is string => Boolean(v && v.trim())).join(\", \");\n}\n\nexport function clampText(value: string, max = 120): string {\n if (value.length <= max) return value;\n return `${value.slice(0, Math.max(0, max - 1))}…`;\n}\n\nexport function truncateText(value: string, max: number): {\n text: string;\n truncated: boolean;\n total: number;\n} {\n if (value.length <= max) {\n return { text: value, truncated: false, total: value.length };\n }\n return {\n text: value.slice(0, Math.max(0, max)),\n truncated: true,\n total: value.length,\n };\n}\n\nexport function toNumber(value: string, fallback: number): number {\n const n = Number(value);\n return Number.isFinite(n) ? n : fallback;\n}\n\nexport function parseList(input: string): string[] {\n return input\n .split(/[,\\n]/)\n .map((v) => v.trim())\n .filter((v) => v.length > 0);\n}\n\nconst THINKING_TAG_RE = /<\\s*\\/?\\s*think(?:ing)?\\s*>/gi;\nconst THINKING_OPEN_RE = /<\\s*think(?:ing)?\\s*>/i;\nconst THINKING_CLOSE_RE = /<\\s*\\/\\s*think(?:ing)?\\s*>/i;\n\nexport function stripThinkingTags(value: string): string {\n if (!value) return value;\n const hasOpen = THINKING_OPEN_RE.test(value);\n const hasClose = THINKING_CLOSE_RE.test(value);\n if (!hasOpen && !hasClose) return value;\n // If we don't have a balanced pair, avoid dropping trailing content.\n if (hasOpen !== hasClose) {\n if (!hasOpen) return value.replace(THINKING_CLOSE_RE, \"\").trimStart();\n return value.replace(THINKING_OPEN_RE, \"\").trimStart();\n }\n\n if (!THINKING_TAG_RE.test(value)) return value;\n THINKING_TAG_RE.lastIndex = 0;\n\n let result = \"\";\n let lastIndex = 0;\n let inThinking = false;\n for (const match of value.matchAll(THINKING_TAG_RE)) {\n const idx = match.index ?? 0;\n if (!inThinking) {\n result += value.slice(lastIndex, idx);\n }\n const tag = match[0].toLowerCase();\n inThinking = !tag.includes(\"/\");\n lastIndex = idx + match[0].length;\n }\n if (!inThinking) {\n result += value.slice(lastIndex);\n }\n return result.trimStart();\n}\n","import { stripThinkingTags } from \"../format\";\n\nconst ENVELOPE_PREFIX = /^\\[([^\\]]+)\\]\\s*/;\nconst ENVELOPE_CHANNELS = [\n \"WebChat\",\n \"WhatsApp\",\n \"Telegram\",\n \"Signal\",\n \"Slack\",\n \"Discord\",\n \"iMessage\",\n \"Teams\",\n \"Matrix\",\n \"Zalo\",\n \"Zalo Personal\",\n \"BlueBubbles\",\n];\n\nfunction looksLikeEnvelopeHeader(header: string): boolean {\n if (/\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}Z\\b/.test(header)) return true;\n if (/\\d{4}-\\d{2}-\\d{2} \\d{2}:\\d{2}\\b/.test(header)) return true;\n return ENVELOPE_CHANNELS.some((label) => header.startsWith(`${label} `));\n}\n\nexport function stripEnvelope(text: string): string {\n const match = text.match(ENVELOPE_PREFIX);\n if (!match) return text;\n const header = match[1] ?? \"\";\n if (!looksLikeEnvelopeHeader(header)) return text;\n return text.slice(match[0].length);\n}\n\nexport function extractText(message: unknown): string | null {\n const m = message as Record;\n const role = typeof m.role === \"string\" ? m.role : \"\";\n const content = m.content;\n if (typeof content === \"string\") {\n const processed = role === \"assistant\" ? stripThinkingTags(content) : stripEnvelope(content);\n return processed;\n }\n if (Array.isArray(content)) {\n const parts = content\n .map((p) => {\n const item = p as Record;\n if (item.type === \"text\" && typeof item.text === \"string\") return item.text;\n return null;\n })\n .filter((v): v is string => typeof v === \"string\");\n if (parts.length > 0) {\n const joined = parts.join(\"\\n\");\n const processed = role === \"assistant\" ? stripThinkingTags(joined) : stripEnvelope(joined);\n return processed;\n }\n }\n if (typeof m.text === \"string\") {\n const processed = role === \"assistant\" ? stripThinkingTags(m.text) : stripEnvelope(m.text);\n return processed;\n }\n return null;\n}\n\nexport function extractThinking(message: unknown): string | null {\n const m = message as Record;\n const content = m.content;\n const parts: string[] = [];\n if (Array.isArray(content)) {\n for (const p of content) {\n const item = p as Record;\n if (item.type === \"thinking\" && typeof item.thinking === \"string\") {\n const cleaned = item.thinking.trim();\n if (cleaned) parts.push(cleaned);\n }\n }\n }\n if (parts.length > 0) return parts.join(\"\\n\");\n\n // Back-compat: older logs may still have tags inside text blocks.\n const rawText = extractRawText(message);\n if (!rawText) return null;\n const matches = [\n ...rawText.matchAll(\n /<\\s*think(?:ing)?\\s*>([\\s\\S]*?)<\\s*\\/\\s*think(?:ing)?\\s*>/gi,\n ),\n ];\n const extracted = matches\n .map((m) => (m[1] ?? \"\").trim())\n .filter(Boolean);\n return extracted.length > 0 ? extracted.join(\"\\n\") : null;\n}\n\nexport function extractRawText(message: unknown): string | null {\n const m = message as Record;\n const content = m.content;\n if (typeof content === \"string\") return content;\n if (Array.isArray(content)) {\n const parts = content\n .map((p) => {\n const item = p as Record;\n if (item.type === \"text\" && typeof item.text === \"string\") return item.text;\n return null;\n })\n .filter((v): v is string => typeof v === \"string\");\n if (parts.length > 0) return parts.join(\"\\n\");\n }\n if (typeof m.text === \"string\") return m.text;\n return null;\n}\n\nexport function formatReasoningMarkdown(text: string): string {\n const trimmed = text.trim();\n if (!trimmed) return \"\";\n const lines = trimmed\n .split(/\\r?\\n/)\n .map((line) => line.trim())\n .filter(Boolean)\n .map((line) => `_${line}_`);\n return lines.length ? [\"_Reasoning:_\", ...lines].join(\"\\n\") : \"\";\n}\n","export type CryptoLike = {\n randomUUID?: (() => string) | undefined;\n getRandomValues?: ((array: Uint8Array) => Uint8Array) | undefined;\n};\n\nfunction uuidFromBytes(bytes: Uint8Array): string {\n bytes[6] = (bytes[6] & 0x0f) | 0x40; // version 4\n bytes[8] = (bytes[8] & 0x3f) | 0x80; // variant 1\n\n let hex = \"\";\n for (let i = 0; i < bytes.length; i++) {\n hex += bytes[i]!.toString(16).padStart(2, \"0\");\n }\n\n return `${hex.slice(0, 8)}-${hex.slice(8, 12)}-${hex.slice(12, 16)}-${hex.slice(\n 16,\n 20,\n )}-${hex.slice(20)}`;\n}\n\nfunction weakRandomBytes(): Uint8Array {\n const bytes = new Uint8Array(16);\n const now = Date.now();\n for (let i = 0; i < bytes.length; i++) bytes[i] = Math.floor(Math.random() * 256);\n bytes[0] ^= now & 0xff;\n bytes[1] ^= (now >>> 8) & 0xff;\n bytes[2] ^= (now >>> 16) & 0xff;\n bytes[3] ^= (now >>> 24) & 0xff;\n return bytes;\n}\n\nexport function generateUUID(cryptoLike: CryptoLike | null = globalThis.crypto): string {\n if (cryptoLike && typeof cryptoLike.randomUUID === \"function\") return cryptoLike.randomUUID();\n\n if (cryptoLike && typeof cryptoLike.getRandomValues === \"function\") {\n const bytes = new Uint8Array(16);\n cryptoLike.getRandomValues(bytes);\n return uuidFromBytes(bytes);\n }\n\n return uuidFromBytes(weakRandomBytes());\n}\n\n","import type { GatewayBrowserClient } from \"../gateway\";\nimport { extractText } from \"../chat/message-extract\";\nimport { generateUUID } from \"../uuid\";\n\nexport type ChatState = {\n client: GatewayBrowserClient | null;\n connected: boolean;\n sessionKey: string;\n chatLoading: boolean;\n chatMessages: unknown[];\n chatThinkingLevel: string | null;\n chatSending: boolean;\n chatMessage: string;\n chatRunId: string | null;\n chatStream: string | null;\n chatStreamStartedAt: number | null;\n lastError: string | null;\n};\n\nexport type ChatEventPayload = {\n runId: string;\n sessionKey: string;\n state: \"delta\" | \"final\" | \"aborted\" | \"error\";\n message?: unknown;\n errorMessage?: string;\n};\n\nexport async function loadChatHistory(state: ChatState) {\n if (!state.client || !state.connected) return;\n state.chatLoading = true;\n state.lastError = null;\n try {\n const res = (await state.client.request(\"chat.history\", {\n sessionKey: state.sessionKey,\n limit: 200,\n })) as { messages?: unknown[]; thinkingLevel?: string | null };\n state.chatMessages = Array.isArray(res.messages) ? res.messages : [];\n state.chatThinkingLevel = res.thinkingLevel ?? null;\n } catch (err) {\n state.lastError = String(err);\n } finally {\n state.chatLoading = false;\n }\n}\n\nexport async function sendChatMessage(state: ChatState, message: string): Promise {\n if (!state.client || !state.connected) return false;\n const msg = message.trim();\n if (!msg) return false;\n\n const now = Date.now();\n state.chatMessages = [\n ...state.chatMessages,\n {\n role: \"user\",\n content: [{ type: \"text\", text: msg }],\n timestamp: now,\n },\n ];\n\n state.chatSending = true;\n state.lastError = null;\n const runId = generateUUID();\n state.chatRunId = runId;\n state.chatStream = \"\";\n state.chatStreamStartedAt = now;\n try {\n await state.client.request(\"chat.send\", {\n sessionKey: state.sessionKey,\n message: msg,\n deliver: false,\n idempotencyKey: runId,\n });\n return true;\n } catch (err) {\n const error = String(err);\n state.chatRunId = null;\n state.chatStream = null;\n state.chatStreamStartedAt = null;\n state.lastError = error;\n state.chatMessages = [\n ...state.chatMessages,\n {\n role: \"assistant\",\n content: [{ type: \"text\", text: \"Error: \" + error }],\n timestamp: Date.now(),\n },\n ];\n return false;\n } finally {\n state.chatSending = false;\n }\n}\n\nexport async function abortChatRun(state: ChatState): Promise {\n if (!state.client || !state.connected) return false;\n const runId = state.chatRunId;\n try {\n await state.client.request(\n \"chat.abort\",\n runId\n ? { sessionKey: state.sessionKey, runId }\n : { sessionKey: state.sessionKey },\n );\n return true;\n } catch (err) {\n state.lastError = String(err);\n return false;\n }\n}\n\nexport function handleChatEvent(\n state: ChatState,\n payload?: ChatEventPayload,\n) {\n if (!payload) return null;\n if (payload.sessionKey !== state.sessionKey) return null;\n if (payload.runId && state.chatRunId && payload.runId !== state.chatRunId)\n return null;\n\n if (payload.state === \"delta\") {\n const next = extractText(payload.message);\n if (typeof next === \"string\") {\n const current = state.chatStream ?? \"\";\n if (!current || next.length >= current.length) {\n state.chatStream = next;\n }\n }\n } else if (payload.state === \"final\") {\n state.chatStream = null;\n state.chatRunId = null;\n state.chatStreamStartedAt = null;\n } else if (payload.state === \"aborted\") {\n state.chatStream = null;\n state.chatRunId = null;\n state.chatStreamStartedAt = null;\n } else if (payload.state === \"error\") {\n state.chatStream = null;\n state.chatRunId = null;\n state.chatStreamStartedAt = null;\n state.lastError = payload.errorMessage ?? \"chat error\";\n }\n return payload.state;\n}\n","import type { GatewayBrowserClient } from \"../gateway\";\nimport { toNumber } from \"../format\";\nimport type { SessionsListResult } from \"../types\";\n\nexport type SessionsState = {\n client: GatewayBrowserClient | null;\n connected: boolean;\n sessionsLoading: boolean;\n sessionsResult: SessionsListResult | null;\n sessionsError: string | null;\n sessionsFilterActive: string;\n sessionsFilterLimit: string;\n sessionsIncludeGlobal: boolean;\n sessionsIncludeUnknown: boolean;\n};\n\nexport async function loadSessions(state: SessionsState) {\n if (!state.client || !state.connected) return;\n if (state.sessionsLoading) return;\n state.sessionsLoading = true;\n state.sessionsError = null;\n try {\n const params: Record = {\n includeGlobal: state.sessionsIncludeGlobal,\n includeUnknown: state.sessionsIncludeUnknown,\n };\n const activeMinutes = toNumber(state.sessionsFilterActive, 0);\n const limit = toNumber(state.sessionsFilterLimit, 0);\n if (activeMinutes > 0) params.activeMinutes = activeMinutes;\n if (limit > 0) params.limit = limit;\n const res = (await state.client.request(\"sessions.list\", params)) as\n | SessionsListResult\n | undefined;\n if (res) state.sessionsResult = res;\n } catch (err) {\n state.sessionsError = String(err);\n } finally {\n state.sessionsLoading = false;\n }\n}\n\nexport async function patchSession(\n state: SessionsState,\n key: string,\n patch: {\n label?: string | null;\n thinkingLevel?: string | null;\n verboseLevel?: string | null;\n reasoningLevel?: string | null;\n },\n) {\n if (!state.client || !state.connected) return;\n const params: Record = { key };\n if (\"label\" in patch) params.label = patch.label;\n if (\"thinkingLevel\" in patch) params.thinkingLevel = patch.thinkingLevel;\n if (\"verboseLevel\" in patch) params.verboseLevel = patch.verboseLevel;\n if (\"reasoningLevel\" in patch) params.reasoningLevel = patch.reasoningLevel;\n try {\n await state.client.request(\"sessions.patch\", params);\n await loadSessions(state);\n } catch (err) {\n state.sessionsError = String(err);\n }\n}\n\nexport async function deleteSession(state: SessionsState, key: string) {\n if (!state.client || !state.connected) return;\n if (state.sessionsLoading) return;\n const confirmed = window.confirm(\n `Delete session \"${key}\"?\\n\\nDeletes the session entry and archives its transcript.`,\n );\n if (!confirmed) return;\n state.sessionsLoading = true;\n state.sessionsError = null;\n try {\n await state.client.request(\"sessions.delete\", { key, deleteTranscript: true });\n await loadSessions(state);\n } catch (err) {\n state.sessionsError = String(err);\n } finally {\n state.sessionsLoading = false;\n }\n}\n","import { truncateText } from \"./format\";\n\nconst TOOL_STREAM_LIMIT = 50;\nconst TOOL_STREAM_THROTTLE_MS = 80;\nconst TOOL_OUTPUT_CHAR_LIMIT = 120_000;\n\nexport type AgentEventPayload = {\n runId: string;\n seq: number;\n stream: string;\n ts: number;\n sessionKey?: string;\n data: Record;\n};\n\nexport type ToolStreamEntry = {\n toolCallId: string;\n runId: string;\n sessionKey?: string;\n name: string;\n args?: unknown;\n output?: string;\n startedAt: number;\n updatedAt: number;\n message: Record;\n};\n\ntype ToolStreamHost = {\n sessionKey: string;\n chatRunId: string | null;\n toolStreamById: Map;\n toolStreamOrder: string[];\n chatToolMessages: Record[];\n toolStreamSyncTimer: number | null;\n};\n\nfunction extractToolOutputText(value: unknown): string | null {\n if (!value || typeof value !== \"object\") return null;\n const record = value as Record;\n if (typeof record.text === \"string\") return record.text;\n const content = record.content;\n if (!Array.isArray(content)) return null;\n const parts = content\n .map((item) => {\n if (!item || typeof item !== \"object\") return null;\n const entry = item as Record;\n if (entry.type === \"text\" && typeof entry.text === \"string\") return entry.text;\n return null;\n })\n .filter((part): part is string => Boolean(part));\n if (parts.length === 0) return null;\n return parts.join(\"\\n\");\n}\n\nfunction formatToolOutput(value: unknown): string | null {\n if (value === null || value === undefined) return null;\n if (typeof value === \"number\" || typeof value === \"boolean\") {\n return String(value);\n }\n const contentText = extractToolOutputText(value);\n let text: string;\n if (typeof value === \"string\") {\n text = value;\n } else if (contentText) {\n text = contentText;\n } else {\n try {\n text = JSON.stringify(value, null, 2);\n } catch {\n text = String(value);\n }\n }\n const truncated = truncateText(text, TOOL_OUTPUT_CHAR_LIMIT);\n if (!truncated.truncated) return truncated.text;\n return `${truncated.text}\\n\\n… truncated (${truncated.total} chars, showing first ${truncated.text.length}).`;\n}\n\nfunction buildToolStreamMessage(entry: ToolStreamEntry): Record {\n const content: Array> = [];\n content.push({\n type: \"toolcall\",\n name: entry.name,\n arguments: entry.args ?? {},\n });\n if (entry.output) {\n content.push({\n type: \"toolresult\",\n name: entry.name,\n text: entry.output,\n });\n }\n return {\n role: \"assistant\",\n toolCallId: entry.toolCallId,\n runId: entry.runId,\n content,\n timestamp: entry.startedAt,\n };\n}\n\nfunction trimToolStream(host: ToolStreamHost) {\n if (host.toolStreamOrder.length <= TOOL_STREAM_LIMIT) return;\n const overflow = host.toolStreamOrder.length - TOOL_STREAM_LIMIT;\n const removed = host.toolStreamOrder.splice(0, overflow);\n for (const id of removed) host.toolStreamById.delete(id);\n}\n\nfunction syncToolStreamMessages(host: ToolStreamHost) {\n host.chatToolMessages = host.toolStreamOrder\n .map((id) => host.toolStreamById.get(id)?.message)\n .filter((msg): msg is Record => Boolean(msg));\n}\n\nexport function flushToolStreamSync(host: ToolStreamHost) {\n if (host.toolStreamSyncTimer != null) {\n clearTimeout(host.toolStreamSyncTimer);\n host.toolStreamSyncTimer = null;\n }\n syncToolStreamMessages(host);\n}\n\nexport function scheduleToolStreamSync(host: ToolStreamHost, force = false) {\n if (force) {\n flushToolStreamSync(host);\n return;\n }\n if (host.toolStreamSyncTimer != null) return;\n host.toolStreamSyncTimer = window.setTimeout(\n () => flushToolStreamSync(host),\n TOOL_STREAM_THROTTLE_MS,\n );\n}\n\nexport function resetToolStream(host: ToolStreamHost) {\n host.toolStreamById.clear();\n host.toolStreamOrder = [];\n host.chatToolMessages = [];\n flushToolStreamSync(host);\n}\n\nexport function handleAgentEvent(host: ToolStreamHost, payload?: AgentEventPayload) {\n if (!payload || payload.stream !== \"tool\") return;\n const sessionKey =\n typeof payload.sessionKey === \"string\" ? payload.sessionKey : undefined;\n if (sessionKey && sessionKey !== host.sessionKey) return;\n // Fallback: only accept session-less events for the active run.\n if (!sessionKey && host.chatRunId && payload.runId !== host.chatRunId) return;\n if (host.chatRunId && payload.runId !== host.chatRunId) return;\n if (!host.chatRunId) return;\n\n const data = payload.data ?? {};\n const toolCallId = typeof data.toolCallId === \"string\" ? data.toolCallId : \"\";\n if (!toolCallId) return;\n const name = typeof data.name === \"string\" ? data.name : \"tool\";\n const phase = typeof data.phase === \"string\" ? data.phase : \"\";\n const args = phase === \"start\" ? data.args : undefined;\n const output =\n phase === \"update\"\n ? formatToolOutput(data.partialResult)\n : phase === \"result\"\n ? formatToolOutput(data.result)\n : undefined;\n\n const now = Date.now();\n let entry = host.toolStreamById.get(toolCallId);\n if (!entry) {\n entry = {\n toolCallId,\n runId: payload.runId,\n sessionKey,\n name,\n args,\n output,\n startedAt: typeof payload.ts === \"number\" ? payload.ts : now,\n updatedAt: now,\n message: {},\n };\n host.toolStreamById.set(toolCallId, entry);\n host.toolStreamOrder.push(toolCallId);\n } else {\n entry.name = name;\n if (args !== undefined) entry.args = args;\n if (output !== undefined) entry.output = output;\n entry.updatedAt = now;\n }\n\n entry.message = buildToolStreamMessage(entry);\n trimToolStream(host);\n scheduleToolStreamSync(host, phase === \"result\");\n}\n","type ScrollHost = {\n updateComplete: Promise;\n querySelector: (selectors: string) => Element | null;\n style: CSSStyleDeclaration;\n chatScrollFrame: number | null;\n chatScrollTimeout: number | null;\n chatHasAutoScrolled: boolean;\n chatUserNearBottom: boolean;\n logsScrollFrame: number | null;\n logsAtBottom: boolean;\n topbarObserver: ResizeObserver | null;\n};\n\nexport function scheduleChatScroll(host: ScrollHost, force = false) {\n if (host.chatScrollFrame) cancelAnimationFrame(host.chatScrollFrame);\n if (host.chatScrollTimeout != null) {\n clearTimeout(host.chatScrollTimeout);\n host.chatScrollTimeout = null;\n }\n const pickScrollTarget = () => {\n const container = host.querySelector(\".chat-thread\") as HTMLElement | null;\n if (container) {\n const overflowY = getComputedStyle(container).overflowY;\n const canScroll =\n overflowY === \"auto\" ||\n overflowY === \"scroll\" ||\n container.scrollHeight - container.clientHeight > 1;\n if (canScroll) return container;\n }\n return (document.scrollingElement ?? document.documentElement) as HTMLElement | null;\n };\n // Wait for Lit render to complete, then scroll\n void host.updateComplete.then(() => {\n host.chatScrollFrame = requestAnimationFrame(() => {\n host.chatScrollFrame = null;\n const target = pickScrollTarget();\n if (!target) return;\n const distanceFromBottom =\n target.scrollHeight - target.scrollTop - target.clientHeight;\n const shouldStick = force || host.chatUserNearBottom || distanceFromBottom < 200;\n if (!shouldStick) return;\n if (force) host.chatHasAutoScrolled = true;\n target.scrollTop = target.scrollHeight;\n host.chatUserNearBottom = true;\n const retryDelay = force ? 150 : 120;\n host.chatScrollTimeout = window.setTimeout(() => {\n host.chatScrollTimeout = null;\n const latest = pickScrollTarget();\n if (!latest) return;\n const latestDistanceFromBottom =\n latest.scrollHeight - latest.scrollTop - latest.clientHeight;\n const shouldStickRetry =\n force || host.chatUserNearBottom || latestDistanceFromBottom < 200;\n if (!shouldStickRetry) return;\n latest.scrollTop = latest.scrollHeight;\n host.chatUserNearBottom = true;\n }, retryDelay);\n });\n });\n}\n\nexport function scheduleLogsScroll(host: ScrollHost, force = false) {\n if (host.logsScrollFrame) cancelAnimationFrame(host.logsScrollFrame);\n void host.updateComplete.then(() => {\n host.logsScrollFrame = requestAnimationFrame(() => {\n host.logsScrollFrame = null;\n const container = host.querySelector(\".log-stream\") as HTMLElement | null;\n if (!container) return;\n const distanceFromBottom =\n container.scrollHeight - container.scrollTop - container.clientHeight;\n const shouldStick = force || distanceFromBottom < 80;\n if (!shouldStick) return;\n container.scrollTop = container.scrollHeight;\n });\n });\n}\n\nexport function handleChatScroll(host: ScrollHost, event: Event) {\n const container = event.currentTarget as HTMLElement | null;\n if (!container) return;\n const distanceFromBottom =\n container.scrollHeight - container.scrollTop - container.clientHeight;\n host.chatUserNearBottom = distanceFromBottom < 200;\n}\n\nexport function handleLogsScroll(host: ScrollHost, event: Event) {\n const container = event.currentTarget as HTMLElement | null;\n if (!container) return;\n const distanceFromBottom =\n container.scrollHeight - container.scrollTop - container.clientHeight;\n host.logsAtBottom = distanceFromBottom < 80;\n}\n\nexport function resetChatScroll(host: ScrollHost) {\n host.chatHasAutoScrolled = false;\n host.chatUserNearBottom = true;\n}\n\nexport function exportLogs(lines: string[], label: string) {\n if (lines.length === 0) return;\n const blob = new Blob([`${lines.join(\"\\n\")}\\n`], { type: \"text/plain\" });\n const url = URL.createObjectURL(blob);\n const anchor = document.createElement(\"a\");\n const stamp = new Date().toISOString().slice(0, 19).replace(/[:T]/g, \"-\");\n anchor.href = url;\n anchor.download = `clawdbot-logs-${label}-${stamp}.log`;\n anchor.click();\n URL.revokeObjectURL(url);\n}\n\nexport function observeTopbar(host: ScrollHost) {\n if (typeof ResizeObserver === \"undefined\") return;\n const topbar = host.querySelector(\".topbar\");\n if (!topbar) return;\n const update = () => {\n const { height } = topbar.getBoundingClientRect();\n host.style.setProperty(\"--topbar-height\", `${height}px`);\n };\n update();\n host.topbarObserver = new ResizeObserver(() => update());\n host.topbarObserver.observe(topbar);\n}\n","export function cloneConfigObject(value: T): T {\n if (typeof structuredClone === \"function\") {\n return structuredClone(value);\n }\n return JSON.parse(JSON.stringify(value)) as T;\n}\n\nexport function serializeConfigForm(form: Record): string {\n return `${JSON.stringify(form, null, 2).trimEnd()}\\n`;\n}\n\nexport function setPathValue(\n obj: Record | unknown[],\n path: Array,\n value: unknown,\n) {\n if (path.length === 0) return;\n let current: Record | unknown[] = obj;\n for (let i = 0; i < path.length - 1; i += 1) {\n const key = path[i];\n const nextKey = path[i + 1];\n if (typeof key === \"number\") {\n if (!Array.isArray(current)) return;\n if (current[key] == null) {\n current[key] =\n typeof nextKey === \"number\" ? [] : ({} as Record);\n }\n current = current[key] as Record | unknown[];\n } else {\n if (typeof current !== \"object\" || current == null) return;\n const record = current as Record;\n if (record[key] == null) {\n record[key] =\n typeof nextKey === \"number\" ? [] : ({} as Record);\n }\n current = record[key] as Record | unknown[];\n }\n }\n const lastKey = path[path.length - 1];\n if (typeof lastKey === \"number\") {\n if (Array.isArray(current)) current[lastKey] = value;\n return;\n }\n if (typeof current === \"object\" && current != null) {\n (current as Record)[lastKey] = value;\n }\n}\n\nexport function removePathValue(\n obj: Record | unknown[],\n path: Array,\n) {\n if (path.length === 0) return;\n let current: Record | unknown[] = obj;\n for (let i = 0; i < path.length - 1; i += 1) {\n const key = path[i];\n if (typeof key === \"number\") {\n if (!Array.isArray(current)) return;\n current = current[key] as Record | unknown[];\n } else {\n if (typeof current !== \"object\" || current == null) return;\n current = (current as Record)[key] as\n | Record\n | unknown[];\n }\n if (current == null) return;\n }\n const lastKey = path[path.length - 1];\n if (typeof lastKey === \"number\") {\n if (Array.isArray(current)) current.splice(lastKey, 1);\n return;\n }\n if (typeof current === \"object\" && current != null) {\n delete (current as Record)[lastKey];\n }\n}\n\n","import type { GatewayBrowserClient } from \"../gateway\";\nimport type {\n ConfigSchemaResponse,\n ConfigSnapshot,\n ConfigUiHints,\n} from \"../types\";\nimport {\n cloneConfigObject,\n removePathValue,\n serializeConfigForm,\n setPathValue,\n} from \"./config/form-utils\";\n\nexport type ConfigState = {\n client: GatewayBrowserClient | null;\n connected: boolean;\n applySessionKey: string;\n configLoading: boolean;\n configRaw: string;\n configValid: boolean | null;\n configIssues: unknown[];\n configSaving: boolean;\n configApplying: boolean;\n updateRunning: boolean;\n configSnapshot: ConfigSnapshot | null;\n configSchema: unknown | null;\n configSchemaVersion: string | null;\n configSchemaLoading: boolean;\n configUiHints: ConfigUiHints;\n configForm: Record | null;\n configFormOriginal: Record | null;\n configFormDirty: boolean;\n configFormMode: \"form\" | \"raw\";\n configSearchQuery: string;\n configActiveSection: string | null;\n configActiveSubsection: string | null;\n lastError: string | null;\n};\n\nexport async function loadConfig(state: ConfigState) {\n if (!state.client || !state.connected) return;\n state.configLoading = true;\n state.lastError = null;\n try {\n const res = (await state.client.request(\"config.get\", {})) as ConfigSnapshot;\n applyConfigSnapshot(state, res);\n } catch (err) {\n state.lastError = String(err);\n } finally {\n state.configLoading = false;\n }\n}\n\nexport async function loadConfigSchema(state: ConfigState) {\n if (!state.client || !state.connected) return;\n if (state.configSchemaLoading) return;\n state.configSchemaLoading = true;\n try {\n const res = (await state.client.request(\n \"config.schema\",\n {},\n )) as ConfigSchemaResponse;\n applyConfigSchema(state, res);\n } catch (err) {\n state.lastError = String(err);\n } finally {\n state.configSchemaLoading = false;\n }\n}\n\nexport function applyConfigSchema(\n state: ConfigState,\n res: ConfigSchemaResponse,\n) {\n state.configSchema = res.schema ?? null;\n state.configUiHints = res.uiHints ?? {};\n state.configSchemaVersion = res.version ?? null;\n}\n\nexport function applyConfigSnapshot(state: ConfigState, snapshot: ConfigSnapshot) {\n state.configSnapshot = snapshot;\n const rawFromSnapshot =\n typeof snapshot.raw === \"string\"\n ? snapshot.raw\n : snapshot.config && typeof snapshot.config === \"object\"\n ? serializeConfigForm(snapshot.config as Record)\n : state.configRaw;\n if (!state.configFormDirty || state.configFormMode === \"raw\") {\n state.configRaw = rawFromSnapshot;\n } else if (state.configForm) {\n state.configRaw = serializeConfigForm(state.configForm);\n } else {\n state.configRaw = rawFromSnapshot;\n }\n state.configValid = typeof snapshot.valid === \"boolean\" ? snapshot.valid : null;\n state.configIssues = Array.isArray(snapshot.issues) ? snapshot.issues : [];\n\n if (!state.configFormDirty) {\n state.configForm = cloneConfigObject(snapshot.config ?? {});\n state.configFormOriginal = cloneConfigObject(snapshot.config ?? {});\n }\n}\n\nexport async function saveConfig(state: ConfigState) {\n if (!state.client || !state.connected) return;\n state.configSaving = true;\n state.lastError = null;\n try {\n const raw =\n state.configFormMode === \"form\" && state.configForm\n ? serializeConfigForm(state.configForm)\n : state.configRaw;\n const baseHash = state.configSnapshot?.hash;\n if (!baseHash) {\n state.lastError = \"Config hash missing; reload and retry.\";\n return;\n }\n await state.client.request(\"config.set\", { raw, baseHash });\n state.configFormDirty = false;\n await loadConfig(state);\n } catch (err) {\n state.lastError = String(err);\n } finally {\n state.configSaving = false;\n }\n}\n\nexport async function applyConfig(state: ConfigState) {\n if (!state.client || !state.connected) return;\n state.configApplying = true;\n state.lastError = null;\n try {\n const raw =\n state.configFormMode === \"form\" && state.configForm\n ? serializeConfigForm(state.configForm)\n : state.configRaw;\n const baseHash = state.configSnapshot?.hash;\n if (!baseHash) {\n state.lastError = \"Config hash missing; reload and retry.\";\n return;\n }\n await state.client.request(\"config.apply\", {\n raw,\n baseHash,\n sessionKey: state.applySessionKey,\n });\n state.configFormDirty = false;\n await loadConfig(state);\n } catch (err) {\n state.lastError = String(err);\n } finally {\n state.configApplying = false;\n }\n}\n\nexport async function runUpdate(state: ConfigState) {\n if (!state.client || !state.connected) return;\n state.updateRunning = true;\n state.lastError = null;\n try {\n await state.client.request(\"update.run\", {\n sessionKey: state.applySessionKey,\n });\n } catch (err) {\n state.lastError = String(err);\n } finally {\n state.updateRunning = false;\n }\n}\n\nexport function updateConfigFormValue(\n state: ConfigState,\n path: Array,\n value: unknown,\n) {\n const base = cloneConfigObject(\n state.configForm ?? state.configSnapshot?.config ?? {},\n );\n setPathValue(base, path, value);\n state.configForm = base;\n state.configFormDirty = true;\n if (state.configFormMode === \"form\") {\n state.configRaw = serializeConfigForm(base);\n }\n}\n\nexport function removeConfigFormValue(\n state: ConfigState,\n path: Array,\n) {\n const base = cloneConfigObject(\n state.configForm ?? state.configSnapshot?.config ?? {},\n );\n removePathValue(base, path);\n state.configForm = base;\n state.configFormDirty = true;\n if (state.configFormMode === \"form\") {\n state.configRaw = serializeConfigForm(base);\n }\n}\n","import { toNumber } from \"../format\";\nimport type { GatewayBrowserClient } from \"../gateway\";\nimport type { CronJob, CronRunLogEntry, CronStatus } from \"../types\";\nimport type { CronFormState } from \"../ui-types\";\n\nexport type CronState = {\n client: GatewayBrowserClient | null;\n connected: boolean;\n cronLoading: boolean;\n cronJobs: CronJob[];\n cronStatus: CronStatus | null;\n cronError: string | null;\n cronForm: CronFormState;\n cronRunsJobId: string | null;\n cronRuns: CronRunLogEntry[];\n cronBusy: boolean;\n};\n\nexport async function loadCronStatus(state: CronState) {\n if (!state.client || !state.connected) return;\n try {\n const res = (await state.client.request(\"cron.status\", {})) as CronStatus;\n state.cronStatus = res;\n } catch (err) {\n state.cronError = String(err);\n }\n}\n\nexport async function loadCronJobs(state: CronState) {\n if (!state.client || !state.connected) return;\n if (state.cronLoading) return;\n state.cronLoading = true;\n state.cronError = null;\n try {\n const res = (await state.client.request(\"cron.list\", {\n includeDisabled: true,\n })) as { jobs?: CronJob[] };\n state.cronJobs = Array.isArray(res.jobs) ? res.jobs : [];\n } catch (err) {\n state.cronError = String(err);\n } finally {\n state.cronLoading = false;\n }\n}\n\nexport function buildCronSchedule(form: CronFormState) {\n if (form.scheduleKind === \"at\") {\n const ms = Date.parse(form.scheduleAt);\n if (!Number.isFinite(ms)) throw new Error(\"Invalid run time.\");\n return { kind: \"at\" as const, atMs: ms };\n }\n if (form.scheduleKind === \"every\") {\n const amount = toNumber(form.everyAmount, 0);\n if (amount <= 0) throw new Error(\"Invalid interval amount.\");\n const unit = form.everyUnit;\n const mult = unit === \"minutes\" ? 60_000 : unit === \"hours\" ? 3_600_000 : 86_400_000;\n return { kind: \"every\" as const, everyMs: amount * mult };\n }\n const expr = form.cronExpr.trim();\n if (!expr) throw new Error(\"Cron expression required.\");\n return { kind: \"cron\" as const, expr, tz: form.cronTz.trim() || undefined };\n}\n\nexport function buildCronPayload(form: CronFormState) {\n if (form.payloadKind === \"systemEvent\") {\n const text = form.payloadText.trim();\n if (!text) throw new Error(\"System event text required.\");\n return { kind: \"systemEvent\" as const, text };\n }\n const message = form.payloadText.trim();\n if (!message) throw new Error(\"Agent message required.\");\n const payload: {\n kind: \"agentTurn\";\n message: string;\n deliver?: boolean;\n channel?: string;\n to?: string;\n timeoutSeconds?: number;\n } = { kind: \"agentTurn\", message };\n if (form.deliver) payload.deliver = true;\n if (form.channel) payload.channel = form.channel;\n if (form.to.trim()) payload.to = form.to.trim();\n const timeoutSeconds = toNumber(form.timeoutSeconds, 0);\n if (timeoutSeconds > 0) payload.timeoutSeconds = timeoutSeconds;\n return payload;\n}\n\nexport async function addCronJob(state: CronState) {\n if (!state.client || !state.connected || state.cronBusy) return;\n state.cronBusy = true;\n state.cronError = null;\n try {\n const schedule = buildCronSchedule(state.cronForm);\n const payload = buildCronPayload(state.cronForm);\n const agentId = state.cronForm.agentId.trim();\n const job = {\n name: state.cronForm.name.trim(),\n description: state.cronForm.description.trim() || undefined,\n agentId: agentId || undefined,\n enabled: state.cronForm.enabled,\n schedule,\n sessionTarget: state.cronForm.sessionTarget,\n wakeMode: state.cronForm.wakeMode,\n payload,\n isolation:\n state.cronForm.postToMainPrefix.trim() &&\n state.cronForm.sessionTarget === \"isolated\"\n ? { postToMainPrefix: state.cronForm.postToMainPrefix.trim() }\n : undefined,\n };\n if (!job.name) throw new Error(\"Name required.\");\n await state.client.request(\"cron.add\", job);\n state.cronForm = {\n ...state.cronForm,\n name: \"\",\n description: \"\",\n payloadText: \"\",\n };\n await loadCronJobs(state);\n await loadCronStatus(state);\n } catch (err) {\n state.cronError = String(err);\n } finally {\n state.cronBusy = false;\n }\n}\n\nexport async function toggleCronJob(\n state: CronState,\n job: CronJob,\n enabled: boolean,\n) {\n if (!state.client || !state.connected || state.cronBusy) return;\n state.cronBusy = true;\n state.cronError = null;\n try {\n await state.client.request(\"cron.update\", { id: job.id, patch: { enabled } });\n await loadCronJobs(state);\n await loadCronStatus(state);\n } catch (err) {\n state.cronError = String(err);\n } finally {\n state.cronBusy = false;\n }\n}\n\nexport async function runCronJob(state: CronState, job: CronJob) {\n if (!state.client || !state.connected || state.cronBusy) return;\n state.cronBusy = true;\n state.cronError = null;\n try {\n await state.client.request(\"cron.run\", { id: job.id, mode: \"force\" });\n await loadCronRuns(state, job.id);\n } catch (err) {\n state.cronError = String(err);\n } finally {\n state.cronBusy = false;\n }\n}\n\nexport async function removeCronJob(state: CronState, job: CronJob) {\n if (!state.client || !state.connected || state.cronBusy) return;\n state.cronBusy = true;\n state.cronError = null;\n try {\n await state.client.request(\"cron.remove\", { id: job.id });\n if (state.cronRunsJobId === job.id) {\n state.cronRunsJobId = null;\n state.cronRuns = [];\n }\n await loadCronJobs(state);\n await loadCronStatus(state);\n } catch (err) {\n state.cronError = String(err);\n } finally {\n state.cronBusy = false;\n }\n}\n\nexport async function loadCronRuns(state: CronState, jobId: string) {\n if (!state.client || !state.connected) return;\n try {\n const res = (await state.client.request(\"cron.runs\", {\n id: jobId,\n limit: 50,\n })) as { entries?: CronRunLogEntry[] };\n state.cronRunsJobId = jobId;\n state.cronRuns = Array.isArray(res.entries) ? res.entries : [];\n } catch (err) {\n state.cronError = String(err);\n }\n}\n","import type { ChannelsStatusSnapshot } from \"../types\";\nimport type { ChannelsState } from \"./channels.types\";\n\nexport type { ChannelsState };\n\nexport async function loadChannels(state: ChannelsState, probe: boolean) {\n if (!state.client || !state.connected) return;\n if (state.channelsLoading) return;\n state.channelsLoading = true;\n state.channelsError = null;\n try {\n const res = (await state.client.request(\"channels.status\", {\n probe,\n timeoutMs: 8000,\n })) as ChannelsStatusSnapshot;\n state.channelsSnapshot = res;\n state.channelsLastSuccess = Date.now();\n } catch (err) {\n state.channelsError = String(err);\n } finally {\n state.channelsLoading = false;\n }\n}\n\nexport async function startWhatsAppLogin(state: ChannelsState, force: boolean) {\n if (!state.client || !state.connected || state.whatsappBusy) return;\n state.whatsappBusy = true;\n try {\n const res = (await state.client.request(\"web.login.start\", {\n force,\n timeoutMs: 30000,\n })) as { message?: string; qrDataUrl?: string };\n state.whatsappLoginMessage = res.message ?? null;\n state.whatsappLoginQrDataUrl = res.qrDataUrl ?? null;\n state.whatsappLoginConnected = null;\n } catch (err) {\n state.whatsappLoginMessage = String(err);\n state.whatsappLoginQrDataUrl = null;\n state.whatsappLoginConnected = null;\n } finally {\n state.whatsappBusy = false;\n }\n}\n\nexport async function waitWhatsAppLogin(state: ChannelsState) {\n if (!state.client || !state.connected || state.whatsappBusy) return;\n state.whatsappBusy = true;\n try {\n const res = (await state.client.request(\"web.login.wait\", {\n timeoutMs: 120000,\n })) as { connected?: boolean; message?: string };\n state.whatsappLoginMessage = res.message ?? null;\n state.whatsappLoginConnected = res.connected ?? null;\n if (res.connected) state.whatsappLoginQrDataUrl = null;\n } catch (err) {\n state.whatsappLoginMessage = String(err);\n state.whatsappLoginConnected = null;\n } finally {\n state.whatsappBusy = false;\n }\n}\n\nexport async function logoutWhatsApp(state: ChannelsState) {\n if (!state.client || !state.connected || state.whatsappBusy) return;\n state.whatsappBusy = true;\n try {\n await state.client.request(\"channels.logout\", { channel: \"whatsapp\" });\n state.whatsappLoginMessage = \"Logged out.\";\n state.whatsappLoginQrDataUrl = null;\n state.whatsappLoginConnected = null;\n } catch (err) {\n state.whatsappLoginMessage = String(err);\n } finally {\n state.whatsappBusy = false;\n }\n}\n","import type { GatewayBrowserClient } from \"../gateway\";\nimport type { HealthSnapshot, StatusSummary } from \"../types\";\n\nexport type DebugState = {\n client: GatewayBrowserClient | null;\n connected: boolean;\n debugLoading: boolean;\n debugStatus: StatusSummary | null;\n debugHealth: HealthSnapshot | null;\n debugModels: unknown[];\n debugHeartbeat: unknown | null;\n debugCallMethod: string;\n debugCallParams: string;\n debugCallResult: string | null;\n debugCallError: string | null;\n};\n\nexport async function loadDebug(state: DebugState) {\n if (!state.client || !state.connected) return;\n if (state.debugLoading) return;\n state.debugLoading = true;\n try {\n const [status, health, models, heartbeat] = await Promise.all([\n state.client.request(\"status\", {}),\n state.client.request(\"health\", {}),\n state.client.request(\"models.list\", {}),\n state.client.request(\"last-heartbeat\", {}),\n ]);\n state.debugStatus = status as StatusSummary;\n state.debugHealth = health as HealthSnapshot;\n const modelPayload = models as { models?: unknown[] } | undefined;\n state.debugModels = Array.isArray(modelPayload?.models)\n ? modelPayload?.models\n : [];\n state.debugHeartbeat = heartbeat as unknown;\n } catch (err) {\n state.debugCallError = String(err);\n } finally {\n state.debugLoading = false;\n }\n}\n\nexport async function callDebugMethod(state: DebugState) {\n if (!state.client || !state.connected) return;\n state.debugCallError = null;\n state.debugCallResult = null;\n try {\n const params = state.debugCallParams.trim()\n ? (JSON.parse(state.debugCallParams) as unknown)\n : {};\n const res = await state.client.request(state.debugCallMethod.trim(), params);\n state.debugCallResult = JSON.stringify(res, null, 2);\n } catch (err) {\n state.debugCallError = String(err);\n }\n}\n\n","import type { GatewayBrowserClient } from \"../gateway\";\nimport type { LogEntry, LogLevel } from \"../types\";\n\nexport type LogsState = {\n client: GatewayBrowserClient | null;\n connected: boolean;\n logsLoading: boolean;\n logsError: string | null;\n logsCursor: number | null;\n logsFile: string | null;\n logsEntries: LogEntry[];\n logsTruncated: boolean;\n logsLastFetchAt: number | null;\n logsLimit: number;\n logsMaxBytes: number;\n};\n\nconst LOG_BUFFER_LIMIT = 2000;\nconst LEVELS = new Set([\n \"trace\",\n \"debug\",\n \"info\",\n \"warn\",\n \"error\",\n \"fatal\",\n]);\n\nfunction parseMaybeJsonString(value: unknown) {\n if (typeof value !== \"string\") return null;\n const trimmed = value.trim();\n if (!trimmed.startsWith(\"{\") || !trimmed.endsWith(\"}\")) return null;\n try {\n const parsed = JSON.parse(trimmed) as unknown;\n if (!parsed || typeof parsed !== \"object\") return null;\n return parsed as Record;\n } catch {\n return null;\n }\n}\n\nfunction normalizeLevel(value: unknown): LogLevel | null {\n if (typeof value !== \"string\") return null;\n const lowered = value.toLowerCase() as LogLevel;\n return LEVELS.has(lowered) ? lowered : null;\n}\n\nexport function parseLogLine(line: string): LogEntry {\n if (!line.trim()) return { raw: line, message: line };\n try {\n const obj = JSON.parse(line) as Record;\n const meta =\n obj && typeof obj._meta === \"object\" && obj._meta !== null\n ? (obj._meta as Record)\n : null;\n const time =\n typeof obj.time === \"string\"\n ? obj.time\n : typeof meta?.date === \"string\"\n ? meta?.date\n : null;\n const level = normalizeLevel(meta?.logLevelName ?? meta?.level);\n\n const contextCandidate =\n typeof obj[\"0\"] === \"string\"\n ? (obj[\"0\"] as string)\n : typeof meta?.name === \"string\"\n ? (meta?.name as string)\n : null;\n const contextObj = parseMaybeJsonString(contextCandidate);\n let subsystem: string | null = null;\n if (contextObj) {\n if (typeof contextObj.subsystem === \"string\") subsystem = contextObj.subsystem;\n else if (typeof contextObj.module === \"string\") subsystem = contextObj.module;\n }\n if (!subsystem && contextCandidate && contextCandidate.length < 120) {\n subsystem = contextCandidate;\n }\n\n let message: string | null = null;\n if (typeof obj[\"1\"] === \"string\") message = obj[\"1\"] as string;\n else if (!contextObj && typeof obj[\"0\"] === \"string\") message = obj[\"0\"] as string;\n else if (typeof obj.message === \"string\") message = obj.message as string;\n\n return {\n raw: line,\n time,\n level,\n subsystem,\n message: message ?? line,\n meta: meta ?? undefined,\n };\n } catch {\n return { raw: line, message: line };\n }\n}\n\nexport async function loadLogs(\n state: LogsState,\n opts?: { reset?: boolean; quiet?: boolean },\n) {\n if (!state.client || !state.connected) return;\n if (state.logsLoading && !opts?.quiet) return;\n if (!opts?.quiet) state.logsLoading = true;\n state.logsError = null;\n try {\n const res = await state.client.request(\"logs.tail\", {\n cursor: opts?.reset ? undefined : state.logsCursor ?? undefined,\n limit: state.logsLimit,\n maxBytes: state.logsMaxBytes,\n });\n const payload = res as {\n file?: string;\n cursor?: number;\n size?: number;\n lines?: unknown;\n truncated?: boolean;\n reset?: boolean;\n };\n const lines = Array.isArray(payload.lines)\n ? (payload.lines.filter((line) => typeof line === \"string\") as string[])\n : [];\n const entries = lines.map(parseLogLine);\n const shouldReset = Boolean(opts?.reset || payload.reset || state.logsCursor == null);\n state.logsEntries = shouldReset\n ? entries\n : [...state.logsEntries, ...entries].slice(-LOG_BUFFER_LIMIT);\n if (typeof payload.cursor === \"number\") state.logsCursor = payload.cursor;\n if (typeof payload.file === \"string\") state.logsFile = payload.file;\n state.logsTruncated = Boolean(payload.truncated);\n state.logsLastFetchAt = Date.now();\n } catch (err) {\n state.logsError = String(err);\n } finally {\n if (!opts?.quiet) state.logsLoading = false;\n }\n}\n","/*! noble-ed25519 - MIT License (c) 2019 Paul Miller (paulmillr.com) */\n/**\n * 5KB JS implementation of ed25519 EdDSA signatures.\n * Compliant with RFC8032, FIPS 186-5 & ZIP215.\n * @module\n * @example\n * ```js\nimport * as ed from '@noble/ed25519';\n(async () => {\n const secretKey = ed.utils.randomSecretKey();\n const message = Uint8Array.from([0xab, 0xbc, 0xcd, 0xde]);\n const pubKey = await ed.getPublicKeyAsync(secretKey); // Sync methods are also present\n const signature = await ed.signAsync(message, secretKey);\n const isValid = await ed.verifyAsync(signature, message, pubKey);\n})();\n```\n */\n/**\n * Curve params. ed25519 is twisted edwards curve. Equation is −x² + y² = -a + dx²y².\n * * P = `2n**255n - 19n` // field over which calculations are done\n * * N = `2n**252n + 27742317777372353535851937790883648493n` // group order, amount of curve points\n * * h = 8 // cofactor\n * * a = `Fp.create(BigInt(-1))` // equation param\n * * d = -121665/121666 a.k.a. `Fp.neg(121665 * Fp.inv(121666))` // equation param\n * * Gx, Gy are coordinates of Generator / base point\n */\nconst ed25519_CURVE = {\n p: 0x7fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffedn,\n n: 0x1000000000000000000000000000000014def9dea2f79cd65812631a5cf5d3edn,\n h: 8n,\n a: 0x7fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffecn,\n d: 0x52036cee2b6ffe738cc740797779e89800700a4d4141d8ab75eb4dca135978a3n,\n Gx: 0x216936d3cd6e53fec0a4e231fdd6dc5c692cc7609525a7b2c9562d608f25d51an,\n Gy: 0x6666666666666666666666666666666666666666666666666666666666666658n,\n};\nconst { p: P, n: N, Gx, Gy, a: _a, d: _d, h } = ed25519_CURVE;\nconst L = 32; // field / group byte length\nconst L2 = 64;\n// Helpers and Precomputes sections are reused between libraries\n// ## Helpers\n// ----------\nconst captureTrace = (...args) => {\n if ('captureStackTrace' in Error && typeof Error.captureStackTrace === 'function') {\n Error.captureStackTrace(...args);\n }\n};\nconst err = (message = '') => {\n const e = new Error(message);\n captureTrace(e, err);\n throw e;\n};\nconst isBig = (n) => typeof n === 'bigint'; // is big integer\nconst isStr = (s) => typeof s === 'string'; // is string\nconst isBytes = (a) => a instanceof Uint8Array || (ArrayBuffer.isView(a) && a.constructor.name === 'Uint8Array');\n/** Asserts something is Uint8Array. */\nconst abytes = (value, length, title = '') => {\n const bytes = isBytes(value);\n const len = value?.length;\n const needsLen = length !== undefined;\n if (!bytes || (needsLen && len !== length)) {\n const prefix = title && `\"${title}\" `;\n const ofLen = needsLen ? ` of length ${length}` : '';\n const got = bytes ? `length=${len}` : `type=${typeof value}`;\n err(prefix + 'expected Uint8Array' + ofLen + ', got ' + got);\n }\n return value;\n};\n/** create Uint8Array */\nconst u8n = (len) => new Uint8Array(len);\nconst u8fr = (buf) => Uint8Array.from(buf);\nconst padh = (n, pad) => n.toString(16).padStart(pad, '0');\nconst bytesToHex = (b) => Array.from(abytes(b))\n .map((e) => padh(e, 2))\n .join('');\nconst C = { _0: 48, _9: 57, A: 65, F: 70, a: 97, f: 102 }; // ASCII characters\nconst _ch = (ch) => {\n if (ch >= C._0 && ch <= C._9)\n return ch - C._0; // '2' => 50-48\n if (ch >= C.A && ch <= C.F)\n return ch - (C.A - 10); // 'B' => 66-(65-10)\n if (ch >= C.a && ch <= C.f)\n return ch - (C.a - 10); // 'b' => 98-(97-10)\n return;\n};\nconst hexToBytes = (hex) => {\n const e = 'hex invalid';\n if (!isStr(hex))\n return err(e);\n const hl = hex.length;\n const al = hl / 2;\n if (hl % 2)\n return err(e);\n const array = u8n(al);\n for (let ai = 0, hi = 0; ai < al; ai++, hi += 2) {\n // treat each char as ASCII\n const n1 = _ch(hex.charCodeAt(hi)); // parse first char, multiply it by 16\n const n2 = _ch(hex.charCodeAt(hi + 1)); // parse second char\n if (n1 === undefined || n2 === undefined)\n return err(e);\n array[ai] = n1 * 16 + n2; // example: 'A9' => 10*16 + 9\n }\n return array;\n};\nconst cr = () => globalThis?.crypto; // WebCrypto is available in all modern environments\nconst subtle = () => cr()?.subtle ?? err('crypto.subtle must be defined, consider polyfill');\n// prettier-ignore\nconst concatBytes = (...arrs) => {\n const r = u8n(arrs.reduce((sum, a) => sum + abytes(a).length, 0)); // create u8a of summed length\n let pad = 0; // walk through each array,\n arrs.forEach(a => { r.set(a, pad); pad += a.length; }); // ensure they have proper type\n return r;\n};\n/** WebCrypto OS-level CSPRNG (random number generator). Will throw when not available. */\nconst randomBytes = (len = L) => {\n const c = cr();\n return c.getRandomValues(u8n(len));\n};\nconst big = BigInt;\nconst assertRange = (n, min, max, msg = 'bad number: out of range') => (isBig(n) && min <= n && n < max ? n : err(msg));\n/** modular division */\nconst M = (a, b = P) => {\n const r = a % b;\n return r >= 0n ? r : b + r;\n};\nconst modN = (a) => M(a, N);\n/** Modular inversion using euclidean GCD (non-CT). No negative exponent for now. */\n// prettier-ignore\nconst invert = (num, md) => {\n if (num === 0n || md <= 0n)\n err('no inverse n=' + num + ' mod=' + md);\n let a = M(num, md), b = md, x = 0n, y = 1n, u = 1n, v = 0n;\n while (a !== 0n) {\n const q = b / a, r = b % a;\n const m = x - u * q, n = y - v * q;\n b = a, a = r, x = u, y = v, u = m, v = n;\n }\n return b === 1n ? M(x, md) : err('no inverse'); // b is gcd at this point\n};\nconst callHash = (name) => {\n // @ts-ignore\n const fn = hashes[name];\n if (typeof fn !== 'function')\n err('hashes.' + name + ' not set');\n return fn;\n};\nconst hash = (msg) => callHash('sha512')(msg);\nconst apoint = (p) => (p instanceof Point ? p : err('Point expected'));\n// ## End of Helpers\n// -----------------\nconst B256 = 2n ** 256n;\n/** Point in XYZT extended coordinates. */\nclass Point {\n static BASE;\n static ZERO;\n X;\n Y;\n Z;\n T;\n constructor(X, Y, Z, T) {\n const max = B256;\n this.X = assertRange(X, 0n, max);\n this.Y = assertRange(Y, 0n, max);\n this.Z = assertRange(Z, 1n, max);\n this.T = assertRange(T, 0n, max);\n Object.freeze(this);\n }\n static CURVE() {\n return ed25519_CURVE;\n }\n static fromAffine(p) {\n return new Point(p.x, p.y, 1n, M(p.x * p.y));\n }\n /** RFC8032 5.1.3: Uint8Array to Point. */\n static fromBytes(hex, zip215 = false) {\n const d = _d;\n // Copy array to not mess it up.\n const normed = u8fr(abytes(hex, L));\n // adjust first LE byte = last BE byte\n const lastByte = hex[31];\n normed[31] = lastByte & ~0x80;\n const y = bytesToNumLE(normed);\n // zip215=true: 0 <= y < 2^256\n // zip215=false, RFC8032: 0 <= y < 2^255-19\n const max = zip215 ? B256 : P;\n assertRange(y, 0n, max);\n const y2 = M(y * y); // y²\n const u = M(y2 - 1n); // u=y²-1\n const v = M(d * y2 + 1n); // v=dy²+1\n let { isValid, value: x } = uvRatio(u, v); // (uv³)(uv⁷)^(p-5)/8; square root\n if (!isValid)\n err('bad point: y not sqrt'); // not square root: bad point\n const isXOdd = (x & 1n) === 1n; // adjust sign of x coordinate\n const isLastByteOdd = (lastByte & 0x80) !== 0; // x_0, last bit\n if (!zip215 && x === 0n && isLastByteOdd)\n err('bad point: x==0, isLastByteOdd'); // x=0, x_0=1\n if (isLastByteOdd !== isXOdd)\n x = M(-x);\n return new Point(x, y, 1n, M(x * y)); // Z=1, T=xy\n }\n static fromHex(hex, zip215) {\n return Point.fromBytes(hexToBytes(hex), zip215);\n }\n get x() {\n return this.toAffine().x;\n }\n get y() {\n return this.toAffine().y;\n }\n /** Checks if the point is valid and on-curve. */\n assertValidity() {\n const a = _a;\n const d = _d;\n const p = this;\n if (p.is0())\n return err('bad point: ZERO'); // TODO: optimize, with vars below?\n // Equation in affine coordinates: ax² + y² = 1 + dx²y²\n // Equation in projective coordinates (X/Z, Y/Z, Z): (aX² + Y²)Z² = Z⁴ + dX²Y²\n const { X, Y, Z, T } = p;\n const X2 = M(X * X); // X²\n const Y2 = M(Y * Y); // Y²\n const Z2 = M(Z * Z); // Z²\n const Z4 = M(Z2 * Z2); // Z⁴\n const aX2 = M(X2 * a); // aX²\n const left = M(Z2 * M(aX2 + Y2)); // (aX² + Y²)Z²\n const right = M(Z4 + M(d * M(X2 * Y2))); // Z⁴ + dX²Y²\n if (left !== right)\n return err('bad point: equation left != right (1)');\n // In Extended coordinates we also have T, which is x*y=T/Z: check X*Y == Z*T\n const XY = M(X * Y);\n const ZT = M(Z * T);\n if (XY !== ZT)\n return err('bad point: equation left != right (2)');\n return this;\n }\n /** Equality check: compare points P&Q. */\n equals(other) {\n const { X: X1, Y: Y1, Z: Z1 } = this;\n const { X: X2, Y: Y2, Z: Z2 } = apoint(other); // checks class equality\n const X1Z2 = M(X1 * Z2);\n const X2Z1 = M(X2 * Z1);\n const Y1Z2 = M(Y1 * Z2);\n const Y2Z1 = M(Y2 * Z1);\n return X1Z2 === X2Z1 && Y1Z2 === Y2Z1;\n }\n is0() {\n return this.equals(I);\n }\n /** Flip point over y coordinate. */\n negate() {\n return new Point(M(-this.X), this.Y, this.Z, M(-this.T));\n }\n /** Point doubling. Complete formula. Cost: `4M + 4S + 1*a + 6add + 1*2`. */\n double() {\n const { X: X1, Y: Y1, Z: Z1 } = this;\n const a = _a;\n // https://hyperelliptic.org/EFD/g1p/auto-twisted-extended.html#doubling-dbl-2008-hwcd\n const A = M(X1 * X1);\n const B = M(Y1 * Y1);\n const C = M(2n * M(Z1 * Z1));\n const D = M(a * A);\n const x1y1 = X1 + Y1;\n const E = M(M(x1y1 * x1y1) - A - B);\n const G = D + B;\n const F = G - C;\n const H = D - B;\n const X3 = M(E * F);\n const Y3 = M(G * H);\n const T3 = M(E * H);\n const Z3 = M(F * G);\n return new Point(X3, Y3, Z3, T3);\n }\n /** Point addition. Complete formula. Cost: `8M + 1*k + 8add + 1*2`. */\n add(other) {\n const { X: X1, Y: Y1, Z: Z1, T: T1 } = this;\n const { X: X2, Y: Y2, Z: Z2, T: T2 } = apoint(other); // doesn't check if other on-curve\n const a = _a;\n const d = _d;\n // https://hyperelliptic.org/EFD/g1p/auto-twisted-extended-1.html#addition-add-2008-hwcd-3\n const A = M(X1 * X2);\n const B = M(Y1 * Y2);\n const C = M(T1 * d * T2);\n const D = M(Z1 * Z2);\n const E = M((X1 + Y1) * (X2 + Y2) - A - B);\n const F = M(D - C);\n const G = M(D + C);\n const H = M(B - a * A);\n const X3 = M(E * F);\n const Y3 = M(G * H);\n const T3 = M(E * H);\n const Z3 = M(F * G);\n return new Point(X3, Y3, Z3, T3);\n }\n subtract(other) {\n return this.add(apoint(other).negate());\n }\n /**\n * Point-by-scalar multiplication. Scalar must be in range 1 <= n < CURVE.n.\n * Uses {@link wNAF} for base point.\n * Uses fake point to mitigate side-channel leakage.\n * @param n scalar by which point is multiplied\n * @param safe safe mode guards against timing attacks; unsafe mode is faster\n */\n multiply(n, safe = true) {\n if (!safe && (n === 0n || this.is0()))\n return I;\n assertRange(n, 1n, N);\n if (n === 1n)\n return this;\n if (this.equals(G))\n return wNAF(n).p;\n // init result point & fake point\n let p = I;\n let f = G;\n for (let d = this; n > 0n; d = d.double(), n >>= 1n) {\n // if bit is present, add to point\n // if not present, add to fake, for timing safety\n if (n & 1n)\n p = p.add(d);\n else if (safe)\n f = f.add(d);\n }\n return p;\n }\n multiplyUnsafe(scalar) {\n return this.multiply(scalar, false);\n }\n /** Convert point to 2d xy affine point. (X, Y, Z) ∋ (x=X/Z, y=Y/Z) */\n toAffine() {\n const { X, Y, Z } = this;\n // fast-paths for ZERO point OR Z=1\n if (this.equals(I))\n return { x: 0n, y: 1n };\n const iz = invert(Z, P);\n // (Z * Z^-1) must be 1, otherwise bad math\n if (M(Z * iz) !== 1n)\n err('invalid inverse');\n // x = X*Z^-1; y = Y*Z^-1\n const x = M(X * iz);\n const y = M(Y * iz);\n return { x, y };\n }\n toBytes() {\n const { x, y } = this.assertValidity().toAffine();\n const b = numTo32bLE(y);\n // store sign in first LE byte\n b[31] |= x & 1n ? 0x80 : 0;\n return b;\n }\n toHex() {\n return bytesToHex(this.toBytes());\n }\n clearCofactor() {\n return this.multiply(big(h), false);\n }\n isSmallOrder() {\n return this.clearCofactor().is0();\n }\n isTorsionFree() {\n // Multiply by big number N. We can't `mul(N)` because of checks. Instead, we `mul(N/2)*2+1`\n let p = this.multiply(N / 2n, false).double();\n if (N % 2n)\n p = p.add(this);\n return p.is0();\n }\n}\n/** Generator / base point */\nconst G = new Point(Gx, Gy, 1n, M(Gx * Gy));\n/** Identity / zero point */\nconst I = new Point(0n, 1n, 1n, 0n);\n// Static aliases\nPoint.BASE = G;\nPoint.ZERO = I;\nconst numTo32bLE = (num) => hexToBytes(padh(assertRange(num, 0n, B256), L2)).reverse();\nconst bytesToNumLE = (b) => big('0x' + bytesToHex(u8fr(abytes(b)).reverse()));\nconst pow2 = (x, power) => {\n // pow2(x, 4) == x^(2^4)\n let r = x;\n while (power-- > 0n) {\n r *= r;\n r %= P;\n }\n return r;\n};\n// prettier-ignore\nconst pow_2_252_3 = (x) => {\n const x2 = (x * x) % P; // x^2, bits 1\n const b2 = (x2 * x) % P; // x^3, bits 11\n const b4 = (pow2(b2, 2n) * b2) % P; // x^(2^4-1), bits 1111\n const b5 = (pow2(b4, 1n) * x) % P; // x^(2^5-1), bits 11111\n const b10 = (pow2(b5, 5n) * b5) % P; // x^(2^10)\n const b20 = (pow2(b10, 10n) * b10) % P; // x^(2^20)\n const b40 = (pow2(b20, 20n) * b20) % P; // x^(2^40)\n const b80 = (pow2(b40, 40n) * b40) % P; // x^(2^80)\n const b160 = (pow2(b80, 80n) * b80) % P; // x^(2^160)\n const b240 = (pow2(b160, 80n) * b80) % P; // x^(2^240)\n const b250 = (pow2(b240, 10n) * b10) % P; // x^(2^250)\n const pow_p_5_8 = (pow2(b250, 2n) * x) % P; // < To pow to (p+3)/8, multiply it by x.\n return { pow_p_5_8, b2 };\n};\nconst RM1 = 0x2b8324804fc1df0b2b4d00993dfbd7a72f431806ad2fe478c4ee1b274a0ea0b0n; // √-1\n// for sqrt comp\n// prettier-ignore\nconst uvRatio = (u, v) => {\n const v3 = M(v * v * v); // v³\n const v7 = M(v3 * v3 * v); // v⁷\n const pow = pow_2_252_3(u * v7).pow_p_5_8; // (uv⁷)^(p-5)/8\n let x = M(u * v3 * pow); // (uv³)(uv⁷)^(p-5)/8\n const vx2 = M(v * x * x); // vx²\n const root1 = x; // First root candidate\n const root2 = M(x * RM1); // Second root candidate; RM1 is √-1\n const useRoot1 = vx2 === u; // If vx² = u (mod p), x is a square root\n const useRoot2 = vx2 === M(-u); // If vx² = -u, set x <-- x * 2^((p-1)/4)\n const noRoot = vx2 === M(-u * RM1); // There is no valid root, vx² = -u√-1\n if (useRoot1)\n x = root1;\n if (useRoot2 || noRoot)\n x = root2; // We return root2 anyway, for const-time\n if ((M(x) & 1n) === 1n)\n x = M(-x); // edIsNegative\n return { isValid: useRoot1 || useRoot2, value: x };\n};\n// N == L, just weird naming\nconst modL_LE = (hash) => modN(bytesToNumLE(hash)); // modulo L; but little-endian\n/** hashes.sha512 should conform to the interface. */\n// TODO: rename\nconst sha512a = (...m) => hashes.sha512Async(concatBytes(...m)); // Async SHA512\nconst sha512s = (...m) => callHash('sha512')(concatBytes(...m));\n// RFC8032 5.1.5\nconst hash2extK = (hashed) => {\n // slice creates a copy, unlike subarray\n const head = hashed.slice(0, L);\n head[0] &= 248; // Clamp bits: 0b1111_1000\n head[31] &= 127; // 0b0111_1111\n head[31] |= 64; // 0b0100_0000\n const prefix = hashed.slice(L, L2); // secret key \"prefix\"\n const scalar = modL_LE(head); // modular division over curve order\n const point = G.multiply(scalar); // public key point\n const pointBytes = point.toBytes(); // point serialized to Uint8Array\n return { head, prefix, scalar, point, pointBytes };\n};\n// RFC8032 5.1.5; getPublicKey async, sync. Hash priv key and extract point.\nconst getExtendedPublicKeyAsync = (secretKey) => sha512a(abytes(secretKey, L)).then(hash2extK);\nconst getExtendedPublicKey = (secretKey) => hash2extK(sha512s(abytes(secretKey, L)));\n/** Creates 32-byte ed25519 public key from 32-byte secret key. Async. */\nconst getPublicKeyAsync = (secretKey) => getExtendedPublicKeyAsync(secretKey).then((p) => p.pointBytes);\n/** Creates 32-byte ed25519 public key from 32-byte secret key. To use, set `hashes.sha512` first. */\nconst getPublicKey = (priv) => getExtendedPublicKey(priv).pointBytes;\nconst hashFinishA = (res) => sha512a(res.hashable).then(res.finish);\nconst hashFinishS = (res) => res.finish(sha512s(res.hashable));\n// Code, shared between sync & async sign\nconst _sign = (e, rBytes, msg) => {\n const { pointBytes: P, scalar: s } = e;\n const r = modL_LE(rBytes); // r was created outside, reduce it modulo L\n const R = G.multiply(r).toBytes(); // R = [r]B\n const hashable = concatBytes(R, P, msg); // dom2(F, C) || R || A || PH(M)\n const finish = (hashed) => {\n // k = SHA512(dom2(F, C) || R || A || PH(M))\n const S = modN(r + modL_LE(hashed) * s); // S = (r + k * s) mod L; 0 <= s < l\n return abytes(concatBytes(R, numTo32bLE(S)), L2); // 64-byte sig: 32b R.x + 32b LE(S)\n };\n return { hashable, finish };\n};\n/**\n * Signs message using secret key. Async.\n * Follows RFC8032 5.1.6.\n */\nconst signAsync = async (message, secretKey) => {\n const m = abytes(message);\n const e = await getExtendedPublicKeyAsync(secretKey);\n const rBytes = await sha512a(e.prefix, m); // r = SHA512(dom2(F, C) || prefix || PH(M))\n return hashFinishA(_sign(e, rBytes, m)); // gen R, k, S, then 64-byte signature\n};\n/**\n * Signs message using secret key. To use, set `hashes.sha512` first.\n * Follows RFC8032 5.1.6.\n */\nconst sign = (message, secretKey) => {\n const m = abytes(message);\n const e = getExtendedPublicKey(secretKey);\n const rBytes = sha512s(e.prefix, m); // r = SHA512(dom2(F, C) || prefix || PH(M))\n return hashFinishS(_sign(e, rBytes, m)); // gen R, k, S, then 64-byte signature\n};\nconst defaultVerifyOpts = { zip215: true };\nconst _verify = (sig, msg, pub, opts = defaultVerifyOpts) => {\n sig = abytes(sig, L2); // Signature hex str/Bytes, must be 64 bytes\n msg = abytes(msg); // Message hex str/Bytes\n pub = abytes(pub, L);\n const { zip215 } = opts; // switch between zip215 and rfc8032 verif\n let A;\n let R;\n let s;\n let SB;\n let hashable = Uint8Array.of();\n try {\n A = Point.fromBytes(pub, zip215); // public key A decoded\n R = Point.fromBytes(sig.slice(0, L), zip215); // 0 <= R < 2^256: ZIP215 R can be >= P\n s = bytesToNumLE(sig.slice(L, L2)); // Decode second half as an integer S\n SB = G.multiply(s, false); // in the range 0 <= s < L\n hashable = concatBytes(R.toBytes(), A.toBytes(), msg); // dom2(F, C) || R || A || PH(M)\n }\n catch (error) { }\n const finish = (hashed) => {\n // k = SHA512(dom2(F, C) || R || A || PH(M))\n if (SB == null)\n return false; // false if try-catch catched an error\n if (!zip215 && A.isSmallOrder())\n return false; // false for SBS: Strongly Binding Signature\n const k = modL_LE(hashed); // decode in little-endian, modulo L\n const RkA = R.add(A.multiply(k, false)); // [8]R + [8][k]A'\n return RkA.add(SB.negate()).clearCofactor().is0(); // [8][S]B = [8]R + [8][k]A'\n };\n return { hashable, finish };\n};\n/** Verifies signature on message and public key. Async. Follows RFC8032 5.1.7. */\nconst verifyAsync = async (signature, message, publicKey, opts = defaultVerifyOpts) => hashFinishA(_verify(signature, message, publicKey, opts));\n/** Verifies signature on message and public key. To use, set `hashes.sha512` first. Follows RFC8032 5.1.7. */\nconst verify = (signature, message, publicKey, opts = defaultVerifyOpts) => hashFinishS(_verify(signature, message, publicKey, opts));\n/** Math, hex, byte helpers. Not in `utils` because utils share API with noble-curves. */\nconst etc = {\n bytesToHex: bytesToHex,\n hexToBytes: hexToBytes,\n concatBytes: concatBytes,\n mod: M,\n invert: invert,\n randomBytes: randomBytes,\n};\nconst hashes = {\n sha512Async: async (message) => {\n const s = subtle();\n const m = concatBytes(message);\n return u8n(await s.digest('SHA-512', m.buffer));\n },\n sha512: undefined,\n};\n// FIPS 186 B.4.1 compliant key generation produces private keys\n// with modulo bias being neglible. takes >N+16 bytes, returns (hash mod n-1)+1\nconst randomSecretKey = (seed = randomBytes(L)) => seed;\nconst keygen = (seed) => {\n const secretKey = randomSecretKey(seed);\n const publicKey = getPublicKey(secretKey);\n return { secretKey, publicKey };\n};\nconst keygenAsync = async (seed) => {\n const secretKey = randomSecretKey(seed);\n const publicKey = await getPublicKeyAsync(secretKey);\n return { secretKey, publicKey };\n};\n/** ed25519-specific key utilities. */\nconst utils = {\n getExtendedPublicKeyAsync: getExtendedPublicKeyAsync,\n getExtendedPublicKey: getExtendedPublicKey,\n randomSecretKey: randomSecretKey,\n};\n// ## Precomputes\n// --------------\nconst W = 8; // W is window size\nconst scalarBits = 256;\nconst pwindows = Math.ceil(scalarBits / W) + 1; // 33 for W=8, NOT 32 - see wNAF loop\nconst pwindowSize = 2 ** (W - 1); // 128 for W=8\nconst precompute = () => {\n const points = [];\n let p = G;\n let b = p;\n for (let w = 0; w < pwindows; w++) {\n b = p;\n points.push(b);\n for (let i = 1; i < pwindowSize; i++) {\n b = b.add(p);\n points.push(b);\n } // i=1, bc we skip 0\n p = b.double();\n }\n return points;\n};\nlet Gpows = undefined; // precomputes for base point G\n// const-time negate\nconst ctneg = (cnd, p) => {\n const n = p.negate();\n return cnd ? n : p;\n};\n/**\n * Precomputes give 12x faster getPublicKey(), 10x sign(), 2x verify() by\n * caching multiples of G (base point). Cache is stored in 32MB of RAM.\n * Any time `G.multiply` is done, precomputes are used.\n * Not used for getSharedSecret, which instead multiplies random pubkey `P.multiply`.\n *\n * w-ary non-adjacent form (wNAF) precomputation method is 10% slower than windowed method,\n * but takes 2x less RAM. RAM reduction is possible by utilizing `.subtract`.\n *\n * !! Precomputes can be disabled by commenting-out call of the wNAF() inside Point#multiply().\n */\nconst wNAF = (n) => {\n const comp = Gpows || (Gpows = precompute());\n let p = I;\n let f = G; // f must be G, or could become I in the end\n const pow_2_w = 2 ** W; // 256 for W=8\n const maxNum = pow_2_w; // 256 for W=8\n const mask = big(pow_2_w - 1); // 255 for W=8 == mask 0b11111111\n const shiftBy = big(W); // 8 for W=8\n for (let w = 0; w < pwindows; w++) {\n let wbits = Number(n & mask); // extract W bits.\n n >>= shiftBy; // shift number by W bits.\n // We use negative indexes to reduce size of precomputed table by 2x.\n // Instead of needing precomputes 0..256, we only calculate them for 0..128.\n // If an index > 128 is found, we do (256-index) - where 256 is next window.\n // Naive: index +127 => 127, +224 => 224\n // Optimized: index +127 => 127, +224 => 256-32\n if (wbits > pwindowSize) {\n wbits -= maxNum;\n n += 1n;\n }\n const off = w * pwindowSize;\n const offF = off; // offsets, evaluate both\n const offP = off + Math.abs(wbits) - 1;\n const isEven = w % 2 !== 0; // conditions, evaluate both\n const isNeg = wbits < 0;\n if (wbits === 0) {\n // off == I: can't add it. Adding random offF instead.\n f = f.add(ctneg(isEven, comp[offF])); // bits are 0: add garbage to fake point\n }\n else {\n p = p.add(ctneg(isNeg, comp[offP])); // bits are 1: add to result point\n }\n }\n if (n !== 0n)\n err('invalid wnaf');\n return { p, f }; // return both real and fake points for JIT\n};\n// !! Remove the export to easily use in REPL / browser console\nexport { etc, getPublicKey, getPublicKeyAsync, hash, hashes, keygen, keygenAsync, Point, sign, signAsync, utils, verify, verifyAsync, };\n","import { getPublicKeyAsync, signAsync, utils } from \"@noble/ed25519\";\n\ntype StoredIdentity = {\n version: 1;\n deviceId: string;\n publicKey: string;\n privateKey: string;\n createdAtMs: number;\n};\n\nexport type DeviceIdentity = {\n deviceId: string;\n publicKey: string;\n privateKey: string;\n};\n\nconst STORAGE_KEY = \"clawdbot-device-identity-v1\";\n\nfunction base64UrlEncode(bytes: Uint8Array): string {\n let binary = \"\";\n for (const byte of bytes) binary += String.fromCharCode(byte);\n return btoa(binary).replaceAll(\"+\", \"-\").replaceAll(\"/\", \"_\").replace(/=+$/g, \"\");\n}\n\nfunction base64UrlDecode(input: string): Uint8Array {\n const normalized = input.replaceAll(\"-\", \"+\").replaceAll(\"_\", \"/\");\n const padded = normalized + \"=\".repeat((4 - (normalized.length % 4)) % 4);\n const binary = atob(padded);\n const out = new Uint8Array(binary.length);\n for (let i = 0; i < binary.length; i += 1) out[i] = binary.charCodeAt(i);\n return out;\n}\n\nfunction bytesToHex(bytes: Uint8Array): string {\n return Array.from(bytes)\n .map((b) => b.toString(16).padStart(2, \"0\"))\n .join(\"\");\n}\n\nasync function fingerprintPublicKey(publicKey: Uint8Array): Promise {\n const hash = await crypto.subtle.digest(\"SHA-256\", publicKey);\n return bytesToHex(new Uint8Array(hash));\n}\n\nasync function generateIdentity(): Promise {\n const privateKey = utils.randomSecretKey();\n const publicKey = await getPublicKeyAsync(privateKey);\n const deviceId = await fingerprintPublicKey(publicKey);\n return {\n deviceId,\n publicKey: base64UrlEncode(publicKey),\n privateKey: base64UrlEncode(privateKey),\n };\n}\n\nexport async function loadOrCreateDeviceIdentity(): Promise {\n try {\n const raw = localStorage.getItem(STORAGE_KEY);\n if (raw) {\n const parsed = JSON.parse(raw) as StoredIdentity;\n if (\n parsed?.version === 1 &&\n typeof parsed.deviceId === \"string\" &&\n typeof parsed.publicKey === \"string\" &&\n typeof parsed.privateKey === \"string\"\n ) {\n const derivedId = await fingerprintPublicKey(base64UrlDecode(parsed.publicKey));\n if (derivedId !== parsed.deviceId) {\n const updated: StoredIdentity = {\n ...parsed,\n deviceId: derivedId,\n };\n localStorage.setItem(STORAGE_KEY, JSON.stringify(updated));\n return {\n deviceId: derivedId,\n publicKey: parsed.publicKey,\n privateKey: parsed.privateKey,\n };\n }\n return {\n deviceId: parsed.deviceId,\n publicKey: parsed.publicKey,\n privateKey: parsed.privateKey,\n };\n }\n }\n } catch {\n // fall through to regenerate\n }\n\n const identity = await generateIdentity();\n const stored: StoredIdentity = {\n version: 1,\n deviceId: identity.deviceId,\n publicKey: identity.publicKey,\n privateKey: identity.privateKey,\n createdAtMs: Date.now(),\n };\n localStorage.setItem(STORAGE_KEY, JSON.stringify(stored));\n return identity;\n}\n\nexport async function signDevicePayload(privateKeyBase64Url: string, payload: string) {\n const key = base64UrlDecode(privateKeyBase64Url);\n const data = new TextEncoder().encode(payload);\n const sig = await signAsync(data, key);\n return base64UrlEncode(sig);\n}\n","export type DeviceAuthEntry = {\n token: string;\n role: string;\n scopes: string[];\n updatedAtMs: number;\n};\n\ntype DeviceAuthStore = {\n version: 1;\n deviceId: string;\n tokens: Record;\n};\n\nconst STORAGE_KEY = \"clawdbot.device.auth.v1\";\n\nfunction normalizeRole(role: string): string {\n return role.trim();\n}\n\nfunction normalizeScopes(scopes: string[] | undefined): string[] {\n if (!Array.isArray(scopes)) return [];\n const out = new Set();\n for (const scope of scopes) {\n const trimmed = scope.trim();\n if (trimmed) out.add(trimmed);\n }\n return [...out].sort();\n}\n\nfunction readStore(): DeviceAuthStore | null {\n try {\n const raw = window.localStorage.getItem(STORAGE_KEY);\n if (!raw) return null;\n const parsed = JSON.parse(raw) as DeviceAuthStore;\n if (!parsed || parsed.version !== 1) return null;\n if (!parsed.deviceId || typeof parsed.deviceId !== \"string\") return null;\n if (!parsed.tokens || typeof parsed.tokens !== \"object\") return null;\n return parsed;\n } catch {\n return null;\n }\n}\n\nfunction writeStore(store: DeviceAuthStore) {\n try {\n window.localStorage.setItem(STORAGE_KEY, JSON.stringify(store));\n } catch {\n // best-effort\n }\n}\n\nexport function loadDeviceAuthToken(params: {\n deviceId: string;\n role: string;\n}): DeviceAuthEntry | null {\n const store = readStore();\n if (!store || store.deviceId !== params.deviceId) return null;\n const role = normalizeRole(params.role);\n const entry = store.tokens[role];\n if (!entry || typeof entry.token !== \"string\") return null;\n return entry;\n}\n\nexport function storeDeviceAuthToken(params: {\n deviceId: string;\n role: string;\n token: string;\n scopes?: string[];\n}): DeviceAuthEntry {\n const role = normalizeRole(params.role);\n const next: DeviceAuthStore = {\n version: 1,\n deviceId: params.deviceId,\n tokens: {},\n };\n const existing = readStore();\n if (existing && existing.deviceId === params.deviceId) {\n next.tokens = { ...existing.tokens };\n }\n const entry: DeviceAuthEntry = {\n token: params.token,\n role,\n scopes: normalizeScopes(params.scopes),\n updatedAtMs: Date.now(),\n };\n next.tokens[role] = entry;\n writeStore(next);\n return entry;\n}\n\nexport function clearDeviceAuthToken(params: { deviceId: string; role: string }) {\n const store = readStore();\n if (!store || store.deviceId !== params.deviceId) return;\n const role = normalizeRole(params.role);\n if (!store.tokens[role]) return;\n const next = { ...store, tokens: { ...store.tokens } };\n delete next.tokens[role];\n writeStore(next);\n}\n","import type { GatewayBrowserClient } from \"../gateway\";\nimport { loadOrCreateDeviceIdentity } from \"../device-identity\";\nimport { clearDeviceAuthToken, storeDeviceAuthToken } from \"../device-auth\";\n\nexport type DeviceTokenSummary = {\n role: string;\n scopes?: string[];\n createdAtMs?: number;\n rotatedAtMs?: number;\n revokedAtMs?: number;\n lastUsedAtMs?: number;\n};\n\nexport type PendingDevice = {\n requestId: string;\n deviceId: string;\n displayName?: string;\n role?: string;\n remoteIp?: string;\n isRepair?: boolean;\n ts?: number;\n};\n\nexport type PairedDevice = {\n deviceId: string;\n displayName?: string;\n roles?: string[];\n scopes?: string[];\n remoteIp?: string;\n tokens?: DeviceTokenSummary[];\n createdAtMs?: number;\n approvedAtMs?: number;\n};\n\nexport type DevicePairingList = {\n pending: PendingDevice[];\n paired: PairedDevice[];\n};\n\nexport type DevicesState = {\n client: GatewayBrowserClient | null;\n connected: boolean;\n devicesLoading: boolean;\n devicesError: string | null;\n devicesList: DevicePairingList | null;\n};\n\nexport async function loadDevices(state: DevicesState, opts?: { quiet?: boolean }) {\n if (!state.client || !state.connected) return;\n if (state.devicesLoading) return;\n state.devicesLoading = true;\n if (!opts?.quiet) state.devicesError = null;\n try {\n const res = (await state.client.request(\"device.pair.list\", {})) as DevicePairingList | null;\n state.devicesList = {\n pending: Array.isArray(res?.pending) ? res!.pending : [],\n paired: Array.isArray(res?.paired) ? res!.paired : [],\n };\n } catch (err) {\n if (!opts?.quiet) state.devicesError = String(err);\n } finally {\n state.devicesLoading = false;\n }\n}\n\nexport async function approveDevicePairing(state: DevicesState, requestId: string) {\n if (!state.client || !state.connected) return;\n try {\n await state.client.request(\"device.pair.approve\", { requestId });\n await loadDevices(state);\n } catch (err) {\n state.devicesError = String(err);\n }\n}\n\nexport async function rejectDevicePairing(state: DevicesState, requestId: string) {\n if (!state.client || !state.connected) return;\n const confirmed = window.confirm(\"Reject this device pairing request?\");\n if (!confirmed) return;\n try {\n await state.client.request(\"device.pair.reject\", { requestId });\n await loadDevices(state);\n } catch (err) {\n state.devicesError = String(err);\n }\n}\n\nexport async function rotateDeviceToken(\n state: DevicesState,\n params: { deviceId: string; role: string; scopes?: string[] },\n) {\n if (!state.client || !state.connected) return;\n try {\n const res = (await state.client.request(\"device.token.rotate\", params)) as\n | { token?: string; role?: string; deviceId?: string; scopes?: string[] }\n | undefined;\n if (res?.token) {\n const identity = await loadOrCreateDeviceIdentity();\n const role = res.role ?? params.role;\n if (res.deviceId === identity.deviceId || params.deviceId === identity.deviceId) {\n storeDeviceAuthToken({\n deviceId: identity.deviceId,\n role,\n token: res.token,\n scopes: res.scopes ?? params.scopes ?? [],\n });\n }\n window.prompt(\"New device token (copy and store securely):\", res.token);\n }\n await loadDevices(state);\n } catch (err) {\n state.devicesError = String(err);\n }\n}\n\nexport async function revokeDeviceToken(\n state: DevicesState,\n params: { deviceId: string; role: string },\n) {\n if (!state.client || !state.connected) return;\n const confirmed = window.confirm(\n `Revoke token for ${params.deviceId} (${params.role})?`,\n );\n if (!confirmed) return;\n try {\n await state.client.request(\"device.token.revoke\", params);\n const identity = await loadOrCreateDeviceIdentity();\n if (params.deviceId === identity.deviceId) {\n clearDeviceAuthToken({ deviceId: identity.deviceId, role: params.role });\n }\n await loadDevices(state);\n } catch (err) {\n state.devicesError = String(err);\n }\n}\n","import type { GatewayBrowserClient } from \"../gateway\";\n\nexport type NodesState = {\n client: GatewayBrowserClient | null;\n connected: boolean;\n nodesLoading: boolean;\n nodes: Array>;\n lastError: string | null;\n};\n\nexport async function loadNodes(\n state: NodesState,\n opts?: { quiet?: boolean },\n) {\n if (!state.client || !state.connected) return;\n if (state.nodesLoading) return;\n state.nodesLoading = true;\n if (!opts?.quiet) state.lastError = null;\n try {\n const res = (await state.client.request(\"node.list\", {})) as {\n nodes?: Array>;\n };\n state.nodes = Array.isArray(res.nodes) ? res.nodes : [];\n } catch (err) {\n if (!opts?.quiet) state.lastError = String(err);\n } finally {\n state.nodesLoading = false;\n }\n}\n","import type { GatewayBrowserClient } from \"../gateway\";\nimport { cloneConfigObject, removePathValue, setPathValue } from \"./config/form-utils\";\n\nexport type ExecApprovalsDefaults = {\n security?: string;\n ask?: string;\n askFallback?: string;\n autoAllowSkills?: boolean;\n};\n\nexport type ExecApprovalsAllowlistEntry = {\n pattern: string;\n lastUsedAt?: number;\n lastUsedCommand?: string;\n lastResolvedPath?: string;\n};\n\nexport type ExecApprovalsAgent = ExecApprovalsDefaults & {\n allowlist?: ExecApprovalsAllowlistEntry[];\n};\n\nexport type ExecApprovalsFile = {\n version?: number;\n socket?: { path?: string };\n defaults?: ExecApprovalsDefaults;\n agents?: Record;\n};\n\nexport type ExecApprovalsSnapshot = {\n path: string;\n exists: boolean;\n hash: string;\n file: ExecApprovalsFile;\n};\n\nexport type ExecApprovalsTarget =\n | { kind: \"gateway\" }\n | { kind: \"node\"; nodeId: string };\n\nexport type ExecApprovalsState = {\n client: GatewayBrowserClient | null;\n connected: boolean;\n execApprovalsLoading: boolean;\n execApprovalsSaving: boolean;\n execApprovalsDirty: boolean;\n execApprovalsSnapshot: ExecApprovalsSnapshot | null;\n execApprovalsForm: ExecApprovalsFile | null;\n execApprovalsSelectedAgent: string | null;\n lastError: string | null;\n};\n\nfunction resolveExecApprovalsRpc(target?: ExecApprovalsTarget | null): {\n method: string;\n params: Record;\n} | null {\n if (!target || target.kind === \"gateway\") {\n return { method: \"exec.approvals.get\", params: {} };\n }\n const nodeId = target.nodeId.trim();\n if (!nodeId) return null;\n return { method: \"exec.approvals.node.get\", params: { nodeId } };\n}\n\nfunction resolveExecApprovalsSaveRpc(\n target: ExecApprovalsTarget | null | undefined,\n params: { file: ExecApprovalsFile; baseHash: string },\n): { method: string; params: Record } | null {\n if (!target || target.kind === \"gateway\") {\n return { method: \"exec.approvals.set\", params };\n }\n const nodeId = target.nodeId.trim();\n if (!nodeId) return null;\n return { method: \"exec.approvals.node.set\", params: { ...params, nodeId } };\n}\n\nexport async function loadExecApprovals(\n state: ExecApprovalsState,\n target?: ExecApprovalsTarget | null,\n) {\n if (!state.client || !state.connected) return;\n if (state.execApprovalsLoading) return;\n state.execApprovalsLoading = true;\n state.lastError = null;\n try {\n const rpc = resolveExecApprovalsRpc(target);\n if (!rpc) {\n state.lastError = \"Select a node before loading exec approvals.\";\n return;\n }\n const res = (await state.client.request(rpc.method, rpc.params)) as ExecApprovalsSnapshot;\n applyExecApprovalsSnapshot(state, res);\n } catch (err) {\n state.lastError = String(err);\n } finally {\n state.execApprovalsLoading = false;\n }\n}\n\nexport function applyExecApprovalsSnapshot(\n state: ExecApprovalsState,\n snapshot: ExecApprovalsSnapshot,\n) {\n state.execApprovalsSnapshot = snapshot;\n if (!state.execApprovalsDirty) {\n state.execApprovalsForm = cloneConfigObject(snapshot.file ?? {});\n }\n}\n\nexport async function saveExecApprovals(\n state: ExecApprovalsState,\n target?: ExecApprovalsTarget | null,\n) {\n if (!state.client || !state.connected) return;\n state.execApprovalsSaving = true;\n state.lastError = null;\n try {\n const baseHash = state.execApprovalsSnapshot?.hash;\n if (!baseHash) {\n state.lastError = \"Exec approvals hash missing; reload and retry.\";\n return;\n }\n const file =\n state.execApprovalsForm ??\n state.execApprovalsSnapshot?.file ??\n {};\n const rpc = resolveExecApprovalsSaveRpc(target, { file, baseHash });\n if (!rpc) {\n state.lastError = \"Select a node before saving exec approvals.\";\n return;\n }\n await state.client.request(rpc.method, rpc.params);\n state.execApprovalsDirty = false;\n await loadExecApprovals(state, target);\n } catch (err) {\n state.lastError = String(err);\n } finally {\n state.execApprovalsSaving = false;\n }\n}\n\nexport function updateExecApprovalsFormValue(\n state: ExecApprovalsState,\n path: Array,\n value: unknown,\n) {\n const base = cloneConfigObject(\n state.execApprovalsForm ?? state.execApprovalsSnapshot?.file ?? {},\n );\n setPathValue(base, path, value);\n state.execApprovalsForm = base;\n state.execApprovalsDirty = true;\n}\n\nexport function removeExecApprovalsFormValue(\n state: ExecApprovalsState,\n path: Array,\n) {\n const base = cloneConfigObject(\n state.execApprovalsForm ?? state.execApprovalsSnapshot?.file ?? {},\n );\n removePathValue(base, path);\n state.execApprovalsForm = base;\n state.execApprovalsDirty = true;\n}\n","import type { GatewayBrowserClient } from \"../gateway\";\nimport type { PresenceEntry } from \"../types\";\n\nexport type PresenceState = {\n client: GatewayBrowserClient | null;\n connected: boolean;\n presenceLoading: boolean;\n presenceEntries: PresenceEntry[];\n presenceError: string | null;\n presenceStatus: string | null;\n};\n\nexport async function loadPresence(state: PresenceState) {\n if (!state.client || !state.connected) return;\n if (state.presenceLoading) return;\n state.presenceLoading = true;\n state.presenceError = null;\n state.presenceStatus = null;\n try {\n const res = (await state.client.request(\"system-presence\", {})) as\n | PresenceEntry[]\n | undefined;\n if (Array.isArray(res)) {\n state.presenceEntries = res;\n state.presenceStatus = res.length === 0 ? \"No instances yet.\" : null;\n } else {\n state.presenceEntries = [];\n state.presenceStatus = \"No presence payload.\";\n }\n } catch (err) {\n state.presenceError = String(err);\n } finally {\n state.presenceLoading = false;\n }\n}\n\n","import type { GatewayBrowserClient } from \"../gateway\";\nimport type { SkillStatusReport } from \"../types\";\n\nexport type SkillsState = {\n client: GatewayBrowserClient | null;\n connected: boolean;\n skillsLoading: boolean;\n skillsReport: SkillStatusReport | null;\n skillsError: string | null;\n skillsBusyKey: string | null;\n skillEdits: Record;\n skillMessages: SkillMessageMap;\n};\n\nexport type SkillMessage = {\n kind: \"success\" | \"error\";\n message: string;\n};\n\nexport type SkillMessageMap = Record;\n\ntype LoadSkillsOptions = {\n clearMessages?: boolean;\n};\n\nfunction setSkillMessage(state: SkillsState, key: string, message?: SkillMessage) {\n if (!key.trim()) return;\n const next = { ...state.skillMessages };\n if (message) next[key] = message;\n else delete next[key];\n state.skillMessages = next;\n}\n\nfunction getErrorMessage(err: unknown) {\n if (err instanceof Error) return err.message;\n return String(err);\n}\n\nexport async function loadSkills(state: SkillsState, options?: LoadSkillsOptions) {\n if (options?.clearMessages && Object.keys(state.skillMessages).length > 0) {\n state.skillMessages = {};\n }\n if (!state.client || !state.connected) return;\n if (state.skillsLoading) return;\n state.skillsLoading = true;\n state.skillsError = null;\n try {\n const res = (await state.client.request(\"skills.status\", {})) as\n | SkillStatusReport\n | undefined;\n if (res) state.skillsReport = res;\n } catch (err) {\n state.skillsError = getErrorMessage(err);\n } finally {\n state.skillsLoading = false;\n }\n}\n\nexport function updateSkillEdit(\n state: SkillsState,\n skillKey: string,\n value: string,\n) {\n state.skillEdits = { ...state.skillEdits, [skillKey]: value };\n}\n\nexport async function updateSkillEnabled(\n state: SkillsState,\n skillKey: string,\n enabled: boolean,\n) {\n if (!state.client || !state.connected) return;\n state.skillsBusyKey = skillKey;\n state.skillsError = null;\n try {\n await state.client.request(\"skills.update\", { skillKey, enabled });\n await loadSkills(state);\n setSkillMessage(state, skillKey, {\n kind: \"success\",\n message: enabled ? \"Skill enabled\" : \"Skill disabled\",\n });\n } catch (err) {\n const message = getErrorMessage(err);\n state.skillsError = message;\n setSkillMessage(state, skillKey, {\n kind: \"error\",\n message,\n });\n } finally {\n state.skillsBusyKey = null;\n }\n}\n\nexport async function saveSkillApiKey(state: SkillsState, skillKey: string) {\n if (!state.client || !state.connected) return;\n state.skillsBusyKey = skillKey;\n state.skillsError = null;\n try {\n const apiKey = state.skillEdits[skillKey] ?? \"\";\n await state.client.request(\"skills.update\", { skillKey, apiKey });\n await loadSkills(state);\n setSkillMessage(state, skillKey, {\n kind: \"success\",\n message: \"API key saved\",\n });\n } catch (err) {\n const message = getErrorMessage(err);\n state.skillsError = message;\n setSkillMessage(state, skillKey, {\n kind: \"error\",\n message,\n });\n } finally {\n state.skillsBusyKey = null;\n }\n}\n\nexport async function installSkill(\n state: SkillsState,\n skillKey: string,\n name: string,\n installId: string,\n) {\n if (!state.client || !state.connected) return;\n state.skillsBusyKey = skillKey;\n state.skillsError = null;\n try {\n const result = (await state.client.request(\"skills.install\", {\n name,\n installId,\n timeoutMs: 120000,\n })) as { ok?: boolean; message?: string };\n await loadSkills(state);\n setSkillMessage(state, skillKey, {\n kind: \"success\",\n message: result?.message ?? \"Installed\",\n });\n } catch (err) {\n const message = getErrorMessage(err);\n state.skillsError = message;\n setSkillMessage(state, skillKey, {\n kind: \"error\",\n message,\n });\n } finally {\n state.skillsBusyKey = null;\n }\n}\n","export type ThemeMode = \"system\" | \"light\" | \"dark\";\nexport type ResolvedTheme = \"light\" | \"dark\";\n\nexport function getSystemTheme(): ResolvedTheme {\n if (typeof window === \"undefined\" || typeof window.matchMedia !== \"function\") {\n return \"dark\";\n }\n return window.matchMedia(\"(prefers-color-scheme: dark)\").matches\n ? \"dark\"\n : \"light\";\n}\n\nexport function resolveTheme(mode: ThemeMode): ResolvedTheme {\n if (mode === \"system\") return getSystemTheme();\n return mode;\n}\n","import type { ThemeMode } from \"./theme\";\n\nexport type ThemeTransitionContext = {\n element?: HTMLElement | null;\n pointerClientX?: number;\n pointerClientY?: number;\n};\n\nexport type ThemeTransitionOptions = {\n nextTheme: ThemeMode;\n applyTheme: () => void;\n context?: ThemeTransitionContext;\n currentTheme?: ThemeMode | null;\n};\n\ntype DocumentWithViewTransition = Document & {\n startViewTransition?: (callback: () => void) => { finished: Promise };\n};\n\nconst clamp01 = (value: number) => {\n if (Number.isNaN(value)) return 0.5;\n if (value <= 0) return 0;\n if (value >= 1) return 1;\n return value;\n};\n\nconst hasReducedMotionPreference = () => {\n if (typeof window === \"undefined\" || typeof window.matchMedia !== \"function\") {\n return false;\n }\n return window.matchMedia(\"(prefers-reduced-motion: reduce)\").matches ?? false;\n};\n\nconst cleanupThemeTransition = (root: HTMLElement) => {\n root.classList.remove(\"theme-transition\");\n root.style.removeProperty(\"--theme-switch-x\");\n root.style.removeProperty(\"--theme-switch-y\");\n};\n\nexport const startThemeTransition = ({\n nextTheme,\n applyTheme,\n context,\n currentTheme,\n}: ThemeTransitionOptions) => {\n if (currentTheme === nextTheme) return;\n\n const documentReference = globalThis.document ?? null;\n if (!documentReference) {\n applyTheme();\n return;\n }\n\n const root = documentReference.documentElement;\n const document_ = documentReference as DocumentWithViewTransition;\n const prefersReducedMotion = hasReducedMotionPreference();\n\n const canUseViewTransition =\n Boolean(document_.startViewTransition) && !prefersReducedMotion;\n\n if (canUseViewTransition) {\n let xPercent = 0.5;\n let yPercent = 0.5;\n\n if (\n context?.pointerClientX !== undefined &&\n context?.pointerClientY !== undefined &&\n typeof window !== \"undefined\"\n ) {\n xPercent = clamp01(context.pointerClientX / window.innerWidth);\n yPercent = clamp01(context.pointerClientY / window.innerHeight);\n } else if (context?.element) {\n const rect = context.element.getBoundingClientRect();\n if (\n rect.width > 0 &&\n rect.height > 0 &&\n typeof window !== \"undefined\"\n ) {\n xPercent = clamp01((rect.left + rect.width / 2) / window.innerWidth);\n yPercent = clamp01((rect.top + rect.height / 2) / window.innerHeight);\n }\n }\n\n root.style.setProperty(\"--theme-switch-x\", `${xPercent * 100}%`);\n root.style.setProperty(\"--theme-switch-y\", `${yPercent * 100}%`);\n root.classList.add(\"theme-transition\");\n\n try {\n const transition = document_.startViewTransition?.(() => {\n applyTheme();\n });\n if (transition?.finished) {\n void transition.finished.finally(() => cleanupThemeTransition(root));\n } else {\n cleanupThemeTransition(root);\n }\n } catch {\n cleanupThemeTransition(root);\n applyTheme();\n }\n return;\n }\n\n applyTheme();\n cleanupThemeTransition(root);\n};\n","import { loadLogs } from \"./controllers/logs\";\nimport { loadNodes } from \"./controllers/nodes\";\nimport { loadDebug } from \"./controllers/debug\";\nimport type { ClawdbotApp } from \"./app\";\n\ntype PollingHost = {\n nodesPollInterval: number | null;\n logsPollInterval: number | null;\n debugPollInterval: number | null;\n tab: string;\n};\n\nexport function startNodesPolling(host: PollingHost) {\n if (host.nodesPollInterval != null) return;\n host.nodesPollInterval = window.setInterval(\n () => void loadNodes(host as unknown as ClawdbotApp, { quiet: true }),\n 5000,\n );\n}\n\nexport function stopNodesPolling(host: PollingHost) {\n if (host.nodesPollInterval == null) return;\n clearInterval(host.nodesPollInterval);\n host.nodesPollInterval = null;\n}\n\nexport function startLogsPolling(host: PollingHost) {\n if (host.logsPollInterval != null) return;\n host.logsPollInterval = window.setInterval(() => {\n if (host.tab !== \"logs\") return;\n void loadLogs(host as unknown as ClawdbotApp, { quiet: true });\n }, 2000);\n}\n\nexport function stopLogsPolling(host: PollingHost) {\n if (host.logsPollInterval == null) return;\n clearInterval(host.logsPollInterval);\n host.logsPollInterval = null;\n}\n\nexport function startDebugPolling(host: PollingHost) {\n if (host.debugPollInterval != null) return;\n host.debugPollInterval = window.setInterval(() => {\n if (host.tab !== \"debug\") return;\n void loadDebug(host as unknown as ClawdbotApp);\n }, 3000);\n}\n\nexport function stopDebugPolling(host: PollingHost) {\n if (host.debugPollInterval == null) return;\n clearInterval(host.debugPollInterval);\n host.debugPollInterval = null;\n}\n","import { loadConfig, loadConfigSchema } from \"./controllers/config\";\nimport { loadCronJobs, loadCronStatus } from \"./controllers/cron\";\nimport { loadChannels } from \"./controllers/channels\";\nimport { loadDebug } from \"./controllers/debug\";\nimport { loadLogs } from \"./controllers/logs\";\nimport { loadDevices } from \"./controllers/devices\";\nimport { loadNodes } from \"./controllers/nodes\";\nimport { loadExecApprovals } from \"./controllers/exec-approvals\";\nimport { loadPresence } from \"./controllers/presence\";\nimport { loadSessions } from \"./controllers/sessions\";\nimport { loadSkills } from \"./controllers/skills\";\nimport { inferBasePathFromPathname, normalizeBasePath, normalizePath, pathForTab, tabFromPath, type Tab } from \"./navigation\";\nimport { saveSettings, type UiSettings } from \"./storage\";\nimport { resolveTheme, type ResolvedTheme, type ThemeMode } from \"./theme\";\nimport { startThemeTransition, type ThemeTransitionContext } from \"./theme-transition\";\nimport { scheduleChatScroll, scheduleLogsScroll } from \"./app-scroll\";\nimport { startLogsPolling, stopLogsPolling, startDebugPolling, stopDebugPolling } from \"./app-polling\";\nimport { refreshChat } from \"./app-chat\";\nimport type { ClawdbotApp } from \"./app\";\n\ntype SettingsHost = {\n settings: UiSettings;\n theme: ThemeMode;\n themeResolved: ResolvedTheme;\n applySessionKey: string;\n sessionKey: string;\n tab: Tab;\n connected: boolean;\n chatHasAutoScrolled: boolean;\n logsAtBottom: boolean;\n eventLog: unknown[];\n eventLogBuffer: unknown[];\n basePath: string;\n themeMedia: MediaQueryList | null;\n themeMediaHandler: ((event: MediaQueryListEvent) => void) | null;\n};\n\nexport function applySettings(host: SettingsHost, next: UiSettings) {\n const normalized = {\n ...next,\n lastActiveSessionKey: next.lastActiveSessionKey?.trim() || next.sessionKey.trim() || \"main\",\n };\n host.settings = normalized;\n saveSettings(normalized);\n if (next.theme !== host.theme) {\n host.theme = next.theme;\n applyResolvedTheme(host, resolveTheme(next.theme));\n }\n host.applySessionKey = host.settings.lastActiveSessionKey;\n}\n\nexport function setLastActiveSessionKey(host: SettingsHost, next: string) {\n const trimmed = next.trim();\n if (!trimmed) return;\n if (host.settings.lastActiveSessionKey === trimmed) return;\n applySettings(host, { ...host.settings, lastActiveSessionKey: trimmed });\n}\n\nexport function applySettingsFromUrl(host: SettingsHost) {\n if (!window.location.search) return;\n const params = new URLSearchParams(window.location.search);\n const tokenRaw = params.get(\"token\");\n const passwordRaw = params.get(\"password\");\n const sessionRaw = params.get(\"session\");\n const gatewayUrlRaw = params.get(\"gatewayUrl\");\n let shouldCleanUrl = false;\n\n if (tokenRaw != null) {\n const token = tokenRaw.trim();\n if (token && token !== host.settings.token) {\n applySettings(host, { ...host.settings, token });\n }\n params.delete(\"token\");\n shouldCleanUrl = true;\n }\n\n if (passwordRaw != null) {\n const password = passwordRaw.trim();\n if (password) {\n (host as { password: string }).password = password;\n }\n params.delete(\"password\");\n shouldCleanUrl = true;\n }\n\n if (sessionRaw != null) {\n const session = sessionRaw.trim();\n if (session) {\n host.sessionKey = session;\n applySettings(host, {\n ...host.settings,\n sessionKey: session,\n lastActiveSessionKey: session,\n });\n }\n }\n\n if (gatewayUrlRaw != null) {\n const gatewayUrl = gatewayUrlRaw.trim();\n if (gatewayUrl && gatewayUrl !== host.settings.gatewayUrl) {\n applySettings(host, { ...host.settings, gatewayUrl });\n }\n params.delete(\"gatewayUrl\");\n shouldCleanUrl = true;\n }\n\n if (!shouldCleanUrl) return;\n const url = new URL(window.location.href);\n url.search = params.toString();\n window.history.replaceState({}, \"\", url.toString());\n}\n\nexport function setTab(host: SettingsHost, next: Tab) {\n if (host.tab !== next) host.tab = next;\n if (next === \"chat\") host.chatHasAutoScrolled = false;\n if (next === \"logs\")\n startLogsPolling(host as unknown as Parameters[0]);\n else stopLogsPolling(host as unknown as Parameters[0]);\n if (next === \"debug\")\n startDebugPolling(host as unknown as Parameters[0]);\n else stopDebugPolling(host as unknown as Parameters[0]);\n void refreshActiveTab(host);\n syncUrlWithTab(host, next, false);\n}\n\nexport function setTheme(\n host: SettingsHost,\n next: ThemeMode,\n context?: ThemeTransitionContext,\n) {\n const applyTheme = () => {\n host.theme = next;\n applySettings(host, { ...host.settings, theme: next });\n applyResolvedTheme(host, resolveTheme(next));\n };\n startThemeTransition({\n nextTheme: next,\n applyTheme,\n context,\n currentTheme: host.theme,\n });\n}\n\nexport async function refreshActiveTab(host: SettingsHost) {\n if (host.tab === \"overview\") await loadOverview(host);\n if (host.tab === \"channels\") await loadChannelsTab(host);\n if (host.tab === \"instances\") await loadPresence(host as unknown as ClawdbotApp);\n if (host.tab === \"sessions\") await loadSessions(host as unknown as ClawdbotApp);\n if (host.tab === \"cron\") await loadCron(host);\n if (host.tab === \"skills\") await loadSkills(host as unknown as ClawdbotApp);\n if (host.tab === \"nodes\") {\n await loadNodes(host as unknown as ClawdbotApp);\n await loadDevices(host as unknown as ClawdbotApp);\n await loadConfig(host as unknown as ClawdbotApp);\n await loadExecApprovals(host as unknown as ClawdbotApp);\n }\n if (host.tab === \"chat\") {\n await refreshChat(host as unknown as Parameters[0]);\n scheduleChatScroll(\n host as unknown as Parameters[0],\n !host.chatHasAutoScrolled,\n );\n }\n if (host.tab === \"config\") {\n await loadConfigSchema(host as unknown as ClawdbotApp);\n await loadConfig(host as unknown as ClawdbotApp);\n }\n if (host.tab === \"debug\") {\n await loadDebug(host as unknown as ClawdbotApp);\n host.eventLog = host.eventLogBuffer;\n }\n if (host.tab === \"logs\") {\n host.logsAtBottom = true;\n await loadLogs(host as unknown as ClawdbotApp, { reset: true });\n scheduleLogsScroll(\n host as unknown as Parameters[0],\n true,\n );\n }\n}\n\nexport function inferBasePath() {\n if (typeof window === \"undefined\") return \"\";\n const configured = window.__CLAWDBOT_CONTROL_UI_BASE_PATH__;\n if (typeof configured === \"string\" && configured.trim()) {\n return normalizeBasePath(configured);\n }\n return inferBasePathFromPathname(window.location.pathname);\n}\n\nexport function syncThemeWithSettings(host: SettingsHost) {\n host.theme = host.settings.theme ?? \"system\";\n applyResolvedTheme(host, resolveTheme(host.theme));\n}\n\nexport function applyResolvedTheme(host: SettingsHost, resolved: ResolvedTheme) {\n host.themeResolved = resolved;\n if (typeof document === \"undefined\") return;\n const root = document.documentElement;\n root.dataset.theme = resolved;\n root.style.colorScheme = resolved;\n}\n\nexport function attachThemeListener(host: SettingsHost) {\n if (typeof window === \"undefined\" || typeof window.matchMedia !== \"function\") return;\n host.themeMedia = window.matchMedia(\"(prefers-color-scheme: dark)\");\n host.themeMediaHandler = (event) => {\n if (host.theme !== \"system\") return;\n applyResolvedTheme(host, event.matches ? \"dark\" : \"light\");\n };\n if (typeof host.themeMedia.addEventListener === \"function\") {\n host.themeMedia.addEventListener(\"change\", host.themeMediaHandler);\n return;\n }\n const legacy = host.themeMedia as MediaQueryList & {\n addListener: (cb: (event: MediaQueryListEvent) => void) => void;\n };\n legacy.addListener(host.themeMediaHandler);\n}\n\nexport function detachThemeListener(host: SettingsHost) {\n if (!host.themeMedia || !host.themeMediaHandler) return;\n if (typeof host.themeMedia.removeEventListener === \"function\") {\n host.themeMedia.removeEventListener(\"change\", host.themeMediaHandler);\n return;\n }\n const legacy = host.themeMedia as MediaQueryList & {\n removeListener: (cb: (event: MediaQueryListEvent) => void) => void;\n };\n legacy.removeListener(host.themeMediaHandler);\n host.themeMedia = null;\n host.themeMediaHandler = null;\n}\n\nexport function syncTabWithLocation(host: SettingsHost, replace: boolean) {\n if (typeof window === \"undefined\") return;\n const resolved = tabFromPath(window.location.pathname, host.basePath) ?? \"chat\";\n setTabFromRoute(host, resolved);\n syncUrlWithTab(host, resolved, replace);\n}\n\nexport function onPopState(host: SettingsHost) {\n if (typeof window === \"undefined\") return;\n const resolved = tabFromPath(window.location.pathname, host.basePath);\n if (!resolved) return;\n\n const url = new URL(window.location.href);\n const session = url.searchParams.get(\"session\")?.trim();\n if (session) {\n host.sessionKey = session;\n applySettings(host, {\n ...host.settings,\n sessionKey: session,\n lastActiveSessionKey: session,\n });\n }\n\n setTabFromRoute(host, resolved);\n}\n\nexport function setTabFromRoute(host: SettingsHost, next: Tab) {\n if (host.tab !== next) host.tab = next;\n if (next === \"chat\") host.chatHasAutoScrolled = false;\n if (next === \"logs\")\n startLogsPolling(host as unknown as Parameters[0]);\n else stopLogsPolling(host as unknown as Parameters[0]);\n if (next === \"debug\")\n startDebugPolling(host as unknown as Parameters[0]);\n else stopDebugPolling(host as unknown as Parameters[0]);\n if (host.connected) void refreshActiveTab(host);\n}\n\nexport function syncUrlWithTab(host: SettingsHost, tab: Tab, replace: boolean) {\n if (typeof window === \"undefined\") return;\n const targetPath = normalizePath(pathForTab(tab, host.basePath));\n const currentPath = normalizePath(window.location.pathname);\n const url = new URL(window.location.href);\n\n if (tab === \"chat\" && host.sessionKey) {\n url.searchParams.set(\"session\", host.sessionKey);\n } else {\n url.searchParams.delete(\"session\");\n }\n\n if (currentPath !== targetPath) {\n url.pathname = targetPath;\n }\n\n if (replace) {\n window.history.replaceState({}, \"\", url.toString());\n } else {\n window.history.pushState({}, \"\", url.toString());\n }\n}\n\nexport function syncUrlWithSessionKey(\n host: SettingsHost,\n sessionKey: string,\n replace: boolean,\n) {\n if (typeof window === \"undefined\") return;\n const url = new URL(window.location.href);\n url.searchParams.set(\"session\", sessionKey);\n if (replace) window.history.replaceState({}, \"\", url.toString());\n else window.history.pushState({}, \"\", url.toString());\n}\n\nexport async function loadOverview(host: SettingsHost) {\n await Promise.all([\n loadChannels(host as unknown as ClawdbotApp, false),\n loadPresence(host as unknown as ClawdbotApp),\n loadSessions(host as unknown as ClawdbotApp),\n loadCronStatus(host as unknown as ClawdbotApp),\n loadDebug(host as unknown as ClawdbotApp),\n ]);\n}\n\nexport async function loadChannelsTab(host: SettingsHost) {\n await Promise.all([\n loadChannels(host as unknown as ClawdbotApp, true),\n loadConfigSchema(host as unknown as ClawdbotApp),\n loadConfig(host as unknown as ClawdbotApp),\n ]);\n}\n\nexport async function loadCron(host: SettingsHost) {\n await Promise.all([\n loadChannels(host as unknown as ClawdbotApp, false),\n loadCronStatus(host as unknown as ClawdbotApp),\n loadCronJobs(host as unknown as ClawdbotApp),\n ]);\n}\n","import { abortChatRun, loadChatHistory, sendChatMessage } from \"./controllers/chat\";\nimport { loadSessions } from \"./controllers/sessions\";\nimport { generateUUID } from \"./uuid\";\nimport { resetToolStream } from \"./app-tool-stream\";\nimport { scheduleChatScroll } from \"./app-scroll\";\nimport { setLastActiveSessionKey } from \"./app-settings\";\nimport { normalizeBasePath } from \"./navigation\";\nimport type { GatewayHelloOk } from \"./gateway\";\nimport { parseAgentSessionKey } from \"../../../src/sessions/session-key-utils.js\";\nimport type { ClawdbotApp } from \"./app\";\n\ntype ChatHost = {\n connected: boolean;\n chatMessage: string;\n chatQueue: Array<{ id: string; text: string; createdAt: number }>;\n chatRunId: string | null;\n chatSending: boolean;\n sessionKey: string;\n basePath: string;\n hello: GatewayHelloOk | null;\n chatAvatarUrl: string | null;\n};\n\nexport function isChatBusy(host: ChatHost) {\n return host.chatSending || Boolean(host.chatRunId);\n}\n\nexport function isChatStopCommand(text: string) {\n const trimmed = text.trim();\n if (!trimmed) return false;\n const normalized = trimmed.toLowerCase();\n if (normalized === \"/stop\") return true;\n return (\n normalized === \"stop\" ||\n normalized === \"esc\" ||\n normalized === \"abort\" ||\n normalized === \"wait\" ||\n normalized === \"exit\"\n );\n}\n\nexport async function handleAbortChat(host: ChatHost) {\n if (!host.connected) return;\n host.chatMessage = \"\";\n await abortChatRun(host as unknown as ClawdbotApp);\n}\n\nfunction enqueueChatMessage(host: ChatHost, text: string) {\n const trimmed = text.trim();\n if (!trimmed) return;\n host.chatQueue = [\n ...host.chatQueue,\n {\n id: generateUUID(),\n text: trimmed,\n createdAt: Date.now(),\n },\n ];\n}\n\nasync function sendChatMessageNow(\n host: ChatHost,\n message: string,\n opts?: { previousDraft?: string; restoreDraft?: boolean },\n) {\n resetToolStream(host as unknown as Parameters[0]);\n const ok = await sendChatMessage(host as unknown as ClawdbotApp, message);\n if (!ok && opts?.previousDraft != null) {\n host.chatMessage = opts.previousDraft;\n }\n if (ok) {\n setLastActiveSessionKey(host as unknown as Parameters[0], host.sessionKey);\n }\n if (ok && opts?.restoreDraft && opts.previousDraft?.trim()) {\n host.chatMessage = opts.previousDraft;\n }\n scheduleChatScroll(host as unknown as Parameters[0]);\n if (ok && !host.chatRunId) {\n void flushChatQueue(host);\n }\n return ok;\n}\n\nasync function flushChatQueue(host: ChatHost) {\n if (!host.connected || isChatBusy(host)) return;\n const [next, ...rest] = host.chatQueue;\n if (!next) return;\n host.chatQueue = rest;\n const ok = await sendChatMessageNow(host, next.text);\n if (!ok) {\n host.chatQueue = [next, ...host.chatQueue];\n }\n}\n\nexport function removeQueuedMessage(host: ChatHost, id: string) {\n host.chatQueue = host.chatQueue.filter((item) => item.id !== id);\n}\n\nexport async function handleSendChat(\n host: ChatHost,\n messageOverride?: string,\n opts?: { restoreDraft?: boolean },\n) {\n if (!host.connected) return;\n const previousDraft = host.chatMessage;\n const message = (messageOverride ?? host.chatMessage).trim();\n if (!message) return;\n\n if (isChatStopCommand(message)) {\n await handleAbortChat(host);\n return;\n }\n\n if (messageOverride == null) {\n host.chatMessage = \"\";\n }\n\n if (isChatBusy(host)) {\n enqueueChatMessage(host, message);\n return;\n }\n\n await sendChatMessageNow(host, message, {\n previousDraft: messageOverride == null ? previousDraft : undefined,\n restoreDraft: Boolean(messageOverride && opts?.restoreDraft),\n });\n}\n\nexport async function refreshChat(host: ChatHost) {\n await Promise.all([\n loadChatHistory(host as unknown as ClawdbotApp),\n loadSessions(host as unknown as ClawdbotApp),\n refreshChatAvatar(host),\n ]);\n scheduleChatScroll(host as unknown as Parameters[0], true);\n}\n\nexport const flushChatQueueForEvent = flushChatQueue;\n\ntype SessionDefaultsSnapshot = {\n defaultAgentId?: string;\n};\n\nfunction resolveAgentIdForSession(host: ChatHost): string | null {\n const parsed = parseAgentSessionKey(host.sessionKey);\n if (parsed?.agentId) return parsed.agentId;\n const snapshot = host.hello?.snapshot as { sessionDefaults?: SessionDefaultsSnapshot } | undefined;\n const fallback = snapshot?.sessionDefaults?.defaultAgentId?.trim();\n return fallback || \"main\";\n}\n\nfunction buildAvatarMetaUrl(basePath: string, agentId: string): string {\n const base = normalizeBasePath(basePath);\n const encoded = encodeURIComponent(agentId);\n return base ? `${base}/avatar/${encoded}?meta=1` : `/avatar/${encoded}?meta=1`;\n}\n\nexport async function refreshChatAvatar(host: ChatHost) {\n if (!host.connected) {\n host.chatAvatarUrl = null;\n return;\n }\n const agentId = resolveAgentIdForSession(host);\n if (!agentId) {\n host.chatAvatarUrl = null;\n return;\n }\n host.chatAvatarUrl = null;\n const url = buildAvatarMetaUrl(host.basePath, agentId);\n try {\n const res = await fetch(url, { method: \"GET\" });\n if (!res.ok) {\n host.chatAvatarUrl = null;\n return;\n }\n const data = (await res.json()) as { avatarUrl?: unknown };\n const avatarUrl = typeof data.avatarUrl === \"string\" ? data.avatarUrl.trim() : \"\";\n host.chatAvatarUrl = avatarUrl || null;\n } catch {\n host.chatAvatarUrl = null;\n }\n}\n","/**\n * @license\n * Copyright 2017 Google LLC\n * SPDX-License-Identifier: BSD-3-Clause\n */\nconst t={ATTRIBUTE:1,CHILD:2,PROPERTY:3,BOOLEAN_ATTRIBUTE:4,EVENT:5,ELEMENT:6},e=t=>(...e)=>({_$litDirective$:t,values:e});class i{constructor(t){}get _$AU(){return this._$AM._$AU}_$AT(t,e,i){this._$Ct=t,this._$AM=e,this._$Ci=i}_$AS(t,e){return this.update(t,e)}update(t,e){return this.render(...e)}}export{i as Directive,t as PartType,e as directive};\n//# sourceMappingURL=directive.js.map\n","import{_$LH as o}from\"./lit-html.js\";\n/**\n * @license\n * Copyright 2020 Google LLC\n * SPDX-License-Identifier: BSD-3-Clause\n */const{I:t}=o,i=o=>o,n=o=>null===o||\"object\"!=typeof o&&\"function\"!=typeof o,e={HTML:1,SVG:2,MATHML:3},l=(o,t)=>void 0===t?void 0!==o?._$litType$:o?._$litType$===t,d=o=>null!=o?._$litType$?.h,c=o=>void 0!==o?._$litDirective$,f=o=>o?._$litDirective$,r=o=>void 0===o.strings,s=()=>document.createComment(\"\"),v=(o,n,e)=>{const l=o._$AA.parentNode,d=void 0===n?o._$AB:n._$AA;if(void 0===e){const i=l.insertBefore(s(),d),n=l.insertBefore(s(),d);e=new t(i,n,o,o.options)}else{const t=e._$AB.nextSibling,n=e._$AM,c=n!==o;if(c){let t;e._$AQ?.(o),e._$AM=o,void 0!==e._$AP&&(t=o._$AU)!==n._$AU&&e._$AP(t)}if(t!==d||c){let o=e._$AA;for(;o!==t;){const t=i(o).nextSibling;i(l).insertBefore(o,d),o=t}}}return e},u=(o,t,i=o)=>(o._$AI(t,i),o),m={},p=(o,t=m)=>o._$AH=t,M=o=>o._$AH,h=o=>{o._$AR(),o._$AA.remove()},j=o=>{o._$AR()};export{e as TemplateResultType,j as clearPart,M as getCommittedValue,f as getDirectiveClass,v as insertPart,d as isCompiledTemplateResult,c as isDirectiveResult,n as isPrimitive,r as isSingleExpression,l as isTemplateResult,h as removePart,u as setChildPartValue,p as setCommittedValue};\n//# sourceMappingURL=directive-helpers.js.map\n","import{noChange as e}from\"../lit-html.js\";import{directive as s,Directive as t,PartType as r}from\"../directive.js\";import{getCommittedValue as l,setChildPartValue as o,insertPart as i,removePart as n,setCommittedValue as f}from\"../directive-helpers.js\";\n/**\n * @license\n * Copyright 2017 Google LLC\n * SPDX-License-Identifier: BSD-3-Clause\n */\nconst u=(e,s,t)=>{const r=new Map;for(let l=s;l<=t;l++)r.set(e[l],l);return r},c=s(class extends t{constructor(e){if(super(e),e.type!==r.CHILD)throw Error(\"repeat() can only be used in text expressions\")}dt(e,s,t){let r;void 0===t?t=s:void 0!==s&&(r=s);const l=[],o=[];let i=0;for(const s of e)l[i]=r?r(s,i):i,o[i]=t(s,i),i++;return{values:o,keys:l}}render(e,s,t){return this.dt(e,s,t).values}update(s,[t,r,c]){const d=l(s),{values:p,keys:a}=this.dt(t,r,c);if(!Array.isArray(d))return this.ut=a,p;const h=this.ut??=[],v=[];let m,y,x=0,j=d.length-1,k=0,w=p.length-1;for(;x<=j&&k<=w;)if(null===d[x])x++;else if(null===d[j])j--;else if(h[x]===a[k])v[k]=o(d[x],p[k]),x++,k++;else if(h[j]===a[w])v[w]=o(d[j],p[w]),j--,w--;else if(h[x]===a[w])v[w]=o(d[x],p[w]),i(s,v[w+1],d[x]),x++,w--;else if(h[j]===a[k])v[k]=o(d[j],p[k]),i(s,d[x],d[j]),j--,k++;else if(void 0===m&&(m=u(a,k,w),y=u(h,x,j)),m.has(h[x]))if(m.has(h[j])){const e=y.get(a[k]),t=void 0!==e?d[e]:null;if(null===t){const e=i(s,d[x]);o(e,p[k]),v[k]=e}else v[k]=o(t,p[k]),i(s,d[x],t),d[e]=null;k++}else n(d[j]),j--;else n(d[x]),x++;for(;k<=w;){const e=i(s,v[w+1]);o(e,p[k]),v[k++]=e}for(;x<=j;){const e=d[x++];null!==e&&n(e)}return this.ut=a,f(s,v),e}});export{c as repeat};\n//# sourceMappingURL=repeat.js.map\n","/**\n * Message normalization utilities for chat rendering.\n */\n\nimport type {\n NormalizedMessage,\n MessageContentItem,\n} from \"../types/chat-types\";\n\n/**\n * Normalize a raw message object into a consistent structure.\n */\nexport function normalizeMessage(message: unknown): NormalizedMessage {\n const m = message as Record;\n let role = typeof m.role === \"string\" ? m.role : \"unknown\";\n\n // Detect tool messages by common gateway shapes.\n // Some tool events come through as assistant role with tool_* items in the content array.\n const hasToolId =\n typeof m.toolCallId === \"string\" || typeof m.tool_call_id === \"string\";\n\n const contentRaw = m.content;\n const contentItems = Array.isArray(contentRaw) ? contentRaw : null;\n const hasToolContent =\n Array.isArray(contentItems) &&\n contentItems.some((item) => {\n const x = item as Record;\n const t = String(x.type ?? \"\").toLowerCase();\n return (\n t === \"toolcall\" ||\n t === \"tool_call\" ||\n t === \"tooluse\" ||\n t === \"tool_use\" ||\n t === \"toolresult\" ||\n t === \"tool_result\" ||\n t === \"tool_call\" ||\n t === \"tool_result\" ||\n (typeof x.name === \"string\" && x.arguments != null)\n );\n });\n\n const hasToolName =\n typeof (m as Record).toolName === \"string\" ||\n typeof (m as Record).tool_name === \"string\";\n\n if (hasToolId || hasToolContent || hasToolName) {\n role = \"toolResult\";\n }\n\n // Extract content\n let content: MessageContentItem[] = [];\n\n if (typeof m.content === \"string\") {\n content = [{ type: \"text\", text: m.content }];\n } else if (Array.isArray(m.content)) {\n content = m.content.map((item: Record) => ({\n type: (item.type as MessageContentItem[\"type\"]) || \"text\",\n text: item.text as string | undefined,\n name: item.name as string | undefined,\n args: item.args || item.arguments,\n }));\n } else if (typeof m.text === \"string\") {\n content = [{ type: \"text\", text: m.text }];\n }\n\n const timestamp = typeof m.timestamp === \"number\" ? m.timestamp : Date.now();\n const id = typeof m.id === \"string\" ? m.id : undefined;\n\n return { role, content, timestamp, id };\n}\n\n/**\n * Normalize role for grouping purposes.\n */\nexport function normalizeRoleForGrouping(role: string): string {\n const lower = role.toLowerCase();\n // Keep tool-related roles distinct so the UI can style/toggle them.\n if (\n lower === \"toolresult\" ||\n lower === \"tool_result\" ||\n lower === \"tool\" ||\n lower === \"function\" ||\n lower === \"toolresult\"\n ) {\n return \"tool\";\n }\n if (lower === \"assistant\") return \"assistant\";\n if (lower === \"user\") return \"user\";\n if (lower === \"system\") return \"system\";\n return role;\n}\n\n/**\n * Check if a message is a tool result message based on its role.\n */\nexport function isToolResultMessage(message: unknown): boolean {\n const m = message as Record;\n const role = typeof m.role === \"string\" ? m.role.toLowerCase() : \"\";\n return role === \"toolresult\" || role === \"tool_result\";\n}\n","import{nothing as t,noChange as i}from\"../lit-html.js\";import{directive as r,Directive as s,PartType as n}from\"../directive.js\";\n/**\n * @license\n * Copyright 2017 Google LLC\n * SPDX-License-Identifier: BSD-3-Clause\n */class e extends s{constructor(i){if(super(i),this.it=t,i.type!==n.CHILD)throw Error(this.constructor.directiveName+\"() can only be used in child bindings\")}render(r){if(r===t||null==r)return this._t=void 0,this.it=r;if(r===i)return r;if(\"string\"!=typeof r)throw Error(this.constructor.directiveName+\"() called with a non-string value\");if(r===this.it)return this._t;this.it=r;const s=[r];return s.raw=s,this._t={_$litType$:this.constructor.resultType,strings:s,values:[]}}}e.directiveName=\"unsafeHTML\",e.resultType=1;const o=r(e);export{e as UnsafeHTMLDirective,o as unsafeHTML};\n//# sourceMappingURL=unsafe-html.js.map\n","/*! @license DOMPurify 3.3.1 | (c) Cure53 and other contributors | Released under the Apache license 2.0 and Mozilla Public License 2.0 | github.com/cure53/DOMPurify/blob/3.3.1/LICENSE */\n\nconst {\n entries,\n setPrototypeOf,\n isFrozen,\n getPrototypeOf,\n getOwnPropertyDescriptor\n} = Object;\nlet {\n freeze,\n seal,\n create\n} = Object; // eslint-disable-line import/no-mutable-exports\nlet {\n apply,\n construct\n} = typeof Reflect !== 'undefined' && Reflect;\nif (!freeze) {\n freeze = function freeze(x) {\n return x;\n };\n}\nif (!seal) {\n seal = function seal(x) {\n return x;\n };\n}\nif (!apply) {\n apply = function apply(func, thisArg) {\n for (var _len = arguments.length, args = new Array(_len > 2 ? _len - 2 : 0), _key = 2; _key < _len; _key++) {\n args[_key - 2] = arguments[_key];\n }\n return func.apply(thisArg, args);\n };\n}\nif (!construct) {\n construct = function construct(Func) {\n for (var _len2 = arguments.length, args = new Array(_len2 > 1 ? _len2 - 1 : 0), _key2 = 1; _key2 < _len2; _key2++) {\n args[_key2 - 1] = arguments[_key2];\n }\n return new Func(...args);\n };\n}\nconst arrayForEach = unapply(Array.prototype.forEach);\nconst arrayLastIndexOf = unapply(Array.prototype.lastIndexOf);\nconst arrayPop = unapply(Array.prototype.pop);\nconst arrayPush = unapply(Array.prototype.push);\nconst arraySplice = unapply(Array.prototype.splice);\nconst stringToLowerCase = unapply(String.prototype.toLowerCase);\nconst stringToString = unapply(String.prototype.toString);\nconst stringMatch = unapply(String.prototype.match);\nconst stringReplace = unapply(String.prototype.replace);\nconst stringIndexOf = unapply(String.prototype.indexOf);\nconst stringTrim = unapply(String.prototype.trim);\nconst objectHasOwnProperty = unapply(Object.prototype.hasOwnProperty);\nconst regExpTest = unapply(RegExp.prototype.test);\nconst typeErrorCreate = unconstruct(TypeError);\n/**\n * Creates a new function that calls the given function with a specified thisArg and arguments.\n *\n * @param func - The function to be wrapped and called.\n * @returns A new function that calls the given function with a specified thisArg and arguments.\n */\nfunction unapply(func) {\n return function (thisArg) {\n if (thisArg instanceof RegExp) {\n thisArg.lastIndex = 0;\n }\n for (var _len3 = arguments.length, args = new Array(_len3 > 1 ? _len3 - 1 : 0), _key3 = 1; _key3 < _len3; _key3++) {\n args[_key3 - 1] = arguments[_key3];\n }\n return apply(func, thisArg, args);\n };\n}\n/**\n * Creates a new function that constructs an instance of the given constructor function with the provided arguments.\n *\n * @param func - The constructor function to be wrapped and called.\n * @returns A new function that constructs an instance of the given constructor function with the provided arguments.\n */\nfunction unconstruct(Func) {\n return function () {\n for (var _len4 = arguments.length, args = new Array(_len4), _key4 = 0; _key4 < _len4; _key4++) {\n args[_key4] = arguments[_key4];\n }\n return construct(Func, args);\n };\n}\n/**\n * Add properties to a lookup table\n *\n * @param set - The set to which elements will be added.\n * @param array - The array containing elements to be added to the set.\n * @param transformCaseFunc - An optional function to transform the case of each element before adding to the set.\n * @returns The modified set with added elements.\n */\nfunction addToSet(set, array) {\n let transformCaseFunc = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : stringToLowerCase;\n if (setPrototypeOf) {\n // Make 'in' and truthy checks like Boolean(set.constructor)\n // independent of any properties defined on Object.prototype.\n // Prevent prototype setters from intercepting set as a this value.\n setPrototypeOf(set, null);\n }\n let l = array.length;\n while (l--) {\n let element = array[l];\n if (typeof element === 'string') {\n const lcElement = transformCaseFunc(element);\n if (lcElement !== element) {\n // Config presets (e.g. tags.js, attrs.js) are immutable.\n if (!isFrozen(array)) {\n array[l] = lcElement;\n }\n element = lcElement;\n }\n }\n set[element] = true;\n }\n return set;\n}\n/**\n * Clean up an array to harden against CSPP\n *\n * @param array - The array to be cleaned.\n * @returns The cleaned version of the array\n */\nfunction cleanArray(array) {\n for (let index = 0; index < array.length; index++) {\n const isPropertyExist = objectHasOwnProperty(array, index);\n if (!isPropertyExist) {\n array[index] = null;\n }\n }\n return array;\n}\n/**\n * Shallow clone an object\n *\n * @param object - The object to be cloned.\n * @returns A new object that copies the original.\n */\nfunction clone(object) {\n const newObject = create(null);\n for (const [property, value] of entries(object)) {\n const isPropertyExist = objectHasOwnProperty(object, property);\n if (isPropertyExist) {\n if (Array.isArray(value)) {\n newObject[property] = cleanArray(value);\n } else if (value && typeof value === 'object' && value.constructor === Object) {\n newObject[property] = clone(value);\n } else {\n newObject[property] = value;\n }\n }\n }\n return newObject;\n}\n/**\n * This method automatically checks if the prop is function or getter and behaves accordingly.\n *\n * @param object - The object to look up the getter function in its prototype chain.\n * @param prop - The property name for which to find the getter function.\n * @returns The getter function found in the prototype chain or a fallback function.\n */\nfunction lookupGetter(object, prop) {\n while (object !== null) {\n const desc = getOwnPropertyDescriptor(object, prop);\n if (desc) {\n if (desc.get) {\n return unapply(desc.get);\n }\n if (typeof desc.value === 'function') {\n return unapply(desc.value);\n }\n }\n object = getPrototypeOf(object);\n }\n function fallbackValue() {\n return null;\n }\n return fallbackValue;\n}\n\nconst html$1 = freeze(['a', 'abbr', 'acronym', 'address', 'area', 'article', 'aside', 'audio', 'b', 'bdi', 'bdo', 'big', 'blink', 'blockquote', 'body', 'br', 'button', 'canvas', 'caption', 'center', 'cite', 'code', 'col', 'colgroup', 'content', 'data', 'datalist', 'dd', 'decorator', 'del', 'details', 'dfn', 'dialog', 'dir', 'div', 'dl', 'dt', 'element', 'em', 'fieldset', 'figcaption', 'figure', 'font', 'footer', 'form', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'head', 'header', 'hgroup', 'hr', 'html', 'i', 'img', 'input', 'ins', 'kbd', 'label', 'legend', 'li', 'main', 'map', 'mark', 'marquee', 'menu', 'menuitem', 'meter', 'nav', 'nobr', 'ol', 'optgroup', 'option', 'output', 'p', 'picture', 'pre', 'progress', 'q', 'rp', 'rt', 'ruby', 's', 'samp', 'search', 'section', 'select', 'shadow', 'slot', 'small', 'source', 'spacer', 'span', 'strike', 'strong', 'style', 'sub', 'summary', 'sup', 'table', 'tbody', 'td', 'template', 'textarea', 'tfoot', 'th', 'thead', 'time', 'tr', 'track', 'tt', 'u', 'ul', 'var', 'video', 'wbr']);\nconst svg$1 = freeze(['svg', 'a', 'altglyph', 'altglyphdef', 'altglyphitem', 'animatecolor', 'animatemotion', 'animatetransform', 'circle', 'clippath', 'defs', 'desc', 'ellipse', 'enterkeyhint', 'exportparts', 'filter', 'font', 'g', 'glyph', 'glyphref', 'hkern', 'image', 'inputmode', 'line', 'lineargradient', 'marker', 'mask', 'metadata', 'mpath', 'part', 'path', 'pattern', 'polygon', 'polyline', 'radialgradient', 'rect', 'stop', 'style', 'switch', 'symbol', 'text', 'textpath', 'title', 'tref', 'tspan', 'view', 'vkern']);\nconst svgFilters = freeze(['feBlend', 'feColorMatrix', 'feComponentTransfer', 'feComposite', 'feConvolveMatrix', 'feDiffuseLighting', 'feDisplacementMap', 'feDistantLight', 'feDropShadow', 'feFlood', 'feFuncA', 'feFuncB', 'feFuncG', 'feFuncR', 'feGaussianBlur', 'feImage', 'feMerge', 'feMergeNode', 'feMorphology', 'feOffset', 'fePointLight', 'feSpecularLighting', 'feSpotLight', 'feTile', 'feTurbulence']);\n// List of SVG elements that are disallowed by default.\n// We still need to know them so that we can do namespace\n// checks properly in case one wants to add them to\n// allow-list.\nconst svgDisallowed = freeze(['animate', 'color-profile', 'cursor', 'discard', 'font-face', 'font-face-format', 'font-face-name', 'font-face-src', 'font-face-uri', 'foreignobject', 'hatch', 'hatchpath', 'mesh', 'meshgradient', 'meshpatch', 'meshrow', 'missing-glyph', 'script', 'set', 'solidcolor', 'unknown', 'use']);\nconst mathMl$1 = freeze(['math', 'menclose', 'merror', 'mfenced', 'mfrac', 'mglyph', 'mi', 'mlabeledtr', 'mmultiscripts', 'mn', 'mo', 'mover', 'mpadded', 'mphantom', 'mroot', 'mrow', 'ms', 'mspace', 'msqrt', 'mstyle', 'msub', 'msup', 'msubsup', 'mtable', 'mtd', 'mtext', 'mtr', 'munder', 'munderover', 'mprescripts']);\n// Similarly to SVG, we want to know all MathML elements,\n// even those that we disallow by default.\nconst mathMlDisallowed = freeze(['maction', 'maligngroup', 'malignmark', 'mlongdiv', 'mscarries', 'mscarry', 'msgroup', 'mstack', 'msline', 'msrow', 'semantics', 'annotation', 'annotation-xml', 'mprescripts', 'none']);\nconst text = freeze(['#text']);\n\nconst html = freeze(['accept', 'action', 'align', 'alt', 'autocapitalize', 'autocomplete', 'autopictureinpicture', 'autoplay', 'background', 'bgcolor', 'border', 'capture', 'cellpadding', 'cellspacing', 'checked', 'cite', 'class', 'clear', 'color', 'cols', 'colspan', 'controls', 'controlslist', 'coords', 'crossorigin', 'datetime', 'decoding', 'default', 'dir', 'disabled', 'disablepictureinpicture', 'disableremoteplayback', 'download', 'draggable', 'enctype', 'enterkeyhint', 'exportparts', 'face', 'for', 'headers', 'height', 'hidden', 'high', 'href', 'hreflang', 'id', 'inert', 'inputmode', 'integrity', 'ismap', 'kind', 'label', 'lang', 'list', 'loading', 'loop', 'low', 'max', 'maxlength', 'media', 'method', 'min', 'minlength', 'multiple', 'muted', 'name', 'nonce', 'noshade', 'novalidate', 'nowrap', 'open', 'optimum', 'part', 'pattern', 'placeholder', 'playsinline', 'popover', 'popovertarget', 'popovertargetaction', 'poster', 'preload', 'pubdate', 'radiogroup', 'readonly', 'rel', 'required', 'rev', 'reversed', 'role', 'rows', 'rowspan', 'spellcheck', 'scope', 'selected', 'shape', 'size', 'sizes', 'slot', 'span', 'srclang', 'start', 'src', 'srcset', 'step', 'style', 'summary', 'tabindex', 'title', 'translate', 'type', 'usemap', 'valign', 'value', 'width', 'wrap', 'xmlns', 'slot']);\nconst svg = freeze(['accent-height', 'accumulate', 'additive', 'alignment-baseline', 'amplitude', 'ascent', 'attributename', 'attributetype', 'azimuth', 'basefrequency', 'baseline-shift', 'begin', 'bias', 'by', 'class', 'clip', 'clippathunits', 'clip-path', 'clip-rule', 'color', 'color-interpolation', 'color-interpolation-filters', 'color-profile', 'color-rendering', 'cx', 'cy', 'd', 'dx', 'dy', 'diffuseconstant', 'direction', 'display', 'divisor', 'dur', 'edgemode', 'elevation', 'end', 'exponent', 'fill', 'fill-opacity', 'fill-rule', 'filter', 'filterunits', 'flood-color', 'flood-opacity', 'font-family', 'font-size', 'font-size-adjust', 'font-stretch', 'font-style', 'font-variant', 'font-weight', 'fx', 'fy', 'g1', 'g2', 'glyph-name', 'glyphref', 'gradientunits', 'gradienttransform', 'height', 'href', 'id', 'image-rendering', 'in', 'in2', 'intercept', 'k', 'k1', 'k2', 'k3', 'k4', 'kerning', 'keypoints', 'keysplines', 'keytimes', 'lang', 'lengthadjust', 'letter-spacing', 'kernelmatrix', 'kernelunitlength', 'lighting-color', 'local', 'marker-end', 'marker-mid', 'marker-start', 'markerheight', 'markerunits', 'markerwidth', 'maskcontentunits', 'maskunits', 'max', 'mask', 'mask-type', 'media', 'method', 'mode', 'min', 'name', 'numoctaves', 'offset', 'operator', 'opacity', 'order', 'orient', 'orientation', 'origin', 'overflow', 'paint-order', 'path', 'pathlength', 'patterncontentunits', 'patterntransform', 'patternunits', 'points', 'preservealpha', 'preserveaspectratio', 'primitiveunits', 'r', 'rx', 'ry', 'radius', 'refx', 'refy', 'repeatcount', 'repeatdur', 'restart', 'result', 'rotate', 'scale', 'seed', 'shape-rendering', 'slope', 'specularconstant', 'specularexponent', 'spreadmethod', 'startoffset', 'stddeviation', 'stitchtiles', 'stop-color', 'stop-opacity', 'stroke-dasharray', 'stroke-dashoffset', 'stroke-linecap', 'stroke-linejoin', 'stroke-miterlimit', 'stroke-opacity', 'stroke', 'stroke-width', 'style', 'surfacescale', 'systemlanguage', 'tabindex', 'tablevalues', 'targetx', 'targety', 'transform', 'transform-origin', 'text-anchor', 'text-decoration', 'text-rendering', 'textlength', 'type', 'u1', 'u2', 'unicode', 'values', 'viewbox', 'visibility', 'version', 'vert-adv-y', 'vert-origin-x', 'vert-origin-y', 'width', 'word-spacing', 'wrap', 'writing-mode', 'xchannelselector', 'ychannelselector', 'x', 'x1', 'x2', 'xmlns', 'y', 'y1', 'y2', 'z', 'zoomandpan']);\nconst mathMl = freeze(['accent', 'accentunder', 'align', 'bevelled', 'close', 'columnsalign', 'columnlines', 'columnspan', 'denomalign', 'depth', 'dir', 'display', 'displaystyle', 'encoding', 'fence', 'frame', 'height', 'href', 'id', 'largeop', 'length', 'linethickness', 'lspace', 'lquote', 'mathbackground', 'mathcolor', 'mathsize', 'mathvariant', 'maxsize', 'minsize', 'movablelimits', 'notation', 'numalign', 'open', 'rowalign', 'rowlines', 'rowspacing', 'rowspan', 'rspace', 'rquote', 'scriptlevel', 'scriptminsize', 'scriptsizemultiplier', 'selection', 'separator', 'separators', 'stretchy', 'subscriptshift', 'supscriptshift', 'symmetric', 'voffset', 'width', 'xmlns']);\nconst xml = freeze(['xlink:href', 'xml:id', 'xlink:title', 'xml:space', 'xmlns:xlink']);\n\n// eslint-disable-next-line unicorn/better-regex\nconst MUSTACHE_EXPR = seal(/\\{\\{[\\w\\W]*|[\\w\\W]*\\}\\}/gm); // Specify template detection regex for SAFE_FOR_TEMPLATES mode\nconst ERB_EXPR = seal(/<%[\\w\\W]*|[\\w\\W]*%>/gm);\nconst TMPLIT_EXPR = seal(/\\$\\{[\\w\\W]*/gm); // eslint-disable-line unicorn/better-regex\nconst DATA_ATTR = seal(/^data-[\\-\\w.\\u00B7-\\uFFFF]+$/); // eslint-disable-line no-useless-escape\nconst ARIA_ATTR = seal(/^aria-[\\-\\w]+$/); // eslint-disable-line no-useless-escape\nconst IS_ALLOWED_URI = seal(/^(?:(?:(?:f|ht)tps?|mailto|tel|callto|sms|cid|xmpp|matrix):|[^a-z]|[a-z+.\\-]+(?:[^a-z+.\\-:]|$))/i // eslint-disable-line no-useless-escape\n);\nconst IS_SCRIPT_OR_DATA = seal(/^(?:\\w+script|data):/i);\nconst ATTR_WHITESPACE = seal(/[\\u0000-\\u0020\\u00A0\\u1680\\u180E\\u2000-\\u2029\\u205F\\u3000]/g // eslint-disable-line no-control-regex\n);\nconst DOCTYPE_NAME = seal(/^html$/i);\nconst CUSTOM_ELEMENT = seal(/^[a-z][.\\w]*(-[.\\w]+)+$/i);\n\nvar EXPRESSIONS = /*#__PURE__*/Object.freeze({\n __proto__: null,\n ARIA_ATTR: ARIA_ATTR,\n ATTR_WHITESPACE: ATTR_WHITESPACE,\n CUSTOM_ELEMENT: CUSTOM_ELEMENT,\n DATA_ATTR: DATA_ATTR,\n DOCTYPE_NAME: DOCTYPE_NAME,\n ERB_EXPR: ERB_EXPR,\n IS_ALLOWED_URI: IS_ALLOWED_URI,\n IS_SCRIPT_OR_DATA: IS_SCRIPT_OR_DATA,\n MUSTACHE_EXPR: MUSTACHE_EXPR,\n TMPLIT_EXPR: TMPLIT_EXPR\n});\n\n/* eslint-disable @typescript-eslint/indent */\n// https://developer.mozilla.org/en-US/docs/Web/API/Node/nodeType\nconst NODE_TYPE = {\n element: 1,\n attribute: 2,\n text: 3,\n cdataSection: 4,\n entityReference: 5,\n // Deprecated\n entityNode: 6,\n // Deprecated\n progressingInstruction: 7,\n comment: 8,\n document: 9,\n documentType: 10,\n documentFragment: 11,\n notation: 12 // Deprecated\n};\nconst getGlobal = function getGlobal() {\n return typeof window === 'undefined' ? null : window;\n};\n/**\n * Creates a no-op policy for internal use only.\n * Don't export this function outside this module!\n * @param trustedTypes The policy factory.\n * @param purifyHostElement The Script element used to load DOMPurify (to determine policy name suffix).\n * @return The policy created (or null, if Trusted Types\n * are not supported or creating the policy failed).\n */\nconst _createTrustedTypesPolicy = function _createTrustedTypesPolicy(trustedTypes, purifyHostElement) {\n if (typeof trustedTypes !== 'object' || typeof trustedTypes.createPolicy !== 'function') {\n return null;\n }\n // Allow the callers to control the unique policy name\n // by adding a data-tt-policy-suffix to the script element with the DOMPurify.\n // Policy creation with duplicate names throws in Trusted Types.\n let suffix = null;\n const ATTR_NAME = 'data-tt-policy-suffix';\n if (purifyHostElement && purifyHostElement.hasAttribute(ATTR_NAME)) {\n suffix = purifyHostElement.getAttribute(ATTR_NAME);\n }\n const policyName = 'dompurify' + (suffix ? '#' + suffix : '');\n try {\n return trustedTypes.createPolicy(policyName, {\n createHTML(html) {\n return html;\n },\n createScriptURL(scriptUrl) {\n return scriptUrl;\n }\n });\n } catch (_) {\n // Policy creation failed (most likely another DOMPurify script has\n // already run). Skip creating the policy, as this will only cause errors\n // if TT are enforced.\n console.warn('TrustedTypes policy ' + policyName + ' could not be created.');\n return null;\n }\n};\nconst _createHooksMap = function _createHooksMap() {\n return {\n afterSanitizeAttributes: [],\n afterSanitizeElements: [],\n afterSanitizeShadowDOM: [],\n beforeSanitizeAttributes: [],\n beforeSanitizeElements: [],\n beforeSanitizeShadowDOM: [],\n uponSanitizeAttribute: [],\n uponSanitizeElement: [],\n uponSanitizeShadowNode: []\n };\n};\nfunction createDOMPurify() {\n let window = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : getGlobal();\n const DOMPurify = root => createDOMPurify(root);\n DOMPurify.version = '3.3.1';\n DOMPurify.removed = [];\n if (!window || !window.document || window.document.nodeType !== NODE_TYPE.document || !window.Element) {\n // Not running in a browser, provide a factory function\n // so that you can pass your own Window\n DOMPurify.isSupported = false;\n return DOMPurify;\n }\n let {\n document\n } = window;\n const originalDocument = document;\n const currentScript = originalDocument.currentScript;\n const {\n DocumentFragment,\n HTMLTemplateElement,\n Node,\n Element,\n NodeFilter,\n NamedNodeMap = window.NamedNodeMap || window.MozNamedAttrMap,\n HTMLFormElement,\n DOMParser,\n trustedTypes\n } = window;\n const ElementPrototype = Element.prototype;\n const cloneNode = lookupGetter(ElementPrototype, 'cloneNode');\n const remove = lookupGetter(ElementPrototype, 'remove');\n const getNextSibling = lookupGetter(ElementPrototype, 'nextSibling');\n const getChildNodes = lookupGetter(ElementPrototype, 'childNodes');\n const getParentNode = lookupGetter(ElementPrototype, 'parentNode');\n // As per issue #47, the web-components registry is inherited by a\n // new document created via createHTMLDocument. As per the spec\n // (http://w3c.github.io/webcomponents/spec/custom/#creating-and-passing-registries)\n // a new empty registry is used when creating a template contents owner\n // document, so we use that as our parent document to ensure nothing\n // is inherited.\n if (typeof HTMLTemplateElement === 'function') {\n const template = document.createElement('template');\n if (template.content && template.content.ownerDocument) {\n document = template.content.ownerDocument;\n }\n }\n let trustedTypesPolicy;\n let emptyHTML = '';\n const {\n implementation,\n createNodeIterator,\n createDocumentFragment,\n getElementsByTagName\n } = document;\n const {\n importNode\n } = originalDocument;\n let hooks = _createHooksMap();\n /**\n * Expose whether this browser supports running the full DOMPurify.\n */\n DOMPurify.isSupported = typeof entries === 'function' && typeof getParentNode === 'function' && implementation && implementation.createHTMLDocument !== undefined;\n const {\n MUSTACHE_EXPR,\n ERB_EXPR,\n TMPLIT_EXPR,\n DATA_ATTR,\n ARIA_ATTR,\n IS_SCRIPT_OR_DATA,\n ATTR_WHITESPACE,\n CUSTOM_ELEMENT\n } = EXPRESSIONS;\n let {\n IS_ALLOWED_URI: IS_ALLOWED_URI$1\n } = EXPRESSIONS;\n /**\n * We consider the elements and attributes below to be safe. Ideally\n * don't add any new ones but feel free to remove unwanted ones.\n */\n /* allowed element names */\n let ALLOWED_TAGS = null;\n const DEFAULT_ALLOWED_TAGS = addToSet({}, [...html$1, ...svg$1, ...svgFilters, ...mathMl$1, ...text]);\n /* Allowed attribute names */\n let ALLOWED_ATTR = null;\n const DEFAULT_ALLOWED_ATTR = addToSet({}, [...html, ...svg, ...mathMl, ...xml]);\n /*\n * Configure how DOMPurify should handle custom elements and their attributes as well as customized built-in elements.\n * @property {RegExp|Function|null} tagNameCheck one of [null, regexPattern, predicate]. Default: `null` (disallow any custom elements)\n * @property {RegExp|Function|null} attributeNameCheck one of [null, regexPattern, predicate]. Default: `null` (disallow any attributes not on the allow list)\n * @property {boolean} allowCustomizedBuiltInElements allow custom elements derived from built-ins if they pass CUSTOM_ELEMENT_HANDLING.tagNameCheck. Default: `false`.\n */\n let CUSTOM_ELEMENT_HANDLING = Object.seal(create(null, {\n tagNameCheck: {\n writable: true,\n configurable: false,\n enumerable: true,\n value: null\n },\n attributeNameCheck: {\n writable: true,\n configurable: false,\n enumerable: true,\n value: null\n },\n allowCustomizedBuiltInElements: {\n writable: true,\n configurable: false,\n enumerable: true,\n value: false\n }\n }));\n /* Explicitly forbidden tags (overrides ALLOWED_TAGS/ADD_TAGS) */\n let FORBID_TAGS = null;\n /* Explicitly forbidden attributes (overrides ALLOWED_ATTR/ADD_ATTR) */\n let FORBID_ATTR = null;\n /* Config object to store ADD_TAGS/ADD_ATTR functions (when used as functions) */\n const EXTRA_ELEMENT_HANDLING = Object.seal(create(null, {\n tagCheck: {\n writable: true,\n configurable: false,\n enumerable: true,\n value: null\n },\n attributeCheck: {\n writable: true,\n configurable: false,\n enumerable: true,\n value: null\n }\n }));\n /* Decide if ARIA attributes are okay */\n let ALLOW_ARIA_ATTR = true;\n /* Decide if custom data attributes are okay */\n let ALLOW_DATA_ATTR = true;\n /* Decide if unknown protocols are okay */\n let ALLOW_UNKNOWN_PROTOCOLS = false;\n /* Decide if self-closing tags in attributes are allowed.\n * Usually removed due to a mXSS issue in jQuery 3.0 */\n let ALLOW_SELF_CLOSE_IN_ATTR = true;\n /* Output should be safe for common template engines.\n * This means, DOMPurify removes data attributes, mustaches and ERB\n */\n let SAFE_FOR_TEMPLATES = false;\n /* Output should be safe even for XML used within HTML and alike.\n * This means, DOMPurify removes comments when containing risky content.\n */\n let SAFE_FOR_XML = true;\n /* Decide if document with ... should be returned */\n let WHOLE_DOCUMENT = false;\n /* Track whether config is already set on this instance of DOMPurify. */\n let SET_CONFIG = false;\n /* Decide if all elements (e.g. style, script) must be children of\n * document.body. By default, browsers might move them to document.head */\n let FORCE_BODY = false;\n /* Decide if a DOM `HTMLBodyElement` should be returned, instead of a html\n * string (or a TrustedHTML object if Trusted Types are supported).\n * If `WHOLE_DOCUMENT` is enabled a `HTMLHtmlElement` will be returned instead\n */\n let RETURN_DOM = false;\n /* Decide if a DOM `DocumentFragment` should be returned, instead of a html\n * string (or a TrustedHTML object if Trusted Types are supported) */\n let RETURN_DOM_FRAGMENT = false;\n /* Try to return a Trusted Type object instead of a string, return a string in\n * case Trusted Types are not supported */\n let RETURN_TRUSTED_TYPE = false;\n /* Output should be free from DOM clobbering attacks?\n * This sanitizes markups named with colliding, clobberable built-in DOM APIs.\n */\n let SANITIZE_DOM = true;\n /* Achieve full DOM Clobbering protection by isolating the namespace of named\n * properties and JS variables, mitigating attacks that abuse the HTML/DOM spec rules.\n *\n * HTML/DOM spec rules that enable DOM Clobbering:\n * - Named Access on Window (§7.3.3)\n * - DOM Tree Accessors (§3.1.5)\n * - Form Element Parent-Child Relations (§4.10.3)\n * - Iframe srcdoc / Nested WindowProxies (§4.8.5)\n * - HTMLCollection (§4.2.10.2)\n *\n * Namespace isolation is implemented by prefixing `id` and `name` attributes\n * with a constant string, i.e., `user-content-`\n */\n let SANITIZE_NAMED_PROPS = false;\n const SANITIZE_NAMED_PROPS_PREFIX = 'user-content-';\n /* Keep element content when removing element? */\n let KEEP_CONTENT = true;\n /* If a `Node` is passed to sanitize(), then performs sanitization in-place instead\n * of importing it into a new Document and returning a sanitized copy */\n let IN_PLACE = false;\n /* Allow usage of profiles like html, svg and mathMl */\n let USE_PROFILES = {};\n /* Tags to ignore content of when KEEP_CONTENT is true */\n let FORBID_CONTENTS = null;\n const DEFAULT_FORBID_CONTENTS = addToSet({}, ['annotation-xml', 'audio', 'colgroup', 'desc', 'foreignobject', 'head', 'iframe', 'math', 'mi', 'mn', 'mo', 'ms', 'mtext', 'noembed', 'noframes', 'noscript', 'plaintext', 'script', 'style', 'svg', 'template', 'thead', 'title', 'video', 'xmp']);\n /* Tags that are safe for data: URIs */\n let DATA_URI_TAGS = null;\n const DEFAULT_DATA_URI_TAGS = addToSet({}, ['audio', 'video', 'img', 'source', 'image', 'track']);\n /* Attributes safe for values like \"javascript:\" */\n let URI_SAFE_ATTRIBUTES = null;\n const DEFAULT_URI_SAFE_ATTRIBUTES = addToSet({}, ['alt', 'class', 'for', 'id', 'label', 'name', 'pattern', 'placeholder', 'role', 'summary', 'title', 'value', 'style', 'xmlns']);\n const MATHML_NAMESPACE = 'http://www.w3.org/1998/Math/MathML';\n const SVG_NAMESPACE = 'http://www.w3.org/2000/svg';\n const HTML_NAMESPACE = 'http://www.w3.org/1999/xhtml';\n /* Document namespace */\n let NAMESPACE = HTML_NAMESPACE;\n let IS_EMPTY_INPUT = false;\n /* Allowed XHTML+XML namespaces */\n let ALLOWED_NAMESPACES = null;\n const DEFAULT_ALLOWED_NAMESPACES = addToSet({}, [MATHML_NAMESPACE, SVG_NAMESPACE, HTML_NAMESPACE], stringToString);\n let MATHML_TEXT_INTEGRATION_POINTS = addToSet({}, ['mi', 'mo', 'mn', 'ms', 'mtext']);\n let HTML_INTEGRATION_POINTS = addToSet({}, ['annotation-xml']);\n // Certain elements are allowed in both SVG and HTML\n // namespace. We need to specify them explicitly\n // so that they don't get erroneously deleted from\n // HTML namespace.\n const COMMON_SVG_AND_HTML_ELEMENTS = addToSet({}, ['title', 'style', 'font', 'a', 'script']);\n /* Parsing of strict XHTML documents */\n let PARSER_MEDIA_TYPE = null;\n const SUPPORTED_PARSER_MEDIA_TYPES = ['application/xhtml+xml', 'text/html'];\n const DEFAULT_PARSER_MEDIA_TYPE = 'text/html';\n let transformCaseFunc = null;\n /* Keep a reference to config to pass to hooks */\n let CONFIG = null;\n /* Ideally, do not touch anything below this line */\n /* ______________________________________________ */\n const formElement = document.createElement('form');\n const isRegexOrFunction = function isRegexOrFunction(testValue) {\n return testValue instanceof RegExp || testValue instanceof Function;\n };\n /**\n * _parseConfig\n *\n * @param cfg optional config literal\n */\n // eslint-disable-next-line complexity\n const _parseConfig = function _parseConfig() {\n let cfg = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {};\n if (CONFIG && CONFIG === cfg) {\n return;\n }\n /* Shield configuration object from tampering */\n if (!cfg || typeof cfg !== 'object') {\n cfg = {};\n }\n /* Shield configuration object from prototype pollution */\n cfg = clone(cfg);\n PARSER_MEDIA_TYPE =\n // eslint-disable-next-line unicorn/prefer-includes\n SUPPORTED_PARSER_MEDIA_TYPES.indexOf(cfg.PARSER_MEDIA_TYPE) === -1 ? DEFAULT_PARSER_MEDIA_TYPE : cfg.PARSER_MEDIA_TYPE;\n // HTML tags and attributes are not case-sensitive, converting to lowercase. Keeping XHTML as is.\n transformCaseFunc = PARSER_MEDIA_TYPE === 'application/xhtml+xml' ? stringToString : stringToLowerCase;\n /* Set configuration parameters */\n ALLOWED_TAGS = objectHasOwnProperty(cfg, 'ALLOWED_TAGS') ? addToSet({}, cfg.ALLOWED_TAGS, transformCaseFunc) : DEFAULT_ALLOWED_TAGS;\n ALLOWED_ATTR = objectHasOwnProperty(cfg, 'ALLOWED_ATTR') ? addToSet({}, cfg.ALLOWED_ATTR, transformCaseFunc) : DEFAULT_ALLOWED_ATTR;\n ALLOWED_NAMESPACES = objectHasOwnProperty(cfg, 'ALLOWED_NAMESPACES') ? addToSet({}, cfg.ALLOWED_NAMESPACES, stringToString) : DEFAULT_ALLOWED_NAMESPACES;\n URI_SAFE_ATTRIBUTES = objectHasOwnProperty(cfg, 'ADD_URI_SAFE_ATTR') ? addToSet(clone(DEFAULT_URI_SAFE_ATTRIBUTES), cfg.ADD_URI_SAFE_ATTR, transformCaseFunc) : DEFAULT_URI_SAFE_ATTRIBUTES;\n DATA_URI_TAGS = objectHasOwnProperty(cfg, 'ADD_DATA_URI_TAGS') ? addToSet(clone(DEFAULT_DATA_URI_TAGS), cfg.ADD_DATA_URI_TAGS, transformCaseFunc) : DEFAULT_DATA_URI_TAGS;\n FORBID_CONTENTS = objectHasOwnProperty(cfg, 'FORBID_CONTENTS') ? addToSet({}, cfg.FORBID_CONTENTS, transformCaseFunc) : DEFAULT_FORBID_CONTENTS;\n FORBID_TAGS = objectHasOwnProperty(cfg, 'FORBID_TAGS') ? addToSet({}, cfg.FORBID_TAGS, transformCaseFunc) : clone({});\n FORBID_ATTR = objectHasOwnProperty(cfg, 'FORBID_ATTR') ? addToSet({}, cfg.FORBID_ATTR, transformCaseFunc) : clone({});\n USE_PROFILES = objectHasOwnProperty(cfg, 'USE_PROFILES') ? cfg.USE_PROFILES : false;\n ALLOW_ARIA_ATTR = cfg.ALLOW_ARIA_ATTR !== false; // Default true\n ALLOW_DATA_ATTR = cfg.ALLOW_DATA_ATTR !== false; // Default true\n ALLOW_UNKNOWN_PROTOCOLS = cfg.ALLOW_UNKNOWN_PROTOCOLS || false; // Default false\n ALLOW_SELF_CLOSE_IN_ATTR = cfg.ALLOW_SELF_CLOSE_IN_ATTR !== false; // Default true\n SAFE_FOR_TEMPLATES = cfg.SAFE_FOR_TEMPLATES || false; // Default false\n SAFE_FOR_XML = cfg.SAFE_FOR_XML !== false; // Default true\n WHOLE_DOCUMENT = cfg.WHOLE_DOCUMENT || false; // Default false\n RETURN_DOM = cfg.RETURN_DOM || false; // Default false\n RETURN_DOM_FRAGMENT = cfg.RETURN_DOM_FRAGMENT || false; // Default false\n RETURN_TRUSTED_TYPE = cfg.RETURN_TRUSTED_TYPE || false; // Default false\n FORCE_BODY = cfg.FORCE_BODY || false; // Default false\n SANITIZE_DOM = cfg.SANITIZE_DOM !== false; // Default true\n SANITIZE_NAMED_PROPS = cfg.SANITIZE_NAMED_PROPS || false; // Default false\n KEEP_CONTENT = cfg.KEEP_CONTENT !== false; // Default true\n IN_PLACE = cfg.IN_PLACE || false; // Default false\n IS_ALLOWED_URI$1 = cfg.ALLOWED_URI_REGEXP || IS_ALLOWED_URI;\n NAMESPACE = cfg.NAMESPACE || HTML_NAMESPACE;\n MATHML_TEXT_INTEGRATION_POINTS = cfg.MATHML_TEXT_INTEGRATION_POINTS || MATHML_TEXT_INTEGRATION_POINTS;\n HTML_INTEGRATION_POINTS = cfg.HTML_INTEGRATION_POINTS || HTML_INTEGRATION_POINTS;\n CUSTOM_ELEMENT_HANDLING = cfg.CUSTOM_ELEMENT_HANDLING || {};\n if (cfg.CUSTOM_ELEMENT_HANDLING && isRegexOrFunction(cfg.CUSTOM_ELEMENT_HANDLING.tagNameCheck)) {\n CUSTOM_ELEMENT_HANDLING.tagNameCheck = cfg.CUSTOM_ELEMENT_HANDLING.tagNameCheck;\n }\n if (cfg.CUSTOM_ELEMENT_HANDLING && isRegexOrFunction(cfg.CUSTOM_ELEMENT_HANDLING.attributeNameCheck)) {\n CUSTOM_ELEMENT_HANDLING.attributeNameCheck = cfg.CUSTOM_ELEMENT_HANDLING.attributeNameCheck;\n }\n if (cfg.CUSTOM_ELEMENT_HANDLING && typeof cfg.CUSTOM_ELEMENT_HANDLING.allowCustomizedBuiltInElements === 'boolean') {\n CUSTOM_ELEMENT_HANDLING.allowCustomizedBuiltInElements = cfg.CUSTOM_ELEMENT_HANDLING.allowCustomizedBuiltInElements;\n }\n if (SAFE_FOR_TEMPLATES) {\n ALLOW_DATA_ATTR = false;\n }\n if (RETURN_DOM_FRAGMENT) {\n RETURN_DOM = true;\n }\n /* Parse profile info */\n if (USE_PROFILES) {\n ALLOWED_TAGS = addToSet({}, text);\n ALLOWED_ATTR = [];\n if (USE_PROFILES.html === true) {\n addToSet(ALLOWED_TAGS, html$1);\n addToSet(ALLOWED_ATTR, html);\n }\n if (USE_PROFILES.svg === true) {\n addToSet(ALLOWED_TAGS, svg$1);\n addToSet(ALLOWED_ATTR, svg);\n addToSet(ALLOWED_ATTR, xml);\n }\n if (USE_PROFILES.svgFilters === true) {\n addToSet(ALLOWED_TAGS, svgFilters);\n addToSet(ALLOWED_ATTR, svg);\n addToSet(ALLOWED_ATTR, xml);\n }\n if (USE_PROFILES.mathMl === true) {\n addToSet(ALLOWED_TAGS, mathMl$1);\n addToSet(ALLOWED_ATTR, mathMl);\n addToSet(ALLOWED_ATTR, xml);\n }\n }\n /* Merge configuration parameters */\n if (cfg.ADD_TAGS) {\n if (typeof cfg.ADD_TAGS === 'function') {\n EXTRA_ELEMENT_HANDLING.tagCheck = cfg.ADD_TAGS;\n } else {\n if (ALLOWED_TAGS === DEFAULT_ALLOWED_TAGS) {\n ALLOWED_TAGS = clone(ALLOWED_TAGS);\n }\n addToSet(ALLOWED_TAGS, cfg.ADD_TAGS, transformCaseFunc);\n }\n }\n if (cfg.ADD_ATTR) {\n if (typeof cfg.ADD_ATTR === 'function') {\n EXTRA_ELEMENT_HANDLING.attributeCheck = cfg.ADD_ATTR;\n } else {\n if (ALLOWED_ATTR === DEFAULT_ALLOWED_ATTR) {\n ALLOWED_ATTR = clone(ALLOWED_ATTR);\n }\n addToSet(ALLOWED_ATTR, cfg.ADD_ATTR, transformCaseFunc);\n }\n }\n if (cfg.ADD_URI_SAFE_ATTR) {\n addToSet(URI_SAFE_ATTRIBUTES, cfg.ADD_URI_SAFE_ATTR, transformCaseFunc);\n }\n if (cfg.FORBID_CONTENTS) {\n if (FORBID_CONTENTS === DEFAULT_FORBID_CONTENTS) {\n FORBID_CONTENTS = clone(FORBID_CONTENTS);\n }\n addToSet(FORBID_CONTENTS, cfg.FORBID_CONTENTS, transformCaseFunc);\n }\n if (cfg.ADD_FORBID_CONTENTS) {\n if (FORBID_CONTENTS === DEFAULT_FORBID_CONTENTS) {\n FORBID_CONTENTS = clone(FORBID_CONTENTS);\n }\n addToSet(FORBID_CONTENTS, cfg.ADD_FORBID_CONTENTS, transformCaseFunc);\n }\n /* Add #text in case KEEP_CONTENT is set to true */\n if (KEEP_CONTENT) {\n ALLOWED_TAGS['#text'] = true;\n }\n /* Add html, head and body to ALLOWED_TAGS in case WHOLE_DOCUMENT is true */\n if (WHOLE_DOCUMENT) {\n addToSet(ALLOWED_TAGS, ['html', 'head', 'body']);\n }\n /* Add tbody to ALLOWED_TAGS in case tables are permitted, see #286, #365 */\n if (ALLOWED_TAGS.table) {\n addToSet(ALLOWED_TAGS, ['tbody']);\n delete FORBID_TAGS.tbody;\n }\n if (cfg.TRUSTED_TYPES_POLICY) {\n if (typeof cfg.TRUSTED_TYPES_POLICY.createHTML !== 'function') {\n throw typeErrorCreate('TRUSTED_TYPES_POLICY configuration option must provide a \"createHTML\" hook.');\n }\n if (typeof cfg.TRUSTED_TYPES_POLICY.createScriptURL !== 'function') {\n throw typeErrorCreate('TRUSTED_TYPES_POLICY configuration option must provide a \"createScriptURL\" hook.');\n }\n // Overwrite existing TrustedTypes policy.\n trustedTypesPolicy = cfg.TRUSTED_TYPES_POLICY;\n // Sign local variables required by `sanitize`.\n emptyHTML = trustedTypesPolicy.createHTML('');\n } else {\n // Uninitialized policy, attempt to initialize the internal dompurify policy.\n if (trustedTypesPolicy === undefined) {\n trustedTypesPolicy = _createTrustedTypesPolicy(trustedTypes, currentScript);\n }\n // If creating the internal policy succeeded sign internal variables.\n if (trustedTypesPolicy !== null && typeof emptyHTML === 'string') {\n emptyHTML = trustedTypesPolicy.createHTML('');\n }\n }\n // Prevent further manipulation of configuration.\n // Not available in IE8, Safari 5, etc.\n if (freeze) {\n freeze(cfg);\n }\n CONFIG = cfg;\n };\n /* Keep track of all possible SVG and MathML tags\n * so that we can perform the namespace checks\n * correctly. */\n const ALL_SVG_TAGS = addToSet({}, [...svg$1, ...svgFilters, ...svgDisallowed]);\n const ALL_MATHML_TAGS = addToSet({}, [...mathMl$1, ...mathMlDisallowed]);\n /**\n * @param element a DOM element whose namespace is being checked\n * @returns Return false if the element has a\n * namespace that a spec-compliant parser would never\n * return. Return true otherwise.\n */\n const _checkValidNamespace = function _checkValidNamespace(element) {\n let parent = getParentNode(element);\n // In JSDOM, if we're inside shadow DOM, then parentNode\n // can be null. We just simulate parent in this case.\n if (!parent || !parent.tagName) {\n parent = {\n namespaceURI: NAMESPACE,\n tagName: 'template'\n };\n }\n const tagName = stringToLowerCase(element.tagName);\n const parentTagName = stringToLowerCase(parent.tagName);\n if (!ALLOWED_NAMESPACES[element.namespaceURI]) {\n return false;\n }\n if (element.namespaceURI === SVG_NAMESPACE) {\n // The only way to switch from HTML namespace to SVG\n // is via