feat(ui): 重构模式选择器为点击切换交互
- 将 Build/Plan 下拉框和 Auto-approve 开关合并为三种模式 - Plan: 调用 plan agent,只读分析 - Ask: 调用 general agent,执行操作前需确认 - Auto: 调用 general agent,自动执行无需确认 - 点击按钮即可循环切换:Ask → Auto → Plan - 每种模式有独特的颜色和图标便于区分
This commit is contained in:
@@ -1,16 +1,20 @@
|
||||
/**
|
||||
* Agent Mode Selector Component
|
||||
*
|
||||
* 在 ChatInput 左侧显示,用于切换 Build/Plan 模式和控制 Auto-approve
|
||||
* 在 ChatInput 左侧显示,点击切换三种模式:
|
||||
* - Plan: 调用 plan agent,只读分析
|
||||
* - Ask: 调用 general agent,执行操作需确认
|
||||
* - Auto: 调用 general agent,自动执行无需确认
|
||||
*/
|
||||
|
||||
import { useState, useRef, useEffect } from 'react';
|
||||
import { Hammer, FileSearch, ChevronDown, Check } from 'lucide-react';
|
||||
import { FileSearch, MessageCircleQuestion, Zap } from 'lucide-react';
|
||||
import clsx from 'clsx';
|
||||
import { Switch } from '../primitives/Switch.js';
|
||||
|
||||
export type AgentModeType = 'build' | 'plan';
|
||||
|
||||
/** 组合模式:Plan / Ask / Auto */
|
||||
export type CombinedModeType = 'plan' | 'ask' | 'auto';
|
||||
|
||||
interface AgentModeSelectorProps {
|
||||
/** 当前模式 */
|
||||
mode: AgentModeType;
|
||||
@@ -24,23 +28,75 @@ interface AgentModeSelectorProps {
|
||||
disabled?: boolean;
|
||||
}
|
||||
|
||||
const modeConfig = {
|
||||
build: {
|
||||
label: 'Build',
|
||||
icon: Hammer,
|
||||
color: 'text-blue-500',
|
||||
bgColor: 'bg-blue-500/10',
|
||||
description: '可执行代码修改',
|
||||
},
|
||||
const modeConfig: Record<
|
||||
CombinedModeType,
|
||||
{
|
||||
label: string;
|
||||
icon: typeof FileSearch;
|
||||
color: string;
|
||||
bgColor: string;
|
||||
borderColor: string;
|
||||
hoverBg: string;
|
||||
description: string;
|
||||
}
|
||||
> = {
|
||||
plan: {
|
||||
label: 'Plan',
|
||||
icon: FileSearch,
|
||||
color: 'text-purple-500',
|
||||
bgColor: 'bg-purple-500/10',
|
||||
description: '只读模式,仅分析',
|
||||
borderColor: 'border-purple-500/30',
|
||||
hoverBg: 'hover:bg-purple-500/15',
|
||||
description: '只读模式,仅分析和规划',
|
||||
},
|
||||
ask: {
|
||||
label: 'Ask',
|
||||
icon: MessageCircleQuestion,
|
||||
color: 'text-blue-500',
|
||||
bgColor: 'bg-blue-500/10',
|
||||
borderColor: 'border-blue-500/30',
|
||||
hoverBg: 'hover:bg-blue-500/15',
|
||||
description: '执行操作前需要确认',
|
||||
},
|
||||
auto: {
|
||||
label: 'Auto',
|
||||
icon: Zap,
|
||||
color: 'text-orange-500',
|
||||
bgColor: 'bg-orange-500/10',
|
||||
borderColor: 'border-orange-500/30',
|
||||
hoverBg: 'hover:bg-orange-500/15',
|
||||
description: '自动执行,无需确认',
|
||||
},
|
||||
};
|
||||
|
||||
// 模式切换顺序:Ask -> Auto -> Plan -> Ask ...
|
||||
const modeOrder: CombinedModeType[] = ['ask', 'auto', 'plan'];
|
||||
|
||||
/**
|
||||
* 根据 agentMode 和 autoApprove 计算当前组合模式
|
||||
*/
|
||||
function getCombinedMode(agentMode: AgentModeType, autoApprove: boolean): CombinedModeType {
|
||||
if (agentMode === 'plan') return 'plan';
|
||||
return autoApprove ? 'auto' : 'ask';
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据组合模式计算 agentMode 和 autoApprove
|
||||
*/
|
||||
function getAgentConfig(combinedMode: CombinedModeType): {
|
||||
agentMode: AgentModeType;
|
||||
autoApprove: boolean;
|
||||
} {
|
||||
switch (combinedMode) {
|
||||
case 'plan':
|
||||
return { agentMode: 'plan', autoApprove: false };
|
||||
case 'ask':
|
||||
return { agentMode: 'build', autoApprove: false };
|
||||
case 'auto':
|
||||
return { agentMode: 'build', autoApprove: true };
|
||||
}
|
||||
}
|
||||
|
||||
export function AgentModeSelector({
|
||||
mode,
|
||||
onModeChange,
|
||||
@@ -48,138 +104,49 @@ export function AgentModeSelector({
|
||||
onAutoApproveChange,
|
||||
disabled = false,
|
||||
}: AgentModeSelectorProps) {
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
const dropdownRef = useRef<HTMLDivElement>(null);
|
||||
// 计算当前组合模式
|
||||
const currentCombinedMode = getCombinedMode(mode, autoApprove);
|
||||
const currentConfig = modeConfig[currentCombinedMode];
|
||||
const ModeIcon = currentConfig.icon;
|
||||
|
||||
const currentMode = modeConfig[mode];
|
||||
const ModeIcon = currentMode.icon;
|
||||
// 点击切换到下一个模式
|
||||
const handleToggle = () => {
|
||||
if (disabled) return;
|
||||
|
||||
// 点击外部关闭下拉菜单
|
||||
useEffect(() => {
|
||||
function handleClickOutside(event: MouseEvent) {
|
||||
if (dropdownRef.current && !dropdownRef.current.contains(event.target as Node)) {
|
||||
setIsOpen(false);
|
||||
}
|
||||
}
|
||||
// 找到当前模式在顺序中的索引
|
||||
const currentIndex = modeOrder.indexOf(currentCombinedMode);
|
||||
// 切换到下一个模式
|
||||
const nextIndex = (currentIndex + 1) % modeOrder.length;
|
||||
const nextMode = modeOrder[nextIndex];
|
||||
|
||||
if (isOpen) {
|
||||
document.addEventListener('mousedown', handleClickOutside);
|
||||
return () => document.removeEventListener('mousedown', handleClickOutside);
|
||||
}
|
||||
}, [isOpen]);
|
||||
|
||||
// 键盘导航
|
||||
useEffect(() => {
|
||||
function handleKeyDown(event: KeyboardEvent) {
|
||||
if (!isOpen) return;
|
||||
|
||||
if (event.key === 'Escape') {
|
||||
setIsOpen(false);
|
||||
} else if (event.key === 'ArrowDown' || event.key === 'ArrowUp') {
|
||||
event.preventDefault();
|
||||
// 切换模式
|
||||
onModeChange(mode === 'build' ? 'plan' : 'build');
|
||||
} else if (event.key === 'Enter') {
|
||||
setIsOpen(false);
|
||||
}
|
||||
}
|
||||
|
||||
document.addEventListener('keydown', handleKeyDown);
|
||||
return () => document.removeEventListener('keydown', handleKeyDown);
|
||||
}, [isOpen, mode, onModeChange]);
|
||||
|
||||
const handleModeSelect = (newMode: AgentModeType) => {
|
||||
onModeChange(newMode);
|
||||
setIsOpen(false);
|
||||
// 获取新的配置并更新
|
||||
const { agentMode, autoApprove: newAutoApprove } = getAgentConfig(nextMode);
|
||||
onModeChange(agentMode);
|
||||
onAutoApproveChange(newAutoApprove);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="flex items-center gap-2">
|
||||
{/* 模式选择器 */}
|
||||
<div ref={dropdownRef} className="relative">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => !disabled && setIsOpen(!isOpen)}
|
||||
disabled={disabled}
|
||||
className={clsx(
|
||||
'flex items-center gap-1.5 px-2.5 py-1.5 rounded-lg border transition-colors',
|
||||
'text-sm font-medium',
|
||||
disabled
|
||||
? 'opacity-50 cursor-not-allowed border-line bg-surface-subtle'
|
||||
: 'border-line hover:border-line-strong hover:bg-surface-subtle cursor-pointer',
|
||||
currentMode.color
|
||||
)}
|
||||
>
|
||||
<ModeIcon size={16} />
|
||||
<span className="hidden sm:inline">{currentMode.label}</span>
|
||||
<ChevronDown
|
||||
size={14}
|
||||
className={clsx(
|
||||
'transition-transform text-fg-muted',
|
||||
isOpen && 'rotate-180'
|
||||
)}
|
||||
/>
|
||||
</button>
|
||||
|
||||
{/* 下拉菜单 */}
|
||||
{isOpen && (
|
||||
<div className="absolute bottom-full left-0 mb-1 w-48 py-1 bg-surface-base border border-line rounded-lg shadow-lg z-50">
|
||||
{(Object.entries(modeConfig) as [AgentModeType, typeof modeConfig.build][]).map(
|
||||
([modeKey, config]) => {
|
||||
const Icon = config.icon;
|
||||
const isSelected = modeKey === mode;
|
||||
|
||||
return (
|
||||
<button
|
||||
key={modeKey}
|
||||
type="button"
|
||||
onClick={() => handleModeSelect(modeKey)}
|
||||
className={clsx(
|
||||
'w-full flex items-center gap-2.5 px-3 py-2 text-left transition-colors',
|
||||
isSelected
|
||||
? `${config.bgColor} ${config.color}`
|
||||
: 'hover:bg-surface-subtle text-fg'
|
||||
)}
|
||||
>
|
||||
<Icon size={16} className={config.color} />
|
||||
<div className="flex-1">
|
||||
<div className="text-sm font-medium">{config.label}</div>
|
||||
<div className="text-xs text-fg-muted">{config.description}</div>
|
||||
</div>
|
||||
{isSelected && <Check size={16} className={config.color} />}
|
||||
</button>
|
||||
);
|
||||
}
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Auto-approve 开关 - 仅在 Build 模式下显示 */}
|
||||
{mode === 'build' && (
|
||||
<div
|
||||
className={clsx(
|
||||
'flex items-center gap-1.5 px-2 py-1 rounded-lg transition-colors',
|
||||
autoApprove ? 'bg-orange-500/10' : 'bg-transparent'
|
||||
)}
|
||||
title={autoApprove ? '自动授权已开启:文件写入/编辑无需确认' : '点击开启自动授权'}
|
||||
>
|
||||
<span
|
||||
className={clsx(
|
||||
'text-xs hidden sm:inline transition-colors',
|
||||
autoApprove ? 'text-orange-500' : 'text-fg-muted'
|
||||
)}
|
||||
>
|
||||
Auto Edit
|
||||
</span>
|
||||
<Switch
|
||||
checked={autoApprove}
|
||||
onCheckedChange={onAutoApproveChange}
|
||||
disabled={disabled}
|
||||
className="scale-75 origin-left"
|
||||
/>
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
onClick={handleToggle}
|
||||
disabled={disabled}
|
||||
title={`${currentConfig.label}: ${currentConfig.description}(点击切换)`}
|
||||
className={clsx(
|
||||
'flex items-center gap-1.5 px-2.5 py-1.5 rounded-lg border transition-all duration-200',
|
||||
'text-sm font-medium',
|
||||
disabled
|
||||
? 'opacity-50 cursor-not-allowed border-line bg-surface-subtle'
|
||||
: [
|
||||
currentConfig.bgColor,
|
||||
currentConfig.borderColor,
|
||||
currentConfig.hoverBg,
|
||||
'cursor-pointer active:scale-95',
|
||||
],
|
||||
currentConfig.color
|
||||
)}
|
||||
</div>
|
||||
>
|
||||
<ModeIcon size={16} />
|
||||
<span className="hidden sm:inline">{currentConfig.label}</span>
|
||||
</button>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user