feat(ui): 实现深色/浅色主题切换功能

- 添加 CSS 变量定义浅色和深色主题色板
- 扩展 Tailwind 配置支持语义化颜色 (surface-*, fg-*, line-*, code)
- 创建 useTheme hook 管理主题状态和持久化
- 创建 ThemeToggle 组件支持三种模式 (light/dark/system)
- 迁移所有组件从硬编码 gray-* 到语义化颜色
- 支持系统主题偏好检测 (prefers-color-scheme)
- 添加主题初始化脚本防止闪烁 (FOUC)
This commit is contained in:
2025-12-15 15:47:32 +08:00
parent cd0dd814ab
commit 5b7b0ff1e4
39 changed files with 1002 additions and 652 deletions
@@ -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 */}
<div
className={cn(
'flex items-center justify-between border-b border-gray-700',
'flex items-center justify-between border-b border-line',
responsive ? 'px-4 md:px-6 py-4' : 'px-6 py-4'
)}
>
{responsive && (
<div className="absolute top-2 left-1/2 -translate-x-1/2 w-10 h-1 bg-gray-600 rounded-full md:hidden" />
<div className="absolute top-2 left-1/2 -translate-x-1/2 w-10 h-1 bg-surface-emphasis rounded-full md:hidden" />
)}
<div className={cn(responsive && 'mt-2 md:mt-0')}>
<h2 className="text-lg font-semibold flex items-center gap-2">
<Settings size={20} className="text-primary-400" />
Global Defaults
</h2>
<p className="text-xs text-gray-500">
<p className="text-xs text-fg-subtle">
These settings apply to all agents unless overridden
</p>
</div>
@@ -203,9 +203,9 @@ export function AgentDefaultsEditor({
{/* Execution Limits */}
<div className="space-y-4">
<h3 className="text-sm font-medium text-gray-300">Execution Limits</h3>
<h3 className="text-sm font-medium text-fg-secondary">Execution Limits</h3>
<div>
<label className="block text-xs text-gray-400 mb-1">Default Max Steps</label>
<label className="block text-xs text-fg-muted mb-1">Default Max Steps</label>
<input
type="number"
value={maxSteps ?? ''}
@@ -214,9 +214,9 @@ export function AgentDefaultsEditor({
}
placeholder="15"
min="1"
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"
/>
<p className="text-xs text-gray-500 mt-1">
<p className="text-xs text-fg-subtle mt-1">
Maximum number of tool call steps for all agents
</p>
</div>
@@ -224,15 +224,15 @@ export function AgentDefaultsEditor({
{/* Model Configuration */}
<div className="space-y-4">
<h3 className="text-sm font-medium text-gray-300">Default Model</h3>
<h3 className="text-sm font-medium text-fg-secondary">Default Model</h3>
<div className="grid grid-cols-2 gap-4">
{/* Provider */}
<div>
<label className="block text-xs text-gray-400 mb-1">Provider</label>
<label className="block text-xs text-fg-muted mb-1">Provider</label>
<select
value={modelProvider}
onChange={(e) => setModelProvider(e.target.value)}
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"
>
<option value="">Default</option>
<option value="anthropic">Anthropic</option>
@@ -243,19 +243,19 @@ export function AgentDefaultsEditor({
{/* Model */}
<div>
<label className="block text-xs text-gray-400 mb-1">Model</label>
<label className="block text-xs text-fg-muted mb-1">Model</label>
<input
type="text"
value={modelName}
onChange={(e) => 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"
/>
</div>
{/* Temperature */}
<div>
<label className="block text-xs text-gray-400 mb-1">Temperature</label>
<label className="block text-xs text-fg-muted mb-1">Temperature</label>
<input
type="number"
value={temperature ?? ''}
@@ -266,13 +266,13 @@ export function AgentDefaultsEditor({
min="0"
max="1"
step="0.1"
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"
/>
</div>
{/* Max Tokens */}
<div>
<label className="block text-xs text-gray-400 mb-1">Max Tokens</label>
<label className="block text-xs text-fg-muted mb-1">Max Tokens</label>
<input
type="number"
value={maxTokens ?? ''}
@@ -281,7 +281,7 @@ export function AgentDefaultsEditor({
}
placeholder="8192"
min="1"
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"
/>
</div>
</div>
@@ -301,7 +301,7 @@ export function AgentDefaultsEditor({
{/* Footer */}
<div
className={cn(
'border-t border-gray-700 flex justify-end gap-2',
'border-t border-line flex justify-end gap-2',
responsive ? 'px-4 py-3 safe-area-pb' : 'px-6 py-3'
)}
>
+39 -39
View File
@@ -55,10 +55,10 @@ function CollapsibleSection({
const [isOpen, setIsOpen] = useState(defaultOpen);
return (
<div className="border border-gray-700 rounded-lg overflow-hidden">
<div className="border border-line rounded-lg overflow-hidden">
<button
type="button"
className="w-full flex items-center justify-between p-3 bg-gray-800/50 hover:bg-gray-800 transition-colors"
className="w-full flex items-center justify-between p-3 bg-surface-subtle/50 hover:bg-surface-subtle transition-colors"
onClick={() => setIsOpen(!isOpen)}
>
<span className="font-medium text-sm">{title}</span>
@@ -72,7 +72,7 @@ function CollapsibleSection({
exit={{ height: 0, opacity: 0 }}
transition={{ duration: 0.2 }}
>
<div className="p-4 border-t border-gray-700 space-y-4">{children}</div>
<div className="p-4 border-t border-line space-y-4">{children}</div>
</motion.div>
)}
</AnimatePresence>
@@ -284,7 +284,7 @@ export function AgentEditor({
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'
@@ -293,12 +293,12 @@ export function AgentEditor({
{/* Header */}
<div
className={cn(
'flex items-center justify-between border-b border-gray-700',
'flex items-center justify-between border-b border-line',
responsive ? 'px-4 md:px-6 py-4' : 'px-6 py-4'
)}
>
{responsive && (
<div className="absolute top-2 left-1/2 -translate-x-1/2 w-10 h-1 bg-gray-600 rounded-full md:hidden" />
<div className="absolute top-2 left-1/2 -translate-x-1/2 w-10 h-1 bg-surface-emphasis rounded-full md:hidden" />
)}
<div className={cn(responsive && 'mt-2 md:mt-0')}>
<h2 className="text-lg font-semibold flex items-center gap-2">
@@ -315,7 +315,7 @@ export function AgentEditor({
)}
</h2>
{copyFrom && (
<p className="text-xs text-gray-500">Copying from: {copyFrom}</p>
<p className="text-xs text-fg-subtle">Copying from: {copyFrom}</p>
)}
{isInternalAgent && (
<p className="text-xs text-slate-400 mt-1">
@@ -363,11 +363,11 @@ export function AgentEditor({
{/* Basic Info */}
<div className="space-y-4">
<h3 className="text-sm font-medium text-gray-300">Basic Information</h3>
<h3 className="text-sm font-medium text-fg-secondary">Basic Information</h3>
{/* Name */}
<div>
<label className="block text-xs text-gray-400 mb-1">Name *</label>
<label className="block text-xs text-fg-muted mb-1">Name *</label>
<input
type="text"
value={name}
@@ -375,19 +375,19 @@ export function AgentEditor({
disabled={!isNewAgent || isInternalAgent}
placeholder="my-agent"
className={cn(
'w-full px-3 py-2 bg-gray-900 border border-gray-700 rounded-lg text-sm',
'w-full px-3 py-2 bg-surface-base border border-line rounded-lg text-sm',
'focus:outline-none focus:border-primary-500',
(!isNewAgent || isInternalAgent) && 'opacity-50 cursor-not-allowed'
)}
/>
{!isNewAgent && (
<p className="text-xs text-gray-500 mt-1">Name cannot be changed after creation</p>
<p className="text-xs text-fg-subtle mt-1">Name cannot be changed after creation</p>
)}
</div>
{/* Description */}
<div>
<label className="block text-xs text-gray-400 mb-1">Description *</label>
<label className="block text-xs text-fg-muted mb-1">Description *</label>
<input
type="text"
value={description}
@@ -395,7 +395,7 @@ export function AgentEditor({
disabled={isInternalAgent}
placeholder="A helpful coding assistant"
className={cn(
'w-full px-3 py-2 bg-gray-900 border border-gray-700 rounded-lg text-sm focus:outline-none focus:border-primary-500',
'w-full px-3 py-2 bg-surface-base border border-line rounded-lg text-sm focus:outline-none focus:border-primary-500',
isInternalAgent && 'opacity-50 cursor-not-allowed'
)}
/>
@@ -404,7 +404,7 @@ export function AgentEditor({
{/* Mode - 不为内部 Agent 显示 */}
{!isInternalAgent && (
<div>
<label className="block text-xs text-gray-400 mb-1">Mode *</label>
<label className="block text-xs text-fg-muted mb-1">Mode *</label>
<div className="flex gap-2">
{(['primary', 'subagent', 'all'] as const).map((m) => (
<button
@@ -415,14 +415,14 @@ export function AgentEditor({
'px-3 py-2 rounded-lg text-sm transition-colors',
mode === m
? 'bg-primary-500 text-white'
: 'bg-gray-900 text-gray-400 hover:bg-gray-800'
: 'bg-surface-base text-fg-muted hover:bg-surface-subtle'
)}
>
{m === 'primary' ? 'Primary' : m === 'subagent' ? 'Subagent' : 'Both'}
</button>
))}
</div>
<p className="text-xs text-gray-500 mt-1">
<p className="text-xs text-fg-subtle mt-1">
{mode === 'primary'
? 'Can be used as the main agent'
: mode === 'subagent'
@@ -435,12 +435,12 @@ export function AgentEditor({
{/* Internal Mode 显示只读标签 */}
{isInternalAgent && (
<div>
<label className="block text-xs text-gray-400 mb-1">Mode</label>
<label className="block text-xs text-fg-muted mb-1">Mode</label>
<div className="px-3 py-2 bg-slate-500/10 border border-slate-600/30 rounded-lg text-sm text-slate-400 inline-flex items-center gap-2">
<Lock size={14} />
Internal (System Agent)
</div>
<p className="text-xs text-gray-500 mt-1">
<p className="text-xs text-fg-subtle mt-1">
System agents are used internally and cannot be called directly
</p>
</div>
@@ -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"
/>
<p className="text-xs text-gray-500 mt-1">
<p className="text-xs text-fg-subtle mt-1">
Custom system prompt for this agent. Leave empty to use defaults.
</p>
</div>
@@ -470,11 +470,11 @@ export function AgentEditor({
<div className="grid grid-cols-2 gap-4">
{/* Provider */}
<div>
<label className="block text-xs text-gray-400 mb-1">Provider</label>
<label className="block text-xs text-fg-muted mb-1">Provider</label>
<select
value={modelProvider}
onChange={(e) => setModelProvider(e.target.value)}
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"
>
<option value="">Default</option>
<option value="anthropic">Anthropic</option>
@@ -485,19 +485,19 @@ export function AgentEditor({
{/* Model */}
<div>
<label className="block text-xs text-gray-400 mb-1">Model</label>
<label className="block text-xs text-fg-muted mb-1">Model</label>
<input
type="text"
value={modelName}
onChange={(e) => 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"
/>
</div>
{/* Temperature */}
<div>
<label className="block text-xs text-gray-400 mb-1">Temperature</label>
<label className="block text-xs text-fg-muted mb-1">Temperature</label>
<input
type="number"
value={temperature ?? ''}
@@ -508,13 +508,13 @@ export function AgentEditor({
min="0"
max="1"
step="0.1"
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"
/>
</div>
{/* Max Tokens */}
<div>
<label className="block text-xs text-gray-400 mb-1">Max Tokens</label>
<label className="block text-xs text-fg-muted mb-1">Max Tokens</label>
<input
type="number"
value={maxTokens ?? ''}
@@ -523,7 +523,7 @@ export function AgentEditor({
}
placeholder="8192"
min="1"
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"
/>
</div>
</div>
@@ -535,7 +535,7 @@ export function AgentEditor({
<div className="space-y-4">
{/* Tool Mode */}
<div>
<label className="block text-xs text-gray-400 mb-1">Tool Access</label>
<label className="block text-xs text-fg-muted mb-1">Tool Access</label>
<div className="flex gap-2">
{(['all', 'enabled', 'disabled'] as const).map((m) => (
<button
@@ -546,7 +546,7 @@ export function AgentEditor({
'px-3 py-2 rounded-lg text-sm transition-colors',
toolMode === m
? 'bg-primary-500 text-white'
: 'bg-gray-900 text-gray-400 hover:bg-gray-800'
: 'bg-surface-base text-fg-muted hover:bg-surface-subtle'
)}
>
{m === 'all' ? 'All Tools' : m === 'enabled' ? 'Only These' : 'Except These'}
@@ -558,7 +558,7 @@ export function AgentEditor({
{/* Tool List */}
{toolMode !== 'all' && (
<div>
<label className="block text-xs text-gray-400 mb-1">
<label className="block text-xs text-fg-muted mb-1">
{toolMode === 'enabled' ? 'Enabled Tools' : 'Disabled Tools'}
</label>
<input
@@ -566,9 +566,9 @@ export function AgentEditor({
value={toolList}
onChange={(e) => setToolList(e.target.value)}
placeholder="bash, read_file, write_file"
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"
/>
<p className="text-xs text-gray-500 mt-1">Comma-separated tool names</p>
<p className="text-xs text-fg-subtle mt-1">Comma-separated tool names</p>
</div>
)}
@@ -579,9 +579,9 @@ export function AgentEditor({
id="noTask"
checked={noTask}
onChange={(e) => setNoTask(e.target.checked)}
className="w-4 h-4 rounded border-gray-600 bg-gray-900 text-primary-500 focus:ring-primary-500"
className="w-4 h-4 rounded border-line-muted bg-surface-base text-primary-500 focus:ring-primary-500"
/>
<label htmlFor="noTask" className="text-sm text-gray-300">
<label htmlFor="noTask" className="text-sm text-fg-secondary">
Disable nested Task calls
</label>
</div>
@@ -593,7 +593,7 @@ export function AgentEditor({
{!isInternalAgent && (
<CollapsibleSection title="Execution Limits" defaultOpen={maxSteps !== undefined}>
<div>
<label className="block text-xs text-gray-400 mb-1">Max Steps</label>
<label className="block text-xs text-fg-muted mb-1">Max Steps</label>
<input
type="number"
value={maxSteps ?? ''}
@@ -602,9 +602,9 @@ export function AgentEditor({
}
placeholder="15"
min="1"
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"
/>
<p className="text-xs text-gray-500 mt-1">
<p className="text-xs text-fg-subtle mt-1">
Maximum number of tool call steps. Leave empty for default.
</p>
</div>
@@ -617,7 +617,7 @@ export function AgentEditor({
{/* Footer */}
<div
className={cn(
'border-t border-gray-700 flex justify-end gap-2',
'border-t border-line flex justify-end gap-2',
responsive ? 'px-4 py-3 safe-area-pb' : 'px-6 py-3'
)}
>
+31 -31
View File
@@ -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 = () => (
<div className="space-y-3 p-4">
{[1, 2, 3, 4].map((i) => (
<div key={i} className="flex items-center gap-3 p-3 bg-gray-900/50 rounded-lg">
<div key={i} className="flex items-center gap-3 p-3 bg-surface-base/50 rounded-lg">
<Skeleton className="h-4 w-4" />
<div className="flex-1 space-y-2">
<Skeleton className="h-4 w-32" />
@@ -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 */}
<div
className={cn(
'flex items-center gap-3 p-3',
'hover:bg-gray-900/80 transition-colors cursor-pointer'
'hover:bg-surface-base/80 transition-colors cursor-pointer'
)}
onClick={() => toggleExpanded(agent.name)}
>
{/* Expand Icon */}
<button className="text-gray-500 hover:text-gray-300">
<button className="text-fg-subtle hover:text-fg-secondary">
{isExpanded ? <ChevronDown size={16} /> : <ChevronRight size={16} />}
</button>
@@ -254,7 +254,7 @@ export function AgentsPanel({ onClose, responsive = false }: AgentsPanelProps) {
{/* Info */}
<div className="flex-1 min-w-0">
<div className="flex items-center gap-2">
<span className="font-medium text-gray-200">{agent.name}</span>
<span className="font-medium text-fg-secondary">{agent.name}</span>
<span className={cn('text-xs px-2 py-0.5 rounded-full', getModeColor(agent.mode))}>
{getModeText(agent.mode)}
</span>
@@ -264,7 +264,7 @@ export function AgentsPanel({ onClose, responsive = false }: AgentsPanelProps) {
</span>
)}
</div>
<p className="text-xs text-gray-500 truncate">{agent.description}</p>
<p className="text-xs text-fg-subtle truncate">{agent.description}</p>
</div>
{/* 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"
>
<Eye size={14} />
@@ -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"
>
<Copy size={14} />
@@ -346,21 +346,21 @@ export function AgentsPanel({ onClose, responsive = false }: AgentsPanelProps) {
transition={{ duration: 0.2 }}
className="overflow-hidden"
>
<div className="px-4 pb-3 pt-1 border-t border-gray-700/50 space-y-3">
<div className="px-4 pb-3 pt-1 border-t border-line/50 space-y-3">
{detail ? (
<>
{/* Model Info */}
{detail.model && (
<div className="flex items-start gap-2">
<Cpu size={14} className="text-gray-500 mt-0.5" />
<Cpu size={14} className="text-fg-subtle mt-0.5" />
<div className="text-xs">
<span className="text-gray-400">Model:</span>{' '}
<span className="text-gray-300">
<span className="text-fg-muted">Model:</span>{' '}
<span className="text-fg-secondary">
{detail.model.provider && `${detail.model.provider}/`}
{detail.model.model || 'default'}
</span>
{detail.model.temperature !== undefined && (
<span className="text-gray-500 ml-2">
<span className="text-fg-subtle ml-2">
temp: {detail.model.temperature}
</span>
)}
@@ -371,9 +371,9 @@ export function AgentsPanel({ onClose, responsive = false }: AgentsPanelProps) {
{/* Tools Config */}
{detail.tools && (
<div className="flex items-start gap-2">
<Layers size={14} className="text-gray-500 mt-0.5" />
<Layers size={14} className="text-fg-subtle mt-0.5" />
<div className="text-xs">
<span className="text-gray-400">Tools:</span>{' '}
<span className="text-fg-muted">Tools:</span>{' '}
{detail.tools.enabled ? (
<span className="text-green-400">
Only: {detail.tools.enabled.join(', ')}
@@ -383,7 +383,7 @@ export function AgentsPanel({ onClose, responsive = false }: AgentsPanelProps) {
Disabled: {detail.tools.disabled.join(', ')}
</span>
) : (
<span className="text-gray-300">All enabled</span>
<span className="text-fg-secondary">All enabled</span>
)}
{detail.tools.noTask && (
<span className="text-red-400 ml-2">(No nested tasks)</span>
@@ -395,16 +395,16 @@ export function AgentsPanel({ onClose, responsive = false }: AgentsPanelProps) {
{/* Max Steps */}
{detail.maxSteps && (
<div className="text-xs">
<span className="text-gray-400">Max Steps:</span>{' '}
<span className="text-gray-300">{detail.maxSteps}</span>
<span className="text-fg-muted">Max Steps:</span>{' '}
<span className="text-fg-secondary">{detail.maxSteps}</span>
</div>
)}
{/* Prompt Preview */}
{detail.prompt && (
<div className="text-xs">
<span className="text-gray-400">System Prompt:</span>
<pre className="mt-1 p-2 bg-gray-800/50 rounded text-gray-300 overflow-x-auto max-h-32 text-[11px] leading-relaxed">
<span className="text-fg-muted">System Prompt:</span>
<pre className="mt-1 p-2 bg-surface-subtle/50 rounded text-fg-secondary overflow-x-auto max-h-32 text-[11px] leading-relaxed">
{detail.prompt.slice(0, 500)}
{detail.prompt.length > 500 && '...'}
</pre>
@@ -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 */}
<div
className={cn(
'flex items-center justify-between border-b border-gray-700',
'flex items-center justify-between border-b border-line',
responsive ? 'px-4 md:px-6 py-4' : 'px-6 py-4'
)}
>
{responsive && (
<div className="absolute top-2 left-1/2 -translate-x-1/2 w-10 h-1 bg-gray-600 rounded-full md:hidden" />
<div className="absolute top-2 left-1/2 -translate-x-1/2 w-10 h-1 bg-surface-emphasis rounded-full md:hidden" />
)}
<div className={cn(responsive && 'mt-2 md:mt-0')}>
<h2 className="text-lg font-semibold flex items-center gap-2">
<Bot size={20} className="text-primary-400" />
Agent Presets
</h2>
<p className="text-xs text-gray-500">
<p className="text-xs text-fg-subtle">
{agents.length} agents ({internalAgents.length} system, {presetAgents.length} preset, {customAgents.length} custom)
</p>
</div>
@@ -544,7 +544,7 @@ export function AgentsPanel({ onClose, responsive = false }: AgentsPanelProps) {
{loading ? (
<LoadingSkeleton />
) : agents.length === 0 ? (
<div className="flex flex-col items-center justify-center py-12 text-gray-500">
<div className="flex flex-col items-center justify-center py-12 text-fg-subtle">
<Bot size={48} className="mb-4 opacity-50" />
<p className="text-center">No agents available</p>
<Button
@@ -580,7 +580,7 @@ export function AgentsPanel({ onClose, responsive = false }: AgentsPanelProps) {
{/* Preset Agents */}
{presetAgents.length > 0 && (
<div>
<h3 className="text-xs font-medium text-gray-400 uppercase tracking-wide mb-2 flex items-center gap-2">
<h3 className="text-xs font-medium text-fg-muted uppercase tracking-wide mb-2 flex items-center gap-2">
<Sparkles size={12} />
Preset Agents
</h3>
@@ -594,7 +594,7 @@ export function AgentsPanel({ onClose, responsive = false }: AgentsPanelProps) {
{/* Custom Agents */}
<div>
<h3 className="text-xs font-medium text-gray-400 uppercase tracking-wide mb-2 flex items-center gap-2">
<h3 className="text-xs font-medium text-fg-muted uppercase tracking-wide mb-2 flex items-center gap-2">
<Bot size={12} />
Custom Agents
</h3>
@@ -605,7 +605,7 @@ export function AgentsPanel({ onClose, responsive = false }: AgentsPanelProps) {
))}
</div>
) : (
<div className="text-center py-6 text-gray-500 text-sm">
<div className="text-center py-6 text-fg-subtle text-sm">
<p>No custom agents yet</p>
<Button
variant="ghost"
@@ -626,12 +626,12 @@ export function AgentsPanel({ onClose, responsive = false }: AgentsPanelProps) {
{/* Footer Info */}
<div
className={cn(
'border-t border-gray-700 text-xs text-gray-500 text-center',
'border-t border-line text-xs text-fg-subtle text-center',
responsive ? 'px-4 py-3 safe-area-pb' : 'px-6 py-3'
)}
>
Config stored in{' '}
<code className="font-mono bg-gray-900 px-1 rounded">.ai-assist/agents.json</code>
<code className="font-mono bg-surface-base px-1 rounded">.ai-assist/agents.json</code>
</div>
</motion.div>
</motion.div>
+4 -4
View File
@@ -142,7 +142,7 @@ export function ChatInput({
return (
<div
className={clsx(
'border-t border-gray-700 bg-gray-900 relative',
'border-t border-line bg-surface-base relative',
responsive ? 'p-3 md:p-4 safe-area-pb' : 'p-4'
)}
>
@@ -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({
</div>
{/* 响应式模式下桌面端显示提示文字 */}
{responsive && (
<p className="hidden md:block text-xs text-gray-500 text-center mt-2">
<p className="hidden md:block text-xs text-fg-subtle text-center mt-2">
Press Enter to send, Shift+Enter for new line, / for commands
</p>
)}
+39 -39
View File
@@ -42,7 +42,7 @@ export const ChatMessage = forwardRef<HTMLDivElement, ChatMessageProps>(
// 优先使用 parts 数组(保持原始顺序)
if (message.parts && message.parts.length > 0) {
return (
<div className="message-content text-gray-200 space-y-3">
<div className="message-content text-fg-secondary space-y-3">
{message.parts.map((part) => {
switch (part.type) {
case 'text':
@@ -58,7 +58,7 @@ export const ChatMessage = forwardRef<HTMLDivElement, ChatMessageProps>(
return <ToolPartItem key={part.id} part={part} />;
case 'reasoning':
return (
<div key={part.id} className="text-gray-400 italic border-l-2 border-gray-600 pl-3">
<div key={part.id} className="text-fg-muted italic border-l-2 border-line pl-3">
{part.text}
</div>
);
@@ -76,7 +76,7 @@ export const ChatMessage = forwardRef<HTMLDivElement, ChatMessageProps>(
{!isUser && message.toolCalls && message.toolCalls.length > 0 && (
<ToolCallsDisplay toolCalls={message.toolCalls} />
)}
<div className="message-content text-gray-200">
<div className="message-content text-fg-secondary">
{isUser ? (
<div className="whitespace-pre-wrap break-words">{message.content ?? ''}</div>
) : (
@@ -97,12 +97,12 @@ export const ChatMessage = forwardRef<HTMLDivElement, ChatMessageProps>(
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'
)}
>
<div
className={cn(
'w-8 h-8 rounded-full flex items-center justify-center flex-shrink-0',
'w-8 h-8 rounded-full flex items-center justify-center flex-shrink-0 text-white',
isUser ? 'bg-primary-600' : 'bg-green-600'
)}
>
@@ -110,12 +110,12 @@ export const ChatMessage = forwardRef<HTMLDivElement, ChatMessageProps>(
</div>
<div className="flex-1 min-w-0 overflow-hidden">
<div className="flex items-center justify-between mb-1">
<span className="text-sm text-gray-400">
<span className="text-sm text-fg-muted">
{isUser ? 'You' : 'AI Assistant'}
</span>
<button
onClick={handleCopy}
className="opacity-0 group-hover:opacity-100 p-1 rounded text-gray-500 hover:text-gray-300 hover:bg-gray-700 transition-all"
className="opacity-0 group-hover:opacity-100 p-1 rounded text-fg-subtle hover:text-fg-muted hover:bg-surface-muted transition-all"
title="Copy message"
>
{copied ? <Check size={14} className="text-green-500" /> : <Copy size={14} />}
@@ -138,14 +138,14 @@ export function StreamingMessage({ content }: StreamingMessageProps) {
initial={{ opacity: 0, y: 10 }}
animate={{ opacity: 1, y: 0 }}
transition={smoothTransition}
className="flex gap-4 p-4 rounded-lg bg-gray-800/50"
className="flex gap-4 p-4 rounded-lg bg-surface-subtle/50"
>
<div className="w-8 h-8 rounded-full flex items-center justify-center flex-shrink-0 bg-green-600">
<div className="w-8 h-8 rounded-full flex items-center justify-center flex-shrink-0 bg-green-600 text-white">
<Bot size={18} />
</div>
<div className="flex-1 min-w-0 overflow-hidden">
<div className="text-sm text-gray-400 mb-1">AI Assistant</div>
<div className="message-content text-gray-200">
<div className="text-sm text-fg-muted mb-1">AI Assistant</div>
<div className="message-content text-fg-secondary">
<Markdown content={content} />
<motion.span
animate={{ opacity: [1, 0] }}
@@ -164,13 +164,13 @@ export function TypingIndicator() {
initial={{ opacity: 0, y: 10 }}
animate={{ opacity: 1, y: 0 }}
transition={smoothTransition}
className="flex gap-4 p-4 rounded-lg bg-gray-800/50"
className="flex gap-4 p-4 rounded-lg bg-surface-subtle/50"
>
<div className="w-8 h-8 rounded-full flex items-center justify-center flex-shrink-0 bg-green-600">
<div className="w-8 h-8 rounded-full flex items-center justify-center flex-shrink-0 bg-green-600 text-white">
<Bot size={18} />
</div>
<div className="flex-1">
<div className="text-sm text-gray-400 mb-1">AI Assistant</div>
<div className="text-sm text-fg-muted mb-1">AI Assistant</div>
<div className="flex items-center gap-1 h-6">
{[0, 1, 2].map((i) => (
<motion.span
@@ -181,7 +181,7 @@ export function TypingIndicator() {
repeat: Infinity,
delay: i * 0.15,
}}
className="w-2 h-2 rounded-full bg-gray-400"
className="w-2 h-2 rounded-full bg-fg-muted"
/>
))}
</div>
@@ -207,26 +207,26 @@ function ToolPartItem({ part }: ToolPartItemProps) {
part.error !== undefined;
return (
<div className="border border-gray-700 rounded-lg overflow-hidden bg-gray-800/30">
<div className="border border-line rounded-lg overflow-hidden bg-surface-subtle/30">
{/* 头部:工具名称、状态、时长 */}
<button
onClick={() => hasDetails && setExpanded(!expanded)}
className={cn(
'w-full flex items-center gap-2 px-3 py-2 text-sm',
hasDetails && 'hover:bg-gray-700/50 cursor-pointer',
hasDetails && 'hover:bg-surface-muted/50 cursor-pointer',
!hasDetails && 'cursor-default'
)}
>
<Wrench size={14} className="text-gray-400 flex-shrink-0" />
<span className="font-mono text-gray-200 flex-1 text-left truncate">
<Wrench size={14} className="text-fg-muted flex-shrink-0" />
<span className="font-mono text-fg-secondary flex-1 text-left truncate">
{part.toolName}
</span>
{getStatusIcon(part.status)}
{part.duration && (
<span className="text-xs text-gray-500">{formatDuration(part.duration)}</span>
<span className="text-xs text-fg-subtle">{formatDuration(part.duration)}</span>
)}
{hasDetails && (
<span className="text-gray-500">
<span className="text-fg-subtle">
{expanded ? <ChevronDown size={14} /> : <ChevronRight size={14} />}
</span>
)}
@@ -240,14 +240,14 @@ function ToolPartItem({ part }: ToolPartItemProps) {
animate={{ height: 'auto', opacity: 1 }}
exit={{ height: 0, opacity: 0 }}
transition={{ duration: 0.2 }}
className="border-t border-gray-700 overflow-hidden"
className="border-t border-line overflow-hidden"
>
<div className="px-3 py-2 space-y-2 text-xs">
{/* 参数 */}
{Object.keys(part.arguments).length > 0 && (
<div>
<div className="text-gray-500 mb-1">Arguments:</div>
<pre className="bg-gray-900 rounded p-2 overflow-x-auto text-gray-300 max-h-48 overflow-y-auto">
<div className="text-fg-subtle mb-1">Arguments:</div>
<pre className="bg-surface-base rounded p-2 overflow-x-auto text-fg-muted max-h-48 overflow-y-auto">
{JSON.stringify(part.arguments, null, 2)}
</pre>
</div>
@@ -256,8 +256,8 @@ function ToolPartItem({ part }: ToolPartItemProps) {
{/* 结果 */}
{part.result !== undefined && (
<div>
<div className="text-gray-500 mb-1">Result:</div>
<pre className="bg-gray-900 rounded p-2 overflow-x-auto text-green-300 max-h-48 overflow-y-auto">
<div className="text-fg-subtle mb-1">Result:</div>
<pre className="bg-surface-base rounded p-2 overflow-x-auto text-green-400 max-h-48 overflow-y-auto">
{typeof part.result === 'string'
? part.result
: JSON.stringify(part.result, null, 2)}
@@ -269,7 +269,7 @@ function ToolPartItem({ part }: ToolPartItemProps) {
{part.error && (
<div>
<div className="text-red-400 mb-1">Error:</div>
<pre className="bg-gray-900 rounded p-2 overflow-x-auto text-red-300 max-h-48 overflow-y-auto">
<pre className="bg-surface-base rounded p-2 overflow-x-auto text-red-300 max-h-48 overflow-y-auto">
{part.error}
</pre>
</div>
@@ -339,26 +339,26 @@ function ToolCallItem({ toolCall }: ToolCallItemProps) {
toolCall.error !== undefined;
return (
<div className="border border-gray-700 rounded-lg overflow-hidden bg-gray-800/30">
<div className="border border-line rounded-lg overflow-hidden bg-surface-subtle/30">
{/* 头部:工具名称、状态、时长 */}
<button
onClick={() => hasDetails && setExpanded(!expanded)}
className={cn(
'w-full flex items-center gap-2 px-3 py-2 text-sm',
hasDetails && 'hover:bg-gray-700/50 cursor-pointer',
hasDetails && 'hover:bg-surface-muted/50 cursor-pointer',
!hasDetails && 'cursor-default'
)}
>
<Wrench size={14} className="text-gray-400 flex-shrink-0" />
<span className="font-mono text-gray-200 flex-1 text-left truncate">
<Wrench size={14} className="text-fg-muted flex-shrink-0" />
<span className="font-mono text-fg-secondary flex-1 text-left truncate">
{toolCall.name}
</span>
{getStatusIcon(toolCall.status)}
{toolCall.duration && (
<span className="text-xs text-gray-500">{formatDuration(toolCall.duration)}</span>
<span className="text-xs text-fg-subtle">{formatDuration(toolCall.duration)}</span>
)}
{hasDetails && (
<span className="text-gray-500">
<span className="text-fg-subtle">
{expanded ? <ChevronDown size={14} /> : <ChevronRight size={14} />}
</span>
)}
@@ -372,14 +372,14 @@ function ToolCallItem({ toolCall }: ToolCallItemProps) {
animate={{ height: 'auto', opacity: 1 }}
exit={{ height: 0, opacity: 0 }}
transition={{ duration: 0.2 }}
className="border-t border-gray-700 overflow-hidden"
className="border-t border-line overflow-hidden"
>
<div className="px-3 py-2 space-y-2 text-xs">
{/* 参数 */}
{Object.keys(toolCall.arguments).length > 0 && (
<div>
<div className="text-gray-500 mb-1">Arguments:</div>
<pre className="bg-gray-900 rounded p-2 overflow-x-auto text-gray-300 max-h-48 overflow-y-auto">
<div className="text-fg-subtle mb-1">Arguments:</div>
<pre className="bg-surface-base rounded p-2 overflow-x-auto text-fg-muted max-h-48 overflow-y-auto">
{JSON.stringify(toolCall.arguments, null, 2)}
</pre>
</div>
@@ -388,8 +388,8 @@ function ToolCallItem({ toolCall }: ToolCallItemProps) {
{/* 结果 */}
{toolCall.result !== undefined && (
<div>
<div className="text-gray-500 mb-1">Result:</div>
<pre className="bg-gray-900 rounded p-2 overflow-x-auto text-green-300 max-h-48 overflow-y-auto">
<div className="text-fg-subtle mb-1">Result:</div>
<pre className="bg-surface-base rounded p-2 overflow-x-auto text-green-400 max-h-48 overflow-y-auto">
{typeof toolCall.result === 'string'
? toolCall.result
: JSON.stringify(toolCall.result, null, 2)}
@@ -401,7 +401,7 @@ function ToolCallItem({ toolCall }: ToolCallItemProps) {
{toolCall.error && (
<div>
<div className="text-red-400 mb-1">Error:</div>
<pre className="bg-gray-900 rounded p-2 overflow-x-auto text-red-300 max-h-48 overflow-y-auto">
<pre className="bg-surface-base rounded p-2 overflow-x-auto text-red-300 max-h-48 overflow-y-auto">
{toolCall.error}
</pre>
</div>
@@ -71,7 +71,7 @@ function getChangeColor(type: FileChangeType) {
case 'renamed':
return 'text-blue-400 bg-blue-400/10';
default:
return 'text-gray-400 bg-gray-400/10';
return 'text-fg-muted bg-surface-muted/10';
}
}
@@ -203,7 +203,7 @@ export function CheckpointDiffViewer({
<Skeleton className="h-6 w-64" />
<div className="space-y-2">
{[1, 2, 3, 4].map((i) => (
<div key={i} className="flex items-center gap-3 p-2 bg-gray-900/50 rounded">
<div key={i} className="flex items-center gap-3 p-2 bg-surface-base/50 rounded">
<Skeleton className="h-4 w-4 rounded" />
<Skeleton className="h-4 w-48" />
<Skeleton className="h-3 w-16 ml-auto" />
@@ -227,7 +227,7 @@ export function CheckpointDiffViewer({
} else if (line.startsWith('@@')) {
className += ' bg-blue-500/10 text-blue-400';
} else {
className += ' text-gray-400';
className += ' text-fg-muted';
}
return (
<div key={index} className={className}>
@@ -261,7 +261,7 @@ export function CheckpointDiffViewer({
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-3xl md:mx-4 rounded-t-2xl md:rounded-lg'
: 'rounded-lg w-full max-w-3xl mx-4'
@@ -270,22 +270,22 @@ export function CheckpointDiffViewer({
{/* Header */}
<div
className={cn(
'flex items-center justify-between border-b border-gray-700',
'flex items-center justify-between border-b border-line',
responsive ? 'px-4 md:px-6 py-4' : 'px-6 py-4'
)}
>
{responsive && (
<div className="absolute top-2 left-1/2 -translate-x-1/2 w-10 h-1 bg-gray-600 rounded-full md:hidden" />
<div className="absolute top-2 left-1/2 -translate-x-1/2 w-10 h-1 bg-surface-emphasis rounded-full md:hidden" />
)}
<div className={cn(responsive && 'mt-2 md:mt-0')}>
<h2 className="text-lg font-semibold flex items-center gap-2">
<Eye size={20} className="text-primary-400" />
Checkpoint Diff
</h2>
<p className="text-xs text-gray-500">
<p className="text-xs text-fg-subtle">
{checkpoint ? (
<>
Comparing <code className="bg-gray-700 px-1 rounded">{checkpoint.commitHash.slice(0, 7)}</code> Current
Comparing <code className="bg-surface-muted px-1 rounded">{checkpoint.commitHash.slice(0, 7)}</code> Current
</>
) : (
'Loading...'
@@ -316,12 +316,12 @@ export function CheckpointDiffViewer({
{/* Summary */}
{diff && (
<div className="px-4 py-3 bg-gray-900/50 border-b border-gray-700">
<div className="px-4 py-3 bg-surface-base/50 border-b border-line">
<div className="flex items-center justify-between text-sm">
<div className="flex items-center gap-4">
<span className="text-green-400">+{diff.totalInsertions}</span>
<span className="text-red-400">-{diff.totalDeletions}</span>
<span className="text-gray-400">across {diff.files.length} files</span>
<span className="text-fg-muted">across {diff.files.length} files</span>
</div>
<button
onClick={toggleSelectAll}
@@ -338,10 +338,10 @@ export function CheckpointDiffViewer({
{loading ? (
<LoadingSkeleton />
) : !diff || diff.files.length === 0 ? (
<div className="flex flex-col items-center justify-center py-12 text-gray-500">
<div className="flex flex-col items-center justify-center py-12 text-fg-subtle">
<Eye size={48} className="mb-4 opacity-50" />
<p className="text-center">No changes detected</p>
<p className="text-xs text-gray-600 mt-2 text-center">
<p className="text-xs text-fg-subtle mt-2 text-center">
The workspace matches this checkpoint
</p>
</div>
@@ -349,7 +349,7 @@ export function CheckpointDiffViewer({
<motion.div
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
className="divide-y divide-gray-700/50"
className="divide-y divide-line/50"
>
{diff.files.map((file) => {
const isSelected = selectedFiles.has(file.path);
@@ -361,7 +361,7 @@ export function CheckpointDiffViewer({
<div
className={cn(
'flex items-center gap-3 px-4 py-2',
'hover:bg-gray-900/50 transition-colors cursor-pointer'
'hover:bg-surface-base/50 transition-colors cursor-pointer'
)}
>
{/* Checkbox */}
@@ -371,7 +371,7 @@ export function CheckpointDiffViewer({
'w-4 h-4 rounded border transition-colors flex items-center justify-center',
isSelected
? 'bg-primary-500 border-primary-500'
: 'border-gray-600 hover:border-gray-500'
: 'border-line-muted hover:border-fg-subtle'
)}
>
{isSelected && <Check size={12} className="text-white" />}
@@ -380,7 +380,7 @@ export function CheckpointDiffViewer({
{/* Expand Icon */}
<button
onClick={() => handleViewFileDiff(file.path)}
className="text-gray-500 hover:text-gray-300"
className="text-fg-subtle hover:text-fg-secondary"
>
{isExpanded ? <ChevronDown size={14} /> : <ChevronRight size={14} />}
</button>
@@ -397,7 +397,7 @@ export function CheckpointDiffViewer({
{/* File Path */}
<span
className="flex-1 text-sm font-mono truncate text-gray-300"
className="flex-1 text-sm font-mono truncate text-fg-secondary"
onClick={() => handleViewFileDiff(file.path)}
>
{file.path}
@@ -405,7 +405,7 @@ export function CheckpointDiffViewer({
{/* Stats */}
{(file.insertions !== undefined || file.deletions !== undefined) && (
<span className="text-xs text-gray-500">
<span className="text-xs text-fg-subtle">
{file.insertions !== undefined && (
<span className="text-green-400 mr-2">+{file.insertions}</span>
)}
@@ -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 ? (
<div className="p-4">
<Skeleton className="h-32 w-full" />
</div>
) : fileDiff?.patch ? (
<div className="max-h-64 overflow-auto border-t border-gray-700/50">
<div className="max-h-64 overflow-auto border-t border-line/50">
{renderPatch(fileDiff.patch)}
</div>
) : (
<div className="p-4 text-center text-gray-500 text-sm">
<div className="p-4 text-center text-fg-subtle text-sm">
No diff content available
</div>
)}
@@ -453,11 +453,11 @@ export function CheckpointDiffViewer({
{diff && diff.files.length > 0 && (onRestoreSelected || onRestoreAll) && (
<div
className={cn(
'flex items-center justify-between border-t border-gray-700',
'flex items-center justify-between border-t border-line',
responsive ? 'px-4 py-3 safe-area-pb' : 'px-6 py-3'
)}
>
<span className="text-xs text-gray-500">
<span className="text-xs text-fg-subtle">
{selectedFiles.size} of {diff.files.length} files selected
</span>
<div className="flex items-center gap-2">
+19 -19
View File
@@ -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 = () => (
<div className="space-y-3 p-4">
{[1, 2, 3, 4].map((i) => (
<div key={i} className="flex items-center gap-3 p-3 bg-gray-900/50 rounded-lg">
<div key={i} className="flex items-center gap-3 p-3 bg-surface-base/50 rounded-lg">
<Skeleton className="h-4 w-4 rounded" />
<div className="flex-1 space-y-2">
<Skeleton className="h-4 w-48" />
@@ -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 */}
<div
className={cn(
'flex items-center justify-between border-b border-gray-700',
'flex items-center justify-between border-b border-line',
responsive ? 'px-4 md:px-6 py-4' : 'px-6 py-4'
)}
>
{responsive && (
<div className="absolute top-2 left-1/2 -translate-x-1/2 w-10 h-1 bg-gray-600 rounded-full md:hidden" />
<div className="absolute top-2 left-1/2 -translate-x-1/2 w-10 h-1 bg-surface-emphasis rounded-full md:hidden" />
)}
<div className={cn(responsive && 'mt-2 md:mt-0')}>
<h2 className="text-lg font-semibold flex items-center gap-2">
<History size={20} className="text-primary-400" />
Checkpoints
</h2>
<p className="text-xs text-gray-500">
<p className="text-xs text-fg-subtle">
{stats ? `${stats.count} checkpoints` : 'Loading...'}
{stats?.oldestTimestamp && (
<> · Oldest: {formatTime(stats.oldestTimestamp)}</>
@@ -431,10 +431,10 @@ export function CheckpointPanel({
{loading ? (
<LoadingSkeleton />
) : checkpoints.length === 0 ? (
<div className="flex flex-col items-center justify-center py-12 text-gray-500">
<div className="flex flex-col items-center justify-center py-12 text-fg-subtle">
<History size={48} className="mb-4 opacity-50" />
<p className="text-center">No checkpoints yet</p>
<p className="text-xs text-gray-600 mt-2 text-center max-w-xs">
<p className="text-xs text-fg-subtle mt-2 text-center max-w-xs">
Checkpoints are created automatically when files are modified
</p>
<Button
@@ -461,12 +461,12 @@ export function CheckpointPanel({
<div key={group.label} className="space-y-1">
{/* Group Header */}
<button
className="flex items-center gap-2 text-sm text-gray-400 hover:text-gray-300 transition-colors w-full"
className="flex items-center gap-2 text-sm text-fg-muted hover:text-fg-secondary transition-colors w-full"
onClick={() => toggleGroup(group.label)}
>
{isExpanded ? <ChevronDown size={14} /> : <ChevronRight size={14} />}
<span className="font-medium">{group.label}</span>
<span className="text-xs text-gray-500">({group.items.length})</span>
<span className="text-xs text-fg-subtle">({group.items.length})</span>
</button>
{/* 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"
>
<div className="flex items-start gap-3">
{/* Trigger Icon */}
@@ -502,16 +502,16 @@ export function CheckpointPanel({
<span className={cn('text-sm font-medium', triggerInfo.color)}>
{triggerInfo.label}
</span>
<span className="text-xs text-gray-500">
<span className="text-xs text-fg-subtle">
{formatTime(cp.timestamp)}
</span>
</div>
{cp.description && (
<p className="text-xs text-gray-400 mt-0.5 truncate">
<p className="text-xs text-fg-muted mt-0.5 truncate">
{cp.description}
</p>
)}
<div className="flex items-center gap-3 mt-1 text-xs text-gray-500">
<div className="flex items-center gap-3 mt-1 text-xs text-fg-subtle">
<span className="flex items-center gap-1">
<FileText size={10} />
{cp.filesChanged} files
@@ -580,11 +580,11 @@ export function CheckpointPanel({
{/* Footer */}
<div
className={cn(
'flex items-center justify-between border-t border-gray-700',
'flex items-center justify-between border-t border-line',
responsive ? 'px-4 py-3 safe-area-pb' : 'px-6 py-3'
)}
>
<span className="text-xs text-gray-500">
<span className="text-xs text-fg-subtle">
Auto-cleanup enabled (7 days / 100 max)
</span>
<Button
@@ -592,10 +592,10 @@ export function CheckpointPanel({
size="sm"
onClick={handleCleanup}
disabled={cleaningUp}
className="text-gray-400 hover:text-gray-300"
className="text-fg-muted hover:text-fg-secondary"
>
{cleaningUp ? (
<div className="animate-spin rounded-full h-3 w-3 border-t-2 border-b-2 border-gray-500 mr-1" />
<div className="animate-spin rounded-full h-3 w-3 border-t-2 border-b-2 border-fg-subtle mr-1" />
) : (
<Trash2 size={12} className="mr-1" />
)}
+6 -6
View File
@@ -61,11 +61,11 @@ export function CodeBlock({ code, language = 'text', className }: CodeBlockProps
return (
<div className={cn('relative group rounded-lg overflow-hidden', className)}>
{/* 语言标签和复制按钮 */}
<div className="flex items-center justify-between px-4 py-2 bg-gray-900 border-b border-gray-700">
<span className="text-xs text-gray-400 font-mono">{language}</span>
<div className="flex items-center justify-between px-4 py-2 bg-code border-b border-line">
<span className="text-xs text-fg-muted font-mono">{language}</span>
<button
onClick={handleCopy}
className="flex items-center gap-1 px-2 py-1 text-xs text-gray-400 hover:text-gray-200 hover:bg-gray-700 rounded transition-colors"
className="flex items-center gap-1 px-2 py-1 text-xs text-fg-muted hover:text-fg-secondary hover:bg-surface-muted rounded transition-colors"
title="Copy code"
>
{copied ? (
@@ -85,12 +85,12 @@ export function CodeBlock({ code, language = 'text', className }: CodeBlockProps
{/* 代码内容 */}
<div className="overflow-x-auto">
{isLoading ? (
<pre className="p-4 bg-gray-900 text-gray-300 text-sm font-mono">
<pre className="p-4 bg-code text-fg-secondary text-sm font-mono">
<code>{code}</code>
</pre>
) : (
<div
className="shiki-wrapper text-sm [&>pre]:p-4 [&>pre]:m-0 [&>pre]:bg-gray-900"
className="shiki-wrapper text-sm [&>pre]:p-4 [&>pre]:m-0 [&>pre]:bg-code"
dangerouslySetInnerHTML={{ __html: highlightedHtml }}
/>
)}
@@ -109,7 +109,7 @@ export function InlineCode({ children, className }: InlineCodeProps) {
return (
<code
className={cn(
'px-1.5 py-0.5 rounded bg-gray-700 text-gray-200 text-sm font-mono',
'px-1.5 py-0.5 rounded bg-surface-muted text-fg-secondary text-sm font-mono',
className
)}
>
+26 -26
View File
@@ -175,7 +175,7 @@ export function CommandEditor({
transition={smoothTransition}
onClick={(e) => e.stopPropagation()}
className={cn(
'bg-gray-800 max-h-[90vh] overflow-auto',
'bg-surface-subtle max-h-[90vh] overflow-auto',
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'
@@ -184,12 +184,12 @@ export function CommandEditor({
{/* Header */}
<div
className={cn(
'sticky top-0 flex items-center justify-between border-b border-gray-700 bg-gray-800 z-10',
'sticky top-0 flex items-center justify-between border-b border-line bg-surface-subtle z-10',
responsive ? 'px-4 md:px-6 py-4' : 'px-6 py-4'
)}
>
{responsive && (
<div className="absolute top-2 left-1/2 -translate-x-1/2 w-10 h-1 bg-gray-600 rounded-full md:hidden" />
<div className="absolute top-2 left-1/2 -translate-x-1/2 w-10 h-1 bg-surface-emphasis rounded-full md:hidden" />
)}
<h2 className={cn('text-lg font-semibold', responsive && 'mt-2 md:mt-0')}>
{isEditMode ? `Edit Command: /${commandName}` : 'Create Command'}
@@ -233,7 +233,7 @@ export function CommandEditor({
{/* Name */}
<div className="space-y-2">
<label className="block text-sm font-medium text-gray-300">
<label className="block text-sm font-medium text-fg-secondary">
Name <span className="text-red-400">*</span>
</label>
<Input
@@ -243,14 +243,14 @@ export function CommandEditor({
disabled={isEditMode || isBuiltin}
className="font-mono"
/>
<p className="text-xs text-gray-500">
<p className="text-xs text-fg-subtle">
Command name. Use / for nested commands (e.g., deploy/staging)
</p>
</div>
{/* Description */}
<div className="space-y-2">
<label className="block text-sm font-medium text-gray-300">Description</label>
<label className="block text-sm font-medium text-fg-secondary">Description</label>
<Input
value={description}
onChange={(e) => setDescription(e.target.value)}
@@ -261,7 +261,7 @@ export function CommandEditor({
{/* Template */}
<div className="space-y-2">
<label className="block text-sm font-medium text-gray-300">
<label className="block text-sm font-medium text-fg-secondary">
Template <span className="text-red-400">*</span>
</label>
<textarea
@@ -271,14 +271,14 @@ export function CommandEditor({
disabled={isBuiltin}
rows={6}
className={cn(
'w-full px-3 py-2 bg-gray-900 border border-gray-700 rounded-lg',
'text-gray-100 placeholder:text-gray-500',
'w-full px-3 py-2 bg-surface-base border border-line rounded-lg',
'text-fg placeholder:text-fg-subtle',
'focus:outline-none focus:ring-2 focus:ring-primary-500 focus:border-transparent',
'font-mono text-sm resize-y',
'disabled:opacity-50 disabled:cursor-not-allowed'
)}
/>
<p className="text-xs text-gray-500">
<p className="text-xs text-fg-subtle">
Template syntax: $ARGUMENTS (all args), $1 $2 (positional), @file (include
file), !`cmd` (shell output)
</p>
@@ -287,41 +287,41 @@ export function CommandEditor({
{/* Agent & Model */}
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<div className="space-y-2">
<label className="block text-sm font-medium text-gray-300">Agent</label>
<label className="block text-sm font-medium text-fg-secondary">Agent</label>
<Input
value={agent}
onChange={(e) => setAgent(e.target.value)}
placeholder="code-review"
disabled={isBuiltin}
/>
<p className="text-xs text-gray-500">Optional agent to use</p>
<p className="text-xs text-fg-subtle">Optional agent to use</p>
</div>
<div className="space-y-2">
<label className="block text-sm font-medium text-gray-300">Model</label>
<label className="block text-sm font-medium text-fg-secondary">Model</label>
<Input
value={model}
onChange={(e) => setModel(e.target.value)}
placeholder="claude-sonnet-4-20250514"
disabled={isBuiltin}
/>
<p className="text-xs text-gray-500">Optional model override</p>
<p className="text-xs text-fg-subtle">Optional model override</p>
</div>
</div>
{/* Subtask & Scope */}
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<div className="flex items-center justify-between p-3 bg-gray-900/50 rounded-lg">
<div className="flex items-center justify-between p-3 bg-surface-base/50 rounded-lg">
<div>
<label className="block text-sm font-medium text-gray-300">Subtask</label>
<p className="text-xs text-gray-500">Run as background subtask</p>
<label className="block text-sm font-medium text-fg-secondary">Subtask</label>
<p className="text-xs text-fg-subtle">Run as background subtask</p>
</div>
<Switch checked={subtask} onCheckedChange={setSubtask} disabled={isBuiltin} />
</div>
{!isEditMode && (
<div className="space-y-2">
<label className="block text-sm font-medium text-gray-300">Scope</label>
<label className="block text-sm font-medium text-fg-secondary">Scope</label>
<Select
value={scope}
onValueChange={(v) => setScope(v as 'user' | 'project')}
@@ -334,24 +334,24 @@ export function CommandEditor({
<SelectItem value="project">Project (local)</SelectItem>
</SelectContent>
</Select>
<p className="text-xs text-gray-500">Where to store the command</p>
<p className="text-xs text-fg-subtle">Where to store the command</p>
</div>
)}
</div>
{/* Source Info (edit mode) */}
{isEditMode && originalData && (
<div className="pt-4 border-t border-gray-700">
<h3 className="text-sm font-medium text-gray-400 mb-2">Command Info</h3>
<div className="pt-4 border-t border-line">
<h3 className="text-sm font-medium text-fg-muted mb-2">Command Info</h3>
<div className="grid grid-cols-2 gap-2 text-sm">
<div>
<span className="text-gray-500">Source:</span>
<span className="ml-2 text-gray-300">{originalData.source}</span>
<span className="text-fg-subtle">Source:</span>
<span className="ml-2 text-fg-secondary">{originalData.source}</span>
</div>
{originalData.sourcePath && (
<div className="col-span-2">
<span className="text-gray-500">Path:</span>
<span className="ml-2 text-gray-300 font-mono text-xs break-all">
<span className="text-fg-subtle">Path:</span>
<span className="ml-2 text-fg-secondary font-mono text-xs break-all">
{originalData.sourcePath}
</span>
</div>
@@ -365,7 +365,7 @@ export function CommandEditor({
{/* Footer */}
<div
className={cn(
'sticky bottom-0 border-t border-gray-700 bg-gray-800',
'sticky bottom-0 border-t border-line bg-surface-subtle',
responsive
? 'flex flex-col-reverse md:flex-row items-stretch md:items-center justify-end gap-2 md:gap-3 p-4 md:px-6 md:py-4 safe-area-pb'
: 'flex items-center justify-end gap-3 px-6 py-4'
+19 -19
View File
@@ -44,7 +44,7 @@ function getSourceIcon(source: string) {
case 'project':
return <FolderGit2 size={14} className="text-green-400" />;
default:
return <Terminal size={14} className="text-gray-400" />;
return <Terminal size={14} className="text-fg-muted" />;
}
}
@@ -55,7 +55,7 @@ function getSourceBadge(source: string) {
user: 'bg-purple-500/20 text-purple-400',
project: 'bg-green-500/20 text-green-400',
};
return colors[source] || 'bg-gray-500/20 text-gray-400';
return colors[source] || 'bg-surface-muted/20 text-fg-muted';
}
export function CommandMenu({
@@ -132,28 +132,28 @@ export function CommandMenu({
transition={{ duration: 0.15 }}
className="absolute bottom-full left-0 right-0 mb-2 mx-4 md:mx-0 z-50"
>
<div className="bg-gray-800 border border-gray-700 rounded-lg shadow-xl overflow-hidden max-h-64 overflow-y-auto">
<div className="bg-surface-subtle border border-line rounded-lg shadow-xl overflow-hidden max-h-64 overflow-y-auto">
{/* Header */}
<div className="px-3 py-2 border-b border-gray-700 bg-gray-800/80 sticky top-0">
<div className="flex items-center gap-2 text-xs text-gray-400">
<div className="px-3 py-2 border-b border-line bg-surface-subtle/80 sticky top-0">
<div className="flex items-center gap-2 text-xs text-fg-muted">
<Terminal size={12} />
<span>Commands</span>
{commands.length > 0 && (
<span className="text-gray-500">({commands.length})</span>
<span className="text-fg-subtle">({commands.length})</span>
)}
</div>
</div>
{/* Loading */}
{isLoading && commands.length === 0 && (
<div className="px-3 py-4 text-center text-gray-400 text-sm">
<div className="px-3 py-4 text-center text-fg-muted text-sm">
Loading commands...
</div>
)}
{/* Empty state */}
{!isLoading && commands.length === 0 && (
<div className="px-3 py-4 text-center text-gray-400 text-sm">
<div className="px-3 py-4 text-center text-fg-muted text-sm">
No commands found
</div>
)}
@@ -171,7 +171,7 @@ export function CommandMenu({
'w-full px-3 py-2 flex items-start gap-3 text-left transition-colors',
index === selectedIndex
? 'bg-primary-600/20'
: 'hover:bg-gray-700/50'
: 'hover:bg-surface-muted/50'
)}
>
{/* Icon */}
@@ -185,7 +185,7 @@ export function CommandMenu({
'font-mono text-sm',
index === selectedIndex
? 'text-primary-300'
: 'text-gray-200'
: 'text-fg-secondary'
)}
>
/{command.name}
@@ -200,7 +200,7 @@ export function CommandMenu({
</span>
</div>
{command.description && (
<p className="text-xs text-gray-400 mt-0.5 truncate">
<p className="text-xs text-fg-muted mt-0.5 truncate">
{command.description}
</p>
)}
@@ -208,8 +208,8 @@ export function CommandMenu({
{/* Keyboard hint */}
{index === selectedIndex && (
<div className="flex items-center gap-1 text-[10px] text-gray-500">
<kbd className="px-1 py-0.5 bg-gray-700 rounded">
<div className="flex items-center gap-1 text-[10px] text-fg-subtle">
<kbd className="px-1 py-0.5 bg-surface-muted rounded">
Enter
</kbd>
</div>
@@ -220,19 +220,19 @@ export function CommandMenu({
)}
{/* Footer hint */}
<div className="px-3 py-1.5 border-t border-gray-700 bg-gray-800/80 sticky bottom-0">
<div className="flex items-center gap-3 text-[10px] text-gray-500">
<div className="px-3 py-1.5 border-t border-line bg-surface-subtle/80 sticky bottom-0">
<div className="flex items-center gap-3 text-[10px] text-fg-subtle">
<span>
<kbd className="px-1 py-0.5 bg-gray-700 rounded mr-1"></kbd>
<kbd className="px-1 py-0.5 bg-gray-700 rounded"></kbd>
<kbd className="px-1 py-0.5 bg-surface-muted rounded mr-1"></kbd>
<kbd className="px-1 py-0.5 bg-surface-muted rounded"></kbd>
{' '}navigate
</span>
<span>
<kbd className="px-1 py-0.5 bg-gray-700 rounded">Tab</kbd>
<kbd className="px-1 py-0.5 bg-surface-muted rounded">Tab</kbd>
{' '}select
</span>
<span>
<kbd className="px-1 py-0.5 bg-gray-700 rounded">Esc</kbd>
<kbd className="px-1 py-0.5 bg-surface-muted rounded">Esc</kbd>
{' '}close
</span>
</div>
+16 -16
View File
@@ -44,13 +44,13 @@ type CommandItem = CommandListResponse['commands'][number];
function getSourceIcon(source: string) {
switch (source) {
case 'builtin':
return <Cog size={14} className="text-gray-400" />;
return <Cog size={14} className="text-fg-muted" />;
case 'user':
return <User size={14} className="text-blue-400" />;
case 'project':
return <FolderOpen size={14} className="text-green-400" />;
default:
return <Terminal size={14} className="text-gray-400" />;
return <Terminal size={14} className="text-fg-muted" />;
}
}
@@ -58,13 +58,13 @@ function getSourceIcon(source: string) {
function getSourceBadgeClass(source: string) {
switch (source) {
case 'builtin':
return 'bg-gray-700 text-gray-300';
return 'bg-surface-muted text-fg-secondary';
case 'user':
return 'bg-blue-500/20 text-blue-400';
case 'project':
return 'bg-green-500/20 text-green-400';
default:
return 'bg-gray-700 text-gray-300';
return 'bg-surface-muted text-fg-secondary';
}
}
@@ -186,7 +186,7 @@ export function CommandPanel({ onClose, responsive = false }: CommandPanelProps)
const LoadingSkeleton = () => (
<div className="space-y-3 p-4">
{[1, 2, 3, 4, 5].map((i) => (
<div key={i} className="flex items-center gap-3 p-3 bg-gray-900/50 rounded-lg">
<div key={i} className="flex items-center gap-3 p-3 bg-surface-base/50 rounded-lg">
<Skeleton className="h-5 w-5 rounded" />
<div className="flex-1 space-y-2">
<Skeleton className="h-4 w-32" />
@@ -223,7 +223,7 @@ export function CommandPanel({ onClose, responsive = false }: CommandPanelProps)
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'
@@ -232,17 +232,17 @@ export function CommandPanel({ onClose, responsive = false }: CommandPanelProps)
{/* Header */}
<div
className={cn(
'flex items-center justify-between border-b border-gray-700',
'flex items-center justify-between border-b border-line',
responsive ? 'px-4 md:px-6 py-4' : 'px-6 py-4'
)}
>
{responsive && (
<div className="absolute top-2 left-1/2 -translate-x-1/2 w-10 h-1 bg-gray-600 rounded-full md:hidden" />
<div className="absolute top-2 left-1/2 -translate-x-1/2 w-10 h-1 bg-surface-muted rounded-full md:hidden" />
)}
<div className={cn(responsive && 'mt-2 md:mt-0')}>
<h2 className="text-lg font-semibold">Commands</h2>
{stats && (
<p className="text-xs text-gray-500">
<p className="text-xs text-fg-subtle">
{stats.total} commands ({stats.bySource.builtin || 0} builtin,{' '}
{stats.bySource.user || 0} user, {stats.bySource.project || 0} project)
</p>
@@ -261,14 +261,14 @@ export function CommandPanel({ onClose, responsive = false }: CommandPanelProps)
{/* Toolbar */}
<div
className={cn(
'flex items-center gap-3 border-b border-gray-700',
'flex items-center gap-3 border-b border-line',
responsive ? 'px-4 md:px-6 py-3' : 'px-6 py-3'
)}
>
<div className="flex-1 relative">
<Search
size={16}
className="absolute left-3 top-1/2 -translate-y-1/2 text-gray-500"
className="absolute left-3 top-1/2 -translate-y-1/2 text-fg-subtle"
/>
<Input
value={searchQuery}
@@ -297,7 +297,7 @@ export function CommandPanel({ onClose, responsive = false }: CommandPanelProps)
{loading ? (
<LoadingSkeleton />
) : filteredCommands.length === 0 ? (
<div className="flex flex-col items-center justify-center py-12 text-gray-500">
<div className="flex flex-col items-center justify-center py-12 text-fg-subtle">
<Terminal size={48} className="mb-4 opacity-50" />
<p>
{searchQuery
@@ -325,8 +325,8 @@ export function CommandPanel({ onClose, responsive = false }: CommandPanelProps)
animate={{ opacity: 1, y: 0 }}
exit={{ opacity: 0, y: -10 }}
className={cn(
'flex items-center gap-3 p-3 bg-gray-900/50 rounded-lg',
'hover:bg-gray-900/80 transition-colors group'
'flex items-center gap-3 p-3 bg-surface-base/50 rounded-lg',
'hover:bg-surface-base/80 transition-colors group'
)}
>
{/* Icon */}
@@ -335,7 +335,7 @@ export function CommandPanel({ onClose, responsive = false }: CommandPanelProps)
{/* Info */}
<div className="flex-1 min-w-0">
<div className="flex items-center gap-2">
<span className="font-mono text-sm text-gray-200">/{command.name}</span>
<span className="font-mono text-sm text-fg-secondary">/{command.name}</span>
<span
className={cn(
'px-1.5 py-0.5 text-xs rounded',
@@ -346,7 +346,7 @@ export function CommandPanel({ onClose, responsive = false }: CommandPanelProps)
</span>
</div>
{command.description && (
<p className="text-xs text-gray-500 truncate mt-0.5">
<p className="text-xs text-fg-subtle truncate mt-0.5">
{command.description}
</p>
)}
+7 -7
View File
@@ -113,7 +113,7 @@ export function ConfigPanel({ onClose, responsive = false }: ConfigPanelProps) {
transition={smoothTransition}
onClick={(e) => e.stopPropagation()}
className={cn(
'bg-gray-800 max-h-[90vh] overflow-auto',
'bg-surface-subtle max-h-[90vh] overflow-auto',
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'
@@ -122,12 +122,12 @@ export function ConfigPanel({ onClose, responsive = false }: ConfigPanelProps) {
{/* Header */}
<div
className={cn(
'sticky top-0 flex items-center justify-between border-b border-gray-700 bg-gray-800 z-10',
'sticky top-0 flex items-center justify-between border-b border-line bg-surface-subtle z-10',
responsive ? 'px-4 md:px-6 py-4' : 'px-6 py-4'
)}
>
{responsive && (
<div className="absolute top-2 left-1/2 -translate-x-1/2 w-10 h-1 bg-gray-600 rounded-full md:hidden" />
<div className="absolute top-2 left-1/2 -translate-x-1/2 w-10 h-1 bg-surface-muted rounded-full md:hidden" />
)}
<h2 className={cn('text-lg font-semibold', responsive && 'mt-2 md:mt-0')}>
Settings
@@ -156,14 +156,14 @@ export function ConfigPanel({ onClose, responsive = false }: ConfigPanelProps) {
>
{/* Working Directory */}
<div className="space-y-2">
<label className="block text-sm font-medium text-gray-300">Working Directory</label>
<label className="block text-sm font-medium text-fg-secondary">Working Directory</label>
<Input
value={formData.workdir}
onChange={(e) => setFormData({ ...formData, workdir: e.target.value })}
className="font-mono"
placeholder="/path/to/project"
/>
<p className="text-xs text-gray-500">Root directory for file operations</p>
<p className="text-xs text-fg-subtle">Root directory for file operations</p>
</div>
</motion.div>
)}
@@ -171,10 +171,10 @@ export function ConfigPanel({ onClose, responsive = false }: ConfigPanelProps) {
{/* Footer */}
<div
className={cn(
'sticky bottom-0 border-t border-gray-700 bg-gray-800',
'sticky bottom-0 border-t border-line bg-surface-subtle',
responsive
? 'flex flex-col-reverse md:flex-row items-stretch md:items-center justify-end gap-2 md:gap-3 p-4 md:px-6 md:py-4 safe-area-pb'
: 'flex items-center justify-end gap-3 px-6 py-4 bg-gray-800/50'
: 'flex items-center justify-end gap-3 px-6 py-4 bg-surface-subtle/50'
)}
>
<Button
+7 -7
View File
@@ -44,7 +44,7 @@ function getUsageColor(percent: number): string {
function getTextColor(percent: number): string {
if (percent >= 90) return 'text-red-400';
if (percent >= 80) return 'text-amber-400';
return 'text-gray-400';
return 'text-fg-muted';
}
export function ContextUsage({
@@ -128,8 +128,8 @@ export function ContextUsage({
// 无数据时显示占位
if (!usage) {
return (
<div className={cn('flex items-center gap-2 text-sm text-gray-500', className)}>
<div className="h-2 w-24 bg-gray-700 rounded-full animate-pulse" />
<div className={cn('flex items-center gap-2 text-sm text-fg-subtle', className)}>
<div className="h-2 w-24 bg-surface-muted rounded-full animate-pulse" />
<span>--</span>
</div>
);
@@ -144,7 +144,7 @@ export function ContextUsage({
return (
<div className={cn('flex items-center gap-2', className)}>
{/* 进度条 */}
<div className="relative h-1.5 w-16 bg-gray-700 rounded-full overflow-hidden">
<div className="relative h-1.5 w-16 bg-surface-muted rounded-full overflow-hidden">
<div
className={cn('absolute h-full rounded-full transition-all', barColor)}
style={{ width: `${Math.min(usagePercent, 100)}%` }}
@@ -166,8 +166,8 @@ export function ContextUsage({
{/* 标题行 */}
<div className="flex items-center justify-between">
<div className="flex items-center gap-2">
<Zap size={14} className="text-gray-400" />
<span className="text-xs font-medium text-gray-300">Context Usage</span>
<Zap size={14} className="text-fg-muted" />
<span className="text-xs font-medium text-fg-secondary">Context Usage</span>
</div>
<div className="flex items-center gap-1">
{/* 刷新按钮 */}
@@ -186,7 +186,7 @@ export function ContextUsage({
</div>
{/* 进度条 */}
<div className="relative h-2 w-full bg-gray-700 rounded-full overflow-hidden">
<div className="relative h-2 w-full bg-surface-muted rounded-full overflow-hidden">
<div
className={cn('absolute h-full rounded-full transition-all duration-300', barColor)}
style={{ width: `${Math.min(usagePercent, 100)}%` }}
+19 -19
View File
@@ -29,7 +29,7 @@ const FileIcon = ({ type, extension }: { type: 'file' | 'directory'; extension?:
js: 'text-yellow-300',
jsx: 'text-yellow-300',
json: 'text-yellow-500',
md: 'text-gray-400',
md: 'text-fg-muted',
css: 'text-pink-400',
html: 'text-orange-400',
py: 'text-green-400',
@@ -37,7 +37,7 @@ const FileIcon = ({ type, extension }: { type: 'file' | 'directory'; extension?:
rs: 'text-orange-500',
};
const color = colors[extension || ''] || 'text-gray-400';
const color = colors[extension || ''] || 'text-fg-muted';
return (
<svg className={`w-4 h-4 ${color}`} fill="currentColor" viewBox="0 0 20 20">
@@ -126,13 +126,13 @@ export function FileBrowser({ onFileSelect, className = '' }: FileBrowserProps)
};
return (
<div className={`flex flex-col h-full bg-gray-900 ${className}`}>
<div className={`flex flex-col h-full bg-surface-base ${className}`}>
{/* 工具栏 */}
<div className="flex items-center gap-2 p-2 border-b border-gray-700 bg-gray-800">
<div className="flex items-center gap-2 p-2 border-b border-line bg-surface-subtle">
<button
onClick={handleGoUp}
disabled={parentPath === null}
className="p-1.5 rounded hover:bg-gray-700 disabled:opacity-50 disabled:cursor-not-allowed"
className="p-1.5 rounded hover:bg-surface-muted disabled:opacity-50 disabled:cursor-not-allowed"
title="Go up"
>
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
@@ -142,7 +142,7 @@ export function FileBrowser({ onFileSelect, className = '' }: FileBrowserProps)
<button
onClick={handleRefresh}
className="p-1.5 rounded hover:bg-gray-700"
className="p-1.5 rounded hover:bg-surface-muted"
title="Refresh"
>
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
@@ -155,11 +155,11 @@ export function FileBrowser({ onFileSelect, className = '' }: FileBrowserProps)
</svg>
</button>
<div className="flex-1 px-2 py-1 text-sm text-gray-400 bg-gray-900 rounded truncate">
<div className="flex-1 px-2 py-1 text-sm text-fg-muted bg-surface-base rounded truncate">
{currentPath}
</div>
<label className="flex items-center gap-1 text-xs text-gray-400 cursor-pointer">
<label className="flex items-center gap-1 text-xs text-fg-muted cursor-pointer">
<input
type="checkbox"
checked={showHidden}
@@ -176,7 +176,7 @@ export function FileBrowser({ onFileSelect, className = '' }: FileBrowserProps)
{/* 文件列表 */}
<div className="flex-1 overflow-auto">
{loading ? (
<div className="flex items-center justify-center h-32 text-gray-400">
<div className="flex items-center justify-center h-32 text-fg-muted">
Loading...
</div>
) : error ? (
@@ -184,23 +184,23 @@ export function FileBrowser({ onFileSelect, className = '' }: FileBrowserProps)
{error}
</div>
) : files.length === 0 ? (
<div className="flex items-center justify-center h-32 text-gray-500">
<div className="flex items-center justify-center h-32 text-fg-subtle">
Empty directory
</div>
) : (
<div className="divide-y divide-gray-800">
<div className="divide-y divide-surface-subtle">
{files.map((file) => (
<div
key={file.path}
onClick={() => handleItemClick(file)}
className={`flex items-center gap-2 px-3 py-2 cursor-pointer hover:bg-gray-800 ${
selectedFile === file.path ? 'bg-gray-800 border-l-2 border-blue-500' : ''
className={`flex items-center gap-2 px-3 py-2 cursor-pointer hover:bg-surface-subtle ${
selectedFile === file.path ? 'bg-surface-subtle border-l-2 border-blue-500' : ''
}`}
>
<FileIcon type={file.type} extension={file.extension} />
<span className="flex-1 truncate text-sm">{file.name}</span>
{file.type === 'file' && (
<span className="text-xs text-gray-500">{formatSize(file.size)}</span>
<span className="text-xs text-fg-subtle">{formatSize(file.size)}</span>
)}
</div>
))}
@@ -210,22 +210,22 @@ export function FileBrowser({ onFileSelect, className = '' }: FileBrowserProps)
{/* 文件预览 */}
{selectedFile && fileContent && (
<div className="border-t border-gray-700 max-h-48 overflow-auto">
<div className="sticky top-0 flex items-center justify-between px-3 py-1 bg-gray-800 border-b border-gray-700">
<span className="text-xs text-gray-400 truncate">{selectedFile}</span>
<div className="border-t border-line max-h-48 overflow-auto">
<div className="sticky top-0 flex items-center justify-between px-3 py-1 bg-surface-subtle border-b border-line">
<span className="text-xs text-fg-muted truncate">{selectedFile}</span>
<button
onClick={() => {
setSelectedFile(null);
setFileContent(null);
}}
className="p-1 hover:bg-gray-700 rounded"
className="p-1 hover:bg-surface-muted rounded"
>
<svg className="w-3 h-3" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M6 18L18 6M6 6l12 12" />
</svg>
</button>
</div>
<pre className="p-3 text-xs text-gray-300 whitespace-pre-wrap font-mono">
<pre className="p-3 text-xs text-fg-secondary whitespace-pre-wrap font-mono">
{fileContent.slice(0, 5000)}
{fileContent.length > 5000 && '\n... (truncated)'}
</pre>
+24 -24
View File
@@ -276,7 +276,7 @@ export function HookEditor({
transition={smoothTransition}
onClick={(e) => e.stopPropagation()}
className={cn(
'bg-gray-800 max-h-[85vh] overflow-hidden flex flex-col',
'bg-surface-subtle max-h-[85vh] 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'
@@ -285,12 +285,12 @@ export function HookEditor({
{/* Header */}
<div
className={cn(
'flex items-center justify-between border-b border-gray-700',
'flex items-center justify-between border-b border-line',
responsive ? 'px-4 py-3' : 'px-6 py-4'
)}
>
{responsive && (
<div className="absolute top-2 left-1/2 -translate-x-1/2 w-10 h-1 bg-gray-600 rounded-full md:hidden" />
<div className="absolute top-2 left-1/2 -translate-x-1/2 w-10 h-1 bg-surface-emphasis rounded-full md:hidden" />
)}
<h3 className={cn('text-lg font-semibold', responsive && 'mt-2 md:mt-0')}>
{title}
@@ -305,7 +305,7 @@ export function HookEditor({
{/* Pattern (for file hooks) */}
{isFileHook && (
<div>
<label className="block text-sm font-medium text-gray-300 mb-1">
<label className="block text-sm font-medium text-fg-secondary mb-1">
Pattern (glob)
</label>
<input
@@ -314,15 +314,15 @@ export function HookEditor({
onChange={(e) => setPattern(e.target.value)}
placeholder="e.g., *.ts, src/**/*.tsx"
className={cn(
'w-full px-3 py-2 bg-gray-900 border rounded-lg text-sm',
'w-full px-3 py-2 bg-surface-base border rounded-lg text-sm',
'focus:outline-none focus:ring-2 focus:ring-primary-500',
errors.pattern ? 'border-red-500' : 'border-gray-700'
errors.pattern ? 'border-red-500' : 'border-line'
)}
/>
{errors.pattern && (
<p className="text-xs text-red-400 mt-1">{errors.pattern}</p>
)}
<p className="text-xs text-gray-500 mt-1">
<p className="text-xs text-fg-subtle mt-1">
Use glob patterns to match files (e.g., *.ts, **/*.json)
</p>
</div>
@@ -331,7 +331,7 @@ export function HookEditor({
{/* Commands */}
<div>
<div className="flex items-center justify-between mb-2">
<label className="text-sm font-medium text-gray-300">
<label className="text-sm font-medium text-fg-secondary">
Commands
</label>
{isFileHook && (
@@ -351,12 +351,12 @@ export function HookEditor({
{commandStates.map((state, cmdIndex) => (
<div
key={cmdIndex}
className="bg-gray-900/50 rounded-lg p-3 border border-gray-700"
className="bg-surface-base/50 rounded-lg p-3 border border-line"
>
{/* Command args */}
<div className="space-y-2">
<div className="flex items-center justify-between">
<span className="text-xs text-gray-500">
<span className="text-xs text-fg-subtle">
Command {commandStates.length > 1 ? cmdIndex + 1 : ''}
</span>
<div className="flex items-center gap-1">
@@ -396,7 +396,7 @@ export function HookEditor({
onChange={(e) => updateCommandArg(cmdIndex, argIndex, e.target.value)}
placeholder={argIndex === 0 ? 'command' : 'arg'}
className={cn(
'px-2 py-1 bg-gray-800 border border-gray-600 rounded text-sm font-mono',
'px-2 py-1 bg-surface-subtle border border-line-muted rounded text-sm font-mono',
'focus:outline-none focus:ring-1 focus:ring-primary-500',
argIndex === 0 ? 'min-w-[100px]' : 'min-w-[80px]'
)}
@@ -404,7 +404,7 @@ export function HookEditor({
{state.command.length > 1 && (
<button
onClick={() => removeCommandArg(cmdIndex, argIndex)}
className="text-gray-500 hover:text-red-400"
className="text-fg-subtle hover:text-red-400"
>
<X size={14} />
</button>
@@ -429,7 +429,7 @@ export function HookEditor({
{/* Advanced options toggle */}
<button
onClick={() => updateCommand(cmdIndex, { showAdvanced: !state.showAdvanced })}
className="flex items-center gap-1 text-xs text-gray-500 hover:text-gray-300 mt-3"
className="flex items-center gap-1 text-xs text-fg-subtle hover:text-fg-secondary mt-3"
>
{state.showAdvanced ? <ChevronDown size={14} /> : <ChevronRight size={14} />}
Advanced options
@@ -437,10 +437,10 @@ export function HookEditor({
{/* Advanced options */}
{state.showAdvanced && (
<div className="mt-3 space-y-3 pl-4 border-l border-gray-700">
<div className="mt-3 space-y-3 pl-4 border-l border-line">
{/* Timeout */}
<div>
<label className="block text-xs text-gray-400 mb-1">
<label className="block text-xs text-fg-muted mb-1">
Timeout (ms)
</label>
<input
@@ -450,13 +450,13 @@ export function HookEditor({
timeout: e.target.value ? parseInt(e.target.value) : undefined
})}
placeholder="30000"
className="w-32 px-2 py-1 bg-gray-800 border border-gray-600 rounded text-sm focus:outline-none focus:ring-1 focus:ring-primary-500"
className="w-32 px-2 py-1 bg-surface-subtle border border-line-muted rounded text-sm focus:outline-none focus:ring-1 focus:ring-primary-500"
/>
</div>
{/* Working Directory */}
<div>
<label className="block text-xs text-gray-400 mb-1">
<label className="block text-xs text-fg-muted mb-1">
Working Directory
</label>
<input
@@ -464,14 +464,14 @@ export function HookEditor({
value={state.cwd || ''}
onChange={(e) => updateCommand(cmdIndex, { cwd: e.target.value || undefined })}
placeholder="(project root)"
className="w-full px-2 py-1 bg-gray-800 border border-gray-600 rounded text-sm focus:outline-none focus:ring-1 focus:ring-primary-500"
className="w-full px-2 py-1 bg-surface-subtle border border-line-muted rounded text-sm focus:outline-none focus:ring-1 focus:ring-primary-500"
/>
</div>
{/* Environment Variables */}
<div>
<div className="flex items-center justify-between mb-1">
<label className="text-xs text-gray-400">
<label className="text-xs text-fg-muted">
Environment Variables
</label>
<Button
@@ -492,19 +492,19 @@ export function HookEditor({
value={key}
onChange={(e) => updateEnvVar(cmdIndex, key, e.target.value, value)}
placeholder="KEY"
className="w-28 px-2 py-1 bg-gray-800 border border-gray-600 rounded text-xs font-mono focus:outline-none focus:ring-1 focus:ring-primary-500"
className="w-28 px-2 py-1 bg-surface-subtle border border-line-muted rounded text-xs font-mono focus:outline-none focus:ring-1 focus:ring-primary-500"
/>
<span className="text-gray-500">=</span>
<span className="text-fg-subtle">=</span>
<input
type="text"
value={value}
onChange={(e) => updateEnvVar(cmdIndex, key, key, e.target.value)}
placeholder="value"
className="flex-1 px-2 py-1 bg-gray-800 border border-gray-600 rounded text-xs font-mono focus:outline-none focus:ring-1 focus:ring-primary-500"
className="flex-1 px-2 py-1 bg-surface-subtle border border-line-muted rounded text-xs font-mono focus:outline-none focus:ring-1 focus:ring-primary-500"
/>
<button
onClick={() => removeEnvVar(cmdIndex, key)}
className="text-gray-500 hover:text-red-400"
className="text-fg-subtle hover:text-red-400"
>
<X size={12} />
</button>
@@ -523,7 +523,7 @@ export function HookEditor({
{/* Footer */}
<div
className={cn(
'flex items-center justify-end gap-2 border-t border-gray-700',
'flex items-center justify-end gap-2 border-t border-line',
responsive ? 'px-4 py-3 safe-area-pb' : 'px-6 py-4'
)}
>
+23 -23
View File
@@ -145,11 +145,11 @@ export function HooksPanel({ onClose, responsive = false }: HooksPanelProps) {
toast.success(
<div>
<div className="font-medium">Command succeeded</div>
<div className="text-xs text-gray-400 mt-1">
<div className="text-xs text-fg-muted mt-1">
Exit code: {result.data.exitCode} ({result.data.duration}ms)
</div>
{result.data.stdout && (
<pre className="text-xs bg-gray-900 p-2 rounded mt-2 max-h-32 overflow-auto">
<pre className="text-xs bg-surface-base p-2 rounded mt-2 max-h-32 overflow-auto">
{result.data.stdout.slice(0, 500)}
</pre>
)}
@@ -159,11 +159,11 @@ export function HooksPanel({ onClose, responsive = false }: HooksPanelProps) {
toast.error(
<div>
<div className="font-medium">Command failed</div>
<div className="text-xs text-gray-400 mt-1">
<div className="text-xs text-fg-muted mt-1">
Exit code: {result.data.exitCode} ({result.data.duration}ms)
</div>
{result.data.stderr && (
<pre className="text-xs bg-gray-900 p-2 rounded mt-2 max-h-32 overflow-auto text-red-400">
<pre className="text-xs bg-surface-base p-2 rounded mt-2 max-h-32 overflow-auto text-red-400">
{result.data.stderr.slice(0, 500)}
</pre>
)}
@@ -274,7 +274,7 @@ export function HooksPanel({ onClose, responsive = false }: HooksPanelProps) {
const LoadingSkeleton = () => (
<div className="space-y-3 p-4">
{[1, 2, 3, 4].map((i) => (
<div key={i} className="bg-gray-900/50 rounded-lg p-3">
<div key={i} className="bg-surface-base/50 rounded-lg p-3">
<div className="flex items-center gap-3">
<Skeleton className="h-4 w-4" />
<Skeleton className="h-4 w-32" />
@@ -292,7 +292,7 @@ export function HooksPanel({ onClose, responsive = false }: HooksPanelProps) {
if (patterns.length === 0) {
return (
<div className="text-xs text-gray-500 py-2 px-3">
<div className="text-xs text-fg-subtle py-2 px-3">
No hooks configured
</div>
);
@@ -305,7 +305,7 @@ export function HooksPanel({ onClose, responsive = false }: HooksPanelProps) {
const cmdId = `${type}-${pattern}`;
return (
<div key={pattern} className="bg-gray-800/50 rounded p-2">
<div key={pattern} className="bg-surface-subtle/50 rounded p-2">
<div className="flex items-center justify-between">
<code className="text-xs font-mono text-blue-400">{pattern}</code>
<div className="flex items-center gap-1">
@@ -332,7 +332,7 @@ export function HooksPanel({ onClose, responsive = false }: HooksPanelProps) {
<div className="mt-1 space-y-1">
{commands.map((cmd, idx) => (
<div key={idx} className="flex items-center justify-between text-xs">
<code className="font-mono text-gray-400 truncate flex-1">
<code className="font-mono text-fg-muted truncate flex-1">
{cmd.command.join(' ')}
</code>
<Button
@@ -365,7 +365,7 @@ export function HooksPanel({ onClose, responsive = false }: HooksPanelProps) {
if (commands.length === 0) {
return (
<div className="text-xs text-gray-500 py-2 px-3">
<div className="text-xs text-fg-subtle py-2 px-3">
No hooks configured
</div>
);
@@ -376,8 +376,8 @@ export function HooksPanel({ onClose, responsive = false }: HooksPanelProps) {
{commands.map((cmd, idx) => {
const cmdId = `session-${idx}`;
return (
<div key={idx} className="bg-gray-800/50 rounded p-2 flex items-center justify-between">
<code className="text-xs font-mono text-gray-400 truncate flex-1">
<div key={idx} className="bg-surface-subtle/50 rounded p-2 flex items-center justify-between">
<code className="text-xs font-mono text-fg-muted truncate flex-1">
{cmd.command.join(' ')}
</code>
<div className="flex items-center gap-1">
@@ -444,7 +444,7 @@ export function HooksPanel({ onClose, responsive = false }: HooksPanelProps) {
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'
@@ -453,19 +453,19 @@ export function HooksPanel({ onClose, responsive = false }: HooksPanelProps) {
{/* Header */}
<div
className={cn(
'flex items-center justify-between border-b border-gray-700',
'flex items-center justify-between border-b border-line',
responsive ? 'px-4 md:px-6 py-4' : 'px-6 py-4'
)}
>
{responsive && (
<div className="absolute top-2 left-1/2 -translate-x-1/2 w-10 h-1 bg-gray-600 rounded-full md:hidden" />
<div className="absolute top-2 left-1/2 -translate-x-1/2 w-10 h-1 bg-surface-emphasis rounded-full md:hidden" />
)}
<div className={cn(responsive && 'mt-2 md:mt-0')}>
<h2 className="text-lg font-semibold flex items-center gap-2">
<Zap size={20} className="text-yellow-400" />
Hooks Configuration
</h2>
<p className="text-xs text-gray-500">
<p className="text-xs text-fg-subtle">
{totalHooks} hooks configured
</p>
</div>
@@ -512,17 +512,17 @@ export function HooksPanel({ onClose, responsive = false }: HooksPanelProps) {
<motion.div
key={type}
layout
className="bg-gray-900/50 rounded-lg overflow-hidden"
className="bg-surface-base/50 rounded-lg overflow-hidden"
>
{/* Type Header */}
<div
className={cn(
'flex items-center gap-3 p-3',
'hover:bg-gray-900/80 transition-colors cursor-pointer'
'hover:bg-surface-base/80 transition-colors cursor-pointer'
)}
onClick={() => toggleExpanded(type)}
>
<button className="text-gray-500 hover:text-gray-300">
<button className="text-fg-subtle hover:text-fg-secondary">
{isExpanded ? <ChevronDown size={16} /> : <ChevronRight size={16} />}
</button>
@@ -530,14 +530,14 @@ export function HooksPanel({ onClose, responsive = false }: HooksPanelProps) {
<div className="flex-1 min-w-0">
<div className="flex items-center gap-2">
<span className="font-medium text-gray-200">{label}</span>
<span className="font-medium text-fg-secondary">{label}</span>
{hookCount > 0 && (
<span className="text-xs bg-gray-700 px-1.5 py-0.5 rounded">
<span className="text-xs bg-surface-muted px-1.5 py-0.5 rounded">
{hookCount}
</span>
)}
</div>
<div className="text-xs text-gray-500">{description}</div>
<div className="text-xs text-fg-subtle">{description}</div>
</div>
<Button
@@ -568,7 +568,7 @@ export function HooksPanel({ onClose, responsive = false }: HooksPanelProps) {
transition={{ duration: 0.2 }}
className="overflow-hidden"
>
<div className="px-4 pb-3 pt-1 border-t border-gray-700/50">
<div className="px-4 pb-3 pt-1 border-t border-line/50">
{isFileHook ? renderFileHooks(type) : renderSessionHooks()}
</div>
</motion.div>
@@ -584,7 +584,7 @@ export function HooksPanel({ onClose, responsive = false }: HooksPanelProps) {
{/* Footer */}
<div
className={cn(
'border-t border-gray-700 text-xs text-gray-500 px-4 py-3 flex items-start gap-2',
'border-t border-line text-xs text-fg-subtle px-4 py-3 flex items-start gap-2',
responsive && 'safe-area-pb'
)}
>
+27 -27
View File
@@ -48,13 +48,13 @@ function getStatusColor(status: MCPServerStatus['status']) {
case 'connecting':
return 'bg-yellow-500 animate-pulse';
case 'disconnected':
return 'bg-gray-500';
return 'bg-surface-muted';
case 'disabled':
return 'bg-gray-600';
return 'bg-surface-emphasis';
case 'error':
return 'bg-red-500';
default:
return 'bg-gray-500';
return 'bg-surface-muted';
}
}
@@ -216,7 +216,7 @@ export function MCPPanel({ onClose, responsive = false }: MCPPanelProps) {
const LoadingSkeleton = () => (
<div className="space-y-3 p-4">
{[1, 2, 3].map((i) => (
<div key={i} className="flex items-center gap-3 p-3 bg-gray-900/50 rounded-lg">
<div key={i} className="flex items-center gap-3 p-3 bg-surface-base/50 rounded-lg">
<Skeleton className="h-3 w-3 rounded-full" />
<div className="flex-1 space-y-2">
<Skeleton className="h-4 w-32" />
@@ -250,7 +250,7 @@ export function MCPPanel({ onClose, responsive = false }: MCPPanelProps) {
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'
@@ -259,19 +259,19 @@ export function MCPPanel({ onClose, responsive = false }: MCPPanelProps) {
{/* Header */}
<div
className={cn(
'flex items-center justify-between border-b border-gray-700',
'flex items-center justify-between border-b border-line',
responsive ? 'px-4 md:px-6 py-4' : 'px-6 py-4'
)}
>
{responsive && (
<div className="absolute top-2 left-1/2 -translate-x-1/2 w-10 h-1 bg-gray-600 rounded-full md:hidden" />
<div className="absolute top-2 left-1/2 -translate-x-1/2 w-10 h-1 bg-surface-emphasis rounded-full md:hidden" />
)}
<div className={cn(responsive && 'mt-2 md:mt-0')}>
<h2 className="text-lg font-semibold flex items-center gap-2">
<Plug size={20} className="text-primary-400" />
MCP Servers
</h2>
<p className="text-xs text-gray-500">
<p className="text-xs text-fg-subtle">
{servers.length} servers ({connectedCount} connected, {totalToolCount} tools)
</p>
</div>
@@ -302,10 +302,10 @@ export function MCPPanel({ onClose, responsive = false }: MCPPanelProps) {
{loading ? (
<LoadingSkeleton />
) : servers.length === 0 ? (
<div className="flex flex-col items-center justify-center py-12 text-gray-500">
<div className="flex flex-col items-center justify-center py-12 text-fg-subtle">
<Plug size={48} className="mb-4 opacity-50" />
<p className="text-center">No MCP servers configured</p>
<p className="text-xs text-gray-600 mt-2 text-center max-w-xs">
<p className="text-xs text-fg-subtle mt-2 text-center max-w-xs">
Configure MCP servers in your .ai-assist/config.json file
</p>
</div>
@@ -325,18 +325,18 @@ export function MCPPanel({ onClose, responsive = false }: MCPPanelProps) {
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"
>
{/* Server Header */}
<div
className={cn(
'flex items-center gap-3 p-3',
'hover:bg-gray-900/80 transition-colors cursor-pointer'
'hover:bg-surface-base/80 transition-colors cursor-pointer'
)}
onClick={() => toggleExpanded(server.name)}
>
{/* Expand Icon */}
<button className="text-gray-500 hover:text-gray-300">
<button className="text-fg-subtle hover:text-fg-secondary">
{isExpanded ? <ChevronDown size={16} /> : <ChevronRight size={16} />}
</button>
@@ -349,10 +349,10 @@ export function MCPPanel({ onClose, responsive = false }: MCPPanelProps) {
{/* Info */}
<div className="flex-1 min-w-0">
<div className="flex items-center gap-2">
<span className="font-medium text-gray-200">{server.name}</span>
<span className="text-xs text-gray-500">{server.type}</span>
<span className="font-medium text-fg-secondary">{server.name}</span>
<span className="text-xs text-fg-subtle">{server.type}</span>
</div>
<div className="flex items-center gap-2 text-xs text-gray-500">
<div className="flex items-center gap-2 text-xs text-fg-subtle">
<span>{getStatusText(server.status)}</span>
{server.toolCount > 0 && (
<>
@@ -428,7 +428,7 @@ export function MCPPanel({ onClose, responsive = false }: MCPPanelProps) {
transition={{ duration: 0.2 }}
className="overflow-hidden"
>
<div className="px-4 pb-3 pt-1 border-t border-gray-700/50">
<div className="px-4 pb-3 pt-1 border-t border-line/50">
{/* Error Message */}
{server.error && (
<div className="flex items-start gap-2 p-2 bg-red-500/10 rounded text-red-400 text-xs mb-2">
@@ -439,26 +439,26 @@ export function MCPPanel({ onClose, responsive = false }: MCPPanelProps) {
{/* Config Info */}
{server.config && (
<div className="space-y-1 text-xs text-gray-500">
<div className="space-y-1 text-xs text-fg-subtle">
{server.config.command && (
<div>
<span className="text-gray-400">Command:</span>{' '}
<code className="font-mono bg-gray-800 px-1 rounded">
<span className="text-fg-muted">Command:</span>{' '}
<code className="font-mono bg-surface-subtle px-1 rounded">
{server.config.command.join(' ')}
</code>
</div>
)}
{server.config.url && (
<div>
<span className="text-gray-400">URL:</span>{' '}
<code className="font-mono bg-gray-800 px-1 rounded">
<span className="text-fg-muted">URL:</span>{' '}
<code className="font-mono bg-surface-subtle px-1 rounded">
{server.config.url}
</code>
</div>
)}
{server.config.timeout && (
<div>
<span className="text-gray-400">Timeout:</span>{' '}
<span className="text-fg-muted">Timeout:</span>{' '}
{server.config.timeout}ms
</div>
)}
@@ -468,12 +468,12 @@ export function MCPPanel({ onClose, responsive = false }: MCPPanelProps) {
{/* Tools List */}
{server.tools && server.tools.length > 0 && (
<div className="mt-3">
<div className="text-xs text-gray-400 mb-1">Tools:</div>
<div className="text-xs text-fg-muted mb-1">Tools:</div>
<div className="flex flex-wrap gap-1">
{server.tools.map((tool) => (
<span
key={tool.name}
className="px-2 py-0.5 bg-gray-800 rounded text-xs text-gray-300"
className="px-2 py-0.5 bg-surface-subtle rounded text-xs text-fg-secondary"
title={tool.description}
>
{tool.originalName}
@@ -496,11 +496,11 @@ export function MCPPanel({ onClose, responsive = false }: MCPPanelProps) {
{/* Footer Info */}
<div
className={cn(
'border-t border-gray-700 text-xs text-gray-500 text-center',
'border-t border-line text-xs text-fg-subtle text-center',
responsive ? 'px-4 py-3 safe-area-pb' : 'px-6 py-3'
)}
>
Configure servers in <code className="font-mono bg-gray-900 px-1 rounded">.ai-assist/config.json</code>
Configure servers in <code className="font-mono bg-surface-base px-1 rounded">.ai-assist/config.json</code>
</div>
</motion.div>
</motion.div>
+10 -10
View File
@@ -69,13 +69,13 @@ export function Markdown({ content, className }: MarkdownProps) {
return <ol className="list-decimal list-inside mb-4 space-y-1">{children}</ol>;
},
li({ children }) {
return <li className="text-gray-200">{children}</li>;
return <li className="text-fg-secondary">{children}</li>;
},
// 引用
blockquote({ children }) {
return (
<blockquote className="border-l-4 border-gray-600 pl-4 my-4 text-gray-400 italic">
<blockquote className="border-l-4 border-line-muted pl-4 my-4 text-fg-muted italic">
{children}
</blockquote>
);
@@ -97,7 +97,7 @@ export function Markdown({ content, className }: MarkdownProps) {
// 强调
strong({ children }) {
return <strong className="font-bold text-gray-100">{children}</strong>;
return <strong className="font-bold text-fg">{children}</strong>;
},
em({ children }) {
return <em className="italic">{children}</em>;
@@ -105,38 +105,38 @@ export function Markdown({ content, className }: MarkdownProps) {
// 分割线
hr() {
return <hr className="my-6 border-gray-700" />;
return <hr className="my-6 border-line" />;
},
// 表格
table({ children }) {
return (
<div className="overflow-x-auto my-4">
<table className="min-w-full border-collapse border border-gray-700">
<table className="min-w-full border-collapse border border-line">
{children}
</table>
</div>
);
},
thead({ children }) {
return <thead className="bg-gray-800">{children}</thead>;
return <thead className="bg-surface-subtle">{children}</thead>;
},
tbody({ children }) {
return <tbody>{children}</tbody>;
},
tr({ children }) {
return <tr className="border-b border-gray-700">{children}</tr>;
return <tr className="border-b border-line">{children}</tr>;
},
th({ children }) {
return (
<th className="px-4 py-2 text-left text-sm font-semibold text-gray-200 border-r border-gray-700 last:border-r-0">
<th className="px-4 py-2 text-left text-sm font-semibold text-fg-secondary border-r border-line last:border-r-0">
{children}
</th>
);
},
td({ children }) {
return (
<td className="px-4 py-2 text-sm text-gray-300 border-r border-gray-700 last:border-r-0">
<td className="px-4 py-2 text-sm text-fg-secondary border-r border-line last:border-r-0">
{children}
</td>
);
@@ -156,7 +156,7 @@ export function Markdown({ content, className }: MarkdownProps) {
// 预格式化文本(非代码块的 pre)
pre({ children }) {
return (
<pre className="bg-gray-900 p-4 rounded-lg overflow-x-auto my-4 text-sm">
<pre className="bg-surface-base p-4 rounded-lg overflow-x-auto my-4 text-sm">
{children}
</pre>
);
+26 -26
View File
@@ -86,7 +86,7 @@ function getPermissionIcon(type: PermissionType) {
case 'web':
return <Globe size={24} className="text-green-400" />;
default:
return <Shield size={24} className="text-gray-400" />;
return <Shield size={24} className="text-fg-muted" />;
}
}
@@ -113,9 +113,9 @@ function DiffViewer({ diff }: { diff: DiffInfo }) {
}
return (
<div className="mt-4 rounded-lg border border-gray-700 overflow-hidden">
<div className="flex items-center justify-between px-3 py-2 bg-gray-900/50 border-b border-gray-700">
<span className="text-xs text-gray-400">
<div className="mt-4 rounded-lg border border-line overflow-hidden">
<div className="flex items-center justify-between px-3 py-2 bg-surface-base/50 border-b border-line">
<span className="text-xs text-fg-muted">
{diff.isNew ? 'New file' : 'Changes'}
</span>
<div className="flex items-center gap-3 text-xs">
@@ -127,7 +127,7 @@ function DiffViewer({ diff }: { diff: DiffInfo }) {
<pre className="text-xs font-mono">
{diff.hunks.map((hunk, hunkIndex) => (
<div key={hunkIndex}>
<div className="px-3 py-1 bg-blue-500/10 text-blue-400 border-y border-gray-700/50">
<div className="px-3 py-1 bg-blue-500/10 text-blue-400 border-y border-line/50">
@@ -{hunk.oldStart},{hunk.oldCount} +{hunk.newStart},{hunk.newCount} @@
</div>
{hunk.lines.map((line, lineIndex) => {
@@ -141,7 +141,7 @@ function DiffViewer({ diff }: { diff: DiffInfo }) {
className += 'bg-red-500/10 text-red-400';
prefix = '-';
} else {
className += 'text-gray-400';
className += 'text-fg-muted';
}
return (
@@ -182,8 +182,8 @@ export function PermissionDialog({
case 'bash':
return (
<div className="space-y-2">
<div className="text-sm text-gray-400">Command:</div>
<code className="block px-3 py-2 bg-gray-900 rounded-lg font-mono text-sm text-yellow-300 break-all">
<div className="text-sm text-fg-muted">Command:</div>
<code className="block px-3 py-2 bg-surface-base rounded-lg font-mono text-sm text-yellow-300 break-all">
{context.command}
</code>
{context.externalPaths && context.externalPaths.length > 0 && (
@@ -206,7 +206,7 @@ export function PermissionDialog({
return (
<div className="space-y-2">
<div className="flex items-center gap-2 text-sm">
<span className="text-gray-400">Operation:</span>
<span className="text-fg-muted">Operation:</span>
<span className={cn(
'px-2 py-0.5 rounded text-xs font-medium',
context.operation === 'delete' ? 'bg-red-500/20 text-red-400' :
@@ -216,8 +216,8 @@ export function PermissionDialog({
{context.operation?.toUpperCase()}
</span>
</div>
<div className="text-sm text-gray-400">Path:</div>
<code className="block px-3 py-2 bg-gray-900 rounded-lg font-mono text-sm text-blue-300 break-all">
<div className="text-sm text-fg-muted">Path:</div>
<code className="block px-3 py-2 bg-surface-base rounded-lg font-mono text-sm text-blue-300 break-all">
{context.path}
</code>
{diff && <DiffViewer diff={diff} />}
@@ -228,15 +228,15 @@ export function PermissionDialog({
return (
<div className="space-y-2">
<div className="flex items-center gap-2 text-sm">
<span className="text-gray-400">Git operation:</span>
<span className="text-fg-muted">Git operation:</span>
<span className="px-2 py-0.5 rounded text-xs font-medium bg-purple-500/20 text-purple-400">
{context.gitOperation?.toUpperCase()}
</span>
</div>
{context.command && (
<>
<div className="text-sm text-gray-400">Command:</div>
<code className="block px-3 py-2 bg-gray-900 rounded-lg font-mono text-sm text-purple-300 break-all">
<div className="text-sm text-fg-muted">Command:</div>
<code className="block px-3 py-2 bg-surface-base rounded-lg font-mono text-sm text-purple-300 break-all">
{context.command}
</code>
</>
@@ -247,8 +247,8 @@ export function PermissionDialog({
case 'web':
return (
<div className="space-y-2">
<div className="text-sm text-gray-400">Request:</div>
<code className="block px-3 py-2 bg-gray-900 rounded-lg font-mono text-sm text-green-300 break-all">
<div className="text-sm text-fg-muted">Request:</div>
<code className="block px-3 py-2 bg-surface-base rounded-lg font-mono text-sm text-green-300 break-all">
{context.query || context.command}
</code>
</div>
@@ -256,8 +256,8 @@ export function PermissionDialog({
default:
return (
<div className="text-sm text-gray-400">
<pre className="bg-gray-900 p-3 rounded-lg overflow-auto">
<div className="text-sm text-fg-muted">
<pre className="bg-surface-base p-3 rounded-lg overflow-auto">
{JSON.stringify(context, null, 2)}
</pre>
</div>
@@ -285,24 +285,24 @@ export function PermissionDialog({
exit="exit"
transition={smoothTransition}
className={cn(
'bg-gray-800 overflow-hidden flex flex-col',
'bg-surface-subtle 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'
)}
>
{/* Header */}
<div className="flex items-center justify-between px-5 py-4 border-b border-gray-700">
<div className="flex items-center justify-between px-5 py-4 border-b border-line">
{responsive && (
<div className="absolute top-2 left-1/2 -translate-x-1/2 w-10 h-1 bg-gray-600 rounded-full md:hidden" />
<div className="absolute top-2 left-1/2 -translate-x-1/2 w-10 h-1 bg-surface-emphasis rounded-full md:hidden" />
)}
<div className={cn('flex items-center gap-3', responsive && 'mt-2 md:mt-0')}>
<div className="p-2 rounded-lg bg-gray-900">
<div className="p-2 rounded-lg bg-surface-base">
{getPermissionIcon(permissionType)}
</div>
<div>
<h2 className="text-lg font-semibold">{getPermissionTitle(permissionType)}</h2>
<p className="text-xs text-gray-500">AI is requesting permission</p>
<p className="text-xs text-fg-subtle">AI is requesting permission</p>
</div>
</div>
<Button
@@ -322,16 +322,16 @@ export function PermissionDialog({
{/* Footer */}
<div className={cn(
'flex flex-col gap-3 border-t border-gray-700',
'flex flex-col gap-3 border-t border-line',
responsive ? 'px-4 py-4 safe-area-pb' : 'px-5 py-4'
)}>
{/* Remember checkbox */}
<label className="flex items-center gap-2 text-sm text-gray-400 cursor-pointer">
<label className="flex items-center gap-2 text-sm text-fg-muted cursor-pointer">
<input
type="checkbox"
checked={remember}
onChange={(e) => setRemember(e.target.checked)}
className="w-4 h-4 rounded border-gray-600 bg-gray-900 text-primary-500 focus:ring-primary-500"
className="w-4 h-4 rounded border-line-muted bg-surface-base text-primary-500 focus:ring-primary-500"
/>
Remember for this session
</label>
+33 -33
View File
@@ -217,7 +217,7 @@ export function ProviderEditor({
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'
@@ -226,12 +226,12 @@ export function ProviderEditor({
{/* Header */}
<div
className={cn(
'flex items-center justify-between border-b border-gray-700',
'flex items-center justify-between border-b border-line',
responsive ? 'px-4 md:px-6 py-4' : 'px-6 py-4'
)}
>
{responsive && (
<div className="absolute top-2 left-1/2 -translate-x-1/2 w-10 h-1 bg-gray-600 rounded-full md:hidden" />
<div className="absolute top-2 left-1/2 -translate-x-1/2 w-10 h-1 bg-surface-emphasis rounded-full md:hidden" />
)}
<div className={cn(responsive && 'mt-2 md:mt-0')}>
<h2 className="text-lg font-semibold flex items-center gap-2">
@@ -239,7 +239,7 @@ export function ProviderEditor({
{loading ? 'Loading...' : provider?.name || providerId}
</h2>
{provider && (
<p className="text-xs text-gray-500">
<p className="text-xs text-fg-subtle">
{provider.builtin ? 'Built-in Provider' : 'Custom Provider'}
</p>
)}
@@ -283,17 +283,17 @@ export function ProviderEditor({
)}
{/* Enabled Toggle */}
<div className="flex items-center justify-between p-3 bg-gray-900/50 rounded-lg">
<div className="flex items-center justify-between p-3 bg-surface-base/50 rounded-lg">
<div>
<span className="text-sm font-medium">Enabled</span>
<p className="text-xs text-gray-500">Use this provider for model selection</p>
<p className="text-xs text-fg-subtle">Use this provider for model selection</p>
</div>
<button
type="button"
onClick={() => setEnabled(!enabled)}
className={cn(
'relative inline-flex h-6 w-11 items-center rounded-full transition-colors',
enabled ? 'bg-primary-500' : 'bg-gray-600'
enabled ? 'bg-primary-500' : 'bg-surface-emphasis'
)}
>
<span
@@ -307,14 +307,14 @@ export function ProviderEditor({
{/* API Key Section */}
<div className="space-y-3">
<h3 className="text-sm font-medium text-gray-300 flex items-center gap-2">
<h3 className="text-sm font-medium text-fg-secondary flex items-center gap-2">
<Key size={14} />
API Key Configuration
</h3>
{/* API Key Input */}
<div>
<label className="block text-xs text-gray-400 mb-1">API Key</label>
<label className="block text-xs text-fg-muted mb-1">API Key</label>
<div className="relative">
<Input
type={showApiKey ? 'text' : 'password'}
@@ -326,7 +326,7 @@ export function ProviderEditor({
<button
type="button"
onClick={() => setShowApiKey(!showApiKey)}
className="absolute right-3 top-1/2 -translate-y-1/2 text-gray-400 hover:text-gray-300"
className="absolute right-3 top-1/2 -translate-y-1/2 text-fg-muted hover:text-fg-secondary"
>
{showApiKey ? <EyeOff size={16} /> : <Eye size={16} />}
</button>
@@ -338,7 +338,7 @@ export function ProviderEditor({
{/* API Key Env Var */}
<div>
<label className="block text-xs text-gray-400 mb-1">
<label className="block text-xs text-fg-muted mb-1">
Environment Variable (alternative)
</label>
<Input
@@ -346,7 +346,7 @@ export function ProviderEditor({
onChange={(e) => setApiKeyEnvVar(e.target.value)}
placeholder={provider?.apiKeyEnvVar || 'PROVIDER_API_KEY'}
/>
<p className="text-xs text-gray-500 mt-1">
<p className="text-xs text-fg-subtle mt-1">
If no API key is set, this env var will be used
</p>
</div>
@@ -354,20 +354,20 @@ export function ProviderEditor({
{/* Base URL Section */}
<div className="space-y-3">
<h3 className="text-sm font-medium text-gray-300 flex items-center gap-2">
<h3 className="text-sm font-medium text-fg-secondary flex items-center gap-2">
<Globe size={14} />
Endpoint Configuration
</h3>
<div>
<label className="block text-xs text-gray-400 mb-1">Base URL</label>
<label className="block text-xs text-fg-muted mb-1">Base URL</label>
<Input
value={baseUrl}
onChange={(e) => setBaseUrl(e.target.value)}
placeholder={provider?.baseUrl || 'https://api.provider.com/v1'}
/>
{provider?.builtin && (
<p className="text-xs text-gray-500 mt-1">
<p className="text-xs text-fg-subtle mt-1">
Leave empty to use default endpoint
</p>
)}
@@ -378,7 +378,7 @@ export function ProviderEditor({
{provider?.allowCustomModels && (
<div className="space-y-3">
<div className="flex items-center justify-between">
<h3 className="text-sm font-medium text-gray-300 flex items-center gap-2">
<h3 className="text-sm font-medium text-fg-secondary flex items-center gap-2">
<Cpu size={14} />
Custom Models ({provider.config.customModels.length})
</h3>
@@ -402,10 +402,10 @@ export function ProviderEditor({
exit={{ height: 0, opacity: 0 }}
className="overflow-hidden"
>
<div className="p-3 bg-gray-900/50 rounded-lg space-y-3 border border-gray-700">
<div className="p-3 bg-surface-base/50 rounded-lg space-y-3 border border-line">
<div className="grid grid-cols-2 gap-3">
<div>
<label className="block text-xs text-gray-400 mb-1">Model ID</label>
<label className="block text-xs text-fg-muted mb-1">Model ID</label>
<Input
value={newModelId}
onChange={(e) => setNewModelId(e.target.value)}
@@ -414,7 +414,7 @@ export function ProviderEditor({
/>
</div>
<div>
<label className="block text-xs text-gray-400 mb-1">Display Name</label>
<label className="block text-xs text-fg-muted mb-1">Display Name</label>
<Input
value={newModelName}
onChange={(e) => setNewModelName(e.target.value)}
@@ -424,21 +424,21 @@ export function ProviderEditor({
</div>
</div>
<div className="flex items-center gap-4">
<label className="flex items-center gap-2 text-sm text-gray-400 cursor-pointer">
<label className="flex items-center gap-2 text-sm text-fg-muted cursor-pointer">
<input
type="checkbox"
checked={newModelVision}
onChange={(e) => setNewModelVision(e.target.checked)}
className="w-4 h-4 rounded border-gray-600 bg-gray-900 text-primary-500"
className="w-4 h-4 rounded border-line-muted bg-surface-base text-primary-500"
/>
Vision
</label>
<label className="flex items-center gap-2 text-sm text-gray-400 cursor-pointer">
<label className="flex items-center gap-2 text-sm text-fg-muted cursor-pointer">
<input
type="checkbox"
checked={newModelTools}
onChange={(e) => setNewModelTools(e.target.checked)}
className="w-4 h-4 rounded border-gray-600 bg-gray-900 text-primary-500"
className="w-4 h-4 rounded border-line-muted bg-surface-base text-primary-500"
/>
Function Calling
</label>
@@ -470,17 +470,17 @@ export function ProviderEditor({
{provider.config.customModels.map((model) => (
<div
key={model.id}
className="flex items-center justify-between p-2 bg-gray-900/50 rounded-lg text-sm"
className="flex items-center justify-between p-2 bg-surface-base/50 rounded-lg text-sm"
>
<div>
<span className="text-gray-200">{model.name}</span>
<span className="text-gray-500 ml-2">({model.id})</span>
<span className="text-fg-secondary">{model.name}</span>
<span className="text-fg-subtle ml-2">({model.id})</span>
<div className="flex items-center gap-2 mt-0.5">
{model.capabilities?.vision && (
<span className="text-[10px] text-gray-500">Vision</span>
<span className="text-[10px] text-fg-subtle">Vision</span>
)}
{model.capabilities?.functionCalling && (
<span className="text-[10px] text-gray-500">Tools</span>
<span className="text-[10px] text-fg-subtle">Tools</span>
)}
</div>
</div>
@@ -496,7 +496,7 @@ export function ProviderEditor({
))}
</div>
) : (
<p className="text-xs text-gray-500 text-center py-2">
<p className="text-xs text-fg-subtle text-center py-2">
No custom models added yet
</p>
)}
@@ -505,16 +505,16 @@ export function ProviderEditor({
{/* Built-in Models Info */}
{provider && (
<div className="text-xs text-gray-500 p-3 bg-gray-900/30 rounded-lg">
<div className="text-xs text-fg-subtle p-3 bg-surface-base/30 rounded-lg">
<p className="font-medium mb-1">Built-in Models:</p>
<div className="flex flex-wrap gap-1">
{provider.models.slice(0, 5).map((m) => (
<span key={m.id} className="px-1.5 py-0.5 bg-gray-800 rounded">
<span key={m.id} className="px-1.5 py-0.5 bg-surface-subtle rounded">
{m.name}
</span>
))}
{provider.models.length > 5 && (
<span className="px-1.5 py-0.5 text-gray-400">
<span className="px-1.5 py-0.5 text-fg-muted">
+{provider.models.length - 5} more
</span>
)}
@@ -528,7 +528,7 @@ export function ProviderEditor({
{/* Footer */}
<div
className={cn(
'border-t border-gray-700 flex justify-end gap-2',
'border-t border-line flex justify-end gap-2',
responsive ? 'px-4 py-3 safe-area-pb' : 'px-6 py-3'
)}
>
+39 -39
View File
@@ -255,7 +255,7 @@ export function ProvidersPanel({ onClose, responsive = false }: ProvidersPanelPr
const LoadingSkeleton = () => (
<div className="space-y-3 p-4">
{[1, 2, 3].map((i) => (
<div key={i} className="flex items-center gap-3 p-3 bg-gray-900/50 rounded-lg">
<div key={i} className="flex items-center gap-3 p-3 bg-surface-base/50 rounded-lg">
<Skeleton className="h-4 w-4" />
<div className="flex-1 space-y-2">
<Skeleton className="h-4 w-32" />
@@ -279,18 +279,18 @@ export function ProvidersPanel({ onClose, responsive = false }: ProvidersPanelPr
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"
>
{/* Provider Header */}
<div
className={cn(
'flex items-center gap-3 p-3',
'hover:bg-gray-900/80 transition-colors cursor-pointer'
'hover:bg-surface-base/80 transition-colors cursor-pointer'
)}
onClick={() => toggleExpanded(provider.id)}
>
{/* Expand Icon */}
<button className="text-gray-500 hover:text-gray-300">
<button className="text-fg-subtle hover:text-fg-secondary">
{isExpanded ? <ChevronDown size={16} /> : <ChevronRight size={16} />}
</button>
@@ -304,7 +304,7 @@ export function ProvidersPanel({ onClose, responsive = false }: ProvidersPanelPr
{/* Info */}
<div className="flex-1 min-w-0">
<div className="flex items-center gap-2">
<span className="font-medium text-gray-200">{provider.name}</span>
<span className="font-medium text-fg-secondary">{provider.name}</span>
<span
className={cn(
'text-xs px-2 py-0.5 rounded-full',
@@ -314,7 +314,7 @@ export function ProvidersPanel({ onClose, responsive = false }: ProvidersPanelPr
{provider.builtin ? 'Built-in' : 'Custom'}
</span>
</div>
<div className="text-xs text-gray-500 flex items-center gap-2">
<div className="text-xs text-fg-subtle flex items-center gap-2">
<span>{provider.modelCount} models</span>
{provider.hasApiKey ? (
<span className="text-green-400 flex items-center gap-1">
@@ -349,7 +349,7 @@ export function ProvidersPanel({ onClose, responsive = false }: ProvidersPanelPr
size="sm"
onClick={() => handleTestConnection(provider.id)}
disabled={isTesting}
className="text-gray-400 hover:text-gray-300"
className="text-fg-muted hover:text-fg-secondary"
title="Test Connection"
>
{isTesting ? (
@@ -364,7 +364,7 @@ export function ProvidersPanel({ onClose, responsive = false }: ProvidersPanelPr
variant="ghost"
size="sm"
onClick={() => setEditingProviderId(provider.id)}
className="text-gray-400 hover:text-gray-300"
className="text-fg-muted hover:text-fg-secondary"
title="Configure"
>
<Settings size={14} />
@@ -395,29 +395,29 @@ export function ProvidersPanel({ onClose, responsive = false }: ProvidersPanelPr
transition={{ duration: 0.2 }}
className="overflow-hidden"
>
<div className="px-4 pb-3 pt-1 border-t border-gray-700/50 space-y-3">
<div className="px-4 pb-3 pt-1 border-t border-line/50 space-y-3">
{detail ? (
<>
{/* Base URL */}
{detail.baseUrl && (
<div className="text-xs">
<span className="text-gray-400">Base URL:</span>{' '}
<code className="text-gray-300 bg-gray-800 px-1 rounded">{detail.baseUrl}</code>
<span className="text-fg-muted">Base URL:</span>{' '}
<code className="text-fg-secondary bg-surface-subtle px-1 rounded">{detail.baseUrl}</code>
</div>
)}
{/* API Key Env Var */}
{detail.apiKeyEnvVar && (
<div className="text-xs">
<span className="text-gray-400">API Key Env:</span>{' '}
<code className="text-gray-300 bg-gray-800 px-1 rounded">{detail.apiKeyEnvVar}</code>
<span className="text-fg-muted">API Key Env:</span>{' '}
<code className="text-fg-secondary bg-surface-subtle px-1 rounded">{detail.apiKeyEnvVar}</code>
</div>
)}
{/* Models */}
<div className="space-y-2">
<div className="flex items-center justify-between">
<span className="text-xs text-gray-400 flex items-center gap-1">
<span className="text-xs text-fg-muted flex items-center gap-1">
<Cpu size={12} />
Models ({detail.models.length + detail.config.customModels.length})
</span>
@@ -440,13 +440,13 @@ export function ProvidersPanel({ onClose, responsive = false }: ProvidersPanelPr
{detail.models.map((model) => (
<div
key={model.id}
className="flex items-center justify-between text-xs p-2 bg-gray-800/50 rounded"
className="flex items-center justify-between text-xs p-2 bg-surface-subtle/50 rounded"
>
<div>
<span className="text-gray-200">{model.name}</span>
<span className="text-gray-500 ml-2">({model.id})</span>
<span className="text-fg-secondary">{model.name}</span>
<span className="text-fg-subtle ml-2">({model.id})</span>
</div>
<div className="flex items-center gap-2 text-gray-500">
<div className="flex items-center gap-2 text-fg-subtle">
{model.capabilities?.vision && (
<span title="Vision" className="text-[10px]">Vision</span>
)}
@@ -461,11 +461,11 @@ export function ProvidersPanel({ onClose, responsive = false }: ProvidersPanelPr
{detail.config.customModels.map((model) => (
<div
key={model.id}
className="flex items-center justify-between text-xs p-2 bg-gray-800/50 rounded border border-green-500/20"
className="flex items-center justify-between text-xs p-2 bg-surface-subtle/50 rounded border border-green-500/20"
>
<div>
<span className="text-gray-200">{model.name}</span>
<span className="text-gray-500 ml-2">({model.id})</span>
<span className="text-fg-secondary">{model.name}</span>
<span className="text-fg-subtle ml-2">({model.id})</span>
<span className="text-green-400 ml-2 text-[10px]">custom</span>
</div>
<Button
@@ -516,7 +516,7 @@ export function ProvidersPanel({ onClose, responsive = false }: ProvidersPanelPr
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'
@@ -525,19 +525,19 @@ export function ProvidersPanel({ onClose, responsive = false }: ProvidersPanelPr
{/* Header */}
<div
className={cn(
'flex items-center justify-between border-b border-gray-700',
'flex items-center justify-between border-b border-line',
responsive ? 'px-4 md:px-6 py-4' : 'px-6 py-4'
)}
>
{responsive && (
<div className="absolute top-2 left-1/2 -translate-x-1/2 w-10 h-1 bg-gray-600 rounded-full md:hidden" />
<div className="absolute top-2 left-1/2 -translate-x-1/2 w-10 h-1 bg-surface-emphasis rounded-full md:hidden" />
)}
<div className={cn(responsive && 'mt-2 md:mt-0')}>
<h2 className="text-lg font-semibold flex items-center gap-2">
<Server size={20} className="text-primary-400" />
Model Providers
</h2>
<p className="text-xs text-gray-500">
<p className="text-xs text-fg-subtle">
{providers.length} providers ({builtinProviders.length} built-in, {customProviders.length} custom)
</p>
</div>
@@ -577,7 +577,7 @@ export function ProvidersPanel({ onClose, responsive = false }: ProvidersPanelPr
{loading ? (
<LoadingSkeleton />
) : providers.length === 0 ? (
<div className="flex flex-col items-center justify-center py-12 text-gray-500">
<div className="flex flex-col items-center justify-center py-12 text-fg-subtle">
<Server size={48} className="mb-4 opacity-50" />
<p className="text-center">No providers available</p>
</div>
@@ -590,7 +590,7 @@ export function ProvidersPanel({ onClose, responsive = false }: ProvidersPanelPr
{/* Built-in Providers */}
{builtinProviders.length > 0 && (
<div>
<h3 className="text-xs font-medium text-gray-400 uppercase tracking-wide mb-2 flex items-center gap-2">
<h3 className="text-xs font-medium text-fg-muted uppercase tracking-wide mb-2 flex items-center gap-2">
<Server size={12} />
Built-in Providers
</h3>
@@ -604,7 +604,7 @@ export function ProvidersPanel({ onClose, responsive = false }: ProvidersPanelPr
{/* Custom Providers */}
<div>
<h3 className="text-xs font-medium text-gray-400 uppercase tracking-wide mb-2 flex items-center gap-2">
<h3 className="text-xs font-medium text-fg-muted uppercase tracking-wide mb-2 flex items-center gap-2">
<Globe size={12} />
Custom Providers
</h3>
@@ -615,7 +615,7 @@ export function ProvidersPanel({ onClose, responsive = false }: ProvidersPanelPr
))}
</div>
) : (
<div className="text-center py-6 text-gray-500 text-sm">
<div className="text-center py-6 text-fg-subtle text-sm">
<p>No custom providers yet</p>
<Button
variant="ghost"
@@ -636,12 +636,12 @@ export function ProvidersPanel({ onClose, responsive = false }: ProvidersPanelPr
{/* Footer */}
<div
className={cn(
'border-t border-gray-700 text-xs text-gray-500 text-center',
'border-t border-line text-xs text-fg-subtle text-center',
responsive ? 'px-4 py-3 safe-area-pb' : 'px-6 py-3'
)}
>
Config stored in{' '}
<code className="font-mono bg-gray-900 px-1 rounded">~/.ai-terminal-assistant/providers.json</code>
<code className="font-mono bg-surface-base px-1 rounded">~/.ai-terminal-assistant/providers.json</code>
</div>
</motion.div>
@@ -660,12 +660,12 @@ export function ProvidersPanel({ onClose, responsive = false }: ProvidersPanelPr
animate={{ scale: 1, opacity: 1 }}
exit={{ scale: 0.95, opacity: 0 }}
onClick={(e) => e.stopPropagation()}
className="bg-gray-800 rounded-lg p-6 w-full max-w-md mx-4 space-y-4"
className="bg-surface-subtle rounded-lg p-6 w-full max-w-md mx-4 space-y-4"
>
<h3 className="text-lg font-semibold">Add Custom Provider</h3>
<div className="space-y-3">
<div>
<label className="text-xs text-gray-400">ID (e.g., ollama)</label>
<label className="text-xs text-fg-muted">ID (e.g., ollama)</label>
<Input
value={newProvider.id || ''}
onChange={(e) => setNewProvider((p) => ({ ...p, id: e.target.value }))}
@@ -673,7 +673,7 @@ export function ProvidersPanel({ onClose, responsive = false }: ProvidersPanelPr
/>
</div>
<div>
<label className="text-xs text-gray-400">Name</label>
<label className="text-xs text-fg-muted">Name</label>
<Input
value={newProvider.name || ''}
onChange={(e) => setNewProvider((p) => ({ ...p, name: e.target.value }))}
@@ -681,7 +681,7 @@ export function ProvidersPanel({ onClose, responsive = false }: ProvidersPanelPr
/>
</div>
<div>
<label className="text-xs text-gray-400">Base URL (OpenAI compatible)</label>
<label className="text-xs text-fg-muted">Base URL (OpenAI compatible)</label>
<Input
value={newProvider.baseUrl || ''}
onChange={(e) => setNewProvider((p) => ({ ...p, baseUrl: e.target.value }))}
@@ -689,7 +689,7 @@ export function ProvidersPanel({ onClose, responsive = false }: ProvidersPanelPr
/>
</div>
<div>
<label className="text-xs text-gray-400">API Key Env Var (optional)</label>
<label className="text-xs text-fg-muted">API Key Env Var (optional)</label>
<Input
value={newProvider.apiKeyEnvVar || ''}
onChange={(e) => setNewProvider((p) => ({ ...p, apiKeyEnvVar: e.target.value }))}
@@ -725,12 +725,12 @@ export function ProvidersPanel({ onClose, responsive = false }: ProvidersPanelPr
animate={{ scale: 1, opacity: 1 }}
exit={{ scale: 0.95, opacity: 0 }}
onClick={(e) => e.stopPropagation()}
className="bg-gray-800 rounded-lg p-6 w-full max-w-md mx-4 space-y-4"
className="bg-surface-subtle rounded-lg p-6 w-full max-w-md mx-4 space-y-4"
>
<h3 className="text-lg font-semibold">Add Custom Model</h3>
<div className="space-y-3">
<div>
<label className="text-xs text-gray-400">Model ID</label>
<label className="text-xs text-fg-muted">Model ID</label>
<Input
value={newModel.id || ''}
onChange={(e) => setNewModel((m) => ({ ...m, id: e.target.value }))}
@@ -738,7 +738,7 @@ export function ProvidersPanel({ onClose, responsive = false }: ProvidersPanelPr
/>
</div>
<div>
<label className="text-xs text-gray-400">Display Name</label>
<label className="text-xs text-fg-muted">Display Name</label>
<Input
value={newModel.name || ''}
onChange={(e) => setNewModel((m) => ({ ...m, name: e.target.value }))}
+13 -13
View File
@@ -187,7 +187,7 @@ export function RestoreDialog({
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'
@@ -196,12 +196,12 @@ export function RestoreDialog({
{/* Header */}
<div
className={cn(
'flex items-center justify-between border-b border-gray-700',
'flex items-center justify-between border-b border-line',
responsive ? 'px-4 md:px-6 py-4' : 'px-6 py-4'
)}
>
{responsive && (
<div className="absolute top-2 left-1/2 -translate-x-1/2 w-10 h-1 bg-gray-600 rounded-full md:hidden" />
<div className="absolute top-2 left-1/2 -translate-x-1/2 w-10 h-1 bg-surface-emphasis rounded-full md:hidden" />
)}
<div className={cn(responsive && 'mt-2 md:mt-0')}>
<h2 className="text-lg font-semibold flex items-center gap-2">
@@ -209,7 +209,7 @@ export function RestoreDialog({
Restore Checkpoint
</h2>
{checkpoint && (
<p className="text-xs text-gray-500">
<p className="text-xs text-fg-subtle">
{formatTime(checkpoint.timestamp)}
</p>
)}
@@ -284,7 +284,7 @@ export function RestoreDialog({
{/* Restore Mode Selection */}
{!files && (
<div className="space-y-2">
<label className="text-sm font-medium text-gray-300">Restore Mode</label>
<label className="text-sm font-medium text-fg-secondary">Restore Mode</label>
<div className="space-y-2">
{RESTORE_MODES.map((mode) => (
<label
@@ -293,7 +293,7 @@ export function RestoreDialog({
'flex items-start gap-3 p-3 rounded-lg cursor-pointer transition-colors',
selectedMode === mode.value
? 'bg-primary-500/10 border border-primary-500/50'
: 'bg-gray-900/50 border border-gray-700 hover:border-gray-600'
: 'bg-surface-base/50 border border-line hover:border-line-muted'
)}
>
<input
@@ -305,8 +305,8 @@ export function RestoreDialog({
className="mt-1"
/>
<div>
<div className="font-medium text-gray-200">{mode.label}</div>
<div className="text-xs text-gray-500">{mode.description}</div>
<div className="font-medium text-fg-secondary">{mode.label}</div>
<div className="text-xs text-fg-subtle">{mode.description}</div>
</div>
</label>
))}
@@ -317,15 +317,15 @@ export function RestoreDialog({
{/* Files to Restore */}
{previewResult && previewResult.restoredFiles.length > 0 && (
<div className="space-y-2">
<label className="text-sm font-medium text-gray-300 flex items-center gap-2">
<label className="text-sm font-medium text-fg-secondary flex items-center gap-2">
<FileText size={14} />
Files to restore ({previewResult.restoredFiles.length})
</label>
<div className="max-h-40 overflow-y-auto bg-gray-900/50 rounded-lg p-2 space-y-1">
<div className="max-h-40 overflow-y-auto bg-surface-base/50 rounded-lg p-2 space-y-1">
{previewResult.restoredFiles.map((file) => (
<div
key={file}
className="text-xs font-mono text-gray-400 px-2 py-1 hover:bg-gray-800 rounded"
className="text-xs font-mono text-fg-muted px-2 py-1 hover:bg-surface-subtle rounded"
>
{file}
</div>
@@ -336,7 +336,7 @@ export function RestoreDialog({
{/* Skip Safety Check Option */}
{safetyResult && !safetyResult.safe && (
<label className="flex items-center gap-2 text-sm text-gray-400 cursor-pointer">
<label className="flex items-center gap-2 text-sm text-fg-muted cursor-pointer">
<input
type="checkbox"
checked={skipSafetyCheck}
@@ -353,7 +353,7 @@ export function RestoreDialog({
{/* Footer */}
<div
className={cn(
'flex items-center justify-end gap-3 border-t border-gray-700',
'flex items-center justify-end gap-3 border-t border-line',
responsive ? 'px-4 py-3 safe-area-pb' : 'px-6 py-3'
)}
>
+23 -21
View File
@@ -12,6 +12,7 @@ import { cn } from '../utils/cn';
import { fadeInUp, smoothTransition } from '../utils/animations';
import { listSessions, createSession, deleteSession, type Session } from '../api/client.js';
import { SessionSkeleton } from './Skeleton';
import { ThemeToggleCompact } from './ThemeToggle';
interface SidebarProps {
currentSessionId: string | null;
@@ -112,10 +113,10 @@ export function Sidebar({
transition={smoothTransition}
className="p-6 text-center"
>
<div className="w-12 h-12 mx-auto mb-4 rounded-full bg-gray-700/50 flex items-center justify-center">
<MessageCircle size={24} className="text-gray-500" />
<div className="w-12 h-12 mx-auto mb-4 rounded-full bg-surface-muted/50 flex items-center justify-center">
<MessageCircle size={24} className="text-fg-subtle" />
</div>
<p className="text-gray-400 mb-4">No conversations yet</p>
<p className="text-fg-muted mb-4">No conversations yet</p>
<motion.button
whileHover={{ scale: 1.02 }}
whileTap={{ scale: 0.98 }}
@@ -141,26 +142,26 @@ export function Sidebar({
onClick={() => handleSelectSession(session.id)}
className={cn(
'flex items-center gap-2 p-3 rounded-lg cursor-pointer group',
'hover:bg-gray-700 transition-colors',
'active:bg-gray-600',
currentSessionId === session.id && 'bg-gray-700'
'hover:bg-surface-muted transition-colors',
'active:bg-surface-emphasis',
currentSessionId === session.id && 'bg-surface-muted'
)}
>
<MessageSquare size={18} className="text-gray-400 flex-shrink-0" />
<MessageSquare size={18} className="text-fg-muted flex-shrink-0" />
<div className="flex-1 min-w-0">
<div className="text-sm truncate">
<div className="text-sm truncate text-fg">
{session.name || `Chat ${session.id.slice(0, 8)}`}
</div>
<div className="text-xs text-gray-500">{session.messageCount} messages</div>
<div className="text-xs text-fg-subtle">{session.messageCount} messages</div>
</div>
<motion.button
whileHover={{ scale: 1.1 }}
whileTap={{ scale: 0.9 }}
onClick={(e) => handleDelete(session.id, e)}
className="opacity-0 group-hover:opacity-100 p-1 hover:bg-gray-600 rounded transition-opacity"
className="opacity-0 group-hover:opacity-100 p-1 hover:bg-surface-emphasis rounded transition-opacity"
aria-label="Delete session"
>
<Trash2 size={14} className="text-gray-400" />
<Trash2 size={14} className="text-fg-muted" />
</motion.button>
</motion.div>
)
@@ -179,13 +180,13 @@ export function Sidebar({
const SidebarContent = () => (
<>
{/* Header */}
<div className="p-4 border-b border-gray-700">
<div className="p-4 border-b border-line">
{responsive && (
<div className="flex items-center justify-between mb-3 md:hidden">
<span className="text-lg font-semibold">Sessions</span>
<span className="text-lg font-semibold text-fg">Sessions</span>
<button
onClick={() => setIsOpen(false)}
className="p-1 hover:bg-gray-700 rounded transition-colors"
className="p-1 hover:bg-surface-muted rounded transition-colors"
aria-label="Close menu"
>
<X size={20} />
@@ -196,7 +197,7 @@ export function Sidebar({
whileHover={{ scale: 1.02 }}
whileTap={{ scale: 0.98 }}
onClick={handleCreate}
className="w-full flex items-center justify-center gap-2 px-4 py-2 bg-primary-600 hover:bg-primary-700 rounded-lg transition-colors"
className="w-full flex items-center justify-center gap-2 px-4 py-2 bg-primary-600 hover:bg-primary-700 text-white rounded-lg transition-colors"
>
<Plus size={18} />
<span>New Chat</span>
@@ -221,8 +222,9 @@ export function Sidebar({
</div>
{/* Footer */}
<div className="p-4 border-t border-gray-700 text-center text-xs text-gray-500">
AI Assistant v1.0
<div className="p-4 border-t border-line flex items-center justify-between">
<span className="text-xs text-fg-subtle">AI Assistant v1.0</span>
<ThemeToggleCompact />
</div>
</>
);
@@ -230,7 +232,7 @@ export function Sidebar({
// 非响应式模式
if (!responsive) {
return (
<div className="w-64 bg-gray-800 border-r border-gray-700 flex flex-col">
<div className="w-64 bg-surface-subtle border-r border-line flex flex-col">
<SidebarContent />
</div>
);
@@ -244,7 +246,7 @@ export function Sidebar({
whileHover={{ scale: 1.05 }}
whileTap={{ scale: 0.95 }}
onClick={() => setIsOpen(true)}
className="fixed top-3 left-3 z-40 p-2 rounded-lg bg-gray-800 text-gray-300 hover:bg-gray-700 transition-colors md:hidden"
className="fixed top-3 left-3 z-40 p-2 rounded-lg bg-surface-subtle text-fg-muted hover:bg-surface-muted transition-colors md:hidden"
aria-label="Open menu"
>
<Menu size={20} />
@@ -269,13 +271,13 @@ export function Sidebar({
initial={false}
animate={{ x: isOpen ? 0 : '-100%' }}
transition={{ type: 'spring', stiffness: 300, damping: 30 }}
className="fixed inset-y-0 left-0 z-50 w-64 bg-gray-800 border-r border-gray-700 flex flex-col md:hidden"
className="fixed inset-y-0 left-0 z-50 w-64 bg-surface-subtle border-r border-line flex flex-col md:hidden"
>
<SidebarContent />
</motion.div>
{/* 桌面端固定侧边栏 */}
<div className="hidden md:flex w-64 bg-gray-800 border-r border-gray-700 flex-col">
<div className="hidden md:flex w-64 bg-surface-subtle border-r border-line flex-col">
<SidebarContent />
</div>
</>
+8 -8
View File
@@ -5,7 +5,7 @@ interface SkeletonProps extends React.HTMLAttributes<HTMLDivElement> {}
export function Skeleton({ className, ...props }: SkeletonProps) {
return (
<div
className={cn('animate-pulse rounded-md bg-gray-700', className)}
className={cn('animate-pulse rounded-md bg-surface-muted', className)}
{...props}
/>
);
@@ -14,11 +14,11 @@ export function Skeleton({ className, ...props }: SkeletonProps) {
export function MessageSkeleton() {
return (
<div className="flex gap-3 animate-pulse">
<div className="w-8 h-8 rounded-full bg-gray-700" />
<div className="w-8 h-8 rounded-full bg-surface-muted" />
<div className="flex-1 space-y-2">
<div className="h-4 bg-gray-700 rounded w-1/4" />
<div className="h-4 bg-gray-700 rounded w-3/4" />
<div className="h-4 bg-gray-700 rounded w-1/2" />
<div className="h-4 bg-surface-muted rounded w-1/4" />
<div className="h-4 bg-surface-muted rounded w-3/4" />
<div className="h-4 bg-surface-muted rounded w-1/2" />
</div>
</div>
);
@@ -27,7 +27,7 @@ export function MessageSkeleton() {
export function SessionSkeleton() {
return (
<div className="px-2 py-2 animate-pulse">
<div className="h-10 bg-gray-700 rounded-lg" />
<div className="h-10 bg-surface-muted rounded-lg" />
</div>
);
}
@@ -36,8 +36,8 @@ export function FileSkeleton() {
return (
<div className="px-3 py-2 animate-pulse">
<div className="flex items-center gap-2">
<div className="w-4 h-4 bg-gray-700 rounded" />
<div className="h-4 bg-gray-700 rounded flex-1" />
<div className="w-4 h-4 bg-surface-muted rounded" />
<div className="h-4 bg-surface-muted rounded flex-1" />
</div>
</div>
);
@@ -0,0 +1,92 @@
/**
* Theme Toggle Component
*
* 主题切换按钮组件,支持浅色/深色/跟随系统三种模式
*/
import { Sun, Moon, Monitor } from 'lucide-react';
import { useTheme, type Theme } from '../hooks/useTheme.js';
import { cn } from '../utils/cn.js';
interface ThemeOption {
value: Theme;
icon: typeof Sun;
label: string;
}
const themeOptions: ThemeOption[] = [
{ value: 'light', icon: Sun, label: '浅色模式' },
{ value: 'dark', icon: Moon, label: '深色模式' },
{ value: 'system', icon: Monitor, label: '跟随系统' },
];
interface ThemeToggleProps {
className?: string;
}
/**
* 主题切换按钮组
*/
export function ThemeToggle({ className }: ThemeToggleProps) {
const { theme, setTheme } = useTheme();
return (
<div
className={cn(
'flex items-center gap-1 p-1 bg-surface-subtle rounded-lg',
className
)}
>
{themeOptions.map(({ value, icon: Icon, label }) => (
<button
key={value}
onClick={() => setTheme(value)}
title={label}
className={cn(
'p-2 rounded-md transition-colors',
theme === value
? 'bg-surface-emphasis text-fg'
: 'text-fg-muted hover:text-fg hover:bg-surface-muted'
)}
>
<Icon size={16} />
</button>
))}
</div>
);
}
/**
* 简洁版主题切换按钮(单个按钮循环切换)
*/
export function ThemeToggleCompact({ className }: ThemeToggleProps) {
const { theme, resolvedTheme, setTheme } = useTheme();
const handleToggle = () => {
const nextTheme: Theme =
theme === 'light' ? 'dark' : theme === 'dark' ? 'system' : 'light';
setTheme(nextTheme);
};
const Icon = theme === 'system' ? Monitor : resolvedTheme === 'dark' ? Moon : Sun;
const label =
theme === 'system'
? '跟随系统'
: resolvedTheme === 'dark'
? '深色模式'
: '浅色模式';
return (
<button
onClick={handleToggle}
title={`当前: ${label} (点击切换)`}
className={cn(
'p-2 rounded-md transition-colors',
'text-fg-muted hover:text-fg hover:bg-surface-muted',
className
)}
>
<Icon size={18} />
</button>
);
}
+169
View File
@@ -0,0 +1,169 @@
/**
* Theme Hook
*
* 管理应用主题状态(浅色/深色/跟随系统)
*/
import { useState, useEffect, useCallback, createContext, useContext } from 'react';
export type Theme = 'light' | 'dark' | 'system';
export type ResolvedTheme = 'light' | 'dark';
const STORAGE_KEY = 'ai-assistant-theme';
interface ThemeContextValue {
theme: Theme;
resolvedTheme: ResolvedTheme;
setTheme: (theme: Theme) => void;
}
const ThemeContext = createContext<ThemeContextValue | null>(null);
/**
* 获取系统主题偏好
*/
function getSystemTheme(): ResolvedTheme {
if (typeof window === 'undefined') return 'dark';
return window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light';
}
/**
* 获取存储的主题设置
*/
function getStoredTheme(): Theme {
if (typeof window === 'undefined') return 'system';
const stored = localStorage.getItem(STORAGE_KEY);
if (stored === 'light' || stored === 'dark' || stored === 'system') {
return stored;
}
return 'system';
}
/**
* 解析最终主题
*/
function resolveTheme(theme: Theme): ResolvedTheme {
if (theme === 'system') {
return getSystemTheme();
}
return theme;
}
/**
* 应用主题到 DOM
*/
function applyTheme(resolvedTheme: ResolvedTheme) {
if (typeof document === 'undefined') return;
document.documentElement.classList.toggle('dark', resolvedTheme === 'dark');
}
/**
* Theme Hook
*
* 提供主题状态管理功能:
* - 支持浅色、深色、跟随系统三种模式
* - 自动持久化到 localStorage
* - 监听系统主题变化
*/
export function useTheme(): ThemeContextValue {
const context = useContext(ThemeContext);
if (context) {
return context;
}
// 如果没有 Provider,使用独立状态(向后兼容)
const [theme, setThemeState] = useState<Theme>(() => getStoredTheme());
const [resolvedTheme, setResolvedTheme] = useState<ResolvedTheme>(() =>
resolveTheme(getStoredTheme())
);
// 监听系统主题变化
useEffect(() => {
const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)');
const updateResolvedTheme = () => {
const resolved = resolveTheme(theme);
setResolvedTheme(resolved);
applyTheme(resolved);
};
updateResolvedTheme();
mediaQuery.addEventListener('change', updateResolvedTheme);
return () => mediaQuery.removeEventListener('change', updateResolvedTheme);
}, [theme]);
// 设置主题
const setTheme = useCallback((newTheme: Theme) => {
setThemeState(newTheme);
localStorage.setItem(STORAGE_KEY, newTheme);
}, []);
return { theme, resolvedTheme, setTheme };
}
/**
* Theme Provider Props
*/
interface ThemeProviderProps {
children: React.ReactNode;
defaultTheme?: Theme;
}
/**
* Theme Provider
*
* 在应用顶层提供主题上下文
*/
export function ThemeProvider({ children, defaultTheme = 'system' }: ThemeProviderProps) {
const [theme, setThemeState] = useState<Theme>(() => {
const stored = getStoredTheme();
return stored !== 'system' ? stored : defaultTheme;
});
const [resolvedTheme, setResolvedTheme] = useState<ResolvedTheme>(() =>
resolveTheme(theme)
);
// 监听系统主题变化
useEffect(() => {
const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)');
const updateResolvedTheme = () => {
const resolved = resolveTheme(theme);
setResolvedTheme(resolved);
applyTheme(resolved);
};
updateResolvedTheme();
mediaQuery.addEventListener('change', updateResolvedTheme);
return () => mediaQuery.removeEventListener('change', updateResolvedTheme);
}, [theme]);
// 设置主题
const setTheme = useCallback((newTheme: Theme) => {
setThemeState(newTheme);
localStorage.setItem(STORAGE_KEY, newTheme);
}, []);
return (
<ThemeContext.Provider value={{ theme, resolvedTheme, setTheme }}>
{children}
</ThemeContext.Provider>
);
}
/**
* 初始化主题(防止闪烁)
*
* 在 HTML head 中调用此脚本,确保页面加载时立即应用正确的主题
*/
export const themeInitScript = `
(function() {
const STORAGE_KEY = 'ai-assistant-theme';
const stored = localStorage.getItem(STORAGE_KEY);
const theme = stored === 'light' || stored === 'dark' ? stored : 'system';
const isDark = theme === 'dark' || (theme === 'system' && window.matchMedia('(prefers-color-scheme: dark)').matches);
document.documentElement.classList.toggle('dark', isDark);
})();
`;
+4
View File
@@ -212,3 +212,7 @@ export { toast } from 'sonner';
// Hooks
export { useChat } from './hooks/useChat.js';
export { useCommands } from './hooks/useCommands.js';
export { useTheme, ThemeProvider, themeInitScript, type Theme, type ResolvedTheme } from './hooks/useTheme.js';
// Theme Components
export { ThemeToggle, ThemeToggleCompact } from './components/ThemeToggle.js';
+3 -3
View File
@@ -8,10 +8,10 @@ const buttonVariants = cva(
variants: {
variant: {
default: 'bg-primary-600 text-white hover:bg-primary-700',
secondary: 'bg-gray-700 text-gray-100 hover:bg-gray-600',
ghost: 'hover:bg-gray-800 text-gray-300',
secondary: 'bg-surface-muted text-fg hover:bg-surface-emphasis',
ghost: 'hover:bg-surface-subtle text-fg-muted',
destructive: 'bg-red-600 text-white hover:bg-red-700',
outline: 'border border-gray-600 bg-transparent hover:bg-gray-800',
outline: 'border border-line bg-transparent hover:bg-surface-subtle',
},
size: {
default: 'h-10 px-4 py-2',
+3 -3
View File
@@ -32,7 +32,7 @@ export const DialogContent = forwardRef<
<DialogPrimitive.Content
ref={ref}
className={cn(
'fixed left-[50%] top-[50%] z-50 translate-x-[-50%] translate-y-[-50%] rounded-lg border border-gray-700 bg-gray-800 p-6 shadow-lg duration-200 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[state=closed]:slide-out-to-left-1/2 data-[state=closed]:slide-out-to-top-[48%] data-[state=open]:slide-in-from-left-1/2 data-[state=open]:slide-in-from-top-[48%]',
'fixed left-[50%] top-[50%] z-50 translate-x-[-50%] translate-y-[-50%] rounded-lg border border-line bg-surface-subtle p-6 shadow-lg duration-200 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[state=closed]:slide-out-to-left-1/2 data-[state=closed]:slide-out-to-top-[48%] data-[state=open]:slide-in-from-left-1/2 data-[state=open]:slide-in-from-top-[48%]',
className
)}
{...props}
@@ -76,7 +76,7 @@ export const DialogTitle = forwardRef<
>(({ className, ...props }, ref) => (
<DialogPrimitive.Title
ref={ref}
className={cn('text-lg font-semibold text-gray-100', className)}
className={cn('text-lg font-semibold text-fg', className)}
{...props}
/>
));
@@ -88,7 +88,7 @@ export const DialogDescription = forwardRef<
>(({ className, ...props }, ref) => (
<DialogPrimitive.Description
ref={ref}
className={cn('text-sm text-gray-400', className)}
className={cn('text-sm text-fg-muted', className)}
{...props}
/>
));
+1 -1
View File
@@ -9,7 +9,7 @@ export const Input = forwardRef<HTMLInputElement, InputProps>(
<input
type={type}
className={cn(
'flex h-10 w-full rounded-lg border border-gray-600 bg-gray-800 px-3 py-2 text-sm text-gray-100 placeholder:text-gray-400 focus:outline-none focus:ring-2 focus:ring-primary-500 disabled:cursor-not-allowed disabled:opacity-50',
'flex h-10 w-full rounded-lg border border-line bg-surface-subtle px-3 py-2 text-sm text-fg placeholder:text-fg-muted focus:outline-none focus:ring-2 focus:ring-primary-500 disabled:cursor-not-allowed disabled:opacity-50',
className
)}
ref={ref}
+4 -4
View File
@@ -14,7 +14,7 @@ export const SelectTrigger = forwardRef<
<SelectPrimitive.Trigger
ref={ref}
className={cn(
'flex h-10 w-full items-center justify-between rounded-lg border border-gray-600 bg-gray-800 px-3 py-2 text-sm text-gray-100 placeholder:text-gray-400 focus:outline-none focus:ring-2 focus:ring-primary-500 disabled:cursor-not-allowed disabled:opacity-50',
'flex h-10 w-full items-center justify-between rounded-lg border border-line bg-surface-subtle px-3 py-2 text-sm text-fg placeholder:text-fg-muted focus:outline-none focus:ring-2 focus:ring-primary-500 disabled:cursor-not-allowed disabled:opacity-50',
className
)}
{...props}
@@ -63,7 +63,7 @@ export const SelectContent = forwardRef<
<SelectPrimitive.Content
ref={ref}
className={cn(
'relative z-50 max-h-96 min-w-[8rem] overflow-hidden rounded-lg border border-gray-600 bg-gray-800 text-gray-100 shadow-md data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95',
'relative z-50 max-h-96 min-w-[8rem] overflow-hidden rounded-lg border border-line bg-surface-subtle text-fg shadow-md data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95',
position === 'popper' &&
'data-[side=bottom]:translate-y-1 data-[side=left]:-translate-x-1 data-[side=right]:translate-x-1 data-[side=top]:-translate-y-1',
className
@@ -106,7 +106,7 @@ export const SelectItem = forwardRef<
<SelectPrimitive.Item
ref={ref}
className={cn(
'relative flex w-full cursor-default select-none items-center rounded-md py-1.5 pl-8 pr-2 text-sm outline-none focus:bg-gray-700 data-[disabled]:pointer-events-none data-[disabled]:opacity-50',
'relative flex w-full cursor-default select-none items-center rounded-md py-1.5 pl-8 pr-2 text-sm outline-none focus:bg-surface-muted data-[disabled]:pointer-events-none data-[disabled]:opacity-50',
className
)}
{...props}
@@ -127,7 +127,7 @@ export const SelectSeparator = forwardRef<
>(({ className, ...props }, ref) => (
<SelectPrimitive.Separator
ref={ref}
className={cn('-mx-1 my-1 h-px bg-gray-700', className)}
className={cn('-mx-1 my-1 h-px bg-line', className)}
{...props}
/>
));
+2 -2
View File
@@ -11,10 +11,10 @@ export const Slider = forwardRef<
className={cn('relative flex w-full touch-none select-none items-center', className)}
{...props}
>
<SliderPrimitive.Track className="relative h-2 w-full grow overflow-hidden rounded-full bg-gray-700">
<SliderPrimitive.Track className="relative h-2 w-full grow overflow-hidden rounded-full bg-surface-muted">
<SliderPrimitive.Range className="absolute h-full bg-primary-500" />
</SliderPrimitive.Track>
<SliderPrimitive.Thumb className="block h-5 w-5 rounded-full border-2 border-primary-500 bg-gray-900 transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary-500 focus-visible:ring-offset-2 focus-visible:ring-offset-gray-900 disabled:pointer-events-none disabled:opacity-50" />
<SliderPrimitive.Thumb className="block h-5 w-5 rounded-full border-2 border-primary-500 bg-surface-base transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary-500 focus-visible:ring-offset-2 focus-visible:ring-offset-surface-base disabled:pointer-events-none disabled:opacity-50" />
</SliderPrimitive.Root>
));
Slider.displayName = SliderPrimitive.Root.displayName;
+1 -1
View File
@@ -8,7 +8,7 @@ export const Switch = forwardRef<
>(({ className, ...props }, ref) => (
<SwitchPrimitive.Root
className={cn(
'peer inline-flex h-6 w-11 shrink-0 cursor-pointer items-center rounded-full border-2 border-transparent transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary-500 focus-visible:ring-offset-2 focus-visible:ring-offset-gray-900 disabled:cursor-not-allowed disabled:opacity-50 data-[state=checked]:bg-primary-600 data-[state=unchecked]:bg-gray-700',
'peer inline-flex h-6 w-11 shrink-0 cursor-pointer items-center rounded-full border-2 border-transparent transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary-500 focus-visible:ring-offset-2 focus-visible:ring-offset-surface-base disabled:cursor-not-allowed disabled:opacity-50 data-[state=checked]:bg-primary-600 data-[state=unchecked]:bg-surface-muted',
className
)}
{...props}
+1 -1
View File
@@ -14,7 +14,7 @@ export const TooltipContent = forwardRef<
ref={ref}
sideOffset={sideOffset}
className={cn(
'z-50 overflow-hidden rounded-md bg-gray-900 px-3 py-1.5 text-sm text-gray-100 animate-in fade-in-0 zoom-in-95 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95',
'z-50 overflow-hidden rounded-md bg-surface-base px-3 py-1.5 text-sm text-fg shadow-lg border border-line animate-in fade-in-0 zoom-in-95 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95',
className
)}
{...props}
+60 -14
View File
@@ -5,7 +5,54 @@
* 注意:使用此文件时,宿主项目需要配置 Tailwind CSS
*/
/* iOS Safe Area Support */
/* ============ 主题 CSS 变量 ============ */
:root {
/* 浅色主题 (默认) */
--color-bg-base: 255 255 255; /* #ffffff */
--color-bg-subtle: 246 248 250; /* #f6f8fa */
--color-bg-muted: 240 242 245; /* #f0f2f5 */
--color-bg-emphasis: 229 231 235; /* #e5e7eb */
--color-text-primary: 31 41 55; /* #1f2937 */
--color-text-secondary: 75 85 99; /* #4b5563 */
--color-text-muted: 107 114 128; /* #6b7280 */
--color-text-subtle: 156 163 175; /* #9ca3af */
--color-border-default: 229 231 235; /* #e5e7eb */
--color-border-muted: 209 213 219; /* #d1d5db */
--color-code-bg: 246 248 250; /* #f6f8fa */
/* 滚动条颜色 */
--scrollbar-thumb: #9ca3af;
--scrollbar-thumb-hover: #6b7280;
}
.dark {
/* 深色主题 */
--color-bg-base: 17 24 39; /* #111827 */
--color-bg-subtle: 31 41 55; /* #1f2937 */
--color-bg-muted: 55 65 81; /* #374151 */
--color-bg-emphasis: 75 85 99; /* #4b5563 */
--color-text-primary: 243 244 246; /* #f3f4f6 */
--color-text-secondary: 229 231 235; /* #e5e7eb */
--color-text-muted: 156 163 175; /* #9ca3af */
--color-text-subtle: 107 114 128; /* #6b7280 */
--color-border-default: 55 65 81; /* #374151 */
--color-border-muted: 75 85 99; /* #4b5563 */
--color-code-bg: 13 17 23; /* #0d1117 */
/* 滚动条颜色 */
--scrollbar-thumb: #4b5563;
--scrollbar-thumb-hover: #6b7280;
}
/* ============ iOS Safe Area Support ============ */
@supports (padding-bottom: env(safe-area-inset-bottom)) {
.safe-area-pb {
padding-bottom: calc(env(safe-area-inset-bottom) + 0.5rem);
@@ -26,7 +73,8 @@ html {
touch-action: pan-y;
}
/* Custom scrollbar */
/* ============ Custom scrollbar ============ */
::-webkit-scrollbar {
width: 8px;
height: 8px;
@@ -37,29 +85,30 @@ html {
}
::-webkit-scrollbar-thumb {
background: #4b5563;
background: var(--scrollbar-thumb);
border-radius: 4px;
}
::-webkit-scrollbar-thumb:hover {
background: #6b7280;
background: var(--scrollbar-thumb-hover);
}
/* Message content */
/* ============ Message content ============ */
.message-content {
max-width: none;
color: #f3f4f6; /* text-gray-100 */
color: rgb(var(--color-text-primary));
}
.message-content pre {
background: #1f2937; /* bg-gray-800 */
background: rgb(var(--color-code-bg));
border-radius: 0.5rem;
padding: 1rem;
overflow-x: auto;
}
.message-content code {
background: #1f2937; /* bg-gray-800 */
background: rgb(var(--color-code-bg));
padding: 0.125rem 0.375rem;
border-radius: 0.25rem;
font-size: 0.875rem;
@@ -70,7 +119,8 @@ html {
padding: 0;
}
/* Typing indicator */
/* ============ Typing indicator ============ */
.typing-indicator {
display: flex;
gap: 4px;
@@ -79,7 +129,7 @@ html {
.typing-indicator span {
width: 8px;
height: 8px;
background: #6b7280;
background: rgb(var(--color-text-muted));
border-radius: 50%;
animation: typing 1.4s infinite ease-in-out;
}
@@ -109,7 +159,3 @@ html {
}
}
/* bg-gray-850 custom color */
.bg-gray-850 {
background-color: #1a1f2a;
}