feat(ui): 实现深色/浅色主题切换功能
- 添加 CSS 变量定义浅色和深色主题色板 - 扩展 Tailwind 配置支持语义化颜色 (surface-*, fg-*, line-*, code) - 创建 useTheme hook 管理主题状态和持久化 - 创建 ThemeToggle 组件支持三种模式 (light/dark/system) - 迁移所有组件从硬编码 gray-* 到语义化颜色 - 支持系统主题偏好检测 (prefers-color-scheme) - 添加主题初始化脚本防止闪烁 (FOUC)
This commit is contained in:
@@ -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'
|
||||
)}
|
||||
>
|
||||
|
||||
@@ -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'
|
||||
)}
|
||||
>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
)}
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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" />
|
||||
)}
|
||||
|
||||
@@ -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
|
||||
)}
|
||||
>
|
||||
|
||||
@@ -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'
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
)}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)}%` }}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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'
|
||||
)}
|
||||
>
|
||||
|
||||
@@ -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'
|
||||
)}
|
||||
>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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'
|
||||
)}
|
||||
>
|
||||
|
||||
@@ -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 }))}
|
||||
|
||||
@@ -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'
|
||||
)}
|
||||
>
|
||||
|
||||
@@ -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>
|
||||
</>
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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);
|
||||
})();
|
||||
`;
|
||||
@@ -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';
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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}
|
||||
/>
|
||||
));
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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}
|
||||
/>
|
||||
));
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
+15
-3
@@ -1,12 +1,13 @@
|
||||
<!doctype html>
|
||||
<html lang="en" class="dark">
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<link rel="icon" type="image/svg+xml" href="/icon.svg" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0, viewport-fit=cover, maximum-scale=1.0, user-scalable=no" />
|
||||
|
||||
<!-- PWA Meta Tags -->
|
||||
<meta name="theme-color" content="#111827" />
|
||||
<meta name="theme-color" content="#111827" media="(prefers-color-scheme: dark)" />
|
||||
<meta name="theme-color" content="#ffffff" media="(prefers-color-scheme: light)" />
|
||||
<meta name="mobile-web-app-capable" content="yes" />
|
||||
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent" />
|
||||
<meta name="apple-mobile-web-app-title" content="AI Assistant" />
|
||||
@@ -19,8 +20,19 @@
|
||||
<link rel="manifest" href="/manifest.json" />
|
||||
|
||||
<title>AI Assistant</title>
|
||||
|
||||
<!-- Theme initialization script (prevents flash) -->
|
||||
<script>
|
||||
(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);
|
||||
})();
|
||||
</script>
|
||||
</head>
|
||||
<body class="bg-gray-900 text-gray-100">
|
||||
<body class="bg-surface-base text-fg">
|
||||
<div id="root"></div>
|
||||
<script type="module" src="/src/main.tsx"></script>
|
||||
</body>
|
||||
|
||||
+114
-109
@@ -16,6 +16,7 @@ import {
|
||||
CheckpointPanel,
|
||||
ProvidersPanel,
|
||||
Toaster,
|
||||
ThemeProvider,
|
||||
listSessions,
|
||||
createSession,
|
||||
type Session,
|
||||
@@ -94,129 +95,133 @@ export function App() {
|
||||
|
||||
if (isInitializing) {
|
||||
return (
|
||||
<div className="h-screen flex items-center justify-center bg-gray-900">
|
||||
<div className="text-center">
|
||||
<div className="w-8 h-8 border-2 border-primary-500 border-t-transparent rounded-full animate-spin mx-auto mb-4" />
|
||||
<p className="text-gray-400">Initializing...</p>
|
||||
<ThemeProvider>
|
||||
<div className="h-screen flex items-center justify-center bg-surface-base">
|
||||
<div className="text-center">
|
||||
<div className="w-8 h-8 border-2 border-primary-500 border-t-transparent rounded-full animate-spin mx-auto mb-4" />
|
||||
<p className="text-fg-muted">Initializing...</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</ThemeProvider>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="h-screen flex bg-gray-900">
|
||||
<Sidebar
|
||||
currentSessionId={currentSessionId}
|
||||
onSelectSession={handleSelectSession}
|
||||
onCreateSession={handleCreateSession}
|
||||
responsive
|
||||
sessionTitleUpdate={sessionTitleUpdate}
|
||||
/>
|
||||
<ThemeProvider>
|
||||
<div className="h-screen flex bg-surface-base">
|
||||
<Sidebar
|
||||
currentSessionId={currentSessionId}
|
||||
onSelectSession={handleSelectSession}
|
||||
onCreateSession={handleCreateSession}
|
||||
responsive
|
||||
sessionTitleUpdate={sessionTitleUpdate}
|
||||
/>
|
||||
|
||||
{/* 主内容区域 */}
|
||||
<div className="flex-1 flex min-w-0">
|
||||
{/* 聊天区域 */}
|
||||
<div className={`flex-1 min-w-0 ${showFileBrowser ? 'hidden md:block md:w-1/2' : 'w-full'}`}>
|
||||
{currentSessionId ? (
|
||||
<ChatPage
|
||||
key={currentSessionId}
|
||||
sessionId={currentSessionId}
|
||||
onSessionNotFound={handleSessionNotFound}
|
||||
onSessionUpdated={handleSessionUpdated}
|
||||
responsive
|
||||
showFileBrowser={showFileBrowser}
|
||||
onToggleFileBrowser={() => setShowFileBrowser(!showFileBrowser)}
|
||||
onOpenConfig={() => setShowConfig(true)}
|
||||
onOpenCommands={() => setShowCommands(true)}
|
||||
onOpenMCP={() => setShowMCP(true)}
|
||||
onOpenHooks={() => setShowHooks(true)}
|
||||
onOpenAgents={() => setShowAgents(true)}
|
||||
onOpenCheckpoints={() => setShowCheckpoints(true)}
|
||||
onOpenProviders={() => setShowProviders(true)}
|
||||
/>
|
||||
) : (
|
||||
<div className="flex-1 flex items-center justify-center h-full">
|
||||
<p className="text-gray-400">Select or create a session</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* 文件浏览器 - 桌面端侧边栏,移动端全屏覆盖 */}
|
||||
{showFileBrowser && (
|
||||
<>
|
||||
{/* 移动端: 全屏覆盖 */}
|
||||
<div className="fixed inset-0 z-50 bg-gray-900 md:hidden">
|
||||
<div className="flex items-center justify-between p-3 border-b border-gray-700">
|
||||
<span className="text-lg font-semibold">Files</span>
|
||||
<button
|
||||
onClick={() => setShowFileBrowser(false)}
|
||||
className="p-2 rounded-lg bg-gray-700 text-gray-300 hover:bg-gray-600"
|
||||
>
|
||||
<svg className="w-5 h-5" 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 className="flex-1 flex min-w-0">
|
||||
{/* 聊天区域 */}
|
||||
<div className={`flex-1 min-w-0 ${showFileBrowser ? 'hidden md:block md:w-1/2' : 'w-full'}`}>
|
||||
{currentSessionId ? (
|
||||
<ChatPage
|
||||
key={currentSessionId}
|
||||
sessionId={currentSessionId}
|
||||
onSessionNotFound={handleSessionNotFound}
|
||||
onSessionUpdated={handleSessionUpdated}
|
||||
responsive
|
||||
showFileBrowser={showFileBrowser}
|
||||
onToggleFileBrowser={() => setShowFileBrowser(!showFileBrowser)}
|
||||
onOpenConfig={() => setShowConfig(true)}
|
||||
onOpenCommands={() => setShowCommands(true)}
|
||||
onOpenMCP={() => setShowMCP(true)}
|
||||
onOpenHooks={() => setShowHooks(true)}
|
||||
onOpenAgents={() => setShowAgents(true)}
|
||||
onOpenCheckpoints={() => setShowCheckpoints(true)}
|
||||
onOpenProviders={() => setShowProviders(true)}
|
||||
/>
|
||||
) : (
|
||||
<div className="flex-1 flex items-center justify-center h-full">
|
||||
<p className="text-fg-muted">Select or create a session</p>
|
||||
</div>
|
||||
<div className="h-[calc(100%-56px)]">
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* 文件浏览器 - 桌面端侧边栏,移动端全屏覆盖 */}
|
||||
{showFileBrowser && (
|
||||
<>
|
||||
{/* 移动端: 全屏覆盖 */}
|
||||
<div className="fixed inset-0 z-50 bg-surface-base md:hidden">
|
||||
<div className="flex items-center justify-between p-3 border-b border-line">
|
||||
<span className="text-lg font-semibold text-fg">Files</span>
|
||||
<button
|
||||
onClick={() => setShowFileBrowser(false)}
|
||||
className="p-2 rounded-lg bg-surface-muted text-fg-muted hover:bg-surface-emphasis"
|
||||
>
|
||||
<svg className="w-5 h-5" 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>
|
||||
<div className="h-[calc(100%-56px)]">
|
||||
<FileBrowser
|
||||
onFileSelect={(path, _content) => {
|
||||
console.log('Selected file:', path);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 桌面端: 侧边栏 */}
|
||||
<div className="hidden md:block w-1/2 border-l border-line">
|
||||
<FileBrowser
|
||||
onFileSelect={(path, _content) => {
|
||||
console.log('Selected file:', path);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* 桌面端: 侧边栏 */}
|
||||
<div className="hidden md:block w-1/2 border-l border-gray-700">
|
||||
<FileBrowser
|
||||
onFileSelect={(path, _content) => {
|
||||
console.log('Selected file:', path);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
{/* 配置面板 */}
|
||||
{showConfig && <ConfigPanel onClose={() => setShowConfig(false)} responsive />}
|
||||
|
||||
{/* 命令面板 */}
|
||||
{showCommands && <CommandPanel onClose={() => setShowCommands(false)} responsive />}
|
||||
|
||||
{/* MCP 面板 */}
|
||||
{showMCP && <MCPPanel onClose={() => setShowMCP(false)} responsive />}
|
||||
|
||||
{/* Hooks 面板 */}
|
||||
{showHooks && <HooksPanel onClose={() => setShowHooks(false)} responsive />}
|
||||
|
||||
{/* Agents 面板 */}
|
||||
{showAgents && <AgentsPanel onClose={() => setShowAgents(false)} responsive />}
|
||||
|
||||
{/* Checkpoints 面板 */}
|
||||
{showCheckpoints && <CheckpointPanel onClose={() => setShowCheckpoints(false)} responsive />}
|
||||
|
||||
{/* Providers 面板 */}
|
||||
{showProviders && <ProvidersPanel onClose={() => setShowProviders(false)} responsive />}
|
||||
|
||||
{/* 移动端底部文件按钮 */}
|
||||
<button
|
||||
onClick={() => setShowFileBrowser(true)}
|
||||
className="fixed bottom-20 right-4 z-30 p-3 rounded-full bg-surface-muted text-fg-muted hover:bg-surface-emphasis active:bg-surface-emphasis shadow-lg md:hidden"
|
||||
title="Browse Files"
|
||||
>
|
||||
<svg className="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth={2}
|
||||
d="M3 7v10a2 2 0 002 2h14a2 2 0 002-2V9a2 2 0 00-2-2h-6l-2-2H5a2 2 0 00-2 2z"
|
||||
/>
|
||||
</svg>
|
||||
</button>
|
||||
|
||||
{/* Toast 通知 */}
|
||||
<Toaster />
|
||||
</div>
|
||||
|
||||
{/* 配置面板 */}
|
||||
{showConfig && <ConfigPanel onClose={() => setShowConfig(false)} responsive />}
|
||||
|
||||
{/* 命令面板 */}
|
||||
{showCommands && <CommandPanel onClose={() => setShowCommands(false)} responsive />}
|
||||
|
||||
{/* MCP 面板 */}
|
||||
{showMCP && <MCPPanel onClose={() => setShowMCP(false)} responsive />}
|
||||
|
||||
{/* Hooks 面板 */}
|
||||
{showHooks && <HooksPanel onClose={() => setShowHooks(false)} responsive />}
|
||||
|
||||
{/* Agents 面板 */}
|
||||
{showAgents && <AgentsPanel onClose={() => setShowAgents(false)} responsive />}
|
||||
|
||||
{/* Checkpoints 面板 */}
|
||||
{showCheckpoints && <CheckpointPanel onClose={() => setShowCheckpoints(false)} responsive />}
|
||||
|
||||
{/* Providers 面板 */}
|
||||
{showProviders && <ProvidersPanel onClose={() => setShowProviders(false)} responsive />}
|
||||
|
||||
{/* 移动端底部文件按钮 */}
|
||||
<button
|
||||
onClick={() => setShowFileBrowser(true)}
|
||||
className="fixed bottom-20 right-4 z-30 p-3 rounded-full bg-gray-700 text-gray-300 hover:bg-gray-600 active:bg-gray-500 shadow-lg md:hidden"
|
||||
title="Browse Files"
|
||||
>
|
||||
<svg className="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth={2}
|
||||
d="M3 7v10a2 2 0 002 2h14a2 2 0 002-2V9a2 2 0 00-2-2h-6l-2-2H5a2 2 0 00-2 2z"
|
||||
/>
|
||||
</svg>
|
||||
</button>
|
||||
|
||||
{/* Toast 通知 */}
|
||||
<Toaster />
|
||||
</div>
|
||||
</ThemeProvider>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -97,11 +97,11 @@ export function ChatPage({
|
||||
<MessageSquare size={32} className="text-primary-400" />
|
||||
</div>
|
||||
|
||||
<h2 className="text-2xl font-semibold mb-2 bg-gradient-to-r from-white to-gray-400 bg-clip-text text-transparent">
|
||||
<h2 className="text-2xl font-semibold mb-2 text-fg">
|
||||
Start a conversation
|
||||
</h2>
|
||||
|
||||
<p className="text-gray-400 mb-6 max-w-md mx-auto">
|
||||
<p className="text-fg-muted mb-6 max-w-md mx-auto">
|
||||
Ask me anything about coding, debugging, or software development.
|
||||
</p>
|
||||
|
||||
@@ -112,7 +112,7 @@ export function ChatPage({
|
||||
whileHover={{ scale: 1.02 }}
|
||||
whileTap={{ scale: 0.98 }}
|
||||
onClick={() => sendMessage(suggestion)}
|
||||
className="px-3 py-1.5 bg-gray-800 hover:bg-gray-700 rounded-full text-sm text-gray-300 transition-colors"
|
||||
className="px-3 py-1.5 bg-surface-subtle hover:bg-surface-muted rounded-full text-sm text-fg-secondary transition-colors"
|
||||
>
|
||||
"{suggestion}"
|
||||
</motion.button>
|
||||
@@ -148,8 +148,8 @@ export function ChatPage({
|
||||
return (
|
||||
<div className="flex-1 flex flex-col h-screen">
|
||||
{/* Header */}
|
||||
<div className="flex items-center justify-between px-4 md:px-6 py-3 border-b border-gray-700 bg-gray-800">
|
||||
<h1 className="text-lg font-medium">Chat</h1>
|
||||
<div className="flex items-center justify-between px-4 md:px-6 py-3 border-b border-line bg-surface-subtle">
|
||||
<h1 className="text-lg font-medium text-fg">Chat</h1>
|
||||
<div className="flex items-center gap-3">
|
||||
{/* 上下文使用情况 - 紧凑模式 */}
|
||||
{sessionId && (
|
||||
@@ -166,14 +166,14 @@ export function ChatPage({
|
||||
|
||||
{/* 工具栏按钮 */}
|
||||
{(onOpenConfig || onToggleFileBrowser || onOpenCommands || onOpenMCP || onOpenHooks || onOpenAgents || onOpenCheckpoints || onOpenProviders) && (
|
||||
<div className="flex items-center gap-1.5 border-l border-gray-600 pl-3">
|
||||
<div className="flex items-center gap-1.5 border-l border-line-muted pl-3">
|
||||
{/* Checkpoints 按钮 */}
|
||||
{onOpenCheckpoints && (
|
||||
<motion.button
|
||||
whileHover={{ scale: 1.1 }}
|
||||
whileTap={{ scale: 0.9 }}
|
||||
onClick={onOpenCheckpoints}
|
||||
className="p-1.5 rounded-lg text-gray-400 hover:text-gray-200 hover:bg-gray-700 transition-colors"
|
||||
className="p-1.5 rounded-lg text-fg-muted hover:text-fg-secondary hover:bg-surface-muted transition-colors"
|
||||
title="Checkpoints"
|
||||
>
|
||||
<History size={20} />
|
||||
@@ -186,7 +186,7 @@ export function ChatPage({
|
||||
whileHover={{ scale: 1.1 }}
|
||||
whileTap={{ scale: 0.9 }}
|
||||
onClick={onOpenProviders}
|
||||
className="p-1.5 rounded-lg text-gray-400 hover:text-gray-200 hover:bg-gray-700 transition-colors"
|
||||
className="p-1.5 rounded-lg text-fg-muted hover:text-fg-secondary hover:bg-surface-muted transition-colors"
|
||||
title="Model Providers"
|
||||
>
|
||||
<Server size={20} />
|
||||
@@ -199,7 +199,7 @@ export function ChatPage({
|
||||
whileHover={{ scale: 1.1 }}
|
||||
whileTap={{ scale: 0.9 }}
|
||||
onClick={onOpenAgents}
|
||||
className="p-1.5 rounded-lg text-gray-400 hover:text-gray-200 hover:bg-gray-700 transition-colors"
|
||||
className="p-1.5 rounded-lg text-fg-muted hover:text-fg-secondary hover:bg-surface-muted transition-colors"
|
||||
title="Agent Presets"
|
||||
>
|
||||
<Bot size={20} />
|
||||
@@ -212,7 +212,7 @@ export function ChatPage({
|
||||
whileHover={{ scale: 1.1 }}
|
||||
whileTap={{ scale: 0.9 }}
|
||||
onClick={onOpenHooks}
|
||||
className="p-1.5 rounded-lg text-gray-400 hover:text-gray-200 hover:bg-gray-700 transition-colors"
|
||||
className="p-1.5 rounded-lg text-fg-muted hover:text-fg-secondary hover:bg-surface-muted transition-colors"
|
||||
title="Hooks"
|
||||
>
|
||||
<Zap size={20} />
|
||||
@@ -225,7 +225,7 @@ export function ChatPage({
|
||||
whileHover={{ scale: 1.1 }}
|
||||
whileTap={{ scale: 0.9 }}
|
||||
onClick={onOpenMCP}
|
||||
className="p-1.5 rounded-lg text-gray-400 hover:text-gray-200 hover:bg-gray-700 transition-colors"
|
||||
className="p-1.5 rounded-lg text-fg-muted hover:text-fg-secondary hover:bg-surface-muted transition-colors"
|
||||
title="MCP Servers"
|
||||
>
|
||||
<Plug size={20} />
|
||||
@@ -238,7 +238,7 @@ export function ChatPage({
|
||||
whileHover={{ scale: 1.1 }}
|
||||
whileTap={{ scale: 0.9 }}
|
||||
onClick={onOpenCommands}
|
||||
className="p-1.5 rounded-lg text-gray-400 hover:text-gray-200 hover:bg-gray-700 transition-colors"
|
||||
className="p-1.5 rounded-lg text-fg-muted hover:text-fg-secondary hover:bg-surface-muted transition-colors"
|
||||
title="Commands"
|
||||
>
|
||||
<Terminal size={20} />
|
||||
@@ -251,7 +251,7 @@ export function ChatPage({
|
||||
whileHover={{ scale: 1.1 }}
|
||||
whileTap={{ scale: 0.9 }}
|
||||
onClick={onOpenConfig}
|
||||
className="p-1.5 rounded-lg text-gray-400 hover:text-gray-200 hover:bg-gray-700 transition-colors"
|
||||
className="p-1.5 rounded-lg text-fg-muted hover:text-fg-secondary hover:bg-surface-muted transition-colors"
|
||||
title="Settings"
|
||||
>
|
||||
<Settings size={20} />
|
||||
@@ -267,7 +267,7 @@ export function ChatPage({
|
||||
className={`hidden md:block p-1.5 rounded-lg transition-colors ${
|
||||
showFileBrowser
|
||||
? 'text-blue-400 bg-blue-500/20'
|
||||
: 'text-gray-400 hover:text-gray-200 hover:bg-gray-700'
|
||||
: 'text-fg-muted hover:text-fg-secondary hover:bg-surface-muted'
|
||||
}`}
|
||||
title={showFileBrowser ? 'Hide Files' : 'Show Files'}
|
||||
>
|
||||
|
||||
@@ -9,6 +9,26 @@ export default {
|
||||
theme: {
|
||||
extend: {
|
||||
colors: {
|
||||
// 语义化颜色 (引用 CSS 变量)
|
||||
surface: {
|
||||
base: 'rgb(var(--color-bg-base) / <alpha-value>)',
|
||||
subtle: 'rgb(var(--color-bg-subtle) / <alpha-value>)',
|
||||
muted: 'rgb(var(--color-bg-muted) / <alpha-value>)',
|
||||
emphasis: 'rgb(var(--color-bg-emphasis) / <alpha-value>)',
|
||||
},
|
||||
fg: {
|
||||
DEFAULT: 'rgb(var(--color-text-primary) / <alpha-value>)',
|
||||
secondary: 'rgb(var(--color-text-secondary) / <alpha-value>)',
|
||||
muted: 'rgb(var(--color-text-muted) / <alpha-value>)',
|
||||
subtle: 'rgb(var(--color-text-subtle) / <alpha-value>)',
|
||||
},
|
||||
line: {
|
||||
DEFAULT: 'rgb(var(--color-border-default) / <alpha-value>)',
|
||||
muted: 'rgb(var(--color-border-muted) / <alpha-value>)',
|
||||
},
|
||||
// 代码块背景
|
||||
code: 'rgb(var(--color-code-bg) / <alpha-value>)',
|
||||
// 保留现有 primary 色板
|
||||
primary: {
|
||||
50: '#f0f9ff',
|
||||
100: '#e0f2fe',
|
||||
|
||||
Reference in New Issue
Block a user