Files
mnote/design/04-tree-domain/done/4-20-vscode-explorer-file-tree-alignment-v1.md
T
lix-2026 d148e1ccc2 feat: 收口本地文件夹入口并推进 page aggregate rust-first
- 为 Rust Web 主入口补齐本地文件夹/云空间切换、最近目录与路径回填体验\n- 对齐 local markdown media 与 inline marks 的 Rust shell / TipTap converter 语义\n- 让 documents/page 优先消费 Rust page aggregate snapshot,并保留 TS fallback\n- 补强 tree live、local markdown 与主入口 smoke,并同步设计稿状态
2026-05-09 19:05:06 +08:00

18 KiB
Raw Blame History

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-runtimebuild_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,不是装饰。
  • selectionfocushoveractive 分离。
  • 右键点击已选区内资源时保留选区,右键点击未选资源时切换到该资源。
  • 键盘能完整操作可见行。
  • 内部拖拽、外部文件拖入、剪贴板和 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 rootroot 下再进入页面/资源树。
  • 多工作区:允许多个 workspace root。
  • root 行是否可折叠由 projection capability 决定。

当前截图中 我的页面 -> Explorer 的组合不够清晰,后续应改为:

  • section headerEXPLORER
  • 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 的关系不清晰。目标规则:

  • 打开文档时,docindex 可共享 active 文档语义,但只能有一个 focused row。
  • 如果 active document 映射到 index.md,应显式定义 active 高亮落在 index 还是 doc。建议:active 落 index.mddoc 行只作为目录/页面容器。
  • 多选时 active 不覆盖 selectionselection 背景优先。

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 runtimeArrow/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
  • resourceMetacapabilitiesiconHint 稳定。
  • 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 renameEsc 取消,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 行为歧义能通过测试说明和验证。
  • 新增能力没有制造第二套树真相。