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 {