feat(ui): 实现 Agent 模式切换和 Auto Edit 功能
- 添加 AgentModeSelector 组件,支持 Build/Plan 模式切换 - Build 模式下显示 Auto Edit 开关,自动授权文件写入/编辑 - 扩展 useChat hook 添加会话级别的 agentMode/autoApprove 状态 - 服务端支持解析和应用 Agent 模式配置 - 权限处理器实现 auto-approve 检查(仅 write/edit,不含 delete)
This commit is contained in:
@@ -728,8 +728,31 @@ export class Agent {
|
||||
|
||||
/**
|
||||
* 切换 Agent 模式
|
||||
* @param agent AgentInfo 对象或模式字符串 ('build'/'plan')
|
||||
*/
|
||||
setAgentMode(agent: AgentInfo | null): void {
|
||||
setAgentMode(agent: AgentInfo | 'build' | 'plan' | null): void {
|
||||
// 如果是字符串模式,从 registry 获取预设
|
||||
if (typeof agent === 'string') {
|
||||
const presetAgent = agentRegistry.get(agent);
|
||||
if (presetAgent) {
|
||||
this.currentAgentMode = presetAgent;
|
||||
if (presetAgent.prompt) {
|
||||
this.config = {
|
||||
...this.config,
|
||||
systemPrompt: presetAgent.prompt,
|
||||
};
|
||||
}
|
||||
} else {
|
||||
// 如果找不到预设,回退到默认模式
|
||||
this.currentAgentMode = null;
|
||||
this.config = {
|
||||
...this.config,
|
||||
systemPrompt: this.originalSystemPrompt,
|
||||
};
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
this.currentAgentMode = agent;
|
||||
|
||||
if (agent?.prompt) {
|
||||
@@ -747,6 +770,35 @@ export class Agent {
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Auto-approve 功能(用于前端 Build 模式的自动授权)
|
||||
// ============================================================================
|
||||
|
||||
/** 临时自动授权配置 */
|
||||
private autoApproveConfig: { file?: { write?: 'allow'; edit?: 'allow' } } | null = null;
|
||||
|
||||
/**
|
||||
* 设置自动授权配置
|
||||
* 仅影响 file write 和 file edit 操作(不包含 delete)
|
||||
*/
|
||||
setAutoApprove(config: { file?: { write?: 'allow'; edit?: 'allow' } }): void {
|
||||
this.autoApproveConfig = config;
|
||||
}
|
||||
|
||||
/**
|
||||
* 清除自动授权配置
|
||||
*/
|
||||
clearAutoApprove(): void {
|
||||
this.autoApproveConfig = null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取当前自动授权配置
|
||||
*/
|
||||
getAutoApproveConfig(): { file?: { write?: 'allow'; edit?: 'allow' } } | null {
|
||||
return this.autoApproveConfig;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取当前 Agent 模式
|
||||
*/
|
||||
|
||||
@@ -11,7 +11,7 @@ import type { SessionStatus } from '../types.js';
|
||||
import { getSessionManager } from '../session/manager.js';
|
||||
import { broadcastToSession } from '../ws.js';
|
||||
import { emitStatusEvent, emitLogEvent } from '../sse.js';
|
||||
import { createServerPermissionCallback } from '../permission/handler.js';
|
||||
import { createServerPermissionCallback, setSessionAutoApprove } from '../permission/handler.js';
|
||||
|
||||
// ============================================================================
|
||||
// Core 模块接口定义(避免直接依赖 @ai-assistant/core 类型)
|
||||
@@ -97,6 +97,16 @@ interface ChatOptions {
|
||||
abortSignal?: AbortSignal;
|
||||
}
|
||||
|
||||
/**
|
||||
* Agent 模式选项
|
||||
*/
|
||||
export interface AgentModeOptions {
|
||||
/** Agent 模式 (build/plan) */
|
||||
agentMode?: 'build' | 'plan';
|
||||
/** 是否自动授权文件写入/编辑 */
|
||||
autoApprove?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* Agent 实例接口
|
||||
*/
|
||||
@@ -112,6 +122,9 @@ interface AgentInstance {
|
||||
shouldCompress(messages: unknown[]): boolean;
|
||||
};
|
||||
getHistory(): unknown[];
|
||||
setAgentMode?(mode: 'build' | 'plan'): void;
|
||||
setAutoApprove?(config: { file?: { write?: 'allow'; edit?: 'allow' } }): void;
|
||||
clearAutoApprove?(): void;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -343,7 +356,11 @@ export async function destroyAgent(sessionId: string): Promise<void> {
|
||||
/**
|
||||
* 处理用户消息并流式返回响应
|
||||
*/
|
||||
export async function processMessage(sessionId: string, content: string): Promise<void> {
|
||||
export async function processMessage(
|
||||
sessionId: string,
|
||||
content: string,
|
||||
options?: AgentModeOptions
|
||||
): Promise<void> {
|
||||
const sessionManager = getSessionManager();
|
||||
|
||||
// 取消之前可能存在的请求
|
||||
@@ -405,6 +422,22 @@ export async function processMessage(sessionId: string, content: string): Promis
|
||||
return;
|
||||
}
|
||||
|
||||
// 应用 Agent 模式和自动授权配置
|
||||
if (options?.agentMode && agent.setAgentMode) {
|
||||
agent.setAgentMode(options.agentMode);
|
||||
}
|
||||
|
||||
// autoApprove 仅对 build 模式生效,且只允许 write/edit(不含 delete)
|
||||
if (options?.autoApprove && options?.agentMode !== 'plan') {
|
||||
// 设置会话级别的 auto-approve 配置(用于权限回调)
|
||||
setSessionAutoApprove(sessionId, {
|
||||
file: { write: 'allow', edit: 'allow' },
|
||||
});
|
||||
} else {
|
||||
// 清除 auto-approve 配置
|
||||
setSessionAutoApprove(sessionId, null);
|
||||
}
|
||||
|
||||
try {
|
||||
// 调用 Agent 的 chat 方法,使用流式回调和 AbortSignal
|
||||
const result = await agent.chat(content, {
|
||||
|
||||
@@ -19,4 +19,5 @@ export {
|
||||
type TokenUsage,
|
||||
type CompressionResult,
|
||||
type ContextUsageInfo,
|
||||
type AgentModeOptions,
|
||||
} from './adapter.js';
|
||||
|
||||
@@ -39,9 +39,56 @@ interface PendingRequest {
|
||||
|
||||
const pendingRequests = new Map<string, PendingRequest>();
|
||||
|
||||
// 会话级别的 auto-approve 配置
|
||||
// key: sessionId, value: auto-approve 配置
|
||||
const sessionAutoApprove = new Map<string, { file?: { write?: 'allow'; edit?: 'allow' } }>();
|
||||
|
||||
// 默认超时时间(60秒)
|
||||
const PERMISSION_TIMEOUT = 60000;
|
||||
|
||||
/**
|
||||
* 设置会话的 auto-approve 配置
|
||||
*/
|
||||
export function setSessionAutoApprove(
|
||||
sessionId: string,
|
||||
config: { file?: { write?: 'allow'; edit?: 'allow' } } | null
|
||||
): void {
|
||||
if (config) {
|
||||
sessionAutoApprove.set(sessionId, config);
|
||||
} else {
|
||||
sessionAutoApprove.delete(sessionId);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取会话的 auto-approve 配置
|
||||
*/
|
||||
export function getSessionAutoApprove(sessionId: string): { file?: { write?: 'allow'; edit?: 'allow' } } | null {
|
||||
return sessionAutoApprove.get(sessionId) ?? null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查操作是否被 auto-approve
|
||||
*/
|
||||
function isAutoApproved(sessionId: string, ctx: PermissionContext): boolean {
|
||||
const config = sessionAutoApprove.get(sessionId);
|
||||
if (!config) return false;
|
||||
|
||||
const command = ctx.command.toLowerCase();
|
||||
|
||||
// 检查是否为文件写入操作
|
||||
if (command.startsWith('write ') && config.file?.write === 'allow') {
|
||||
return true;
|
||||
}
|
||||
|
||||
// 检查是否为文件编辑操作
|
||||
if (command.startsWith('edit ') && config.file?.edit === 'allow') {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* 从命令或上下文检测权限类型
|
||||
*/
|
||||
@@ -117,6 +164,13 @@ function buildRequestContext(ctx: PermissionContext): PermissionRequestContext {
|
||||
export function createServerPermissionCallback(sessionId: string) {
|
||||
return async (ctx: unknown): Promise<PermissionDecision> => {
|
||||
const permCtx = ctx as PermissionContext;
|
||||
|
||||
// 检查 auto-approve 配置
|
||||
if (isAutoApproved(sessionId, permCtx)) {
|
||||
console.log(`[Permission] Auto-approved: ${permCtx.command}`);
|
||||
return { allow: true, remember: false };
|
||||
}
|
||||
|
||||
const requestId = randomUUID();
|
||||
const permissionType = detectPermissionType(permCtx);
|
||||
const context = buildRequestContext(permCtx);
|
||||
|
||||
@@ -87,6 +87,9 @@ export type Tool = z.infer<typeof ToolSchema>;
|
||||
|
||||
// ============ WebSocket 消息 ============
|
||||
|
||||
// Agent 模式类型
|
||||
export type AgentModeType = 'build' | 'plan';
|
||||
|
||||
// 客户端发送的消息
|
||||
export interface ClientMessage {
|
||||
type: 'message' | 'cancel' | 'tool_response' | 'permission_response';
|
||||
@@ -99,6 +102,9 @@ export interface ClientMessage {
|
||||
requestId?: string;
|
||||
allow?: boolean;
|
||||
remember?: boolean;
|
||||
// Agent mode fields
|
||||
agentMode?: AgentModeType;
|
||||
autoApprove?: boolean;
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -106,6 +106,8 @@ export async function handleWebSocketMessage(
|
||||
case 'message': {
|
||||
// 用户发送消息
|
||||
let content = message.payload?.content || '';
|
||||
const agentMode = message.payload?.agentMode as 'build' | 'plan' | undefined;
|
||||
const autoApprove = message.payload?.autoApprove as boolean | undefined;
|
||||
|
||||
// 将 @filepath 转换为 ./filepath 格式(方便 AI 识别为文件路径)
|
||||
content = content.replace(/@([\w./-]+)/g, './$1');
|
||||
@@ -119,7 +121,7 @@ export async function handleWebSocketMessage(
|
||||
|
||||
// 调用 Agent 处理消息(异步,不阻塞)
|
||||
// 消息存储由 Core Agent 负责
|
||||
processMessage(sessionId, content).catch((error) => {
|
||||
processMessage(sessionId, content, { agentMode, autoApprove }).catch((error) => {
|
||||
console.error('[WS] Agent processing error:', error);
|
||||
});
|
||||
break;
|
||||
|
||||
@@ -129,6 +129,8 @@ export type {
|
||||
// File search types
|
||||
FileSearchResult,
|
||||
FileSearchResponse,
|
||||
// Agent mode types
|
||||
AgentModeType,
|
||||
} from './types.js';
|
||||
|
||||
// API Configuration
|
||||
|
||||
@@ -877,6 +877,11 @@ export interface FileSearchResponse {
|
||||
};
|
||||
}
|
||||
|
||||
// ============ Agent 模式切换相关 ============
|
||||
|
||||
/** Agent 模式类型 (Build/Plan) */
|
||||
export type AgentModeType = 'build' | 'plan';
|
||||
|
||||
// ============ 流式工具调用事件 ============
|
||||
|
||||
/** 工具开始事件 Payload */
|
||||
|
||||
@@ -0,0 +1,185 @@
|
||||
/**
|
||||
* Agent Mode Selector Component
|
||||
*
|
||||
* 在 ChatInput 左侧显示,用于切换 Build/Plan 模式和控制 Auto-approve
|
||||
*/
|
||||
|
||||
import { useState, useRef, useEffect } from 'react';
|
||||
import { Hammer, FileSearch, ChevronDown, Check } from 'lucide-react';
|
||||
import clsx from 'clsx';
|
||||
import { Switch } from '../primitives/Switch.js';
|
||||
|
||||
export type AgentModeType = 'build' | 'plan';
|
||||
|
||||
interface AgentModeSelectorProps {
|
||||
/** 当前模式 */
|
||||
mode: AgentModeType;
|
||||
/** 模式变更回调 */
|
||||
onModeChange: (mode: AgentModeType) => void;
|
||||
/** 是否自动授权文件写入/编辑 */
|
||||
autoApprove: boolean;
|
||||
/** 自动授权变更回调 */
|
||||
onAutoApproveChange: (enabled: boolean) => void;
|
||||
/** 是否禁用 */
|
||||
disabled?: boolean;
|
||||
}
|
||||
|
||||
const modeConfig = {
|
||||
build: {
|
||||
label: 'Build',
|
||||
icon: Hammer,
|
||||
color: 'text-blue-500',
|
||||
bgColor: 'bg-blue-500/10',
|
||||
description: '可执行代码修改',
|
||||
},
|
||||
plan: {
|
||||
label: 'Plan',
|
||||
icon: FileSearch,
|
||||
color: 'text-purple-500',
|
||||
bgColor: 'bg-purple-500/10',
|
||||
description: '只读模式,仅分析',
|
||||
},
|
||||
};
|
||||
|
||||
export function AgentModeSelector({
|
||||
mode,
|
||||
onModeChange,
|
||||
autoApprove,
|
||||
onAutoApproveChange,
|
||||
disabled = false,
|
||||
}: AgentModeSelectorProps) {
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
const dropdownRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
const currentMode = modeConfig[mode];
|
||||
const ModeIcon = currentMode.icon;
|
||||
|
||||
// 点击外部关闭下拉菜单
|
||||
useEffect(() => {
|
||||
function handleClickOutside(event: MouseEvent) {
|
||||
if (dropdownRef.current && !dropdownRef.current.contains(event.target as Node)) {
|
||||
setIsOpen(false);
|
||||
}
|
||||
}
|
||||
|
||||
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);
|
||||
};
|
||||
|
||||
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>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -12,6 +12,7 @@ import clsx from 'clsx';
|
||||
import { CommandMenu, type CommandMenuItem } from './CommandMenu.js';
|
||||
import { FileMenu, type FileMenuItem } from './FileMenu.js';
|
||||
import { FileMentionTag } from './FileMentionTag.js';
|
||||
import { AgentModeSelector, type AgentModeType } from './AgentModeSelector.js';
|
||||
import { useCommands } from '../hooks/useCommands.js';
|
||||
import { useFileMention } from '../hooks/useFileMention.js';
|
||||
|
||||
@@ -26,6 +27,14 @@ interface ChatInputProps {
|
||||
enableCommands?: boolean;
|
||||
/** 是否启用文件提及 (@) */
|
||||
enableFileMention?: boolean;
|
||||
/** Agent 模式 (build/plan) */
|
||||
agentMode?: AgentModeType;
|
||||
/** Agent 模式变更回调 */
|
||||
onAgentModeChange?: (mode: AgentModeType) => void;
|
||||
/** 是否自动授权文件写入/编辑 */
|
||||
autoApprove?: boolean;
|
||||
/** 自动授权变更回调 */
|
||||
onAutoApproveChange?: (enabled: boolean) => void;
|
||||
}
|
||||
|
||||
export function ChatInput({
|
||||
@@ -36,6 +45,10 @@ export function ChatInput({
|
||||
responsive = false,
|
||||
enableCommands = true,
|
||||
enableFileMention = true,
|
||||
agentMode = 'build',
|
||||
onAgentModeChange,
|
||||
autoApprove = false,
|
||||
onAutoApproveChange,
|
||||
}: ChatInputProps) {
|
||||
const [input, setInput] = useState('');
|
||||
const [showCommandMenu, setShowCommandMenu] = useState(false);
|
||||
@@ -281,7 +294,20 @@ export function ChatInput({
|
||||
)}
|
||||
|
||||
{/* 输入区域 */}
|
||||
<div className="flex gap-2">
|
||||
<div className="flex gap-2 items-end">
|
||||
{/* Agent 模式选择器 */}
|
||||
{onAgentModeChange && (
|
||||
<div className="flex-shrink-0 pb-1.5">
|
||||
<AgentModeSelector
|
||||
mode={agentMode}
|
||||
onModeChange={onAgentModeChange}
|
||||
autoApprove={autoApprove}
|
||||
onAutoApproveChange={onAutoApproveChange ?? (() => {})}
|
||||
disabled={disabled || isLoading}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="flex-1 relative">
|
||||
<textarea
|
||||
ref={textareaRef}
|
||||
|
||||
@@ -13,6 +13,7 @@ import type {
|
||||
ToolEndPayload,
|
||||
MessagePart,
|
||||
ToolMessagePart,
|
||||
AgentModeType,
|
||||
} from '../api/types.js';
|
||||
|
||||
interface UseChatOptions {
|
||||
@@ -31,6 +32,10 @@ interface ChatState {
|
||||
/** 流式消息对象,复用 Message 结构 */
|
||||
streamingMessage: Message | null;
|
||||
permissionRequest: PermissionRequest | null;
|
||||
/** Agent 模式 (会话级别) */
|
||||
agentMode: AgentModeType;
|
||||
/** 是否自动授权文件写入/编辑 (会话级别) */
|
||||
autoApprove: boolean;
|
||||
}
|
||||
|
||||
export function useChat({ sessionId, onError, onSessionNotFound, onSessionUpdated, onConfigError }: UseChatOptions) {
|
||||
@@ -40,6 +45,8 @@ export function useChat({ sessionId, onError, onSessionNotFound, onSessionUpdate
|
||||
isLoading: false,
|
||||
streamingMessage: null,
|
||||
permissionRequest: null,
|
||||
agentMode: 'build',
|
||||
autoApprove: false,
|
||||
});
|
||||
|
||||
const wsRef = useRef<WebSocket | null>(null);
|
||||
@@ -323,11 +330,15 @@ export function useChat({ sessionId, onError, onSessionNotFound, onSessionUpdate
|
||||
JSON.stringify({
|
||||
type: 'message',
|
||||
sessionId,
|
||||
payload: { content },
|
||||
payload: {
|
||||
content,
|
||||
agentMode: state.agentMode,
|
||||
autoApprove: state.autoApprove,
|
||||
},
|
||||
})
|
||||
);
|
||||
},
|
||||
[sessionId]
|
||||
[sessionId, state.agentMode, state.autoApprove]
|
||||
);
|
||||
|
||||
// 取消处理
|
||||
@@ -401,6 +412,16 @@ export function useChat({ sessionId, onError, onSessionNotFound, onSessionUpdate
|
||||
[respondToPermission]
|
||||
);
|
||||
|
||||
// 设置 Agent 模式 (会话级别)
|
||||
const setAgentMode = useCallback((mode: AgentModeType) => {
|
||||
setState((prev) => ({ ...prev, agentMode: mode }));
|
||||
}, []);
|
||||
|
||||
// 设置自动授权 (会话级别)
|
||||
const setAutoApprove = useCallback((enabled: boolean) => {
|
||||
setState((prev) => ({ ...prev, autoApprove: enabled }));
|
||||
}, []);
|
||||
|
||||
// 初始化
|
||||
useEffect(() => {
|
||||
// 重置状态
|
||||
@@ -411,6 +432,8 @@ export function useChat({ sessionId, onError, onSessionNotFound, onSessionUpdate
|
||||
isLoading: false,
|
||||
streamingMessage: null,
|
||||
permissionRequest: null,
|
||||
agentMode: 'build',
|
||||
autoApprove: false,
|
||||
});
|
||||
reconnectAttemptsRef.current = 0;
|
||||
|
||||
@@ -448,5 +471,7 @@ export function useChat({ sessionId, onError, onSessionNotFound, onSessionUpdate
|
||||
reload: loadMessages,
|
||||
allowPermission,
|
||||
denyPermission,
|
||||
setAgentMode,
|
||||
setAutoApprove,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -174,6 +174,8 @@ export type {
|
||||
// File Search types
|
||||
FileSearchResult,
|
||||
FileSearchResponse,
|
||||
// Agent Mode types
|
||||
AgentModeType,
|
||||
} from './api/client.js';
|
||||
|
||||
// Primitives (shadcn/ui style)
|
||||
@@ -186,6 +188,7 @@ export * from './utils/animations.js';
|
||||
// Components
|
||||
export { ChatMessage, StreamingMessage, TypingIndicator } from './components/ChatMessage.js';
|
||||
export { ChatInput } from './components/ChatInput.js';
|
||||
export { AgentModeSelector } from './components/AgentModeSelector.js';
|
||||
export { CommandMenu, type CommandMenuItem } from './components/CommandMenu.js';
|
||||
export { FileMenu, type FileMenuItem } from './components/FileMenu.js';
|
||||
export {
|
||||
|
||||
@@ -57,6 +57,10 @@ export function ChatPage({
|
||||
permissionRequest,
|
||||
allowPermission,
|
||||
denyPermission,
|
||||
agentMode,
|
||||
autoApprove,
|
||||
setAgentMode,
|
||||
setAutoApprove,
|
||||
} = useChat({
|
||||
sessionId,
|
||||
onError: (error) => {
|
||||
@@ -307,6 +311,10 @@ export function ChatPage({
|
||||
isLoading={isLoading}
|
||||
disabled={!isConnected}
|
||||
responsive={responsive}
|
||||
agentMode={agentMode}
|
||||
onAgentModeChange={setAgentMode}
|
||||
autoApprove={autoApprove}
|
||||
onAutoApproveChange={setAutoApprove}
|
||||
/>
|
||||
|
||||
{/* Permission Dialog */}
|
||||
|
||||
Reference in New Issue
Block a user