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 && (
onRetry(getPlainText(message))}
- className="absolute top-2 right-2 h-7 w-7 rounded-lg border border-pc-border bg-pc-elevated/80 backdrop-blur-sm flex items-center justify-center text-pc-text-secondary hover:text-pc-accent-light hover:border-[var(--pc-accent-dim)] transition-all opacity-0 group-hover:opacity-100"
+ className={`absolute top-2 right-2 h-7 w-7 rounded-lg border border-pc-border bg-pc-elevated/80 backdrop-blur-sm flex items-center justify-center text-pc-text-secondary hover:text-pc-accent-light hover:border-[var(--pc-accent-dim)] transition-all ${message.sendStatus === 'error' ? 'opacity-100' : 'opacity-0 group-hover:opacity-100'}`}
title={t('message.retry')}
aria-label={t('message.retry')}
>
-
+
)}
{/* 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 */}
setOpen(!open)}
- className={`inline-flex items-center gap-1.5 rounded-2xl border ${c.border} ${c.bg} ${c.glow} px-3 py-1.5 text-xs ${c.text} hover:brightness-125 transition-all max-w-full`}
+ className={`inline-flex items-center gap-1.5 rounded-2xl border px-3 py-1.5 text-xs hover:brightness-125 transition-all max-w-full ${cs.glow}`}
+ style={{ ...cs.badge, ...cs.text }}
>
{getToolEmoji(name)}
{name}
@@ -273,10 +291,10 @@ export function ToolCall({ name, input, result }: { name: string; input?: Record
{/* Expanded content */}
{open && (
-
+
{inputStr && (
-
{t('tool.parameters')}
+
{t('tool.parameters')}
- {t('tool.result')}
+ {t('tool.result')}
{imageData ? (
<>
{imageData.remaining && (
diff --git a/src/contexts/ThemeContext.tsx b/src/contexts/ThemeContext.tsx
index 7b1948c..f07ed6b 100644
--- a/src/contexts/ThemeContext.tsx
+++ b/src/contexts/ThemeContext.tsx
@@ -28,8 +28,6 @@ const themes: Record> = {
'--pc-scrollbar-thumb': '#52525b',
'--pc-scrollbar-track': '#27272a',
'--pc-scrollbar-thumb-hover': '#71717a',
- '--pc-user-bubble': 'rgba(34,211,238,0.06)',
- '--pc-user-border': 'rgba(34,211,238,0.15)',
'--pc-hover': 'rgba(255,255,255,0.05)',
'--pc-hover-strong': 'rgba(255,255,255,0.08)',
'--pc-separator': 'rgba(255,255,255,0.05)',
@@ -50,8 +48,6 @@ const themes: Record> = {
'--pc-scrollbar-thumb': '#a1a1aa',
'--pc-scrollbar-track': '#e4e4e7',
'--pc-scrollbar-thumb-hover': '#71717a',
- '--pc-user-bubble': 'rgba(34,211,238,0.08)',
- '--pc-user-border': 'rgba(34,211,238,0.25)',
'--pc-hover': 'rgba(0,0,0,0.05)',
'--pc-hover-strong': 'rgba(0,0,0,0.08)',
'--pc-separator': 'rgba(0,0,0,0.08)',
@@ -72,8 +68,6 @@ const themes: Record> = {
'--pc-scrollbar-thumb': '#3f3f46',
'--pc-scrollbar-track': '#0a0a0a',
'--pc-scrollbar-thumb-hover': '#52525b',
- '--pc-user-bubble': 'rgba(34,211,238,0.05)',
- '--pc-user-border': 'rgba(34,211,238,0.12)',
'--pc-hover': 'rgba(255,255,255,0.04)',
'--pc-hover-strong': 'rgba(255,255,255,0.06)',
'--pc-separator': 'rgba(255,255,255,0.04)',
diff --git a/src/hooks/useGateway.ts b/src/hooks/useGateway.ts
index b5cc743..a155323 100644
--- a/src/hooks/useGateway.ts
+++ b/src/hooks/useGateway.ts
@@ -399,12 +399,14 @@ export function useGateway() {
}, [setupClient]);
const sendMessage = useCallback(async (text: string, attachments?: Array<{ mimeType: string; fileName: string; content: string }>) => {
+ const msgId = 'user-' + Date.now();
const userMsg: ChatMessage = {
- id: 'user-' + Date.now(),
+ id: msgId,
role: 'user',
content: text,
timestamp: Date.now(),
blocks: [{ type: 'text', text }],
+ sendStatus: 'sending',
};
setMessages(prev => [...prev, userMsg]);
setIsGenerating(true);
@@ -417,8 +419,11 @@ export function useGateway() {
idempotencyKey: genIdempotencyKey(),
...(attachments && attachments.length > 0 ? { attachments } : {}),
});
+ // Mark as sent
+ setMessages(prev => prev.map(m => m.id === msgId ? { ...m, sendStatus: 'sent' as const } : m));
} catch {
- // Failed to send — stop generating indicator
+ // Mark as error and stop generating
+ setMessages(prev => prev.map(m => m.id === msgId ? { ...m, sendStatus: 'error' as const } : m));
setIsGenerating(false);
}
}, []);
diff --git a/src/hooks/useSecondarySession.ts b/src/hooks/useSecondarySession.ts
index 7a41334..1a6a014 100644
--- a/src/hooks/useSecondarySession.ts
+++ b/src/hooks/useSecondarySession.ts
@@ -214,12 +214,14 @@ export function useSecondarySession(
const sendMessage = useCallback(async (text: string, attachments?: Array<{ mimeType: string; fileName: string; content: string }>) => {
if (!sessionKeyRef.current) return;
+ const msgId = 'user-' + Date.now();
const userMsg: ChatMessage = {
- id: 'user-' + Date.now(),
+ id: msgId,
role: 'user',
content: text,
timestamp: Date.now(),
blocks: [{ type: 'text', text }],
+ sendStatus: 'sending',
};
setMessages(prev => [...prev, userMsg]);
setIsGenerating(true);
@@ -230,7 +232,9 @@ export function useSecondarySession(
deliver: false,
...(attachments && attachments.length > 0 ? { attachments } : {}),
});
+ setMessages(prev => prev.map(m => m.id === msgId ? { ...m, sendStatus: 'sent' as const } : m));
} catch {
+ setMessages(prev => prev.map(m => m.id === msgId ? { ...m, sendStatus: 'error' as const } : m));
setIsGenerating(false);
}
}, [getClient]);
diff --git a/src/index.css b/src/index.css
index feac257..8fad2e1 100644
--- a/src/index.css
+++ b/src/index.css
@@ -39,8 +39,6 @@
--pc-accent-dim: rgba(34,211,238,0.3);
--pc-accent-glow: rgba(34,211,238,0.1);
--pc-accent-rgb: 34,211,238;
- --pc-user-bubble: rgba(34,211,238,0.06);
- --pc-user-border: rgba(34,211,238,0.15);
--pc-hover: rgba(255,255,255,0.05);
--pc-hover-strong: rgba(255,255,255,0.08);
--pc-separator: rgba(255,255,255,0.05);
diff --git a/src/types/index.ts b/src/types/index.ts
index fc73ccd..012b115 100644
--- a/src/types/index.ts
+++ b/src/types/index.ts
@@ -8,6 +8,8 @@ export interface ChatMessage {
runId?: string;
isSystemEvent?: boolean;
metadata?: Record;
+ /** Optimistic send status for user messages */
+ sendStatus?: 'sending' | 'sent' | 'error';
}
export type MessageBlock =