[0]);
diff --git a/ui/src/ui/app-render.ts b/ui/src/ui/app-render.ts
index 8275ca147..37f0b6bea 100644
--- a/ui/src/ui/app-render.ts
+++ b/ui/src/ui/app-render.ts
@@ -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`
@@ -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,
diff --git a/ui/src/ui/app-view-state.ts b/ui/src/ui/app-view-state.ts
index 5af2d0a8e..3f969e5b9 100644
--- a/ui/src/ui/app-view-state.ts
+++ b/ui/src/ui/app-view-state.ts
@@ -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;
diff --git a/ui/src/ui/app.ts b/ui/src/ui/app.ts
index 53f21316c..35506e76a 100644
--- a/ui/src/ui/app.ts
+++ b/ui/src/ui/app.ts
@@ -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;
diff --git a/ui/src/ui/chat/grouped-render.ts b/ui/src/ui/chat/grouped-render.ts
index c90493219..ff155a256 100644
--- a/ui/src/ui/chat/grouped-render.ts
+++ b/ui/src/ui/chat/grouped-render.ts
@@ -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",
diff --git a/ui/src/ui/controllers/agents.ts b/ui/src/ui/controllers/agents.ts
new file mode 100644
index 000000000..deb79ef6b
--- /dev/null
+++ b/ui/src/ui/controllers/agents.ts
@@ -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;
+ }
+}
diff --git a/ui/src/ui/types.ts b/ui/src/ui/types.ts
index 6cdbfb029..be278b8e5 100644
--- a/ui/src/ui/types.ts
+++ b/ui/src/ui/types.ts
@@ -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";
diff --git a/ui/src/ui/views/chat.ts b/ui/src/ui/views/chat.ts
index fddc98b68..c8938521e 100644
--- a/ui/src/ui/views/chat.ts
+++ b/ui/src/ui/views/chat.ts
@@ -122,8 +122,8 @@ export function renderChat(props: ChatProps) {
return renderStreamingGroup(
item.text,
item.startedAt,
- props.onOpenSidebar,
props.assistantAvatarUrl ?? null,
+ props.onOpenSidebar,
);
}
From d425f1ebead91da113599a1e991f113b8f4b0066 Mon Sep 17 00:00:00 2001
From: Peter Steinberger
Date: Thu, 22 Jan 2026 05:51:42 +0000
Subject: [PATCH 5/6] test: align envelope timestamp expectations (#1329)
(thanks @dlauer)
---
...patterns-match-without-botusername.test.ts | 9 +++----
...gram-bot.installs-grammy-throttler.test.ts | 7 ++----
src/telegram/bot.test.ts | 11 +++-----
....reconnects-after-connection-close.test.ts | 9 +++----
test/helpers/envelope-timestamp.ts | 25 ++++++++++++++++++-
5 files changed, 36 insertions(+), 25 deletions(-)
diff --git a/src/telegram/bot.create-telegram-bot.accepts-group-messages-mentionpatterns-match-without-botusername.test.ts b/src/telegram/bot.create-telegram-bot.accepts-group-messages-mentionpatterns-match-without-botusername.test.ts
index 1379b6e6f..a648b9511 100644
--- a/src/telegram/bot.create-telegram-bot.accepts-group-messages-mentionpatterns-match-without-botusername.test.ts
+++ b/src/telegram/bot.create-telegram-bot.accepts-group-messages-mentionpatterns-match-without-botusername.test.ts
@@ -1,8 +1,5 @@
import { beforeEach, describe, expect, it, vi } from "vitest";
-import {
- escapeRegExp,
- formatLocalEnvelopeTimestamp,
-} from "../../test/helpers/envelope-timestamp.js";
+import { escapeRegExp, formatEnvelopeTimestamp } from "../../test/helpers/envelope-timestamp.js";
import { resetInboundDedupe } from "../auto-reply/reply/inbound-dedupe.js";
import { createTelegramBot } from "./bot.js";
@@ -180,7 +177,7 @@ describe("createTelegramBot", () => {
expect(payload.WasMentioned).toBe(true);
expect(payload.SenderName).toBe("Ada");
expect(payload.SenderId).toBe("9");
- const expectedTimestamp = formatLocalEnvelopeTimestamp(new Date("2025-01-09T00:00:00Z"));
+ const expectedTimestamp = formatEnvelopeTimestamp(new Date("2025-01-09T00:00:00Z"));
const timestampPattern = escapeRegExp(expectedTimestamp);
expect(payload.Body).toMatch(
new RegExp(`^\\[Telegram Test Group id:7 (\\+\\d+[smhd] )?${timestampPattern}\\]`),
@@ -225,7 +222,7 @@ describe("createTelegramBot", () => {
expect(payload.SenderName).toBe("Ada Lovelace");
expect(payload.SenderId).toBe("99");
expect(payload.SenderUsername).toBe("ada");
- const expectedTimestamp = formatLocalEnvelopeTimestamp(new Date("2025-01-09T00:00:00Z"));
+ const expectedTimestamp = formatEnvelopeTimestamp(new Date("2025-01-09T00:00:00Z"));
const timestampPattern = escapeRegExp(expectedTimestamp);
expect(payload.Body).toMatch(
new RegExp(`^\\[Telegram Ops id:42 (\\+\\d+[smhd] )?${timestampPattern}\\]`),
diff --git a/src/telegram/bot.create-telegram-bot.installs-grammy-throttler.test.ts b/src/telegram/bot.create-telegram-bot.installs-grammy-throttler.test.ts
index 141532796..e27e119f0 100644
--- a/src/telegram/bot.create-telegram-bot.installs-grammy-throttler.test.ts
+++ b/src/telegram/bot.create-telegram-bot.installs-grammy-throttler.test.ts
@@ -1,8 +1,5 @@
import { beforeEach, describe, expect, it, vi } from "vitest";
-import {
- escapeRegExp,
- formatLocalEnvelopeTimestamp,
-} from "../../test/helpers/envelope-timestamp.js";
+import { escapeRegExp, formatEnvelopeTimestamp } from "../../test/helpers/envelope-timestamp.js";
import { resetInboundDedupe } from "../auto-reply/reply/inbound-dedupe.js";
import { createTelegramBot, getTelegramSequentialKey } from "./bot.js";
import { resolveTelegramFetch } from "./fetch.js";
@@ -332,7 +329,7 @@ describe("createTelegramBot", () => {
expect(replySpy).toHaveBeenCalledTimes(1);
const payload = replySpy.mock.calls[0][0];
- const expectedTimestamp = formatLocalEnvelopeTimestamp(new Date("2025-01-09T00:00:00Z"));
+ const expectedTimestamp = formatEnvelopeTimestamp(new Date("2025-01-09T00:00:00Z"));
const timestampPattern = escapeRegExp(expectedTimestamp);
expect(payload.Body).toMatch(
new RegExp(
diff --git a/src/telegram/bot.test.ts b/src/telegram/bot.test.ts
index 2d04d6aa4..21f52c50d 100644
--- a/src/telegram/bot.test.ts
+++ b/src/telegram/bot.test.ts
@@ -10,10 +10,7 @@ import { listSkillCommandsForAgents } from "../auto-reply/skill-commands.js";
import { resetInboundDedupe } from "../auto-reply/reply/inbound-dedupe.js";
import * as replyModule from "../auto-reply/reply.js";
import { expectInboundContextContract } from "../../test/helpers/inbound-contract.js";
-import {
- escapeRegExp,
- formatLocalEnvelopeTimestamp,
-} from "../../test/helpers/envelope-timestamp.js";
+import { escapeRegExp, formatEnvelopeTimestamp } from "../../test/helpers/envelope-timestamp.js";
import { createTelegramBot, getTelegramSequentialKey } from "./bot.js";
import { resolveTelegramFetch } from "./fetch.js";
@@ -454,7 +451,7 @@ describe("createTelegramBot", () => {
expect(replySpy).toHaveBeenCalledTimes(1);
const payload = replySpy.mock.calls[0][0];
- const expectedTimestamp = formatLocalEnvelopeTimestamp(new Date("2025-01-09T00:00:00Z"));
+ const expectedTimestamp = formatEnvelopeTimestamp(new Date("2025-01-09T00:00:00Z"));
const timestampPattern = escapeRegExp(expectedTimestamp);
expect(payload.Body).toMatch(
new RegExp(
@@ -593,7 +590,7 @@ describe("createTelegramBot", () => {
const payload = replySpy.mock.calls[0][0];
expectInboundContextContract(payload);
expect(payload.WasMentioned).toBe(true);
- const expectedTimestamp = formatLocalEnvelopeTimestamp(new Date("2025-01-09T00:00:00Z"));
+ const expectedTimestamp = formatEnvelopeTimestamp(new Date("2025-01-09T00:00:00Z"));
const timestampPattern = escapeRegExp(expectedTimestamp);
expect(payload.Body).toMatch(
new RegExp(`^\\[Telegram Test Group id:7 (\\+\\d+[smhd] )?${timestampPattern}\\]`),
@@ -639,7 +636,7 @@ describe("createTelegramBot", () => {
expect(replySpy).toHaveBeenCalledTimes(1);
const payload = replySpy.mock.calls[0][0];
expectInboundContextContract(payload);
- const expectedTimestamp = formatLocalEnvelopeTimestamp(new Date("2025-01-09T00:00:00Z"));
+ const expectedTimestamp = formatEnvelopeTimestamp(new Date("2025-01-09T00:00:00Z"));
const timestampPattern = escapeRegExp(expectedTimestamp);
expect(payload.Body).toMatch(
new RegExp(`^\\[Telegram Ops id:42 (\\+\\d+[smhd] )?${timestampPattern}\\]`),
diff --git a/src/web/auto-reply.web-auto-reply.reconnects-after-connection-close.test.ts b/src/web/auto-reply.web-auto-reply.reconnects-after-connection-close.test.ts
index 870a018d5..f40e4e3ab 100644
--- a/src/web/auto-reply.web-auto-reply.reconnects-after-connection-close.test.ts
+++ b/src/web/auto-reply.web-auto-reply.reconnects-after-connection-close.test.ts
@@ -3,10 +3,7 @@ import fs from "node:fs/promises";
import os from "node:os";
import path from "node:path";
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
-import {
- escapeRegExp,
- formatLocalEnvelopeTimestamp,
-} from "../../test/helpers/envelope-timestamp.js";
+import { escapeRegExp, formatEnvelopeTimestamp } from "../../test/helpers/envelope-timestamp.js";
vi.mock("../agents/pi-embedded.js", () => ({
abortEmbeddedPiRun: vi.fn().mockReturnValue(false),
@@ -332,8 +329,8 @@ describe("web auto-reply", () => {
expect(resolver).toHaveBeenCalledTimes(2);
const firstArgs = resolver.mock.calls[0][0];
const secondArgs = resolver.mock.calls[1][0];
- const firstTimestamp = formatLocalEnvelopeTimestamp(new Date("2025-01-01T00:00:00Z"));
- const secondTimestamp = formatLocalEnvelopeTimestamp(new Date("2025-01-01T01:00:00Z"));
+ const firstTimestamp = formatEnvelopeTimestamp(new Date("2025-01-01T00:00:00Z"));
+ const secondTimestamp = formatEnvelopeTimestamp(new Date("2025-01-01T01:00:00Z"));
const firstPattern = escapeRegExp(firstTimestamp);
const secondPattern = escapeRegExp(secondTimestamp);
expect(firstArgs.Body).toMatch(
diff --git a/test/helpers/envelope-timestamp.ts b/test/helpers/envelope-timestamp.ts
index 135063e41..934608204 100644
--- a/test/helpers/envelope-timestamp.ts
+++ b/test/helpers/envelope-timestamp.ts
@@ -1,5 +1,17 @@
-export function formatLocalEnvelopeTimestamp(date: Date): string {
+type EnvelopeTimestampZone = string;
+
+function formatUtcTimestamp(date: Date): string {
+ const yyyy = String(date.getUTCFullYear()).padStart(4, "0");
+ const mm = String(date.getUTCMonth() + 1).padStart(2, "0");
+ const dd = String(date.getUTCDate()).padStart(2, "0");
+ const hh = String(date.getUTCHours()).padStart(2, "0");
+ const min = String(date.getUTCMinutes()).padStart(2, "0");
+ return `${yyyy}-${mm}-${dd}T${hh}:${min}Z`;
+}
+
+function formatZonedTimestamp(date: Date, timeZone?: string): string {
const parts = new Intl.DateTimeFormat("en-US", {
+ timeZone,
year: "numeric",
month: "2-digit",
day: "2-digit",
@@ -27,6 +39,17 @@ export function formatLocalEnvelopeTimestamp(date: Date): string {
return `${yyyy}-${mm}-${dd} ${hh}:${min}${tz ? ` ${tz}` : ""}`;
}
+export function formatEnvelopeTimestamp(date: Date, zone: EnvelopeTimestampZone = "utc"): string {
+ const normalized = zone.trim().toLowerCase();
+ if (normalized === "utc" || normalized === "gmt") return formatUtcTimestamp(date);
+ if (normalized === "local" || normalized === "host") return formatZonedTimestamp(date);
+ return formatZonedTimestamp(date, zone);
+}
+
+export function formatLocalEnvelopeTimestamp(date: Date): string {
+ return formatEnvelopeTimestamp(date, "local");
+}
+
export function escapeRegExp(value: string): string {
return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
}
From 7b7c107ffe02db1b391ca6ad0c4c74649af7a7a4 Mon Sep 17 00:00:00 2001
From: Peter Steinberger
Date: Thu, 22 Jan 2026 05:58:46 +0000
Subject: [PATCH 6/6] docs: update changelog for avatar follow-up (#1424)
(thanks @dlauer)
---
CHANGELOG.md | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/CHANGELOG.md b/CHANGELOG.md
index da54152b6..cf05ea0d5 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -6,7 +6,7 @@ Docs: https://docs.clawd.bot
### 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) Thanks @dlauer.
+- 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 per-message Gmail search example for gog. (#1220) Thanks @mbelinky.