import React, { useEffect, useMemo, useRef, useState } from 'react'; import { Box, Text, useApp, useInput, useStdout } from 'ink'; import { setTimeout as sleep } from 'node:timers/promises'; import { slashCommands, filterCommands } from './commands.js'; import { createWorkspaceForPath, findWorkspaceByPath, loadWorkspaceCatalog, repoRoot } from './workspaces.js'; import type { CliStatus, ExecutionMode, ModelDefinition, PermissionMode, RunMode, SkillDefinition, SlashCommand, TimelineItem, Workspace, WorkspaceCatalog } from './types.js'; import { nowSessionId, shortPath } from './format.js'; import { Composer, HelpPanel, Picker, SkillHint, SlashMenu, StatusLine, StatusPanel, Timeline, WelcomePanel, WorkspacePrompt } from './components.js'; import { ApiClient, createDefaultApiClient } from './api.js'; import { createEventRenderState, reduceTaskEvent } from './eventMapper.js'; type InputMode = 'workspace_prompt' | 'composer' | 'slash_menu' | 'picker' | 'status_panel' | 'help_panel' | 'approval'; type PickerOption = { label: string; description?: string; disabled?: boolean; onSelect: () => void | Promise; }; type ApprovalOption = { label: string; description?: string; decision: 'approved' | 'rejected'; }; const cwd = process.cwd(); const initialCatalog = loadWorkspaceCatalog(); const matchedWorkspace = findWorkspaceByPath(initialCatalog, cwd); function id(): string { return globalThis.crypto?.randomUUID?.() || `${Date.now()}-${Math.random().toString(16).slice(2)}`; } function initialStatus(workspace: Workspace): CliStatus { return { model: '连接中', runMode: 'thinking', directory: workspace.path, workspace, permissionMode: 'auto_approval', executionMode: 'sandbox', sessionId: nowSessionId(), contextUsed: 0, contextLimit: 0, versioningEnabled: false, backgroundAgents: 0, backgroundCommands: 0, }; } function workspaceFromApi(item: any): Workspace { return { workspace_id: String(item.workspace_id || item.id || item.label || 'workspace'), label: String(item.label || item.workspace_id || 'workspace'), path: String(item.path || ''), }; } export default function App() { const { exit } = useApp(); const { stdout } = useStdout(); const apiRef = useRef(null); const runningTaskRef = useRef(null); const currentConversationRef = useRef(undefined); const bootstrappedRef = useRef(false); const [catalog, setCatalog] = useState(initialCatalog); const [mode, setMode] = useState(matchedWorkspace ? 'composer' : 'workspace_prompt'); const [workspacePromptIndex, setWorkspacePromptIndex] = useState(0); const [input, setInput] = useState(''); const [slashIndex, setSlashIndex] = useState(0); const [pickerTitle, setPickerTitle] = useState(''); const [pickerOptions, setPickerOptions] = useState([]); const [pickerIndex, setPickerIndex] = useState(0); const [approvalTitle, setApprovalTitle] = useState(''); const [approvalId, setApprovalId] = useState(''); const [approvalOptions] = useState([ { label: '允许一次', decision: 'approved' }, { label: '拒绝', decision: 'rejected' }, ]); const [approvalIndex, setApprovalIndex] = useState(0); const [timeline, setTimeline] = useState([]); const [status, setStatus] = useState(() => initialStatus(matchedWorkspace || initialCatalog.workspaces[0]!)); const [connected, setConnected] = useState(false); const [connecting, setConnecting] = useState(false); const [running, setRunning] = useState(false); const [runningStartedAt, setRunningStartedAt] = useState(null); const [nowMs, setNowMs] = useState(Date.now()); const [workingTextTick, setWorkingTextTick] = useState(0); const [workingDotTick, setWorkingDotTick] = useState(0); const [timelineDotTick, setTimelineDotTick] = useState(0); const [layoutRefreshTick, setLayoutRefreshTick] = useState(0); const [models, setModels] = useState([]); const [skills, setSkills] = useState([]); const [permissionOptions, setPermissionOptions] = useState([]); const [executionOptions, setExecutionOptions] = useState([]); const commandMatches = useMemo(() => filterCommands(input), [input]); const runningSeconds = runningStartedAt ? Math.max(0, Math.floor((nowMs - runningStartedAt) / 1000)) : 0; const hasRunningTimeline = useMemo(() => timeline.some((item) => item.status === 'running'), [timeline]); const slashMenuVisibleCount = mode === 'slash_menu' ? Math.max(1, Math.min(5, commandMatches.length)) : 0; const composerCursorYOffset = mode === 'slash_menu' ? 1 + slashMenuVisibleCount : 0; useEffect(() => { if (!running) return; const timer = setInterval(() => { setNowMs(Date.now()); }, 1000); return () => clearInterval(timer); }, [running]); useEffect(() => { if (!running) return; const timer = setInterval(() => { setWorkingTextTick((value) => value + 1); }, 100); return () => clearInterval(timer); }, [running]); useEffect(() => { if (!running) return; const timer = setInterval(() => { setWorkingDotTick((value) => value + 1); }, 800); return () => clearInterval(timer); }, [running]); useEffect(() => { if (!hasRunningTimeline) return; let timer: ReturnType | undefined; const delay = setTimeout(() => { setTimelineDotTick((value) => value + 1); timer = setInterval(() => { setTimelineDotTick((value) => value + 1); }, 800); }, 400); return () => { clearTimeout(delay); if (timer) clearInterval(timer); }; }, [hasRunningTimeline]); useEffect(() => { if (mode !== 'composer' || bootstrappedRef.current) return; bootstrappedRef.current = true; getApi() .then((api) => bootstrapConversation(api)) .catch((err) => addItem({ kind: 'tool', title: '初始化失败', status: 'failed', body: String(err.message || err) })); }, [mode]); useEffect(() => { const timer = setTimeout(() => { setLayoutRefreshTick((v) => v + 1); }, 0); return () => clearTimeout(timer); }, [mode, commandMatches.length, running, input.length, timeline.length]); function addItem(item: Omit) { setTimeline((prev) => [...prev, { id: id(), ...item }]); } function patchStatusFromServer(raw: any, workspaceOverride?: Workspace) { const execMode = typeof raw?.execution_mode === 'object' ? raw.execution_mode.mode : raw?.execution_mode; const contextUsed = Number(raw?.context?.total_size || raw?.context_tokens || raw?.token_statistics?.current_context_tokens || 0); setStatus((prev) => ({ ...prev, model: raw?.model_key || prev.model, runMode: raw?.run_mode || prev.runMode, directory: raw?.project_path || workspaceOverride?.path || prev.directory, workspace: workspaceOverride || prev.workspace, permissionMode: raw?.permission_mode || prev.permissionMode, executionMode: execMode || prev.executionMode, sessionId: raw?.conversation?.current_id || prev.sessionId, contextUsed: Number.isFinite(contextUsed) && contextUsed > 0 ? contextUsed : prev.contextUsed, })); if (raw?.conversation?.current_id) currentConversationRef.current = raw.conversation.current_id; } async function getApi(): Promise { if (!apiRef.current) apiRef.current = createDefaultApiClient(cwd, repoRoot); if (!connected && !connecting) { setConnecting(true); try { await apiRef.current.ensureConnected(); setConnected(true); } finally { setConnecting(false); } } else if (!connected) { while (!connected && connecting) await sleep(100); } return apiRef.current; } async function syncBackendState(api = apiRef.current) { if (!api) return; try { const wsData = await api.listWorkspaces(); const workspaces = (wsData.workspaces || []).map(workspaceFromApi); const current = workspaces.find((ws) => ws.workspace_id === wsData.current_workspace_id) || workspaces.find((ws) => ws.path === status.workspace.path) || status.workspace; setCatalog({ default_workspace_id: wsData.default_workspace_id, workspaces }); const rawStatus = await api.getStatus(); patchStatusFromServer(rawStatus, current); const conv = await api.getCurrentConversation().catch(() => null); const convId = conv?.id || rawStatus?.conversation?.current_id; if (convId) currentConversationRef.current = convId; await syncRuntimeCatalogs(api, convId).catch(() => undefined); } catch (err) { addItem({ kind: 'tool', title: '同步后端状态失败', status: 'failed', body: String((err as Error).message || err) }); } } async function syncRuntimeCatalogs(api: ApiClient, conversationId?: string) { const [modelItems, personalization, permission, execution, subAgents, backgroundCommands, tokens, versioning] = await Promise.all([ api.listModels().catch(() => []), api.getPersonalization().catch(() => null), api.getPermissionMode().catch(() => null), api.getExecutionMode().catch(() => null), api.listSubAgents().catch(() => []), api.listBackgroundCommands().catch(() => []), conversationId ? api.getConversationTokens(conversationId).catch(() => null) : Promise.resolve(null), conversationId ? api.getConversationVersioning(conversationId).catch(() => null) : Promise.resolve(null), ]); if (modelItems.length) setModels(modelItems); const enabled = new Set((personalization?.data?.enabled_skills || []).map(String)); const skillItems = (personalization?.skills_catalog || []) .filter((skill: any) => !enabled.size || enabled.has(String(skill.id))) .map((skill: any) => ({ id: String(skill.id), label: String(skill.label || skill.id), description: String(skill.description || '') })); if (skillItems.length) setSkills(skillItems); const rawCompressionLimit = Number( personalization?.context_compression_settings?.context_window_tokens || personalization?.context_compression_settings?.deep_trigger_tokens || personalization?.data?.deep_compress_trigger_tokens || 0, ); const currentModel = modelItems.find((item) => item.model_key === status.model); const modelWindow = Number(currentModel?.context_window || 0); const compressionLimit = rawCompressionLimit > 0 && modelWindow > 0 ? Math.min(rawCompressionLimit, modelWindow) : rawCompressionLimit || modelWindow; if (Array.isArray(permission?.options)) setPermissionOptions(permission.options); if (Array.isArray(execution?.options)) setExecutionOptions(execution.options); const runningAgents = subAgents.filter((item: any) => !['completed', 'failed', 'cancelled', 'terminated', 'timeout'].includes(String(item.status || '').toLowerCase())).length; const runningCommands = backgroundCommands.filter((item: any) => !['completed', 'failed', 'cancelled', 'timeout'].includes(String(item.status || '').toLowerCase())).length; const tokenUsed = Number(tokens?.total_tokens || 0); setStatus((prev) => ({ ...prev, contextUsed: Number.isFinite(tokenUsed) && tokenUsed > 0 ? tokenUsed : prev.contextUsed, contextLimit: Number.isFinite(compressionLimit) && compressionLimit > 0 ? compressionLimit : prev.contextLimit, versioningEnabled: versioning ? Boolean(versioning.enabled) : prev.versioningEnabled, backgroundAgents: runningAgents, backgroundCommands: runningCommands, })); } async function bootstrapConversation(api: ApiClient) { const result = await api.createConversation(); const convId = result.conversation_id || result.data?.conversation_id || result.data?.id || result.id; if (convId) { currentConversationRef.current = String(convId); setStatus((prev) => ({ ...prev, sessionId: String(convId), contextUsed: 0 })); await syncBackendState(api); await syncRuntimeCatalogs(api, String(convId)).catch(() => undefined); } } function openPicker(title: string, options: PickerOption[]) { setPickerTitle(title); setPickerOptions(options); setPickerIndex(0); setMode('picker'); } function closeOverlay() { setMode('composer'); setInput(''); setSlashIndex(0); setPickerIndex(0); } async function executeCommand(command: SlashCommand) { const name = command.name; try { if (name === '/new') { const api = await getApi(); const result = await api.createConversation(); const convId = result.conversation_id || result.data?.conversation_id; currentConversationRef.current = convId; setTimeline([]); setStatus((prev) => ({ ...prev, sessionId: convId || nowSessionId(), contextUsed: 0 })); if (convId) await syncRuntimeCatalogs(api, String(convId)).catch(() => undefined); closeOverlay(); return; } if (name === '/resume') { const api = await getApi(); const conversations = await api.listConversations(30); openPicker('Resume conversation', conversations.map((conv: any) => ({ label: String(conv.title || conv.id || '未命名对话'), description: String(conv.updated_at || conv.created_at || conv.id || ''), onSelect: async () => { await api.loadConversation(String(conv.id)); currentConversationRef.current = String(conv.id); setStatus((prev) => ({ ...prev, sessionId: String(conv.id) })); await syncRuntimeCatalogs(api, String(conv.id)).catch(() => undefined); addItem({ kind: 'system', body: `已切换对话:${conv.title || conv.id}` }); closeOverlay(); }, }))); return; } if (name === '/model') { const api = await getApi(); const modelItems = (await api.listModels().catch(() => models)).filter((item) => item.model_key); if (modelItems.length) setModels(modelItems); const currentRunMode = status.runMode; openPicker('Model', modelItems.map((model) => ({ label: model.name || model.model_key, description: `${model.model_key === status.model ? 'current · ' : ''}${model.description || model.model_key}`, onSelect: async () => { await api.setModel(model.model_key); setStatus((prev) => ({ ...prev, model: model.model_key, contextLimit: Number(model.context_window) || prev.contextLimit })); addItem({ kind: 'system', body: `已切换模型:${model.name || model.model_key}` }); const modes = getRunModesForModel(model); openPicker('Run Mode', modes.map((runMode) => ({ label: runMode, description: runMode === currentRunMode ? 'current' : '', onSelect: async () => { await api.setRunMode(runMode); setStatus((prev) => ({ ...prev, runMode })); await syncBackendState(api); addItem({ kind: 'system', body: `已切换运行模式:${runMode}` }); closeOverlay(); }, }))); }, }))); return; } if (name === '/versioning') { const api = await getApi(); const convId = currentConversationRef.current || status.sessionId; const current = await api.getConversationVersioning(convId).catch(() => null); const enabled = current ? Boolean(current.enabled) : status.versioningEnabled; const trackingMode = current?.tracking_mode; openPicker('Versioning', [ { label: enabled ? '关闭版本控制' : '开启版本控制', onSelect: async () => { const result = await api.setConversationVersioning(convId, !enabled, trackingMode); setStatus((prev) => ({ ...prev, versioningEnabled: Boolean(result.enabled) })); addItem({ kind: 'system', body: `版本控制:${result.enabled ? '开' : '关'}` }); closeOverlay(); } }, ]); return; } if (name === '/permission') { const api = await getApi(); const current = await api.getPermissionMode().catch(() => null); const modes: PermissionMode[] = Array.isArray(current?.options) && current.options.length ? current.options : (permissionOptions.length ? permissionOptions : [status.permissionMode]); openPicker('Permission Mode', modes.map((permissionMode) => ({ label: permissionMode, description: permissionMode === status.permissionMode ? 'current' : '', onSelect: async () => { await api.setPermissionMode(permissionMode); setStatus((prev) => ({ ...prev, permissionMode })); addItem({ kind: 'system', body: `权限模式:${permissionMode}` }); closeOverlay(); } }))); return; } if (name === '/execution') { const api = await getApi(); const current = await api.getExecutionMode().catch(() => null); const modes: ExecutionMode[] = Array.isArray(current?.options) && current.options.length ? current.options : (executionOptions.length ? executionOptions : [status.executionMode]); openPicker('Execution Environment', modes.map((executionMode) => ({ label: executionMode, description: executionMode === status.executionMode ? 'current' : executionMode === 'direct' ? '高风险' : '', onSelect: async () => { await api.setExecutionMode(executionMode); setStatus((prev) => ({ ...prev, executionMode })); addItem({ kind: 'system', body: `执行环境:${executionMode}` }); closeOverlay(); } }))); return; } if (name === '/workspace') { const api = await getApi(); const wsData = await api.listWorkspaces(); const workspaces = (wsData.workspaces || []).map(workspaceFromApi); setCatalog({ default_workspace_id: wsData.default_workspace_id, workspaces }); openPicker('Workspace', workspaces.map((workspace) => ({ label: workspace.label, description: `${shortPath(workspace.path)}${workspace.workspace_id === wsData.current_workspace_id ? ' current' : ''}`, onSelect: async () => { await api.selectWorkspace(workspace.workspace_id); setStatus((prev) => ({ ...prev, workspace, directory: workspace.path })); addItem({ kind: 'system', body: `已切换工作区:${workspace.label}` }); await syncBackendState(api); closeOverlay(); } }))); return; } if (name === '/compact') { const api = await getApi(); const convId = currentConversationRef.current || status.sessionId; const result = await api.compressConversation(convId); addItem({ kind: 'tool', title: '对话已压缩', body: result.compact_file || '', status: 'success' }); if (result.compressed_conversation_id) { currentConversationRef.current = result.compressed_conversation_id; setStatus((prev) => ({ ...prev, sessionId: result.compressed_conversation_id })); } closeOverlay(); return; } if (name === '/tools') { const api = await getApi(); const snapshot = await api.getToolSettings(); const categories = snapshot.categories || {}; openPicker('Tools', Object.entries(categories).map(([key, value]: [string, any]) => ({ label: `${value.enabled ? '[x]' : '[ ]'} ${key}`, description: value.label || '', onSelect: async () => { await api.setToolCategory(key, !value.enabled); addItem({ kind: 'system', body: `工具 ${key}:${!value.enabled ? '启用' : '禁用'}` }); closeOverlay(); } }))); return; } if (name === '/path') { const api = await getApi(); const auth = await api.getPathAuthorization(); openPicker('Authorized Paths', [ { label: '当前可读写路径', description: (auth.writable_paths || []).join(', ') || '无', onSelect: () => closeOverlay() }, { label: '当前只读路径', description: (auth.readable_extra_paths || []).join(', ') || '无', onSelect: () => closeOverlay() }, ]); return; } if (name === '/skill') { const api = await getApi(); const personalization = await api.getPersonalization().catch(() => null); const enabled = new Set((personalization?.data?.enabled_skills || []).map(String)); const skillItems = (personalization?.skills_catalog || skills) .filter((skill: any) => !enabled.size || enabled.has(String(skill.id))) .map((skill: any) => ({ id: String(skill.id), label: String(skill.label || skill.id), description: String(skill.description || '') })); if (skillItems.length) setSkills(skillItems); openPicker('Skill', skillItems.map((skill: SkillDefinition) => ({ label: skill.label, description: skill.description, onSelect: () => { setStatus((prev) => ({ ...prev, activeSkill: skill })); addItem({ kind: 'system', body: `下一条消息将使用 skill:${skill.label}` }); closeOverlay(); } })).concat(skillItems.length ? [] : [{ label: '暂无可用 skill', disabled: true, onSelect: () => closeOverlay() }])); return; } if (name === '/status') { await getApi().then((api) => syncBackendState(api)).catch(() => undefined); setMode('status_panel'); setInput(''); return; } if (name === '/help') { setMode('help_panel'); setInput(''); return; } if (name === '/clear') { setTimeline([]); closeOverlay(); return; } if (name === '/exit') { if (runningTaskRef.current) await apiRef.current?.cancelTask(runningTaskRef.current).catch(() => undefined); exit(); } } catch (err) { addItem({ kind: 'tool', title: `命令失败:${name}`, status: 'failed', body: String((err as Error).message || err) }); closeOverlay(); } } async function submitUserMessage(text: string) { addItem({ kind: 'user', body: text }); const skill = status.activeSkill; const message = skill ? `${text}\n\n先阅读 ${skill.label} skill` : text; if (skill) addItem({ kind: 'user', body: `先阅读 ${skill.label} skill` }); setStatus((prev) => ({ ...prev, activeSkill: undefined })); setRunning(true); setRunningStartedAt(Date.now()); try { const api = await getApi(); const task = await api.createTask({ message, conversation_id: currentConversationRef.current, model_key: status.model, run_mode: status.runMode }); runningTaskRef.current = task.task_id; if (task.conversation_id) currentConversationRef.current = task.conversation_id; await pollTask(api, task.task_id); await syncBackendState(api); } catch (err) { addItem({ kind: 'tool', title: '发送失败', status: 'failed', body: String((err as Error).message || err) }); } finally { runningTaskRef.current = null; setRunning(false); setRunningStartedAt(null); } } async function pollTask(api: ApiClient, taskId: string) { let offset = 0; const renderState = createEventRenderState(); for (;;) { const result = await api.pollTask(taskId, offset); offset = result.next_offset; for (const event of result.events || []) { if (event.type === 'tool_approval_required') { const approval = event.data?.approval || event.data; if (approval?.approval_id) { setApprovalId(String(approval.approval_id)); setApprovalTitle(`需要审批:${approval.tool_name || '工具调用'}`); setApprovalIndex(0); setMode('approval'); } } setTimeline((prev) => reduceTaskEvent(prev, renderState, event)); } if (!['pending', 'running', 'cancel_requested'].includes(String(result.status))) break; await sleep(700); } } useInput((chunk, key) => { void handleInput(chunk, key); }); function handleComposerChange(next: string) { setInput(next); if (next.startsWith('/')) { if (mode !== 'slash_menu') setMode('slash_menu'); setSlashIndex(0); } else if (mode === 'slash_menu') { setMode('composer'); setSlashIndex(0); } } async function handleComposerSubmit(raw: string) { const text = String(raw || '').trim(); if (!text) return; if (text.startsWith('/')) { const command = filterCommands(text)[0]; if (command) await executeCommand(command); return; } setInput(''); await submitUserMessage(text); } async function handleInput(chunk: string, key: any) { if (mode === 'workspace_prompt') { if (key.upArrow || chunk === 'k') setWorkspacePromptIndex(0); else if (key.downArrow || chunk === 'j') setWorkspacePromptIndex(1); else if (key.return) { if (workspacePromptIndex === 0) { const result = createWorkspaceForPath(catalog, cwd); setCatalog(result.catalog); setStatus((prev) => ({ ...prev, workspace: result.workspace, directory: result.workspace.path })); bootstrappedRef.current = true; setMode('composer'); getApi().then(async (api) => { await api.createWorkspace(result.workspace.path, result.workspace.label).catch(() => undefined); const wsData = await api.listWorkspaces().catch(() => null); const backendWs = wsData?.workspaces?.find((ws: any) => String(ws.path) === result.workspace.path); if (backendWs) await api.selectWorkspace(String(backendWs.workspace_id)); await bootstrapConversation(api); }).catch((err) => addItem({ kind: 'tool', title: '连接后端失败', status: 'failed', body: String(err.message || err) })); } else { exit(); } } return; } if (mode === 'approval') { if (key.upArrow) setApprovalIndex((prev) => Math.max(0, prev - 1)); else if (key.downArrow) setApprovalIndex((prev) => Math.min(approvalOptions.length - 1, prev + 1)); else if (key.return && approvalId) { const selected = approvalOptions[approvalIndex]!; await apiRef.current?.decideApproval(approvalId, selected.decision).catch((err) => addItem({ kind: 'tool', title: '审批提交失败', status: 'failed', body: String(err.message || err) })); addItem({ kind: 'system', body: `审批已${selected.decision === 'approved' ? '允许' : '拒绝'}` }); setMode('composer'); } return; } if (key.escape) { if (runningTaskRef.current) { await apiRef.current?.cancelTask(runningTaskRef.current).catch(() => undefined); addItem({ kind: 'system', body: '已请求停止当前任务。' }); return; } closeOverlay(); return; } if (mode === 'status_panel' || mode === 'help_panel') { if (key.return || chunk === 'q') closeOverlay(); return; } if (mode === 'picker') { if (key.upArrow) setPickerIndex((prev) => Math.max(0, prev - 1)); else if (key.downArrow) setPickerIndex((prev) => Math.min(pickerOptions.length - 1, prev + 1)); else if (key.return) await pickerOptions[pickerIndex]?.onSelect(); return; } if (key.ctrl && chunk === 'c') { exit(); return; } if (mode === 'slash_menu') { if (key.upArrow) setSlashIndex((prev) => Math.max(0, prev - 1)); else if (key.downArrow) setSlashIndex((prev) => Math.min(commandMatches.length - 1, prev + 1)); else if (key.return) { const command = commandMatches[slashIndex]; if (command) await executeCommand(command); } return; } if (mode === 'composer') return; if (key.return) { const text = input.trim(); if (!text) return; if (text.startsWith('/')) { const command = filterCommands(text)[0]; if (command) await executeCommand(command); return; } setInput(''); await submitUserMessage(text); return; } if (key.backspace || key.delete) { setInput((prev) => Array.from(prev).slice(0, -1).join('')); return; } if (chunk && !key.ctrl && !key.meta) { const normalized = chunk.replace(/\r\n/g, '\n').replace(/\r/g, '\n'); setInput((prev) => { const next = prev + normalized; if (next.startsWith('/')) { setMode('slash_menu'); setSlashIndex(0); } return next; }); } } if (mode === 'workspace_prompt') { return ; } return ( {timeline.length === 0 ? : } {timeline.length > 0 && timeline[timeline.length - 1]?.kind === 'assistant' ? : null} {running ? ( {workingDotTick % 2 === 0 ? '◦' : '•'}{' '} {` (${runningSeconds}s • 按 Esc 停止)`} ) : null} {mode === 'slash_menu' ? : null} {mode === 'picker' ? : null} {mode === 'status_panel' ? : null} {mode === 'help_panel' ? : null} {mode === 'approval' ? : null} {mode === 'composer' || mode === 'slash_menu' ? <> : null} ); } function AnimatedSummary({ text, tick }: { text: string; tick: number }) { const chars = Array.from(text || '正在执行...'); const cycle = chars.length + 8; const head = tick % cycle; return ( <> {chars.map((char, index) => { const distance = head - index; const active = head < chars.length + 4 && distance >= 0 && distance < 5; return ( {char === ' ' ? '\u00A0' : char} ); })} ); } function getRunModesForModel(model?: ModelDefinition): RunMode[] { if (!model) return ['fast', 'thinking', 'deep']; if (model.deep_only) return ['deep']; if (model.fast_only || !model.supports_thinking) return ['fast']; return ['fast', 'thinking', 'deep']; }