diff --git a/FEEDBACK.md b/FEEDBACK.md index a44fe11..0260053 100644 --- a/FEEDBACK.md +++ b/FEEDBACK.md @@ -633,3 +633,25 @@ 3. ToolCollapseContext.tsx: same react-refresh/only-export-components issue. 4. ToolCall.tsx:246: setState in useEffect → react-hooks/set-state-in-effect. Fix: use useSyncExternalStore or useCallback pattern. - This is BLOCKING all GitHub Releases and Docker image builds. Fix ASAP. + +## Item #59 +- **Date:** 2026-02-13 +- **Priority:** high +- **Status:** pending +- **Description:** Optimistic message rendering — messages sent by the user should appear immediately in the chat, not wait for server echo. Currently when the agent is busy processing, user messages can take a long time to appear. + - Show the message instantly in the chat (optimistic UI) with a subtle "sending" indicator (e.g. clock icon or dimmed opacity) + - When server confirms (echo received), update to "sent" state (e.g. checkmark or full opacity) + - If send fails, show error state with retry option + - Message should always appear at the bottom of the chat immediately after pressing Send + - This is important UX — users think their message didn't go through + +## Item #60 +- **Date:** 2026-02-13 +- **Priority:** high +- **Status:** pending +- **Description:** Light theme fixes — 4 issues: + 1. Progress bar colors (sidebar + header) should follow accent color, not hardcoded cyan + 2. Send button gradient should adapt to theme/accent + 3. Tool call badges are unreadable in light theme (dark-only Tailwind classes like amber-950, sky-950) + 4. User message bubble background too subtle in light theme — needs more contrast + - Sub-agent working on this (session pinchchat-light-theme-fix) diff --git a/src/components/ChatInput.tsx b/src/components/ChatInput.tsx index 08c1dc5..74c31d8 100644 --- a/src/components/ChatInput.tsx +++ b/src/components/ChatInput.tsx @@ -329,7 +329,7 @@ export function ChatInput({ onSend, onAbort, isGenerating, disabled, sessionKey onClick={handleSubmit} disabled={(!text.trim() && files.length === 0) || disabled} aria-label={t('chat.send')} - className="shrink-0 h-11 px-5 rounded-2xl bg-gradient-to-r from-cyan-500/80 via-indigo-500/70 to-violet-500/80 text-pc-text font-semibold text-sm hover:opacity-90 shadow-[0_8px_24px_rgba(34,211,238,0.1)] disabled:opacity-30 disabled:shadow-none transition-all flex items-center gap-2" + className="shrink-0 h-11 px-5 rounded-2xl bg-[var(--pc-accent)] text-white font-semibold text-sm hover:opacity-90 shadow-[0_8px_24px_rgba(var(--pc-accent-rgb),0.15)] disabled:opacity-30 disabled:shadow-none transition-all flex items-center gap-2" > {t('chat.send')} diff --git a/src/components/ChatMessage.tsx b/src/components/ChatMessage.tsx index 0a19b0a..f0010fa 100644 --- a/src/components/ChatMessage.tsx +++ b/src/components/ChatMessage.tsx @@ -13,7 +13,7 @@ import { CodeBlock } from './CodeBlock'; import { ToolCall } from './ToolCall'; import { ImageBlock } from './ImageBlock'; import { buildImageSrc } from '../lib/image'; -import { Bot, User, Wrench, Copy, Check, RefreshCw, Zap, Info, Webhook, Braces } from 'lucide-react'; +import { Bot, User, Wrench, Copy, Check, CheckCheck, RefreshCw, Zap, Info, Webhook, Braces, Clock, AlertCircle } from 'lucide-react'; import { t, getLocale } from '../lib/i18n'; import { useLocale } from '../hooks/useLocale'; import { stripWebhookScaffolding, hasWebhookScaffolding } from '../lib/systemEvent'; @@ -431,7 +431,7 @@ export function ChatMessageComponent({ message: rawMessage, onRetry, agentAvatar } return ( -
+
{/* Avatar */}
{isUser @@ -446,7 +446,7 @@ export function ChatMessageComponent({ message: rawMessage, onRetry, agentAvatar
{/* Action buttons */} @@ -461,11 +461,11 @@ export function ChatMessageComponent({ message: rawMessage, onRetry, agentAvatar {isUser && onRetry && ( )} {/* User-visible text */} @@ -510,6 +510,15 @@ export function ChatMessageComponent({ message: rawMessage, onRetry, agentAvatar )} {message.timestamp && formatTimestamp(message.timestamp)} + {isUser && message.sendStatus === 'sending' && ( + + )} + {isUser && message.sendStatus === 'sent' && ( + + )} + {isUser && message.sendStatus === 'error' && ( + + )}
)}
diff --git a/src/components/Header.tsx b/src/components/Header.tsx index e17eb3f..83f77d9 100644 --- a/src/components/Header.tsx +++ b/src/components/Header.tsx @@ -113,7 +113,7 @@ export function Header({ status, sessionKey, onToggleSidebar, activeSessionData, if (!ctx) return null; const pct = Math.min(100, (total / ctx) * 100); const opacity = Math.max(0.35, Math.min(1, pct / 100)); - const barStyle = { width: `${pct}%`, backgroundColor: `rgba(56, 189, 248, ${opacity})` }; + const barStyle = { width: `${pct}%`, backgroundColor: `rgba(var(--pc-accent-rgb), ${opacity})` }; return (
{activeSessionData?.model && ( diff --git a/src/components/Sidebar.tsx b/src/components/Sidebar.tsx index 4c140f4..069a8b4 100644 --- a/src/components/Sidebar.tsx +++ b/src/components/Sidebar.tsx @@ -370,7 +370,7 @@ export function Sidebar({ sessions, activeSession, onSwitch, onDelete, onSplit, if (!s.contextTokens) return null; const pct = Math.min(100, ((s.totalTokens || 0) / s.contextTokens) * 100); const barOpacity = Math.max(0.35, Math.min(1, pct / 100)); - const barStyle = { width: `${pct}%`, backgroundColor: `rgba(56, 189, 248, ${barOpacity})` }; + const barStyle = { width: `${pct}%`, backgroundColor: `rgba(var(--pc-accent-rgb), ${barOpacity})` }; return (
diff --git a/src/components/ToolCall.tsx b/src/components/ToolCall.tsx index 7367187..860513f 100644 --- a/src/components/ToolCall.tsx +++ b/src/components/ToolCall.tsx @@ -7,31 +7,48 @@ import { useToolCollapse } from '../hooks/useToolCollapse'; type ToolColor = { border: string; bg: string; text: string; icon: string; glow: string; expandBorder: string; expandBg: string }; -const toolColors: Record = { - exec: { border: 'border-amber-500/30', bg: 'bg-amber-500/10', text: 'text-amber-300', icon: 'text-amber-400', glow: 'shadow-[0_0_8px_rgba(245,158,11,0.15)]', expandBorder: 'border-amber-500/20', expandBg: 'bg-amber-950/20' }, - web_search: { border: 'border-emerald-500/30', bg: 'bg-emerald-500/10', text: 'text-emerald-300', icon: 'text-emerald-400', glow: 'shadow-[0_0_8px_rgba(16,185,129,0.15)]', expandBorder: 'border-emerald-500/20', expandBg: 'bg-emerald-950/20' }, - web_fetch: { border: 'border-emerald-500/30', bg: 'bg-emerald-500/10', text: 'text-emerald-300', icon: 'text-emerald-400', glow: 'shadow-[0_0_8px_rgba(16,185,129,0.15)]', expandBorder: 'border-emerald-500/20', expandBg: 'bg-emerald-950/20' }, - Read: { border: 'border-sky-500/30', bg: 'bg-sky-500/10', text: 'text-sky-300', icon: 'text-sky-400', glow: 'shadow-[0_0_8px_rgba(14,165,233,0.15)]', expandBorder: 'border-sky-500/20', expandBg: 'bg-sky-950/20' }, - read: { border: 'border-sky-500/30', bg: 'bg-sky-500/10', text: 'text-sky-300', icon: 'text-sky-400', glow: 'shadow-[0_0_8px_rgba(14,165,233,0.15)]', expandBorder: 'border-sky-500/20', expandBg: 'bg-sky-950/20' }, - Write: { border: 'border-violet-500/30', bg: 'bg-violet-500/10', text: 'text-violet-300', icon: 'text-violet-400', glow: 'shadow-[0_0_8px_rgba(139,92,246,0.15)]', expandBorder: 'border-violet-500/20', expandBg: 'bg-violet-950/20' }, - write: { border: 'border-violet-500/30', bg: 'bg-violet-500/10', text: 'text-violet-300', icon: 'text-violet-400', glow: 'shadow-[0_0_8px_rgba(139,92,246,0.15)]', expandBorder: 'border-violet-500/20', expandBg: 'bg-violet-950/20' }, - Edit: { border: 'border-violet-500/30', bg: 'bg-violet-500/10', text: 'text-violet-300', icon: 'text-violet-400', glow: 'shadow-[0_0_8px_rgba(139,92,246,0.15)]', expandBorder: 'border-violet-500/20', expandBg: 'bg-violet-950/20' }, - edit: { border: 'border-violet-500/30', bg: 'bg-violet-500/10', text: 'text-violet-300', icon: 'text-violet-400', glow: 'shadow-[0_0_8px_rgba(139,92,246,0.15)]', expandBorder: 'border-violet-500/20', expandBg: 'bg-violet-950/20' }, - browser: { border: 'border-cyan-500/30', bg: 'bg-cyan-500/10', text: 'text-pc-accent-light', icon: 'text-pc-accent', glow: 'shadow-[0_0_8px_rgba(6,182,212,0.15)]', expandBorder: 'border-cyan-500/20', expandBg: 'bg-cyan-950/20' }, - image: { border: 'border-pink-500/30', bg: 'bg-pink-500/10', text: 'text-pink-300', icon: 'text-pink-400', glow: 'shadow-[0_0_8px_rgba(236,72,153,0.15)]', expandBorder: 'border-pink-500/20', expandBg: 'bg-pink-950/20' }, - message: { border: 'border-indigo-500/30', bg: 'bg-indigo-500/10', text: 'text-indigo-300', icon: 'text-indigo-400', glow: 'shadow-[0_0_8px_rgba(99,102,241,0.15)]', expandBorder: 'border-indigo-500/20', expandBg: 'bg-indigo-950/20' }, - memory_search: { border: 'border-rose-500/30', bg: 'bg-rose-500/10', text: 'text-rose-300', icon: 'text-rose-400', glow: 'shadow-[0_0_8px_rgba(244,63,94,0.15)]', expandBorder: 'border-rose-500/20', expandBg: 'bg-rose-950/20' }, - memory_get: { border: 'border-rose-500/30', bg: 'bg-rose-500/10', text: 'text-rose-300', icon: 'text-rose-400', glow: 'shadow-[0_0_8px_rgba(244,63,94,0.15)]', expandBorder: 'border-rose-500/20', expandBg: 'bg-rose-950/20' }, - cron: { border: 'border-orange-500/30', bg: 'bg-orange-500/10', text: 'text-orange-300', icon: 'text-orange-400', glow: 'shadow-[0_0_8px_rgba(249,115,22,0.15)]', expandBorder: 'border-orange-500/20', expandBg: 'bg-orange-950/20' }, - sessions_spawn: { border: 'border-teal-500/30', bg: 'bg-teal-500/10', text: 'text-teal-300', icon: 'text-teal-400', glow: 'shadow-[0_0_8px_rgba(20,184,166,0.15)]', expandBorder: 'border-teal-500/20', expandBg: 'bg-teal-950/20' }, +// RGB values for each tool color — used with rgba() for theme-safe rendering +type ToolRGB = { r: number; g: number; b: number }; +const toolRGBs: Record = { + exec: { r: 245, g: 158, b: 11 }, // amber + web_search: { r: 16, g: 185, b: 129 }, // emerald + web_fetch: { r: 16, g: 185, b: 129 }, + Read: { r: 14, g: 165, b: 233 }, // sky + read: { r: 14, g: 165, b: 233 }, + Write: { r: 139, g: 92, b: 246 }, // violet + write: { r: 139, g: 92, b: 246 }, + Edit: { r: 139, g: 92, b: 246 }, + edit: { r: 139, g: 92, b: 246 }, + browser: { r: 6, g: 182, b: 212 }, // cyan + image: { r: 236, g: 72, b: 153 }, // pink + message: { r: 99, g: 102, b: 241 }, // indigo + memory_search: { r: 244, g: 63, b: 94 }, // rose + memory_get: { r: 244, g: 63, b: 94 }, + cron: { r: 249, g: 115, b: 22 }, // orange + sessions_spawn: { r: 20, g: 184, b: 166 },// teal }; +const defaultRGB: ToolRGB = { r: 161, g: 161, b: 170 }; // zinc -const defaultColor: ToolColor = { border: 'border-pc-border-strong', bg: 'bg-pc-elevated/10', text: 'text-pc-text', icon: 'text-pc-text-secondary', glow: 'shadow-[0_0_8px_rgba(161,161,170,0.1)]', expandBorder: 'border-pc-border', expandBg: 'bg-pc-elevated/25' }; +function rgbStr(c: ToolRGB): string { return `${c.r},${c.g},${c.b}`; } -function getColor(name: string): ToolColor { - return toolColors[name] || defaultColor; +function getColorStyles(name: string): { badge: React.CSSProperties; text: React.CSSProperties; expand: React.CSSProperties; glow: string } { + const c = toolRGBs[name] || defaultRGB; + const rgb = rgbStr(c); + return { + badge: { borderColor: `rgba(${rgb},0.3)`, backgroundColor: `rgba(${rgb},0.10)` }, + text: { color: `rgb(${c.r},${c.g},${c.b})` }, + expand: { borderColor: `rgba(${rgb},0.2)`, backgroundColor: `rgba(${rgb},0.05)` }, + glow: `shadow-[0_0_8px_rgba(${rgb},0.15)]`, + }; } +// Keep ToolColor type for compatibility but now only used for classes that are theme-safe +const toolColors: Record = {}; +const defaultColor: ToolColor = { border: '', bg: '', text: '', icon: '', glow: '', expandBorder: '', expandBg: '' }; + +// toolColors/defaultColor kept for potential future use +void toolColors; void defaultColor; + const toolEmojis: Record = { exec: '⚡', web_search: '🔍', @@ -237,7 +254,7 @@ export function ToolCall({ name, input, result }: { name: string; input?: Record const [wrap, setWrap] = useState(true); const { globalState, version } = useToolCollapse(); const lastVersion = useRef(version); - const c = getColor(name); + const cs = getColorStyles(name); // Respond to global collapse/expand commands useEffect(() => { @@ -256,7 +273,8 @@ export function ToolCall({ name, input, result }: { name: string; input?: Record {/* Tool use badge */}