# 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` 是长期对象真相层。
- Rust SQLite control-plane 承接默认 auth / share / sync / AI policy 控制面，Convex 仅保留历史迁移源、显式 cloud source / compat / sync replica 边界。
- `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 行为歧义能通过测试说明和验证。
- 新增能力没有制造第二套树真相。
