feat: add agent identity avatars (#1329) (thanks @dlauer)

This commit is contained in:
Peter Steinberger
2026-01-22 05:21:47 +00:00
parent a2bea8e366
commit a59ac5cf6f
26 changed files with 477 additions and 22 deletions

View File

@@ -1,10 +1,11 @@
import { loadChatHistory } from "./controllers/chat";
import { loadDevices } from "./controllers/devices";
import { loadNodes } from "./controllers/nodes";
import { loadAgents } from "./controllers/agents";
import type { GatewayEventFrame, GatewayHelloOk } from "./gateway";
import { GatewayBrowserClient } from "./gateway";
import type { EventLogEntry } from "./app-events";
import type { PresenceEntry, HealthSnapshot, StatusSummary } from "./types";
import type { AgentsListResult, PresenceEntry, HealthSnapshot, StatusSummary } from "./types";
import type { Tab } from "./navigation";
import type { UiSettings } from "./storage";
import { handleAgentEvent, resetToolStream, type AgentEventPayload } from "./app-tool-stream";
@@ -38,6 +39,9 @@ type GatewayHost = {
presenceEntries: PresenceEntry[];
presenceError: string | null;
presenceStatus: StatusSummary | null;
agentsLoading: boolean;
agentsList: AgentsListResult | null;
agentsError: string | null;
debugHealth: HealthSnapshot | null;
sessionKey: string;
chatRunId: string | null;
@@ -117,6 +121,7 @@ export function connectGateway(host: GatewayHost) {
host.connected = true;
host.hello = hello;
applySnapshot(host, hello);
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 });
void refreshActiveTab(host as unknown as Parameters<typeof refreshActiveTab>[0]);

View File

@@ -2,6 +2,7 @@ import { html, nothing } from "lit";
import type { GatewayBrowserClient, GatewayHelloOk } from "./gateway";
import type { AppViewState } from "./app-view-state";
import { parseAgentSessionKey } from "../../../src/routing/session-key.js";
import {
TAB_GROUPS,
iconForTab,
@@ -80,6 +81,24 @@ import { loadCronRuns, toggleCronJob, runCronJob, removeCronJob, addCronJob } fr
import { loadDebug, callDebugMethod } from "./controllers/debug";
import { loadLogs } from "./controllers/logs";
const AVATAR_DATA_RE = /^data:/i;
const AVATAR_HTTP_RE = /^https?:\/\//i;
function resolveAssistantAvatarUrl(state: AppViewState): string | undefined {
const list = state.agentsList?.agents ?? [];
const parsed = parseAgentSessionKey(state.sessionKey);
const agentId =
parsed?.agentId ??
state.agentsList?.defaultId ??
"main";
const agent = list.find((entry) => entry.id === agentId);
const identity = agent?.identity;
const candidate = identity?.avatarUrl ?? identity?.avatar;
if (!candidate) return undefined;
if (AVATAR_DATA_RE.test(candidate) || AVATAR_HTTP_RE.test(candidate)) return candidate;
return identity?.avatarUrl;
}
export function renderApp(state: AppViewState) {
const presenceCount = state.presenceEntries.length;
const sessionsCount = state.sessionsResult?.count ?? null;
@@ -87,6 +106,8 @@ export function renderApp(state: AppViewState) {
const chatDisabledReason = state.connected ? null : "Disconnected from gateway.";
const isChat = state.tab === "chat";
const chatFocus = isChat && state.settings.chatFocusMode;
const assistantAvatarUrl = resolveAssistantAvatarUrl(state);
const chatAvatarUrl = state.chatAvatarUrl ?? assistantAvatarUrl ?? null;
return html`
<div class="shell ${isChat ? "shell--chat" : ""} ${chatFocus ? "shell--chat-focus" : ""} ${state.settings.navCollapsed ? "shell--nav-collapsed" : ""}">
@@ -420,11 +441,11 @@ export function renderApp(state: AppViewState) {
showThinking: state.settings.chatShowThinking,
loading: state.chatLoading,
sending: state.chatSending,
assistantAvatarUrl: chatAvatarUrl,
messages: state.chatMessages,
toolMessages: state.chatToolMessages,
stream: state.chatStream,
streamStartedAt: state.chatStreamStartedAt,
assistantAvatarUrl: state.chatAvatarUrl,
draft: state.chatMessage,
queue: state.chatQueue,
connected: state.connected,

View File

@@ -4,6 +4,7 @@ import type { UiSettings } from "./storage";
import type { ThemeMode } from "./theme";
import type { ThemeTransitionContext } from "./theme-transition";
import type {
AgentsListResult,
ChannelsStatusSnapshot,
ConfigSnapshot,
CronJob,
@@ -95,6 +96,9 @@ export type AppViewState = {
presenceEntries: PresenceEntry[];
presenceError: string | null;
presenceStatus: string | null;
agentsLoading: boolean;
agentsList: AgentsListResult | null;
agentsError: string | null;
sessionsLoading: boolean;
sessionsResult: SessionsListResult | null;
sessionsError: string | null;

View File

@@ -7,6 +7,7 @@ import { renderApp } from "./app-render";
import type { Tab } from "./navigation";
import type { ResolvedTheme, ThemeMode } from "./theme";
import type {
AgentsListResult,
ConfigSnapshot,
ConfigUiHints,
CronJob,
@@ -169,6 +170,10 @@ export class ClawdbotApp extends LitElement {
@state() presenceError: string | null = null;
@state() presenceStatus: string | null = null;
@state() agentsLoading = false;
@state() agentsList: AgentsListResult | null = null;
@state() agentsError: string | null = null;
@state() sessionsLoading = false;
@state() sessionsResult: SessionsListResult | null = null;
@state() sessionsError: string | null = null;

View File

@@ -30,8 +30,8 @@ export function renderReadingIndicatorGroup(assistantAvatarUrl?: string | null)
export function renderStreamingGroup(
text: string,
startedAt: number,
onOpenSidebar?: (content: string) => void,
assistantAvatarUrl?: string | null,
onOpenSidebar?: (content: string) => void,
) {
const timestamp = new Date(startedAt).toLocaleTimeString([], {
hour: "numeric",

View File

@@ -0,0 +1,25 @@
import type { GatewayBrowserClient } from "../gateway";
import type { AgentsListResult } from "../types";
export type AgentsState = {
client: GatewayBrowserClient | null;
connected: boolean;
agentsLoading: boolean;
agentsError: string | null;
agentsList: AgentsListResult | null;
};
export async function loadAgents(state: AgentsState) {
if (!state.client || !state.connected) return;
if (state.agentsLoading) return;
state.agentsLoading = true;
state.agentsError = null;
try {
const res = (await state.client.request("agents.list", {})) as AgentsListResult | undefined;
if (res) state.agentsList = res;
} catch (err) {
state.agentsError = String(err);
} finally {
state.agentsLoading = false;
}
}

View File

@@ -294,6 +294,25 @@ export type GatewaySessionsDefaults = {
contextTokens: number | null;
};
export type GatewayAgentRow = {
id: string;
name?: string;
identity?: {
name?: string;
theme?: string;
emoji?: string;
avatar?: string;
avatarUrl?: string;
};
};
export type AgentsListResult = {
defaultId: string;
mainKey: string;
scope: string;
agents: GatewayAgentRow[];
};
export type GatewaySessionRow = {
key: string;
kind: "direct" | "group" | "global" | "unknown";

View File

@@ -122,8 +122,8 @@ export function renderChat(props: ChatProps) {
return renderStreamingGroup(
item.text,
item.startedAt,
props.onOpenSidebar,
props.assistantAvatarUrl ?? null,
props.onOpenSidebar,
);
}