From 250d2cb4b523cf2736544f9254b51ed261aac2a3 Mon Sep 17 00:00:00 2001 From: kurihada Date: Wed, 17 Dec 2025 16:55:22 +0800 Subject: [PATCH] =?UTF-8?q?feat(ui):=20=E6=B7=BB=E5=8A=A0=20IDE=20?= =?UTF-8?q?=E7=BB=84=E4=BB=B6=EF=BC=88=E6=96=87=E4=BB=B6=E6=B5=8F=E8=A7=88?= =?UTF-8?q?=E5=99=A8=20+=20=E4=BB=A3=E7=A0=81=E7=BC=96=E8=BE=91=E5=99=A8?= =?UTF-8?q?=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 新增 CodeEditor 组件,基于 CodeMirror 实现多标签代码编辑 - 新增 FileExplorer 组件,支持文件树展开/折叠和文件选择 - 新增 IDE 组件,整合文件浏览器和代码编辑器 - 新增 SessionPanel 组件,用于会话管理 - 添加文件写入 API(PUT /api/files/write) - 优化布局:IDE 始终显示,移除文件切换按钮 - 工作目录路径显示在文件浏览器标题栏,支持悬浮显示完整路径 --- packages/server/src/routes/files.ts | 48 ++- packages/ui/package.json | 8 + packages/ui/src/api/client.ts | 14 + packages/ui/src/components/CodeEditor.tsx | 286 +++++++++++++++++ packages/ui/src/components/FileExplorer.tsx | 253 +++++++++++++++ packages/ui/src/components/IDE.tsx | 142 +++++++++ packages/ui/src/components/SessionPanel.tsx | 258 ++++++++++++++++ packages/ui/src/index.ts | 6 + packages/web/src/App.tsx | 106 ++----- packages/web/src/pages/Chat.tsx | 42 +-- pnpm-lock.yaml | 326 ++++++++++++++++++++ 11 files changed, 1376 insertions(+), 113 deletions(-) create mode 100644 packages/ui/src/components/CodeEditor.tsx create mode 100644 packages/ui/src/components/FileExplorer.tsx create mode 100644 packages/ui/src/components/IDE.tsx create mode 100644 packages/ui/src/components/SessionPanel.tsx diff --git a/packages/server/src/routes/files.ts b/packages/server/src/routes/files.ts index 1a2868b..349605d 100644 --- a/packages/server/src/routes/files.ts +++ b/packages/server/src/routes/files.ts @@ -5,7 +5,7 @@ */ import { Hono } from 'hono'; -import { readdir, stat, readFile } from 'node:fs/promises'; +import { readdir, stat, readFile, writeFile, mkdir } from 'node:fs/promises'; import { join, resolve, basename, extname, dirname } from 'node:path'; import { searchFiles as coreSearchFiles, type FileIndexEntry } from '@ai-assistant/core'; @@ -398,4 +398,50 @@ filesRouter.get('/search', async (c) => { } }); +// ============================================================================ +// PUT /api/files/write - 写入文件内容 +// ============================================================================ +filesRouter.put('/write', async (c) => { + try { + const body = await c.req.json(); + const { path: requestedPath, content } = body; + + if (!requestedPath) { + return c.json({ success: false, error: 'Path is required' }, 400); + } + + if (typeof content !== 'string') { + return c.json({ success: false, error: 'Content must be a string' }, 400); + } + + const absolutePath = safePath(requestedPath); + if (!absolutePath) { + return c.json({ success: false, error: 'Access denied: path outside working directory' }, 403); + } + + // 确保父目录存在 + const parentDir = dirname(absolutePath); + await mkdir(parentDir, { recursive: true }); + + // 写入文件 + await writeFile(absolutePath, content, 'utf-8'); + + const stats = await stat(absolutePath); + const name = basename(absolutePath); + + return c.json({ + success: true, + data: { + path: requestedPath, + name, + size: stats.size, + modified: stats.mtime.toISOString(), + }, + }); + } catch (error) { + const message = error instanceof Error ? error.message : 'Unknown error'; + return c.json({ success: false, error: message }, 500); + } +}); + export { filesRouter }; diff --git a/packages/ui/package.json b/packages/ui/package.json index 15725a5..0a1ddeb 100644 --- a/packages/ui/package.json +++ b/packages/ui/package.json @@ -22,6 +22,13 @@ "react-dom": "^18.0.0" }, "dependencies": { + "@codemirror/lang-css": "^6.3.1", + "@codemirror/lang-html": "^6.4.11", + "@codemirror/lang-javascript": "^6.2.4", + "@codemirror/lang-json": "^6.0.2", + "@codemirror/lang-markdown": "^6.5.0", + "@codemirror/lang-python": "^6.2.1", + "@codemirror/theme-one-dark": "^6.1.3", "@radix-ui/react-dialog": "^1.1.15", "@radix-ui/react-dropdown-menu": "^2.1.16", "@radix-ui/react-popover": "^1.1.15", @@ -29,6 +36,7 @@ "@radix-ui/react-slider": "^1.3.6", "@radix-ui/react-switch": "^1.2.6", "@radix-ui/react-tooltip": "^1.2.8", + "@uiw/react-codemirror": "^4.25.4", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", "framer-motion": "^12.23.26", diff --git a/packages/ui/src/api/client.ts b/packages/ui/src/api/client.ts index cef4a1f..70d2e34 100644 --- a/packages/ui/src/api/client.ts +++ b/packages/ui/src/api/client.ts @@ -270,6 +270,20 @@ export async function readFile(path: string): Promise { return request('GET', `/files/read?path=${encodeURIComponent(path)}`); } +export interface FileWriteResponse { + success: boolean; + data: { + path: string; + name: string; + size: number; + modified: string; + }; +} + +export async function writeFile(path: string, content: string): Promise { + return request('PUT', '/files/write', { path, content }); +} + export async function getFileTree(path: string = '.', depth: number = 3): Promise { const params = new URLSearchParams({ path, depth: String(depth) }); return request('GET', `/files/tree?${params}`); diff --git a/packages/ui/src/components/CodeEditor.tsx b/packages/ui/src/components/CodeEditor.tsx new file mode 100644 index 0000000..52b8010 --- /dev/null +++ b/packages/ui/src/components/CodeEditor.tsx @@ -0,0 +1,286 @@ +/** + * CodeEditor Component + * + * 基于 CodeMirror 的代码编辑器,支持多标签页和保存功能 + */ + +import { useState, useCallback, useEffect, useMemo } from 'react'; +import CodeMirror from '@uiw/react-codemirror'; +import { javascript } from '@codemirror/lang-javascript'; +import { python } from '@codemirror/lang-python'; +import { json } from '@codemirror/lang-json'; +import { html } from '@codemirror/lang-html'; +import { css } from '@codemirror/lang-css'; +import { markdown } from '@codemirror/lang-markdown'; +import { oneDark } from '@codemirror/theme-one-dark'; +import { X, Save, Circle } from 'lucide-react'; +import { motion, AnimatePresence } from 'framer-motion'; +import { toast } from 'sonner'; +import { cn } from '../utils/cn.js'; +import { writeFile } from '../api/client.js'; +import { useTheme } from '../hooks/useTheme.js'; + +export interface EditorTab { + id: string; + path: string; + name: string; + content: string; + originalContent: string; + language: string; +} + +interface CodeEditorProps { + tabs: EditorTab[]; + activeTabId: string | null; + onTabChange: (tabId: string) => void; + onTabClose: (tabId: string) => void; + onContentChange: (tabId: string, content: string) => void; + onSave?: (tabId: string, path: string, content: string) => void; + className?: string; +} + +// 根据文件扩展名获取语言 +function getLanguageExtension(language: string) { + switch (language) { + case 'javascript': + case 'typescript': + case 'jsx': + case 'tsx': + return javascript({ jsx: true, typescript: language.includes('typescript') || language === 'tsx' }); + case 'python': + return python(); + case 'json': + return json(); + case 'html': + return html(); + case 'css': + case 'scss': + case 'less': + return css(); + case 'markdown': + case 'md': + return markdown(); + default: + return []; + } +} + +// 根据文件名获取语言类型 +export function getLanguageFromFilename(filename: string): string { + const ext = filename.split('.').pop()?.toLowerCase() || ''; + const languageMap: Record = { + ts: 'typescript', + tsx: 'tsx', + js: 'javascript', + jsx: 'jsx', + py: 'python', + json: 'json', + html: 'html', + htm: 'html', + css: 'css', + scss: 'scss', + less: 'less', + md: 'markdown', + markdown: 'markdown', + }; + return languageMap[ext] || 'text'; +} + +export function CodeEditor({ + tabs, + activeTabId, + onTabChange, + onTabClose, + onContentChange, + onSave, + className, +}: CodeEditorProps) { + const { resolvedTheme } = useTheme(); + const [saving, setSaving] = useState(null); + + const activeTab = useMemo( + () => tabs.find((tab) => tab.id === activeTabId), + [tabs, activeTabId] + ); + + // 检查标签是否有未保存的更改 + const hasUnsavedChanges = useCallback( + (tab: EditorTab) => tab.content !== tab.originalContent, + [] + ); + + // 保存文件 + const handleSave = useCallback( + async (tab: EditorTab) => { + if (!hasUnsavedChanges(tab)) return; + + setSaving(tab.id); + try { + const result = await writeFile(tab.path, tab.content); + if (result.success) { + onSave?.(tab.id, tab.path, tab.content); + toast.success(`Saved ${tab.name}`); + } else { + toast.error('Failed to save file'); + } + } catch (error) { + console.error('Failed to save file:', error); + toast.error('Failed to save file'); + } finally { + setSaving(null); + } + }, + [hasUnsavedChanges, onSave] + ); + + // 键盘快捷键 + useEffect(() => { + const handleKeyDown = (e: KeyboardEvent) => { + if ((e.metaKey || e.ctrlKey) && e.key === 's') { + e.preventDefault(); + if (activeTab && hasUnsavedChanges(activeTab)) { + handleSave(activeTab); + } + } + }; + + window.addEventListener('keydown', handleKeyDown); + return () => window.removeEventListener('keydown', handleKeyDown); + }, [activeTab, hasUnsavedChanges, handleSave]); + + // 编辑器扩展 + const extensions = useMemo(() => { + if (!activeTab) return []; + return [getLanguageExtension(activeTab.language)].flat(); + }, [activeTab]); + + if (tabs.length === 0) { + return ( +
+

No files open

+
+ ); + } + + return ( +
+ {/* Tab Bar */} +
+ + {tabs.map((tab) => { + const isActive = tab.id === activeTabId; + const isModified = hasUnsavedChanges(tab); + const isSaving = saving === tab.id; + + return ( + onTabChange(tab.id)} + > + {/* 修改指示器 */} + {isModified && !isSaving && ( + + )} + {isSaving && ( +
+ )} + + {/* 文件名 */} + + {tab.name} + + + {/* 关闭按钮 */} + + + ); + })} + + + {/* 保存按钮 */} + {activeTab && hasUnsavedChanges(activeTab) && ( + handleSave(activeTab)} + disabled={saving === activeTab.id} + className="ml-auto mr-2 p-1.5 rounded hover:bg-surface-muted transition-colors" + title="Save (Cmd+S)" + > + + + )} +
+ + {/* Editor */} +
+ {activeTab && ( + onContentChange(activeTab.id, value)} + basicSetup={{ + lineNumbers: true, + highlightActiveLineGutter: true, + highlightSpecialChars: true, + history: true, + foldGutter: true, + drawSelection: true, + dropCursor: true, + allowMultipleSelections: true, + indentOnInput: true, + syntaxHighlighting: true, + bracketMatching: true, + closeBrackets: true, + autocompletion: true, + rectangularSelection: true, + crosshairCursor: true, + highlightActiveLine: true, + highlightSelectionMatches: true, + closeBracketsKeymap: true, + defaultKeymap: true, + searchKeymap: true, + historyKeymap: true, + foldKeymap: true, + completionKeymap: true, + lintKeymap: true, + }} + className="h-full text-sm" + /> + )} +
+
+ ); +} diff --git a/packages/ui/src/components/FileExplorer.tsx b/packages/ui/src/components/FileExplorer.tsx new file mode 100644 index 0000000..6e91912 --- /dev/null +++ b/packages/ui/src/components/FileExplorer.tsx @@ -0,0 +1,253 @@ +/** + * FileExplorer Component + * + * 文件树组件,支持展开/折叠和文件选择 + */ + +import { useState, useEffect, useCallback } from 'react'; +import { ChevronRight, ChevronDown, File, Folder, FolderOpen, RefreshCw } from 'lucide-react'; +import { motion, AnimatePresence } from 'framer-motion'; +import { cn } from '../utils/cn.js'; +import { getFileTree, type FileTreeNode } from '../api/client.js'; + +interface FileExplorerProps { + onFileSelect?: (path: string, name: string) => void; + className?: string; + /** 工作目录路径,显示在标题栏 */ + workingDirectory?: string; +} + +// 文件图标颜色映射 +const fileIconColors: Record = { + ts: 'text-blue-400', + tsx: 'text-blue-400', + js: 'text-yellow-300', + jsx: 'text-yellow-300', + json: 'text-yellow-500', + md: 'text-fg-muted', + css: 'text-pink-400', + scss: 'text-pink-400', + html: 'text-orange-400', + py: 'text-green-400', + go: 'text-cyan-400', + rs: 'text-orange-500', + java: 'text-red-400', + c: 'text-blue-500', + cpp: 'text-blue-500', + h: 'text-purple-400', +}; + +function getFileIconColor(name: string): string { + const ext = name.split('.').pop()?.toLowerCase() || ''; + return fileIconColors[ext] || 'text-fg-muted'; +} + +interface TreeNodeProps { + node: FileTreeNode; + depth: number; + onFileSelect?: (path: string, name: string) => void; + expandedPaths: Set; + onToggleExpand: (path: string) => void; +} + +function TreeNode({ node, depth, onFileSelect, expandedPaths, onToggleExpand }: TreeNodeProps) { + const isExpanded = expandedPaths.has(node.path); + const isDirectory = node.type === 'directory'; + const hasChildren = isDirectory && node.children && node.children.length > 0; + + const handleClick = () => { + if (isDirectory) { + onToggleExpand(node.path); + } else { + onFileSelect?.(node.path, node.name); + } + }; + + return ( +
+ + {/* 展开/折叠图标 */} + {isDirectory ? ( + + {hasChildren ? ( + isExpanded ? ( + + ) : ( + + ) + ) : null} + + ) : ( + + )} + + {/* 文件/文件夹图标 */} + {isDirectory ? ( + isExpanded ? ( + + ) : ( + + ) + ) : ( + + )} + + {/* 文件名 */} + {node.name} + + + {/* 子节点 */} + + {isDirectory && isExpanded && node.children && ( + + {node.children.map((child) => ( + + ))} + + )} + +
+ ); +} + +export function FileExplorer({ onFileSelect, className, workingDirectory }: FileExplorerProps) { + const [tree, setTree] = useState([]); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(null); + const [expandedPaths, setExpandedPaths] = useState>(new Set()); + + // 加载文件树 + const loadTree = useCallback(async () => { + setLoading(true); + setError(null); + try { + const result = await getFileTree('.', 4); + if (result.success) { + setTree(result.data.tree); + // 默认展开第一层 + const firstLevel = new Set( + result.data.tree + .filter((n) => n.type === 'directory') + .map((n) => n.path) + ); + setExpandedPaths(firstLevel); + } else { + setError('Failed to load file tree'); + } + } catch (err) { + setError(err instanceof Error ? err.message : 'Failed to load file tree'); + } finally { + setLoading(false); + } + }, []); + + useEffect(() => { + loadTree(); + }, [loadTree]); + + const handleToggleExpand = useCallback((path: string) => { + setExpandedPaths((prev) => { + const next = new Set(prev); + if (next.has(path)) { + next.delete(path); + } else { + next.add(path); + } + return next; + }); + }, []); + + if (loading) { + return ( +
+
+
+ ); + } + + if (error) { + return ( +
+

{error}

+ +
+ ); + } + + return ( +
+ {/* Header */} +
+
+ + + {workingDirectory || 'Files'} + + {/* Tooltip - 悬浮时显示完整路径 */} + {workingDirectory && ( +
+
+

+ {workingDirectory} +

+
+
+ )} +
+ +
+ + {/* Tree */} +
+ {tree.length === 0 ? ( +

No files found

+ ) : ( + tree.map((node) => ( + + )) + )} +
+
+ ); +} diff --git a/packages/ui/src/components/IDE.tsx b/packages/ui/src/components/IDE.tsx new file mode 100644 index 0000000..b421efe --- /dev/null +++ b/packages/ui/src/components/IDE.tsx @@ -0,0 +1,142 @@ +/** + * IDE Component + * + * 整合文件浏览器和代码编辑器的 IDE 组件 + */ + +import { useState, useCallback, useEffect } from 'react'; +import { toast } from 'sonner'; +import { cn } from '../utils/cn.js'; +import { readFile, getWorkingDirectory } from '../api/client.js'; +import { FileExplorer } from './FileExplorer.js'; +import { CodeEditor, getLanguageFromFilename, type EditorTab } from './CodeEditor.js'; + +interface IDEProps { + className?: string; + /** 文件浏览器宽度 */ + sidebarWidth?: number; +} + +export function IDE({ className, sidebarWidth = 256 }: IDEProps) { + const [tabs, setTabs] = useState([]); + const [activeTabId, setActiveTabId] = useState(null); + const [workingDirectory, setWorkingDirectory] = useState(''); + + // 获取工作目录 + useEffect(() => { + getWorkingDirectory() + .then((result) => { + if (result?.data?.workingDirectory) { + setWorkingDirectory(result.data.workingDirectory); + } + }) + .catch(() => { + // 忽略错误 + }); + }, []); + + // 打开文件 + const handleFileSelect = useCallback(async (path: string, name: string) => { + // 检查是否已打开 + const existingTab = tabs.find((tab) => tab.path === path); + if (existingTab) { + setActiveTabId(existingTab.id); + return; + } + + // 读取文件内容 + try { + const result = await readFile(path); + if (result.success && result.data.encoding === 'utf-8') { + const newTab: EditorTab = { + id: `tab-${Date.now()}`, + path, + name, + content: result.data.content, + originalContent: result.data.content, + language: getLanguageFromFilename(name), + }; + setTabs((prev) => [...prev, newTab]); + setActiveTabId(newTab.id); + } else if (result.data.encoding === 'base64') { + toast.error('Cannot edit binary files'); + } else { + toast.error('Failed to open file'); + } + } catch (error) { + console.error('Failed to open file:', error); + toast.error('Failed to open file'); + } + }, [tabs]); + + // 切换标签 + const handleTabChange = useCallback((tabId: string) => { + setActiveTabId(tabId); + }, []); + + // 关闭标签 + const handleTabClose = useCallback((tabId: string) => { + const tabIndex = tabs.findIndex((tab) => tab.id === tabId); + const tab = tabs[tabIndex]; + + // 检查是否有未保存的更改 + if (tab && tab.content !== tab.originalContent) { + const confirmed = window.confirm(`${tab.name} has unsaved changes. Close anyway?`); + if (!confirmed) return; + } + + setTabs((prev) => prev.filter((tab) => tab.id !== tabId)); + + // 如果关闭的是当前标签,切换到相邻标签 + if (activeTabId === tabId) { + if (tabs.length > 1) { + const newIndex = tabIndex === 0 ? 1 : tabIndex - 1; + setActiveTabId(tabs[newIndex].id); + } else { + setActiveTabId(null); + } + } + }, [tabs, activeTabId]); + + // 内容更改 + const handleContentChange = useCallback((tabId: string, content: string) => { + setTabs((prev) => + prev.map((tab) => + tab.id === tabId ? { ...tab, content } : tab + ) + ); + }, []); + + // 保存后更新原始内容 + const handleSave = useCallback((tabId: string, _path: string, content: string) => { + setTabs((prev) => + prev.map((tab) => + tab.id === tabId ? { ...tab, originalContent: content } : tab + ) + ); + }, []); + + return ( +
+ {/* 文件浏览器 */} +
+ +
+ + {/* 代码编辑器 */} +
+ +
+
+ ); +} diff --git a/packages/ui/src/components/SessionPanel.tsx b/packages/ui/src/components/SessionPanel.tsx new file mode 100644 index 0000000..05bab66 --- /dev/null +++ b/packages/ui/src/components/SessionPanel.tsx @@ -0,0 +1,258 @@ +/** + * SessionPanel Component + * + * 会话管理面板:列出、创建、切换、删除会话 + */ + +import { useState, useEffect, useCallback, forwardRef } from 'react'; +import { X, Plus, MessageSquare, Trash2, MessageCircle } from 'lucide-react'; +import { motion, AnimatePresence } from 'framer-motion'; +import { toast } from 'sonner'; +import { cn } from '../utils/cn'; +import { modalOverlay, modalContent, smoothTransition, fadeInUp } from '../utils/animations'; +import { Button } from '../primitives/Button'; +import { SessionSkeleton } from './Skeleton'; +import { listSessions, createSession, deleteSession, type Session } from '../api/client.js'; + +interface SessionPanelProps { + onClose: () => void; + /** 当前选中的会话 ID */ + currentSessionId: string | null; + /** 选择会话回调 */ + onSelectSession: (id: string) => void; + /** 创建会话回调 */ + onCreateSession: (session: Session) => void; + /** 会话标题更新事件(从 WebSocket 接收) */ + sessionTitleUpdate?: { sessionId: string; name: string } | null; + /** 是否启用响应式布局 */ + responsive?: boolean; +} + +export function SessionPanel({ + onClose, + currentSessionId, + onSelectSession, + onCreateSession, + sessionTitleUpdate, + responsive = false, +}: SessionPanelProps) { + const [sessions, setSessions] = useState([]); + const [isLoading, setIsLoading] = useState(true); + + // 加载会话列表 + const loadSessions = useCallback(async () => { + setIsLoading(true); + try { + const { data } = await listSessions(); + setSessions(data); + } catch (error) { + console.error('Failed to load sessions:', error); + toast.error('Failed to load sessions'); + } finally { + setIsLoading(false); + } + }, []); + + // 创建新会话 + const handleCreate = async () => { + try { + const { data } = await createSession(); + setSessions((prev) => [data, ...prev]); + onCreateSession(data); + toast.success('Session created'); + onClose(); + } catch (error) { + console.error('Failed to create session:', error); + toast.error('Failed to create session'); + } + }; + + // 删除会话 + const handleDelete = async (id: string, e: React.MouseEvent) => { + e.stopPropagation(); + try { + await deleteSession(id); + setSessions((prev) => prev.filter((s) => s.id !== id)); + toast.success('Session deleted'); + if (currentSessionId === id) { + const remaining = sessions.filter((s) => s.id !== id); + if (remaining.length > 0) { + onSelectSession(remaining[0].id); + } + } + } catch (error) { + console.error('Failed to delete session:', error); + toast.error('Failed to delete session'); + } + }; + + // 选择会话 + const handleSelectSession = (id: string) => { + onSelectSession(id); + onClose(); + }; + + // 初始加载 + useEffect(() => { + loadSessions(); + }, [loadSessions]); + + // 处理会话标题更新 + useEffect(() => { + if (sessionTitleUpdate) { + setSessions((prev) => + prev.map((s) => + s.id === sessionTitleUpdate.sessionId ? { ...s, name: sessionTitleUpdate.name } : s + ) + ); + } + }, [sessionTitleUpdate]); + + // 会话列表项 + const SessionItem = forwardRef(({ session }, ref) => ( + handleSelectSession(session.id)} + className={cn( + 'flex items-center gap-3 p-3 rounded-lg cursor-pointer group', + 'hover:bg-surface-muted transition-colors', + 'active:bg-surface-emphasis', + currentSessionId === session.id && 'bg-primary-500/20 border border-primary-500/30' + )} + > + +
+
+ {session.name || `Chat ${session.id.slice(0, 8)}`} +
+
{session.messageCount} messages
+
+ handleDelete(session.id, e)} + className="opacity-0 group-hover:opacity-100 p-1.5 hover:bg-red-500/20 rounded transition-all" + aria-label="Delete session" + > + + +
+ )); + + // 空状态 + const EmptyState = () => ( + +
+ +
+

No conversations yet

+ +
+ ); + + // 加载状态 + const LoadingState = () => ( +
+ {[1, 2, 3, 4, 5].map((i) => ( + + ))} +
+ ); + + // 面板内容 + const panelContent = ( + e.stopPropagation()} + > + {/* Header */} +
+
+ +

Sessions

+ {!isLoading && ( + + {sessions.length} + + )} +
+
+ + + + +
+
+ + {/* Session List */} +
+ {isLoading ? ( + + ) : sessions.length === 0 ? ( + + ) : ( +
+ + {sessions.map((session) => ( + + ))} + +
+ )} +
+ + {/* Footer */} +
+ + Click a session to switch, or create a new one + +
+
+ ); + + return ( + + + {panelContent} + + + ); +} diff --git a/packages/ui/src/index.ts b/packages/ui/src/index.ts index 3c10ea3..68e80b2 100644 --- a/packages/ui/src/index.ts +++ b/packages/ui/src/index.ts @@ -18,6 +18,7 @@ export { getWorkingDirectory, listFiles, readFile, + writeFile, getFileTree, getConfig, updateConfig, @@ -113,6 +114,7 @@ export type { FileInfo, FileListResponse, FileReadResponse, + FileWriteResponse, FileTreeNode, FileTreeResponse, ServerConfig, @@ -245,6 +247,10 @@ export { AskUserQuestion } from './components/AskUserQuestion.js'; export { LSPPanel } from './components/LSPPanel.js'; export { DiagnosticsPanel } from './components/DiagnosticsPanel.js'; export { DiagnosticsIndicator, DiagnosticsIndicatorCompact } from './components/DiagnosticsIndicator.js'; +export { SessionPanel } from './components/SessionPanel.js'; +export { FileExplorer } from './components/FileExplorer.js'; +export { CodeEditor, getLanguageFromFilename, type EditorTab } from './components/CodeEditor.js'; +export { IDE } from './components/IDE.js'; // Toast function (re-export from sonner) export { toast } from 'sonner'; diff --git a/packages/web/src/App.tsx b/packages/web/src/App.tsx index c6a30fe..89d846e 100644 --- a/packages/web/src/App.tsx +++ b/packages/web/src/App.tsx @@ -6,8 +6,7 @@ import { useState, useEffect, useCallback } from 'react'; import { - Sidebar, - FileBrowser, + IDE, CommandPanel, MCPPanel, HooksPanel, @@ -16,11 +15,11 @@ import { ProvidersPanel, LSPPanel, DiagnosticsPanel, + SessionPanel, Toaster, ThemeProvider, listSessions, createSession, - getWorkingDirectory, type Session, } from '@ai-assistant/ui'; import { ChatPage } from './pages/Chat'; @@ -28,7 +27,6 @@ import { ChatPage } from './pages/Chat'; export function App() { const [currentSessionId, setCurrentSessionId] = useState(null); const [isInitializing, setIsInitializing] = useState(true); - const [showFileBrowser, setShowFileBrowser] = useState(false); const [showCommands, setShowCommands] = useState(false); const [showMCP, setShowMCP] = useState(false); const [showHooks, setShowHooks] = useState(false); @@ -37,28 +35,18 @@ export function App() { 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 [workingDirectory, setWorkingDirectory] = useState(''); - // 初始化:加载会话和工作目录 + // 初始化:加载会话 useEffect(() => { const HAS_SESSIONS_KEY = 'ai-assistant-has-sessions'; async function init() { try { - // 并行获取会话和工作目录 - const [sessionsResult, workdirResult] = await Promise.all([ - listSessions(), - getWorkingDirectory().catch(() => null), - ]); - + const sessionsResult = await listSessions(); const { data: sessions } = sessionsResult; - // 设置工作目录 - if (workdirResult?.data?.workingDirectory) { - setWorkingDirectory(workdirResult.data.workingDirectory); - } - if (sessions.length > 0) { // 有会话,选择最近的 setCurrentSessionId(sessions[0].id); @@ -124,18 +112,15 @@ export function App() { return (
- - - {/* 主内容区域 */} + {/* 主内容区域:左侧文件浏览器 + 右侧对话框 */}
- {/* 聊天区域 */} -
+ {/* 左侧:IDE(文件浏览器 + 代码编辑器) */} +
+ +
+ + {/* 右侧:聊天区域 */} +
{currentSessionId ? ( setShowFileBrowser(!showFileBrowser)} onOpenCommands={() => setShowCommands(true)} onOpenMCP={() => setShowMCP(true)} onOpenHooks={() => setShowHooks(true)} @@ -153,7 +136,7 @@ export function App() { onOpenProviders={() => setShowProviders(true)} onOpenLSP={() => setShowLSP(true)} onOpenDiagnostics={() => setShowDiagnostics(true)} - workingDirectory={workingDirectory} + onOpenSessions={() => setShowSessions(true)} /> ) : (
@@ -162,41 +145,6 @@ export function App() { )}
- {/* 文件浏览器 - 桌面端侧边栏,移动端全屏覆盖 */} - {showFileBrowser && ( - <> - {/* 移动端: 全屏覆盖 */} -
-
- Files - -
-
- { - console.log('Selected file:', path); - }} - /> -
-
- - {/* 桌面端: 侧边栏 */} -
- { - console.log('Selected file:', path); - }} - /> -
- - )}
{/* 命令面板 */} @@ -241,21 +189,17 @@ export function App() { /> )} - {/* 移动端底部文件按钮 */} - + {/* Sessions 面板 */} + {showSessions && ( + setShowSessions(false)} + currentSessionId={currentSessionId} + onSelectSession={handleSelectSession} + onCreateSession={handleCreateSession} + sessionTitleUpdate={sessionTitleUpdate} + responsive + /> + )} {/* Toast 通知 */} diff --git a/packages/web/src/pages/Chat.tsx b/packages/web/src/pages/Chat.tsx index 075f605..7dcb4fa 100644 --- a/packages/web/src/pages/Chat.tsx +++ b/packages/web/src/pages/Chat.tsx @@ -3,7 +3,7 @@ */ import { useEffect, useRef } from 'react'; -import { WifiOff, MessageSquare, FolderOpen, Terminal, Plug, Zap, Bot, History, Server, Folder } from 'lucide-react'; +import { WifiOff, MessageSquare, Terminal, Plug, Zap, Bot, History, Server, MessagesSquare } from 'lucide-react'; import { motion, AnimatePresence } from 'framer-motion'; import { toast } from 'sonner'; import { @@ -23,8 +23,6 @@ interface ChatPageProps { onSessionUpdated?: (sessionId: string, name: string) => void; responsive?: boolean; // 工具栏按钮 - showFileBrowser?: boolean; - onToggleFileBrowser?: () => void; onOpenCommands?: () => void; onOpenMCP?: () => void; onOpenHooks?: () => void; @@ -33,8 +31,7 @@ interface ChatPageProps { onOpenProviders?: () => void; onOpenLSP?: () => void; onOpenDiagnostics?: () => void; - // Working Directory - workingDirectory?: string; + onOpenSessions?: () => void; } export function ChatPage({ @@ -42,8 +39,6 @@ export function ChatPage({ onSessionNotFound, onSessionUpdated, responsive = false, - showFileBrowser, - onToggleFileBrowser, onOpenCommands, onOpenMCP, onOpenHooks, @@ -52,7 +47,7 @@ export function ChatPage({ onOpenProviders, onOpenLSP, onOpenDiagnostics, - workingDirectory, + onOpenSessions, }: ChatPageProps) { const { messages, @@ -162,18 +157,7 @@ export function ChatPage({
{/* Header */}
-
-

Chat

- {/* Working Directory */} - {workingDirectory && ( -
- - - {workingDirectory} - -
- )} -
+

Chat

{/* 上下文使用情况 - 紧凑模式 */} {sessionId && ( @@ -189,7 +173,7 @@ export function ChatPage({ {/* 工具栏按钮 */} - {(onToggleFileBrowser || onOpenCommands || onOpenMCP || onOpenHooks || onOpenAgents || onOpenCheckpoints || onOpenProviders || onOpenLSP || onOpenDiagnostics) && ( + {(onOpenCommands || onOpenMCP || onOpenHooks || onOpenAgents || onOpenCheckpoints || onOpenProviders || onOpenLSP || onOpenDiagnostics || onOpenSessions) && (
{/* LSP 诊断指示器 */} {(onOpenLSP || onOpenDiagnostics) && ( @@ -278,20 +262,16 @@ export function ChatPage({ )} - {/* 文件浏览器按钮 - 仅桌面端显示 */} - {onToggleFileBrowser && ( + {/* Sessions 按钮 */} + {onOpenSessions && ( - + )}
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index aca6108..553f818 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -234,6 +234,27 @@ importers: packages/ui: dependencies: + '@codemirror/lang-css': + specifier: ^6.3.1 + version: 6.3.1 + '@codemirror/lang-html': + specifier: ^6.4.11 + version: 6.4.11 + '@codemirror/lang-javascript': + specifier: ^6.2.4 + version: 6.2.4 + '@codemirror/lang-json': + specifier: ^6.0.2 + version: 6.0.2 + '@codemirror/lang-markdown': + specifier: ^6.5.0 + version: 6.5.0 + '@codemirror/lang-python': + specifier: ^6.2.1 + version: 6.2.1 + '@codemirror/theme-one-dark': + specifier: ^6.1.3 + version: 6.1.3 '@radix-ui/react-dialog': specifier: ^1.1.15 version: 1.1.15(@types/react-dom@18.3.7(@types/react@18.3.27))(@types/react@18.3.27)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) @@ -255,6 +276,9 @@ importers: '@radix-ui/react-tooltip': specifier: ^1.2.8 version: 1.2.8(@types/react-dom@18.3.7(@types/react@18.3.27))(@types/react@18.3.27)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@uiw/react-codemirror': + specifier: ^4.25.4 + version: 4.25.4(@babel/runtime@7.28.4)(@codemirror/autocomplete@6.20.0)(@codemirror/language@6.11.3)(@codemirror/lint@6.9.2)(@codemirror/search@6.5.11)(@codemirror/state@6.5.2)(@codemirror/theme-one-dark@6.1.3)(@codemirror/view@6.39.4)(codemirror@6.0.2)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) class-variance-authority: specifier: ^0.7.1 version: 0.7.1 @@ -953,6 +977,48 @@ packages: resolution: {integrity: sha512-8XqW8xGn++Eqqbz3e9wKuK7mxryeRjs4LOHLxbh2lwKeSbuNR4NFifDZT4KzvjU6HMOPbiNTsWpniK5EJfTWkg==} engines: {node: '>=18'} + '@codemirror/autocomplete@6.20.0': + resolution: {integrity: sha512-bOwvTOIJcG5FVo5gUUupiwYh8MioPLQ4UcqbcRf7UQ98X90tCa9E1kZ3Z7tqwpZxYyOvh1YTYbmZE9RTfTp5hg==} + + '@codemirror/commands@6.10.1': + resolution: {integrity: sha512-uWDWFypNdQmz2y1LaNJzK7fL7TYKLeUAU0npEC685OKTF3KcQ2Vu3klIM78D7I6wGhktme0lh3CuQLv0ZCrD9Q==} + + '@codemirror/lang-css@6.3.1': + resolution: {integrity: sha512-kr5fwBGiGtmz6l0LSJIbno9QrifNMUusivHbnA1H6Dmqy4HZFte3UAICix1VuKo0lMPKQr2rqB+0BkKi/S3Ejg==} + + '@codemirror/lang-html@6.4.11': + resolution: {integrity: sha512-9NsXp7Nwp891pQchI7gPdTwBuSuT3K65NGTHWHNJ55HjYcHLllr0rbIZNdOzas9ztc1EUVBlHou85FFZS4BNnw==} + + '@codemirror/lang-javascript@6.2.4': + resolution: {integrity: sha512-0WVmhp1QOqZ4Rt6GlVGwKJN3KW7Xh4H2q8ZZNGZaP6lRdxXJzmjm4FqvmOojVj6khWJHIb9sp7U/72W7xQgqAA==} + + '@codemirror/lang-json@6.0.2': + resolution: {integrity: sha512-x2OtO+AvwEHrEwR0FyyPtfDUiloG3rnVTSZV1W8UteaLL8/MajQd8DpvUb2YVzC+/T18aSDv0H9mu+xw0EStoQ==} + + '@codemirror/lang-markdown@6.5.0': + resolution: {integrity: sha512-0K40bZ35jpHya6FriukbgaleaqzBLZfOh7HuzqbMxBXkbYMJDxfF39c23xOgxFezR+3G+tR2/Mup+Xk865OMvw==} + + '@codemirror/lang-python@6.2.1': + resolution: {integrity: sha512-IRjC8RUBhn9mGR9ywecNhB51yePWCGgvHfY1lWN/Mrp3cKuHr0isDKia+9HnvhiWNnMpbGhWrkhuWOc09exRyw==} + + '@codemirror/language@6.11.3': + resolution: {integrity: sha512-9HBM2XnwDj7fnu0551HkGdrUrrqmYq/WC5iv6nbY2WdicXdGbhR/gfbZOH73Aqj4351alY1+aoG9rCNfiwS1RA==} + + '@codemirror/lint@6.9.2': + resolution: {integrity: sha512-sv3DylBiIyi+xKwRCJAAsBZZZWo82shJ/RTMymLabAdtbkV5cSKwWDeCgtUq3v8flTaXS2y1kKkICuRYtUswyQ==} + + '@codemirror/search@6.5.11': + resolution: {integrity: sha512-KmWepDE6jUdL6n8cAAqIpRmLPBZ5ZKnicE8oGU/s3QrAVID+0VhLFrzUucVKHG5035/BSykhExDL/Xm7dHthiA==} + + '@codemirror/state@6.5.2': + resolution: {integrity: sha512-FVqsPqtPWKVVL3dPSxy8wEF/ymIEuVzF1PK3VbUgrxXpJUSHQWWZz4JMToquRxnkw+36LTamCZG2iua2Ptq0fA==} + + '@codemirror/theme-one-dark@6.1.3': + resolution: {integrity: sha512-NzBdIvEJmx6fjeremiGp3t/okrLPYT0d9orIc7AFun8oZcRk58aejkqhv6spnz4MLAevrKNPMQYXEWMg4s+sKA==} + + '@codemirror/view@6.39.4': + resolution: {integrity: sha512-xMF6OfEAUVY5Waega4juo1QGACfNkNF+aJLqpd8oUJz96ms2zbfQ9Gh35/tI3y8akEV31FruKfj7hBnIU/nkqA==} + '@emnapi/runtime@1.7.1': resolution: {integrity: sha512-PVtJr5CmLwYAU9PZDMITZoR5iAOShYREoR45EyyLrbntV50mdePTgUn4AmOw90Ifcj+x2kRjdzr1HP3RrNiHGA==} @@ -1452,6 +1518,36 @@ packages: '@kwsites/promise-deferred@1.1.1': resolution: {integrity: sha512-GaHYm+c0O9MjZRu0ongGBRbinu8gVAMd2UZjji6jVmqKtZluZnptXGWhz1E8j8D2HJ3f/yMxKAUC0b+57wncIw==} + '@lezer/common@1.4.0': + resolution: {integrity: sha512-DVeMRoGrgn/k45oQNu189BoW4SZwgZFzJ1+1TV5j2NJ/KFC83oa/enRqZSGshyeMk5cPWMhsKs9nx+8o0unwGg==} + + '@lezer/css@1.3.0': + resolution: {integrity: sha512-pBL7hup88KbI7hXnZV3PQsn43DHy6TWyzuyk2AO9UyoXcDltvIdqWKE1dLL/45JVZ+YZkHe1WVHqO6wugZZWcw==} + + '@lezer/highlight@1.2.3': + resolution: {integrity: sha512-qXdH7UqTvGfdVBINrgKhDsVTJTxactNNxLk7+UMwZhU13lMHaOBlJe9Vqp907ya56Y3+ed2tlqzys7jDkTmW0g==} + + '@lezer/html@1.3.12': + resolution: {integrity: sha512-RJ7eRWdaJe3bsiiLLHjCFT1JMk8m1YP9kaUbvu2rMLEoOnke9mcTVDyfOslsln0LtujdWespjJ39w6zo+RsQYw==} + + '@lezer/javascript@1.5.4': + resolution: {integrity: sha512-vvYx3MhWqeZtGPwDStM2dwgljd5smolYD2lR2UyFcHfxbBQebqx8yjmFmxtJ/E6nN6u1D9srOiVWm3Rb4tmcUA==} + + '@lezer/json@1.0.3': + resolution: {integrity: sha512-BP9KzdF9Y35PDpv04r0VeSTKDeox5vVr3efE7eBbx3r4s3oNLfunchejZhjArmeieBH+nVOpgIiBJpEAv8ilqQ==} + + '@lezer/lr@1.4.5': + resolution: {integrity: sha512-/YTRKP5yPPSo1xImYQk7AZZMAgap0kegzqCSYHjAL9x1AZ0ZQW+IpcEzMKagCsbTsLnVeWkxYrCNeXG8xEPrjg==} + + '@lezer/markdown@1.6.1': + resolution: {integrity: sha512-72ah+Sml7lD8Wn7lnz9vwYmZBo9aQT+I2gjK/0epI+gjdwUbWw3MJ/ZBGEqG1UfrIauRqH37/c5mVHXeCTGXtA==} + + '@lezer/python@1.1.18': + resolution: {integrity: sha512-31FiUrU7z9+d/ElGQLJFXl+dKOdx0jALlP3KEOsGTex8mvj+SoE1FgItcHWK/axkxCHGUSpqIHt6JAWfWu9Rhg==} + + '@marijn/find-cluster-break@1.0.2': + resolution: {integrity: sha512-l0h88YhZFyKdXIFNfSWpyjStDjGHwZ/U7iobcK1cQQD8sejsONdQtTVU+1wVN1PBw40PiiHB1vA5S7VTfQiP9g==} + '@nodelib/fs.scandir@2.1.5': resolution: {integrity: sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==} engines: {node: '>= 8'} @@ -2217,6 +2313,28 @@ packages: '@types/uuid@10.0.0': resolution: {integrity: sha512-7gqG38EyHgyP1S+7+xomFtL+ZNHcKv6DwNaCZmJmo1vgMugyF3TCnXVg4t1uk89mLNwnLtnY3TpOpCOyp1/xHQ==} + '@uiw/codemirror-extensions-basic-setup@4.25.4': + resolution: {integrity: sha512-YzNwkm0AbPv1EXhCHYR5v0nqfemG2jEB0Z3Att4rBYqKrlG7AA9Rhjc3IyBaOzsBu18wtrp9/+uhTyu7TXSRng==} + peerDependencies: + '@codemirror/autocomplete': '>=6.0.0' + '@codemirror/commands': '>=6.0.0' + '@codemirror/language': '>=6.0.0' + '@codemirror/lint': '>=6.0.0' + '@codemirror/search': '>=6.0.0' + '@codemirror/state': '>=6.0.0' + '@codemirror/view': '>=6.0.0' + + '@uiw/react-codemirror@4.25.4': + resolution: {integrity: sha512-ipO067oyfUw+DVaXhQCxkB0ZD9b7RnY+ByrprSYSKCHaULvJ3sqWYC/Zen6zVQ8/XC4o5EPBfatGiX20kC7XGA==} + peerDependencies: + '@babel/runtime': '>=7.11.0' + '@codemirror/state': '>=6.0.0' + '@codemirror/theme-one-dark': '>=6.0.0' + '@codemirror/view': '>=6.0.0' + codemirror: '>=6.0.0' + react: '>=17.0.0' + react-dom: '>=17.0.0' + '@ungap/structured-clone@1.3.0': resolution: {integrity: sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g==} @@ -2559,6 +2677,9 @@ packages: resolution: {integrity: sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==} engines: {node: '>=6'} + codemirror@6.0.2: + resolution: {integrity: sha512-VhydHotNW5w1UGK0Qj96BwSk/Zqbp9WbnyK2W/eVMv4QyF41INRGpjUhFJY7/uDNuudSc33a/PKr4iDqRduvHw==} + color-convert@2.0.1: resolution: {integrity: sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==} engines: {node: '>=7.0.0'} @@ -2608,6 +2729,9 @@ packages: core-js-compat@3.47.0: resolution: {integrity: sha512-IGfuznZ/n7Kp9+nypamBhvwdwLsW6KC8IOaURw2doAK5e98AG3acVLdh0woOnEqCfUtS+Vu882JE4k/DAm3ItQ==} + crelt@1.0.6: + resolution: {integrity: sha512-VQ2MBenTq1fWZUH9DJNGti7kKv6EeAuYr3cLwxUWhIu1baTaXh4Ib5W2CqHVqib4/MqbYGJqiL3Zb8GJZr3l4g==} + cross-spawn@7.0.6: resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==} engines: {node: '>= 8'} @@ -4302,6 +4426,9 @@ packages: resolution: {integrity: sha512-ZprKx+bBLXv067WTCALv8SSz5l2+XhpYCsVtSqlMnkAXMWDq+/ekVbl1ghqP9rUHTzv6sm/DwCOiYutU/yp1fw==} engines: {node: '>=10'} + style-mod@4.1.3: + resolution: {integrity: sha512-i/n8VsZydrugj3Iuzll8+x/00GH2vnYsk1eomD8QiRrSAeW6ItbCQDtfXCeJHd0iwiNagqjQkvpvREEPtW3IoQ==} + style-to-js@1.1.21: resolution: {integrity: sha512-RjQetxJrrUJLQPHbLku6U/ocGtzyjbJMP9lCNK7Ag0CNh690nSH8woqWH9u16nMjYBAok+i7JO1NP2pOy8IsPQ==} @@ -4818,6 +4945,9 @@ packages: vscode-languageserver-types@3.17.5: resolution: {integrity: sha512-Ld1VelNuX9pdF39h2Hgaeb5hEZM2Z3jUrrMgWQAu82jMtZp7p3vJT3BzToKtZI7NgQssZje5o0zryOrhQvzQAg==} + w3c-keyname@2.2.8: + resolution: {integrity: sha512-dpojBhNsCNN7T82Tm7k26A6G9ML3NkhDsnw9n/eoxSRlVBB4CEtIQ/KTCLI2Fwf3ataSXRhYFkQi3SlnFwPvPQ==} + wcwidth@1.0.1: resolution: {integrity: sha512-XHPEwS0q6TaxcvG85+8EYkbiCux2XtWG2mkc47Ng2A77BQu9+DqIOJldST4HgPkuea7dvKSj5VgX3P1d4rW8Tg==} @@ -5776,6 +5906,112 @@ snapshots: dependencies: fontkit: 2.0.4 + '@codemirror/autocomplete@6.20.0': + dependencies: + '@codemirror/language': 6.11.3 + '@codemirror/state': 6.5.2 + '@codemirror/view': 6.39.4 + '@lezer/common': 1.4.0 + + '@codemirror/commands@6.10.1': + dependencies: + '@codemirror/language': 6.11.3 + '@codemirror/state': 6.5.2 + '@codemirror/view': 6.39.4 + '@lezer/common': 1.4.0 + + '@codemirror/lang-css@6.3.1': + dependencies: + '@codemirror/autocomplete': 6.20.0 + '@codemirror/language': 6.11.3 + '@codemirror/state': 6.5.2 + '@lezer/common': 1.4.0 + '@lezer/css': 1.3.0 + + '@codemirror/lang-html@6.4.11': + dependencies: + '@codemirror/autocomplete': 6.20.0 + '@codemirror/lang-css': 6.3.1 + '@codemirror/lang-javascript': 6.2.4 + '@codemirror/language': 6.11.3 + '@codemirror/state': 6.5.2 + '@codemirror/view': 6.39.4 + '@lezer/common': 1.4.0 + '@lezer/css': 1.3.0 + '@lezer/html': 1.3.12 + + '@codemirror/lang-javascript@6.2.4': + dependencies: + '@codemirror/autocomplete': 6.20.0 + '@codemirror/language': 6.11.3 + '@codemirror/lint': 6.9.2 + '@codemirror/state': 6.5.2 + '@codemirror/view': 6.39.4 + '@lezer/common': 1.4.0 + '@lezer/javascript': 1.5.4 + + '@codemirror/lang-json@6.0.2': + dependencies: + '@codemirror/language': 6.11.3 + '@lezer/json': 1.0.3 + + '@codemirror/lang-markdown@6.5.0': + dependencies: + '@codemirror/autocomplete': 6.20.0 + '@codemirror/lang-html': 6.4.11 + '@codemirror/language': 6.11.3 + '@codemirror/state': 6.5.2 + '@codemirror/view': 6.39.4 + '@lezer/common': 1.4.0 + '@lezer/markdown': 1.6.1 + + '@codemirror/lang-python@6.2.1': + dependencies: + '@codemirror/autocomplete': 6.20.0 + '@codemirror/language': 6.11.3 + '@codemirror/state': 6.5.2 + '@lezer/common': 1.4.0 + '@lezer/python': 1.1.18 + + '@codemirror/language@6.11.3': + dependencies: + '@codemirror/state': 6.5.2 + '@codemirror/view': 6.39.4 + '@lezer/common': 1.4.0 + '@lezer/highlight': 1.2.3 + '@lezer/lr': 1.4.5 + style-mod: 4.1.3 + + '@codemirror/lint@6.9.2': + dependencies: + '@codemirror/state': 6.5.2 + '@codemirror/view': 6.39.4 + crelt: 1.0.6 + + '@codemirror/search@6.5.11': + dependencies: + '@codemirror/state': 6.5.2 + '@codemirror/view': 6.39.4 + crelt: 1.0.6 + + '@codemirror/state@6.5.2': + dependencies: + '@marijn/find-cluster-break': 1.0.2 + + '@codemirror/theme-one-dark@6.1.3': + dependencies: + '@codemirror/language': 6.11.3 + '@codemirror/state': 6.5.2 + '@codemirror/view': 6.39.4 + '@lezer/highlight': 1.2.3 + + '@codemirror/view@6.39.4': + dependencies: + '@codemirror/state': 6.5.2 + crelt: 1.0.6 + style-mod: 4.1.3 + w3c-keyname: 2.2.8 + '@emnapi/runtime@1.7.1': dependencies: tslib: 2.8.1 @@ -6098,6 +6334,53 @@ snapshots: '@kwsites/promise-deferred@1.1.1': {} + '@lezer/common@1.4.0': {} + + '@lezer/css@1.3.0': + dependencies: + '@lezer/common': 1.4.0 + '@lezer/highlight': 1.2.3 + '@lezer/lr': 1.4.5 + + '@lezer/highlight@1.2.3': + dependencies: + '@lezer/common': 1.4.0 + + '@lezer/html@1.3.12': + dependencies: + '@lezer/common': 1.4.0 + '@lezer/highlight': 1.2.3 + '@lezer/lr': 1.4.5 + + '@lezer/javascript@1.5.4': + dependencies: + '@lezer/common': 1.4.0 + '@lezer/highlight': 1.2.3 + '@lezer/lr': 1.4.5 + + '@lezer/json@1.0.3': + dependencies: + '@lezer/common': 1.4.0 + '@lezer/highlight': 1.2.3 + '@lezer/lr': 1.4.5 + + '@lezer/lr@1.4.5': + dependencies: + '@lezer/common': 1.4.0 + + '@lezer/markdown@1.6.1': + dependencies: + '@lezer/common': 1.4.0 + '@lezer/highlight': 1.2.3 + + '@lezer/python@1.1.18': + dependencies: + '@lezer/common': 1.4.0 + '@lezer/highlight': 1.2.3 + '@lezer/lr': 1.4.5 + + '@marijn/find-cluster-break@1.0.2': {} + '@nodelib/fs.scandir@2.1.5': dependencies: '@nodelib/fs.stat': 2.0.5 @@ -6851,6 +7134,33 @@ snapshots: '@types/uuid@10.0.0': {} + '@uiw/codemirror-extensions-basic-setup@4.25.4(@codemirror/autocomplete@6.20.0)(@codemirror/commands@6.10.1)(@codemirror/language@6.11.3)(@codemirror/lint@6.9.2)(@codemirror/search@6.5.11)(@codemirror/state@6.5.2)(@codemirror/view@6.39.4)': + dependencies: + '@codemirror/autocomplete': 6.20.0 + '@codemirror/commands': 6.10.1 + '@codemirror/language': 6.11.3 + '@codemirror/lint': 6.9.2 + '@codemirror/search': 6.5.11 + '@codemirror/state': 6.5.2 + '@codemirror/view': 6.39.4 + + '@uiw/react-codemirror@4.25.4(@babel/runtime@7.28.4)(@codemirror/autocomplete@6.20.0)(@codemirror/language@6.11.3)(@codemirror/lint@6.9.2)(@codemirror/search@6.5.11)(@codemirror/state@6.5.2)(@codemirror/theme-one-dark@6.1.3)(@codemirror/view@6.39.4)(codemirror@6.0.2)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + dependencies: + '@babel/runtime': 7.28.4 + '@codemirror/commands': 6.10.1 + '@codemirror/state': 6.5.2 + '@codemirror/theme-one-dark': 6.1.3 + '@codemirror/view': 6.39.4 + '@uiw/codemirror-extensions-basic-setup': 4.25.4(@codemirror/autocomplete@6.20.0)(@codemirror/commands@6.10.1)(@codemirror/language@6.11.3)(@codemirror/lint@6.9.2)(@codemirror/search@6.5.11)(@codemirror/state@6.5.2)(@codemirror/view@6.39.4) + codemirror: 6.0.2 + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + transitivePeerDependencies: + - '@codemirror/autocomplete' + - '@codemirror/language' + - '@codemirror/lint' + - '@codemirror/search' + '@ungap/structured-clone@1.3.0': {} '@vercel/oidc@3.0.5': {} @@ -7310,6 +7620,16 @@ snapshots: clsx@2.1.1: {} + codemirror@6.0.2: + dependencies: + '@codemirror/autocomplete': 6.20.0 + '@codemirror/commands': 6.10.1 + '@codemirror/language': 6.11.3 + '@codemirror/lint': 6.9.2 + '@codemirror/search': 6.5.11 + '@codemirror/state': 6.5.2 + '@codemirror/view': 6.39.4 + color-convert@2.0.1: dependencies: color-name: 1.1.4 @@ -7344,6 +7664,8 @@ snapshots: dependencies: browserslist: 4.28.1 + crelt@1.0.6: {} + cross-spawn@7.0.6: dependencies: path-key: 3.1.1 @@ -9514,6 +9836,8 @@ snapshots: strip-comments@2.0.1: {} + style-mod@4.1.3: {} + style-to-js@1.1.21: dependencies: style-to-object: 1.0.14 @@ -9944,6 +10268,8 @@ snapshots: vscode-languageserver-types@3.17.5: {} + w3c-keyname@2.2.8: {} + wcwidth@1.0.1: dependencies: defaults: 1.0.4