本文档供后续 agent 接棒使用。功能已实现并提交本地,未推送、未合并,等待人工验证。
- 日期:2026-08-28
- 仓库:
opensquilla(dark605/opensquilla) - worktree:
D:\WorkTree\workspace-file-tree - 分支:
feat/workspace-file-tree(基于main@4de93713) - 状态:✅ 已合并并推送(2026-08-29):
main与feat/workspace-file-tree均在远程33546342(直连推送成功,无需镜像);合并方式为快进,main 无冲突 - 清理待办:
git worktree remove D:\WorkTree\workspace-file-tree+git branch -d feat/workspace-file-tree(注意先停掉从本目录启动的网关服务) - 提交:
d6e4b01d feat: 新增工作空间文件树(侧边栏浏览 + workbench 预览 + 附加到对话)7fe8147d docs: 修正交接文档统计·7c4675e6 fix: 修复文件预览请求风暴与内容不显示9f818980 feat: 预览面板选中文本右键菜单(复制/添加到对话)46386aee fix: 根除预览右键原生菜单遮挡与重载后编辑器空白e5ba2ffb fix: 修复任务执行期间预览面板闪烁4d0c40f7 fix: 文件页签纳入工作台 8 页签淘汰上限a42b40f3 feat: 页签栏右端新增倒三角下拉,可切换被压缩遮挡的页签0a0ce31d fix: 页签下拉按钮贴右缘,消除页签旁泄漏的 identity 块e566fb5d feat: 移除页签栏右侧的收起工作区按钮,关闭入口收进下拉与单页签行 - 状态:✅ 本地提交完成,❌ 未推送(等用户验证后手动推送)
2026-08-29 首轮走查发现并已修复两个 bug(
7c4675e6): ① 预览面板 watch 依赖含 state 且回调内自调 load() → 请求指数风暴 → 网关 OOM 崩溃(MemoryError 冒顶未捕获)→ WS 断开 → 主区被重置到新任务界面; ② Monaco 在 onMounted 创建时容器 ref 为 null 被 early-return,编辑器从未创建、 内容不显示。修复:watch 只跟踪 path/workspace、幂等 ensureEditor() 在容器 渲染后创建、网关两个 handler 捕 MemoryError 返回 500。前后端各补 2 项回归测试。2026-08-29 新增右键菜单(
9f818980):预览面板选中文本后右键/Shift+F10 弹出"复制 / 添加到对话"菜单;选区经 attach 事件新增的content字段直达 ChatView 管线,以<文件名>.selection.txt附件进 composer(保留 1MB 上限)。2026-08-29 右键菜单两缺陷修复(
46386aee):① 原生菜单遮挡 —— Monaco ContextMenuController 先注册先执行,onContextMenu 里 preventDefault 迟于原生 菜单弹出;改为contextmenu:false根除原生菜单 + 容器 DOM contextmenu 监听 成为唯一菜单来源。② 重载后白屏不可恢复 —— load() 回 loading 时模板条件分支 销毁编辑器容器、ensureEditor 幂等守卫拒绝重建;编辑器容器改 v-show 恒在。 另加 openNonce 贯穿(重点击同文件 = 强制重载而非静默 no-op)。测试改走 synthetic contextmenu 真实 DOM 路径。2026-08-29 任务执行期间面板闪烁修复(
e5ba2ffb):reload watch 的 单-getter-返回-数组写法被 Vue 以引用比较(恒新),流式任务执行期间 AppWorkbench 高频重渲染 + getProps 每次重建 workspace prop 对象 → load() 被误触发风暴(15s 内 32 次闪烁 + 33 次重复拉取);改数组源写法 (逐值比较)后归零。2026-08-29 内存边界验证与修复(
4d0c40f7):实测重点击 30 次零泄漏 (nonce 重载复用实例)、关闭页签正确释放 Monaco;发现 file 面板无上限且 永不淘汰(每个 keep-alive 页签 = 常驻 Monaco 实例),已纳入与 artifact 预览相同的 8 页签激活顺序淘汰(openItem 保护当前项)。2026-08-29 页签溢出下拉(
a42b40f3):tab 栏为隐藏滚动条的横向滚动区, 页签多时被压缩截断难辨难点。栏末新增倒三角按钮(sticky 右缘),点击弹出 全部页签下拉(激活项高亮),选择即切换;Escape/外点关闭。列表渲染在 滚动区外防裁剪;i18n 六语言 workbench.tabOverflow。2026-08-29 下拉两处 UI 修复(
0a0ce31d):菜单 div 原插在 tabs 的 v-if 与 single-title 的 v-else 之间,v-else 错配到菜单自身 v-if (!tabMenuOpen),菜单关闭时 single-title identity 无条件渲染成"幽灵 页签";菜单移到 header 末尾(v-if/v-else 链外)+ ▽ 加 margin-left:auto 贴右缘。注意:往 v-if/v-else 结构化对之间插元素会破坏配对,编译器 不报错,改 WorkbenchHost header 模板时留意。2026-08-29 移除页签栏收起按钮(
e566fb5d):用户反馈 ▽ 右侧的 ×(收起工作区)易误解为页签关闭。关闭/折叠入口重排:▽ 菜单末尾新增 分隔线 +「收起工作区」项;单页签模式 identity 旁新增 × 关闭当前文件。 焦点管理同步调整(aside tabindex=-1、关闭页签后聚焦剩余 tab 或宿主根)。 推送状态:未推送(ghfast.top 镜像与直连当时均连不上,用户令先不提交)。
需求:参考 anomalyco/opencode(https://github.com/anomalyco/opencode)工作空间文件树的实现,
为 OpenSquilla Web UI 增加文件树功能。用户拍板的决策(勿改动):
| 决策项 | 结论 |
|---|---|
| 入口布局 | 侧边栏项目行操作区("新建任务"按钮与"更多"菜单之间)加"查看文件"按钮;点击后左侧边栏整体替换为该项目 workspace 的文件树;顶部"< 返回任务"切回会话列表 |
| 根目录范围 | 按钮所属项目行的 workspace(不跟随当前会话) |
| 文件动作 | 预览(workbench 面板)+ 附加到对话 + 复制路径 |
| 实时性 | 手动刷新按钮(v1 不做轮询/watcher) |
行数大头来自 6 个 locale 文件的重新格式化(单行 JSON 展开为多行,文案内容未变); 扣除后实际净增约 2400 行。
| 文件 | 说明 |
|---|---|
src/opensquilla/gateway/files_api.py(新增,424 行) |
两个端点:GET /api/v1/files?workspace=<id>&path=<rel>(单目录列取,非递归)与 GET /api/v1/files/content?workspace=<id>&path=<rel>&max_bytes=<n>(文本预览,默认/上限 1MB 截断) |
src/opensquilla/gateway/app.py(+8 行) |
create_gateway_app 中注册 register_workspace_files_routes(在 uploads 注册之后) |
tests/test_gateway/test_workspace_files_api.py(新增,~330 行) |
19 项 pytest:路径归一化/穿越拒绝、workspace 404/409、点文件与 .gitignore 过滤(含 !negation)、symlink 逃逸、二进制、截断、token auth |
后端关键设计(接棒者须知):
- workspace 解析走
resolve_validated_project_workspace(storage, id)(project_workspaces.py), 只服务 trusted 的已注册 workspace;ProjectWorkspaceStateError映射 404/409 - 路径安全双保险:
normalize_workspace_rel_path(内部..折叠、越界/绝对路径拒绝)+_resolve_inside_workspace(resolve()后 containment 检查,挡 symlink 逃逸) - 点文件(
.env/.git等)一律不出现在列表和预览中;根.gitignore规则生效 (last-match-wins 语义,与 OpenTUI completion walker 一致,实现自包含在 files_api.py 内, 未复用 tui 模块以避免 gateway→tui 依赖);node_modules/.venv/__pycache__无条件跳过 - 鉴权照抄
uploads.py模式:request_origin_allowed+ Bearer token(token mode 下) - 阻塞 I/O 全部
asyncio.to_thread
| 文件 | 说明 |
|---|---|
src/lib/fileTreeModel.ts(新增)+ .test.ts |
自 opencode 移植的纯函数模型层(build/flatten/normalize),6 项 vitest |
src/stores/fileTree.ts(新增)+ .test.ts |
自 opencode 移植的 Pinia store:目录懒加载、inflight 去重、scope 序号使过期响应失效、删除文件时子树级联清理、refreshDir/refreshAll,8 项 vitest |
src/components/SidebarFileTree.vue(新增,424 行) |
虚拟化文件树视图(@tanstack/vue-virtual,行高 28px/overscan 10):目录展开/折叠、缩进导轨、hover 出"附加/复制"按钮、错误/空/加载态 |
src/components/workbench/WorkspaceFilePreviewPanel.vue(新增) |
workbench 预览面板:fetch content API → Monaco 只读编辑器(按扩展名切语言,面板复用时语言跟随);二进制/截断/错误降级提示;Monaco 加载失败退化为 <pre> |
src/components/workbench/workspaceFileWorkbenchProvider.ts(新增) |
workbench kind:'file' 面板 provider(无 runtime,component + getProps 即可) |
src/components/workbench/AppWorkbench.vue(+2 行) |
workbenchPanelRegistry.register(createWorkspaceFileWorkbenchDefinition()) —— 关键集成点,漏注册则预览面板不渲染 |
src/workbench/workspaceFileItems.ts(新增) |
createWorkspaceFileWorkbenchItem / workspaceFileFromWorkbenchItem(item id = ws-file:<workspaceId>:<path>,scope = {type:'workspace',id}) |
src/workbench/workspaceFileAttachEvent.ts(新增) |
自定义事件 opensquilla:workspace-file-attach(App.vue 派发,ChatView 监听) |
src/App.vue(+~70 行) |
侧边栏状态机:fileTreeWorkspace ref + sidebarView computed;SidebarConversations 改 v-show 保留挂载;SidebarFileTree 条件渲染;viewWorkspaceFiles(经 useProjectWorkspaces().byId 取 name/path);preview → workbenchStore.openItem;attach → 派发自定义事件 |
src/components/SidebarConversations.vue(+~30 行) |
项目行新按钮 data-testid="project-workspace-view-files"(可见性/禁用规则同 new-task:!selectionMode && canManageProjects && workspaceId && workspaceAvailable!==false)+ view-workspace-files emit |
src/views/ChatView.vue(+~60 行) |
监听附加事件 → fetch content → 构造 File 走既有 addAttachments 管线;拒绝二进制/截断/超 1MB(带 i18n toast 提示) |
src/locales/*.json(6 语言 × +14 key) |
fileTree.* 命名空间(backToTasks/refresh/retry/empty/loading/viewFiles/attachToChat/copyPath/previewTruncated/binaryNotPreviewable/attachTooLarge/attachFailed/attachUnsupported/attached) |
package.json + lock |
新增依赖 @tanstack/vue-virtual |
THIRD_PARTY_NOTICES.md(+~40 行) |
opencode MIT 许可登记 + 三个移植文件映射 |
提交时全部通过;2026-08-28 接棒复核重跑,结论一致:
- 后端:
uv run pytest tests/test_gateway/test_workspace_files_api.py→ 19 passed; 叠加tests/test_gateway/test_uploads_endpoint.py(46 项)回归共 65 passed;ruff 干净 (提交信息中"27 项后端新测"为笔误,以后端新测试文件 19 项 为准) - 前端:
npx vitest run→ 4794 项中 4792 过,仅ChannelsView.router.test.ts/ChannelsView.test.ts各 1 项 5s 超时 —— 系全量并行负载抖动,两文件单独重跑 69/69 全过, 与本功能无关(文件树相关 14 项 fileTreeModel + fileTree 测试全绿);npx vue-tsc --noEmit干净 npm run build全量 gate 通过:architecture / chat-security / colors(9 主题对比度)/ motion / radius / theme-contract / theme-scope / cross-surface-tokens / i18n(6 语言 key 对齐)/ theme bundle guard / 404 产物写入src/opensquilla/gateway/static/dist
- 未做:文件变更实时刷新(计划中 v1 为手动刷新);目录拖拽;git 变更角标(opencode 有,v1 砍掉)
- 附加到对话的边界:内容 >1MB 的文件拒绝附加(content API 上限),toast 提示;
二进制文件拒绝。若产品要求大文件附件,需后端新增"按 workspace 路径引用"的附件类型
(composer 的
Attachment目前只接受上传的File) - token 键名:HTTP 预览/列取用
sessionStorage.getItem('opensquilla.wsToken'), 与src/stores/rpc.ts的WS_TOKEN_KEY约定一致(4 处新代码均遵循) App.sidebar-contract.test.ts是文本扫描型契约测试,本次未断言项目行按钮构成, 无需改动;若后续在 App.vue 侧栏改结构,先看它- Windows 行尾:新 Python 文件 LF,git autocrlf 会转 CRLF(警告无害)
- 环境特殊性(本机):直连 GitHub 443 被墙,git 操作走
ghfast.top镜像 (git remote set-url origin https://ghfast.top/https://github.com/...用后改回);uv在C:\Users\Zc\.local\bin;worktree 的.venv与node_modules已就绪, 接棒无需重装
cd D:\WorkTree\workspace-file-tree
# 本地运行验证(webui 已构建,可直接起网关)
uv run opensquilla gateway run # → http://127.0.0.1:18791/control/
# 用户验证通过后推送(直连不通时临时切镜像)
git push -u origin feat/workspace-file-tree
# 按 worktree 规则合并(用户确认后在主目录执行)
git merge feat/workspace-file-tree && git push
git worktree remove D:\WorkTree\workspace-file-tree && git branch -d feat/workspace-file-tree- 侧边栏项目行 hover → 文件夹图标按钮 → 侧栏切为文件树;"< 返回任务"切回(会话列表滚动位置不丢)
- 目录展开懒加载、刷新按钮、错误态重试
- 点文件 → 右侧 workbench Monaco 只读预览;切文件语言跟随;二进制/超 1MB 降级提示
- 文件行 hover → "+" 附加进 composer 输入框、复制按钮 → 剪贴板为绝对路径
- workspace 不可用(
workspaceAvailable===false)时按钮禁用
分支未推送,git worktree remove + git branch -D feat/workspace-file-tree 即可完全移除,
main 无任何改动。