diff --git a/CHANGELOG.md b/CHANGELOG.md index 4baa46ea4..3658404b3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,7 @@ Docs: https://docs.clawd.bot - 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. diff --git a/docs/gateway/configuration.md b/docs/gateway/configuration.md index 6f2b064bf..a6e190d9e 100644 --- a/docs/gateway/configuration.md +++ b/docs/gateway/configuration.md @@ -2654,7 +2654,13 @@ If unset, clients fall back to a muted light-blue. ```json5 { ui: { - seamColor: "#FF4500" // hex (RRGGBB or #RRGGBB) + seamColor: "#FF4500", // hex (RRGGBB or #RRGGBB) + // Optional: Control UI assistant identity override. + // If unset, the Control UI uses the active agent identity (config or IDENTITY.md). + assistant: { + name: "Clawdbot", + avatar: "CB" // emoji, short text, or image URL/data URI + } } } ``` diff --git a/src/agents/bash-tools.exec.ts b/src/agents/bash-tools.exec.ts index 1ddcbc913..79082e321 100644 --- a/src/agents/bash-tools.exec.ts +++ b/src/agents/bash-tools.exec.ts @@ -756,6 +756,8 @@ export function createExecTool( .join("\n"), ); } + } + if (elevatedRequested) { logInfo(`exec: elevated command ${truncateMiddle(params.command, 120)}`); } const configuredHost = defaults?.host ?? "sandbox"; diff --git a/src/config/schema.ts b/src/config/schema.ts index 3bb39674d..5404e6538 100644 --- a/src/config/schema.ts +++ b/src/config/schema.ts @@ -261,6 +261,8 @@ const FIELD_LABELS: Record = { "commands.restart": "Allow Restart", "commands.useAccessGroups": "Use Access Groups", "ui.seamColor": "Accent Color", + "ui.assistant.name": "Assistant Name", + "ui.assistant.avatar": "Assistant Avatar", "browser.controlUrl": "Browser Control URL", "browser.snapshotDefaults": "Browser Snapshot Defaults", "browser.snapshotDefaults.mode": "Browser Snapshot Mode", diff --git a/src/config/types.clawdbot.ts b/src/config/types.clawdbot.ts index d80b74284..102febcf2 100644 --- a/src/config/types.clawdbot.ts +++ b/src/config/types.clawdbot.ts @@ -65,6 +65,12 @@ export type ClawdbotConfig = { ui?: { /** Accent color for Clawdbot UI chrome (hex). */ seamColor?: string; + assistant?: { + /** Assistant display name for UI surfaces. */ + name?: string; + /** Assistant avatar (emoji, short text, or image URL/data URI). */ + avatar?: string; + }; }; skills?: SkillsConfig; plugins?: PluginsConfig; diff --git a/src/gateway/assistant-identity.ts b/src/gateway/assistant-identity.ts new file mode 100644 index 000000000..72f521d12 --- /dev/null +++ b/src/gateway/assistant-identity.ts @@ -0,0 +1,55 @@ +import { resolveAgentWorkspaceDir, resolveDefaultAgentId } from "../agents/agent-scope.js"; +import { resolveAgentIdentity } from "../agents/identity.js"; +import { loadAgentIdentity } from "../commands/agents.config.js"; +import type { ClawdbotConfig } from "../config/config.js"; +import { normalizeAgentId } from "../routing/session-key.js"; + +const MAX_ASSISTANT_NAME = 50; +const MAX_ASSISTANT_AVATAR = 200; + +export const DEFAULT_ASSISTANT_IDENTITY = { + name: "Assistant", + avatar: "A", +}; + +export type AssistantIdentity = { + agentId: string; + name: string; + avatar: string; +}; + +function coerceIdentityValue(value: string | undefined, maxLength: number): string | undefined { + if (typeof value !== "string") return undefined; + const trimmed = value.trim(); + if (!trimmed) return undefined; + if (trimmed.length <= maxLength) return trimmed; + return trimmed.slice(0, maxLength); +} + +export function resolveAssistantIdentity(params: { + cfg: ClawdbotConfig; + agentId?: string | null; + workspaceDir?: string | null; +}): AssistantIdentity { + const agentId = normalizeAgentId(params.agentId ?? resolveDefaultAgentId(params.cfg)); + const workspaceDir = params.workspaceDir ?? resolveAgentWorkspaceDir(params.cfg, agentId); + const configAssistant = params.cfg.ui?.assistant; + const agentIdentity = resolveAgentIdentity(params.cfg, agentId); + const fileIdentity = workspaceDir ? loadAgentIdentity(workspaceDir) : null; + + const name = + coerceIdentityValue(configAssistant?.name, MAX_ASSISTANT_NAME) ?? + coerceIdentityValue(agentIdentity?.name, MAX_ASSISTANT_NAME) ?? + coerceIdentityValue(fileIdentity?.name, MAX_ASSISTANT_NAME) ?? + DEFAULT_ASSISTANT_IDENTITY.name; + + 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) ?? + DEFAULT_ASSISTANT_IDENTITY.avatar; + + return { agentId, name, avatar }; +} diff --git a/src/gateway/control-ui.ts b/src/gateway/control-ui.ts index 416f803d7..8d2d881b1 100644 --- a/src/gateway/control-ui.ts +++ b/src/gateway/control-ui.ts @@ -3,84 +3,16 @@ import type { IncomingMessage, ServerResponse } from "node:http"; import path from "node:path"; import { fileURLToPath } from "node:url"; +import type { ClawdbotConfig } from "../config/config.js"; +import { DEFAULT_ASSISTANT_IDENTITY, resolveAssistantIdentity } from "./assistant-identity.js"; + const ROOT_PREFIX = "/"; const AVATAR_PREFIX = "/avatar"; -// === Assistant Identity Resolution === - -const DEFAULT_ASSISTANT_IDENTITY = { - name: "Assistant", - avatar: "A", -}; - -interface AssistantIdentity { - name: string; - avatar: string; -} - -interface AssistantConfig { - name?: string; - avatar?: string; -} - -function parseIdentityMd(content: string): Partial { - const result: Partial = {}; - const nameMatch = content.match(/^-\s*Name:\s*(.+)$/im); - if (nameMatch?.[1]) { - const name = nameMatch[1].trim(); - if (name && name.length <= 50) { - result.name = name; - } - } - const emojiMatch = content.match(/^-\s*Emoji:\s*(.+)$/im); - if (emojiMatch?.[1]) { - const emoji = emojiMatch[1].trim(); - if (emoji) { - result.avatar = emoji; - } - } - return result; -} - -function resolveAssistantIdentity(opts?: { - configAssistant?: AssistantConfig; - workspaceDir?: string; -}): AssistantIdentity { - const { configAssistant, workspaceDir } = opts ?? {}; - let name = DEFAULT_ASSISTANT_IDENTITY.name; - let avatar = DEFAULT_ASSISTANT_IDENTITY.avatar; - - // Try IDENTITY.md from workspace - if (workspaceDir) { - try { - const identityPath = path.join(workspaceDir, "IDENTITY.md"); - if (fs.existsSync(identityPath)) { - const identityMd = parseIdentityMd(fs.readFileSync(identityPath, "utf8")); - if (identityMd.name) name = identityMd.name; - if (identityMd.avatar) avatar = identityMd.avatar; - } - } catch { - // Ignore errors reading IDENTITY.md - } - } - - // Config overrides IDENTITY.md - if (configAssistant?.name?.trim()) name = configAssistant.name.trim(); - if (configAssistant?.avatar?.trim()) avatar = configAssistant.avatar.trim(); - - return { name, avatar }; -} - -// === End Assistant Identity === - export type ControlUiRequestOptions = { basePath?: string; - config?: { - ui?: { - assistant?: AssistantConfig; - }; - }; - workspaceDir?: string; + config?: ClawdbotConfig; + agentId?: string; }; export function normalizeControlUiBasePath(basePath?: string): string { @@ -252,8 +184,12 @@ function injectControlUiConfig(html: string, opts: ControlUiInjectionOpts): stri const script = ``; // Check if already injected if (html.includes("__CLAWDBOT_ASSISTANT_NAME__")) return html; @@ -266,16 +202,15 @@ function injectControlUiConfig(html: string, opts: ControlUiInjectionOpts): stri interface ServeIndexHtmlOpts { basePath: string; - config?: ControlUiRequestOptions["config"]; - workspaceDir?: string; + config?: ClawdbotConfig; + agentId?: string; } function serveIndexHtml(res: ServerResponse, indexPath: string, opts: ServeIndexHtmlOpts) { - const { basePath, config, workspaceDir } = opts; - const identity = resolveAssistantIdentity({ - configAssistant: config?.ui?.assistant, - workspaceDir, - }); + const { basePath, config, agentId } = opts; + const identity = config + ? resolveAssistantIdentity({ cfg: config, agentId }) + : DEFAULT_ASSISTANT_IDENTITY; res.setHeader("Content-Type", "text/html; charset=utf-8"); res.setHeader("Cache-Control", "no-cache"); const raw = fs.readFileSync(indexPath, "utf8"); @@ -364,7 +299,11 @@ export function handleControlUiHttpRequest( if (fs.existsSync(filePath) && fs.statSync(filePath).isFile()) { if (path.basename(filePath) === "index.html") { - serveIndexHtml(res, filePath, { basePath, config: opts?.config, workspaceDir: opts?.workspaceDir }); + serveIndexHtml(res, filePath, { + basePath, + config: opts?.config, + agentId: opts?.agentId, + }); return true; } serveFile(res, filePath); @@ -374,7 +313,11 @@ export function handleControlUiHttpRequest( // SPA fallback (client-side router): serve index.html for unknown paths. const indexPath = path.join(root, "index.html"); if (fs.existsSync(indexPath)) { - serveIndexHtml(res, indexPath, { basePath, config: opts?.config, workspaceDir: opts?.workspaceDir }); + serveIndexHtml(res, indexPath, { + basePath, + config: opts?.config, + agentId: opts?.agentId, + }); return true; } diff --git a/src/gateway/protocol/index.ts b/src/gateway/protocol/index.ts index dba040fe5..d1656135c 100644 --- a/src/gateway/protocol/index.ts +++ b/src/gateway/protocol/index.ts @@ -2,6 +2,10 @@ import AjvPkg, { type ErrorObject } from "ajv"; import { type AgentEvent, AgentEventSchema, + type AgentIdentityParams, + AgentIdentityParamsSchema, + type AgentIdentityResult, + AgentIdentityResultSchema, AgentParamsSchema, type AgentSummary, AgentSummarySchema, @@ -198,6 +202,8 @@ export const validateEventFrame = ajv.compile(EventFrameSchema); export const validateSendParams = ajv.compile(SendParamsSchema); export const validatePollParams = ajv.compile(PollParamsSchema); export const validateAgentParams = ajv.compile(AgentParamsSchema); +export const validateAgentIdentityParams = + ajv.compile(AgentIdentityParamsSchema); export const validateAgentWaitParams = ajv.compile(AgentWaitParamsSchema); export const validateWakeParams = ajv.compile(WakeParamsSchema); export const validateAgentsListParams = ajv.compile(AgentsListParamsSchema); @@ -359,6 +365,8 @@ export { SendParamsSchema, PollParamsSchema, AgentParamsSchema, + AgentIdentityParamsSchema, + AgentIdentityResultSchema, WakeParamsSchema, NodePairRequestParamsSchema, NodePairListParamsSchema, @@ -432,6 +440,8 @@ export type { ErrorShape, StateVersion, AgentEvent, + AgentIdentityParams, + AgentIdentityResult, AgentWaitParams, ChatEvent, TickEvent, diff --git a/src/gateway/protocol/schema/agent.ts b/src/gateway/protocol/schema/agent.ts index f31b6cd92..54da9d23c 100644 --- a/src/gateway/protocol/schema/agent.ts +++ b/src/gateway/protocol/schema/agent.ts @@ -68,6 +68,23 @@ export const AgentParamsSchema = Type.Object( { additionalProperties: false }, ); +export const AgentIdentityParamsSchema = Type.Object( + { + agentId: Type.Optional(NonEmptyString), + sessionKey: Type.Optional(Type.String()), + }, + { additionalProperties: false }, +); + +export const AgentIdentityResultSchema = Type.Object( + { + agentId: NonEmptyString, + name: Type.Optional(NonEmptyString), + avatar: Type.Optional(NonEmptyString), + }, + { additionalProperties: false }, +); + export const AgentWaitParamsSchema = Type.Object( { runId: NonEmptyString, diff --git a/src/gateway/protocol/schema/protocol-schemas.ts b/src/gateway/protocol/schema/protocol-schemas.ts index 9a18af061..7e55d2075 100644 --- a/src/gateway/protocol/schema/protocol-schemas.ts +++ b/src/gateway/protocol/schema/protocol-schemas.ts @@ -2,6 +2,8 @@ import type { TSchema } from "@sinclair/typebox"; import { AgentEventSchema, + AgentIdentityParamsSchema, + AgentIdentityResultSchema, AgentParamsSchema, AgentWaitParamsSchema, PollParamsSchema, @@ -136,6 +138,8 @@ export const ProtocolSchemas: Record = { SendParams: SendParamsSchema, PollParams: PollParamsSchema, AgentParams: AgentParamsSchema, + AgentIdentityParams: AgentIdentityParamsSchema, + AgentIdentityResult: AgentIdentityResultSchema, AgentWaitParams: AgentWaitParamsSchema, WakeParams: WakeParamsSchema, NodePairRequestParams: NodePairRequestParamsSchema, diff --git a/src/gateway/protocol/schema/types.ts b/src/gateway/protocol/schema/types.ts index 01de1749f..164d6b902 100644 --- a/src/gateway/protocol/schema/types.ts +++ b/src/gateway/protocol/schema/types.ts @@ -2,6 +2,8 @@ import type { Static } from "@sinclair/typebox"; import type { AgentEventSchema, + AgentIdentityParamsSchema, + AgentIdentityResultSchema, AgentWaitParamsSchema, PollParamsSchema, WakeParamsSchema, @@ -125,6 +127,8 @@ export type PresenceEntry = Static; export type ErrorShape = Static; export type StateVersion = Static; export type AgentEvent = Static; +export type AgentIdentityParams = Static; +export type AgentIdentityResult = Static; export type PollParams = Static; export type AgentWaitParams = Static; export type WakeParams = Static; diff --git a/src/gateway/server-http.ts b/src/gateway/server-http.ts index 2d736e092..49257c61d 100644 --- a/src/gateway/server-http.ts +++ b/src/gateway/server-http.ts @@ -9,6 +9,7 @@ import type { TlsOptions } from "node:tls"; import type { WebSocketServer } from "ws"; import { handleA2uiHttpRequest } from "../canvas-host/a2ui.js"; import type { CanvasHostHandler } from "../canvas-host/server.js"; +import { loadConfig } from "../config/config.js"; import type { createSubsystemLogger } from "../logging/subsystem.js"; import { handleSlackHttpRequest } from "../slack/http/index.js"; import { resolveAgentAvatar } from "../agents/identity-avatar.js"; @@ -256,6 +257,7 @@ export function createGatewayHttpServer(opts: { if ( handleControlUiHttpRequest(req, res, { basePath: controlUiBasePath, + config: loadConfig(), }) ) return; diff --git a/src/gateway/server-methods-list.ts b/src/gateway/server-methods-list.ts index a1aa20cbb..b90643df8 100644 --- a/src/gateway/server-methods-list.ts +++ b/src/gateway/server-methods-list.ts @@ -68,6 +68,7 @@ const BASE_METHODS = [ "system-event", "send", "agent", + "agent.identity.get", "agent.wait", // WebChat WebSocket-native chat methods "chat.history", diff --git a/src/gateway/server-methods.ts b/src/gateway/server-methods.ts index 0759744d8..486bd249c 100644 --- a/src/gateway/server-methods.ts +++ b/src/gateway/server-methods.ts @@ -55,6 +55,7 @@ const READ_METHODS = new Set([ "usage.cost", "models.list", "agents.list", + "agent.identity.get", "skills.status", "voicewake.get", "sessions.list", diff --git a/src/gateway/server-methods/agent.ts b/src/gateway/server-methods/agent.ts index a82e31676..aaeb3c272 100644 --- a/src/gateway/server-methods/agent.ts +++ b/src/gateway/server-methods/agent.ts @@ -26,15 +26,18 @@ import { import { normalizeAgentId } from "../../routing/session-key.js"; import { parseMessageWithAttachments } from "../chat-attachments.js"; import { + type AgentIdentityParams, type AgentWaitParams, ErrorCodes, errorShape, formatValidationErrors, + validateAgentIdentityParams, validateAgentParams, validateAgentWaitParams, } from "../protocol/index.js"; import { loadSessionEntry } from "../session-utils.js"; import { formatForLog } from "../ws-log.js"; +import { resolveAssistantIdentity } from "../assistant-identity.js"; import { waitForAgentJob } from "./agent-job.js"; import type { GatewayRequestHandlers } from "./types.js"; @@ -369,6 +372,43 @@ export const agentHandlers: GatewayRequestHandlers = { }); }); }, + "agent.identity.get": ({ params, respond }) => { + if (!validateAgentIdentityParams(params)) { + respond( + false, + undefined, + errorShape( + ErrorCodes.INVALID_REQUEST, + `invalid agent.identity.get params: ${formatValidationErrors( + validateAgentIdentityParams.errors, + )}`, + ), + ); + return; + } + const p = params as AgentIdentityParams; + const agentIdRaw = typeof p.agentId === "string" ? p.agentId.trim() : ""; + const sessionKeyRaw = typeof p.sessionKey === "string" ? p.sessionKey.trim() : ""; + let agentId = agentIdRaw ? normalizeAgentId(agentIdRaw) : undefined; + if (sessionKeyRaw) { + const resolved = resolveAgentIdFromSessionKey(sessionKeyRaw); + if (agentId && resolved !== agentId) { + respond( + false, + undefined, + errorShape( + ErrorCodes.INVALID_REQUEST, + `invalid agent.identity.get params: agent "${agentIdRaw}" does not match session key agent "${resolved}"`, + ), + ); + return; + } + agentId = resolved; + } + const cfg = loadConfig(); + const identity = resolveAssistantIdentity({ cfg, agentId }); + respond(true, identity, undefined); + }, "agent.wait": async ({ params, respond }) => { if (!validateAgentWaitParams(params)) { respond( diff --git a/ui/src/ui/app-gateway.ts b/ui/src/ui/app-gateway.ts index d5bc0c5d9..e35c5e078 100644 --- a/ui/src/ui/app-gateway.ts +++ b/ui/src/ui/app-gateway.ts @@ -25,6 +25,7 @@ import { } from "./controllers/exec-approval"; import type { ClawdbotApp } from "./app"; import type { ExecApprovalRequest } from "./controllers/exec-approval"; +import { loadAssistantIdentity } from "./controllers/assistant-identity"; type GatewayHost = { settings: UiSettings; @@ -43,6 +44,9 @@ type GatewayHost = { agentsList: AgentsListResult | null; agentsError: string | null; debugHealth: HealthSnapshot | null; + assistantName: string; + assistantAvatar: string | null; + assistantAgentId: string | null; sessionKey: string; chatRunId: string | null; execApprovalQueue: ExecApprovalRequest[]; @@ -121,6 +125,7 @@ export function connectGateway(host: GatewayHost) { host.connected = true; host.hello = hello; applySnapshot(host, hello); + void loadAssistantIdentity(host as unknown as ClawdbotApp); void loadAgents(host as unknown as ClawdbotApp); void loadNodes(host as unknown as ClawdbotApp, { quiet: true }); void loadDevices(host as unknown as ClawdbotApp, { quiet: true }); diff --git a/ui/src/ui/app-render.helpers.ts b/ui/src/ui/app-render.helpers.ts index 7f7ed7db9..e8a93a100 100644 --- a/ui/src/ui/app-render.helpers.ts +++ b/ui/src/ui/app-render.helpers.ts @@ -62,6 +62,7 @@ export function renderChatControls(state: AppViewState) { sessionKey: next, lastActiveSessionKey: next, }); + void state.loadAssistantIdentity(); syncUrlWithSessionKey(state, next, true); void loadChatHistory(state); }} diff --git a/ui/src/ui/app-render.ts b/ui/src/ui/app-render.ts index 37f0b6bea..81176ebc3 100644 --- a/ui/src/ui/app-render.ts +++ b/ui/src/ui/app-render.ts @@ -221,6 +221,7 @@ export function renderApp(state: AppViewState) { sessionKey: next, lastActiveSessionKey: next, }); + void state.loadAssistantIdentity(); }, onConnect: () => state.connect(), onRefresh: () => state.loadOverview(), @@ -434,6 +435,7 @@ export function renderApp(state: AppViewState) { sessionKey: next, lastActiveSessionKey: next, }); + void state.loadAssistantIdentity(); void loadChatHistory(state); void refreshChatAvatar(state); }, @@ -479,6 +481,8 @@ export function renderApp(state: AppViewState) { onOpenSidebar: (content: string) => state.handleOpenSidebar(content), onCloseSidebar: () => state.handleCloseSidebar(), onSplitRatioChange: (ratio: number) => state.handleSplitRatioChange(ratio), + assistantName: state.assistantName, + assistantAvatar: state.assistantAvatar, }) : nothing} diff --git a/ui/src/ui/app-view-state.ts b/ui/src/ui/app-view-state.ts index 3f969e5b9..5832e879a 100644 --- a/ui/src/ui/app-view-state.ts +++ b/ui/src/ui/app-view-state.ts @@ -41,6 +41,9 @@ export type AppViewState = { hello: GatewayHelloOk | null; lastError: string | null; eventLog: EventLogEntry[]; + assistantName: string; + assistantAvatar: string | null; + assistantAgentId: string | null; sessionKey: string; chatLoading: boolean; chatSending: boolean; @@ -144,6 +147,7 @@ export type AppViewState = { setTheme: (theme: ThemeMode, context?: ThemeTransitionContext) => void; applySettings: (next: UiSettings) => void; loadOverview: () => Promise; + loadAssistantIdentity: () => Promise; loadCron: () => Promise; handleWhatsAppStart: (force: boolean) => Promise; handleWhatsAppWait: () => Promise; diff --git a/ui/src/ui/app.ts b/ui/src/ui/app.ts index 35506e76a..ba0622d57 100644 --- a/ui/src/ui/app.ts +++ b/ui/src/ui/app.ts @@ -2,6 +2,7 @@ import { LitElement, html, nothing } from "lit"; import { customElement, state } from "lit/decorators.js"; import type { GatewayBrowserClient, GatewayHelloOk } from "./gateway"; +import { resolveInjectedAssistantIdentity } from "./assistant-identity"; import { loadSettings, type UiSettings } from "./storage"; import { renderApp } from "./app-render"; import type { Tab } from "./navigation"; @@ -76,6 +77,7 @@ import { handleWhatsAppWait as handleWhatsAppWaitInternal, } from "./app-channels"; import type { NostrProfileFormState } from "./views/channels.nostr-profile-form"; +import { loadAssistantIdentity as loadAssistantIdentityInternal } from "./controllers/assistant-identity"; declare global { interface Window { @@ -83,6 +85,8 @@ declare global { } } +const injectedAssistantIdentity = resolveInjectedAssistantIdentity(); + @customElement("clawdbot-app") export class ClawdbotApp extends LitElement { @state() settings: UiSettings = loadSettings(); @@ -98,6 +102,10 @@ export class ClawdbotApp extends LitElement { private toolStreamSyncTimer: number | null = null; private sidebarCloseTimer: number | null = null; + @state() assistantName = injectedAssistantIdentity.name; + @state() assistantAvatar = injectedAssistantIdentity.avatar; + @state() assistantAgentId = injectedAssistantIdentity.agentId ?? null; + @state() sessionKey = this.settings.sessionKey; @state() chatLoading = false; @state() chatSending = false; @@ -306,6 +314,10 @@ export class ClawdbotApp extends LitElement { ); } + async loadAssistantIdentity() { + await loadAssistantIdentityInternal(this); + } + applySettings(next: UiSettings) { applySettingsInternal( this as unknown as Parameters[0], diff --git a/ui/src/ui/assistant-identity.ts b/ui/src/ui/assistant-identity.ts new file mode 100644 index 000000000..c21ac6de6 --- /dev/null +++ b/ui/src/ui/assistant-identity.ts @@ -0,0 +1,49 @@ +const MAX_ASSISTANT_NAME = 50; +const MAX_ASSISTANT_AVATAR = 200; + +export const DEFAULT_ASSISTANT_NAME = "Assistant"; +export const DEFAULT_ASSISTANT_AVATAR = "A"; + +export type AssistantIdentity = { + agentId?: string | null; + name: string; + avatar: string | null; +}; + +declare global { + interface Window { + __CLAWDBOT_ASSISTANT_NAME__?: string; + __CLAWDBOT_ASSISTANT_AVATAR__?: string; + } +} + +function coerceIdentityValue(value: string | undefined, maxLength: number): string | undefined { + if (typeof value !== "string") return undefined; + const trimmed = value.trim(); + if (!trimmed) return undefined; + if (trimmed.length <= maxLength) return trimmed; + return trimmed.slice(0, maxLength); +} + +export function normalizeAssistantIdentity( + input?: Partial | null, +): AssistantIdentity { + const name = + coerceIdentityValue(input?.name, MAX_ASSISTANT_NAME) ?? DEFAULT_ASSISTANT_NAME; + const avatar = coerceIdentityValue(input?.avatar ?? undefined, MAX_ASSISTANT_AVATAR) ?? null; + const agentId = + typeof input?.agentId === "string" && input.agentId.trim() + ? input.agentId.trim() + : null; + return { agentId, name, avatar }; +} + +export function resolveInjectedAssistantIdentity(): AssistantIdentity { + if (typeof window === "undefined") { + return normalizeAssistantIdentity({}); + } + return normalizeAssistantIdentity({ + name: window.__CLAWDBOT_ASSISTANT_NAME__, + avatar: window.__CLAWDBOT_ASSISTANT_AVATAR__, + }); +} diff --git a/ui/src/ui/chat/grouped-render.ts b/ui/src/ui/chat/grouped-render.ts index ff155a256..6a36c7898 100644 --- a/ui/src/ui/chat/grouped-render.ts +++ b/ui/src/ui/chat/grouped-render.ts @@ -1,6 +1,7 @@ import { html, nothing } from "lit"; import { unsafeHTML } from "lit/directives/unsafe-html.js"; +import type { AssistantIdentity } from "../assistant-identity"; import { toSanitizedMarkdownHtml } from "../markdown"; import type { MessageGroup } from "../types/chat-types"; import { renderCopyAsMarkdownButton } from "./copy-as-markdown"; @@ -12,10 +13,10 @@ import { } from "./message-extract"; import { extractToolCards, renderToolCardSidebar } from "./tool-cards"; -export function renderReadingIndicatorGroup(assistantAvatarUrl?: string | null) { +export function renderReadingIndicatorGroup(assistant?: AssistantIdentity) { return html`
- ${renderAvatar("assistant", assistantAvatarUrl ?? undefined)} + ${renderAvatar("assistant", assistant)}