feat: add agent identity avatars (#1329) (thanks @dlauer)
This commit is contained in:
@@ -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]);
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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",
|
||||
|
||||
25
ui/src/ui/controllers/agents.ts
Normal file
25
ui/src/ui/controllers/agents.ts
Normal 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;
|
||||
}
|
||||
}
|
||||
@@ -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";
|
||||
|
||||
@@ -122,8 +122,8 @@ export function renderChat(props: ChatProps) {
|
||||
return renderStreamingGroup(
|
||||
item.text,
|
||||
item.startedAt,
|
||||
props.onOpenSidebar,
|
||||
props.assistantAvatarUrl ?? null,
|
||||
props.onOpenSidebar,
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user