From 5b7b0ff1e4ca271ad16a716c36f4d5c40a196d09 Mon Sep 17 00:00:00 2001 From: kurihada Date: Mon, 15 Dec 2025 15:47:32 +0800 Subject: [PATCH] =?UTF-8?q?feat(ui):=20=E5=AE=9E=E7=8E=B0=E6=B7=B1?= =?UTF-8?q?=E8=89=B2/=E6=B5=85=E8=89=B2=E4=B8=BB=E9=A2=98=E5=88=87?= =?UTF-8?q?=E6=8D=A2=E5=8A=9F=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 添加 CSS 变量定义浅色和深色主题色板 - 扩展 Tailwind 配置支持语义化颜色 (surface-*, fg-*, line-*, code) - 创建 useTheme hook 管理主题状态和持久化 - 创建 ThemeToggle 组件支持三种模式 (light/dark/system) - 迁移所有组件从硬编码 gray-* 到语义化颜色 - 支持系统主题偏好检测 (prefers-color-scheme) - 添加主题初始化脚本防止闪烁 (FOUC) --- .../ui/src/components/AgentDefaultsEditor.tsx | 36 +-- packages/ui/src/components/AgentEditor.tsx | 78 +++--- packages/ui/src/components/AgentsPanel.tsx | 62 ++--- packages/ui/src/components/ChatInput.tsx | 8 +- packages/ui/src/components/ChatMessage.tsx | 78 +++--- .../src/components/CheckpointDiffViewer.tsx | 46 ++-- .../ui/src/components/CheckpointPanel.tsx | 38 +-- packages/ui/src/components/CodeBlock.tsx | 12 +- packages/ui/src/components/CommandEditor.tsx | 52 ++-- packages/ui/src/components/CommandMenu.tsx | 38 +-- packages/ui/src/components/CommandPanel.tsx | 32 +-- packages/ui/src/components/ConfigPanel.tsx | 14 +- packages/ui/src/components/ContextUsage.tsx | 14 +- packages/ui/src/components/FileBrowser.tsx | 38 +-- packages/ui/src/components/HookEditor.tsx | 48 ++-- packages/ui/src/components/HooksPanel.tsx | 46 ++-- packages/ui/src/components/MCPPanel.tsx | 54 ++--- packages/ui/src/components/Markdown.tsx | 20 +- .../ui/src/components/PermissionDialog.tsx | 52 ++-- packages/ui/src/components/ProviderEditor.tsx | 66 +++--- packages/ui/src/components/ProvidersPanel.tsx | 78 +++--- packages/ui/src/components/RestoreDialog.tsx | 26 +- packages/ui/src/components/Sidebar.tsx | 44 ++-- packages/ui/src/components/Skeleton.tsx | 16 +- packages/ui/src/components/ThemeToggle.tsx | 92 ++++++++ packages/ui/src/hooks/useTheme.tsx | 169 +++++++++++++ packages/ui/src/index.ts | 4 + packages/ui/src/primitives/Button.tsx | 6 +- packages/ui/src/primitives/Dialog.tsx | 6 +- packages/ui/src/primitives/Input.tsx | 2 +- packages/ui/src/primitives/Select.tsx | 8 +- packages/ui/src/primitives/Slider.tsx | 4 +- packages/ui/src/primitives/Switch.tsx | 2 +- packages/ui/src/primitives/Tooltip.tsx | 2 +- packages/ui/src/styles/index.css | 74 ++++-- packages/web/index.html | 18 +- packages/web/src/App.tsx | 223 +++++++++--------- packages/web/src/pages/Chat.tsx | 28 +-- packages/web/tailwind.config.js | 20 ++ 39 files changed, 1002 insertions(+), 652 deletions(-) create mode 100644 packages/ui/src/components/ThemeToggle.tsx create mode 100644 packages/ui/src/hooks/useTheme.tsx diff --git a/packages/ui/src/components/AgentDefaultsEditor.tsx b/packages/ui/src/components/AgentDefaultsEditor.tsx index 784f3cb..b20bd20 100644 --- a/packages/ui/src/components/AgentDefaultsEditor.tsx +++ b/packages/ui/src/components/AgentDefaultsEditor.tsx @@ -150,7 +150,7 @@ export function AgentDefaultsEditor({ transition={smoothTransition} onClick={(e) => e.stopPropagation()} className={cn( - 'bg-gray-800 max-h-[90vh] overflow-hidden flex flex-col', + 'bg-surface-subtle max-h-[90vh] overflow-hidden flex flex-col', responsive ? 'w-full md:w-full md:max-w-lg md:mx-4 rounded-t-2xl md:rounded-lg' : 'rounded-lg w-full max-w-lg mx-4' @@ -159,19 +159,19 @@ export function AgentDefaultsEditor({ {/* Header */}
{responsive && ( -
+
)}

Global Defaults

-

+

These settings apply to all agents unless overridden

@@ -203,9 +203,9 @@ export function AgentDefaultsEditor({ {/* Execution Limits */}
-

Execution Limits

+

Execution Limits

- + -

+

Maximum number of tool call steps for all agents

@@ -224,15 +224,15 @@ export function AgentDefaultsEditor({ {/* Model Configuration */}
-

Default Model

+

Default Model

{/* Provider */}
- + setModelName(e.target.value)} placeholder="claude-sonnet-4-20250514" - className="w-full px-3 py-2 bg-gray-900 border border-gray-700 rounded-lg text-sm focus:outline-none focus:border-primary-500" + className="w-full px-3 py-2 bg-surface-base border border-line rounded-lg text-sm focus:outline-none focus:border-primary-500" />
{/* Temperature */}
- +
{/* Max Tokens */}
- +
@@ -301,7 +301,7 @@ export function AgentDefaultsEditor({ {/* Footer */}
diff --git a/packages/ui/src/components/AgentEditor.tsx b/packages/ui/src/components/AgentEditor.tsx index 02cd98f..17e08c8 100644 --- a/packages/ui/src/components/AgentEditor.tsx +++ b/packages/ui/src/components/AgentEditor.tsx @@ -55,10 +55,10 @@ function CollapsibleSection({ const [isOpen, setIsOpen] = useState(defaultOpen); return ( -
+
))}
-

+

{mode === 'primary' ? 'Can be used as the main agent' : mode === 'subagent' @@ -435,12 +435,12 @@ export function AgentEditor({ {/* Internal Mode 显示只读标签 */} {isInternalAgent && (

- +
Internal (System Agent)
-

+

System agents are used internally and cannot be called directly

@@ -456,9 +456,9 @@ export function AgentEditor({ onChange={(e) => setPrompt(e.target.value)} placeholder="You are a helpful assistant..." rows={8} - className="w-full px-3 py-2 bg-gray-900 border border-gray-700 rounded-lg text-sm font-mono focus:outline-none focus:border-primary-500 resize-y" + className="w-full px-3 py-2 bg-surface-base border border-line rounded-lg text-sm font-mono focus:outline-none focus:border-primary-500 resize-y" /> -

+

Custom system prompt for this agent. Leave empty to use defaults.

@@ -470,11 +470,11 @@ export function AgentEditor({
{/* Provider */}
- + setModelName(e.target.value)} placeholder="claude-sonnet-4-20250514" - className="w-full px-3 py-2 bg-gray-900 border border-gray-700 rounded-lg text-sm focus:outline-none focus:border-primary-500" + className="w-full px-3 py-2 bg-surface-base border border-line rounded-lg text-sm focus:outline-none focus:border-primary-500" />
{/* Temperature */}
- +
{/* Max Tokens */}
- +
@@ -535,7 +535,7 @@ export function AgentEditor({
{/* Tool Mode */}
- +
{(['all', 'enabled', 'disabled'] as const).map((m) => (
@@ -593,7 +593,7 @@ export function AgentEditor({ {!isInternalAgent && (
- + -

+

Maximum number of tool call steps. Leave empty for default.

@@ -617,7 +617,7 @@ export function AgentEditor({ {/* Footer */}
diff --git a/packages/ui/src/components/AgentsPanel.tsx b/packages/ui/src/components/AgentsPanel.tsx index 447f857..c1a38d1 100644 --- a/packages/ui/src/components/AgentsPanel.tsx +++ b/packages/ui/src/components/AgentsPanel.tsx @@ -56,7 +56,7 @@ function getModeColor(mode: AgentListItem['mode']) { case 'internal': return 'bg-slate-500/20 text-slate-400'; default: - return 'bg-gray-500/20 text-gray-400'; + return 'bg-surface-muted/20 text-fg-muted'; } } @@ -203,7 +203,7 @@ export function AgentsPanel({ onClose, responsive = false }: AgentsPanelProps) { const LoadingSkeleton = () => (
{[1, 2, 3, 4].map((i) => ( -
+
@@ -227,18 +227,18 @@ export function AgentsPanel({ onClose, responsive = false }: AgentsPanelProps) { layout initial={{ opacity: 0, y: 10 }} animate={{ opacity: 1, y: 0 }} - className="bg-gray-900/50 rounded-lg overflow-hidden" + className="bg-surface-base/50 rounded-lg overflow-hidden" > {/* Agent Header */}
toggleExpanded(agent.name)} > {/* Expand Icon */} - @@ -254,7 +254,7 @@ export function AgentsPanel({ onClose, responsive = false }: AgentsPanelProps) { {/* Info */}
- {agent.name} + {agent.name} {getModeText(agent.mode)} @@ -264,7 +264,7 @@ export function AgentsPanel({ onClose, responsive = false }: AgentsPanelProps) { )}
-

{agent.description}

+

{agent.description}

{/* Actions */} @@ -289,7 +289,7 @@ export function AgentsPanel({ onClose, responsive = false }: AgentsPanelProps) { variant="ghost" size="sm" onClick={() => toggleExpanded(agent.name)} - className="text-gray-400 hover:text-gray-300" + className="text-fg-muted hover:text-fg-secondary" title="View" > @@ -312,7 +312,7 @@ export function AgentsPanel({ onClose, responsive = false }: AgentsPanelProps) { variant="ghost" size="sm" onClick={() => handleCopy(agent.name)} - className="text-gray-400 hover:text-gray-300" + className="text-fg-muted hover:text-fg-secondary" title="Copy" > @@ -346,21 +346,21 @@ export function AgentsPanel({ onClose, responsive = false }: AgentsPanelProps) { transition={{ duration: 0.2 }} className="overflow-hidden" > -
+
{detail ? ( <> {/* Model Info */} {detail.model && (
- +
- Model:{' '} - + Model:{' '} + {detail.model.provider && `${detail.model.provider}/`} {detail.model.model || 'default'} {detail.model.temperature !== undefined && ( - + temp: {detail.model.temperature} )} @@ -371,9 +371,9 @@ export function AgentsPanel({ onClose, responsive = false }: AgentsPanelProps) { {/* Tools Config */} {detail.tools && (
- +
- Tools:{' '} + Tools:{' '} {detail.tools.enabled ? ( Only: {detail.tools.enabled.join(', ')} @@ -383,7 +383,7 @@ export function AgentsPanel({ onClose, responsive = false }: AgentsPanelProps) { Disabled: {detail.tools.disabled.join(', ')} ) : ( - All enabled + All enabled )} {detail.tools.noTask && ( (No nested tasks) @@ -395,16 +395,16 @@ export function AgentsPanel({ onClose, responsive = false }: AgentsPanelProps) { {/* Max Steps */} {detail.maxSteps && (
- Max Steps:{' '} - {detail.maxSteps} + Max Steps:{' '} + {detail.maxSteps}
)} {/* Prompt Preview */} {detail.prompt && (
- System Prompt: -
+                        System Prompt:
+                        
                           {detail.prompt.slice(0, 500)}
                           {detail.prompt.length > 500 && '...'}
                         
@@ -474,7 +474,7 @@ export function AgentsPanel({ onClose, responsive = false }: AgentsPanelProps) { transition={smoothTransition} onClick={(e) => e.stopPropagation()} className={cn( - 'bg-gray-800 max-h-[90vh] overflow-hidden flex flex-col', + 'bg-surface-subtle max-h-[90vh] overflow-hidden flex flex-col', responsive ? 'w-full md:w-full md:max-w-2xl md:mx-4 rounded-t-2xl md:rounded-lg' : 'rounded-lg w-full max-w-2xl mx-4' @@ -483,19 +483,19 @@ export function AgentsPanel({ onClose, responsive = false }: AgentsPanelProps) { {/* Header */}
{responsive && ( -
+
)}

Agent Presets

-

+

{agents.length} agents ({internalAgents.length} system, {presetAgents.length} preset, {customAgents.length} custom)

@@ -544,7 +544,7 @@ export function AgentsPanel({ onClose, responsive = false }: AgentsPanelProps) { {loading ? ( ) : agents.length === 0 ? ( -
+

No agents available

diff --git a/packages/ui/src/components/ChatInput.tsx b/packages/ui/src/components/ChatInput.tsx index ade207c..442537b 100644 --- a/packages/ui/src/components/ChatInput.tsx +++ b/packages/ui/src/components/ChatInput.tsx @@ -142,7 +142,7 @@ export function ChatInput({ return (
@@ -174,10 +174,10 @@ export function ChatInput({ disabled={disabled} rows={1} className={clsx( - 'w-full resize-none rounded-lg border border-gray-600 bg-gray-800', + 'w-full resize-none rounded-lg border border-line bg-surface-subtle', responsive ? 'px-3 py-2.5 md:px-4 md:py-3' : 'px-4 py-3', responsive ? 'text-base md:text-sm' : 'text-sm', // 移动端使用 16px 防止缩放 - 'text-gray-100 placeholder-gray-500', + 'text-fg placeholder-fg-subtle', 'focus:outline-none focus:ring-2 focus:ring-primary-500 focus:border-transparent', 'disabled:opacity-50 disabled:cursor-not-allowed' )} @@ -202,7 +202,7 @@ export function ChatInput({
{/* 响应式模式下桌面端显示提示文字 */} {responsive && ( -

+

Press Enter to send, Shift+Enter for new line, / for commands

)} diff --git a/packages/ui/src/components/ChatMessage.tsx b/packages/ui/src/components/ChatMessage.tsx index 33a4675..e0c2a1f 100644 --- a/packages/ui/src/components/ChatMessage.tsx +++ b/packages/ui/src/components/ChatMessage.tsx @@ -42,7 +42,7 @@ export const ChatMessage = forwardRef( // 优先使用 parts 数组(保持原始顺序) if (message.parts && message.parts.length > 0) { return ( -
+
{message.parts.map((part) => { switch (part.type) { case 'text': @@ -58,7 +58,7 @@ export const ChatMessage = forwardRef( return ; case 'reasoning': return ( -
+
{part.text}
); @@ -76,7 +76,7 @@ export const ChatMessage = forwardRef( {!isUser && message.toolCalls && message.toolCalls.length > 0 && ( )} -
+
{isUser ? (
{message.content ?? ''}
) : ( @@ -97,12 +97,12 @@ export const ChatMessage = forwardRef( transition={smoothTransition} className={cn( 'group flex gap-4 p-4 rounded-lg', - isUser ? 'bg-gray-800' : 'bg-gray-800/50' + isUser ? 'bg-surface-subtle' : 'bg-surface-subtle/50' )} >
@@ -110,12 +110,12 @@ export const ChatMessage = forwardRef(
- + {isUser ? 'You' : 'AI Assistant'} @@ -397,7 +397,7 @@ export function CheckpointDiffViewer({ {/* File Path */} handleViewFileDiff(file.path)} > {file.path} @@ -405,7 +405,7 @@ export function CheckpointDiffViewer({ {/* Stats */} {(file.insertions !== undefined || file.deletions !== undefined) && ( - + {file.insertions !== undefined && ( +{file.insertions} )} @@ -424,18 +424,18 @@ export function CheckpointDiffViewer({ animate={{ height: 'auto', opacity: 1 }} exit={{ height: 0, opacity: 0 }} transition={{ duration: 0.2 }} - className="overflow-hidden bg-gray-900/30" + className="overflow-hidden bg-surface-base/30" > {loadingFileDiff ? (
) : fileDiff?.patch ? ( -
+
{renderPatch(fileDiff.patch)}
) : ( -
+
No diff content available
)} @@ -453,11 +453,11 @@ export function CheckpointDiffViewer({ {diff && diff.files.length > 0 && (onRestoreSelected || onRestoreAll) && (
- + {selectedFiles.size} of {diff.files.length} files selected
diff --git a/packages/ui/src/components/CheckpointPanel.tsx b/packages/ui/src/components/CheckpointPanel.tsx index abd9ff0..3758628 100644 --- a/packages/ui/src/components/CheckpointPanel.tsx +++ b/packages/ui/src/components/CheckpointPanel.tsx @@ -71,10 +71,10 @@ function getTriggerInfo(trigger: CheckpointTrigger) { case 'session_end': return { icon: '⏹️', label: 'Session End', color: 'text-cyan-400' }; case 'pre_rollback': - return { icon: '🔙', label: 'Pre-Rollback', color: 'text-gray-400' }; + return { icon: '🔙', label: 'Pre-Rollback', color: 'text-fg-muted' }; case 'auto': default: - return { icon: '⚪', label: 'Auto', color: 'text-gray-400' }; + return { icon: '⚪', label: 'Auto', color: 'text-fg-muted' }; } } @@ -295,7 +295,7 @@ export function CheckpointPanel({ const LoadingSkeleton = () => (
{[1, 2, 3, 4].map((i) => ( -
+
@@ -329,7 +329,7 @@ export function CheckpointPanel({ transition={smoothTransition} onClick={(e) => e.stopPropagation()} className={cn( - 'bg-gray-800 max-h-[90vh] overflow-hidden flex flex-col', + 'bg-surface-subtle max-h-[90vh] overflow-hidden flex flex-col', responsive ? 'w-full md:w-full md:max-w-2xl md:mx-4 rounded-t-2xl md:rounded-lg' : 'rounded-lg w-full max-w-2xl mx-4' @@ -338,19 +338,19 @@ export function CheckpointPanel({ {/* Header */}
{responsive && ( -
+
)}

Checkpoints

-

+

{stats ? `${stats.count} checkpoints` : 'Loading...'} {stats?.oldestTimestamp && ( <> · Oldest: {formatTime(stats.oldestTimestamp)} @@ -431,10 +431,10 @@ export function CheckpointPanel({ {loading ? ( ) : checkpoints.length === 0 ? ( -

+

No checkpoints yet

-

+

Checkpoints are created automatically when files are modified

{/* Group Items */} @@ -488,7 +488,7 @@ export function CheckpointPanel({ key={cp.id} initial={{ opacity: 0, x: -10 }} animate={{ opacity: 1, x: 0 }} - className="bg-gray-900/50 rounded-lg p-3 hover:bg-gray-900/80 transition-colors" + className="bg-surface-base/50 rounded-lg p-3 hover:bg-surface-base/80 transition-colors" >
{/* Trigger Icon */} @@ -502,16 +502,16 @@ export function CheckpointPanel({ {triggerInfo.label} - + {formatTime(cp.timestamp)}
{cp.description && ( -

+

{cp.description}

)} -
+
{cp.filesChanged} files @@ -580,11 +580,11 @@ export function CheckpointPanel({ {/* Footer */}
- + Auto-cleanup enabled (7 days / 100 max)