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}
|
transition={smoothTransition}
|
||||||
onClick={(e) => e.stopPropagation()}
|
onClick={(e) => e.stopPropagation()}
|
||||||
className={cn(
|
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
|
responsive
|
||||||
? 'w-full md:w-full md:max-w-lg md:mx-4 rounded-t-2xl md:rounded-lg'
|
? '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'
|
: 'rounded-lg w-full max-w-lg mx-4'
|
||||||
@@ -159,19 +159,19 @@ export function AgentDefaultsEditor({
|
|||||||
{/* Header */}
|
{/* Header */}
|
||||||
<div
|
<div
|
||||||
className={cn(
|
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 ? 'px-4 md:px-6 py-4' : 'px-6 py-4'
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
{responsive && (
|
{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')}>
|
<div className={cn(responsive && 'mt-2 md:mt-0')}>
|
||||||
<h2 className="text-lg font-semibold flex items-center gap-2">
|
<h2 className="text-lg font-semibold flex items-center gap-2">
|
||||||
<Settings size={20} className="text-primary-400" />
|
<Settings size={20} className="text-primary-400" />
|
||||||
Global Defaults
|
Global Defaults
|
||||||
</h2>
|
</h2>
|
||||||
<p className="text-xs text-gray-500">
|
<p className="text-xs text-fg-subtle">
|
||||||
These settings apply to all agents unless overridden
|
These settings apply to all agents unless overridden
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
@@ -203,9 +203,9 @@ export function AgentDefaultsEditor({
|
|||||||
|
|
||||||
{/* Execution Limits */}
|
{/* Execution Limits */}
|
||||||
<div className="space-y-4">
|
<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>
|
<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
|
<input
|
||||||
type="number"
|
type="number"
|
||||||
value={maxSteps ?? ''}
|
value={maxSteps ?? ''}
|
||||||
@@ -214,9 +214,9 @@ export function AgentDefaultsEditor({
|
|||||||
}
|
}
|
||||||
placeholder="15"
|
placeholder="15"
|
||||||
min="1"
|
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
|
Maximum number of tool call steps for all agents
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
@@ -224,15 +224,15 @@ export function AgentDefaultsEditor({
|
|||||||
|
|
||||||
{/* Model Configuration */}
|
{/* Model Configuration */}
|
||||||
<div className="space-y-4">
|
<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">
|
<div className="grid grid-cols-2 gap-4">
|
||||||
{/* Provider */}
|
{/* Provider */}
|
||||||
<div>
|
<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
|
<select
|
||||||
value={modelProvider}
|
value={modelProvider}
|
||||||
onChange={(e) => setModelProvider(e.target.value)}
|
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="">Default</option>
|
||||||
<option value="anthropic">Anthropic</option>
|
<option value="anthropic">Anthropic</option>
|
||||||
@@ -243,19 +243,19 @@ export function AgentDefaultsEditor({
|
|||||||
|
|
||||||
{/* Model */}
|
{/* Model */}
|
||||||
<div>
|
<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
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
value={modelName}
|
value={modelName}
|
||||||
onChange={(e) => setModelName(e.target.value)}
|
onChange={(e) => setModelName(e.target.value)}
|
||||||
placeholder="claude-sonnet-4-20250514"
|
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>
|
</div>
|
||||||
|
|
||||||
{/* Temperature */}
|
{/* Temperature */}
|
||||||
<div>
|
<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
|
<input
|
||||||
type="number"
|
type="number"
|
||||||
value={temperature ?? ''}
|
value={temperature ?? ''}
|
||||||
@@ -266,13 +266,13 @@ export function AgentDefaultsEditor({
|
|||||||
min="0"
|
min="0"
|
||||||
max="1"
|
max="1"
|
||||||
step="0.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>
|
</div>
|
||||||
|
|
||||||
{/* Max Tokens */}
|
{/* Max Tokens */}
|
||||||
<div>
|
<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
|
<input
|
||||||
type="number"
|
type="number"
|
||||||
value={maxTokens ?? ''}
|
value={maxTokens ?? ''}
|
||||||
@@ -281,7 +281,7 @@ export function AgentDefaultsEditor({
|
|||||||
}
|
}
|
||||||
placeholder="8192"
|
placeholder="8192"
|
||||||
min="1"
|
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>
|
||||||
</div>
|
</div>
|
||||||
@@ -301,7 +301,7 @@ export function AgentDefaultsEditor({
|
|||||||
{/* Footer */}
|
{/* Footer */}
|
||||||
<div
|
<div
|
||||||
className={cn(
|
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'
|
responsive ? 'px-4 py-3 safe-area-pb' : 'px-6 py-3'
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -55,10 +55,10 @@ function CollapsibleSection({
|
|||||||
const [isOpen, setIsOpen] = useState(defaultOpen);
|
const [isOpen, setIsOpen] = useState(defaultOpen);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="border border-gray-700 rounded-lg overflow-hidden">
|
<div className="border border-line rounded-lg overflow-hidden">
|
||||||
<button
|
<button
|
||||||
type="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)}
|
onClick={() => setIsOpen(!isOpen)}
|
||||||
>
|
>
|
||||||
<span className="font-medium text-sm">{title}</span>
|
<span className="font-medium text-sm">{title}</span>
|
||||||
@@ -72,7 +72,7 @@ function CollapsibleSection({
|
|||||||
exit={{ height: 0, opacity: 0 }}
|
exit={{ height: 0, opacity: 0 }}
|
||||||
transition={{ duration: 0.2 }}
|
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>
|
</motion.div>
|
||||||
)}
|
)}
|
||||||
</AnimatePresence>
|
</AnimatePresence>
|
||||||
@@ -284,7 +284,7 @@ export function AgentEditor({
|
|||||||
transition={smoothTransition}
|
transition={smoothTransition}
|
||||||
onClick={(e) => e.stopPropagation()}
|
onClick={(e) => e.stopPropagation()}
|
||||||
className={cn(
|
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
|
responsive
|
||||||
? 'w-full md:w-full md:max-w-2xl md:mx-4 rounded-t-2xl md:rounded-lg'
|
? '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'
|
: 'rounded-lg w-full max-w-2xl mx-4'
|
||||||
@@ -293,12 +293,12 @@ export function AgentEditor({
|
|||||||
{/* Header */}
|
{/* Header */}
|
||||||
<div
|
<div
|
||||||
className={cn(
|
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 ? 'px-4 md:px-6 py-4' : 'px-6 py-4'
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
{responsive && (
|
{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')}>
|
<div className={cn(responsive && 'mt-2 md:mt-0')}>
|
||||||
<h2 className="text-lg font-semibold flex items-center gap-2">
|
<h2 className="text-lg font-semibold flex items-center gap-2">
|
||||||
@@ -315,7 +315,7 @@ export function AgentEditor({
|
|||||||
)}
|
)}
|
||||||
</h2>
|
</h2>
|
||||||
{copyFrom && (
|
{copyFrom && (
|
||||||
<p className="text-xs text-gray-500">Copying from: {copyFrom}</p>
|
<p className="text-xs text-fg-subtle">Copying from: {copyFrom}</p>
|
||||||
)}
|
)}
|
||||||
{isInternalAgent && (
|
{isInternalAgent && (
|
||||||
<p className="text-xs text-slate-400 mt-1">
|
<p className="text-xs text-slate-400 mt-1">
|
||||||
@@ -363,11 +363,11 @@ export function AgentEditor({
|
|||||||
|
|
||||||
{/* Basic Info */}
|
{/* Basic Info */}
|
||||||
<div className="space-y-4">
|
<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 */}
|
{/* Name */}
|
||||||
<div>
|
<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
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
value={name}
|
value={name}
|
||||||
@@ -375,19 +375,19 @@ export function AgentEditor({
|
|||||||
disabled={!isNewAgent || isInternalAgent}
|
disabled={!isNewAgent || isInternalAgent}
|
||||||
placeholder="my-agent"
|
placeholder="my-agent"
|
||||||
className={cn(
|
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',
|
'focus:outline-none focus:border-primary-500',
|
||||||
(!isNewAgent || isInternalAgent) && 'opacity-50 cursor-not-allowed'
|
(!isNewAgent || isInternalAgent) && 'opacity-50 cursor-not-allowed'
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
{!isNewAgent && (
|
{!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>
|
</div>
|
||||||
|
|
||||||
{/* Description */}
|
{/* Description */}
|
||||||
<div>
|
<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
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
value={description}
|
value={description}
|
||||||
@@ -395,7 +395,7 @@ export function AgentEditor({
|
|||||||
disabled={isInternalAgent}
|
disabled={isInternalAgent}
|
||||||
placeholder="A helpful coding assistant"
|
placeholder="A helpful coding assistant"
|
||||||
className={cn(
|
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'
|
isInternalAgent && 'opacity-50 cursor-not-allowed'
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
@@ -404,7 +404,7 @@ export function AgentEditor({
|
|||||||
{/* Mode - 不为内部 Agent 显示 */}
|
{/* Mode - 不为内部 Agent 显示 */}
|
||||||
{!isInternalAgent && (
|
{!isInternalAgent && (
|
||||||
<div>
|
<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">
|
<div className="flex gap-2">
|
||||||
{(['primary', 'subagent', 'all'] as const).map((m) => (
|
{(['primary', 'subagent', 'all'] as const).map((m) => (
|
||||||
<button
|
<button
|
||||||
@@ -415,14 +415,14 @@ export function AgentEditor({
|
|||||||
'px-3 py-2 rounded-lg text-sm transition-colors',
|
'px-3 py-2 rounded-lg text-sm transition-colors',
|
||||||
mode === m
|
mode === m
|
||||||
? 'bg-primary-500 text-white'
|
? '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'}
|
{m === 'primary' ? 'Primary' : m === 'subagent' ? 'Subagent' : 'Both'}
|
||||||
</button>
|
</button>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
<p className="text-xs text-gray-500 mt-1">
|
<p className="text-xs text-fg-subtle mt-1">
|
||||||
{mode === 'primary'
|
{mode === 'primary'
|
||||||
? 'Can be used as the main agent'
|
? 'Can be used as the main agent'
|
||||||
: mode === 'subagent'
|
: mode === 'subagent'
|
||||||
@@ -435,12 +435,12 @@ export function AgentEditor({
|
|||||||
{/* Internal Mode 显示只读标签 */}
|
{/* Internal Mode 显示只读标签 */}
|
||||||
{isInternalAgent && (
|
{isInternalAgent && (
|
||||||
<div>
|
<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">
|
<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} />
|
<Lock size={14} />
|
||||||
Internal (System Agent)
|
Internal (System Agent)
|
||||||
</div>
|
</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
|
System agents are used internally and cannot be called directly
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
@@ -456,9 +456,9 @@ export function AgentEditor({
|
|||||||
onChange={(e) => setPrompt(e.target.value)}
|
onChange={(e) => setPrompt(e.target.value)}
|
||||||
placeholder="You are a helpful assistant..."
|
placeholder="You are a helpful assistant..."
|
||||||
rows={8}
|
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.
|
Custom system prompt for this agent. Leave empty to use defaults.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
@@ -470,11 +470,11 @@ export function AgentEditor({
|
|||||||
<div className="grid grid-cols-2 gap-4">
|
<div className="grid grid-cols-2 gap-4">
|
||||||
{/* Provider */}
|
{/* Provider */}
|
||||||
<div>
|
<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
|
<select
|
||||||
value={modelProvider}
|
value={modelProvider}
|
||||||
onChange={(e) => setModelProvider(e.target.value)}
|
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="">Default</option>
|
||||||
<option value="anthropic">Anthropic</option>
|
<option value="anthropic">Anthropic</option>
|
||||||
@@ -485,19 +485,19 @@ export function AgentEditor({
|
|||||||
|
|
||||||
{/* Model */}
|
{/* Model */}
|
||||||
<div>
|
<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
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
value={modelName}
|
value={modelName}
|
||||||
onChange={(e) => setModelName(e.target.value)}
|
onChange={(e) => setModelName(e.target.value)}
|
||||||
placeholder="claude-sonnet-4-20250514"
|
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>
|
</div>
|
||||||
|
|
||||||
{/* Temperature */}
|
{/* Temperature */}
|
||||||
<div>
|
<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
|
<input
|
||||||
type="number"
|
type="number"
|
||||||
value={temperature ?? ''}
|
value={temperature ?? ''}
|
||||||
@@ -508,13 +508,13 @@ export function AgentEditor({
|
|||||||
min="0"
|
min="0"
|
||||||
max="1"
|
max="1"
|
||||||
step="0.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>
|
</div>
|
||||||
|
|
||||||
{/* Max Tokens */}
|
{/* Max Tokens */}
|
||||||
<div>
|
<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
|
<input
|
||||||
type="number"
|
type="number"
|
||||||
value={maxTokens ?? ''}
|
value={maxTokens ?? ''}
|
||||||
@@ -523,7 +523,7 @@ export function AgentEditor({
|
|||||||
}
|
}
|
||||||
placeholder="8192"
|
placeholder="8192"
|
||||||
min="1"
|
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>
|
||||||
</div>
|
</div>
|
||||||
@@ -535,7 +535,7 @@ export function AgentEditor({
|
|||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
{/* Tool Mode */}
|
{/* Tool Mode */}
|
||||||
<div>
|
<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">
|
<div className="flex gap-2">
|
||||||
{(['all', 'enabled', 'disabled'] as const).map((m) => (
|
{(['all', 'enabled', 'disabled'] as const).map((m) => (
|
||||||
<button
|
<button
|
||||||
@@ -546,7 +546,7 @@ export function AgentEditor({
|
|||||||
'px-3 py-2 rounded-lg text-sm transition-colors',
|
'px-3 py-2 rounded-lg text-sm transition-colors',
|
||||||
toolMode === m
|
toolMode === m
|
||||||
? 'bg-primary-500 text-white'
|
? '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'}
|
{m === 'all' ? 'All Tools' : m === 'enabled' ? 'Only These' : 'Except These'}
|
||||||
@@ -558,7 +558,7 @@ export function AgentEditor({
|
|||||||
{/* Tool List */}
|
{/* Tool List */}
|
||||||
{toolMode !== 'all' && (
|
{toolMode !== 'all' && (
|
||||||
<div>
|
<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'}
|
{toolMode === 'enabled' ? 'Enabled Tools' : 'Disabled Tools'}
|
||||||
</label>
|
</label>
|
||||||
<input
|
<input
|
||||||
@@ -566,9 +566,9 @@ export function AgentEditor({
|
|||||||
value={toolList}
|
value={toolList}
|
||||||
onChange={(e) => setToolList(e.target.value)}
|
onChange={(e) => setToolList(e.target.value)}
|
||||||
placeholder="bash, read_file, write_file"
|
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>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
@@ -579,9 +579,9 @@ export function AgentEditor({
|
|||||||
id="noTask"
|
id="noTask"
|
||||||
checked={noTask}
|
checked={noTask}
|
||||||
onChange={(e) => setNoTask(e.target.checked)}
|
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
|
Disable nested Task calls
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
@@ -593,7 +593,7 @@ export function AgentEditor({
|
|||||||
{!isInternalAgent && (
|
{!isInternalAgent && (
|
||||||
<CollapsibleSection title="Execution Limits" defaultOpen={maxSteps !== undefined}>
|
<CollapsibleSection title="Execution Limits" defaultOpen={maxSteps !== undefined}>
|
||||||
<div>
|
<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
|
<input
|
||||||
type="number"
|
type="number"
|
||||||
value={maxSteps ?? ''}
|
value={maxSteps ?? ''}
|
||||||
@@ -602,9 +602,9 @@ export function AgentEditor({
|
|||||||
}
|
}
|
||||||
placeholder="15"
|
placeholder="15"
|
||||||
min="1"
|
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.
|
Maximum number of tool call steps. Leave empty for default.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
@@ -617,7 +617,7 @@ export function AgentEditor({
|
|||||||
{/* Footer */}
|
{/* Footer */}
|
||||||
<div
|
<div
|
||||||
className={cn(
|
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'
|
responsive ? 'px-4 py-3 safe-area-pb' : 'px-6 py-3'
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -56,7 +56,7 @@ function getModeColor(mode: AgentListItem['mode']) {
|
|||||||
case 'internal':
|
case 'internal':
|
||||||
return 'bg-slate-500/20 text-slate-400';
|
return 'bg-slate-500/20 text-slate-400';
|
||||||
default:
|
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 = () => (
|
const LoadingSkeleton = () => (
|
||||||
<div className="space-y-3 p-4">
|
<div className="space-y-3 p-4">
|
||||||
{[1, 2, 3, 4].map((i) => (
|
{[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" />
|
<Skeleton className="h-4 w-4" />
|
||||||
<div className="flex-1 space-y-2">
|
<div className="flex-1 space-y-2">
|
||||||
<Skeleton className="h-4 w-32" />
|
<Skeleton className="h-4 w-32" />
|
||||||
@@ -227,18 +227,18 @@ export function AgentsPanel({ onClose, responsive = false }: AgentsPanelProps) {
|
|||||||
layout
|
layout
|
||||||
initial={{ opacity: 0, y: 10 }}
|
initial={{ opacity: 0, y: 10 }}
|
||||||
animate={{ opacity: 1, y: 0 }}
|
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 */}
|
{/* Agent Header */}
|
||||||
<div
|
<div
|
||||||
className={cn(
|
className={cn(
|
||||||
'flex items-center gap-3 p-3',
|
'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)}
|
onClick={() => toggleExpanded(agent.name)}
|
||||||
>
|
>
|
||||||
{/* Expand Icon */}
|
{/* 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} />}
|
{isExpanded ? <ChevronDown size={16} /> : <ChevronRight size={16} />}
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
@@ -254,7 +254,7 @@ export function AgentsPanel({ onClose, responsive = false }: AgentsPanelProps) {
|
|||||||
{/* Info */}
|
{/* Info */}
|
||||||
<div className="flex-1 min-w-0">
|
<div className="flex-1 min-w-0">
|
||||||
<div className="flex items-center gap-2">
|
<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))}>
|
<span className={cn('text-xs px-2 py-0.5 rounded-full', getModeColor(agent.mode))}>
|
||||||
{getModeText(agent.mode)}
|
{getModeText(agent.mode)}
|
||||||
</span>
|
</span>
|
||||||
@@ -264,7 +264,7 @@ export function AgentsPanel({ onClose, responsive = false }: AgentsPanelProps) {
|
|||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
<p className="text-xs text-gray-500 truncate">{agent.description}</p>
|
<p className="text-xs text-fg-subtle truncate">{agent.description}</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Actions */}
|
{/* Actions */}
|
||||||
@@ -289,7 +289,7 @@ export function AgentsPanel({ onClose, responsive = false }: AgentsPanelProps) {
|
|||||||
variant="ghost"
|
variant="ghost"
|
||||||
size="sm"
|
size="sm"
|
||||||
onClick={() => toggleExpanded(agent.name)}
|
onClick={() => toggleExpanded(agent.name)}
|
||||||
className="text-gray-400 hover:text-gray-300"
|
className="text-fg-muted hover:text-fg-secondary"
|
||||||
title="View"
|
title="View"
|
||||||
>
|
>
|
||||||
<Eye size={14} />
|
<Eye size={14} />
|
||||||
@@ -312,7 +312,7 @@ export function AgentsPanel({ onClose, responsive = false }: AgentsPanelProps) {
|
|||||||
variant="ghost"
|
variant="ghost"
|
||||||
size="sm"
|
size="sm"
|
||||||
onClick={() => handleCopy(agent.name)}
|
onClick={() => handleCopy(agent.name)}
|
||||||
className="text-gray-400 hover:text-gray-300"
|
className="text-fg-muted hover:text-fg-secondary"
|
||||||
title="Copy"
|
title="Copy"
|
||||||
>
|
>
|
||||||
<Copy size={14} />
|
<Copy size={14} />
|
||||||
@@ -346,21 +346,21 @@ export function AgentsPanel({ onClose, responsive = false }: AgentsPanelProps) {
|
|||||||
transition={{ duration: 0.2 }}
|
transition={{ duration: 0.2 }}
|
||||||
className="overflow-hidden"
|
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 ? (
|
{detail ? (
|
||||||
<>
|
<>
|
||||||
{/* Model Info */}
|
{/* Model Info */}
|
||||||
{detail.model && (
|
{detail.model && (
|
||||||
<div className="flex items-start gap-2">
|
<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">
|
<div className="text-xs">
|
||||||
<span className="text-gray-400">Model:</span>{' '}
|
<span className="text-fg-muted">Model:</span>{' '}
|
||||||
<span className="text-gray-300">
|
<span className="text-fg-secondary">
|
||||||
{detail.model.provider && `${detail.model.provider}/`}
|
{detail.model.provider && `${detail.model.provider}/`}
|
||||||
{detail.model.model || 'default'}
|
{detail.model.model || 'default'}
|
||||||
</span>
|
</span>
|
||||||
{detail.model.temperature !== undefined && (
|
{detail.model.temperature !== undefined && (
|
||||||
<span className="text-gray-500 ml-2">
|
<span className="text-fg-subtle ml-2">
|
||||||
temp: {detail.model.temperature}
|
temp: {detail.model.temperature}
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
@@ -371,9 +371,9 @@ export function AgentsPanel({ onClose, responsive = false }: AgentsPanelProps) {
|
|||||||
{/* Tools Config */}
|
{/* Tools Config */}
|
||||||
{detail.tools && (
|
{detail.tools && (
|
||||||
<div className="flex items-start gap-2">
|
<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">
|
<div className="text-xs">
|
||||||
<span className="text-gray-400">Tools:</span>{' '}
|
<span className="text-fg-muted">Tools:</span>{' '}
|
||||||
{detail.tools.enabled ? (
|
{detail.tools.enabled ? (
|
||||||
<span className="text-green-400">
|
<span className="text-green-400">
|
||||||
Only: {detail.tools.enabled.join(', ')}
|
Only: {detail.tools.enabled.join(', ')}
|
||||||
@@ -383,7 +383,7 @@ export function AgentsPanel({ onClose, responsive = false }: AgentsPanelProps) {
|
|||||||
Disabled: {detail.tools.disabled.join(', ')}
|
Disabled: {detail.tools.disabled.join(', ')}
|
||||||
</span>
|
</span>
|
||||||
) : (
|
) : (
|
||||||
<span className="text-gray-300">All enabled</span>
|
<span className="text-fg-secondary">All enabled</span>
|
||||||
)}
|
)}
|
||||||
{detail.tools.noTask && (
|
{detail.tools.noTask && (
|
||||||
<span className="text-red-400 ml-2">(No nested tasks)</span>
|
<span className="text-red-400 ml-2">(No nested tasks)</span>
|
||||||
@@ -395,16 +395,16 @@ export function AgentsPanel({ onClose, responsive = false }: AgentsPanelProps) {
|
|||||||
{/* Max Steps */}
|
{/* Max Steps */}
|
||||||
{detail.maxSteps && (
|
{detail.maxSteps && (
|
||||||
<div className="text-xs">
|
<div className="text-xs">
|
||||||
<span className="text-gray-400">Max Steps:</span>{' '}
|
<span className="text-fg-muted">Max Steps:</span>{' '}
|
||||||
<span className="text-gray-300">{detail.maxSteps}</span>
|
<span className="text-fg-secondary">{detail.maxSteps}</span>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Prompt Preview */}
|
{/* Prompt Preview */}
|
||||||
{detail.prompt && (
|
{detail.prompt && (
|
||||||
<div className="text-xs">
|
<div className="text-xs">
|
||||||
<span className="text-gray-400">System Prompt:</span>
|
<span className="text-fg-muted">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">
|
<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.slice(0, 500)}
|
||||||
{detail.prompt.length > 500 && '...'}
|
{detail.prompt.length > 500 && '...'}
|
||||||
</pre>
|
</pre>
|
||||||
@@ -474,7 +474,7 @@ export function AgentsPanel({ onClose, responsive = false }: AgentsPanelProps) {
|
|||||||
transition={smoothTransition}
|
transition={smoothTransition}
|
||||||
onClick={(e) => e.stopPropagation()}
|
onClick={(e) => e.stopPropagation()}
|
||||||
className={cn(
|
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
|
responsive
|
||||||
? 'w-full md:w-full md:max-w-2xl md:mx-4 rounded-t-2xl md:rounded-lg'
|
? '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'
|
: 'rounded-lg w-full max-w-2xl mx-4'
|
||||||
@@ -483,19 +483,19 @@ export function AgentsPanel({ onClose, responsive = false }: AgentsPanelProps) {
|
|||||||
{/* Header */}
|
{/* Header */}
|
||||||
<div
|
<div
|
||||||
className={cn(
|
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 ? 'px-4 md:px-6 py-4' : 'px-6 py-4'
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
{responsive && (
|
{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')}>
|
<div className={cn(responsive && 'mt-2 md:mt-0')}>
|
||||||
<h2 className="text-lg font-semibold flex items-center gap-2">
|
<h2 className="text-lg font-semibold flex items-center gap-2">
|
||||||
<Bot size={20} className="text-primary-400" />
|
<Bot size={20} className="text-primary-400" />
|
||||||
Agent Presets
|
Agent Presets
|
||||||
</h2>
|
</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)
|
{agents.length} agents ({internalAgents.length} system, {presetAgents.length} preset, {customAgents.length} custom)
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
@@ -544,7 +544,7 @@ export function AgentsPanel({ onClose, responsive = false }: AgentsPanelProps) {
|
|||||||
{loading ? (
|
{loading ? (
|
||||||
<LoadingSkeleton />
|
<LoadingSkeleton />
|
||||||
) : agents.length === 0 ? (
|
) : 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" />
|
<Bot size={48} className="mb-4 opacity-50" />
|
||||||
<p className="text-center">No agents available</p>
|
<p className="text-center">No agents available</p>
|
||||||
<Button
|
<Button
|
||||||
@@ -580,7 +580,7 @@ export function AgentsPanel({ onClose, responsive = false }: AgentsPanelProps) {
|
|||||||
{/* Preset Agents */}
|
{/* Preset Agents */}
|
||||||
{presetAgents.length > 0 && (
|
{presetAgents.length > 0 && (
|
||||||
<div>
|
<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} />
|
<Sparkles size={12} />
|
||||||
Preset Agents
|
Preset Agents
|
||||||
</h3>
|
</h3>
|
||||||
@@ -594,7 +594,7 @@ export function AgentsPanel({ onClose, responsive = false }: AgentsPanelProps) {
|
|||||||
|
|
||||||
{/* Custom Agents */}
|
{/* Custom Agents */}
|
||||||
<div>
|
<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} />
|
<Bot size={12} />
|
||||||
Custom Agents
|
Custom Agents
|
||||||
</h3>
|
</h3>
|
||||||
@@ -605,7 +605,7 @@ export function AgentsPanel({ onClose, responsive = false }: AgentsPanelProps) {
|
|||||||
))}
|
))}
|
||||||
</div>
|
</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>
|
<p>No custom agents yet</p>
|
||||||
<Button
|
<Button
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
@@ -626,12 +626,12 @@ export function AgentsPanel({ onClose, responsive = false }: AgentsPanelProps) {
|
|||||||
{/* Footer Info */}
|
{/* Footer Info */}
|
||||||
<div
|
<div
|
||||||
className={cn(
|
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'
|
responsive ? 'px-4 py-3 safe-area-pb' : 'px-6 py-3'
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
Config stored in{' '}
|
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>
|
</div>
|
||||||
</motion.div>
|
</motion.div>
|
||||||
</motion.div>
|
</motion.div>
|
||||||
|
|||||||
@@ -142,7 +142,7 @@ export function ChatInput({
|
|||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className={clsx(
|
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'
|
responsive ? 'p-3 md:p-4 safe-area-pb' : 'p-4'
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
@@ -174,10 +174,10 @@ export function ChatInput({
|
|||||||
disabled={disabled}
|
disabled={disabled}
|
||||||
rows={1}
|
rows={1}
|
||||||
className={clsx(
|
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 ? 'px-3 py-2.5 md:px-4 md:py-3' : 'px-4 py-3',
|
||||||
responsive ? 'text-base md:text-sm' : 'text-sm', // 移动端使用 16px 防止缩放
|
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',
|
'focus:outline-none focus:ring-2 focus:ring-primary-500 focus:border-transparent',
|
||||||
'disabled:opacity-50 disabled:cursor-not-allowed'
|
'disabled:opacity-50 disabled:cursor-not-allowed'
|
||||||
)}
|
)}
|
||||||
@@ -202,7 +202,7 @@ export function ChatInput({
|
|||||||
</div>
|
</div>
|
||||||
{/* 响应式模式下桌面端显示提示文字 */}
|
{/* 响应式模式下桌面端显示提示文字 */}
|
||||||
{responsive && (
|
{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
|
Press Enter to send, Shift+Enter for new line, / for commands
|
||||||
</p>
|
</p>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -42,7 +42,7 @@ export const ChatMessage = forwardRef<HTMLDivElement, ChatMessageProps>(
|
|||||||
// 优先使用 parts 数组(保持原始顺序)
|
// 优先使用 parts 数组(保持原始顺序)
|
||||||
if (message.parts && message.parts.length > 0) {
|
if (message.parts && message.parts.length > 0) {
|
||||||
return (
|
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) => {
|
{message.parts.map((part) => {
|
||||||
switch (part.type) {
|
switch (part.type) {
|
||||||
case 'text':
|
case 'text':
|
||||||
@@ -58,7 +58,7 @@ export const ChatMessage = forwardRef<HTMLDivElement, ChatMessageProps>(
|
|||||||
return <ToolPartItem key={part.id} part={part} />;
|
return <ToolPartItem key={part.id} part={part} />;
|
||||||
case 'reasoning':
|
case 'reasoning':
|
||||||
return (
|
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}
|
{part.text}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
@@ -76,7 +76,7 @@ export const ChatMessage = forwardRef<HTMLDivElement, ChatMessageProps>(
|
|||||||
{!isUser && message.toolCalls && message.toolCalls.length > 0 && (
|
{!isUser && message.toolCalls && message.toolCalls.length > 0 && (
|
||||||
<ToolCallsDisplay toolCalls={message.toolCalls} />
|
<ToolCallsDisplay toolCalls={message.toolCalls} />
|
||||||
)}
|
)}
|
||||||
<div className="message-content text-gray-200">
|
<div className="message-content text-fg-secondary">
|
||||||
{isUser ? (
|
{isUser ? (
|
||||||
<div className="whitespace-pre-wrap break-words">{message.content ?? ''}</div>
|
<div className="whitespace-pre-wrap break-words">{message.content ?? ''}</div>
|
||||||
) : (
|
) : (
|
||||||
@@ -97,12 +97,12 @@ export const ChatMessage = forwardRef<HTMLDivElement, ChatMessageProps>(
|
|||||||
transition={smoothTransition}
|
transition={smoothTransition}
|
||||||
className={cn(
|
className={cn(
|
||||||
'group flex gap-4 p-4 rounded-lg',
|
'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
|
<div
|
||||||
className={cn(
|
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'
|
isUser ? 'bg-primary-600' : 'bg-green-600'
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
@@ -110,12 +110,12 @@ export const ChatMessage = forwardRef<HTMLDivElement, ChatMessageProps>(
|
|||||||
</div>
|
</div>
|
||||||
<div className="flex-1 min-w-0 overflow-hidden">
|
<div className="flex-1 min-w-0 overflow-hidden">
|
||||||
<div className="flex items-center justify-between mb-1">
|
<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'}
|
{isUser ? 'You' : 'AI Assistant'}
|
||||||
</span>
|
</span>
|
||||||
<button
|
<button
|
||||||
onClick={handleCopy}
|
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"
|
title="Copy message"
|
||||||
>
|
>
|
||||||
{copied ? <Check size={14} className="text-green-500" /> : <Copy size={14} />}
|
{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 }}
|
initial={{ opacity: 0, y: 10 }}
|
||||||
animate={{ opacity: 1, y: 0 }}
|
animate={{ opacity: 1, y: 0 }}
|
||||||
transition={smoothTransition}
|
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} />
|
<Bot size={18} />
|
||||||
</div>
|
</div>
|
||||||
<div className="flex-1 min-w-0 overflow-hidden">
|
<div className="flex-1 min-w-0 overflow-hidden">
|
||||||
<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="message-content text-gray-200">
|
<div className="message-content text-fg-secondary">
|
||||||
<Markdown content={content} />
|
<Markdown content={content} />
|
||||||
<motion.span
|
<motion.span
|
||||||
animate={{ opacity: [1, 0] }}
|
animate={{ opacity: [1, 0] }}
|
||||||
@@ -164,13 +164,13 @@ export function TypingIndicator() {
|
|||||||
initial={{ opacity: 0, y: 10 }}
|
initial={{ opacity: 0, y: 10 }}
|
||||||
animate={{ opacity: 1, y: 0 }}
|
animate={{ opacity: 1, y: 0 }}
|
||||||
transition={smoothTransition}
|
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} />
|
<Bot size={18} />
|
||||||
</div>
|
</div>
|
||||||
<div className="flex-1">
|
<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">
|
<div className="flex items-center gap-1 h-6">
|
||||||
{[0, 1, 2].map((i) => (
|
{[0, 1, 2].map((i) => (
|
||||||
<motion.span
|
<motion.span
|
||||||
@@ -181,7 +181,7 @@ export function TypingIndicator() {
|
|||||||
repeat: Infinity,
|
repeat: Infinity,
|
||||||
delay: i * 0.15,
|
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>
|
</div>
|
||||||
@@ -207,26 +207,26 @@ function ToolPartItem({ part }: ToolPartItemProps) {
|
|||||||
part.error !== undefined;
|
part.error !== undefined;
|
||||||
|
|
||||||
return (
|
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
|
<button
|
||||||
onClick={() => hasDetails && setExpanded(!expanded)}
|
onClick={() => hasDetails && setExpanded(!expanded)}
|
||||||
className={cn(
|
className={cn(
|
||||||
'w-full flex items-center gap-2 px-3 py-2 text-sm',
|
'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'
|
!hasDetails && 'cursor-default'
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<Wrench size={14} className="text-gray-400 flex-shrink-0" />
|
<Wrench size={14} className="text-fg-muted flex-shrink-0" />
|
||||||
<span className="font-mono text-gray-200 flex-1 text-left truncate">
|
<span className="font-mono text-fg-secondary flex-1 text-left truncate">
|
||||||
{part.toolName}
|
{part.toolName}
|
||||||
</span>
|
</span>
|
||||||
{getStatusIcon(part.status)}
|
{getStatusIcon(part.status)}
|
||||||
{part.duration && (
|
{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 && (
|
{hasDetails && (
|
||||||
<span className="text-gray-500">
|
<span className="text-fg-subtle">
|
||||||
{expanded ? <ChevronDown size={14} /> : <ChevronRight size={14} />}
|
{expanded ? <ChevronDown size={14} /> : <ChevronRight size={14} />}
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
@@ -240,14 +240,14 @@ function ToolPartItem({ part }: ToolPartItemProps) {
|
|||||||
animate={{ height: 'auto', opacity: 1 }}
|
animate={{ height: 'auto', opacity: 1 }}
|
||||||
exit={{ height: 0, opacity: 0 }}
|
exit={{ height: 0, opacity: 0 }}
|
||||||
transition={{ duration: 0.2 }}
|
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">
|
<div className="px-3 py-2 space-y-2 text-xs">
|
||||||
{/* 参数 */}
|
{/* 参数 */}
|
||||||
{Object.keys(part.arguments).length > 0 && (
|
{Object.keys(part.arguments).length > 0 && (
|
||||||
<div>
|
<div>
|
||||||
<div className="text-gray-500 mb-1">Arguments:</div>
|
<div className="text-fg-subtle mb-1">Arguments:</div>
|
||||||
<pre className="bg-gray-900 rounded p-2 overflow-x-auto text-gray-300 max-h-48 overflow-y-auto">
|
<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)}
|
{JSON.stringify(part.arguments, null, 2)}
|
||||||
</pre>
|
</pre>
|
||||||
</div>
|
</div>
|
||||||
@@ -256,8 +256,8 @@ function ToolPartItem({ part }: ToolPartItemProps) {
|
|||||||
{/* 结果 */}
|
{/* 结果 */}
|
||||||
{part.result !== undefined && (
|
{part.result !== undefined && (
|
||||||
<div>
|
<div>
|
||||||
<div className="text-gray-500 mb-1">Result:</div>
|
<div className="text-fg-subtle mb-1">Result:</div>
|
||||||
<pre className="bg-gray-900 rounded p-2 overflow-x-auto text-green-300 max-h-48 overflow-y-auto">
|
<pre className="bg-surface-base rounded p-2 overflow-x-auto text-green-400 max-h-48 overflow-y-auto">
|
||||||
{typeof part.result === 'string'
|
{typeof part.result === 'string'
|
||||||
? part.result
|
? part.result
|
||||||
: JSON.stringify(part.result, null, 2)}
|
: JSON.stringify(part.result, null, 2)}
|
||||||
@@ -269,7 +269,7 @@ function ToolPartItem({ part }: ToolPartItemProps) {
|
|||||||
{part.error && (
|
{part.error && (
|
||||||
<div>
|
<div>
|
||||||
<div className="text-red-400 mb-1">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}
|
{part.error}
|
||||||
</pre>
|
</pre>
|
||||||
</div>
|
</div>
|
||||||
@@ -339,26 +339,26 @@ function ToolCallItem({ toolCall }: ToolCallItemProps) {
|
|||||||
toolCall.error !== undefined;
|
toolCall.error !== undefined;
|
||||||
|
|
||||||
return (
|
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
|
<button
|
||||||
onClick={() => hasDetails && setExpanded(!expanded)}
|
onClick={() => hasDetails && setExpanded(!expanded)}
|
||||||
className={cn(
|
className={cn(
|
||||||
'w-full flex items-center gap-2 px-3 py-2 text-sm',
|
'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'
|
!hasDetails && 'cursor-default'
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<Wrench size={14} className="text-gray-400 flex-shrink-0" />
|
<Wrench size={14} className="text-fg-muted flex-shrink-0" />
|
||||||
<span className="font-mono text-gray-200 flex-1 text-left truncate">
|
<span className="font-mono text-fg-secondary flex-1 text-left truncate">
|
||||||
{toolCall.name}
|
{toolCall.name}
|
||||||
</span>
|
</span>
|
||||||
{getStatusIcon(toolCall.status)}
|
{getStatusIcon(toolCall.status)}
|
||||||
{toolCall.duration && (
|
{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 && (
|
{hasDetails && (
|
||||||
<span className="text-gray-500">
|
<span className="text-fg-subtle">
|
||||||
{expanded ? <ChevronDown size={14} /> : <ChevronRight size={14} />}
|
{expanded ? <ChevronDown size={14} /> : <ChevronRight size={14} />}
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
@@ -372,14 +372,14 @@ function ToolCallItem({ toolCall }: ToolCallItemProps) {
|
|||||||
animate={{ height: 'auto', opacity: 1 }}
|
animate={{ height: 'auto', opacity: 1 }}
|
||||||
exit={{ height: 0, opacity: 0 }}
|
exit={{ height: 0, opacity: 0 }}
|
||||||
transition={{ duration: 0.2 }}
|
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">
|
<div className="px-3 py-2 space-y-2 text-xs">
|
||||||
{/* 参数 */}
|
{/* 参数 */}
|
||||||
{Object.keys(toolCall.arguments).length > 0 && (
|
{Object.keys(toolCall.arguments).length > 0 && (
|
||||||
<div>
|
<div>
|
||||||
<div className="text-gray-500 mb-1">Arguments:</div>
|
<div className="text-fg-subtle mb-1">Arguments:</div>
|
||||||
<pre className="bg-gray-900 rounded p-2 overflow-x-auto text-gray-300 max-h-48 overflow-y-auto">
|
<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)}
|
{JSON.stringify(toolCall.arguments, null, 2)}
|
||||||
</pre>
|
</pre>
|
||||||
</div>
|
</div>
|
||||||
@@ -388,8 +388,8 @@ function ToolCallItem({ toolCall }: ToolCallItemProps) {
|
|||||||
{/* 结果 */}
|
{/* 结果 */}
|
||||||
{toolCall.result !== undefined && (
|
{toolCall.result !== undefined && (
|
||||||
<div>
|
<div>
|
||||||
<div className="text-gray-500 mb-1">Result:</div>
|
<div className="text-fg-subtle mb-1">Result:</div>
|
||||||
<pre className="bg-gray-900 rounded p-2 overflow-x-auto text-green-300 max-h-48 overflow-y-auto">
|
<pre className="bg-surface-base rounded p-2 overflow-x-auto text-green-400 max-h-48 overflow-y-auto">
|
||||||
{typeof toolCall.result === 'string'
|
{typeof toolCall.result === 'string'
|
||||||
? toolCall.result
|
? toolCall.result
|
||||||
: JSON.stringify(toolCall.result, null, 2)}
|
: JSON.stringify(toolCall.result, null, 2)}
|
||||||
@@ -401,7 +401,7 @@ function ToolCallItem({ toolCall }: ToolCallItemProps) {
|
|||||||
{toolCall.error && (
|
{toolCall.error && (
|
||||||
<div>
|
<div>
|
||||||
<div className="text-red-400 mb-1">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}
|
{toolCall.error}
|
||||||
</pre>
|
</pre>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -71,7 +71,7 @@ function getChangeColor(type: FileChangeType) {
|
|||||||
case 'renamed':
|
case 'renamed':
|
||||||
return 'text-blue-400 bg-blue-400/10';
|
return 'text-blue-400 bg-blue-400/10';
|
||||||
default:
|
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" />
|
<Skeleton className="h-6 w-64" />
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
{[1, 2, 3, 4].map((i) => (
|
{[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-4 rounded" />
|
||||||
<Skeleton className="h-4 w-48" />
|
<Skeleton className="h-4 w-48" />
|
||||||
<Skeleton className="h-3 w-16 ml-auto" />
|
<Skeleton className="h-3 w-16 ml-auto" />
|
||||||
@@ -227,7 +227,7 @@ export function CheckpointDiffViewer({
|
|||||||
} else if (line.startsWith('@@')) {
|
} else if (line.startsWith('@@')) {
|
||||||
className += ' bg-blue-500/10 text-blue-400';
|
className += ' bg-blue-500/10 text-blue-400';
|
||||||
} else {
|
} else {
|
||||||
className += ' text-gray-400';
|
className += ' text-fg-muted';
|
||||||
}
|
}
|
||||||
return (
|
return (
|
||||||
<div key={index} className={className}>
|
<div key={index} className={className}>
|
||||||
@@ -261,7 +261,7 @@ export function CheckpointDiffViewer({
|
|||||||
transition={smoothTransition}
|
transition={smoothTransition}
|
||||||
onClick={(e) => e.stopPropagation()}
|
onClick={(e) => e.stopPropagation()}
|
||||||
className={cn(
|
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
|
responsive
|
||||||
? 'w-full md:w-full md:max-w-3xl md:mx-4 rounded-t-2xl md:rounded-lg'
|
? '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'
|
: 'rounded-lg w-full max-w-3xl mx-4'
|
||||||
@@ -270,22 +270,22 @@ export function CheckpointDiffViewer({
|
|||||||
{/* Header */}
|
{/* Header */}
|
||||||
<div
|
<div
|
||||||
className={cn(
|
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 ? 'px-4 md:px-6 py-4' : 'px-6 py-4'
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
{responsive && (
|
{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')}>
|
<div className={cn(responsive && 'mt-2 md:mt-0')}>
|
||||||
<h2 className="text-lg font-semibold flex items-center gap-2">
|
<h2 className="text-lg font-semibold flex items-center gap-2">
|
||||||
<Eye size={20} className="text-primary-400" />
|
<Eye size={20} className="text-primary-400" />
|
||||||
Checkpoint Diff
|
Checkpoint Diff
|
||||||
</h2>
|
</h2>
|
||||||
<p className="text-xs text-gray-500">
|
<p className="text-xs text-fg-subtle">
|
||||||
{checkpoint ? (
|
{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...'
|
'Loading...'
|
||||||
@@ -316,12 +316,12 @@ export function CheckpointDiffViewer({
|
|||||||
|
|
||||||
{/* Summary */}
|
{/* Summary */}
|
||||||
{diff && (
|
{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 justify-between text-sm">
|
||||||
<div className="flex items-center gap-4">
|
<div className="flex items-center gap-4">
|
||||||
<span className="text-green-400">+{diff.totalInsertions}</span>
|
<span className="text-green-400">+{diff.totalInsertions}</span>
|
||||||
<span className="text-red-400">-{diff.totalDeletions}</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>
|
</div>
|
||||||
<button
|
<button
|
||||||
onClick={toggleSelectAll}
|
onClick={toggleSelectAll}
|
||||||
@@ -338,10 +338,10 @@ export function CheckpointDiffViewer({
|
|||||||
{loading ? (
|
{loading ? (
|
||||||
<LoadingSkeleton />
|
<LoadingSkeleton />
|
||||||
) : !diff || diff.files.length === 0 ? (
|
) : !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" />
|
<Eye size={48} className="mb-4 opacity-50" />
|
||||||
<p className="text-center">No changes detected</p>
|
<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
|
The workspace matches this checkpoint
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
@@ -349,7 +349,7 @@ export function CheckpointDiffViewer({
|
|||||||
<motion.div
|
<motion.div
|
||||||
initial={{ opacity: 0 }}
|
initial={{ opacity: 0 }}
|
||||||
animate={{ opacity: 1 }}
|
animate={{ opacity: 1 }}
|
||||||
className="divide-y divide-gray-700/50"
|
className="divide-y divide-line/50"
|
||||||
>
|
>
|
||||||
{diff.files.map((file) => {
|
{diff.files.map((file) => {
|
||||||
const isSelected = selectedFiles.has(file.path);
|
const isSelected = selectedFiles.has(file.path);
|
||||||
@@ -361,7 +361,7 @@ export function CheckpointDiffViewer({
|
|||||||
<div
|
<div
|
||||||
className={cn(
|
className={cn(
|
||||||
'flex items-center gap-3 px-4 py-2',
|
'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 */}
|
{/* Checkbox */}
|
||||||
@@ -371,7 +371,7 @@ export function CheckpointDiffViewer({
|
|||||||
'w-4 h-4 rounded border transition-colors flex items-center justify-center',
|
'w-4 h-4 rounded border transition-colors flex items-center justify-center',
|
||||||
isSelected
|
isSelected
|
||||||
? 'bg-primary-500 border-primary-500'
|
? '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" />}
|
{isSelected && <Check size={12} className="text-white" />}
|
||||||
@@ -380,7 +380,7 @@ export function CheckpointDiffViewer({
|
|||||||
{/* Expand Icon */}
|
{/* Expand Icon */}
|
||||||
<button
|
<button
|
||||||
onClick={() => handleViewFileDiff(file.path)}
|
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} />}
|
{isExpanded ? <ChevronDown size={14} /> : <ChevronRight size={14} />}
|
||||||
</button>
|
</button>
|
||||||
@@ -397,7 +397,7 @@ export function CheckpointDiffViewer({
|
|||||||
|
|
||||||
{/* File Path */}
|
{/* File Path */}
|
||||||
<span
|
<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)}
|
onClick={() => handleViewFileDiff(file.path)}
|
||||||
>
|
>
|
||||||
{file.path}
|
{file.path}
|
||||||
@@ -405,7 +405,7 @@ export function CheckpointDiffViewer({
|
|||||||
|
|
||||||
{/* Stats */}
|
{/* Stats */}
|
||||||
{(file.insertions !== undefined || file.deletions !== undefined) && (
|
{(file.insertions !== undefined || file.deletions !== undefined) && (
|
||||||
<span className="text-xs text-gray-500">
|
<span className="text-xs text-fg-subtle">
|
||||||
{file.insertions !== undefined && (
|
{file.insertions !== undefined && (
|
||||||
<span className="text-green-400 mr-2">+{file.insertions}</span>
|
<span className="text-green-400 mr-2">+{file.insertions}</span>
|
||||||
)}
|
)}
|
||||||
@@ -424,18 +424,18 @@ export function CheckpointDiffViewer({
|
|||||||
animate={{ height: 'auto', opacity: 1 }}
|
animate={{ height: 'auto', opacity: 1 }}
|
||||||
exit={{ height: 0, opacity: 0 }}
|
exit={{ height: 0, opacity: 0 }}
|
||||||
transition={{ duration: 0.2 }}
|
transition={{ duration: 0.2 }}
|
||||||
className="overflow-hidden bg-gray-900/30"
|
className="overflow-hidden bg-surface-base/30"
|
||||||
>
|
>
|
||||||
{loadingFileDiff ? (
|
{loadingFileDiff ? (
|
||||||
<div className="p-4">
|
<div className="p-4">
|
||||||
<Skeleton className="h-32 w-full" />
|
<Skeleton className="h-32 w-full" />
|
||||||
</div>
|
</div>
|
||||||
) : fileDiff?.patch ? (
|
) : 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)}
|
{renderPatch(fileDiff.patch)}
|
||||||
</div>
|
</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
|
No diff content available
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
@@ -453,11 +453,11 @@ export function CheckpointDiffViewer({
|
|||||||
{diff && diff.files.length > 0 && (onRestoreSelected || onRestoreAll) && (
|
{diff && diff.files.length > 0 && (onRestoreSelected || onRestoreAll) && (
|
||||||
<div
|
<div
|
||||||
className={cn(
|
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'
|
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
|
{selectedFiles.size} of {diff.files.length} files selected
|
||||||
</span>
|
</span>
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
|
|||||||
@@ -71,10 +71,10 @@ function getTriggerInfo(trigger: CheckpointTrigger) {
|
|||||||
case 'session_end':
|
case 'session_end':
|
||||||
return { icon: '⏹️', label: 'Session End', color: 'text-cyan-400' };
|
return { icon: '⏹️', label: 'Session End', color: 'text-cyan-400' };
|
||||||
case 'pre_rollback':
|
case 'pre_rollback':
|
||||||
return { icon: '🔙', label: 'Pre-Rollback', color: 'text-gray-400' };
|
return { icon: '🔙', label: 'Pre-Rollback', color: 'text-fg-muted' };
|
||||||
case 'auto':
|
case 'auto':
|
||||||
default:
|
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 = () => (
|
const LoadingSkeleton = () => (
|
||||||
<div className="space-y-3 p-4">
|
<div className="space-y-3 p-4">
|
||||||
{[1, 2, 3, 4].map((i) => (
|
{[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" />
|
<Skeleton className="h-4 w-4 rounded" />
|
||||||
<div className="flex-1 space-y-2">
|
<div className="flex-1 space-y-2">
|
||||||
<Skeleton className="h-4 w-48" />
|
<Skeleton className="h-4 w-48" />
|
||||||
@@ -329,7 +329,7 @@ export function CheckpointPanel({
|
|||||||
transition={smoothTransition}
|
transition={smoothTransition}
|
||||||
onClick={(e) => e.stopPropagation()}
|
onClick={(e) => e.stopPropagation()}
|
||||||
className={cn(
|
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
|
responsive
|
||||||
? 'w-full md:w-full md:max-w-2xl md:mx-4 rounded-t-2xl md:rounded-lg'
|
? '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'
|
: 'rounded-lg w-full max-w-2xl mx-4'
|
||||||
@@ -338,19 +338,19 @@ export function CheckpointPanel({
|
|||||||
{/* Header */}
|
{/* Header */}
|
||||||
<div
|
<div
|
||||||
className={cn(
|
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 ? 'px-4 md:px-6 py-4' : 'px-6 py-4'
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
{responsive && (
|
{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')}>
|
<div className={cn(responsive && 'mt-2 md:mt-0')}>
|
||||||
<h2 className="text-lg font-semibold flex items-center gap-2">
|
<h2 className="text-lg font-semibold flex items-center gap-2">
|
||||||
<History size={20} className="text-primary-400" />
|
<History size={20} className="text-primary-400" />
|
||||||
Checkpoints
|
Checkpoints
|
||||||
</h2>
|
</h2>
|
||||||
<p className="text-xs text-gray-500">
|
<p className="text-xs text-fg-subtle">
|
||||||
{stats ? `${stats.count} checkpoints` : 'Loading...'}
|
{stats ? `${stats.count} checkpoints` : 'Loading...'}
|
||||||
{stats?.oldestTimestamp && (
|
{stats?.oldestTimestamp && (
|
||||||
<> · Oldest: {formatTime(stats.oldestTimestamp)}</>
|
<> · Oldest: {formatTime(stats.oldestTimestamp)}</>
|
||||||
@@ -431,10 +431,10 @@ export function CheckpointPanel({
|
|||||||
{loading ? (
|
{loading ? (
|
||||||
<LoadingSkeleton />
|
<LoadingSkeleton />
|
||||||
) : checkpoints.length === 0 ? (
|
) : 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" />
|
<History size={48} className="mb-4 opacity-50" />
|
||||||
<p className="text-center">No checkpoints yet</p>
|
<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
|
Checkpoints are created automatically when files are modified
|
||||||
</p>
|
</p>
|
||||||
<Button
|
<Button
|
||||||
@@ -461,12 +461,12 @@ export function CheckpointPanel({
|
|||||||
<div key={group.label} className="space-y-1">
|
<div key={group.label} className="space-y-1">
|
||||||
{/* Group Header */}
|
{/* Group Header */}
|
||||||
<button
|
<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)}
|
onClick={() => toggleGroup(group.label)}
|
||||||
>
|
>
|
||||||
{isExpanded ? <ChevronDown size={14} /> : <ChevronRight size={14} />}
|
{isExpanded ? <ChevronDown size={14} /> : <ChevronRight size={14} />}
|
||||||
<span className="font-medium">{group.label}</span>
|
<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>
|
</button>
|
||||||
|
|
||||||
{/* Group Items */}
|
{/* Group Items */}
|
||||||
@@ -488,7 +488,7 @@ export function CheckpointPanel({
|
|||||||
key={cp.id}
|
key={cp.id}
|
||||||
initial={{ opacity: 0, x: -10 }}
|
initial={{ opacity: 0, x: -10 }}
|
||||||
animate={{ opacity: 1, x: 0 }}
|
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">
|
<div className="flex items-start gap-3">
|
||||||
{/* Trigger Icon */}
|
{/* Trigger Icon */}
|
||||||
@@ -502,16 +502,16 @@ export function CheckpointPanel({
|
|||||||
<span className={cn('text-sm font-medium', triggerInfo.color)}>
|
<span className={cn('text-sm font-medium', triggerInfo.color)}>
|
||||||
{triggerInfo.label}
|
{triggerInfo.label}
|
||||||
</span>
|
</span>
|
||||||
<span className="text-xs text-gray-500">
|
<span className="text-xs text-fg-subtle">
|
||||||
{formatTime(cp.timestamp)}
|
{formatTime(cp.timestamp)}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
{cp.description && (
|
{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}
|
{cp.description}
|
||||||
</p>
|
</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">
|
<span className="flex items-center gap-1">
|
||||||
<FileText size={10} />
|
<FileText size={10} />
|
||||||
{cp.filesChanged} files
|
{cp.filesChanged} files
|
||||||
@@ -580,11 +580,11 @@ export function CheckpointPanel({
|
|||||||
{/* Footer */}
|
{/* Footer */}
|
||||||
<div
|
<div
|
||||||
className={cn(
|
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'
|
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)
|
Auto-cleanup enabled (7 days / 100 max)
|
||||||
</span>
|
</span>
|
||||||
<Button
|
<Button
|
||||||
@@ -592,10 +592,10 @@ export function CheckpointPanel({
|
|||||||
size="sm"
|
size="sm"
|
||||||
onClick={handleCleanup}
|
onClick={handleCleanup}
|
||||||
disabled={cleaningUp}
|
disabled={cleaningUp}
|
||||||
className="text-gray-400 hover:text-gray-300"
|
className="text-fg-muted hover:text-fg-secondary"
|
||||||
>
|
>
|
||||||
{cleaningUp ? (
|
{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" />
|
<Trash2 size={12} className="mr-1" />
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -61,11 +61,11 @@ export function CodeBlock({ code, language = 'text', className }: CodeBlockProps
|
|||||||
return (
|
return (
|
||||||
<div className={cn('relative group rounded-lg overflow-hidden', className)}>
|
<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">
|
<div className="flex items-center justify-between px-4 py-2 bg-code border-b border-line">
|
||||||
<span className="text-xs text-gray-400 font-mono">{language}</span>
|
<span className="text-xs text-fg-muted font-mono">{language}</span>
|
||||||
<button
|
<button
|
||||||
onClick={handleCopy}
|
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"
|
title="Copy code"
|
||||||
>
|
>
|
||||||
{copied ? (
|
{copied ? (
|
||||||
@@ -85,12 +85,12 @@ export function CodeBlock({ code, language = 'text', className }: CodeBlockProps
|
|||||||
{/* 代码内容 */}
|
{/* 代码内容 */}
|
||||||
<div className="overflow-x-auto">
|
<div className="overflow-x-auto">
|
||||||
{isLoading ? (
|
{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>
|
<code>{code}</code>
|
||||||
</pre>
|
</pre>
|
||||||
) : (
|
) : (
|
||||||
<div
|
<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 }}
|
dangerouslySetInnerHTML={{ __html: highlightedHtml }}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
@@ -109,7 +109,7 @@ export function InlineCode({ children, className }: InlineCodeProps) {
|
|||||||
return (
|
return (
|
||||||
<code
|
<code
|
||||||
className={cn(
|
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
|
className
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -175,7 +175,7 @@ export function CommandEditor({
|
|||||||
transition={smoothTransition}
|
transition={smoothTransition}
|
||||||
onClick={(e) => e.stopPropagation()}
|
onClick={(e) => e.stopPropagation()}
|
||||||
className={cn(
|
className={cn(
|
||||||
'bg-gray-800 max-h-[90vh] overflow-auto',
|
'bg-surface-subtle max-h-[90vh] overflow-auto',
|
||||||
responsive
|
responsive
|
||||||
? 'w-full md:w-full md:max-w-2xl md:mx-4 rounded-t-2xl md:rounded-lg'
|
? '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'
|
: 'rounded-lg w-full max-w-2xl mx-4'
|
||||||
@@ -184,12 +184,12 @@ export function CommandEditor({
|
|||||||
{/* Header */}
|
{/* Header */}
|
||||||
<div
|
<div
|
||||||
className={cn(
|
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 ? 'px-4 md:px-6 py-4' : 'px-6 py-4'
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
{responsive && (
|
{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')}>
|
<h2 className={cn('text-lg font-semibold', responsive && 'mt-2 md:mt-0')}>
|
||||||
{isEditMode ? `Edit Command: /${commandName}` : 'Create Command'}
|
{isEditMode ? `Edit Command: /${commandName}` : 'Create Command'}
|
||||||
@@ -233,7 +233,7 @@ export function CommandEditor({
|
|||||||
|
|
||||||
{/* Name */}
|
{/* Name */}
|
||||||
<div className="space-y-2">
|
<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>
|
Name <span className="text-red-400">*</span>
|
||||||
</label>
|
</label>
|
||||||
<Input
|
<Input
|
||||||
@@ -243,14 +243,14 @@ export function CommandEditor({
|
|||||||
disabled={isEditMode || isBuiltin}
|
disabled={isEditMode || isBuiltin}
|
||||||
className="font-mono"
|
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)
|
Command name. Use / for nested commands (e.g., deploy/staging)
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Description */}
|
{/* Description */}
|
||||||
<div className="space-y-2">
|
<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
|
<Input
|
||||||
value={description}
|
value={description}
|
||||||
onChange={(e) => setDescription(e.target.value)}
|
onChange={(e) => setDescription(e.target.value)}
|
||||||
@@ -261,7 +261,7 @@ export function CommandEditor({
|
|||||||
|
|
||||||
{/* Template */}
|
{/* Template */}
|
||||||
<div className="space-y-2">
|
<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>
|
Template <span className="text-red-400">*</span>
|
||||||
</label>
|
</label>
|
||||||
<textarea
|
<textarea
|
||||||
@@ -271,14 +271,14 @@ export function CommandEditor({
|
|||||||
disabled={isBuiltin}
|
disabled={isBuiltin}
|
||||||
rows={6}
|
rows={6}
|
||||||
className={cn(
|
className={cn(
|
||||||
'w-full px-3 py-2 bg-gray-900 border border-gray-700 rounded-lg',
|
'w-full px-3 py-2 bg-surface-base border border-line rounded-lg',
|
||||||
'text-gray-100 placeholder:text-gray-500',
|
'text-fg placeholder:text-fg-subtle',
|
||||||
'focus:outline-none focus:ring-2 focus:ring-primary-500 focus:border-transparent',
|
'focus:outline-none focus:ring-2 focus:ring-primary-500 focus:border-transparent',
|
||||||
'font-mono text-sm resize-y',
|
'font-mono text-sm resize-y',
|
||||||
'disabled:opacity-50 disabled:cursor-not-allowed'
|
'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
|
Template syntax: $ARGUMENTS (all args), $1 $2 (positional), @file (include
|
||||||
file), !`cmd` (shell output)
|
file), !`cmd` (shell output)
|
||||||
</p>
|
</p>
|
||||||
@@ -287,41 +287,41 @@ export function CommandEditor({
|
|||||||
{/* Agent & Model */}
|
{/* Agent & Model */}
|
||||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||||
<div className="space-y-2">
|
<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
|
<Input
|
||||||
value={agent}
|
value={agent}
|
||||||
onChange={(e) => setAgent(e.target.value)}
|
onChange={(e) => setAgent(e.target.value)}
|
||||||
placeholder="code-review"
|
placeholder="code-review"
|
||||||
disabled={isBuiltin}
|
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>
|
||||||
|
|
||||||
<div className="space-y-2">
|
<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
|
<Input
|
||||||
value={model}
|
value={model}
|
||||||
onChange={(e) => setModel(e.target.value)}
|
onChange={(e) => setModel(e.target.value)}
|
||||||
placeholder="claude-sonnet-4-20250514"
|
placeholder="claude-sonnet-4-20250514"
|
||||||
disabled={isBuiltin}
|
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>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Subtask & Scope */}
|
{/* Subtask & Scope */}
|
||||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
<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>
|
<div>
|
||||||
<label className="block text-sm font-medium text-gray-300">Subtask</label>
|
<label className="block text-sm font-medium text-fg-secondary">Subtask</label>
|
||||||
<p className="text-xs text-gray-500">Run as background subtask</p>
|
<p className="text-xs text-fg-subtle">Run as background subtask</p>
|
||||||
</div>
|
</div>
|
||||||
<Switch checked={subtask} onCheckedChange={setSubtask} disabled={isBuiltin} />
|
<Switch checked={subtask} onCheckedChange={setSubtask} disabled={isBuiltin} />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{!isEditMode && (
|
{!isEditMode && (
|
||||||
<div className="space-y-2">
|
<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
|
<Select
|
||||||
value={scope}
|
value={scope}
|
||||||
onValueChange={(v) => setScope(v as 'user' | 'project')}
|
onValueChange={(v) => setScope(v as 'user' | 'project')}
|
||||||
@@ -334,24 +334,24 @@ export function CommandEditor({
|
|||||||
<SelectItem value="project">Project (local)</SelectItem>
|
<SelectItem value="project">Project (local)</SelectItem>
|
||||||
</SelectContent>
|
</SelectContent>
|
||||||
</Select>
|
</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>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Source Info (edit mode) */}
|
{/* Source Info (edit mode) */}
|
||||||
{isEditMode && originalData && (
|
{isEditMode && originalData && (
|
||||||
<div className="pt-4 border-t border-gray-700">
|
<div className="pt-4 border-t border-line">
|
||||||
<h3 className="text-sm font-medium text-gray-400 mb-2">Command Info</h3>
|
<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 className="grid grid-cols-2 gap-2 text-sm">
|
||||||
<div>
|
<div>
|
||||||
<span className="text-gray-500">Source:</span>
|
<span className="text-fg-subtle">Source:</span>
|
||||||
<span className="ml-2 text-gray-300">{originalData.source}</span>
|
<span className="ml-2 text-fg-secondary">{originalData.source}</span>
|
||||||
</div>
|
</div>
|
||||||
{originalData.sourcePath && (
|
{originalData.sourcePath && (
|
||||||
<div className="col-span-2">
|
<div className="col-span-2">
|
||||||
<span className="text-gray-500">Path:</span>
|
<span className="text-fg-subtle">Path:</span>
|
||||||
<span className="ml-2 text-gray-300 font-mono text-xs break-all">
|
<span className="ml-2 text-fg-secondary font-mono text-xs break-all">
|
||||||
{originalData.sourcePath}
|
{originalData.sourcePath}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
@@ -365,7 +365,7 @@ export function CommandEditor({
|
|||||||
{/* Footer */}
|
{/* Footer */}
|
||||||
<div
|
<div
|
||||||
className={cn(
|
className={cn(
|
||||||
'sticky bottom-0 border-t border-gray-700 bg-gray-800',
|
'sticky bottom-0 border-t border-line bg-surface-subtle',
|
||||||
responsive
|
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 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'
|
: 'flex items-center justify-end gap-3 px-6 py-4'
|
||||||
|
|||||||
@@ -44,7 +44,7 @@ function getSourceIcon(source: string) {
|
|||||||
case 'project':
|
case 'project':
|
||||||
return <FolderGit2 size={14} className="text-green-400" />;
|
return <FolderGit2 size={14} className="text-green-400" />;
|
||||||
default:
|
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',
|
user: 'bg-purple-500/20 text-purple-400',
|
||||||
project: 'bg-green-500/20 text-green-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({
|
export function CommandMenu({
|
||||||
@@ -132,28 +132,28 @@ export function CommandMenu({
|
|||||||
transition={{ duration: 0.15 }}
|
transition={{ duration: 0.15 }}
|
||||||
className="absolute bottom-full left-0 right-0 mb-2 mx-4 md:mx-0 z-50"
|
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 */}
|
{/* Header */}
|
||||||
<div className="px-3 py-2 border-b border-gray-700 bg-gray-800/80 sticky top-0">
|
<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-gray-400">
|
<div className="flex items-center gap-2 text-xs text-fg-muted">
|
||||||
<Terminal size={12} />
|
<Terminal size={12} />
|
||||||
<span>Commands</span>
|
<span>Commands</span>
|
||||||
{commands.length > 0 && (
|
{commands.length > 0 && (
|
||||||
<span className="text-gray-500">({commands.length})</span>
|
<span className="text-fg-subtle">({commands.length})</span>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Loading */}
|
{/* Loading */}
|
||||||
{isLoading && commands.length === 0 && (
|
{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...
|
Loading commands...
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Empty state */}
|
{/* Empty state */}
|
||||||
{!isLoading && commands.length === 0 && (
|
{!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
|
No commands found
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
@@ -171,7 +171,7 @@ export function CommandMenu({
|
|||||||
'w-full px-3 py-2 flex items-start gap-3 text-left transition-colors',
|
'w-full px-3 py-2 flex items-start gap-3 text-left transition-colors',
|
||||||
index === selectedIndex
|
index === selectedIndex
|
||||||
? 'bg-primary-600/20'
|
? 'bg-primary-600/20'
|
||||||
: 'hover:bg-gray-700/50'
|
: 'hover:bg-surface-muted/50'
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
{/* Icon */}
|
{/* Icon */}
|
||||||
@@ -185,7 +185,7 @@ export function CommandMenu({
|
|||||||
'font-mono text-sm',
|
'font-mono text-sm',
|
||||||
index === selectedIndex
|
index === selectedIndex
|
||||||
? 'text-primary-300'
|
? 'text-primary-300'
|
||||||
: 'text-gray-200'
|
: 'text-fg-secondary'
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
/{command.name}
|
/{command.name}
|
||||||
@@ -200,7 +200,7 @@ export function CommandMenu({
|
|||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
{command.description && (
|
{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}
|
{command.description}
|
||||||
</p>
|
</p>
|
||||||
)}
|
)}
|
||||||
@@ -208,8 +208,8 @@ export function CommandMenu({
|
|||||||
|
|
||||||
{/* Keyboard hint */}
|
{/* Keyboard hint */}
|
||||||
{index === selectedIndex && (
|
{index === selectedIndex && (
|
||||||
<div className="flex items-center gap-1 text-[10px] text-gray-500">
|
<div className="flex items-center gap-1 text-[10px] text-fg-subtle">
|
||||||
<kbd className="px-1 py-0.5 bg-gray-700 rounded">
|
<kbd className="px-1 py-0.5 bg-surface-muted rounded">
|
||||||
Enter
|
Enter
|
||||||
</kbd>
|
</kbd>
|
||||||
</div>
|
</div>
|
||||||
@@ -220,19 +220,19 @@ export function CommandMenu({
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Footer hint */}
|
{/* Footer hint */}
|
||||||
<div className="px-3 py-1.5 border-t border-gray-700 bg-gray-800/80 sticky bottom-0">
|
<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-gray-500">
|
<div className="flex items-center gap-3 text-[10px] text-fg-subtle">
|
||||||
<span>
|
<span>
|
||||||
<kbd className="px-1 py-0.5 bg-gray-700 rounded mr-1">↑</kbd>
|
<kbd className="px-1 py-0.5 bg-surface-muted 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">↓</kbd>
|
||||||
{' '}navigate
|
{' '}navigate
|
||||||
</span>
|
</span>
|
||||||
<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
|
{' '}select
|
||||||
</span>
|
</span>
|
||||||
<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
|
{' '}close
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -44,13 +44,13 @@ type CommandItem = CommandListResponse['commands'][number];
|
|||||||
function getSourceIcon(source: string) {
|
function getSourceIcon(source: string) {
|
||||||
switch (source) {
|
switch (source) {
|
||||||
case 'builtin':
|
case 'builtin':
|
||||||
return <Cog size={14} className="text-gray-400" />;
|
return <Cog size={14} className="text-fg-muted" />;
|
||||||
case 'user':
|
case 'user':
|
||||||
return <User size={14} className="text-blue-400" />;
|
return <User size={14} className="text-blue-400" />;
|
||||||
case 'project':
|
case 'project':
|
||||||
return <FolderOpen size={14} className="text-green-400" />;
|
return <FolderOpen size={14} className="text-green-400" />;
|
||||||
default:
|
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) {
|
function getSourceBadgeClass(source: string) {
|
||||||
switch (source) {
|
switch (source) {
|
||||||
case 'builtin':
|
case 'builtin':
|
||||||
return 'bg-gray-700 text-gray-300';
|
return 'bg-surface-muted text-fg-secondary';
|
||||||
case 'user':
|
case 'user':
|
||||||
return 'bg-blue-500/20 text-blue-400';
|
return 'bg-blue-500/20 text-blue-400';
|
||||||
case 'project':
|
case 'project':
|
||||||
return 'bg-green-500/20 text-green-400';
|
return 'bg-green-500/20 text-green-400';
|
||||||
default:
|
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 = () => (
|
const LoadingSkeleton = () => (
|
||||||
<div className="space-y-3 p-4">
|
<div className="space-y-3 p-4">
|
||||||
{[1, 2, 3, 4, 5].map((i) => (
|
{[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" />
|
<Skeleton className="h-5 w-5 rounded" />
|
||||||
<div className="flex-1 space-y-2">
|
<div className="flex-1 space-y-2">
|
||||||
<Skeleton className="h-4 w-32" />
|
<Skeleton className="h-4 w-32" />
|
||||||
@@ -223,7 +223,7 @@ export function CommandPanel({ onClose, responsive = false }: CommandPanelProps)
|
|||||||
transition={smoothTransition}
|
transition={smoothTransition}
|
||||||
onClick={(e) => e.stopPropagation()}
|
onClick={(e) => e.stopPropagation()}
|
||||||
className={cn(
|
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
|
responsive
|
||||||
? 'w-full md:w-full md:max-w-2xl md:mx-4 rounded-t-2xl md:rounded-lg'
|
? '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'
|
: 'rounded-lg w-full max-w-2xl mx-4'
|
||||||
@@ -232,17 +232,17 @@ export function CommandPanel({ onClose, responsive = false }: CommandPanelProps)
|
|||||||
{/* Header */}
|
{/* Header */}
|
||||||
<div
|
<div
|
||||||
className={cn(
|
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 ? 'px-4 md:px-6 py-4' : 'px-6 py-4'
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
{responsive && (
|
{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')}>
|
<div className={cn(responsive && 'mt-2 md:mt-0')}>
|
||||||
<h2 className="text-lg font-semibold">Commands</h2>
|
<h2 className="text-lg font-semibold">Commands</h2>
|
||||||
{stats && (
|
{stats && (
|
||||||
<p className="text-xs text-gray-500">
|
<p className="text-xs text-fg-subtle">
|
||||||
{stats.total} commands ({stats.bySource.builtin || 0} builtin,{' '}
|
{stats.total} commands ({stats.bySource.builtin || 0} builtin,{' '}
|
||||||
{stats.bySource.user || 0} user, {stats.bySource.project || 0} project)
|
{stats.bySource.user || 0} user, {stats.bySource.project || 0} project)
|
||||||
</p>
|
</p>
|
||||||
@@ -261,14 +261,14 @@ export function CommandPanel({ onClose, responsive = false }: CommandPanelProps)
|
|||||||
{/* Toolbar */}
|
{/* Toolbar */}
|
||||||
<div
|
<div
|
||||||
className={cn(
|
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'
|
responsive ? 'px-4 md:px-6 py-3' : 'px-6 py-3'
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<div className="flex-1 relative">
|
<div className="flex-1 relative">
|
||||||
<Search
|
<Search
|
||||||
size={16}
|
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
|
<Input
|
||||||
value={searchQuery}
|
value={searchQuery}
|
||||||
@@ -297,7 +297,7 @@ export function CommandPanel({ onClose, responsive = false }: CommandPanelProps)
|
|||||||
{loading ? (
|
{loading ? (
|
||||||
<LoadingSkeleton />
|
<LoadingSkeleton />
|
||||||
) : filteredCommands.length === 0 ? (
|
) : 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" />
|
<Terminal size={48} className="mb-4 opacity-50" />
|
||||||
<p>
|
<p>
|
||||||
{searchQuery
|
{searchQuery
|
||||||
@@ -325,8 +325,8 @@ export function CommandPanel({ onClose, responsive = false }: CommandPanelProps)
|
|||||||
animate={{ opacity: 1, y: 0 }}
|
animate={{ opacity: 1, y: 0 }}
|
||||||
exit={{ opacity: 0, y: -10 }}
|
exit={{ opacity: 0, y: -10 }}
|
||||||
className={cn(
|
className={cn(
|
||||||
'flex items-center gap-3 p-3 bg-gray-900/50 rounded-lg',
|
'flex items-center gap-3 p-3 bg-surface-base/50 rounded-lg',
|
||||||
'hover:bg-gray-900/80 transition-colors group'
|
'hover:bg-surface-base/80 transition-colors group'
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
{/* Icon */}
|
{/* Icon */}
|
||||||
@@ -335,7 +335,7 @@ export function CommandPanel({ onClose, responsive = false }: CommandPanelProps)
|
|||||||
{/* Info */}
|
{/* Info */}
|
||||||
<div className="flex-1 min-w-0">
|
<div className="flex-1 min-w-0">
|
||||||
<div className="flex items-center gap-2">
|
<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
|
<span
|
||||||
className={cn(
|
className={cn(
|
||||||
'px-1.5 py-0.5 text-xs rounded',
|
'px-1.5 py-0.5 text-xs rounded',
|
||||||
@@ -346,7 +346,7 @@ export function CommandPanel({ onClose, responsive = false }: CommandPanelProps)
|
|||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
{command.description && (
|
{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}
|
{command.description}
|
||||||
</p>
|
</p>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -113,7 +113,7 @@ export function ConfigPanel({ onClose, responsive = false }: ConfigPanelProps) {
|
|||||||
transition={smoothTransition}
|
transition={smoothTransition}
|
||||||
onClick={(e) => e.stopPropagation()}
|
onClick={(e) => e.stopPropagation()}
|
||||||
className={cn(
|
className={cn(
|
||||||
'bg-gray-800 max-h-[90vh] overflow-auto',
|
'bg-surface-subtle max-h-[90vh] overflow-auto',
|
||||||
responsive
|
responsive
|
||||||
? 'w-full md:w-full md:max-w-lg md:mx-4 rounded-t-2xl md:rounded-lg'
|
? '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'
|
: 'rounded-lg w-full max-w-lg mx-4'
|
||||||
@@ -122,12 +122,12 @@ export function ConfigPanel({ onClose, responsive = false }: ConfigPanelProps) {
|
|||||||
{/* Header */}
|
{/* Header */}
|
||||||
<div
|
<div
|
||||||
className={cn(
|
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 ? 'px-4 md:px-6 py-4' : 'px-6 py-4'
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
{responsive && (
|
{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')}>
|
<h2 className={cn('text-lg font-semibold', responsive && 'mt-2 md:mt-0')}>
|
||||||
Settings
|
Settings
|
||||||
@@ -156,14 +156,14 @@ export function ConfigPanel({ onClose, responsive = false }: ConfigPanelProps) {
|
|||||||
>
|
>
|
||||||
{/* Working Directory */}
|
{/* Working Directory */}
|
||||||
<div className="space-y-2">
|
<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
|
<Input
|
||||||
value={formData.workdir}
|
value={formData.workdir}
|
||||||
onChange={(e) => setFormData({ ...formData, workdir: e.target.value })}
|
onChange={(e) => setFormData({ ...formData, workdir: e.target.value })}
|
||||||
className="font-mono"
|
className="font-mono"
|
||||||
placeholder="/path/to/project"
|
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>
|
</div>
|
||||||
</motion.div>
|
</motion.div>
|
||||||
)}
|
)}
|
||||||
@@ -171,10 +171,10 @@ export function ConfigPanel({ onClose, responsive = false }: ConfigPanelProps) {
|
|||||||
{/* Footer */}
|
{/* Footer */}
|
||||||
<div
|
<div
|
||||||
className={cn(
|
className={cn(
|
||||||
'sticky bottom-0 border-t border-gray-700 bg-gray-800',
|
'sticky bottom-0 border-t border-line bg-surface-subtle',
|
||||||
responsive
|
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 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
|
<Button
|
||||||
|
|||||||
@@ -44,7 +44,7 @@ function getUsageColor(percent: number): string {
|
|||||||
function getTextColor(percent: number): string {
|
function getTextColor(percent: number): string {
|
||||||
if (percent >= 90) return 'text-red-400';
|
if (percent >= 90) return 'text-red-400';
|
||||||
if (percent >= 80) return 'text-amber-400';
|
if (percent >= 80) return 'text-amber-400';
|
||||||
return 'text-gray-400';
|
return 'text-fg-muted';
|
||||||
}
|
}
|
||||||
|
|
||||||
export function ContextUsage({
|
export function ContextUsage({
|
||||||
@@ -128,8 +128,8 @@ export function ContextUsage({
|
|||||||
// 无数据时显示占位
|
// 无数据时显示占位
|
||||||
if (!usage) {
|
if (!usage) {
|
||||||
return (
|
return (
|
||||||
<div className={cn('flex items-center gap-2 text-sm text-gray-500', className)}>
|
<div className={cn('flex items-center gap-2 text-sm text-fg-subtle', className)}>
|
||||||
<div className="h-2 w-24 bg-gray-700 rounded-full animate-pulse" />
|
<div className="h-2 w-24 bg-surface-muted rounded-full animate-pulse" />
|
||||||
<span>--</span>
|
<span>--</span>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
@@ -144,7 +144,7 @@ export function ContextUsage({
|
|||||||
return (
|
return (
|
||||||
<div className={cn('flex items-center gap-2', className)}>
|
<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
|
<div
|
||||||
className={cn('absolute h-full rounded-full transition-all', barColor)}
|
className={cn('absolute h-full rounded-full transition-all', barColor)}
|
||||||
style={{ width: `${Math.min(usagePercent, 100)}%` }}
|
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 justify-between">
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<Zap size={14} className="text-gray-400" />
|
<Zap size={14} className="text-fg-muted" />
|
||||||
<span className="text-xs font-medium text-gray-300">Context Usage</span>
|
<span className="text-xs font-medium text-fg-secondary">Context Usage</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center gap-1">
|
<div className="flex items-center gap-1">
|
||||||
{/* 刷新按钮 */}
|
{/* 刷新按钮 */}
|
||||||
@@ -186,7 +186,7 @@ export function ContextUsage({
|
|||||||
</div>
|
</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
|
<div
|
||||||
className={cn('absolute h-full rounded-full transition-all duration-300', barColor)}
|
className={cn('absolute h-full rounded-full transition-all duration-300', barColor)}
|
||||||
style={{ width: `${Math.min(usagePercent, 100)}%` }}
|
style={{ width: `${Math.min(usagePercent, 100)}%` }}
|
||||||
|
|||||||
@@ -29,7 +29,7 @@ const FileIcon = ({ type, extension }: { type: 'file' | 'directory'; extension?:
|
|||||||
js: 'text-yellow-300',
|
js: 'text-yellow-300',
|
||||||
jsx: 'text-yellow-300',
|
jsx: 'text-yellow-300',
|
||||||
json: 'text-yellow-500',
|
json: 'text-yellow-500',
|
||||||
md: 'text-gray-400',
|
md: 'text-fg-muted',
|
||||||
css: 'text-pink-400',
|
css: 'text-pink-400',
|
||||||
html: 'text-orange-400',
|
html: 'text-orange-400',
|
||||||
py: 'text-green-400',
|
py: 'text-green-400',
|
||||||
@@ -37,7 +37,7 @@ const FileIcon = ({ type, extension }: { type: 'file' | 'directory'; extension?:
|
|||||||
rs: 'text-orange-500',
|
rs: 'text-orange-500',
|
||||||
};
|
};
|
||||||
|
|
||||||
const color = colors[extension || ''] || 'text-gray-400';
|
const color = colors[extension || ''] || 'text-fg-muted';
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<svg className={`w-4 h-4 ${color}`} fill="currentColor" viewBox="0 0 20 20">
|
<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 (
|
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
|
<button
|
||||||
onClick={handleGoUp}
|
onClick={handleGoUp}
|
||||||
disabled={parentPath === null}
|
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"
|
title="Go up"
|
||||||
>
|
>
|
||||||
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
<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
|
<button
|
||||||
onClick={handleRefresh}
|
onClick={handleRefresh}
|
||||||
className="p-1.5 rounded hover:bg-gray-700"
|
className="p-1.5 rounded hover:bg-surface-muted"
|
||||||
title="Refresh"
|
title="Refresh"
|
||||||
>
|
>
|
||||||
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
<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>
|
</svg>
|
||||||
</button>
|
</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}
|
{currentPath}
|
||||||
</div>
|
</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
|
<input
|
||||||
type="checkbox"
|
type="checkbox"
|
||||||
checked={showHidden}
|
checked={showHidden}
|
||||||
@@ -176,7 +176,7 @@ export function FileBrowser({ onFileSelect, className = '' }: FileBrowserProps)
|
|||||||
{/* 文件列表 */}
|
{/* 文件列表 */}
|
||||||
<div className="flex-1 overflow-auto">
|
<div className="flex-1 overflow-auto">
|
||||||
{loading ? (
|
{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...
|
Loading...
|
||||||
</div>
|
</div>
|
||||||
) : error ? (
|
) : error ? (
|
||||||
@@ -184,23 +184,23 @@ export function FileBrowser({ onFileSelect, className = '' }: FileBrowserProps)
|
|||||||
{error}
|
{error}
|
||||||
</div>
|
</div>
|
||||||
) : files.length === 0 ? (
|
) : 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
|
Empty directory
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<div className="divide-y divide-gray-800">
|
<div className="divide-y divide-surface-subtle">
|
||||||
{files.map((file) => (
|
{files.map((file) => (
|
||||||
<div
|
<div
|
||||||
key={file.path}
|
key={file.path}
|
||||||
onClick={() => handleItemClick(file)}
|
onClick={() => handleItemClick(file)}
|
||||||
className={`flex items-center gap-2 px-3 py-2 cursor-pointer hover:bg-gray-800 ${
|
className={`flex items-center gap-2 px-3 py-2 cursor-pointer hover:bg-surface-subtle ${
|
||||||
selectedFile === file.path ? 'bg-gray-800 border-l-2 border-blue-500' : ''
|
selectedFile === file.path ? 'bg-surface-subtle border-l-2 border-blue-500' : ''
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
<FileIcon type={file.type} extension={file.extension} />
|
<FileIcon type={file.type} extension={file.extension} />
|
||||||
<span className="flex-1 truncate text-sm">{file.name}</span>
|
<span className="flex-1 truncate text-sm">{file.name}</span>
|
||||||
{file.type === 'file' && (
|
{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>
|
</div>
|
||||||
))}
|
))}
|
||||||
@@ -210,22 +210,22 @@ export function FileBrowser({ onFileSelect, className = '' }: FileBrowserProps)
|
|||||||
|
|
||||||
{/* 文件预览 */}
|
{/* 文件预览 */}
|
||||||
{selectedFile && fileContent && (
|
{selectedFile && fileContent && (
|
||||||
<div className="border-t border-gray-700 max-h-48 overflow-auto">
|
<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-gray-800 border-b border-gray-700">
|
<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-gray-400 truncate">{selectedFile}</span>
|
<span className="text-xs text-fg-muted truncate">{selectedFile}</span>
|
||||||
<button
|
<button
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
setSelectedFile(null);
|
setSelectedFile(null);
|
||||||
setFileContent(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">
|
<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" />
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M6 18L18 6M6 6l12 12" />
|
||||||
</svg>
|
</svg>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</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.slice(0, 5000)}
|
||||||
{fileContent.length > 5000 && '\n... (truncated)'}
|
{fileContent.length > 5000 && '\n... (truncated)'}
|
||||||
</pre>
|
</pre>
|
||||||
|
|||||||
@@ -276,7 +276,7 @@ export function HookEditor({
|
|||||||
transition={smoothTransition}
|
transition={smoothTransition}
|
||||||
onClick={(e) => e.stopPropagation()}
|
onClick={(e) => e.stopPropagation()}
|
||||||
className={cn(
|
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
|
responsive
|
||||||
? 'w-full md:w-full md:max-w-lg md:mx-4 rounded-t-2xl md:rounded-lg'
|
? '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'
|
: 'rounded-lg w-full max-w-lg mx-4'
|
||||||
@@ -285,12 +285,12 @@ export function HookEditor({
|
|||||||
{/* Header */}
|
{/* Header */}
|
||||||
<div
|
<div
|
||||||
className={cn(
|
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 ? 'px-4 py-3' : 'px-6 py-4'
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
{responsive && (
|
{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')}>
|
<h3 className={cn('text-lg font-semibold', responsive && 'mt-2 md:mt-0')}>
|
||||||
{title}
|
{title}
|
||||||
@@ -305,7 +305,7 @@ export function HookEditor({
|
|||||||
{/* Pattern (for file hooks) */}
|
{/* Pattern (for file hooks) */}
|
||||||
{isFileHook && (
|
{isFileHook && (
|
||||||
<div>
|
<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)
|
Pattern (glob)
|
||||||
</label>
|
</label>
|
||||||
<input
|
<input
|
||||||
@@ -314,15 +314,15 @@ export function HookEditor({
|
|||||||
onChange={(e) => setPattern(e.target.value)}
|
onChange={(e) => setPattern(e.target.value)}
|
||||||
placeholder="e.g., *.ts, src/**/*.tsx"
|
placeholder="e.g., *.ts, src/**/*.tsx"
|
||||||
className={cn(
|
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',
|
'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 && (
|
{errors.pattern && (
|
||||||
<p className="text-xs text-red-400 mt-1">{errors.pattern}</p>
|
<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)
|
Use glob patterns to match files (e.g., *.ts, **/*.json)
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
@@ -331,7 +331,7 @@ export function HookEditor({
|
|||||||
{/* Commands */}
|
{/* Commands */}
|
||||||
<div>
|
<div>
|
||||||
<div className="flex items-center justify-between mb-2">
|
<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
|
Commands
|
||||||
</label>
|
</label>
|
||||||
{isFileHook && (
|
{isFileHook && (
|
||||||
@@ -351,12 +351,12 @@ export function HookEditor({
|
|||||||
{commandStates.map((state, cmdIndex) => (
|
{commandStates.map((state, cmdIndex) => (
|
||||||
<div
|
<div
|
||||||
key={cmdIndex}
|
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 */}
|
{/* Command args */}
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<div className="flex items-center justify-between">
|
<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 : ''}
|
Command {commandStates.length > 1 ? cmdIndex + 1 : ''}
|
||||||
</span>
|
</span>
|
||||||
<div className="flex items-center gap-1">
|
<div className="flex items-center gap-1">
|
||||||
@@ -396,7 +396,7 @@ export function HookEditor({
|
|||||||
onChange={(e) => updateCommandArg(cmdIndex, argIndex, e.target.value)}
|
onChange={(e) => updateCommandArg(cmdIndex, argIndex, e.target.value)}
|
||||||
placeholder={argIndex === 0 ? 'command' : 'arg'}
|
placeholder={argIndex === 0 ? 'command' : 'arg'}
|
||||||
className={cn(
|
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',
|
'focus:outline-none focus:ring-1 focus:ring-primary-500',
|
||||||
argIndex === 0 ? 'min-w-[100px]' : 'min-w-[80px]'
|
argIndex === 0 ? 'min-w-[100px]' : 'min-w-[80px]'
|
||||||
)}
|
)}
|
||||||
@@ -404,7 +404,7 @@ export function HookEditor({
|
|||||||
{state.command.length > 1 && (
|
{state.command.length > 1 && (
|
||||||
<button
|
<button
|
||||||
onClick={() => removeCommandArg(cmdIndex, argIndex)}
|
onClick={() => removeCommandArg(cmdIndex, argIndex)}
|
||||||
className="text-gray-500 hover:text-red-400"
|
className="text-fg-subtle hover:text-red-400"
|
||||||
>
|
>
|
||||||
<X size={14} />
|
<X size={14} />
|
||||||
</button>
|
</button>
|
||||||
@@ -429,7 +429,7 @@ export function HookEditor({
|
|||||||
{/* Advanced options toggle */}
|
{/* Advanced options toggle */}
|
||||||
<button
|
<button
|
||||||
onClick={() => updateCommand(cmdIndex, { showAdvanced: !state.showAdvanced })}
|
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} />}
|
{state.showAdvanced ? <ChevronDown size={14} /> : <ChevronRight size={14} />}
|
||||||
Advanced options
|
Advanced options
|
||||||
@@ -437,10 +437,10 @@ export function HookEditor({
|
|||||||
|
|
||||||
{/* Advanced options */}
|
{/* Advanced options */}
|
||||||
{state.showAdvanced && (
|
{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 */}
|
{/* Timeout */}
|
||||||
<div>
|
<div>
|
||||||
<label className="block text-xs text-gray-400 mb-1">
|
<label className="block text-xs text-fg-muted mb-1">
|
||||||
Timeout (ms)
|
Timeout (ms)
|
||||||
</label>
|
</label>
|
||||||
<input
|
<input
|
||||||
@@ -450,13 +450,13 @@ export function HookEditor({
|
|||||||
timeout: e.target.value ? parseInt(e.target.value) : undefined
|
timeout: e.target.value ? parseInt(e.target.value) : undefined
|
||||||
})}
|
})}
|
||||||
placeholder="30000"
|
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>
|
</div>
|
||||||
|
|
||||||
{/* Working Directory */}
|
{/* Working Directory */}
|
||||||
<div>
|
<div>
|
||||||
<label className="block text-xs text-gray-400 mb-1">
|
<label className="block text-xs text-fg-muted mb-1">
|
||||||
Working Directory
|
Working Directory
|
||||||
</label>
|
</label>
|
||||||
<input
|
<input
|
||||||
@@ -464,14 +464,14 @@ export function HookEditor({
|
|||||||
value={state.cwd || ''}
|
value={state.cwd || ''}
|
||||||
onChange={(e) => updateCommand(cmdIndex, { cwd: e.target.value || undefined })}
|
onChange={(e) => updateCommand(cmdIndex, { cwd: e.target.value || undefined })}
|
||||||
placeholder="(project root)"
|
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>
|
</div>
|
||||||
|
|
||||||
{/* Environment Variables */}
|
{/* Environment Variables */}
|
||||||
<div>
|
<div>
|
||||||
<div className="flex items-center justify-between mb-1">
|
<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
|
Environment Variables
|
||||||
</label>
|
</label>
|
||||||
<Button
|
<Button
|
||||||
@@ -492,19 +492,19 @@ export function HookEditor({
|
|||||||
value={key}
|
value={key}
|
||||||
onChange={(e) => updateEnvVar(cmdIndex, key, e.target.value, value)}
|
onChange={(e) => updateEnvVar(cmdIndex, key, e.target.value, value)}
|
||||||
placeholder="KEY"
|
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
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
value={value}
|
value={value}
|
||||||
onChange={(e) => updateEnvVar(cmdIndex, key, key, e.target.value)}
|
onChange={(e) => updateEnvVar(cmdIndex, key, key, e.target.value)}
|
||||||
placeholder="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
|
<button
|
||||||
onClick={() => removeEnvVar(cmdIndex, key)}
|
onClick={() => removeEnvVar(cmdIndex, key)}
|
||||||
className="text-gray-500 hover:text-red-400"
|
className="text-fg-subtle hover:text-red-400"
|
||||||
>
|
>
|
||||||
<X size={12} />
|
<X size={12} />
|
||||||
</button>
|
</button>
|
||||||
@@ -523,7 +523,7 @@ export function HookEditor({
|
|||||||
{/* Footer */}
|
{/* Footer */}
|
||||||
<div
|
<div
|
||||||
className={cn(
|
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'
|
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(
|
toast.success(
|
||||||
<div>
|
<div>
|
||||||
<div className="font-medium">Command succeeded</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)
|
Exit code: {result.data.exitCode} ({result.data.duration}ms)
|
||||||
</div>
|
</div>
|
||||||
{result.data.stdout && (
|
{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)}
|
{result.data.stdout.slice(0, 500)}
|
||||||
</pre>
|
</pre>
|
||||||
)}
|
)}
|
||||||
@@ -159,11 +159,11 @@ export function HooksPanel({ onClose, responsive = false }: HooksPanelProps) {
|
|||||||
toast.error(
|
toast.error(
|
||||||
<div>
|
<div>
|
||||||
<div className="font-medium">Command failed</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)
|
Exit code: {result.data.exitCode} ({result.data.duration}ms)
|
||||||
</div>
|
</div>
|
||||||
{result.data.stderr && (
|
{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)}
|
{result.data.stderr.slice(0, 500)}
|
||||||
</pre>
|
</pre>
|
||||||
)}
|
)}
|
||||||
@@ -274,7 +274,7 @@ export function HooksPanel({ onClose, responsive = false }: HooksPanelProps) {
|
|||||||
const LoadingSkeleton = () => (
|
const LoadingSkeleton = () => (
|
||||||
<div className="space-y-3 p-4">
|
<div className="space-y-3 p-4">
|
||||||
{[1, 2, 3, 4].map((i) => (
|
{[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">
|
<div className="flex items-center gap-3">
|
||||||
<Skeleton className="h-4 w-4" />
|
<Skeleton className="h-4 w-4" />
|
||||||
<Skeleton className="h-4 w-32" />
|
<Skeleton className="h-4 w-32" />
|
||||||
@@ -292,7 +292,7 @@ export function HooksPanel({ onClose, responsive = false }: HooksPanelProps) {
|
|||||||
|
|
||||||
if (patterns.length === 0) {
|
if (patterns.length === 0) {
|
||||||
return (
|
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
|
No hooks configured
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
@@ -305,7 +305,7 @@ export function HooksPanel({ onClose, responsive = false }: HooksPanelProps) {
|
|||||||
const cmdId = `${type}-${pattern}`;
|
const cmdId = `${type}-${pattern}`;
|
||||||
|
|
||||||
return (
|
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">
|
<div className="flex items-center justify-between">
|
||||||
<code className="text-xs font-mono text-blue-400">{pattern}</code>
|
<code className="text-xs font-mono text-blue-400">{pattern}</code>
|
||||||
<div className="flex items-center gap-1">
|
<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">
|
<div className="mt-1 space-y-1">
|
||||||
{commands.map((cmd, idx) => (
|
{commands.map((cmd, idx) => (
|
||||||
<div key={idx} className="flex items-center justify-between text-xs">
|
<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(' ')}
|
{cmd.command.join(' ')}
|
||||||
</code>
|
</code>
|
||||||
<Button
|
<Button
|
||||||
@@ -365,7 +365,7 @@ export function HooksPanel({ onClose, responsive = false }: HooksPanelProps) {
|
|||||||
|
|
||||||
if (commands.length === 0) {
|
if (commands.length === 0) {
|
||||||
return (
|
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
|
No hooks configured
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
@@ -376,8 +376,8 @@ export function HooksPanel({ onClose, responsive = false }: HooksPanelProps) {
|
|||||||
{commands.map((cmd, idx) => {
|
{commands.map((cmd, idx) => {
|
||||||
const cmdId = `session-${idx}`;
|
const cmdId = `session-${idx}`;
|
||||||
return (
|
return (
|
||||||
<div key={idx} className="bg-gray-800/50 rounded p-2 flex items-center justify-between">
|
<div key={idx} className="bg-surface-subtle/50 rounded p-2 flex items-center justify-between">
|
||||||
<code className="text-xs font-mono text-gray-400 truncate flex-1">
|
<code className="text-xs font-mono text-fg-muted truncate flex-1">
|
||||||
{cmd.command.join(' ')}
|
{cmd.command.join(' ')}
|
||||||
</code>
|
</code>
|
||||||
<div className="flex items-center gap-1">
|
<div className="flex items-center gap-1">
|
||||||
@@ -444,7 +444,7 @@ export function HooksPanel({ onClose, responsive = false }: HooksPanelProps) {
|
|||||||
transition={smoothTransition}
|
transition={smoothTransition}
|
||||||
onClick={(e) => e.stopPropagation()}
|
onClick={(e) => e.stopPropagation()}
|
||||||
className={cn(
|
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
|
responsive
|
||||||
? 'w-full md:w-full md:max-w-2xl md:mx-4 rounded-t-2xl md:rounded-lg'
|
? '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'
|
: 'rounded-lg w-full max-w-2xl mx-4'
|
||||||
@@ -453,19 +453,19 @@ export function HooksPanel({ onClose, responsive = false }: HooksPanelProps) {
|
|||||||
{/* Header */}
|
{/* Header */}
|
||||||
<div
|
<div
|
||||||
className={cn(
|
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 ? 'px-4 md:px-6 py-4' : 'px-6 py-4'
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
{responsive && (
|
{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')}>
|
<div className={cn(responsive && 'mt-2 md:mt-0')}>
|
||||||
<h2 className="text-lg font-semibold flex items-center gap-2">
|
<h2 className="text-lg font-semibold flex items-center gap-2">
|
||||||
<Zap size={20} className="text-yellow-400" />
|
<Zap size={20} className="text-yellow-400" />
|
||||||
Hooks Configuration
|
Hooks Configuration
|
||||||
</h2>
|
</h2>
|
||||||
<p className="text-xs text-gray-500">
|
<p className="text-xs text-fg-subtle">
|
||||||
{totalHooks} hooks configured
|
{totalHooks} hooks configured
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
@@ -512,17 +512,17 @@ export function HooksPanel({ onClose, responsive = false }: HooksPanelProps) {
|
|||||||
<motion.div
|
<motion.div
|
||||||
key={type}
|
key={type}
|
||||||
layout
|
layout
|
||||||
className="bg-gray-900/50 rounded-lg overflow-hidden"
|
className="bg-surface-base/50 rounded-lg overflow-hidden"
|
||||||
>
|
>
|
||||||
{/* Type Header */}
|
{/* Type Header */}
|
||||||
<div
|
<div
|
||||||
className={cn(
|
className={cn(
|
||||||
'flex items-center gap-3 p-3',
|
'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)}
|
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} />}
|
{isExpanded ? <ChevronDown size={16} /> : <ChevronRight size={16} />}
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
@@ -530,14 +530,14 @@ export function HooksPanel({ onClose, responsive = false }: HooksPanelProps) {
|
|||||||
|
|
||||||
<div className="flex-1 min-w-0">
|
<div className="flex-1 min-w-0">
|
||||||
<div className="flex items-center gap-2">
|
<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 && (
|
{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}
|
{hookCount}
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
<div className="text-xs text-gray-500">{description}</div>
|
<div className="text-xs text-fg-subtle">{description}</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Button
|
<Button
|
||||||
@@ -568,7 +568,7 @@ export function HooksPanel({ onClose, responsive = false }: HooksPanelProps) {
|
|||||||
transition={{ duration: 0.2 }}
|
transition={{ duration: 0.2 }}
|
||||||
className="overflow-hidden"
|
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()}
|
{isFileHook ? renderFileHooks(type) : renderSessionHooks()}
|
||||||
</div>
|
</div>
|
||||||
</motion.div>
|
</motion.div>
|
||||||
@@ -584,7 +584,7 @@ export function HooksPanel({ onClose, responsive = false }: HooksPanelProps) {
|
|||||||
{/* Footer */}
|
{/* Footer */}
|
||||||
<div
|
<div
|
||||||
className={cn(
|
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'
|
responsive && 'safe-area-pb'
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -48,13 +48,13 @@ function getStatusColor(status: MCPServerStatus['status']) {
|
|||||||
case 'connecting':
|
case 'connecting':
|
||||||
return 'bg-yellow-500 animate-pulse';
|
return 'bg-yellow-500 animate-pulse';
|
||||||
case 'disconnected':
|
case 'disconnected':
|
||||||
return 'bg-gray-500';
|
return 'bg-surface-muted';
|
||||||
case 'disabled':
|
case 'disabled':
|
||||||
return 'bg-gray-600';
|
return 'bg-surface-emphasis';
|
||||||
case 'error':
|
case 'error':
|
||||||
return 'bg-red-500';
|
return 'bg-red-500';
|
||||||
default:
|
default:
|
||||||
return 'bg-gray-500';
|
return 'bg-surface-muted';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -216,7 +216,7 @@ export function MCPPanel({ onClose, responsive = false }: MCPPanelProps) {
|
|||||||
const LoadingSkeleton = () => (
|
const LoadingSkeleton = () => (
|
||||||
<div className="space-y-3 p-4">
|
<div className="space-y-3 p-4">
|
||||||
{[1, 2, 3].map((i) => (
|
{[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" />
|
<Skeleton className="h-3 w-3 rounded-full" />
|
||||||
<div className="flex-1 space-y-2">
|
<div className="flex-1 space-y-2">
|
||||||
<Skeleton className="h-4 w-32" />
|
<Skeleton className="h-4 w-32" />
|
||||||
@@ -250,7 +250,7 @@ export function MCPPanel({ onClose, responsive = false }: MCPPanelProps) {
|
|||||||
transition={smoothTransition}
|
transition={smoothTransition}
|
||||||
onClick={(e) => e.stopPropagation()}
|
onClick={(e) => e.stopPropagation()}
|
||||||
className={cn(
|
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
|
responsive
|
||||||
? 'w-full md:w-full md:max-w-2xl md:mx-4 rounded-t-2xl md:rounded-lg'
|
? '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'
|
: 'rounded-lg w-full max-w-2xl mx-4'
|
||||||
@@ -259,19 +259,19 @@ export function MCPPanel({ onClose, responsive = false }: MCPPanelProps) {
|
|||||||
{/* Header */}
|
{/* Header */}
|
||||||
<div
|
<div
|
||||||
className={cn(
|
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 ? 'px-4 md:px-6 py-4' : 'px-6 py-4'
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
{responsive && (
|
{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')}>
|
<div className={cn(responsive && 'mt-2 md:mt-0')}>
|
||||||
<h2 className="text-lg font-semibold flex items-center gap-2">
|
<h2 className="text-lg font-semibold flex items-center gap-2">
|
||||||
<Plug size={20} className="text-primary-400" />
|
<Plug size={20} className="text-primary-400" />
|
||||||
MCP Servers
|
MCP Servers
|
||||||
</h2>
|
</h2>
|
||||||
<p className="text-xs text-gray-500">
|
<p className="text-xs text-fg-subtle">
|
||||||
{servers.length} servers ({connectedCount} connected, {totalToolCount} tools)
|
{servers.length} servers ({connectedCount} connected, {totalToolCount} tools)
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
@@ -302,10 +302,10 @@ export function MCPPanel({ onClose, responsive = false }: MCPPanelProps) {
|
|||||||
{loading ? (
|
{loading ? (
|
||||||
<LoadingSkeleton />
|
<LoadingSkeleton />
|
||||||
) : servers.length === 0 ? (
|
) : 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" />
|
<Plug size={48} className="mb-4 opacity-50" />
|
||||||
<p className="text-center">No MCP servers configured</p>
|
<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
|
Configure MCP servers in your .ai-assist/config.json file
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
@@ -325,18 +325,18 @@ export function MCPPanel({ onClose, responsive = false }: MCPPanelProps) {
|
|||||||
layout
|
layout
|
||||||
initial={{ opacity: 0, y: 10 }}
|
initial={{ opacity: 0, y: 10 }}
|
||||||
animate={{ opacity: 1, y: 0 }}
|
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 */}
|
{/* Server Header */}
|
||||||
<div
|
<div
|
||||||
className={cn(
|
className={cn(
|
||||||
'flex items-center gap-3 p-3',
|
'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)}
|
onClick={() => toggleExpanded(server.name)}
|
||||||
>
|
>
|
||||||
{/* Expand Icon */}
|
{/* 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} />}
|
{isExpanded ? <ChevronDown size={16} /> : <ChevronRight size={16} />}
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
@@ -349,10 +349,10 @@ export function MCPPanel({ onClose, responsive = false }: MCPPanelProps) {
|
|||||||
{/* Info */}
|
{/* Info */}
|
||||||
<div className="flex-1 min-w-0">
|
<div className="flex-1 min-w-0">
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<span className="font-medium text-gray-200">{server.name}</span>
|
<span className="font-medium text-fg-secondary">{server.name}</span>
|
||||||
<span className="text-xs text-gray-500">{server.type}</span>
|
<span className="text-xs text-fg-subtle">{server.type}</span>
|
||||||
</div>
|
</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>
|
<span>{getStatusText(server.status)}</span>
|
||||||
{server.toolCount > 0 && (
|
{server.toolCount > 0 && (
|
||||||
<>
|
<>
|
||||||
@@ -428,7 +428,7 @@ export function MCPPanel({ onClose, responsive = false }: MCPPanelProps) {
|
|||||||
transition={{ duration: 0.2 }}
|
transition={{ duration: 0.2 }}
|
||||||
className="overflow-hidden"
|
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 */}
|
{/* Error Message */}
|
||||||
{server.error && (
|
{server.error && (
|
||||||
<div className="flex items-start gap-2 p-2 bg-red-500/10 rounded text-red-400 text-xs mb-2">
|
<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 */}
|
{/* Config Info */}
|
||||||
{server.config && (
|
{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 && (
|
{server.config.command && (
|
||||||
<div>
|
<div>
|
||||||
<span className="text-gray-400">Command:</span>{' '}
|
<span className="text-fg-muted">Command:</span>{' '}
|
||||||
<code className="font-mono bg-gray-800 px-1 rounded">
|
<code className="font-mono bg-surface-subtle px-1 rounded">
|
||||||
{server.config.command.join(' ')}
|
{server.config.command.join(' ')}
|
||||||
</code>
|
</code>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{server.config.url && (
|
{server.config.url && (
|
||||||
<div>
|
<div>
|
||||||
<span className="text-gray-400">URL:</span>{' '}
|
<span className="text-fg-muted">URL:</span>{' '}
|
||||||
<code className="font-mono bg-gray-800 px-1 rounded">
|
<code className="font-mono bg-surface-subtle px-1 rounded">
|
||||||
{server.config.url}
|
{server.config.url}
|
||||||
</code>
|
</code>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{server.config.timeout && (
|
{server.config.timeout && (
|
||||||
<div>
|
<div>
|
||||||
<span className="text-gray-400">Timeout:</span>{' '}
|
<span className="text-fg-muted">Timeout:</span>{' '}
|
||||||
{server.config.timeout}ms
|
{server.config.timeout}ms
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
@@ -468,12 +468,12 @@ export function MCPPanel({ onClose, responsive = false }: MCPPanelProps) {
|
|||||||
{/* Tools List */}
|
{/* Tools List */}
|
||||||
{server.tools && server.tools.length > 0 && (
|
{server.tools && server.tools.length > 0 && (
|
||||||
<div className="mt-3">
|
<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">
|
<div className="flex flex-wrap gap-1">
|
||||||
{server.tools.map((tool) => (
|
{server.tools.map((tool) => (
|
||||||
<span
|
<span
|
||||||
key={tool.name}
|
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}
|
title={tool.description}
|
||||||
>
|
>
|
||||||
{tool.originalName}
|
{tool.originalName}
|
||||||
@@ -496,11 +496,11 @@ export function MCPPanel({ onClose, responsive = false }: MCPPanelProps) {
|
|||||||
{/* Footer Info */}
|
{/* Footer Info */}
|
||||||
<div
|
<div
|
||||||
className={cn(
|
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'
|
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>
|
</div>
|
||||||
</motion.div>
|
</motion.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>;
|
return <ol className="list-decimal list-inside mb-4 space-y-1">{children}</ol>;
|
||||||
},
|
},
|
||||||
li({ children }) {
|
li({ children }) {
|
||||||
return <li className="text-gray-200">{children}</li>;
|
return <li className="text-fg-secondary">{children}</li>;
|
||||||
},
|
},
|
||||||
|
|
||||||
// 引用
|
// 引用
|
||||||
blockquote({ children }) {
|
blockquote({ children }) {
|
||||||
return (
|
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}
|
{children}
|
||||||
</blockquote>
|
</blockquote>
|
||||||
);
|
);
|
||||||
@@ -97,7 +97,7 @@ export function Markdown({ content, className }: MarkdownProps) {
|
|||||||
|
|
||||||
// 强调
|
// 强调
|
||||||
strong({ children }) {
|
strong({ children }) {
|
||||||
return <strong className="font-bold text-gray-100">{children}</strong>;
|
return <strong className="font-bold text-fg">{children}</strong>;
|
||||||
},
|
},
|
||||||
em({ children }) {
|
em({ children }) {
|
||||||
return <em className="italic">{children}</em>;
|
return <em className="italic">{children}</em>;
|
||||||
@@ -105,38 +105,38 @@ export function Markdown({ content, className }: MarkdownProps) {
|
|||||||
|
|
||||||
// 分割线
|
// 分割线
|
||||||
hr() {
|
hr() {
|
||||||
return <hr className="my-6 border-gray-700" />;
|
return <hr className="my-6 border-line" />;
|
||||||
},
|
},
|
||||||
|
|
||||||
// 表格
|
// 表格
|
||||||
table({ children }) {
|
table({ children }) {
|
||||||
return (
|
return (
|
||||||
<div className="overflow-x-auto my-4">
|
<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}
|
{children}
|
||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
thead({ children }) {
|
thead({ children }) {
|
||||||
return <thead className="bg-gray-800">{children}</thead>;
|
return <thead className="bg-surface-subtle">{children}</thead>;
|
||||||
},
|
},
|
||||||
tbody({ children }) {
|
tbody({ children }) {
|
||||||
return <tbody>{children}</tbody>;
|
return <tbody>{children}</tbody>;
|
||||||
},
|
},
|
||||||
tr({ children }) {
|
tr({ children }) {
|
||||||
return <tr className="border-b border-gray-700">{children}</tr>;
|
return <tr className="border-b border-line">{children}</tr>;
|
||||||
},
|
},
|
||||||
th({ children }) {
|
th({ children }) {
|
||||||
return (
|
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}
|
{children}
|
||||||
</th>
|
</th>
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
td({ children }) {
|
td({ children }) {
|
||||||
return (
|
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}
|
{children}
|
||||||
</td>
|
</td>
|
||||||
);
|
);
|
||||||
@@ -156,7 +156,7 @@ export function Markdown({ content, className }: MarkdownProps) {
|
|||||||
// 预格式化文本(非代码块的 pre)
|
// 预格式化文本(非代码块的 pre)
|
||||||
pre({ children }) {
|
pre({ children }) {
|
||||||
return (
|
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}
|
{children}
|
||||||
</pre>
|
</pre>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -86,7 +86,7 @@ function getPermissionIcon(type: PermissionType) {
|
|||||||
case 'web':
|
case 'web':
|
||||||
return <Globe size={24} className="text-green-400" />;
|
return <Globe size={24} className="text-green-400" />;
|
||||||
default:
|
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 (
|
return (
|
||||||
<div className="mt-4 rounded-lg border border-gray-700 overflow-hidden">
|
<div className="mt-4 rounded-lg border border-line overflow-hidden">
|
||||||
<div className="flex items-center justify-between px-3 py-2 bg-gray-900/50 border-b border-gray-700">
|
<div className="flex items-center justify-between px-3 py-2 bg-surface-base/50 border-b border-line">
|
||||||
<span className="text-xs text-gray-400">
|
<span className="text-xs text-fg-muted">
|
||||||
{diff.isNew ? 'New file' : 'Changes'}
|
{diff.isNew ? 'New file' : 'Changes'}
|
||||||
</span>
|
</span>
|
||||||
<div className="flex items-center gap-3 text-xs">
|
<div className="flex items-center gap-3 text-xs">
|
||||||
@@ -127,7 +127,7 @@ function DiffViewer({ diff }: { diff: DiffInfo }) {
|
|||||||
<pre className="text-xs font-mono">
|
<pre className="text-xs font-mono">
|
||||||
{diff.hunks.map((hunk, hunkIndex) => (
|
{diff.hunks.map((hunk, hunkIndex) => (
|
||||||
<div key={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} @@
|
@@ -{hunk.oldStart},{hunk.oldCount} +{hunk.newStart},{hunk.newCount} @@
|
||||||
</div>
|
</div>
|
||||||
{hunk.lines.map((line, lineIndex) => {
|
{hunk.lines.map((line, lineIndex) => {
|
||||||
@@ -141,7 +141,7 @@ function DiffViewer({ diff }: { diff: DiffInfo }) {
|
|||||||
className += 'bg-red-500/10 text-red-400';
|
className += 'bg-red-500/10 text-red-400';
|
||||||
prefix = '-';
|
prefix = '-';
|
||||||
} else {
|
} else {
|
||||||
className += 'text-gray-400';
|
className += 'text-fg-muted';
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -182,8 +182,8 @@ export function PermissionDialog({
|
|||||||
case 'bash':
|
case 'bash':
|
||||||
return (
|
return (
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<div className="text-sm text-gray-400">Command:</div>
|
<div className="text-sm text-fg-muted">Command:</div>
|
||||||
<code className="block px-3 py-2 bg-gray-900 rounded-lg font-mono text-sm text-yellow-300 break-all">
|
<code className="block px-3 py-2 bg-surface-base rounded-lg font-mono text-sm text-yellow-300 break-all">
|
||||||
{context.command}
|
{context.command}
|
||||||
</code>
|
</code>
|
||||||
{context.externalPaths && context.externalPaths.length > 0 && (
|
{context.externalPaths && context.externalPaths.length > 0 && (
|
||||||
@@ -206,7 +206,7 @@ export function PermissionDialog({
|
|||||||
return (
|
return (
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<div className="flex items-center gap-2 text-sm">
|
<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(
|
<span className={cn(
|
||||||
'px-2 py-0.5 rounded text-xs font-medium',
|
'px-2 py-0.5 rounded text-xs font-medium',
|
||||||
context.operation === 'delete' ? 'bg-red-500/20 text-red-400' :
|
context.operation === 'delete' ? 'bg-red-500/20 text-red-400' :
|
||||||
@@ -216,8 +216,8 @@ export function PermissionDialog({
|
|||||||
{context.operation?.toUpperCase()}
|
{context.operation?.toUpperCase()}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="text-sm text-gray-400">Path:</div>
|
<div className="text-sm text-fg-muted">Path:</div>
|
||||||
<code className="block px-3 py-2 bg-gray-900 rounded-lg font-mono text-sm text-blue-300 break-all">
|
<code className="block px-3 py-2 bg-surface-base rounded-lg font-mono text-sm text-blue-300 break-all">
|
||||||
{context.path}
|
{context.path}
|
||||||
</code>
|
</code>
|
||||||
{diff && <DiffViewer diff={diff} />}
|
{diff && <DiffViewer diff={diff} />}
|
||||||
@@ -228,15 +228,15 @@ export function PermissionDialog({
|
|||||||
return (
|
return (
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<div className="flex items-center gap-2 text-sm">
|
<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">
|
<span className="px-2 py-0.5 rounded text-xs font-medium bg-purple-500/20 text-purple-400">
|
||||||
{context.gitOperation?.toUpperCase()}
|
{context.gitOperation?.toUpperCase()}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
{context.command && (
|
{context.command && (
|
||||||
<>
|
<>
|
||||||
<div className="text-sm text-gray-400">Command:</div>
|
<div className="text-sm text-fg-muted">Command:</div>
|
||||||
<code className="block px-3 py-2 bg-gray-900 rounded-lg font-mono text-sm text-purple-300 break-all">
|
<code className="block px-3 py-2 bg-surface-base rounded-lg font-mono text-sm text-purple-300 break-all">
|
||||||
{context.command}
|
{context.command}
|
||||||
</code>
|
</code>
|
||||||
</>
|
</>
|
||||||
@@ -247,8 +247,8 @@ export function PermissionDialog({
|
|||||||
case 'web':
|
case 'web':
|
||||||
return (
|
return (
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<div className="text-sm text-gray-400">Request:</div>
|
<div className="text-sm text-fg-muted">Request:</div>
|
||||||
<code className="block px-3 py-2 bg-gray-900 rounded-lg font-mono text-sm text-green-300 break-all">
|
<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}
|
{context.query || context.command}
|
||||||
</code>
|
</code>
|
||||||
</div>
|
</div>
|
||||||
@@ -256,8 +256,8 @@ export function PermissionDialog({
|
|||||||
|
|
||||||
default:
|
default:
|
||||||
return (
|
return (
|
||||||
<div className="text-sm text-gray-400">
|
<div className="text-sm text-fg-muted">
|
||||||
<pre className="bg-gray-900 p-3 rounded-lg overflow-auto">
|
<pre className="bg-surface-base p-3 rounded-lg overflow-auto">
|
||||||
{JSON.stringify(context, null, 2)}
|
{JSON.stringify(context, null, 2)}
|
||||||
</pre>
|
</pre>
|
||||||
</div>
|
</div>
|
||||||
@@ -285,24 +285,24 @@ export function PermissionDialog({
|
|||||||
exit="exit"
|
exit="exit"
|
||||||
transition={smoothTransition}
|
transition={smoothTransition}
|
||||||
className={cn(
|
className={cn(
|
||||||
'bg-gray-800 overflow-hidden flex flex-col',
|
'bg-surface-subtle overflow-hidden flex flex-col',
|
||||||
responsive
|
responsive
|
||||||
? 'w-full md:w-full md:max-w-lg md:mx-4 rounded-t-2xl md:rounded-lg'
|
? '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'
|
: 'rounded-lg w-full max-w-lg mx-4'
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
{/* Header */}
|
{/* 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 && (
|
{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={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)}
|
{getPermissionIcon(permissionType)}
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<h2 className="text-lg font-semibold">{getPermissionTitle(permissionType)}</h2>
|
<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>
|
||||||
</div>
|
</div>
|
||||||
<Button
|
<Button
|
||||||
@@ -322,16 +322,16 @@ export function PermissionDialog({
|
|||||||
|
|
||||||
{/* Footer */}
|
{/* Footer */}
|
||||||
<div className={cn(
|
<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'
|
responsive ? 'px-4 py-4 safe-area-pb' : 'px-5 py-4'
|
||||||
)}>
|
)}>
|
||||||
{/* Remember checkbox */}
|
{/* 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
|
<input
|
||||||
type="checkbox"
|
type="checkbox"
|
||||||
checked={remember}
|
checked={remember}
|
||||||
onChange={(e) => setRemember(e.target.checked)}
|
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
|
Remember for this session
|
||||||
</label>
|
</label>
|
||||||
|
|||||||
@@ -217,7 +217,7 @@ export function ProviderEditor({
|
|||||||
transition={smoothTransition}
|
transition={smoothTransition}
|
||||||
onClick={(e) => e.stopPropagation()}
|
onClick={(e) => e.stopPropagation()}
|
||||||
className={cn(
|
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
|
responsive
|
||||||
? 'w-full md:w-full md:max-w-lg md:mx-4 rounded-t-2xl md:rounded-lg'
|
? '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'
|
: 'rounded-lg w-full max-w-lg mx-4'
|
||||||
@@ -226,12 +226,12 @@ export function ProviderEditor({
|
|||||||
{/* Header */}
|
{/* Header */}
|
||||||
<div
|
<div
|
||||||
className={cn(
|
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 ? 'px-4 md:px-6 py-4' : 'px-6 py-4'
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
{responsive && (
|
{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')}>
|
<div className={cn(responsive && 'mt-2 md:mt-0')}>
|
||||||
<h2 className="text-lg font-semibold flex items-center gap-2">
|
<h2 className="text-lg font-semibold flex items-center gap-2">
|
||||||
@@ -239,7 +239,7 @@ export function ProviderEditor({
|
|||||||
{loading ? 'Loading...' : provider?.name || providerId}
|
{loading ? 'Loading...' : provider?.name || providerId}
|
||||||
</h2>
|
</h2>
|
||||||
{provider && (
|
{provider && (
|
||||||
<p className="text-xs text-gray-500">
|
<p className="text-xs text-fg-subtle">
|
||||||
{provider.builtin ? 'Built-in Provider' : 'Custom Provider'}
|
{provider.builtin ? 'Built-in Provider' : 'Custom Provider'}
|
||||||
</p>
|
</p>
|
||||||
)}
|
)}
|
||||||
@@ -283,17 +283,17 @@ export function ProviderEditor({
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Enabled Toggle */}
|
{/* 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>
|
<div>
|
||||||
<span className="text-sm font-medium">Enabled</span>
|
<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>
|
</div>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() => setEnabled(!enabled)}
|
onClick={() => setEnabled(!enabled)}
|
||||||
className={cn(
|
className={cn(
|
||||||
'relative inline-flex h-6 w-11 items-center rounded-full transition-colors',
|
'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
|
<span
|
||||||
@@ -307,14 +307,14 @@ export function ProviderEditor({
|
|||||||
|
|
||||||
{/* API Key Section */}
|
{/* API Key Section */}
|
||||||
<div className="space-y-3">
|
<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} />
|
<Key size={14} />
|
||||||
API Key Configuration
|
API Key Configuration
|
||||||
</h3>
|
</h3>
|
||||||
|
|
||||||
{/* API Key Input */}
|
{/* API Key Input */}
|
||||||
<div>
|
<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">
|
<div className="relative">
|
||||||
<Input
|
<Input
|
||||||
type={showApiKey ? 'text' : 'password'}
|
type={showApiKey ? 'text' : 'password'}
|
||||||
@@ -326,7 +326,7 @@ export function ProviderEditor({
|
|||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() => setShowApiKey(!showApiKey)}
|
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} />}
|
{showApiKey ? <EyeOff size={16} /> : <Eye size={16} />}
|
||||||
</button>
|
</button>
|
||||||
@@ -338,7 +338,7 @@ export function ProviderEditor({
|
|||||||
|
|
||||||
{/* API Key Env Var */}
|
{/* API Key Env Var */}
|
||||||
<div>
|
<div>
|
||||||
<label className="block text-xs text-gray-400 mb-1">
|
<label className="block text-xs text-fg-muted mb-1">
|
||||||
Environment Variable (alternative)
|
Environment Variable (alternative)
|
||||||
</label>
|
</label>
|
||||||
<Input
|
<Input
|
||||||
@@ -346,7 +346,7 @@ export function ProviderEditor({
|
|||||||
onChange={(e) => setApiKeyEnvVar(e.target.value)}
|
onChange={(e) => setApiKeyEnvVar(e.target.value)}
|
||||||
placeholder={provider?.apiKeyEnvVar || 'PROVIDER_API_KEY'}
|
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
|
If no API key is set, this env var will be used
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
@@ -354,20 +354,20 @@ export function ProviderEditor({
|
|||||||
|
|
||||||
{/* Base URL Section */}
|
{/* Base URL Section */}
|
||||||
<div className="space-y-3">
|
<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} />
|
<Globe size={14} />
|
||||||
Endpoint Configuration
|
Endpoint Configuration
|
||||||
</h3>
|
</h3>
|
||||||
|
|
||||||
<div>
|
<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
|
<Input
|
||||||
value={baseUrl}
|
value={baseUrl}
|
||||||
onChange={(e) => setBaseUrl(e.target.value)}
|
onChange={(e) => setBaseUrl(e.target.value)}
|
||||||
placeholder={provider?.baseUrl || 'https://api.provider.com/v1'}
|
placeholder={provider?.baseUrl || 'https://api.provider.com/v1'}
|
||||||
/>
|
/>
|
||||||
{provider?.builtin && (
|
{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
|
Leave empty to use default endpoint
|
||||||
</p>
|
</p>
|
||||||
)}
|
)}
|
||||||
@@ -378,7 +378,7 @@ export function ProviderEditor({
|
|||||||
{provider?.allowCustomModels && (
|
{provider?.allowCustomModels && (
|
||||||
<div className="space-y-3">
|
<div className="space-y-3">
|
||||||
<div className="flex items-center justify-between">
|
<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} />
|
<Cpu size={14} />
|
||||||
Custom Models ({provider.config.customModels.length})
|
Custom Models ({provider.config.customModels.length})
|
||||||
</h3>
|
</h3>
|
||||||
@@ -402,10 +402,10 @@ export function ProviderEditor({
|
|||||||
exit={{ height: 0, opacity: 0 }}
|
exit={{ height: 0, opacity: 0 }}
|
||||||
className="overflow-hidden"
|
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 className="grid grid-cols-2 gap-3">
|
||||||
<div>
|
<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
|
<Input
|
||||||
value={newModelId}
|
value={newModelId}
|
||||||
onChange={(e) => setNewModelId(e.target.value)}
|
onChange={(e) => setNewModelId(e.target.value)}
|
||||||
@@ -414,7 +414,7 @@ export function ProviderEditor({
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<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
|
<Input
|
||||||
value={newModelName}
|
value={newModelName}
|
||||||
onChange={(e) => setNewModelName(e.target.value)}
|
onChange={(e) => setNewModelName(e.target.value)}
|
||||||
@@ -424,21 +424,21 @@ export function ProviderEditor({
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center gap-4">
|
<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
|
<input
|
||||||
type="checkbox"
|
type="checkbox"
|
||||||
checked={newModelVision}
|
checked={newModelVision}
|
||||||
onChange={(e) => setNewModelVision(e.target.checked)}
|
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
|
Vision
|
||||||
</label>
|
</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
|
<input
|
||||||
type="checkbox"
|
type="checkbox"
|
||||||
checked={newModelTools}
|
checked={newModelTools}
|
||||||
onChange={(e) => setNewModelTools(e.target.checked)}
|
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
|
Function Calling
|
||||||
</label>
|
</label>
|
||||||
@@ -470,17 +470,17 @@ export function ProviderEditor({
|
|||||||
{provider.config.customModels.map((model) => (
|
{provider.config.customModels.map((model) => (
|
||||||
<div
|
<div
|
||||||
key={model.id}
|
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>
|
<div>
|
||||||
<span className="text-gray-200">{model.name}</span>
|
<span className="text-fg-secondary">{model.name}</span>
|
||||||
<span className="text-gray-500 ml-2">({model.id})</span>
|
<span className="text-fg-subtle ml-2">({model.id})</span>
|
||||||
<div className="flex items-center gap-2 mt-0.5">
|
<div className="flex items-center gap-2 mt-0.5">
|
||||||
{model.capabilities?.vision && (
|
{model.capabilities?.vision && (
|
||||||
<span className="text-[10px] text-gray-500">Vision</span>
|
<span className="text-[10px] text-fg-subtle">Vision</span>
|
||||||
)}
|
)}
|
||||||
{model.capabilities?.functionCalling && (
|
{model.capabilities?.functionCalling && (
|
||||||
<span className="text-[10px] text-gray-500">Tools</span>
|
<span className="text-[10px] text-fg-subtle">Tools</span>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -496,7 +496,7 @@ export function ProviderEditor({
|
|||||||
))}
|
))}
|
||||||
</div>
|
</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
|
No custom models added yet
|
||||||
</p>
|
</p>
|
||||||
)}
|
)}
|
||||||
@@ -505,16 +505,16 @@ export function ProviderEditor({
|
|||||||
|
|
||||||
{/* Built-in Models Info */}
|
{/* Built-in Models Info */}
|
||||||
{provider && (
|
{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>
|
<p className="font-medium mb-1">Built-in Models:</p>
|
||||||
<div className="flex flex-wrap gap-1">
|
<div className="flex flex-wrap gap-1">
|
||||||
{provider.models.slice(0, 5).map((m) => (
|
{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}
|
{m.name}
|
||||||
</span>
|
</span>
|
||||||
))}
|
))}
|
||||||
{provider.models.length > 5 && (
|
{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
|
+{provider.models.length - 5} more
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
@@ -528,7 +528,7 @@ export function ProviderEditor({
|
|||||||
{/* Footer */}
|
{/* Footer */}
|
||||||
<div
|
<div
|
||||||
className={cn(
|
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'
|
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 = () => (
|
const LoadingSkeleton = () => (
|
||||||
<div className="space-y-3 p-4">
|
<div className="space-y-3 p-4">
|
||||||
{[1, 2, 3].map((i) => (
|
{[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" />
|
<Skeleton className="h-4 w-4" />
|
||||||
<div className="flex-1 space-y-2">
|
<div className="flex-1 space-y-2">
|
||||||
<Skeleton className="h-4 w-32" />
|
<Skeleton className="h-4 w-32" />
|
||||||
@@ -279,18 +279,18 @@ export function ProvidersPanel({ onClose, responsive = false }: ProvidersPanelPr
|
|||||||
layout
|
layout
|
||||||
initial={{ opacity: 0, y: 10 }}
|
initial={{ opacity: 0, y: 10 }}
|
||||||
animate={{ opacity: 1, y: 0 }}
|
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 */}
|
{/* Provider Header */}
|
||||||
<div
|
<div
|
||||||
className={cn(
|
className={cn(
|
||||||
'flex items-center gap-3 p-3',
|
'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)}
|
onClick={() => toggleExpanded(provider.id)}
|
||||||
>
|
>
|
||||||
{/* Expand Icon */}
|
{/* 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} />}
|
{isExpanded ? <ChevronDown size={16} /> : <ChevronRight size={16} />}
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
@@ -304,7 +304,7 @@ export function ProvidersPanel({ onClose, responsive = false }: ProvidersPanelPr
|
|||||||
{/* Info */}
|
{/* Info */}
|
||||||
<div className="flex-1 min-w-0">
|
<div className="flex-1 min-w-0">
|
||||||
<div className="flex items-center gap-2">
|
<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
|
<span
|
||||||
className={cn(
|
className={cn(
|
||||||
'text-xs px-2 py-0.5 rounded-full',
|
'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'}
|
{provider.builtin ? 'Built-in' : 'Custom'}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</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>
|
<span>{provider.modelCount} models</span>
|
||||||
{provider.hasApiKey ? (
|
{provider.hasApiKey ? (
|
||||||
<span className="text-green-400 flex items-center gap-1">
|
<span className="text-green-400 flex items-center gap-1">
|
||||||
@@ -349,7 +349,7 @@ export function ProvidersPanel({ onClose, responsive = false }: ProvidersPanelPr
|
|||||||
size="sm"
|
size="sm"
|
||||||
onClick={() => handleTestConnection(provider.id)}
|
onClick={() => handleTestConnection(provider.id)}
|
||||||
disabled={isTesting}
|
disabled={isTesting}
|
||||||
className="text-gray-400 hover:text-gray-300"
|
className="text-fg-muted hover:text-fg-secondary"
|
||||||
title="Test Connection"
|
title="Test Connection"
|
||||||
>
|
>
|
||||||
{isTesting ? (
|
{isTesting ? (
|
||||||
@@ -364,7 +364,7 @@ export function ProvidersPanel({ onClose, responsive = false }: ProvidersPanelPr
|
|||||||
variant="ghost"
|
variant="ghost"
|
||||||
size="sm"
|
size="sm"
|
||||||
onClick={() => setEditingProviderId(provider.id)}
|
onClick={() => setEditingProviderId(provider.id)}
|
||||||
className="text-gray-400 hover:text-gray-300"
|
className="text-fg-muted hover:text-fg-secondary"
|
||||||
title="Configure"
|
title="Configure"
|
||||||
>
|
>
|
||||||
<Settings size={14} />
|
<Settings size={14} />
|
||||||
@@ -395,29 +395,29 @@ export function ProvidersPanel({ onClose, responsive = false }: ProvidersPanelPr
|
|||||||
transition={{ duration: 0.2 }}
|
transition={{ duration: 0.2 }}
|
||||||
className="overflow-hidden"
|
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 ? (
|
{detail ? (
|
||||||
<>
|
<>
|
||||||
{/* Base URL */}
|
{/* Base URL */}
|
||||||
{detail.baseUrl && (
|
{detail.baseUrl && (
|
||||||
<div className="text-xs">
|
<div className="text-xs">
|
||||||
<span className="text-gray-400">Base URL:</span>{' '}
|
<span className="text-fg-muted">Base URL:</span>{' '}
|
||||||
<code className="text-gray-300 bg-gray-800 px-1 rounded">{detail.baseUrl}</code>
|
<code className="text-fg-secondary bg-surface-subtle px-1 rounded">{detail.baseUrl}</code>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* API Key Env Var */}
|
{/* API Key Env Var */}
|
||||||
{detail.apiKeyEnvVar && (
|
{detail.apiKeyEnvVar && (
|
||||||
<div className="text-xs">
|
<div className="text-xs">
|
||||||
<span className="text-gray-400">API Key Env:</span>{' '}
|
<span className="text-fg-muted">API Key Env:</span>{' '}
|
||||||
<code className="text-gray-300 bg-gray-800 px-1 rounded">{detail.apiKeyEnvVar}</code>
|
<code className="text-fg-secondary bg-surface-subtle px-1 rounded">{detail.apiKeyEnvVar}</code>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Models */}
|
{/* Models */}
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<div className="flex items-center justify-between">
|
<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} />
|
<Cpu size={12} />
|
||||||
Models ({detail.models.length + detail.config.customModels.length})
|
Models ({detail.models.length + detail.config.customModels.length})
|
||||||
</span>
|
</span>
|
||||||
@@ -440,13 +440,13 @@ export function ProvidersPanel({ onClose, responsive = false }: ProvidersPanelPr
|
|||||||
{detail.models.map((model) => (
|
{detail.models.map((model) => (
|
||||||
<div
|
<div
|
||||||
key={model.id}
|
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>
|
<div>
|
||||||
<span className="text-gray-200">{model.name}</span>
|
<span className="text-fg-secondary">{model.name}</span>
|
||||||
<span className="text-gray-500 ml-2">({model.id})</span>
|
<span className="text-fg-subtle ml-2">({model.id})</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center gap-2 text-gray-500">
|
<div className="flex items-center gap-2 text-fg-subtle">
|
||||||
{model.capabilities?.vision && (
|
{model.capabilities?.vision && (
|
||||||
<span title="Vision" className="text-[10px]">Vision</span>
|
<span title="Vision" className="text-[10px]">Vision</span>
|
||||||
)}
|
)}
|
||||||
@@ -461,11 +461,11 @@ export function ProvidersPanel({ onClose, responsive = false }: ProvidersPanelPr
|
|||||||
{detail.config.customModels.map((model) => (
|
{detail.config.customModels.map((model) => (
|
||||||
<div
|
<div
|
||||||
key={model.id}
|
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>
|
<div>
|
||||||
<span className="text-gray-200">{model.name}</span>
|
<span className="text-fg-secondary">{model.name}</span>
|
||||||
<span className="text-gray-500 ml-2">({model.id})</span>
|
<span className="text-fg-subtle ml-2">({model.id})</span>
|
||||||
<span className="text-green-400 ml-2 text-[10px]">custom</span>
|
<span className="text-green-400 ml-2 text-[10px]">custom</span>
|
||||||
</div>
|
</div>
|
||||||
<Button
|
<Button
|
||||||
@@ -516,7 +516,7 @@ export function ProvidersPanel({ onClose, responsive = false }: ProvidersPanelPr
|
|||||||
transition={smoothTransition}
|
transition={smoothTransition}
|
||||||
onClick={(e) => e.stopPropagation()}
|
onClick={(e) => e.stopPropagation()}
|
||||||
className={cn(
|
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
|
responsive
|
||||||
? 'w-full md:w-full md:max-w-2xl md:mx-4 rounded-t-2xl md:rounded-lg'
|
? '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'
|
: 'rounded-lg w-full max-w-2xl mx-4'
|
||||||
@@ -525,19 +525,19 @@ export function ProvidersPanel({ onClose, responsive = false }: ProvidersPanelPr
|
|||||||
{/* Header */}
|
{/* Header */}
|
||||||
<div
|
<div
|
||||||
className={cn(
|
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 ? 'px-4 md:px-6 py-4' : 'px-6 py-4'
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
{responsive && (
|
{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')}>
|
<div className={cn(responsive && 'mt-2 md:mt-0')}>
|
||||||
<h2 className="text-lg font-semibold flex items-center gap-2">
|
<h2 className="text-lg font-semibold flex items-center gap-2">
|
||||||
<Server size={20} className="text-primary-400" />
|
<Server size={20} className="text-primary-400" />
|
||||||
Model Providers
|
Model Providers
|
||||||
</h2>
|
</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)
|
{providers.length} providers ({builtinProviders.length} built-in, {customProviders.length} custom)
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
@@ -577,7 +577,7 @@ export function ProvidersPanel({ onClose, responsive = false }: ProvidersPanelPr
|
|||||||
{loading ? (
|
{loading ? (
|
||||||
<LoadingSkeleton />
|
<LoadingSkeleton />
|
||||||
) : providers.length === 0 ? (
|
) : 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" />
|
<Server size={48} className="mb-4 opacity-50" />
|
||||||
<p className="text-center">No providers available</p>
|
<p className="text-center">No providers available</p>
|
||||||
</div>
|
</div>
|
||||||
@@ -590,7 +590,7 @@ export function ProvidersPanel({ onClose, responsive = false }: ProvidersPanelPr
|
|||||||
{/* Built-in Providers */}
|
{/* Built-in Providers */}
|
||||||
{builtinProviders.length > 0 && (
|
{builtinProviders.length > 0 && (
|
||||||
<div>
|
<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} />
|
<Server size={12} />
|
||||||
Built-in Providers
|
Built-in Providers
|
||||||
</h3>
|
</h3>
|
||||||
@@ -604,7 +604,7 @@ export function ProvidersPanel({ onClose, responsive = false }: ProvidersPanelPr
|
|||||||
|
|
||||||
{/* Custom Providers */}
|
{/* Custom Providers */}
|
||||||
<div>
|
<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} />
|
<Globe size={12} />
|
||||||
Custom Providers
|
Custom Providers
|
||||||
</h3>
|
</h3>
|
||||||
@@ -615,7 +615,7 @@ export function ProvidersPanel({ onClose, responsive = false }: ProvidersPanelPr
|
|||||||
))}
|
))}
|
||||||
</div>
|
</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>
|
<p>No custom providers yet</p>
|
||||||
<Button
|
<Button
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
@@ -636,12 +636,12 @@ export function ProvidersPanel({ onClose, responsive = false }: ProvidersPanelPr
|
|||||||
{/* Footer */}
|
{/* Footer */}
|
||||||
<div
|
<div
|
||||||
className={cn(
|
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'
|
responsive ? 'px-4 py-3 safe-area-pb' : 'px-6 py-3'
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
Config stored in{' '}
|
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>
|
</div>
|
||||||
</motion.div>
|
</motion.div>
|
||||||
|
|
||||||
@@ -660,12 +660,12 @@ export function ProvidersPanel({ onClose, responsive = false }: ProvidersPanelPr
|
|||||||
animate={{ scale: 1, opacity: 1 }}
|
animate={{ scale: 1, opacity: 1 }}
|
||||||
exit={{ scale: 0.95, opacity: 0 }}
|
exit={{ scale: 0.95, opacity: 0 }}
|
||||||
onClick={(e) => e.stopPropagation()}
|
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>
|
<h3 className="text-lg font-semibold">Add Custom Provider</h3>
|
||||||
<div className="space-y-3">
|
<div className="space-y-3">
|
||||||
<div>
|
<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
|
<Input
|
||||||
value={newProvider.id || ''}
|
value={newProvider.id || ''}
|
||||||
onChange={(e) => setNewProvider((p) => ({ ...p, id: e.target.value }))}
|
onChange={(e) => setNewProvider((p) => ({ ...p, id: e.target.value }))}
|
||||||
@@ -673,7 +673,7 @@ export function ProvidersPanel({ onClose, responsive = false }: ProvidersPanelPr
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<label className="text-xs text-gray-400">Name</label>
|
<label className="text-xs text-fg-muted">Name</label>
|
||||||
<Input
|
<Input
|
||||||
value={newProvider.name || ''}
|
value={newProvider.name || ''}
|
||||||
onChange={(e) => setNewProvider((p) => ({ ...p, name: e.target.value }))}
|
onChange={(e) => setNewProvider((p) => ({ ...p, name: e.target.value }))}
|
||||||
@@ -681,7 +681,7 @@ export function ProvidersPanel({ onClose, responsive = false }: ProvidersPanelPr
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<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
|
<Input
|
||||||
value={newProvider.baseUrl || ''}
|
value={newProvider.baseUrl || ''}
|
||||||
onChange={(e) => setNewProvider((p) => ({ ...p, baseUrl: e.target.value }))}
|
onChange={(e) => setNewProvider((p) => ({ ...p, baseUrl: e.target.value }))}
|
||||||
@@ -689,7 +689,7 @@ export function ProvidersPanel({ onClose, responsive = false }: ProvidersPanelPr
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<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
|
<Input
|
||||||
value={newProvider.apiKeyEnvVar || ''}
|
value={newProvider.apiKeyEnvVar || ''}
|
||||||
onChange={(e) => setNewProvider((p) => ({ ...p, apiKeyEnvVar: e.target.value }))}
|
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 }}
|
animate={{ scale: 1, opacity: 1 }}
|
||||||
exit={{ scale: 0.95, opacity: 0 }}
|
exit={{ scale: 0.95, opacity: 0 }}
|
||||||
onClick={(e) => e.stopPropagation()}
|
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>
|
<h3 className="text-lg font-semibold">Add Custom Model</h3>
|
||||||
<div className="space-y-3">
|
<div className="space-y-3">
|
||||||
<div>
|
<div>
|
||||||
<label className="text-xs text-gray-400">Model ID</label>
|
<label className="text-xs text-fg-muted">Model ID</label>
|
||||||
<Input
|
<Input
|
||||||
value={newModel.id || ''}
|
value={newModel.id || ''}
|
||||||
onChange={(e) => setNewModel((m) => ({ ...m, id: e.target.value }))}
|
onChange={(e) => setNewModel((m) => ({ ...m, id: e.target.value }))}
|
||||||
@@ -738,7 +738,7 @@ export function ProvidersPanel({ onClose, responsive = false }: ProvidersPanelPr
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<label className="text-xs text-gray-400">Display Name</label>
|
<label className="text-xs text-fg-muted">Display Name</label>
|
||||||
<Input
|
<Input
|
||||||
value={newModel.name || ''}
|
value={newModel.name || ''}
|
||||||
onChange={(e) => setNewModel((m) => ({ ...m, name: e.target.value }))}
|
onChange={(e) => setNewModel((m) => ({ ...m, name: e.target.value }))}
|
||||||
|
|||||||
@@ -187,7 +187,7 @@ export function RestoreDialog({
|
|||||||
transition={smoothTransition}
|
transition={smoothTransition}
|
||||||
onClick={(e) => e.stopPropagation()}
|
onClick={(e) => e.stopPropagation()}
|
||||||
className={cn(
|
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
|
responsive
|
||||||
? 'w-full md:w-full md:max-w-lg md:mx-4 rounded-t-2xl md:rounded-lg'
|
? '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'
|
: 'rounded-lg w-full max-w-lg mx-4'
|
||||||
@@ -196,12 +196,12 @@ export function RestoreDialog({
|
|||||||
{/* Header */}
|
{/* Header */}
|
||||||
<div
|
<div
|
||||||
className={cn(
|
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 ? 'px-4 md:px-6 py-4' : 'px-6 py-4'
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
{responsive && (
|
{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')}>
|
<div className={cn(responsive && 'mt-2 md:mt-0')}>
|
||||||
<h2 className="text-lg font-semibold flex items-center gap-2">
|
<h2 className="text-lg font-semibold flex items-center gap-2">
|
||||||
@@ -209,7 +209,7 @@ export function RestoreDialog({
|
|||||||
Restore Checkpoint
|
Restore Checkpoint
|
||||||
</h2>
|
</h2>
|
||||||
{checkpoint && (
|
{checkpoint && (
|
||||||
<p className="text-xs text-gray-500">
|
<p className="text-xs text-fg-subtle">
|
||||||
{formatTime(checkpoint.timestamp)}
|
{formatTime(checkpoint.timestamp)}
|
||||||
</p>
|
</p>
|
||||||
)}
|
)}
|
||||||
@@ -284,7 +284,7 @@ export function RestoreDialog({
|
|||||||
{/* Restore Mode Selection */}
|
{/* Restore Mode Selection */}
|
||||||
{!files && (
|
{!files && (
|
||||||
<div className="space-y-2">
|
<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">
|
<div className="space-y-2">
|
||||||
{RESTORE_MODES.map((mode) => (
|
{RESTORE_MODES.map((mode) => (
|
||||||
<label
|
<label
|
||||||
@@ -293,7 +293,7 @@ export function RestoreDialog({
|
|||||||
'flex items-start gap-3 p-3 rounded-lg cursor-pointer transition-colors',
|
'flex items-start gap-3 p-3 rounded-lg cursor-pointer transition-colors',
|
||||||
selectedMode === mode.value
|
selectedMode === mode.value
|
||||||
? 'bg-primary-500/10 border border-primary-500/50'
|
? '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
|
<input
|
||||||
@@ -305,8 +305,8 @@ export function RestoreDialog({
|
|||||||
className="mt-1"
|
className="mt-1"
|
||||||
/>
|
/>
|
||||||
<div>
|
<div>
|
||||||
<div className="font-medium text-gray-200">{mode.label}</div>
|
<div className="font-medium text-fg-secondary">{mode.label}</div>
|
||||||
<div className="text-xs text-gray-500">{mode.description}</div>
|
<div className="text-xs text-fg-subtle">{mode.description}</div>
|
||||||
</div>
|
</div>
|
||||||
</label>
|
</label>
|
||||||
))}
|
))}
|
||||||
@@ -317,15 +317,15 @@ export function RestoreDialog({
|
|||||||
{/* Files to Restore */}
|
{/* Files to Restore */}
|
||||||
{previewResult && previewResult.restoredFiles.length > 0 && (
|
{previewResult && previewResult.restoredFiles.length > 0 && (
|
||||||
<div className="space-y-2">
|
<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} />
|
<FileText size={14} />
|
||||||
Files to restore ({previewResult.restoredFiles.length})
|
Files to restore ({previewResult.restoredFiles.length})
|
||||||
</label>
|
</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) => (
|
{previewResult.restoredFiles.map((file) => (
|
||||||
<div
|
<div
|
||||||
key={file}
|
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}
|
{file}
|
||||||
</div>
|
</div>
|
||||||
@@ -336,7 +336,7 @@ export function RestoreDialog({
|
|||||||
|
|
||||||
{/* Skip Safety Check Option */}
|
{/* Skip Safety Check Option */}
|
||||||
{safetyResult && !safetyResult.safe && (
|
{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
|
<input
|
||||||
type="checkbox"
|
type="checkbox"
|
||||||
checked={skipSafetyCheck}
|
checked={skipSafetyCheck}
|
||||||
@@ -353,7 +353,7 @@ export function RestoreDialog({
|
|||||||
{/* Footer */}
|
{/* Footer */}
|
||||||
<div
|
<div
|
||||||
className={cn(
|
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'
|
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 { fadeInUp, smoothTransition } from '../utils/animations';
|
||||||
import { listSessions, createSession, deleteSession, type Session } from '../api/client.js';
|
import { listSessions, createSession, deleteSession, type Session } from '../api/client.js';
|
||||||
import { SessionSkeleton } from './Skeleton';
|
import { SessionSkeleton } from './Skeleton';
|
||||||
|
import { ThemeToggleCompact } from './ThemeToggle';
|
||||||
|
|
||||||
interface SidebarProps {
|
interface SidebarProps {
|
||||||
currentSessionId: string | null;
|
currentSessionId: string | null;
|
||||||
@@ -112,10 +113,10 @@ export function Sidebar({
|
|||||||
transition={smoothTransition}
|
transition={smoothTransition}
|
||||||
className="p-6 text-center"
|
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">
|
<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-gray-500" />
|
<MessageCircle size={24} className="text-fg-subtle" />
|
||||||
</div>
|
</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
|
<motion.button
|
||||||
whileHover={{ scale: 1.02 }}
|
whileHover={{ scale: 1.02 }}
|
||||||
whileTap={{ scale: 0.98 }}
|
whileTap={{ scale: 0.98 }}
|
||||||
@@ -141,26 +142,26 @@ export function Sidebar({
|
|||||||
onClick={() => handleSelectSession(session.id)}
|
onClick={() => handleSelectSession(session.id)}
|
||||||
className={cn(
|
className={cn(
|
||||||
'flex items-center gap-2 p-3 rounded-lg cursor-pointer group',
|
'flex items-center gap-2 p-3 rounded-lg cursor-pointer group',
|
||||||
'hover:bg-gray-700 transition-colors',
|
'hover:bg-surface-muted transition-colors',
|
||||||
'active:bg-gray-600',
|
'active:bg-surface-emphasis',
|
||||||
currentSessionId === session.id && 'bg-gray-700'
|
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="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)}`}
|
{session.name || `Chat ${session.id.slice(0, 8)}`}
|
||||||
</div>
|
</div>
|
||||||
<div className="text-xs text-gray-500">{session.messageCount} messages</div>
|
<div className="text-xs text-fg-subtle">{session.messageCount} messages</div>
|
||||||
</div>
|
</div>
|
||||||
<motion.button
|
<motion.button
|
||||||
whileHover={{ scale: 1.1 }}
|
whileHover={{ scale: 1.1 }}
|
||||||
whileTap={{ scale: 0.9 }}
|
whileTap={{ scale: 0.9 }}
|
||||||
onClick={(e) => handleDelete(session.id, e)}
|
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"
|
aria-label="Delete session"
|
||||||
>
|
>
|
||||||
<Trash2 size={14} className="text-gray-400" />
|
<Trash2 size={14} className="text-fg-muted" />
|
||||||
</motion.button>
|
</motion.button>
|
||||||
</motion.div>
|
</motion.div>
|
||||||
)
|
)
|
||||||
@@ -179,13 +180,13 @@ export function Sidebar({
|
|||||||
const SidebarContent = () => (
|
const SidebarContent = () => (
|
||||||
<>
|
<>
|
||||||
{/* Header */}
|
{/* Header */}
|
||||||
<div className="p-4 border-b border-gray-700">
|
<div className="p-4 border-b border-line">
|
||||||
{responsive && (
|
{responsive && (
|
||||||
<div className="flex items-center justify-between mb-3 md:hidden">
|
<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
|
<button
|
||||||
onClick={() => setIsOpen(false)}
|
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"
|
aria-label="Close menu"
|
||||||
>
|
>
|
||||||
<X size={20} />
|
<X size={20} />
|
||||||
@@ -196,7 +197,7 @@ export function Sidebar({
|
|||||||
whileHover={{ scale: 1.02 }}
|
whileHover={{ scale: 1.02 }}
|
||||||
whileTap={{ scale: 0.98 }}
|
whileTap={{ scale: 0.98 }}
|
||||||
onClick={handleCreate}
|
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} />
|
<Plus size={18} />
|
||||||
<span>New Chat</span>
|
<span>New Chat</span>
|
||||||
@@ -221,8 +222,9 @@ export function Sidebar({
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Footer */}
|
{/* Footer */}
|
||||||
<div className="p-4 border-t border-gray-700 text-center text-xs text-gray-500">
|
<div className="p-4 border-t border-line flex items-center justify-between">
|
||||||
AI Assistant v1.0
|
<span className="text-xs text-fg-subtle">AI Assistant v1.0</span>
|
||||||
|
<ThemeToggleCompact />
|
||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
@@ -230,7 +232,7 @@ export function Sidebar({
|
|||||||
// 非响应式模式
|
// 非响应式模式
|
||||||
if (!responsive) {
|
if (!responsive) {
|
||||||
return (
|
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 />
|
<SidebarContent />
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
@@ -244,7 +246,7 @@ export function Sidebar({
|
|||||||
whileHover={{ scale: 1.05 }}
|
whileHover={{ scale: 1.05 }}
|
||||||
whileTap={{ scale: 0.95 }}
|
whileTap={{ scale: 0.95 }}
|
||||||
onClick={() => setIsOpen(true)}
|
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"
|
aria-label="Open menu"
|
||||||
>
|
>
|
||||||
<Menu size={20} />
|
<Menu size={20} />
|
||||||
@@ -269,13 +271,13 @@ export function Sidebar({
|
|||||||
initial={false}
|
initial={false}
|
||||||
animate={{ x: isOpen ? 0 : '-100%' }}
|
animate={{ x: isOpen ? 0 : '-100%' }}
|
||||||
transition={{ type: 'spring', stiffness: 300, damping: 30 }}
|
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 />
|
<SidebarContent />
|
||||||
</motion.div>
|
</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 />
|
<SidebarContent />
|
||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ interface SkeletonProps extends React.HTMLAttributes<HTMLDivElement> {}
|
|||||||
export function Skeleton({ className, ...props }: SkeletonProps) {
|
export function Skeleton({ className, ...props }: SkeletonProps) {
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className={cn('animate-pulse rounded-md bg-gray-700', className)}
|
className={cn('animate-pulse rounded-md bg-surface-muted', className)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
@@ -14,11 +14,11 @@ export function Skeleton({ className, ...props }: SkeletonProps) {
|
|||||||
export function MessageSkeleton() {
|
export function MessageSkeleton() {
|
||||||
return (
|
return (
|
||||||
<div className="flex gap-3 animate-pulse">
|
<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="flex-1 space-y-2">
|
||||||
<div className="h-4 bg-gray-700 rounded w-1/4" />
|
<div className="h-4 bg-surface-muted rounded w-1/4" />
|
||||||
<div className="h-4 bg-gray-700 rounded w-3/4" />
|
<div className="h-4 bg-surface-muted 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/2" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
@@ -27,7 +27,7 @@ export function MessageSkeleton() {
|
|||||||
export function SessionSkeleton() {
|
export function SessionSkeleton() {
|
||||||
return (
|
return (
|
||||||
<div className="px-2 py-2 animate-pulse">
|
<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>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -36,8 +36,8 @@ export function FileSkeleton() {
|
|||||||
return (
|
return (
|
||||||
<div className="px-3 py-2 animate-pulse">
|
<div className="px-3 py-2 animate-pulse">
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<div className="w-4 h-4 bg-gray-700 rounded" />
|
<div className="w-4 h-4 bg-surface-muted rounded" />
|
||||||
<div className="h-4 bg-gray-700 rounded flex-1" />
|
<div className="h-4 bg-surface-muted rounded flex-1" />
|
||||||
</div>
|
</div>
|
||||||
</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
|
// Hooks
|
||||||
export { useChat } from './hooks/useChat.js';
|
export { useChat } from './hooks/useChat.js';
|
||||||
export { useCommands } from './hooks/useCommands.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: {
|
variants: {
|
||||||
variant: {
|
variant: {
|
||||||
default: 'bg-primary-600 text-white hover:bg-primary-700',
|
default: 'bg-primary-600 text-white hover:bg-primary-700',
|
||||||
secondary: 'bg-gray-700 text-gray-100 hover:bg-gray-600',
|
secondary: 'bg-surface-muted text-fg hover:bg-surface-emphasis',
|
||||||
ghost: 'hover:bg-gray-800 text-gray-300',
|
ghost: 'hover:bg-surface-subtle text-fg-muted',
|
||||||
destructive: 'bg-red-600 text-white hover:bg-red-700',
|
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: {
|
size: {
|
||||||
default: 'h-10 px-4 py-2',
|
default: 'h-10 px-4 py-2',
|
||||||
|
|||||||
@@ -32,7 +32,7 @@ export const DialogContent = forwardRef<
|
|||||||
<DialogPrimitive.Content
|
<DialogPrimitive.Content
|
||||||
ref={ref}
|
ref={ref}
|
||||||
className={cn(
|
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
|
className
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
@@ -76,7 +76,7 @@ export const DialogTitle = forwardRef<
|
|||||||
>(({ className, ...props }, ref) => (
|
>(({ className, ...props }, ref) => (
|
||||||
<DialogPrimitive.Title
|
<DialogPrimitive.Title
|
||||||
ref={ref}
|
ref={ref}
|
||||||
className={cn('text-lg font-semibold text-gray-100', className)}
|
className={cn('text-lg font-semibold text-fg', className)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
));
|
));
|
||||||
@@ -88,7 +88,7 @@ export const DialogDescription = forwardRef<
|
|||||||
>(({ className, ...props }, ref) => (
|
>(({ className, ...props }, ref) => (
|
||||||
<DialogPrimitive.Description
|
<DialogPrimitive.Description
|
||||||
ref={ref}
|
ref={ref}
|
||||||
className={cn('text-sm text-gray-400', className)}
|
className={cn('text-sm text-fg-muted', className)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
));
|
));
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ export const Input = forwardRef<HTMLInputElement, InputProps>(
|
|||||||
<input
|
<input
|
||||||
type={type}
|
type={type}
|
||||||
className={cn(
|
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
|
className
|
||||||
)}
|
)}
|
||||||
ref={ref}
|
ref={ref}
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ export const SelectTrigger = forwardRef<
|
|||||||
<SelectPrimitive.Trigger
|
<SelectPrimitive.Trigger
|
||||||
ref={ref}
|
ref={ref}
|
||||||
className={cn(
|
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
|
className
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
@@ -63,7 +63,7 @@ export const SelectContent = forwardRef<
|
|||||||
<SelectPrimitive.Content
|
<SelectPrimitive.Content
|
||||||
ref={ref}
|
ref={ref}
|
||||||
className={cn(
|
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' &&
|
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',
|
'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
|
className
|
||||||
@@ -106,7 +106,7 @@ export const SelectItem = forwardRef<
|
|||||||
<SelectPrimitive.Item
|
<SelectPrimitive.Item
|
||||||
ref={ref}
|
ref={ref}
|
||||||
className={cn(
|
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
|
className
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
@@ -127,7 +127,7 @@ export const SelectSeparator = forwardRef<
|
|||||||
>(({ className, ...props }, ref) => (
|
>(({ className, ...props }, ref) => (
|
||||||
<SelectPrimitive.Separator
|
<SelectPrimitive.Separator
|
||||||
ref={ref}
|
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}
|
{...props}
|
||||||
/>
|
/>
|
||||||
));
|
));
|
||||||
|
|||||||
@@ -11,10 +11,10 @@ export const Slider = forwardRef<
|
|||||||
className={cn('relative flex w-full touch-none select-none items-center', className)}
|
className={cn('relative flex w-full touch-none select-none items-center', className)}
|
||||||
{...props}
|
{...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.Range className="absolute h-full bg-primary-500" />
|
||||||
</SliderPrimitive.Track>
|
</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>
|
</SliderPrimitive.Root>
|
||||||
));
|
));
|
||||||
Slider.displayName = SliderPrimitive.Root.displayName;
|
Slider.displayName = SliderPrimitive.Root.displayName;
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ export const Switch = forwardRef<
|
|||||||
>(({ className, ...props }, ref) => (
|
>(({ className, ...props }, ref) => (
|
||||||
<SwitchPrimitive.Root
|
<SwitchPrimitive.Root
|
||||||
className={cn(
|
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
|
className
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ export const TooltipContent = forwardRef<
|
|||||||
ref={ref}
|
ref={ref}
|
||||||
sideOffset={sideOffset}
|
sideOffset={sideOffset}
|
||||||
className={cn(
|
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
|
className
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
|
|||||||
@@ -5,7 +5,54 @@
|
|||||||
* 注意:使用此文件时,宿主项目需要配置 Tailwind CSS
|
* 注意:使用此文件时,宿主项目需要配置 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)) {
|
@supports (padding-bottom: env(safe-area-inset-bottom)) {
|
||||||
.safe-area-pb {
|
.safe-area-pb {
|
||||||
padding-bottom: calc(env(safe-area-inset-bottom) + 0.5rem);
|
padding-bottom: calc(env(safe-area-inset-bottom) + 0.5rem);
|
||||||
@@ -26,7 +73,8 @@ html {
|
|||||||
touch-action: pan-y;
|
touch-action: pan-y;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Custom scrollbar */
|
/* ============ Custom scrollbar ============ */
|
||||||
|
|
||||||
::-webkit-scrollbar {
|
::-webkit-scrollbar {
|
||||||
width: 8px;
|
width: 8px;
|
||||||
height: 8px;
|
height: 8px;
|
||||||
@@ -37,29 +85,30 @@ html {
|
|||||||
}
|
}
|
||||||
|
|
||||||
::-webkit-scrollbar-thumb {
|
::-webkit-scrollbar-thumb {
|
||||||
background: #4b5563;
|
background: var(--scrollbar-thumb);
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
}
|
}
|
||||||
|
|
||||||
::-webkit-scrollbar-thumb:hover {
|
::-webkit-scrollbar-thumb:hover {
|
||||||
background: #6b7280;
|
background: var(--scrollbar-thumb-hover);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Message content */
|
/* ============ Message content ============ */
|
||||||
|
|
||||||
.message-content {
|
.message-content {
|
||||||
max-width: none;
|
max-width: none;
|
||||||
color: #f3f4f6; /* text-gray-100 */
|
color: rgb(var(--color-text-primary));
|
||||||
}
|
}
|
||||||
|
|
||||||
.message-content pre {
|
.message-content pre {
|
||||||
background: #1f2937; /* bg-gray-800 */
|
background: rgb(var(--color-code-bg));
|
||||||
border-radius: 0.5rem;
|
border-radius: 0.5rem;
|
||||||
padding: 1rem;
|
padding: 1rem;
|
||||||
overflow-x: auto;
|
overflow-x: auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
.message-content code {
|
.message-content code {
|
||||||
background: #1f2937; /* bg-gray-800 */
|
background: rgb(var(--color-code-bg));
|
||||||
padding: 0.125rem 0.375rem;
|
padding: 0.125rem 0.375rem;
|
||||||
border-radius: 0.25rem;
|
border-radius: 0.25rem;
|
||||||
font-size: 0.875rem;
|
font-size: 0.875rem;
|
||||||
@@ -70,7 +119,8 @@ html {
|
|||||||
padding: 0;
|
padding: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Typing indicator */
|
/* ============ Typing indicator ============ */
|
||||||
|
|
||||||
.typing-indicator {
|
.typing-indicator {
|
||||||
display: flex;
|
display: flex;
|
||||||
gap: 4px;
|
gap: 4px;
|
||||||
@@ -79,7 +129,7 @@ html {
|
|||||||
.typing-indicator span {
|
.typing-indicator span {
|
||||||
width: 8px;
|
width: 8px;
|
||||||
height: 8px;
|
height: 8px;
|
||||||
background: #6b7280;
|
background: rgb(var(--color-text-muted));
|
||||||
border-radius: 50%;
|
border-radius: 50%;
|
||||||
animation: typing 1.4s infinite ease-in-out;
|
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>
|
<!doctype html>
|
||||||
<html lang="en" class="dark">
|
<html lang="en">
|
||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8" />
|
<meta charset="UTF-8" />
|
||||||
<link rel="icon" type="image/svg+xml" href="/icon.svg" />
|
<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" />
|
<meta name="viewport" content="width=device-width, initial-scale=1.0, viewport-fit=cover, maximum-scale=1.0, user-scalable=no" />
|
||||||
|
|
||||||
<!-- PWA Meta Tags -->
|
<!-- 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="mobile-web-app-capable" content="yes" />
|
||||||
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent" />
|
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent" />
|
||||||
<meta name="apple-mobile-web-app-title" content="AI Assistant" />
|
<meta name="apple-mobile-web-app-title" content="AI Assistant" />
|
||||||
@@ -19,8 +20,19 @@
|
|||||||
<link rel="manifest" href="/manifest.json" />
|
<link rel="manifest" href="/manifest.json" />
|
||||||
|
|
||||||
<title>AI Assistant</title>
|
<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>
|
</head>
|
||||||
<body class="bg-gray-900 text-gray-100">
|
<body class="bg-surface-base text-fg">
|
||||||
<div id="root"></div>
|
<div id="root"></div>
|
||||||
<script type="module" src="/src/main.tsx"></script>
|
<script type="module" src="/src/main.tsx"></script>
|
||||||
</body>
|
</body>
|
||||||
|
|||||||
+15
-10
@@ -16,6 +16,7 @@ import {
|
|||||||
CheckpointPanel,
|
CheckpointPanel,
|
||||||
ProvidersPanel,
|
ProvidersPanel,
|
||||||
Toaster,
|
Toaster,
|
||||||
|
ThemeProvider,
|
||||||
listSessions,
|
listSessions,
|
||||||
createSession,
|
createSession,
|
||||||
type Session,
|
type Session,
|
||||||
@@ -94,17 +95,20 @@ export function App() {
|
|||||||
|
|
||||||
if (isInitializing) {
|
if (isInitializing) {
|
||||||
return (
|
return (
|
||||||
<div className="h-screen flex items-center justify-center bg-gray-900">
|
<ThemeProvider>
|
||||||
|
<div className="h-screen flex items-center justify-center bg-surface-base">
|
||||||
<div className="text-center">
|
<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" />
|
<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>
|
<p className="text-fg-muted">Initializing...</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</ThemeProvider>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="h-screen flex bg-gray-900">
|
<ThemeProvider>
|
||||||
|
<div className="h-screen flex bg-surface-base">
|
||||||
<Sidebar
|
<Sidebar
|
||||||
currentSessionId={currentSessionId}
|
currentSessionId={currentSessionId}
|
||||||
onSelectSession={handleSelectSession}
|
onSelectSession={handleSelectSession}
|
||||||
@@ -136,7 +140,7 @@ export function App() {
|
|||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
<div className="flex-1 flex items-center justify-center h-full">
|
<div className="flex-1 flex items-center justify-center h-full">
|
||||||
<p className="text-gray-400">Select or create a session</p>
|
<p className="text-fg-muted">Select or create a session</p>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
@@ -145,12 +149,12 @@ export function App() {
|
|||||||
{showFileBrowser && (
|
{showFileBrowser && (
|
||||||
<>
|
<>
|
||||||
{/* 移动端: 全屏覆盖 */}
|
{/* 移动端: 全屏覆盖 */}
|
||||||
<div className="fixed inset-0 z-50 bg-gray-900 md:hidden">
|
<div className="fixed inset-0 z-50 bg-surface-base md:hidden">
|
||||||
<div className="flex items-center justify-between p-3 border-b border-gray-700">
|
<div className="flex items-center justify-between p-3 border-b border-line">
|
||||||
<span className="text-lg font-semibold">Files</span>
|
<span className="text-lg font-semibold text-fg">Files</span>
|
||||||
<button
|
<button
|
||||||
onClick={() => setShowFileBrowser(false)}
|
onClick={() => setShowFileBrowser(false)}
|
||||||
className="p-2 rounded-lg bg-gray-700 text-gray-300 hover:bg-gray-600"
|
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">
|
<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" />
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M6 18L18 6M6 6l12 12" />
|
||||||
@@ -167,7 +171,7 @@ export function App() {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* 桌面端: 侧边栏 */}
|
{/* 桌面端: 侧边栏 */}
|
||||||
<div className="hidden md:block w-1/2 border-l border-gray-700">
|
<div className="hidden md:block w-1/2 border-l border-line">
|
||||||
<FileBrowser
|
<FileBrowser
|
||||||
onFileSelect={(path, _content) => {
|
onFileSelect={(path, _content) => {
|
||||||
console.log('Selected file:', path);
|
console.log('Selected file:', path);
|
||||||
@@ -202,7 +206,7 @@ export function App() {
|
|||||||
{/* 移动端底部文件按钮 */}
|
{/* 移动端底部文件按钮 */}
|
||||||
<button
|
<button
|
||||||
onClick={() => setShowFileBrowser(true)}
|
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"
|
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"
|
title="Browse Files"
|
||||||
>
|
>
|
||||||
<svg className="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
<svg className="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
@@ -218,5 +222,6 @@ export function App() {
|
|||||||
{/* Toast 通知 */}
|
{/* Toast 通知 */}
|
||||||
<Toaster />
|
<Toaster />
|
||||||
</div>
|
</div>
|
||||||
|
</ThemeProvider>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -97,11 +97,11 @@ export function ChatPage({
|
|||||||
<MessageSquare size={32} className="text-primary-400" />
|
<MessageSquare size={32} className="text-primary-400" />
|
||||||
</div>
|
</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
|
Start a conversation
|
||||||
</h2>
|
</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.
|
Ask me anything about coding, debugging, or software development.
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
@@ -112,7 +112,7 @@ export function ChatPage({
|
|||||||
whileHover={{ scale: 1.02 }}
|
whileHover={{ scale: 1.02 }}
|
||||||
whileTap={{ scale: 0.98 }}
|
whileTap={{ scale: 0.98 }}
|
||||||
onClick={() => sendMessage(suggestion)}
|
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}"
|
"{suggestion}"
|
||||||
</motion.button>
|
</motion.button>
|
||||||
@@ -148,8 +148,8 @@ export function ChatPage({
|
|||||||
return (
|
return (
|
||||||
<div className="flex-1 flex flex-col h-screen">
|
<div className="flex-1 flex flex-col h-screen">
|
||||||
{/* Header */}
|
{/* Header */}
|
||||||
<div className="flex items-center justify-between px-4 md:px-6 py-3 border-b border-gray-700 bg-gray-800">
|
<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">Chat</h1>
|
<h1 className="text-lg font-medium text-fg">Chat</h1>
|
||||||
<div className="flex items-center gap-3">
|
<div className="flex items-center gap-3">
|
||||||
{/* 上下文使用情况 - 紧凑模式 */}
|
{/* 上下文使用情况 - 紧凑模式 */}
|
||||||
{sessionId && (
|
{sessionId && (
|
||||||
@@ -166,14 +166,14 @@ export function ChatPage({
|
|||||||
|
|
||||||
{/* 工具栏按钮 */}
|
{/* 工具栏按钮 */}
|
||||||
{(onOpenConfig || onToggleFileBrowser || onOpenCommands || onOpenMCP || onOpenHooks || onOpenAgents || onOpenCheckpoints || onOpenProviders) && (
|
{(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 按钮 */}
|
{/* Checkpoints 按钮 */}
|
||||||
{onOpenCheckpoints && (
|
{onOpenCheckpoints && (
|
||||||
<motion.button
|
<motion.button
|
||||||
whileHover={{ scale: 1.1 }}
|
whileHover={{ scale: 1.1 }}
|
||||||
whileTap={{ scale: 0.9 }}
|
whileTap={{ scale: 0.9 }}
|
||||||
onClick={onOpenCheckpoints}
|
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"
|
title="Checkpoints"
|
||||||
>
|
>
|
||||||
<History size={20} />
|
<History size={20} />
|
||||||
@@ -186,7 +186,7 @@ export function ChatPage({
|
|||||||
whileHover={{ scale: 1.1 }}
|
whileHover={{ scale: 1.1 }}
|
||||||
whileTap={{ scale: 0.9 }}
|
whileTap={{ scale: 0.9 }}
|
||||||
onClick={onOpenProviders}
|
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"
|
title="Model Providers"
|
||||||
>
|
>
|
||||||
<Server size={20} />
|
<Server size={20} />
|
||||||
@@ -199,7 +199,7 @@ export function ChatPage({
|
|||||||
whileHover={{ scale: 1.1 }}
|
whileHover={{ scale: 1.1 }}
|
||||||
whileTap={{ scale: 0.9 }}
|
whileTap={{ scale: 0.9 }}
|
||||||
onClick={onOpenAgents}
|
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"
|
title="Agent Presets"
|
||||||
>
|
>
|
||||||
<Bot size={20} />
|
<Bot size={20} />
|
||||||
@@ -212,7 +212,7 @@ export function ChatPage({
|
|||||||
whileHover={{ scale: 1.1 }}
|
whileHover={{ scale: 1.1 }}
|
||||||
whileTap={{ scale: 0.9 }}
|
whileTap={{ scale: 0.9 }}
|
||||||
onClick={onOpenHooks}
|
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"
|
title="Hooks"
|
||||||
>
|
>
|
||||||
<Zap size={20} />
|
<Zap size={20} />
|
||||||
@@ -225,7 +225,7 @@ export function ChatPage({
|
|||||||
whileHover={{ scale: 1.1 }}
|
whileHover={{ scale: 1.1 }}
|
||||||
whileTap={{ scale: 0.9 }}
|
whileTap={{ scale: 0.9 }}
|
||||||
onClick={onOpenMCP}
|
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"
|
title="MCP Servers"
|
||||||
>
|
>
|
||||||
<Plug size={20} />
|
<Plug size={20} />
|
||||||
@@ -238,7 +238,7 @@ export function ChatPage({
|
|||||||
whileHover={{ scale: 1.1 }}
|
whileHover={{ scale: 1.1 }}
|
||||||
whileTap={{ scale: 0.9 }}
|
whileTap={{ scale: 0.9 }}
|
||||||
onClick={onOpenCommands}
|
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"
|
title="Commands"
|
||||||
>
|
>
|
||||||
<Terminal size={20} />
|
<Terminal size={20} />
|
||||||
@@ -251,7 +251,7 @@ export function ChatPage({
|
|||||||
whileHover={{ scale: 1.1 }}
|
whileHover={{ scale: 1.1 }}
|
||||||
whileTap={{ scale: 0.9 }}
|
whileTap={{ scale: 0.9 }}
|
||||||
onClick={onOpenConfig}
|
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"
|
title="Settings"
|
||||||
>
|
>
|
||||||
<Settings size={20} />
|
<Settings size={20} />
|
||||||
@@ -267,7 +267,7 @@ export function ChatPage({
|
|||||||
className={`hidden md:block p-1.5 rounded-lg transition-colors ${
|
className={`hidden md:block p-1.5 rounded-lg transition-colors ${
|
||||||
showFileBrowser
|
showFileBrowser
|
||||||
? 'text-blue-400 bg-blue-500/20'
|
? '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'}
|
title={showFileBrowser ? 'Hide Files' : 'Show Files'}
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -9,6 +9,26 @@ export default {
|
|||||||
theme: {
|
theme: {
|
||||||
extend: {
|
extend: {
|
||||||
colors: {
|
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: {
|
primary: {
|
||||||
50: '#f0f9ff',
|
50: '#f0f9ff',
|
||||||
100: '#e0f2fe',
|
100: '#e0f2fe',
|
||||||
|
|||||||
Reference in New Issue
Block a user