feat(desktop): 同步 web 模块界面和修复会话初始化
- 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: 同步修复会话初始化逻辑
This commit is contained in:
+228
-89
@@ -1,64 +1,87 @@
|
|||||||
/**
|
/**
|
||||||
* App Component
|
* App Component
|
||||||
|
*
|
||||||
|
* 响应式布局:支持桌面端和移动端
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { useState, useEffect, useCallback } from 'react';
|
import { useState, useEffect, useCallback } from 'react';
|
||||||
import {
|
import {
|
||||||
Sidebar,
|
IDE,
|
||||||
FileBrowser,
|
|
||||||
ConfigPanel,
|
|
||||||
CommandPanel,
|
CommandPanel,
|
||||||
MCPPanel,
|
MCPPanel,
|
||||||
HooksPanel,
|
HooksPanel,
|
||||||
AgentsPanel,
|
AgentsPanel,
|
||||||
CheckpointPanel,
|
CheckpointPanel,
|
||||||
ProvidersPanel,
|
ProvidersPanel,
|
||||||
|
LSPPanel,
|
||||||
|
DiagnosticsPanel,
|
||||||
|
SessionPanel,
|
||||||
|
StatusBar,
|
||||||
|
Resizer,
|
||||||
Toaster,
|
Toaster,
|
||||||
|
ThemeProvider,
|
||||||
listSessions,
|
listSessions,
|
||||||
createSession,
|
createSession,
|
||||||
type Session,
|
type Session,
|
||||||
|
type ActiveFileInfo,
|
||||||
|
type FileDiffInfo,
|
||||||
} from '@ai-assistant/ui';
|
} from '@ai-assistant/ui';
|
||||||
import { ChatPage } from './pages/Chat';
|
import { ChatPage } from './pages/Chat';
|
||||||
|
|
||||||
export function App() {
|
export function App() {
|
||||||
const [currentSessionId, setCurrentSessionId] = useState<string | null>(null);
|
const [currentSessionId, setCurrentSessionId] = useState<string | null>(null);
|
||||||
const [isInitializing, setIsInitializing] = useState(true);
|
const [isInitializing, setIsInitializing] = useState(true);
|
||||||
const [showFileBrowser, setShowFileBrowser] = useState(false);
|
const [connectionError, setConnectionError] = useState<string | null>(null);
|
||||||
const [showConfig, setShowConfig] = useState(false);
|
|
||||||
const [showCommands, setShowCommands] = useState(false);
|
const [showCommands, setShowCommands] = useState(false);
|
||||||
const [showMCP, setShowMCP] = useState(false);
|
const [showMCP, setShowMCP] = useState(false);
|
||||||
const [showHooks, setShowHooks] = useState(false);
|
const [showHooks, setShowHooks] = useState(false);
|
||||||
const [showAgents, setShowAgents] = useState(false);
|
const [showAgents, setShowAgents] = useState(false);
|
||||||
const [showCheckpoints, setShowCheckpoints] = useState(false);
|
const [showCheckpoints, setShowCheckpoints] = useState(false);
|
||||||
const [showProviders, setShowProviders] = 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);
|
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<ActiveFileInfo | null>(null);
|
||||||
|
const [autoAttachActiveFile, setAutoAttachActiveFile] = useState(() => {
|
||||||
|
const saved = localStorage.getItem('ai-assistant-auto-attach-file');
|
||||||
|
return saved !== 'false'; // 默认开启
|
||||||
|
});
|
||||||
|
|
||||||
|
// Diff 显示状态(当 AI 编辑/写入文件时触发)
|
||||||
|
const [pendingDiff, setPendingDiff] = useState<FileDiffInfo | null>(null);
|
||||||
|
|
||||||
|
// 持久化自动附加开关状态
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const HAS_SESSIONS_KEY = 'ai-assistant-has-sessions';
|
localStorage.setItem('ai-assistant-auto-attach-file', String(autoAttachActiveFile));
|
||||||
|
}, [autoAttachActiveFile]);
|
||||||
|
|
||||||
|
// 初始化:加载会话
|
||||||
|
useEffect(() => {
|
||||||
async function init() {
|
async function init() {
|
||||||
try {
|
try {
|
||||||
const { data: sessions } = await listSessions();
|
const sessionsResult = await listSessions();
|
||||||
|
const { data: sessions } = sessionsResult;
|
||||||
|
|
||||||
if (sessions.length > 0) {
|
if (sessions.length > 0) {
|
||||||
// 有会话,选择最近的
|
// 有会话,选择最近的
|
||||||
setCurrentSessionId(sessions[0].id);
|
setCurrentSessionId(sessions[0].id);
|
||||||
localStorage.setItem(HAS_SESSIONS_KEY, 'true');
|
|
||||||
} else {
|
} else {
|
||||||
// 无会话:检查是否是首次启动
|
// 无会话,自动创建一个新会话
|
||||||
const hasHadSessions = localStorage.getItem(HAS_SESSIONS_KEY);
|
// 这处理了新 project 目录的情况
|
||||||
|
const { data: newSession } = await createSession();
|
||||||
if (!hasHadSessions) {
|
setCurrentSessionId(newSession.id);
|
||||||
// 首次启动,自动创建会话
|
|
||||||
const { data: newSession } = await createSession();
|
|
||||||
setCurrentSessionId(newSession.id);
|
|
||||||
localStorage.setItem(HAS_SESSIONS_KEY, 'true');
|
|
||||||
}
|
|
||||||
// 用户删除了所有会话:不自动创建,显示空状态
|
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Failed to initialize:', error);
|
console.error('Failed to initialize:', error);
|
||||||
|
setConnectionError('无法连接到服务器。请确保后端服务已启动 (pnpm server:dev)');
|
||||||
} finally {
|
} finally {
|
||||||
setIsInitializing(false);
|
setIsInitializing(false);
|
||||||
}
|
}
|
||||||
@@ -75,91 +98,207 @@ export function App() {
|
|||||||
setCurrentSessionId(session.id);
|
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) => {
|
const handleSessionUpdated = useCallback((sessionId: string, name: string) => {
|
||||||
setSessionTitleUpdate({ sessionId, name });
|
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) {
|
if (isInitializing) {
|
||||||
return (
|
return (
|
||||||
<div className="h-screen flex items-center justify-center bg-gray-900">
|
<ThemeProvider defaultTheme="dark">
|
||||||
<div className="text-center">
|
<div className="h-screen flex items-center justify-center bg-surface-base">
|
||||||
<div className="w-8 h-8 border-2 border-primary-500 border-t-transparent rounded-full animate-spin mx-auto mb-4" />
|
<div className="text-center">
|
||||||
<p className="text-gray-400">Initializing...</p>
|
<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-fg-muted">Initializing...</p>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</ThemeProvider>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="h-screen flex bg-gray-900">
|
<ThemeProvider defaultTheme="dark">
|
||||||
<Sidebar
|
<div className="h-screen flex flex-col bg-surface-base">
|
||||||
currentSessionId={currentSessionId}
|
{/* 主内容区域:左侧文件浏览器 + 右侧对话框 */}
|
||||||
onSelectSession={handleSelectSession}
|
<div className="flex-1 flex min-w-0 overflow-hidden">
|
||||||
onCreateSession={handleCreateSession}
|
{/* 左侧:IDE(文件浏览器 + 代码编辑器) */}
|
||||||
sessionTitleUpdate={sessionTitleUpdate}
|
<div
|
||||||
/>
|
className="hidden md:flex flex-col"
|
||||||
|
style={{ width: `${idePanelWidth}%` }}
|
||||||
<div className="flex-1 flex">
|
>
|
||||||
{/* 聊天区域 */}
|
<IDE
|
||||||
<div className={`flex-1 ${showFileBrowser ? 'w-1/2' : 'w-full'}`}>
|
onActiveFileChange={setActiveFile}
|
||||||
{currentSessionId ? (
|
pendingDiff={pendingDiff}
|
||||||
<ChatPage
|
onDiffClose={handleDiffClose}
|
||||||
key={currentSessionId}
|
|
||||||
sessionId={currentSessionId}
|
|
||||||
onSessionUpdated={handleSessionUpdated}
|
|
||||||
showFileBrowser={showFileBrowser}
|
|
||||||
onToggleFileBrowser={() => setShowFileBrowser(!showFileBrowser)}
|
|
||||||
onOpenConfig={() => setShowConfig(true)}
|
|
||||||
onOpenCommands={() => setShowCommands(true)}
|
|
||||||
onOpenMCP={() => setShowMCP(true)}
|
|
||||||
onOpenHooks={() => setShowHooks(true)}
|
|
||||||
onOpenAgents={() => setShowAgents(true)}
|
|
||||||
onOpenCheckpoints={() => setShowCheckpoints(true)}
|
|
||||||
onOpenProviders={() => setShowProviders(true)}
|
|
||||||
/>
|
|
||||||
) : (
|
|
||||||
<div className="flex-1 flex items-center justify-center h-full">
|
|
||||||
<p className="text-gray-400">Select or create a session</p>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* 文件浏览器 */}
|
|
||||||
{showFileBrowser && (
|
|
||||||
<div className="w-1/2 border-l border-gray-700">
|
|
||||||
<FileBrowser
|
|
||||||
onFileSelect={(path, _content) => {
|
|
||||||
console.log('Selected file:', path);
|
|
||||||
}}
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* 可拖拽分割线 */}
|
||||||
|
<Resizer
|
||||||
|
onResize={handleResize}
|
||||||
|
onResizeEnd={handleResizeEnd}
|
||||||
|
className="hidden md:block"
|
||||||
|
/>
|
||||||
|
|
||||||
|
{/* 右侧:聊天区域 */}
|
||||||
|
<div className="flex-1 min-w-0">
|
||||||
|
{connectionError ? (
|
||||||
|
<div className="flex-1 flex flex-col items-center justify-center h-full gap-4 p-8">
|
||||||
|
<div className="w-16 h-16 rounded-full bg-red-500/10 flex items-center justify-center">
|
||||||
|
<svg className="w-8 h-8 text-red-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z" />
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
<div className="text-center max-w-md">
|
||||||
|
<h3 className="text-lg font-semibold text-fg mb-2">连接失败</h3>
|
||||||
|
<p className="text-fg-muted text-sm mb-4">{connectionError}</p>
|
||||||
|
<button
|
||||||
|
onClick={() => {
|
||||||
|
setConnectionError(null);
|
||||||
|
setIsInitializing(true);
|
||||||
|
window.location.reload();
|
||||||
|
}}
|
||||||
|
className="px-4 py-2 bg-primary-500 hover:bg-primary-600 text-white rounded-lg transition-colors"
|
||||||
|
>
|
||||||
|
重试连接
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
) : currentSessionId ? (
|
||||||
|
<ChatPage
|
||||||
|
key={currentSessionId}
|
||||||
|
sessionId={currentSessionId}
|
||||||
|
onSessionNotFound={handleSessionNotFound}
|
||||||
|
onSessionUpdated={handleSessionUpdated}
|
||||||
|
onSessionSwitch={handleSessionSwitch}
|
||||||
|
responsive
|
||||||
|
onOpenCommands={() => 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}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<div className="flex-1 flex items-center justify-center h-full">
|
||||||
|
<p className="text-fg-muted">Select or create a session</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 底部状态栏 */}
|
||||||
|
<StatusBar
|
||||||
|
sessionId={currentSessionId}
|
||||||
|
onDiagnosticsClick={() => setShowDiagnostics(true)}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{/* 命令面板 */}
|
||||||
|
{showCommands && <CommandPanel onClose={() => setShowCommands(false)} responsive />}
|
||||||
|
|
||||||
|
{/* MCP 面板 */}
|
||||||
|
{showMCP && <MCPPanel onClose={() => setShowMCP(false)} responsive />}
|
||||||
|
|
||||||
|
{/* Hooks 面板 */}
|
||||||
|
{showHooks && <HooksPanel onClose={() => setShowHooks(false)} responsive />}
|
||||||
|
|
||||||
|
{/* Agents 面板 */}
|
||||||
|
{showAgents && <AgentsPanel onClose={() => setShowAgents(false)} responsive />}
|
||||||
|
|
||||||
|
{/* Checkpoints 面板 */}
|
||||||
|
{showCheckpoints && <CheckpointPanel onClose={() => setShowCheckpoints(false)} responsive />}
|
||||||
|
|
||||||
|
{/* Providers 面板 */}
|
||||||
|
{showProviders && <ProvidersPanel onClose={() => setShowProviders(false)} responsive />}
|
||||||
|
|
||||||
|
{/* LSP 面板 */}
|
||||||
|
{showLSP && (
|
||||||
|
<LSPPanel
|
||||||
|
onClose={() => setShowLSP(false)}
|
||||||
|
onOpenDiagnostics={() => {
|
||||||
|
setShowLSP(false);
|
||||||
|
setShowDiagnostics(true);
|
||||||
|
}}
|
||||||
|
responsive
|
||||||
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{/* Diagnostics 面板 */}
|
||||||
|
{showDiagnostics && (
|
||||||
|
<DiagnosticsPanel
|
||||||
|
onClose={() => setShowDiagnostics(false)}
|
||||||
|
onFileClick={(file, line) => {
|
||||||
|
console.log('Navigate to:', file, line);
|
||||||
|
// TODO: Integrate with file browser or editor
|
||||||
|
}}
|
||||||
|
responsive
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Sessions 面板 */}
|
||||||
|
{showSessions && (
|
||||||
|
<SessionPanel
|
||||||
|
onClose={() => setShowSessions(false)}
|
||||||
|
currentSessionId={currentSessionId}
|
||||||
|
onSelectSession={handleSelectSession}
|
||||||
|
onCreateSession={handleCreateSession}
|
||||||
|
sessionTitleUpdate={sessionTitleUpdate}
|
||||||
|
responsive
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Toast 通知 */}
|
||||||
|
<Toaster />
|
||||||
</div>
|
</div>
|
||||||
|
</ThemeProvider>
|
||||||
{/* 配置面板 */}
|
|
||||||
{showConfig && <ConfigPanel onClose={() => setShowConfig(false)} />}
|
|
||||||
|
|
||||||
{/* 命令面板 */}
|
|
||||||
{showCommands && <CommandPanel onClose={() => setShowCommands(false)} />}
|
|
||||||
|
|
||||||
{/* MCP 面板 */}
|
|
||||||
{showMCP && <MCPPanel onClose={() => setShowMCP(false)} />}
|
|
||||||
|
|
||||||
{/* Hooks 面板 */}
|
|
||||||
{showHooks && <HooksPanel onClose={() => setShowHooks(false)} />}
|
|
||||||
|
|
||||||
{/* Agents 面板 */}
|
|
||||||
{showAgents && <AgentsPanel onClose={() => setShowAgents(false)} />}
|
|
||||||
|
|
||||||
{/* Checkpoints 面板 */}
|
|
||||||
{showCheckpoints && <CheckpointPanel onClose={() => setShowCheckpoints(false)} />}
|
|
||||||
|
|
||||||
{/* Providers 面板 */}
|
|
||||||
{showProviders && <ProvidersPanel onClose={() => setShowProviders(false)} />}
|
|
||||||
|
|
||||||
{/* Toast 通知 */}
|
|
||||||
<Toaster />
|
|
||||||
</div>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
+127
-155
@@ -3,7 +3,7 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import { useEffect, useRef } from 'react';
|
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 { motion, AnimatePresence } from 'framer-motion';
|
||||||
import { toast } from 'sonner';
|
import { toast } from 'sonner';
|
||||||
import {
|
import {
|
||||||
@@ -11,38 +11,64 @@ import {
|
|||||||
ChatMessage,
|
ChatMessage,
|
||||||
TypingIndicator,
|
TypingIndicator,
|
||||||
ChatInput,
|
ChatInput,
|
||||||
|
ContextUsage,
|
||||||
|
SubagentProgress,
|
||||||
|
DiagnosticsIndicator,
|
||||||
|
ToolbarOverflowMenu,
|
||||||
|
type ActiveFileInfo,
|
||||||
|
type FileDiffInfo,
|
||||||
} from '@ai-assistant/ui';
|
} from '@ai-assistant/ui';
|
||||||
|
|
||||||
interface ChatPageProps {
|
interface ChatPageProps {
|
||||||
sessionId: string;
|
sessionId: string;
|
||||||
|
onSessionNotFound?: () => void;
|
||||||
onSessionUpdated?: (sessionId: string, name: string) => void;
|
onSessionUpdated?: (sessionId: string, name: string) => void;
|
||||||
/** 切换会话回调(如 :new 命令创建新会话) */
|
/** 切换会话回调(如 :new 命令创建新会话) */
|
||||||
onSessionSwitch?: (newSessionId: string) => void;
|
onSessionSwitch?: (newSessionId: string) => void;
|
||||||
|
responsive?: boolean;
|
||||||
// 工具栏按钮
|
// 工具栏按钮
|
||||||
showFileBrowser?: boolean;
|
|
||||||
onToggleFileBrowser?: () => void;
|
|
||||||
onOpenConfig?: () => void;
|
|
||||||
onOpenCommands?: () => void;
|
onOpenCommands?: () => void;
|
||||||
onOpenMCP?: () => void;
|
onOpenMCP?: () => void;
|
||||||
onOpenHooks?: () => void;
|
onOpenHooks?: () => void;
|
||||||
onOpenAgents?: () => void;
|
onOpenAgents?: () => void;
|
||||||
onOpenCheckpoints?: () => void;
|
onOpenCheckpoints?: () => void;
|
||||||
onOpenProviders?: () => 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({
|
export function ChatPage({
|
||||||
sessionId,
|
sessionId,
|
||||||
|
onSessionNotFound,
|
||||||
onSessionUpdated,
|
onSessionUpdated,
|
||||||
onSessionSwitch,
|
onSessionSwitch,
|
||||||
showFileBrowser,
|
responsive = false,
|
||||||
onToggleFileBrowser,
|
|
||||||
onOpenConfig,
|
|
||||||
onOpenCommands,
|
onOpenCommands,
|
||||||
onOpenMCP,
|
onOpenMCP,
|
||||||
onOpenHooks,
|
onOpenHooks,
|
||||||
onOpenAgents,
|
onOpenAgents,
|
||||||
onOpenCheckpoints,
|
onOpenCheckpoints,
|
||||||
onOpenProviders,
|
onOpenProviders,
|
||||||
|
onOpenLSP,
|
||||||
|
onOpenDiagnostics,
|
||||||
|
onOpenSessions,
|
||||||
|
activeFile,
|
||||||
|
autoAttachActiveFile,
|
||||||
|
onAutoAttachActiveFileToggle,
|
||||||
|
onFileDiff,
|
||||||
|
onViewDiff,
|
||||||
}: ChatPageProps) {
|
}: ChatPageProps) {
|
||||||
const {
|
const {
|
||||||
messages,
|
messages,
|
||||||
@@ -51,12 +77,21 @@ export function ChatPage({
|
|||||||
streamingMessage,
|
streamingMessage,
|
||||||
sendMessage,
|
sendMessage,
|
||||||
cancelProcessing,
|
cancelProcessing,
|
||||||
|
allowPermission,
|
||||||
|
denyPermission,
|
||||||
|
agentMode,
|
||||||
|
autoApprove,
|
||||||
|
setAgentMode,
|
||||||
|
setAutoApprove,
|
||||||
|
currentAgent,
|
||||||
|
currentSubagent,
|
||||||
answerQuestion,
|
answerQuestion,
|
||||||
} = useChat({
|
} = useChat({
|
||||||
sessionId,
|
sessionId,
|
||||||
onError: (error) => {
|
onError: (error) => {
|
||||||
console.error('Chat error:', error);
|
console.error('Chat error:', error);
|
||||||
},
|
},
|
||||||
|
onSessionNotFound,
|
||||||
onSessionUpdated,
|
onSessionUpdated,
|
||||||
onSessionSwitch,
|
onSessionSwitch,
|
||||||
onConfigError: (error) => {
|
onConfigError: (error) => {
|
||||||
@@ -70,6 +105,7 @@ export function ChatPage({
|
|||||||
: undefined,
|
: undefined,
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
onFileDiff,
|
||||||
});
|
});
|
||||||
|
|
||||||
const messagesEndRef = useRef<HTMLDivElement>(null);
|
const messagesEndRef = useRef<HTMLDivElement>(null);
|
||||||
@@ -91,11 +127,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>
|
||||||
|
|
||||||
@@ -106,7 +142,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>
|
||||||
@@ -115,152 +151,60 @@ export function ChatPage({
|
|||||||
</motion.div>
|
</motion.div>
|
||||||
);
|
);
|
||||||
|
|
||||||
// 连接状态指示器
|
|
||||||
const ConnectionStatus = () => (
|
|
||||||
<div className="flex items-center gap-1.5 text-sm">
|
|
||||||
{isConnected ? (
|
|
||||||
<motion.div
|
|
||||||
initial={{ opacity: 0 }}
|
|
||||||
animate={{ opacity: 1 }}
|
|
||||||
className="flex items-center gap-1.5"
|
|
||||||
>
|
|
||||||
<span className="relative flex h-2.5 w-2.5">
|
|
||||||
<span className="animate-ping absolute inline-flex h-full w-full rounded-full bg-green-400 opacity-75" />
|
|
||||||
<span className="relative inline-flex rounded-full h-2.5 w-2.5 bg-green-500" />
|
|
||||||
</span>
|
|
||||||
<span className="text-green-400">Connected</span>
|
|
||||||
</motion.div>
|
|
||||||
) : (
|
|
||||||
<div className="flex items-center gap-1.5 text-red-400">
|
|
||||||
<WifiOff size={16} />
|
|
||||||
<span>Disconnected</span>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex-1 flex flex-col h-screen">
|
<div className="flex-1 flex flex-col h-full">
|
||||||
{/* Header */}
|
{/* Header */}
|
||||||
<div className="flex items-center justify-between 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>
|
{/* 左侧:上下文使用情况 */}
|
||||||
<div className="flex items-center gap-3">
|
<div className="flex items-center">
|
||||||
{/* 连接状态 */}
|
{sessionId && (
|
||||||
<ConnectionStatus />
|
<ContextUsage
|
||||||
|
sessionId={sessionId}
|
||||||
{/* 工具栏按钮 */}
|
compact
|
||||||
{(onOpenConfig || onToggleFileBrowser || onOpenCommands || onOpenMCP || onOpenHooks || onOpenAgents || onOpenCheckpoints || onOpenProviders) && (
|
showCompressButton
|
||||||
<div className="flex items-center gap-1.5 border-l border-gray-600 pl-3">
|
refreshInterval={30000}
|
||||||
{/* Checkpoints 按钮 */}
|
/>
|
||||||
{onOpenCheckpoints && (
|
|
||||||
<motion.button
|
|
||||||
whileHover={{ scale: 1.1 }}
|
|
||||||
whileTap={{ scale: 0.9 }}
|
|
||||||
onClick={onOpenCheckpoints}
|
|
||||||
className="p-1.5 rounded-lg text-gray-400 hover:text-gray-200 hover:bg-gray-700 transition-colors"
|
|
||||||
title="Checkpoints"
|
|
||||||
>
|
|
||||||
<History size={20} />
|
|
||||||
</motion.button>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* Providers 按钮 */}
|
|
||||||
{onOpenProviders && (
|
|
||||||
<motion.button
|
|
||||||
whileHover={{ scale: 1.1 }}
|
|
||||||
whileTap={{ scale: 0.9 }}
|
|
||||||
onClick={onOpenProviders}
|
|
||||||
className="p-1.5 rounded-lg text-gray-400 hover:text-gray-200 hover:bg-gray-700 transition-colors"
|
|
||||||
title="Model Providers"
|
|
||||||
>
|
|
||||||
<Server size={20} />
|
|
||||||
</motion.button>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* Agents 按钮 */}
|
|
||||||
{onOpenAgents && (
|
|
||||||
<motion.button
|
|
||||||
whileHover={{ scale: 1.1 }}
|
|
||||||
whileTap={{ scale: 0.9 }}
|
|
||||||
onClick={onOpenAgents}
|
|
||||||
className="p-1.5 rounded-lg text-gray-400 hover:text-gray-200 hover:bg-gray-700 transition-colors"
|
|
||||||
title="Agent Presets"
|
|
||||||
>
|
|
||||||
<Bot size={20} />
|
|
||||||
</motion.button>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* Hooks 按钮 */}
|
|
||||||
{onOpenHooks && (
|
|
||||||
<motion.button
|
|
||||||
whileHover={{ scale: 1.1 }}
|
|
||||||
whileTap={{ scale: 0.9 }}
|
|
||||||
onClick={onOpenHooks}
|
|
||||||
className="p-1.5 rounded-lg text-gray-400 hover:text-gray-200 hover:bg-gray-700 transition-colors"
|
|
||||||
title="Hooks"
|
|
||||||
>
|
|
||||||
<Zap size={20} />
|
|
||||||
</motion.button>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* MCP 按钮 */}
|
|
||||||
{onOpenMCP && (
|
|
||||||
<motion.button
|
|
||||||
whileHover={{ scale: 1.1 }}
|
|
||||||
whileTap={{ scale: 0.9 }}
|
|
||||||
onClick={onOpenMCP}
|
|
||||||
className="p-1.5 rounded-lg text-gray-400 hover:text-gray-200 hover:bg-gray-700 transition-colors"
|
|
||||||
title="MCP Servers"
|
|
||||||
>
|
|
||||||
<Plug size={20} />
|
|
||||||
</motion.button>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* 命令按钮 */}
|
|
||||||
{onOpenCommands && (
|
|
||||||
<motion.button
|
|
||||||
whileHover={{ scale: 1.1 }}
|
|
||||||
whileTap={{ scale: 0.9 }}
|
|
||||||
onClick={onOpenCommands}
|
|
||||||
className="p-1.5 rounded-lg text-gray-400 hover:text-gray-200 hover:bg-gray-700 transition-colors"
|
|
||||||
title="Commands"
|
|
||||||
>
|
|
||||||
<Terminal size={20} />
|
|
||||||
</motion.button>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* 配置按钮 */}
|
|
||||||
{onOpenConfig && (
|
|
||||||
<motion.button
|
|
||||||
whileHover={{ scale: 1.1 }}
|
|
||||||
whileTap={{ scale: 0.9 }}
|
|
||||||
onClick={onOpenConfig}
|
|
||||||
className="p-1.5 rounded-lg text-gray-400 hover:text-gray-200 hover:bg-gray-700 transition-colors"
|
|
||||||
title="Settings"
|
|
||||||
>
|
|
||||||
<Settings size={20} />
|
|
||||||
</motion.button>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* 文件浏览器按钮 */}
|
|
||||||
{onToggleFileBrowser && (
|
|
||||||
<motion.button
|
|
||||||
whileHover={{ scale: 1.1 }}
|
|
||||||
whileTap={{ scale: 0.9 }}
|
|
||||||
onClick={onToggleFileBrowser}
|
|
||||||
className={`p-1.5 rounded-lg transition-colors ${
|
|
||||||
showFileBrowser
|
|
||||||
? 'text-blue-400 bg-blue-500/20'
|
|
||||||
: 'text-gray-400 hover:text-gray-200 hover:bg-gray-700'
|
|
||||||
}`}
|
|
||||||
title={showFileBrowser ? 'Hide Files' : 'Show Files'}
|
|
||||||
>
|
|
||||||
<FolderOpen size={20} />
|
|
||||||
</motion.button>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* 右侧:工具栏按钮 */}
|
||||||
|
{(onOpenCommands || onOpenMCP || onOpenHooks || onOpenAgents || onOpenCheckpoints || onOpenProviders || onOpenLSP || onOpenDiagnostics || onOpenSessions) && (
|
||||||
|
<div className="flex items-center gap-1.5 flex-shrink-0">
|
||||||
|
{/* LSP 诊断指示器 */}
|
||||||
|
{(onOpenLSP || onOpenDiagnostics) && (
|
||||||
|
<DiagnosticsIndicator
|
||||||
|
onClickDiagnostics={onOpenDiagnostics}
|
||||||
|
onClickLSP={onOpenLSP}
|
||||||
|
refreshInterval={30000}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Sessions 按钮 */}
|
||||||
|
{onOpenSessions && (
|
||||||
|
<motion.button
|
||||||
|
whileHover={{ scale: 1.1 }}
|
||||||
|
whileTap={{ scale: 0.9 }}
|
||||||
|
onClick={onOpenSessions}
|
||||||
|
className="p-1.5 rounded-lg text-fg-muted hover:text-fg-secondary hover:bg-surface-muted transition-colors"
|
||||||
|
title="Sessions"
|
||||||
|
>
|
||||||
|
<MessagesSquare size={20} />
|
||||||
|
</motion.button>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* 设置菜单 - 齿轮图标,放在最右侧 */}
|
||||||
|
<ToolbarOverflowMenu
|
||||||
|
items={[
|
||||||
|
{ icon: History, label: 'Checkpoints', onClick: onOpenCheckpoints },
|
||||||
|
{ icon: Server, label: 'Model Providers', onClick: onOpenProviders },
|
||||||
|
{ icon: Bot, label: 'Agent Presets', onClick: onOpenAgents },
|
||||||
|
{ icon: Zap, label: 'Hooks', onClick: onOpenHooks },
|
||||||
|
{ icon: Plug, label: 'MCP Servers', onClick: onOpenMCP },
|
||||||
|
{ icon: Terminal, label: 'Commands', onClick: onOpenCommands },
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Messages */}
|
{/* Messages */}
|
||||||
@@ -270,16 +214,35 @@ export function ChatPage({
|
|||||||
|
|
||||||
<AnimatePresence mode="popLayout">
|
<AnimatePresence mode="popLayout">
|
||||||
{messages.map((message) => (
|
{messages.map((message) => (
|
||||||
<ChatMessage key={message.id} message={message} onAnswerQuestion={answerQuestion} />
|
<ChatMessage
|
||||||
|
key={message.id}
|
||||||
|
message={message}
|
||||||
|
onAnswerQuestion={answerQuestion}
|
||||||
|
onViewDiff={onViewDiff ?? onFileDiff}
|
||||||
|
onAllowPermission={allowPermission}
|
||||||
|
onDenyPermission={denyPermission}
|
||||||
|
/>
|
||||||
))}
|
))}
|
||||||
</AnimatePresence>
|
</AnimatePresence>
|
||||||
|
|
||||||
{/* 流式消息 - 复用 ChatMessage 组件 */}
|
{/* 流式消息 - 复用 ChatMessage 组件 */}
|
||||||
{streamingMessage && (
|
{streamingMessage && (
|
||||||
<ChatMessage message={streamingMessage} isStreaming onAnswerQuestion={answerQuestion} />
|
<ChatMessage
|
||||||
|
message={streamingMessage}
|
||||||
|
isStreaming
|
||||||
|
onAnswerQuestion={answerQuestion}
|
||||||
|
onViewDiff={onViewDiff ?? onFileDiff}
|
||||||
|
onAllowPermission={allowPermission}
|
||||||
|
onDenyPermission={denyPermission}
|
||||||
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{isLoading && !streamingMessage && <TypingIndicator />}
|
{/* 子 Agent 进度显示 */}
|
||||||
|
{currentSubagent && (
|
||||||
|
<SubagentProgress subagent={currentSubagent} />
|
||||||
|
)}
|
||||||
|
|
||||||
|
{isLoading && !streamingMessage && !currentSubagent && <TypingIndicator agentName={currentAgent} />}
|
||||||
|
|
||||||
<div ref={messagesEndRef} />
|
<div ref={messagesEndRef} />
|
||||||
</div>
|
</div>
|
||||||
@@ -291,7 +254,16 @@ export function ChatPage({
|
|||||||
onCancel={cancelProcessing}
|
onCancel={cancelProcessing}
|
||||||
isLoading={isLoading}
|
isLoading={isLoading}
|
||||||
disabled={!isConnected}
|
disabled={!isConnected}
|
||||||
|
responsive={responsive}
|
||||||
|
agentMode={agentMode}
|
||||||
|
onAgentModeChange={setAgentMode}
|
||||||
|
autoApprove={autoApprove}
|
||||||
|
onAutoApproveChange={setAutoApprove}
|
||||||
|
activeFile={activeFile}
|
||||||
|
autoAttachActiveFile={autoAttachActiveFile}
|
||||||
|
onAutoAttachActiveFileToggle={onAutoAttachActiveFileToggle}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,77 +2,14 @@
|
|||||||
@tailwind components;
|
@tailwind components;
|
||||||
@tailwind utilities;
|
@tailwind utilities;
|
||||||
|
|
||||||
/* Custom scrollbar */
|
/* 导入 UI 包的共享样式会在 main.tsx 中通过 import '@ai-assistant/ui/styles' 完成 */
|
||||||
::-webkit-scrollbar {
|
/* 此文件仅包含 desktop 特有的样式覆盖 */
|
||||||
width: 8px;
|
|
||||||
height: 8px;
|
/* Desktop 特有的 Tauri 窗口拖拽区域 */
|
||||||
|
.titlebar-drag-region {
|
||||||
|
-webkit-app-region: drag;
|
||||||
}
|
}
|
||||||
|
|
||||||
::-webkit-scrollbar-track {
|
.no-drag {
|
||||||
background: transparent;
|
-webkit-app-region: no-drag;
|
||||||
}
|
|
||||||
|
|
||||||
::-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;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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',
|
||||||
|
|||||||
@@ -64,8 +64,6 @@ export function App() {
|
|||||||
|
|
||||||
// 初始化:加载会话
|
// 初始化:加载会话
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const HAS_SESSIONS_KEY = 'ai-assistant-has-sessions';
|
|
||||||
|
|
||||||
async function init() {
|
async function init() {
|
||||||
try {
|
try {
|
||||||
const sessionsResult = await listSessions();
|
const sessionsResult = await listSessions();
|
||||||
@@ -74,18 +72,11 @@ export function App() {
|
|||||||
if (sessions.length > 0) {
|
if (sessions.length > 0) {
|
||||||
// 有会话,选择最近的
|
// 有会话,选择最近的
|
||||||
setCurrentSessionId(sessions[0].id);
|
setCurrentSessionId(sessions[0].id);
|
||||||
localStorage.setItem(HAS_SESSIONS_KEY, 'true');
|
|
||||||
} else {
|
} else {
|
||||||
// 无会话:检查是否是首次启动
|
// 无会话,自动创建一个新会话
|
||||||
const hasHadSessions = localStorage.getItem(HAS_SESSIONS_KEY);
|
// 这处理了新 project 目录的情况
|
||||||
|
const { data: newSession } = await createSession();
|
||||||
if (!hasHadSessions) {
|
setCurrentSessionId(newSession.id);
|
||||||
// 首次启动,自动创建会话
|
|
||||||
const { data: newSession } = await createSession();
|
|
||||||
setCurrentSessionId(newSession.id);
|
|
||||||
localStorage.setItem(HAS_SESSIONS_KEY, 'true');
|
|
||||||
}
|
|
||||||
// 用户删除了所有会话:不自动创建,显示空状态
|
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Failed to initialize:', error);
|
console.error('Failed to initialize:', error);
|
||||||
|
|||||||
Reference in New Issue
Block a user