# 4-20 [done] VSCode Explorer 文件树对标设计 v1 > 更新时间:2026-05-06 > > 关联: > - `/mnt/Data1T/mnote/tmp/image copy 47.png` > - `/mnt/Data1T/mnote/ARCHITECTURE.md` > - `/mnt/Data1T/mnote/design/01-05-current-priority-overview.md` > - `/mnt/Data1T/mnote/design/04-tree-domain/done/4-2-sidebar-pagetree-filetree-product-interaction-contract-v1.md` > - `/mnt/Data1T/mnote/design/04-tree-domain/done/4-4-tree-projection-protocol-contract-v1.md` > - `/mnt/Data1T/mnote/design/04-tree-domain/done/4-6-tree-command-protocol-cutover-stage2-v1.md` > - `/mnt/Data1T/mnote/design/04-tree-domain/done/4-7-tree-shell-ui-state-boundary-v1.md` > - `/mnt/Data1T/mnote/design/04-tree-domain/done/4-14-task-005-file-tree-final-renderer-capability-checklist-v1.md` > - `/mnt/Data1T/mnote/design/05-editor-mainline/done/5-5-1-page-aggregate-contract-v1.md` > - `/mnt/Data1T/mnote/design/90-reference/90-1-filetree.md` ## 1. 目的 这份文档只处理 `filesystem / Explorer` 文件树下一阶段怎么对标 VSCode Explorer。 它不重新定义树域协议。已有结论继续成立: - `tree-first graph kernel` 是长期对象真相层。 - `Convex` 继续作为本地自托管存储、实时、文件协作底座。 - `mnote-web` 是 Rust Web 主执行面,负责 projection、command、transport 与 realtime stream。 - 前端和 tree shell 只消费稳定 projection,并持有本地交互态。 本设计补的是此前文档没有写细的 Explorer 级显示和操作合同:行模型、选择/焦点、打开、右键、键盘、拖放、剪贴板、新建、重命名、删除、上传、资源移动和验收清单。 ## 2. 当前截图判断 `/mnt/Data1T/mnote/tmp/image copy 47.png` 显示当前文件树已经有 Explorer 入口,但仍不像 VSCode Explorer: - `我的页面` 下直接混排页面节点和 `index.md`,但没有清晰的 workspace root / resource root 语义。 - 页面行使用小菱形 / 折叠箭头,文件行使用蓝色文档图标,信息密度接近,但行列网格不稳定。 - 当前选中态落在 `index.md` 文件行,和右侧文档页 `新页面` 的 active / focus / selection 关系不够清楚。 - `Explorer` 标题像 tab 文案,不像 VSCode Explorer 的 section header + toolbar。 - hover 操作只看到 `+`,缺少新建文件、新建文件夹、刷新、全部折叠、更多等容器级动作。 - 长列表中大量重复 `新页面/index.md`,缺少资源装饰、路径 disambiguation、命名冲突处理和 reveal 规则。 结论:当前不是“样式再修一点”的问题,而是 Explorer 行为合同还没有写到足够细。 ## 3. 唯一真源 ### 3.1 结构显示真源 文件树显示的唯一真源固定为: `kernel.project_view(file_tree)` 产出的 `KernelProjectionResult`。 当前代码证据: - 首屏 query 会从 Convex `sidebar.datasetList` 取数据,再通过 `resolveKernelFileTreeProjection` 调 Rust `kernel.project_view(file_tree)` 附加 `kernel_file_tree_projection`。 - 实时链路 `/api/mnote-web/stream` snapshot 也会附加同一份 file tree projection。 - Rust 侧生成点是 `bridge-runtime` 的 `build_file_tree_projection_result`。 - projection item 的核心字段已经在 `core-protocol/src/kernel.rs` 与前端 `tree-protocol.ts` 固定。 因此,Explorer 不能在 UI 层重新推导层级、排序、资源类型、child count、capabilities 或合法性。UI 只能把 projection item materialize 成 row。 ### 3.2 持久化操作真源 持久化动作的唯一正式命令面固定为: - 页面:`tree.node.*`、`tree.subtree.*` - 资源:`tree.resource.*` - 文件树预检:`tree.filetree.*.preflight` 当前已经成立: - 页面新建、重命名、移动、归档、恢复、永久删除、嵌入、复制子树已经接到 `tree.*`。 - 文件树 `drop/delete/paste/upload-target` 已有 Rust preflight。 - 资源 `copy/move/upload` 已有 `tree.resource.copy/move/upload`。 当前仍未完成: - 资源 `rename/delete/restore` 仍经过 legacy `/api/media/batch` 直接 patch / restore。 - `cut` 还没有成为剪贴板动作。 - 资源“移动到...”仍有 `prompt("输入目标页面 ID")` 的临时 UX。 ### 3.3 本地交互态 这些状态只能属于 tree shell / runtime 本地态,不得写进 projection: - `expanded` - `selected` - `focused` - `hovered` - `editingName` - `clipboard` - `dragging` - `dropTarget` - `contextMenuOpen` - `keyboardAnchor` - `revealPending` `expandedByDefault` 只是 projection 给初始展开的建议,不是展开态真相。 ## 4. VSCode Explorer 对标边界 ### 4.1 必须对齐 文件树必须对齐 VSCode Explorer 的资源管理器模型: - 一行就是一个资源命中目标。 - `twistie` 只负责展开/折叠,打开资源是另一种动作。 - 图标来自 `rowKind/resourceMeta/iconHint/assetKind`,不是装饰。 - `selection`、`focus`、`hover`、`active` 分离。 - 右键点击已选区内资源时保留选区,右键点击未选资源时切换到该资源。 - 键盘能完整操作可见行。 - 内部拖拽、外部文件拖入、剪贴板和 context menu 都基于同一 selection/focus 状态。 - 所有危险或跨资源类型动作先走 preflight plan,再落正式 command。 ### 4.2 不能照搬 文件树不能照搬 VSCode 的真实文件系统语义: - mnote 的文件树是页面、`index.md`、附件、mindmap 文件夹、导图子资源、table/book/pdf 等复合资源投影,不是磁盘目录树。 - `page -> index.md -> asset-folder -> asset` 这类多语义链路不能用 VSCode `compact folders` 强行压成一行。 - “在终端打开”“Reveal in Finder/Explorer”“文件比较”等 OS 动作不进入默认合同,除非资源模型已有明确承载。 - VSCode 的单击预览 / 双击固定标签页节奏不能直接照搬。mnote 的打开必须服从当前文档页、Page Aggregate、附件打开链路和权限边界。 - Explorer 是 sidebar 中的创新视图,不是整个产品的唯一主导航。`section` 页面树仍保留 Wolai / Notion 方向。 ## 5. 显示合同 ### 5.1 Row Grid 每个 file tree row 固定为以下视觉列: | 列 | 说明 | | --- | --- | | indent | 只由 `depth` 决定,步长固定 | | twistie | 仅 `expandable=true` 时可见并可点击 | | icon | 由 `iconHint/assetKind/resourceKind` 决定 | | label | 主名称,单行截断 | | decoration | 条件性 badge、状态色、资源提示 | | actions | hover / focus 时出现的轻量动作 | 不得让 label 长度、hover 按钮或选中态改变行高和缩进。 ### 5.2 Row Kind 当前稳定行类型继续保留: - `doc` - `index` - `asset-folder` - `asset` 后续如果需要真实目录或资源组,必须先扩展 Rust projection 的 `rowKind/resourceKind`,不能在 UI 层把某类 asset 临时伪装成目录。 ### 5.3 Root Explorer 顶部应有明确 root: - 单工作区:显示 workspace root,root 下再进入页面/资源树。 - 多工作区:允许多个 workspace root。 - root 行是否可折叠由 projection capability 决定。 当前截图中 `我的页面 -> Explorer` 的组合不够清晰,后续应改为: - section header:`EXPLORER` - root row:当前 workspace 名称 - toolbar:新建页面 / 新建资源 / 刷新 / 全部折叠 / 更多 ### 5.4 Icon 与 Decoration 图标语义至少覆盖: - workspace / folder / page / index - asset - image / video / audio - pdf / book - table - mindmap - unknown Decoration 只表达轻量状态,例如: - stale / indexing / uploading - read-only / shared / public - conflict / invalid target - cut clipboard visual Decoration 不得变成第二行副标题。 ### 5.5 Active、Selection、Focus 三者必须分离: - `active`:右侧主区域正在打开的 document / asset。 - `selection`:Explorer 当前选中集合,支持多选。 - `focus`:键盘焦点所在 row。 截图中 `index.md` 选中与右侧 `新页面` active 的关系不清晰。目标规则: - 打开文档时,`doc` 与 `index` 可共享 active 文档语义,但只能有一个 focused row。 - 如果 active document 映射到 `index.md`,应显式定义 active 高亮落在 `index` 还是 `doc`。建议:active 落 `index.md`,doc 行只作为目录/页面容器。 - 多选时 active 不覆盖 selection,selection 背景优先。 ## 6. 操作合同 ### 6.1 打开 P0: - `Enter` 打开 focused row。 - 双击 row 打开资源。 - 单击只负责 selection/focus,不自动打开重资源。 - `index.md` 打开对应文档页。 - asset 打开对应资源 viewer / editor。 P1: - 支持“Reveal active resource in Explorer”。 - 页面切换后自动展开祖先链并定位 active row。 - 搜索过滤后保留 active/reveal 的路径上下文。 说明:当前代码已明确与 VSCode 单击打开不同,这是合理差异,但必须写成正式合同,避免后续反复摇摆。 ### 6.2 展开 / 折叠 P0: - 点击 twistie 只展开/折叠,不打开。 - `ArrowRight`:可展开行未展开则展开;已展开或叶子则可进入打开动作,具体由 runtime 定义。 - `ArrowLeft`:已展开则折叠;未展开则焦点移动到父节点。 - `Home/End` 跳到第一/最后可见行。 P1: - `Collapse All` 容器动作。 - 外部拖拽悬停到可展开目标一段时间后自动展开。 ### 6.3 选择与右键 P0: - 单击:单选并设置 anchor/focus。 - `Ctrl/Cmd + Click`:切换选中。 - `Shift + Click`:从 anchor 到目标做范围选。 - 右键已选区内 row:保留选区并打开菜单。 - 右键未选 row:切换 selection 到该 row,再打开菜单。 - 空白区点击:清空 selection/focus。 P1: - `Ctrl/Cmd + A` 选中当前 Explorer 可见资源。 - 过滤态下范围选只覆盖可见 row。 ### 6.4 键盘 P0 最小按键: | 按键 | 动作 | | --- | --- | | `ArrowUp/ArrowDown` | 上下移动 focus | | `ArrowLeft/ArrowRight` | 折叠 / 展开 / 父子导航 | | `Home/End` | 首尾行 | | `Enter` | 打开 focused row | | `F2` | 重命名 | | `Delete/Backspace` | 删除 / 归档 | | `Ctrl/Cmd+C` | copy | | `Ctrl/Cmd+X` | cut | | `Ctrl/Cmd+V` | paste | | `Esc` | 取消 inline rename / 关闭菜单 / 清理 drag feedback | 当前缺口:已有 copy/paste,但缺 `cut`;键盘能力需要以 runtime contract 覆盖 file tree,而不是散在 `sidebar.tsx`。 ### 6.5 新建 P0: - `New Page`:映射 `tree.node.create`,在目标 doc/root 下创建页面,并进入 inline rename。 - `New File`:只允许创建 mnote 资源模型内真实存在的资源类型;如果当前没有通用文件创建命令,入口应隐藏或置灰。 - `New Folder`:当前没有真实目录语义,不提供默认入口;mindmap asset-folder 不是通用文件夹。 P1: - 支持上传文件作为 `New File` 的可用替代路径,走 `tree.filetree.upload-target.preflight -> tree.resource.upload`。 - 如果未来引入通用资源目录,先扩展 kernel projection / command,再开启 `New Folder`。 ### 6.6 重命名 P0: - `F2` 和 context menu `Rename` 进入 inline rename,不再使用 `prompt`。 - doc / index 重命名最终落 `tree.node.rename` 或 Page Aggregate title command 的正式命名面。 - asset 重命名必须从 legacy `/api/media/batch` 收口到 `tree.resource.rename` 或等价正式命令。 - `Enter` 提交,`Esc` 取消。 - 空名、重名、非法字符、只读资源必须由 preflight / command 返回可解释错误。 P1: - 文件型 asset 默认只选中文件名 stem,不误选扩展名。 - rename 期间保留 row 高度和缩进,不触发布局跳动。 ### 6.7 删除 / 恢复 / 永久删除 P0: - doc 删除走 `tree.node.archive`。 - doc 恢复走 `tree.node.restore`。 - doc 永久删除走 `tree.node.purge`,必须是更强确认路径。 - asset 删除 / 恢复必须补正式 `tree.resource.archive/restore` 或等价命令,不能长期停留在 legacy `/api/media/batch`。 - 多选删除先走 `tree.filetree.delete.preflight`,展示将影响的 doc/asset 集合。 P1: - 支持删除后 undo / toast action,命令结果仍以 kernel event 和 projection resync 为准。 ### 6.8 剪贴板 P0: - `copy`:复制选中 doc subtree / asset resource。 - `cut`:进入 cut clipboard 状态,源 row 降噪显示;paste 成功后清空 cut 状态。 - `paste`:目标由 focused row / context target / active doc 推导,必须先走 `tree.filetree.paste.preflight`。 - 多选 paste 保持原有可见顺序,并过滤父子重复选择。 P1: - 系统剪贴板只保存安全 payload,不暴露敏感内部 token。 - 跨窗口 paste 需要校验 workspace 和权限。 ### 6.9 拖放 P0: - 内部拖放支持 move/copy,修饰键控制 copy。 - 外部文件拖入支持 upload target preflight。 - 非法目标即时反馈,不发持久化命令。 - doc 拖到自身或后代节点必须拒绝。 - asset-folder / asset 的可投放能力由 `capabilities` 和 preflight 决定。 P1: - 拖到可展开目标 hover 延迟后自动展开。 - 显示 before / inside / after 三种 drop indicator,而不是只有整行背景。 - 多选拖放过滤已被父节点包含的子节点。 ### 6.10 Context Menu 菜单按 `rowKind/resourceKind/selection` 裁剪: | 目标 | P0 动作 | | --- | --- | | root | 新建页面、上传文件、刷新、全部折叠 | | doc | 打开、新建子页面、重命名、复制、剪切、粘贴、移动到、归档、复制链接 | | index | 打开、复制、剪切、粘贴、Reveal 页面 | | asset-folder | 打开、复制、剪切、粘贴、重命名、删除、移动到 | | asset | 打开、复制、剪切、重命名、删除、下载、移动到 | | 多选 | 打开禁用、复制、剪切、删除、移动到 | 当前缺口: - mindmap rename 提示不支持。 - table move/download 提示不支持。 - asset 移动仍靠输入页面 ID。 这些必须进入 P0/P1 差异清单,不得被样式修复掩盖。 ## 7. 资源排序与混排规则 P0 固定当前最小规则: 1. workspace root 2. doc row 3. doc 的 `index.md` 4. doc 的 direct assets / asset folders 5. child documents 排序由 Rust projection 的 `position` 与 preorder 决定。UI 只渲染。 需要新增到 projection meta 的候选规则: - `ordering: "kernel_file_tree_preorder"` - `folderPolicy: "mnote_composite_resource"` - `indexPlacement: "first_child"` - `assetPlacement: "after_index_before_child_documents"` 这样可以避免 UI 通过 hardcode 解释为什么 `index.md` 总是出现在页面下第一行。 ## 8. Search / Filter / Reveal P0: - Explorer filter 只改变可见 row,不改变 projection truth。 - filter 命中子节点时必须补全祖先链。 - active row 在 filter 后不可见时,保留 active 状态但不强制显示。 - `Reveal in Explorer` 必须展开祖先链、滚动到目标 row、设置 focus,不破坏 selection,除非调用者明确要求 select。 P1: - 支持 `maxResults` 下的 ancestor completion。 - 支持 indexing visibility,展示 stale / refreshing 状态。 当前 `KernelFileTreeProjection.meta.search.indexingVisibility` 已有雏形,后续应把它用于 UI 状态,而不是在 Explorer 自行发明刷新状态。 ## 9. 分阶段任务 ### P0:让当前 Explorer 行为不再含糊 - 固定 active / selection / focus 的视觉优先级。 - 补齐 file tree keyboard runtime:`Arrow/Home/End/Enter/F2/Delete/Cut/Copy/Paste/Esc`。 - 补 `cut` clipboard 状态和 cut row decoration。 - 把 doc / index / asset / asset-folder 的 context menu 动作裁剪成合同。 - 移除资源移动的 `prompt("输入目标页面 ID")`,改用 tree picker / move target picker。 - asset rename/delete/restore 制定 `tree.resource.*` 正式命令或等价 bridge 命名,并保留 legacy alias。 - 写 smoke 固化当前截图中重复页面 + `index.md` 场景的 active/selection/reveal 行为。 ### P1:让资源管理器达到 VSCode 常用操作预期 - inline rename 全面替代 prompt。 - root toolbar 对齐 Explorer:新建页面、上传文件、刷新、全部折叠、更多。 - 拖放支持 before / inside / after indicator 与 hover auto expand。 - 上传 target preflight 失败时在 drop target 上显示明确反馈。 - 多选 delete / move / copy 的 preflight plan 显示影响集合。 - mindmap/table 的 rename/move/download 能力从“不支持”变为可解释菜单状态或正式实现。 ### P2:高级 Explorer 能力 - compact folders 只用于未来真实目录链,不用于页面/附件复合链。 - git-like / sync-like decorations,如果未来有对应数据源再加入。 - 多 workspace root。 - 大树虚拟化和增量加载。 - 跨窗口剪贴板。 ## 10. 验收 ### 10.1 单元 / 合同测试 必须覆盖: - projection item 到 row model 的稳定映射。 - active / selection / focus 分离。 - `Ctrl/Cmd` 多选、`Shift` 范围选、右键保留选区。 - 可见 row 变化后的 selection normalize。 - `cut/copy/paste` payload encode/decode 与目标推导。 - drop preflight payload 构造。 - illegal drop:自身、后代、跨 workspace、只读资源。 - inline rename submit/cancel。 ### 10.2 Rust 测试 必须覆盖: - `kernel.project_view(file_tree)` 输出 `doc/index/asset-folder/asset`。 - `resourceMeta`、`capabilities`、`iconHint` 稳定。 - `tree.filetree.*.preflight` 对多选和非法目标返回 plan。 - 新增 `tree.resource.rename/archive/restore` 或等价命令后,覆盖 command event 与 projection resync。 ### 10.3 浏览器 smoke 必须覆盖: - 当前截图同类数据:多个同名 `新页面`,每个页面下有 `index.md`。 - 点击 `index.md` 后右侧打开对应页面,Explorer active/focus/selection 可解释。 - 右键已选区保留多选。 - `F2` inline rename,`Esc` 取消,`Enter` 提交。 - `Ctrl/Cmd+X` 后 cut decoration 出现,paste 成功后消失。 - 外部文件拖入 doc / index / asset-folder 的目标差异。 - 资源移动不再弹 `prompt`。 ## 11. 非目标 - 不把文件树改成真实磁盘文件系统。 - 不把 `Explorer` 替代 `section` 页面树作为唯一导航。 - 不在 UI 层重新拼排序、层级、资源类型或权限。 - 不为了像 VSCode 而绕过 Page Aggregate、tree projection 或 `tree.*` 命令。 - 不把 `90-1-filetree.md` 的 Rust 生态调研当作实现模板;它只作为参考资料。 ## 12. 完成判定 这份设计进入 done 的条件: - P0 项全部有代码和 smoke 覆盖。 - file tree 的 active / selection / focus / keyboard / context menu / clipboard / DnD 有稳定 runtime contract。 - 资源 rename/delete/restore 不再只依赖 legacy `/api/media/batch` 主路径。 - 当前截图暴露的 Explorer 行为歧义能通过测试说明和验证。 - 新增能力没有制造第二套树真相。