From 243f8dc8602a91a1dee81fcf62de1125e67e1966 Mon Sep 17 00:00:00 2001 From: kurihada Date: Thu, 18 Dec 2025 17:48:46 +0800 Subject: [PATCH] =?UTF-8?q?feat(desktop):=20=E5=90=8C=E6=AD=A5=20web=20?= =?UTF-8?q?=E6=A8=A1=E5=9D=97=E7=95=8C=E9=9D=A2=E5=92=8C=E4=BF=AE=E5=A4=8D?= =?UTF-8?q?=E4=BC=9A=E8=AF=9D=E5=88=9D=E5=A7=8B=E5=8C=96?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - App.tsx: 添加 IDE 面板、StatusBar、Resizer 等组件 - App.tsx: 添加连接错误处理和用户友好提示 - App.tsx: 修复新 project 目录下不自动创建会话的问题 - Chat.tsx: 同步 web 版本的所有 UI 组件和功能 - tailwind.config.js: 添加语义化颜色 (surface, fg, line, code) - index.css: 精简为仅包含桌面端特有样式 - ThemeProvider 设置 defaultTheme='dark' 修复代码编辑器主题 - web/App.tsx: 同步修复会话初始化逻辑 --- packages/desktop/src/App.tsx | 317 ++++++++++++++++++-------- packages/desktop/src/pages/Chat.tsx | 282 +++++++++++------------ packages/desktop/src/styles/index.css | 79 +------ packages/desktop/tailwind.config.js | 20 ++ packages/web/src/App.tsx | 17 +- 5 files changed, 387 insertions(+), 328 deletions(-) diff --git a/packages/desktop/src/App.tsx b/packages/desktop/src/App.tsx index 5ba1e94..e0ca1c8 100644 --- a/packages/desktop/src/App.tsx +++ b/packages/desktop/src/App.tsx @@ -1,64 +1,87 @@ /** * App Component + * + * 响应式布局:支持桌面端和移动端 */ import { useState, useEffect, useCallback } from 'react'; import { - Sidebar, - FileBrowser, - ConfigPanel, + IDE, CommandPanel, MCPPanel, HooksPanel, AgentsPanel, CheckpointPanel, ProvidersPanel, + LSPPanel, + DiagnosticsPanel, + SessionPanel, + StatusBar, + Resizer, Toaster, + ThemeProvider, listSessions, createSession, type Session, + type ActiveFileInfo, + type FileDiffInfo, } from '@ai-assistant/ui'; import { ChatPage } from './pages/Chat'; export function App() { const [currentSessionId, setCurrentSessionId] = useState(null); const [isInitializing, setIsInitializing] = useState(true); - const [showFileBrowser, setShowFileBrowser] = useState(false); - const [showConfig, setShowConfig] = useState(false); + const [connectionError, setConnectionError] = useState(null); const [showCommands, setShowCommands] = useState(false); const [showMCP, setShowMCP] = useState(false); const [showHooks, setShowHooks] = useState(false); const [showAgents, setShowAgents] = useState(false); const [showCheckpoints, setShowCheckpoints] = useState(false); const [showProviders, setShowProviders] = useState(false); + const [showLSP, setShowLSP] = useState(false); + const [showDiagnostics, setShowDiagnostics] = useState(false); + const [showSessions, setShowSessions] = useState(false); const [sessionTitleUpdate, setSessionTitleUpdate] = useState<{ sessionId: string; name: string } | null>(null); + // IDE 面板宽度(百分比) + const [idePanelWidth, setIdePanelWidth] = useState(() => { + const saved = localStorage.getItem('ai-assistant-ide-width'); + return saved ? parseFloat(saved) : 70; + }); - // 初始化:加载会话(只在首次启动时自动创建) + // 编辑器联动状态 + const [activeFile, setActiveFile] = useState(null); + const [autoAttachActiveFile, setAutoAttachActiveFile] = useState(() => { + const saved = localStorage.getItem('ai-assistant-auto-attach-file'); + return saved !== 'false'; // 默认开启 + }); + + // Diff 显示状态(当 AI 编辑/写入文件时触发) + const [pendingDiff, setPendingDiff] = useState(null); + + // 持久化自动附加开关状态 useEffect(() => { - const HAS_SESSIONS_KEY = 'ai-assistant-has-sessions'; + localStorage.setItem('ai-assistant-auto-attach-file', String(autoAttachActiveFile)); + }, [autoAttachActiveFile]); + // 初始化:加载会话 + useEffect(() => { async function init() { try { - const { data: sessions } = await listSessions(); + const sessionsResult = await listSessions(); + const { data: sessions } = sessionsResult; if (sessions.length > 0) { // 有会话,选择最近的 setCurrentSessionId(sessions[0].id); - localStorage.setItem(HAS_SESSIONS_KEY, 'true'); } else { - // 无会话:检查是否是首次启动 - const hasHadSessions = localStorage.getItem(HAS_SESSIONS_KEY); - - if (!hasHadSessions) { - // 首次启动,自动创建会话 - const { data: newSession } = await createSession(); - setCurrentSessionId(newSession.id); - localStorage.setItem(HAS_SESSIONS_KEY, 'true'); - } - // 用户删除了所有会话:不自动创建,显示空状态 + // 无会话,自动创建一个新会话 + // 这处理了新 project 目录的情况 + const { data: newSession } = await createSession(); + setCurrentSessionId(newSession.id); } } catch (error) { console.error('Failed to initialize:', error); + setConnectionError('无法连接到服务器。请确保后端服务已启动 (pnpm server:dev)'); } finally { setIsInitializing(false); } @@ -75,91 +98,207 @@ export function App() { setCurrentSessionId(session.id); }; + // 会话不存在时自动创建新会话 + const handleSessionNotFound = useCallback(async () => { + try { + const { data: newSession } = await createSession(); + setCurrentSessionId(newSession.id); + } catch (error) { + console.error('Failed to create new session:', error); + } + }, []); + // 会话标题更新回调 const handleSessionUpdated = useCallback((sessionId: string, name: string) => { setSessionTitleUpdate({ sessionId, name }); }, []); + // 会话切换回调(:new 命令创建新会话后切换) + const handleSessionSwitch = useCallback((newSessionId: string) => { + setCurrentSessionId(newSessionId); + }, []); + + // 文件 diff 回调(当 AI 编辑/写入文件时触发) + const handleFileDiff = useCallback((diff: FileDiffInfo) => { + setPendingDiff(diff); + }, []); + + // Diff 关闭回调 + const handleDiffClose = useCallback(() => { + setPendingDiff(null); + }, []); + + // 处理面板宽度调整 + const handleResize = useCallback((delta: number) => { + setIdePanelWidth((prev) => { + // 计算百分比变化(基于窗口宽度) + const percentDelta = (delta / window.innerWidth) * 100; + // 限制范围:30% - 80% + return Math.min(80, Math.max(30, prev + percentDelta)); + }); + }, []); + + // 保存面板宽度到 localStorage + const handleResizeEnd = useCallback(() => { + localStorage.setItem('ai-assistant-ide-width', String(idePanelWidth)); + }, [idePanelWidth]); + if (isInitializing) { return ( -
-
-
-

Initializing...

+ +
+
+
+

Initializing...

+
-
+
); } return ( -
- - -
- {/* 聊天区域 */} -
- {currentSessionId ? ( - setShowFileBrowser(!showFileBrowser)} - onOpenConfig={() => setShowConfig(true)} - onOpenCommands={() => setShowCommands(true)} - onOpenMCP={() => setShowMCP(true)} - onOpenHooks={() => setShowHooks(true)} - onOpenAgents={() => setShowAgents(true)} - onOpenCheckpoints={() => setShowCheckpoints(true)} - onOpenProviders={() => setShowProviders(true)} - /> - ) : ( -
-

Select or create a session

-
- )} -
- - {/* 文件浏览器 */} - {showFileBrowser && ( -
- { - console.log('Selected file:', path); - }} + +
+ {/* 主内容区域:左侧文件浏览器 + 右侧对话框 */} +
+ {/* 左侧:IDE(文件浏览器 + 代码编辑器) */} +
+
+ + {/* 可拖拽分割线 */} + + + {/* 右侧:聊天区域 */} +
+ {connectionError ? ( +
+
+ + + +
+
+

连接失败

+

{connectionError}

+ +
+
+ ) : currentSessionId ? ( + setShowCommands(true)} + onOpenMCP={() => setShowMCP(true)} + onOpenHooks={() => setShowHooks(true)} + onOpenAgents={() => setShowAgents(true)} + onOpenCheckpoints={() => setShowCheckpoints(true)} + onOpenProviders={() => setShowProviders(true)} + onOpenLSP={() => setShowLSP(true)} + onOpenDiagnostics={() => setShowDiagnostics(true)} + onOpenSessions={() => setShowSessions(true)} + activeFile={activeFile} + autoAttachActiveFile={autoAttachActiveFile} + onAutoAttachActiveFileToggle={setAutoAttachActiveFile} + onFileDiff={handleFileDiff} + onViewDiff={handleFileDiff} + /> + ) : ( +
+

Select or create a session

+
+ )} +
+ +
+ + {/* 底部状态栏 */} + setShowDiagnostics(true)} + /> + + {/* 命令面板 */} + {showCommands && setShowCommands(false)} responsive />} + + {/* MCP 面板 */} + {showMCP && setShowMCP(false)} responsive />} + + {/* Hooks 面板 */} + {showHooks && setShowHooks(false)} responsive />} + + {/* Agents 面板 */} + {showAgents && setShowAgents(false)} responsive />} + + {/* Checkpoints 面板 */} + {showCheckpoints && setShowCheckpoints(false)} responsive />} + + {/* Providers 面板 */} + {showProviders && setShowProviders(false)} responsive />} + + {/* LSP 面板 */} + {showLSP && ( + setShowLSP(false)} + onOpenDiagnostics={() => { + setShowLSP(false); + setShowDiagnostics(true); + }} + responsive + /> )} + + {/* Diagnostics 面板 */} + {showDiagnostics && ( + setShowDiagnostics(false)} + onFileClick={(file, line) => { + console.log('Navigate to:', file, line); + // TODO: Integrate with file browser or editor + }} + responsive + /> + )} + + {/* Sessions 面板 */} + {showSessions && ( + setShowSessions(false)} + currentSessionId={currentSessionId} + onSelectSession={handleSelectSession} + onCreateSession={handleCreateSession} + sessionTitleUpdate={sessionTitleUpdate} + responsive + /> + )} + + {/* Toast 通知 */} +
- - {/* 配置面板 */} - {showConfig && setShowConfig(false)} />} - - {/* 命令面板 */} - {showCommands && setShowCommands(false)} />} - - {/* MCP 面板 */} - {showMCP && setShowMCP(false)} />} - - {/* Hooks 面板 */} - {showHooks && setShowHooks(false)} />} - - {/* Agents 面板 */} - {showAgents && setShowAgents(false)} />} - - {/* Checkpoints 面板 */} - {showCheckpoints && setShowCheckpoints(false)} />} - - {/* Providers 面板 */} - {showProviders && setShowProviders(false)} />} - - {/* Toast 通知 */} - -
+ ); } diff --git a/packages/desktop/src/pages/Chat.tsx b/packages/desktop/src/pages/Chat.tsx index ae5fe77..7c9af33 100644 --- a/packages/desktop/src/pages/Chat.tsx +++ b/packages/desktop/src/pages/Chat.tsx @@ -3,7 +3,7 @@ */ import { useEffect, useRef } from 'react'; -import { WifiOff, MessageSquare, Settings, FolderOpen, Terminal, Plug, Zap, Bot, History, Server } from 'lucide-react'; +import { MessageSquare, Terminal, Plug, Zap, Bot, History, Server, MessagesSquare } from 'lucide-react'; import { motion, AnimatePresence } from 'framer-motion'; import { toast } from 'sonner'; import { @@ -11,38 +11,64 @@ import { ChatMessage, TypingIndicator, ChatInput, + ContextUsage, + SubagentProgress, + DiagnosticsIndicator, + ToolbarOverflowMenu, + type ActiveFileInfo, + type FileDiffInfo, } from '@ai-assistant/ui'; interface ChatPageProps { sessionId: string; + onSessionNotFound?: () => void; onSessionUpdated?: (sessionId: string, name: string) => void; /** 切换会话回调(如 :new 命令创建新会话) */ onSessionSwitch?: (newSessionId: string) => void; + responsive?: boolean; // 工具栏按钮 - showFileBrowser?: boolean; - onToggleFileBrowser?: () => void; - onOpenConfig?: () => void; onOpenCommands?: () => void; onOpenMCP?: () => void; onOpenHooks?: () => void; onOpenAgents?: () => void; onOpenCheckpoints?: () => void; onOpenProviders?: () => void; + onOpenLSP?: () => void; + onOpenDiagnostics?: () => void; + onOpenSessions?: () => void; + // 编辑器联动 + /** 当前编辑器活动文件 */ + activeFile?: ActiveFileInfo | null; + /** 是否自动附加当前编辑器文件 */ + autoAttachActiveFile?: boolean; + /** 自动附加开关变更回调 */ + onAutoAttachActiveFileToggle?: (enabled: boolean) => void; + /** 文件 diff 回调(当 AI 写入/编辑文件时触发) */ + onFileDiff?: (diff: FileDiffInfo) => void; + /** 查看文件 diff 回调(点击 View Diff 按钮) */ + onViewDiff?: (diff: FileDiffInfo) => void; } export function ChatPage({ sessionId, + onSessionNotFound, onSessionUpdated, onSessionSwitch, - showFileBrowser, - onToggleFileBrowser, - onOpenConfig, + responsive = false, onOpenCommands, onOpenMCP, onOpenHooks, onOpenAgents, onOpenCheckpoints, onOpenProviders, + onOpenLSP, + onOpenDiagnostics, + onOpenSessions, + activeFile, + autoAttachActiveFile, + onAutoAttachActiveFileToggle, + onFileDiff, + onViewDiff, }: ChatPageProps) { const { messages, @@ -51,12 +77,21 @@ export function ChatPage({ streamingMessage, sendMessage, cancelProcessing, + allowPermission, + denyPermission, + agentMode, + autoApprove, + setAgentMode, + setAutoApprove, + currentAgent, + currentSubagent, answerQuestion, } = useChat({ sessionId, onError: (error) => { console.error('Chat error:', error); }, + onSessionNotFound, onSessionUpdated, onSessionSwitch, onConfigError: (error) => { @@ -70,6 +105,7 @@ export function ChatPage({ : undefined, }); }, + onFileDiff, }); const messagesEndRef = useRef(null); @@ -91,11 +127,11 @@ export function ChatPage({
-

+

Start a conversation

-

+

Ask me anything about coding, debugging, or software development.

@@ -106,7 +142,7 @@ export function ChatPage({ whileHover={{ scale: 1.02 }} whileTap={{ scale: 0.98 }} onClick={() => sendMessage(suggestion)} - className="px-3 py-1.5 bg-gray-800 hover:bg-gray-700 rounded-full text-sm text-gray-300 transition-colors" + className="px-3 py-1.5 bg-surface-subtle hover:bg-surface-muted rounded-full text-sm text-fg-secondary transition-colors" > "{suggestion}" @@ -115,152 +151,60 @@ export function ChatPage({ ); - // 连接状态指示器 - const ConnectionStatus = () => ( -
- {isConnected ? ( - - - - - - Connected - - ) : ( -
- - Disconnected -
- )} -
- ); - return ( -
+
{/* Header */} -
-

Chat

-
- {/* 连接状态 */} - - - {/* 工具栏按钮 */} - {(onOpenConfig || onToggleFileBrowser || onOpenCommands || onOpenMCP || onOpenHooks || onOpenAgents || onOpenCheckpoints || onOpenProviders) && ( -
- {/* Checkpoints 按钮 */} - {onOpenCheckpoints && ( - - - - )} - - {/* Providers 按钮 */} - {onOpenProviders && ( - - - - )} - - {/* Agents 按钮 */} - {onOpenAgents && ( - - - - )} - - {/* Hooks 按钮 */} - {onOpenHooks && ( - - - - )} - - {/* MCP 按钮 */} - {onOpenMCP && ( - - - - )} - - {/* 命令按钮 */} - {onOpenCommands && ( - - - - )} - - {/* 配置按钮 */} - {onOpenConfig && ( - - - - )} - - {/* 文件浏览器按钮 */} - {onToggleFileBrowser && ( - - - - )} -
+
+ {/* 左侧:上下文使用情况 */} +
+ {sessionId && ( + )}
+ + {/* 右侧:工具栏按钮 */} + {(onOpenCommands || onOpenMCP || onOpenHooks || onOpenAgents || onOpenCheckpoints || onOpenProviders || onOpenLSP || onOpenDiagnostics || onOpenSessions) && ( +
+ {/* LSP 诊断指示器 */} + {(onOpenLSP || onOpenDiagnostics) && ( + + )} + + {/* Sessions 按钮 */} + {onOpenSessions && ( + + + + )} + + {/* 设置菜单 - 齿轮图标,放在最右侧 */} + +
+ )}
{/* Messages */} @@ -270,16 +214,35 @@ export function ChatPage({ {messages.map((message) => ( - + ))} {/* 流式消息 - 复用 ChatMessage 组件 */} {streamingMessage && ( - + )} - {isLoading && !streamingMessage && } + {/* 子 Agent 进度显示 */} + {currentSubagent && ( + + )} + + {isLoading && !streamingMessage && !currentSubagent && }
@@ -291,7 +254,16 @@ export function ChatPage({ onCancel={cancelProcessing} isLoading={isLoading} disabled={!isConnected} + responsive={responsive} + agentMode={agentMode} + onAgentModeChange={setAgentMode} + autoApprove={autoApprove} + onAutoApproveChange={setAutoApprove} + activeFile={activeFile} + autoAttachActiveFile={autoAttachActiveFile} + onAutoAttachActiveFileToggle={onAutoAttachActiveFileToggle} /> +
); } diff --git a/packages/desktop/src/styles/index.css b/packages/desktop/src/styles/index.css index 4035566..3a710f3 100644 --- a/packages/desktop/src/styles/index.css +++ b/packages/desktop/src/styles/index.css @@ -2,77 +2,14 @@ @tailwind components; @tailwind utilities; -/* Custom scrollbar */ -::-webkit-scrollbar { - width: 8px; - height: 8px; +/* 导入 UI 包的共享样式会在 main.tsx 中通过 import '@ai-assistant/ui/styles' 完成 */ +/* 此文件仅包含 desktop 特有的样式覆盖 */ + +/* Desktop 特有的 Tauri 窗口拖拽区域 */ +.titlebar-drag-region { + -webkit-app-region: drag; } -::-webkit-scrollbar-track { - background: transparent; -} - -::-webkit-scrollbar-thumb { - background: #4b5563; - border-radius: 4px; -} - -::-webkit-scrollbar-thumb:hover { - background: #6b7280; -} - -/* Message content */ -.message-content { - @apply max-w-none text-gray-100; -} - -.message-content pre { - @apply bg-gray-800 rounded-lg p-4 overflow-x-auto; -} - -.message-content code { - @apply bg-gray-800 px-1.5 py-0.5 rounded text-sm; -} - -.message-content pre code { - @apply bg-transparent p-0; -} - -/* Typing indicator */ -.typing-indicator { - display: flex; - gap: 4px; -} - -.typing-indicator span { - width: 8px; - height: 8px; - background: #6b7280; - border-radius: 50%; - animation: typing 1.4s infinite ease-in-out; -} - -.typing-indicator span:nth-child(1) { - animation-delay: 0s; -} - -.typing-indicator span:nth-child(2) { - animation-delay: 0.2s; -} - -.typing-indicator span:nth-child(3) { - animation-delay: 0.4s; -} - -@keyframes typing { - 0%, - 80%, - 100% { - transform: scale(1); - opacity: 0.5; - } - 40% { - transform: scale(1.2); - opacity: 1; - } +.no-drag { + -webkit-app-region: no-drag; } diff --git a/packages/desktop/tailwind.config.js b/packages/desktop/tailwind.config.js index 49b0b9c..2256352 100644 --- a/packages/desktop/tailwind.config.js +++ b/packages/desktop/tailwind.config.js @@ -9,6 +9,26 @@ export default { theme: { extend: { colors: { + // 语义化颜色 (引用 CSS 变量) + surface: { + base: 'rgb(var(--color-bg-base) / )', + subtle: 'rgb(var(--color-bg-subtle) / )', + muted: 'rgb(var(--color-bg-muted) / )', + emphasis: 'rgb(var(--color-bg-emphasis) / )', + }, + fg: { + DEFAULT: 'rgb(var(--color-text-primary) / )', + secondary: 'rgb(var(--color-text-secondary) / )', + muted: 'rgb(var(--color-text-muted) / )', + subtle: 'rgb(var(--color-text-subtle) / )', + }, + line: { + DEFAULT: 'rgb(var(--color-border-default) / )', + muted: 'rgb(var(--color-border-muted) / )', + }, + // 代码块背景 + code: 'rgb(var(--color-code-bg) / )', + // 保留现有 primary 色板 primary: { 50: '#f0f9ff', 100: '#e0f2fe', diff --git a/packages/web/src/App.tsx b/packages/web/src/App.tsx index 218a03b..4072991 100644 --- a/packages/web/src/App.tsx +++ b/packages/web/src/App.tsx @@ -64,8 +64,6 @@ export function App() { // 初始化:加载会话 useEffect(() => { - const HAS_SESSIONS_KEY = 'ai-assistant-has-sessions'; - async function init() { try { const sessionsResult = await listSessions(); @@ -74,18 +72,11 @@ export function App() { if (sessions.length > 0) { // 有会话,选择最近的 setCurrentSessionId(sessions[0].id); - localStorage.setItem(HAS_SESSIONS_KEY, 'true'); } else { - // 无会话:检查是否是首次启动 - const hasHadSessions = localStorage.getItem(HAS_SESSIONS_KEY); - - if (!hasHadSessions) { - // 首次启动,自动创建会话 - const { data: newSession } = await createSession(); - setCurrentSessionId(newSession.id); - localStorage.setItem(HAS_SESSIONS_KEY, 'true'); - } - // 用户删除了所有会话:不自动创建,显示空状态 + // 无会话,自动创建一个新会话 + // 这处理了新 project 目录的情况 + const { data: newSession } = await createSession(); + setCurrentSessionId(newSession.id); } } catch (error) { console.error('Failed to initialize:', error);