feat(ui): 添加 IDE 组件(文件浏览器 + 代码编辑器)

- 新增 CodeEditor 组件,基于 CodeMirror 实现多标签代码编辑
- 新增 FileExplorer 组件,支持文件树展开/折叠和文件选择
- 新增 IDE 组件,整合文件浏览器和代码编辑器
- 新增 SessionPanel 组件,用于会话管理
- 添加文件写入 API(PUT /api/files/write)
- 优化布局:IDE 始终显示,移除文件切换按钮
- 工作目录路径显示在文件浏览器标题栏,支持悬浮显示完整路径
This commit is contained in:
2025-12-17 16:55:22 +08:00
parent ddec356117
commit 250d2cb4b5
11 changed files with 1376 additions and 113 deletions
+47 -1
View File
@@ -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 };