From da2e4862ddfcae4e5036f46b186e5f3cd1819994 Mon Sep 17 00:00:00 2001 From: Nicolas Varrot Date: Thu, 12 Feb 2026 23:29:32 +0000 Subject: [PATCH] feat: display agent avatar from OpenClaw identity config Fetch agent identity via agent.identity.get WS method after connect. Display avatar in message bubbles (replacing Bot icon) and in the header (replacing the PinchChat logo when an avatar is configured). Falls back to default icons when no avatar is set. --- FEEDBACK.md | 53 ++++++++++++++++++++++++++++++++++ src/App.tsx | 6 ++-- src/components/Chat.tsx | 5 ++-- src/components/ChatMessage.tsx | 8 +++-- src/components/Header.tsx | 5 ++-- src/hooks/useGateway.ts | 24 +++++++++++++-- src/types/index.ts | 7 +++++ 7 files changed, 95 insertions(+), 13 deletions(-) diff --git a/FEEDBACK.md b/FEEDBACK.md index e03a291..31fa0fb 100644 --- a/FEEDBACK.md +++ b/FEEDBACK.md @@ -501,3 +501,56 @@ - Clicking the info button on messages does nothing — no panel appears - Introduced in v1.15.0 (commit `b4813f0`) - Fix the click handler / panel display logic + +## Item #47 +- **Date:** 2026-02-12 +- **Priority:** medium +- **Status:** pending +- **Description:** Themes — light mode, OLED black, custom theme support + - Add theme switcher (dark default, light mode, OLED black) + - Configurable accent color + - Persist choice in localStorage + +## Item #48 +- **Date:** 2026-02-12 +- **Priority:** medium +- **Status:** pending +- **Description:** Message search — Ctrl+F in conversation history + - Search bar that filters/highlights messages in the current session + - Navigate between results (up/down arrows) + - Keyboard shortcut Ctrl+F + +## Item #49 +- **Date:** 2026-02-12 +- **Priority:** medium +- **Status:** pending +- **Description:** Syntax highlight in the input textarea + - Color code blocks even while typing in the prompt input + - Highlight markdown syntax (bold, code, headers) in real-time + +## Item #50 +- **Date:** 2026-02-12 +- **Priority:** medium +- **Status:** pending +- **Description:** Multi-tab — split view for 2 sessions side by side + - Allow viewing 2 sessions simultaneously in a split pane layout + - Drag-to-resize divider between panes + - Toggle via button or keyboard shortcut + +## Item #51 +- **Date:** 2026-02-12 +- **Priority:** medium +- **Status:** pending +- **Description:** Typing preview — live markdown render while typing + - Show a preview pane below or beside the input with rendered markdown + - Toggle on/off + - Helps compose complex messages with formatting + +## Item #52 +- **Date:** 2026-02-12 +- **Priority:** medium +- **Status:** pending +- **Description:** Raw JSON viewer — toggle to see raw gateway messages + - Toggle button to switch between rendered view and raw JSON + - Show the full gateway message payload as formatted JSON + - Useful for debugging and understanding the protocol diff --git a/src/App.tsx b/src/App.tsx index 2ecd6a9..5107cc9 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -14,7 +14,7 @@ export default function App() { const { status, messages, sessions, activeSession, isGenerating, isLoadingHistory, sendMessage, abort, switchSession, deleteSession, - authenticated, login, logout, connectError, isConnecting, + authenticated, login, logout, connectError, isConnecting, agentIdentity, } = useGateway(); const [sidebarOpen, setSidebarOpen] = useState(false); const [shortcutsOpen, setShortcutsOpen] = useState(false); @@ -97,10 +97,10 @@ export default function App() { onClose={() => setSidebarOpen(false)} />
-
setSidebarOpen(!sidebarOpen)} activeSessionData={sessions.find(s => s.key === activeSession)} onLogout={logout} soundEnabled={soundEnabled} onToggleSound={toggleSound} messages={messages} /> +
setSidebarOpen(!sidebarOpen)} activeSessionData={sessions.find(s => s.key === activeSession)} onLogout={logout} soundEnabled={soundEnabled} onToggleSound={toggleSound} messages={messages} agentAvatarUrl={agentIdentity?.avatar} />
Loading…
}> - + setShortcutsOpen(false)} /> diff --git a/src/components/Chat.tsx b/src/components/Chat.tsx index 2953c6a..da66522 100644 --- a/src/components/Chat.tsx +++ b/src/components/Chat.tsx @@ -16,6 +16,7 @@ interface Props { sessionKey?: string; onSend: (text: string, attachments?: Array<{ mimeType: string; fileName: string; content: string }>) => void; onAbort: () => void; + agentAvatarUrl?: string; } function isNoReply(msg: ChatMessage): boolean { @@ -66,7 +67,7 @@ function getDateKey(ts: number): string { /** Threshold in pixels — if the user is within this distance of the bottom, auto-scroll */ const SCROLL_THRESHOLD = 150; -export function Chat({ messages, isGenerating, isLoadingHistory, status, sessionKey, onSend, onAbort }: Props) { +export function Chat({ messages, isGenerating, isLoadingHistory, status, sessionKey, onSend, onAbort, agentAvatarUrl }: Props) { const t = useT(); const bottomRef = useRef(null); const scrollContainerRef = useRef(null); @@ -161,7 +162,7 @@ export function Chat({ messages, isGenerating, isLoadingHistory, status, session
)} - + ))} {showTyping && } diff --git a/src/components/ChatMessage.tsx b/src/components/ChatMessage.tsx index 015dc33..4b507b7 100644 --- a/src/components/ChatMessage.tsx +++ b/src/components/ChatMessage.tsx @@ -320,7 +320,7 @@ function SystemEventMessage({ message }: { message: ChatMessageType }) { ); } -export function ChatMessageComponent({ message, onRetry }: { message: ChatMessageType; onRetry?: (text: string) => void }) { +export function ChatMessageComponent({ message, onRetry, agentAvatarUrl }: { message: ChatMessageType; onRetry?: (text: string) => void; agentAvatarUrl?: string }) { useLocale(); // re-render on locale change const isUser = message.role === 'user'; @@ -342,10 +342,12 @@ export function ChatMessageComponent({ message, onRetry }: { message: ChatMessag return (
{/* Avatar */} -
+
{isUser ? - : + : agentAvatarUrl + ? Agent + : }
diff --git a/src/components/Header.tsx b/src/components/Header.tsx index e71b6d7..04ed521 100644 --- a/src/components/Header.tsx +++ b/src/components/Header.tsx @@ -15,9 +15,10 @@ interface Props { soundEnabled?: boolean; onToggleSound?: () => void; messages?: ChatMessage[]; + agentAvatarUrl?: string; } -export function Header({ status, sessionKey, onToggleSidebar, activeSessionData, onLogout, soundEnabled, onToggleSound, messages }: Props) { +export function Header({ status, sessionKey, onToggleSidebar, activeSessionData, onLogout, soundEnabled, onToggleSound, messages, agentAvatarUrl }: Props) { const t = useT(); const sessionLabel = activeSessionData ? sessionDisplayName(activeSessionData) : (sessionKey.split(':').pop() || sessionKey); @@ -36,7 +37,7 @@ export function Header({ status, sessionKey, onToggleSidebar, activeSessionData,
- PinchChat + PinchChat
{t('header.title')} diff --git a/src/hooks/useGateway.ts b/src/hooks/useGateway.ts index 9e8f42c..a02443f 100644 --- a/src/hooks/useGateway.ts +++ b/src/hooks/useGateway.ts @@ -3,7 +3,7 @@ import { GatewayClient, type JsonPayload } from '../lib/gateway'; import { genIdempotencyKey } from '../lib/utils'; import { getStoredCredentials, storeCredentials, clearCredentials } from '../lib/credentials'; import { isSystemEvent } from '../lib/systemEvent'; -import type { ChatMessage, MessageBlock, ConnectionStatus, Session } from '../types'; +import type { ChatMessage, MessageBlock, ConnectionStatus, Session, AgentIdentity } from '../types'; interface ChatPayloadMessage { content?: string | Array<{ type: string; text?: string }>; @@ -43,6 +43,7 @@ export function useGateway() { const currentRunIdRef = useRef(null); const [activeSessions, setActiveSessions] = useState>(new Set()); const [unreadSessions, setUnreadSessions] = useState>(new Set()); + const [agentIdentity, setAgentIdentity] = useState(null); const handleAgentEvent = useCallback((payload: JsonPayload) => { if (payload?.stream !== 'tool') return; @@ -94,6 +95,22 @@ export function useGateway() { localStorage.setItem('pinchchat-deleted-sessions', JSON.stringify([...deleted])); }, [getDeletedSessions]); + const loadAgentIdentity = useCallback(async () => { + try { + const res = await clientRef.current?.send('agent.identity.get', {}); + if (res) { + setAgentIdentity({ + name: res.name as string | undefined, + emoji: res.emoji as string | undefined, + avatar: res.avatar as string | undefined, + agentId: res.agentId as string | undefined, + }); + } + } catch { + // Silently ignore — identity is optional + } + }, []); + const loadSessions = useCallback(async () => { try { const res = await clientRef.current?.send('sessions.list', {}); @@ -228,6 +245,7 @@ export function useGateway() { isConnectingRef.current = false; storeCredentials(wsUrl, token); loadSessions(); + loadAgentIdentity(); loadHistory(activeSessionRef.current); } else if (s === 'disconnected' && !client.isConnected) { // If we never connected successfully, this is an auth/connection error @@ -345,7 +363,7 @@ export function useGateway() { isConnectingRef.current = true; setConnectError(null); client.connect(); - }, [handleAgentEvent, loadHistory, loadSessions]); + }, [handleAgentEvent, loadHistory, loadSessions, loadAgentIdentity]); // On mount: try stored credentials const initRef = useRef(false); @@ -461,6 +479,6 @@ export function useGateway() { return { status, messages, sessions: enrichedSessions, activeSession, isGenerating, isLoadingHistory, sendMessage, abort, switchSession, loadSessions, deleteSession, - authenticated, login, logout, connectError, isConnecting, + authenticated, login, logout, connectError, isConnecting, agentIdentity, }; } diff --git a/src/types/index.ts b/src/types/index.ts index 2e0aef8..fc73ccd 100644 --- a/src/types/index.ts +++ b/src/types/index.ts @@ -35,6 +35,13 @@ export interface Session { lastMessagePreview?: string; } +export interface AgentIdentity { + name?: string; + emoji?: string; + avatar?: string; + agentId?: string; +} + export type ConnectionStatus = 'disconnected' | 'connecting' | 'connected'; export interface GatewayState {