docs: 继续收口设计稿状态与主线口径
This commit is contained in:
@@ -0,0 +1,44 @@
|
||||
# 3-11 [done] Rust Web Legacy Next Retirement Gates v1
|
||||
|
||||
> 状态说明:
|
||||
> - 本稿定义的 default gate 已在当前主线代码中成立:`mnote-web` 是 `3000` owner,Next 仅保留 legacy compat/debug 边界
|
||||
> - `MNOTE_WEB_ENABLE_LEGACY_NEXT_COMPAT` 默认关闭与 `task117` guard 已落地,故迁入 `done/`
|
||||
|
||||
## 目标
|
||||
|
||||
将 Next App Router 从 3000 默认主链降级为显式 legacy/debug/internal 兼容边界。默认首页、文档页、搜索页、导图页、tree SSE 与 AI bridge host 均由 `mnote-web` 承接;长期 agent 执行面收口到 `mnote-cli`。
|
||||
|
||||
## 当前 owner
|
||||
|
||||
- `/`:Rust Web `gateway::root_entry`,owner `mnote-web`。
|
||||
- `/documents/{document_id}`:Rust Web `web_shell::document_page_shell`,Page Aggregate owner `rust-kernel`。
|
||||
- `/search`:Rust Web `search::shell`,Search projection owner `rust-kernel`。
|
||||
- `/mindmap/{doc_id}/{mindmap_id}`:Rust Web `mindmap_shell::mindmap_object_shell`,Mindmap projection owner `rust-kernel`。
|
||||
- `/api/tree/events`:Rust Web SSE,stream owner `rust-web`。
|
||||
- `/api/hermes/bridge`:Rust Web 兼容 AI bridge;仅作为过渡边界,不是长期 agent 执行面。
|
||||
- `/api/compat/next/*`:legacy compat boundary,仅用于迁移期兼容与调试。
|
||||
|
||||
## Gate
|
||||
|
||||
- `MNOTE_WEB_ENABLE_LEGACY_NEXT_COMPAT` 默认关闭;只有显式设置为 `1/true/yes` 才允许 fallback proxy。
|
||||
- 默认主路径不得返回 `x-mnote-legacy-upstream: next-app-router`。
|
||||
- 导图、搜索、文档页 contract 不得再把 `next-app-router` 声明为主 runtime。
|
||||
- `/api/ai-agent/run` 当前仍保留兼容 route,但长期应降为 `mnote-cli` host / adapter,结构化写入必须经 Rust runtime,外置 agent 不得拥有第二执行面。
|
||||
|
||||
## 删除条件
|
||||
|
||||
- 删除 `/api/compat/next/sidebar`:Sidebar、workspace shell、file tree smoke 均证明 Rust projection 可覆盖默认入口。
|
||||
- 删除 `/api/compat/next/ai-agent/run`:AI 面板默认请求统一 CLI host,legacy 调用方清零;Hermes 仅保留为可插拔外置 agent。
|
||||
- 删除 fallback proxy:`task117` 默认关闭 legacy compat 后覆盖首页、文档页、搜索页、导图页与 tree SSE。
|
||||
|
||||
## 验收命令
|
||||
|
||||
```bash
|
||||
cd /mnt/Data1T/mnote/rust
|
||||
cargo test -p mnote-web gateway
|
||||
cargo test -p mnote-web legacy_next
|
||||
|
||||
cd /mnt/Data1T/mnote
|
||||
MNOTE_UI_BASE_URL=http://127.0.0.1:3000 node scripts/task117-next-retirement-guard.js
|
||||
rg -n "legacy_next|compat/next|next-app-router|fallback proxy" rust/crates/mnote-web wolai-frontend design
|
||||
```
|
||||
@@ -0,0 +1,807 @@
|
||||
# 3-14 [done] Rust Web 双窗格可编辑文档壳方案 v1
|
||||
|
||||
> 更新时间:2026-05-08
|
||||
>
|
||||
> 状态说明:
|
||||
> - 本稿对应的双窗格文档壳、共享 session、事件流复用、URL 恢复与自动化 smoke 已完成,故迁入 `done/`
|
||||
> - 下文第 2 节保留的是实现前问题基线;当前真实完成状态与验证证据以第 15-16 节为准
|
||||
>
|
||||
> 关联文档:
|
||||
> - `/mnt/Data1T/mnote/design/03-rust-web/process/3-rust-web-long-term-architecture-v1.md`
|
||||
> - `/mnt/Data1T/mnote/design/03-rust-web/process/3-3-rust-web-tree-realtime-event-stream-v1.md`
|
||||
> - `/mnt/Data1T/mnote/design/03-rust-web/process/3-13-rust-web-local-markdown-gfm-ast-parser-migration-v1.md`
|
||||
> - `/mnt/Data1T/mnote/design/05-editor-mainline/process/5-5-page-aggregate-single-truth-alignment-v1.md`
|
||||
> - `/mnt/Data1T/mnote/design/05-editor-mainline/process/5-6-page-aggregate-alignment-checklist-v1.md`
|
||||
|
||||
## 1. 结论
|
||||
|
||||
截至 2026-05-09,当前 `3000` 已经具备:
|
||||
|
||||
- 右侧 `secondary pane` 文档容器
|
||||
- pane 状态模型与 URL 恢复
|
||||
- 同文档双开共享 session 与即时同步
|
||||
- tree live stream / local-folder 事件流复用
|
||||
- 对应 Rust 单测与浏览器 smoke
|
||||
|
||||
本稿记录的是这条双窗格能力从问题定义到完成验收的收口过程。
|
||||
|
||||
最终实现没有做成“再开一个完整页面”或“在右侧塞一个 iframe”,而是在现有 `workspace shell` 内引入了:
|
||||
|
||||
- 双窗格文档宿主
|
||||
- 可复用的文档 session 层
|
||||
- 可复用的 tree / local-folder 事件流
|
||||
|
||||
目标是做到接近 Wolai / VSCode 的第一阶段体验:
|
||||
|
||||
- 左侧主文档可继续导航
|
||||
- 右侧文档独立保持不变
|
||||
- 右侧可直接编辑
|
||||
- 同一文档允许同时在左右两个 pane 打开
|
||||
- 左右任一侧编辑,另一侧即时同步
|
||||
- watcher / SSE 数量不随 pane 数线性膨胀
|
||||
|
||||
## 2. 当前问题
|
||||
|
||||
### 2.1 入口已存在,但容器不存在
|
||||
|
||||
当前右键菜单中的“在右侧边栏打开”会派发:
|
||||
|
||||
- `tree.page.open-right`
|
||||
|
||||
但 `mnote-web` 还没有:
|
||||
|
||||
- `secondary pane` 宿主
|
||||
- pane 状态模型
|
||||
- 右侧文档加载/关闭/替换逻辑
|
||||
- 多编辑器实例同步机制
|
||||
|
||||
所以当前缺的不是单个按钮,而是整套双窗格运行时。
|
||||
|
||||
### 2.2 不能用“再开一页”糊过去
|
||||
|
||||
如果把右侧打开实现为:
|
||||
|
||||
- 新窗口
|
||||
- iframe
|
||||
- 第二个完整 document shell 页面
|
||||
|
||||
那么会重复创建:
|
||||
|
||||
- 标题控制器
|
||||
- editor bootstrap
|
||||
- tree SSE
|
||||
- local-folder EventSource
|
||||
- 保存队列
|
||||
- 外部变更检测
|
||||
|
||||
这样做短期能跑,长期会把实时同步、冲突检测和性能问题一起放大。
|
||||
|
||||
### 2.3 本地 Markdown 和 Convex Workspace 都需要纳入统一模型
|
||||
|
||||
当前文档来源至少有两类:
|
||||
|
||||
- `convex_workspace`
|
||||
- `local_folder`
|
||||
|
||||
双窗格方案必须同时覆盖这两类来源,不能只对 Convex 页面生效,再把本地 Markdown 排除到特殊路径。
|
||||
|
||||
## 3. 目标
|
||||
|
||||
- 在单一 `workspace shell` 内支持 `primary pane + secondary pane`
|
||||
- 右侧 pane 默认直接可编辑
|
||||
- 左侧切换页面时,右侧 pane 保持不变
|
||||
- 支持同一文档同时在左右两个 pane 打开
|
||||
- 同一文档双开时,左右编辑器共享同一份文档 session,并即时同步
|
||||
- 复用 tree realtime 和 local-folder 外部变更链路,不按 pane 数重复建立 watcher
|
||||
- reload 后恢复双窗格状态
|
||||
|
||||
## 4. 非目标
|
||||
|
||||
- 这一步不直接做 VSCode 式多 group + tabs
|
||||
- 这一步不做跨浏览器窗口的协同光标
|
||||
- 这一步不把 page aggregate、title controller、settings 全部重构一遍
|
||||
- 这一步不处理“无限多个右侧 pane”
|
||||
- 这一步不做右侧 pane 的独立页面树
|
||||
|
||||
## 5. 方案比较
|
||||
|
||||
### 方案 A:右侧嵌完整 document shell / iframe
|
||||
|
||||
优点:
|
||||
|
||||
- 实现快
|
||||
|
||||
缺点:
|
||||
|
||||
- 会复制整套文档运行时
|
||||
- watcher / SSE / 保存逻辑重复
|
||||
- 同一文档双开难以稳定同步
|
||||
- 后续演进到 tabs / groups 时基本要重做
|
||||
|
||||
### 方案 B:单一 workspace shell + pane manager + document session registry
|
||||
|
||||
优点:
|
||||
|
||||
- 单一页面壳、单一树事件流
|
||||
- 文档实例和外层壳职责清晰
|
||||
- 同一文档双开可以共享 session
|
||||
- 最适合作为后续 tabs / groups 的基础
|
||||
|
||||
缺点:
|
||||
|
||||
- 需要补一层 pane/session 运行时
|
||||
- 需要收口 editor 实例与 session 的边界
|
||||
|
||||
### 方案 C:直接做 VSCode 风格 editor groups + tabs
|
||||
|
||||
优点:
|
||||
|
||||
- 长期形态最强
|
||||
|
||||
缺点:
|
||||
|
||||
- 范围明显过大
|
||||
- 当前没有 tab/group/session 三层基础,直接做会把任务扩散
|
||||
|
||||
### 推荐
|
||||
|
||||
选方案 B。
|
||||
|
||||
它能在不过度扩面的前提下,把真正需要的底层模型一次收对。
|
||||
|
||||
## 6. 交互契约
|
||||
|
||||
### 6.1 窗格结构
|
||||
|
||||
页面维持一个共享 `workspace shell`,内容区拆为:
|
||||
|
||||
- `primary pane`
|
||||
- `secondary pane`
|
||||
|
||||
共享部分只有一份:
|
||||
|
||||
- 左侧树
|
||||
- 顶部壳
|
||||
- workspace 级搜索
|
||||
- tree realtime stream
|
||||
|
||||
### 6.2 打开规则
|
||||
|
||||
普通点击页面树:
|
||||
|
||||
- 继续只驱动 `primary pane`
|
||||
|
||||
“在右侧边栏打开”:
|
||||
|
||||
- 若右侧未打开,则创建 `secondary pane`
|
||||
- 若右侧已打开,则替换右侧当前文档
|
||||
- 不影响 `primary pane`
|
||||
|
||||
### 6.3 保持规则
|
||||
|
||||
当左侧主 pane 切换页面时:
|
||||
|
||||
- `secondary pane` 保持不变
|
||||
|
||||
只有以下操作会改变右侧内容:
|
||||
|
||||
- 再次执行“在右侧边栏打开”
|
||||
- 用户主动在右侧 pane 内导航
|
||||
- 用户关闭右侧 pane
|
||||
|
||||
### 6.4 同文档双开规则
|
||||
|
||||
允许同一文档同时出现在左右两个 pane:
|
||||
|
||||
- 两侧都可编辑
|
||||
- 任一侧输入后,另一侧即时同步
|
||||
- 保存只走同一个共享 session 的保存队列
|
||||
|
||||
这一步的目标是接近 VSCode:
|
||||
|
||||
- 同一文件可在两个 editor view 中同时打开
|
||||
- 修改后另一侧无需 reload 即更新
|
||||
|
||||
### 6.5 关闭与布局规则
|
||||
|
||||
第一阶段支持:
|
||||
|
||||
- 关闭右侧 pane
|
||||
- 拖动分割线调整宽度
|
||||
|
||||
第一阶段不支持:
|
||||
|
||||
- 多个右侧 pane
|
||||
- 窗格 tab bar
|
||||
- 拖拽重排成多个 group
|
||||
|
||||
## 7. URL 与恢复策略
|
||||
|
||||
为保证 reload、复制链接和恢复状态稳定,建议 URL 扩展为:
|
||||
|
||||
- 主文档仍使用当前 `documentId`
|
||||
- 右侧文档增加 `secondaryDocumentId`
|
||||
|
||||
对于本地 Markdown,再补:
|
||||
|
||||
- `secondarySourceKind`
|
||||
- `secondaryRootUri`
|
||||
|
||||
示意:
|
||||
|
||||
```text
|
||||
/documents/doc_main?workspaceId=ws_demo&secondaryDocumentId=doc_side
|
||||
```
|
||||
|
||||
本地 Markdown 示例:
|
||||
|
||||
```text
|
||||
/documents/local-md:README.md?sourceKind=local_folder&rootUri=file:///root&secondaryDocumentId=local-md:guide.md&secondarySourceKind=local_folder&secondaryRootUri=file:///root
|
||||
```
|
||||
|
||||
恢复规则:
|
||||
|
||||
- 页面刷新后,按 URL 先恢复 `primary pane`
|
||||
- 若存在 `secondaryDocumentId`,再恢复右侧 pane
|
||||
- 若右侧文档失效,则降级为仅保留主 pane,并清理失效的 secondary 参数
|
||||
|
||||
## 8. 运行时分层
|
||||
|
||||
### 8.1 Pane Manager
|
||||
|
||||
新增 `PaneManager`,只负责:
|
||||
|
||||
- 当前布局是否双栏
|
||||
- `primary / secondary` 各自绑定哪个文档
|
||||
- 当前聚焦 pane
|
||||
- 右侧 pane 的宽度状态
|
||||
|
||||
它不负责:
|
||||
|
||||
- 文档内容真相
|
||||
- 保存逻辑
|
||||
- 外部文件 watcher
|
||||
|
||||
### 8.2 Document Session Registry
|
||||
|
||||
新增 `DocumentSessionRegistry`,按“文档来源键”维护共享 session。
|
||||
|
||||
建议 session key 形状:
|
||||
|
||||
```text
|
||||
{sourceKind}:{workspaceId or rootUri}:{documentId}
|
||||
```
|
||||
|
||||
例如:
|
||||
|
||||
- `convex_workspace:ws_demo:doc_1`
|
||||
- `local_folder:file:///mnt/Data1T/mnote/design:local-md:README.md`
|
||||
|
||||
每个 session 负责:
|
||||
|
||||
- 最新 `PageAggregate` 快照
|
||||
- editor 共享 revision / conflictDetectionKey
|
||||
- dirty 状态
|
||||
- 保存队列
|
||||
- 外部变更状态
|
||||
- 已挂载的 editor view 列表
|
||||
|
||||
### 8.3 Editor View
|
||||
|
||||
每个 pane 内的实际编辑器实例叫 `EditorViewBinding`。
|
||||
|
||||
它只负责:
|
||||
|
||||
- 渲染当前 session 内容
|
||||
- 把本地输入回传到 session
|
||||
- 接收 session 推送并更新本 view
|
||||
- 维护本 view 的 selection / focus
|
||||
|
||||
因此:
|
||||
|
||||
- 一个 session 可挂多个 editor view
|
||||
- 左右双开同一文档时,是“两个 view 绑定同一个 session”
|
||||
|
||||
## 9. 同文档双开同步模型
|
||||
|
||||
### 9.1 为什么不能只靠保存后 reload
|
||||
|
||||
如果左右同步只依赖:
|
||||
|
||||
- 一侧保存
|
||||
- 另一侧通过 page aggregate reload
|
||||
|
||||
那么会出现:
|
||||
|
||||
- 输入延迟可见
|
||||
- 正在编辑时 selection 容易抖动
|
||||
- 双侧频繁互相 replaceContent
|
||||
|
||||
这不符合“同一文档双开”的编辑预期。
|
||||
|
||||
### 9.2 推荐同步口径
|
||||
|
||||
同一 session 下的多个 editor view 应采用:
|
||||
|
||||
- session 内即时内存同步
|
||||
- session 到后端的单一保存队列
|
||||
- 后端返回成功后再统一推进 revision / conflict key
|
||||
|
||||
也就是:
|
||||
|
||||
1. 左侧输入
|
||||
2. 左侧 view 把变更交给 session
|
||||
3. session 更新内存态
|
||||
4. session 向右侧 view 广播变更
|
||||
5. session 统一防抖保存
|
||||
|
||||
这样可以保证:
|
||||
|
||||
- 左右即时同步
|
||||
- 只保存一次
|
||||
- revision / conflictDetectionKey 始终只有一份
|
||||
|
||||
### 9.3 Selection 规则
|
||||
|
||||
同步内容时不强制同步对侧 selection。
|
||||
|
||||
即:
|
||||
|
||||
- 同步文档内容
|
||||
- 不同步光标和滚动位置
|
||||
|
||||
这是第一阶段最稳的边界。
|
||||
|
||||
## 10. Tree Realtime 与 Watcher 复用
|
||||
|
||||
### 10.1 Tree SSE
|
||||
|
||||
每个浏览器窗口只保留一条 workspace 级 tree realtime 连接。
|
||||
|
||||
它属于 `workspace shell`,不属于某个 pane。
|
||||
|
||||
因此:
|
||||
|
||||
- 不因为打开右侧 pane 再创建第二条 tree SSE
|
||||
|
||||
### 10.2 Frontend Local Folder Event Registry
|
||||
|
||||
本地 Markdown 外部变更建议新增前端 `LocalFolderEventRegistry`:
|
||||
|
||||
- 按 `rootUri` 复用 EventSource
|
||||
- 一个 `rootUri` 只建立一条 `/api/local-folder/events`
|
||||
- 命中的文件变化再按 `documentId` 分发到对应 session
|
||||
|
||||
这样:
|
||||
|
||||
- 同一根目录下左右打开两个 Markdown 文件,不会创建两条 EventSource
|
||||
- 同一 Markdown 在左右双开,也不会创建两条 EventSource
|
||||
|
||||
### 10.3 Backend Local Folder Watcher Registry
|
||||
|
||||
Rust 侧继续往前收口,新增或演进为 `LocalFolderWatcherRegistry`:
|
||||
|
||||
- 按 `rootUri` 维护单例 watcher
|
||||
- 一个 `rootUri` 在单个服务进程内只保留一个 `notify` watcher
|
||||
- 多个 SSE 订阅者共享 watcher 输出
|
||||
|
||||
watcher 发出的应是:
|
||||
|
||||
- 相对路径变化
|
||||
- 事件类型
|
||||
|
||||
再由 session 判断:
|
||||
|
||||
- 当前文档是否命中
|
||||
- 当前是否 dirty
|
||||
- 是自动同步还是冲突提示
|
||||
|
||||
### 10.4 设计目标
|
||||
|
||||
最终目标不是:
|
||||
|
||||
- `N 个 pane = N 个 watcher`
|
||||
|
||||
而是:
|
||||
|
||||
- `同窗口同 workspace` 下 tree SSE 恒为 1
|
||||
- `同窗口同 rootUri` 下 local-folder EventSource 恒为 1
|
||||
- `同进程同 rootUri` 下 Rust notify watcher 恒为 1
|
||||
|
||||
## 11. 保存与冲突策略
|
||||
|
||||
### 11.1 Convex Workspace
|
||||
|
||||
同一 session 的多个 editor view 共享一份:
|
||||
|
||||
- dirty 状态
|
||||
- saving 状态
|
||||
- revision
|
||||
|
||||
只要 session 正在保存:
|
||||
|
||||
- 两侧都显示同一保存状态
|
||||
|
||||
### 11.2 Local Folder
|
||||
|
||||
同一 session 的多个 editor view 共享一份:
|
||||
|
||||
- conflictDetectionKey
|
||||
- 外部变更状态
|
||||
|
||||
如果命中文件外部更新:
|
||||
|
||||
- session clean:自动拉新 aggregate,并广播到两个 view
|
||||
- session dirty:session 标记 `external-change-conflict`,两个 view 统一显示冲突状态
|
||||
|
||||
注意这里的冲突判断应该只做一次,不能让左右各自判断、各自报警。
|
||||
|
||||
## 12. 对现有代码的落点建议
|
||||
|
||||
### 12.1 `layout.rs`
|
||||
|
||||
负责:
|
||||
|
||||
- 右键菜单事件继续发出
|
||||
- 新增 pane layout 宿主 HTML
|
||||
- 新增分割线、关闭按钮和右侧容器
|
||||
- 新增 pane 级 URL 恢复逻辑
|
||||
|
||||
不负责:
|
||||
|
||||
- 文档 session 真相
|
||||
|
||||
### 12.2 `web_shell.rs`
|
||||
|
||||
负责:
|
||||
|
||||
- 文档 bootstrap 适配为可挂载到多个 pane
|
||||
- 抽出 session / view 绑定逻辑
|
||||
- 保存、replaceContent、外部变更处理改为 session 级而不是单 view 级
|
||||
|
||||
### 12.3 `local_folder_events.rs`
|
||||
|
||||
负责:
|
||||
|
||||
- 继续输出文件变化事件
|
||||
- 后续收口为可复用 watcher registry
|
||||
|
||||
### 12.4 `styles.rs`
|
||||
|
||||
负责:
|
||||
|
||||
- 双栏布局
|
||||
- 右侧 pane 样式
|
||||
- 分割线和关闭按钮
|
||||
- 窄屏下的降级显示
|
||||
|
||||
## 13. 分阶段执行
|
||||
|
||||
### Phase 1:固定双栏宿主
|
||||
|
||||
- 增加 `primary pane + secondary pane`
|
||||
- 右侧可打开、替换、关闭
|
||||
- 左侧切页不影响右侧
|
||||
- URL 可恢复 secondary 状态
|
||||
|
||||
### Phase 2:文档 session 共享
|
||||
|
||||
- 同一文档双开挂到同一 session
|
||||
- 左右输入即时同步
|
||||
- 保存队列收口为 session 单例
|
||||
|
||||
### Phase 3:watcher / EventSource 复用
|
||||
|
||||
- tree SSE 明确归属 workspace shell
|
||||
- local-folder EventSource 按 rootUri 复用
|
||||
- Rust watcher 按 rootUri 复用
|
||||
|
||||
### Phase 4:回归与体验补齐
|
||||
|
||||
- 关闭、替换、reload 恢复
|
||||
- 同文档双开冲突链路
|
||||
- local_folder / convex_workspace 双链路验证
|
||||
|
||||
## 14. 测试计划
|
||||
|
||||
### 14.1 Rust / 服务侧
|
||||
|
||||
至少覆盖:
|
||||
|
||||
- secondary URL 参数解析
|
||||
- local-folder watcher registry 复用
|
||||
- 相同 rootUri 多订阅者不会重复建 watcher
|
||||
|
||||
### 14.2 浏览器 smoke
|
||||
|
||||
至少覆盖:
|
||||
|
||||
- 右键“在右侧边栏打开”后出现右侧 pane
|
||||
- 左侧切页后右侧保持不变
|
||||
- 同一文档左右双开时,左改右同步、右改左同步
|
||||
- 右侧关闭后 URL secondary 参数清理
|
||||
- 本地 Markdown 双开时,不重复建立 rootUri 级事件流
|
||||
|
||||
### 14.3 回归边界
|
||||
|
||||
- 不破坏当前单页文档路径
|
||||
- 不破坏 Page Aggregate 主链
|
||||
- 不让 watcher 数和 pane 数线性相关
|
||||
|
||||
## 15. 详细 checklist
|
||||
|
||||
### 15.1 范围冻结与基线取证
|
||||
|
||||
- [x] 固定第一阶段只做 `primary pane + secondary pane`,不引入 tabs / 多 group。
|
||||
- [x] 固定右侧 pane 默认可编辑,不做只读 preview 过渡态。
|
||||
- [x] 固定左侧普通导航只驱动 `primary pane`。
|
||||
- [x] 固定左侧切页后 `secondary pane` 保持不变。
|
||||
- [x] 固定同一文档允许左右双开。
|
||||
- [x] 固定同一文档双开时只同步内容,不同步 selection / scroll。
|
||||
- [x] 记录当前本地“在右侧边栏打开”仅发事件、无容器承接的基线证据。
|
||||
- [x] 补一条 design 说明:第一阶段不扩到跨窗口同步,只覆盖单个浏览器窗口内双 pane。
|
||||
|
||||
> 当前阶段说明:
|
||||
> - 第一阶段正式范围固定为“单个浏览器窗口内的 `primary pane + secondary pane` 双栏文档宿主”
|
||||
> - 不扩到 tabs / groups / 跨窗口共享 session;跨窗口同步仍留给后续单独方案
|
||||
> - 当前实现里 secondary pane 默认就是可编辑 editor,不存在只读 preview 过渡态
|
||||
> - `task165-rust-web-dual-pane-smoke.js` 已覆盖“同一文档左右双开”和“双 editor 都可编辑”两条事实
|
||||
> - `task165-rust-web-dual-pane-smoke.js` 还覆盖了:打开 `primary=README.md / secondary=side.md` 后,左侧普通导航切到 `third.md` 只会更新 primary,secondary 仍保持 `side.md`
|
||||
> - `task165-rust-web-dual-pane-smoke.js` 还覆盖了:
|
||||
> - fixture 同文档双开时,primary 输入后焦点仍留在 primary,secondary 输入后焦点仍留在 secondary
|
||||
> - local folder 外部更新命中 `replaceContent` 后,共享页面滚动位置保持不变
|
||||
> - 浏览器天然只有 1 份全局 DOM selection;当前验收以“对侧同步不会抢走当前 pane 焦点/选择上下文”作为 selection 不串 pane 的标准
|
||||
> - 基线问题已记录:用户在最初验收阶段明确指出“在右侧边栏打开这个功能并没有实现”,对应证据为 `/mnt/Data1T/mnote/tmp/image copy 57.png`;当时表现为右侧打开链路没有真实宿主承接
|
||||
|
||||
### 15.2 URL 与路由参数
|
||||
|
||||
- [x] 为右侧 pane 增加 `secondaryDocumentId` 查询参数。
|
||||
- [x] 为本地 Markdown 右侧 pane 增加 `secondarySourceKind` 查询参数。
|
||||
- [x] 为本地 Markdown 右侧 pane 增加 `secondaryRootUri` 查询参数。
|
||||
- [x] 约定缺少 secondary 参数时,页面回退为单 pane。
|
||||
- [x] 约定 secondary 文档无效时,自动清理 secondary 参数并保留主 pane。
|
||||
- [x] 固定 URL 更新策略:打开右侧、替换右侧、关闭右侧都同步回写 URL。
|
||||
- [x] 补测试覆盖:reload 后按 URL 正确恢复双 pane。
|
||||
|
||||
### 15.3 Pane 宿主与布局容器
|
||||
|
||||
- [x] 在 `workspace shell` 内容区引入双 pane 宿主节点。
|
||||
- [x] 保留现有左侧树和顶部壳只渲染一份,不复制第二套外层壳。
|
||||
- [x] 增加右侧 pane 关闭按钮。
|
||||
- [x] 增加主次 pane 之间的分割线容器。
|
||||
- [x] 增加分割线拖拽后的宽度状态存储。
|
||||
- [x] 固定右侧 pane 最小宽度与默认宽度。
|
||||
- [x] 窄屏下定义降级行为,避免直接把正文压到不可用宽度。
|
||||
- [x] 补 smoke:打开右侧 pane 后 DOM 中出现稳定的 secondary 宿主标记。
|
||||
|
||||
### 15.4 Pane Manager
|
||||
|
||||
- [x] 新增 `PaneManager`,管理 `primary / secondary` 的打开状态。
|
||||
- [x] `PaneManager` 管理当前 active pane,但不管理文档内容真相。
|
||||
- [x] `PaneManager` 能处理“打开右侧”“替换右侧”“关闭右侧”三种动作。
|
||||
- [x] `PaneManager` 能处理从 URL 恢复双 pane。
|
||||
- [x] `PaneManager` 能处理 secondary 文档失效时的降级回退。
|
||||
- [x] 固定“普通树点击默认作用于 primary pane”的路由策略。
|
||||
- [x] 为后续 tabs / groups 预留最小扩展接口,但这一步不实现 tabs。
|
||||
|
||||
> 当前验证证据:
|
||||
> - runtime 已收口 `paneRouteConfig / secondaryQueryParamNames / paneQueryParams` 这组最小角色配置
|
||||
> - 当前仍只落到 `primary / secondary` 两个 role,没有引入 tabs / groups UI,但后续若扩展额外 pane role,查询参数映射不再散落在各处硬编码
|
||||
|
||||
### 15.5 Document Session Registry
|
||||
|
||||
- [x] 新增 `DocumentSessionRegistry`。
|
||||
- [x] 固定 session key 由 `sourceKind + workspaceId/rootUri + documentId` 组成。
|
||||
- [x] Convex workspace 文档和 local folder 文档都接入同一 registry 接口。
|
||||
- [x] 一个 session 可同时挂多个 editor view。
|
||||
- [x] 同一 session 只保留一份 aggregate / revision / conflictDetectionKey / dirty 状态。
|
||||
- [x] 同一 session 只保留一条保存队列。
|
||||
- [x] session 生命周期与 view 挂载数关联,最后一个 view 卸载后可延迟释放。
|
||||
- [x] 补测试:同 key 请求复用同一 session,不同 key 返回不同 session。
|
||||
|
||||
> 当前验证证据:
|
||||
> - runtime 已收口 `createEditorViewBinding / unmountEditorViewBinding / scheduleDocumentSessionRelease / releaseDocumentSession`
|
||||
> - `task165-rust-web-dual-pane-smoke.js` 通过 debug 快照断言:
|
||||
> - fixture 同文档双开时 `sessionCount=1`、`viewCount=2`
|
||||
> - local folder 不同文档双开时 `sessionCount=2`
|
||||
> - 手工移除一个 pane 后,原 session 保持存活且 `viewCount=1`
|
||||
> - 手工移除最后一个 pane 并等待释放窗口后,`sessionCount=0`、`localFolderChannelCount=0`
|
||||
> - 由于 registry 位于浏览器端 inline runtime,这里的覆盖采用“浏览器自动化 + debug 快照断言”,不再额外补一层只验证字符串存在的伪单测
|
||||
|
||||
### 15.6 Editor View Binding
|
||||
|
||||
- [x] 把当前单编辑器挂载逻辑抽成可复用 `EditorViewBinding`。
|
||||
- [x] 一个 pane 对应一个 view binding,而不是一个完整独立 shell。
|
||||
- [x] view binding 初始化时从 session 读取当前内容。
|
||||
- [x] view binding 的本地输入统一回传 session,而不是直接各自保存。
|
||||
- [x] view binding 接收 session 广播时能更新本 view 内容。
|
||||
- [x] 来自同 session 的远端更新不能把当前 view 的 selection 直接重置为对侧状态。
|
||||
- [x] 补 smoke:同文档双开时左右都出现真实可编辑 editor。
|
||||
|
||||
> 当前验证证据:
|
||||
> - runtime 已显式引入 `createEditorViewBinding()` 与 `unmountEditorViewBinding()`,每个 pane 挂一个 binding,binding 自己持有 mountId、事件监听和 observer 清理逻辑
|
||||
> - `mountPane()` 现在只负责取 runtime / session、创建 binding、执行 mount,不再把所有逻辑堆在一个匿名挂载过程里
|
||||
> - `task165-rust-web-dual-pane-smoke.js` 已自动断言:primary 输入后焦点仍留在 primary,secondary 输入后焦点仍留在 secondary
|
||||
> - `task165-rust-web-dual-pane-smoke.js` 已自动断言:local folder 外部更新触发 `replaceContent` 后,共享页面滚动位置不被重置
|
||||
|
||||
### 15.7 同文档双开即时同步
|
||||
|
||||
- [x] 在 session 内建立“单写入源 + 多 view 广播”机制。
|
||||
- [x] 左侧编辑时,右侧无需 reload 即更新。
|
||||
- [x] 右侧编辑时,左侧无需 reload 即更新。
|
||||
- [x] 同步链路不依赖保存成功后重新拉 aggregate。
|
||||
- [x] 同步链路要避免 view A 更新触发 view B,再回写成无限循环。
|
||||
- [x] 固定本地广播与服务端回包的优先级规则,避免旧回包覆盖较新内存态。
|
||||
- [x] 补 smoke:左改右同步、右改左同步都能自动断言。
|
||||
|
||||
### 15.8 保存队列与共享状态
|
||||
|
||||
- [x] 保存状态提升到 session 级,而不是 view 级。
|
||||
- [x] 同一 session 下多个 view 共享 `dirty / saving / saved / error` 状态。
|
||||
- [x] 同一 session 只允许一个防抖保存队列处于活动中。
|
||||
- [x] 服务端保存成功后统一更新所有 view 的 revision。
|
||||
- [x] local folder 保存成功后统一更新所有 view 的 conflictDetectionKey。
|
||||
- [x] 任一 view 触发保存失败时,错误状态广播到同 session 其他 view。
|
||||
- [x] 补测试:同 session 双 view 输入后只触发一次保存请求。
|
||||
|
||||
### 15.9 Tree Realtime 复用
|
||||
|
||||
- [x] 固定 tree SSE 属于 `workspace shell`,不属于 pane。
|
||||
- [x] 打开 secondary pane 后,不创建第二条 tree realtime EventSource。
|
||||
- [x] 双 pane 状态下,页面标题、树 active 态、投影刷新仍由共享 tree 链路驱动。
|
||||
- [x] 若后续 pane 内导航触发页面切换,仍复用同一条 tree SSE。
|
||||
- [x] 补浏览器验证:双 pane 状态下 tree 事件流连接数保持为 1。
|
||||
|
||||
> 当前验证证据:
|
||||
> - tree live bootstrap 与 `TREE_LIVE_CONTROLLER_JS` 挂在 `PageLayout` 的 workspace shell 上,不在 `DocumentPane` 内重复注入
|
||||
> - `task165-rust-web-dual-pane-smoke.js` 在 fixture 双 pane 页面里断言:
|
||||
> - 首次进入只出现 1 条 `GET /api/tree/events`
|
||||
> - reload 后当前页面仍只出现 1 条 `GET /api/tree/events`
|
||||
> - `task165-rust-web-dual-pane-smoke.js` 在 local folder 双 pane 页面里断言:
|
||||
> - 左侧普通导航切到 `Local Third` 后,共享 topbar 标题同步切到 `Local Third`
|
||||
> - 共享侧栏 active / selected 状态同步落到 `Local Third`
|
||||
> - secondary pane 仍保持原来的 `Local Side` 文档,不复制第二套页面壳
|
||||
> - `task165-rust-web-dual-pane-smoke.js` 在 fixture 双 pane 页面里还断言:
|
||||
> - 通过共享 sidebar 导航切到 `Fixture Other` 后,当前页面只建立 1 条新的 `GET /api/tree/events`
|
||||
> - 导航后 `secondaryDocumentId` 仍保持原值,没有因为 primary 切页而再额外起第二条 tree SSE
|
||||
|
||||
### 15.10 Frontend Local Folder Event Registry
|
||||
|
||||
- [x] 新增前端 `LocalFolderEventRegistry`。
|
||||
- [x] 按 `rootUri` 复用 `/api/local-folder/events` EventSource。
|
||||
- [x] 同一 `rootUri` 下两个不同 Markdown 文档复用同一条 EventSource。
|
||||
- [x] 同一 Markdown 左右双开复用同一条 EventSource。
|
||||
- [x] 文件变化事件到达后,先路由到 session,再决定是否刷新 view。
|
||||
- [x] clean session 自动同步;dirty session 统一进入冲突状态。
|
||||
- [x] 补 smoke:双 pane 本地 Markdown 场景下,不重复建立 rootUri 级事件流。
|
||||
|
||||
> 当前手工验证证据:
|
||||
> - `local_folder` 同文档左右双开时,浏览器侧只建立 1 条 `GET /api/local-folder/events?rootUri=...`
|
||||
> - 主 pane 输入后,secondary pane 无需 reload 即同步
|
||||
> - 单轮输入后只出现 1 次 `POST /api/documents/save`
|
||||
> - local folder watcher 回流已压缩到 1 次 `GET /api/page-aggregate/...`,不再出现重复回读
|
||||
> - `task165-rust-web-dual-pane-smoke.js` 还自动断言了:
|
||||
> - 同 rootUri 同文档双开只建立 1 条 `GET /api/local-folder/events`
|
||||
> - 同 rootUri 不同文档双开也只建立 1 条 `GET /api/local-folder/events`
|
||||
> - debug 快照下 `localFolderChannelCount=1`
|
||||
|
||||
### 15.11 Backend Local Folder Watcher Registry
|
||||
|
||||
- [x] Rust 侧引入或收口 `LocalFolderWatcherRegistry`。
|
||||
- [x] 按 `rootUri` 维护单例 `notify` watcher。
|
||||
- [x] 多个 SSE 订阅者共享同一个 watcher 输出。
|
||||
- [x] watcher 只上报有效 Create / Remove / 真正 Modify 事件,不把 access 噪音当作变更。
|
||||
- [x] SSE 订阅者断开后,registry 能在安全时机回收无引用 watcher。
|
||||
- [x] 补 Rust 单测:相同 `rootUri` 多订阅不会重复创建 watcher。
|
||||
- [x] 补 Rust 单测:不同 `rootUri` 会创建独立 watcher。
|
||||
|
||||
> 当前验证证据:
|
||||
> - `cargo test -p mnote-web local_folder_watcher_registry -- --nocapture` 通过
|
||||
> - `same_root_subscribers_share_single_watcher` 覆盖“同 root 不重复建 watcher”
|
||||
> - `different_roots_create_independent_watchers` 覆盖“不同 root 分别建 watcher”
|
||||
> - 订阅 drop 后 `active_watcher_count()` 回落到 `0`,覆盖 watcher 回收路径
|
||||
|
||||
### 15.12 外部变更与冲突处理
|
||||
|
||||
- [x] Convex 文档沿用共享 revision 判断,不为每个 view 单独做冲突模型。
|
||||
- [x] local folder 文档沿用共享 conflictDetectionKey 判断,不为每个 view 单独判断。
|
||||
- [x] 同一 session clean 时命中外部文件变化,两个 view 同时自动同步。
|
||||
- [x] 同一 session dirty 时命中外部文件变化,两个 view 同时显示冲突状态。
|
||||
- [x] 冲突提示提升到 session 级,避免左右状态不一致。
|
||||
- [x] 补 smoke:dirty 状态下修改磁盘文件,左右两侧都进入统一冲突状态。
|
||||
|
||||
> 当前验证证据:
|
||||
> - clean 状态下外部改写本地 Markdown 后,左右 pane 都会同步更新正文,并共同进入 `synced-external-change`
|
||||
> - session 级 `conflictDetectionKey` 已前移为共享字段;Rust 侧 local Markdown `conflictDetectionKey` 已加固为 `mtime + len + content hash`
|
||||
> - `cargo test -p mnote-web local_folder_source -- --nocapture` 通过,包含 `local_markdown_conflict_detection_key_changes_when_content_changes_with_same_size`
|
||||
> - dirty 状态下通过独立外部保存请求写盘同一 local Markdown 文件时,左右 pane 会共同进入 `external-change-conflict`
|
||||
> - 冲突提示文案在左右 pane 一致:`本地 Markdown 文件已在外部更新,请刷新或保存前先处理冲突`
|
||||
|
||||
### 15.13 标题、Page Aggregate 与页面壳一致性
|
||||
|
||||
- [x] 主标题控制器在双 pane 下不要错误写到另一个 pane。
|
||||
- [x] 需要明确每个 pane 的标题显示与编辑归属。
|
||||
- [x] 若同一文档双开,标题修改后两个 pane 标题都同步。
|
||||
- [x] `PageAggregate` 拉取逻辑要支持 secondary 文档,不复制整页壳。
|
||||
- [x] 不引入第二份页面真相,不在 pane 层重新拼文档对象。
|
||||
|
||||
> 当前验证证据:
|
||||
> - 同一 local Markdown 左右双开时,在 primary pane 改标题并 blur 后:
|
||||
> - secondary pane 标题输入同步为同一标题
|
||||
> - 顶部 breadcrumb 标题同步
|
||||
> - `document.title` 同步
|
||||
> - 磁盘文件写入 `title:` frontmatter
|
||||
> - primary/secondary 打开不同 local Markdown 时,在 primary pane 改标题后:
|
||||
> - secondary pane 标题保持原文档标题,不被串改
|
||||
> - 只有 primary 对应文件的 `title:` frontmatter 被改写
|
||||
> - primary=`README.md`、secondary=`side-one.md` 时,secondary pane 渲染 `Side One / 右一正文`
|
||||
> - 通过 `tree.page.open-right` 切换 secondary 到 `side-two.md` 后,secondary pane 渲染 `Side Two / 右二正文`
|
||||
> - 切换前后页面都只保留 1 份侧栏/顶部壳,secondary editor root 数量保持为 1
|
||||
> - `build_document_panes_bootstrap_json()` 直接把 primary / secondary 的 `PageAggregate + bootstrap` 序列化进同一份 `panes` contract,pane runtime 只消费已有 aggregate,不再在 pane 层拼第二份页面对象
|
||||
> - `DocumentPage / DocumentPane` 只消费 route 已经解出的 `title / document_id / pageOptions / pageSubtree` 等字段;secondary 只是同一 workspace shell 内的第二个 pane,不复制第二套 workspace 壳
|
||||
|
||||
### 15.14 交互与体验收尾
|
||||
|
||||
- [x] 右侧 pane 支持键盘关闭或显式关闭按钮关闭。
|
||||
- [x] 右侧 pane 替换文档时,旧 session view 正确卸载。
|
||||
- [x] 关闭右侧 pane 后回到单 pane 布局,不残留 secondary 宿主空壳。
|
||||
- [x] reload 后能恢复上次 secondary 打开的文档。
|
||||
- [x] 本地 Markdown 与 Convex 页面两条链都通过一次手工 smoke。
|
||||
|
||||
> 当前验证证据:
|
||||
> - 带 `secondaryDocumentId / secondarySourceKind / secondaryRootUri` 参数的 local Markdown 双 pane URL 在 reload 后仍恢复左右两个 pane
|
||||
> - 点击右侧 pane 关闭按钮后,URL 中 secondary 参数被清理,页面回落为单 pane,快照中不再出现 secondary 文档区
|
||||
> - 本地 Markdown 链:同文档左右双开时,左右编辑器都可输入,左改右同步、右改左同步、单轮输入只出现 1 次保存请求、同 rootUri 只建立 1 条 local-folder EventSource
|
||||
> - Convex / fixture 链:补齐 `MNOTE_WEB_QUERY_FIXTURES_JSON` + `MNOTE_WEB_MUTATION_FIXTURES_JSON` 后,双 pane 页面可正常输入保存,`POST /api/documents/save` 返回 `200`,状态进入 `saved`
|
||||
|
||||
### 15.15 验证命令与回归清单
|
||||
|
||||
- [x] 右侧 pane 宿主已完成一轮手工浏览器 smoke。
|
||||
- [x] 右侧 pane 宿主补自动化浏览器 smoke。
|
||||
- [x] 同文档双开同步已完成一轮手工浏览器 smoke。
|
||||
- [x] 同文档双开同步补自动断言 smoke。
|
||||
- [x] local folder 事件流复用已完成一轮手工浏览器 smoke。
|
||||
- [x] local folder 事件流复用补自动断言 smoke。
|
||||
- [x] watcher registry 复用已补 Rust 单测。
|
||||
- [x] URL 恢复与 secondary 参数清理已完成一轮手工 smoke。
|
||||
- [x] URL 恢复与 secondary 参数清理补自动化测试。
|
||||
- [x] 在 `3000` 常驻服务和临时端口两种模式下都验证一次,避免打到旧进程误判。
|
||||
|
||||
> 当前验证证据:
|
||||
> - 手工浏览器 smoke 已覆盖:
|
||||
> - secondary pane 真实渲染、可关闭、可 reload 恢复
|
||||
> - 同文档双开时左改右同步、右改左同步
|
||||
> - local folder 双 pane 下 rootUri 级事件流只建立 1 条,且单轮输入只出现 1 次保存请求
|
||||
> - `node scripts/task165-rust-web-dual-pane-smoke.js` 通过,自动覆盖:
|
||||
> - fixture 双 pane 宿主渲染
|
||||
> - 左改右同步、右改左同步
|
||||
> - 同 session 双 view 单轮输入只触发 1 次 `POST /api/documents/save`
|
||||
> - fixture 双 pane 只建立 1 条 `GET /api/tree/events`
|
||||
> - local folder 双 pane 只建立 1 条 `GET /api/local-folder/events`
|
||||
> - local folder 同 root 不同文档双开时仍只建立 1 条 `GET /api/local-folder/events`
|
||||
> - dual pane URL reload 恢复
|
||||
> - 关闭 secondary 后 URL 参数清理,reload 后保持单 pane
|
||||
> - 最后一个 view 卸载后延迟释放 session 与 local-folder channel
|
||||
> - `cargo test -p mnote-web local_folder_watcher_registry -- --nocapture` 通过,已覆盖:
|
||||
> - `same_root_subscribers_share_single_watcher`
|
||||
> - `different_roots_create_independent_watchers`
|
||||
> - `3000` 常驻服务当前仍指向旧进程:
|
||||
> - 只读 `curl` 核查 `http://127.0.0.1:3000/documents/local-md:README.md?...secondary...` 时,响应虽为 `200`,但 HTML 中缺少 `data-has-secondary-pane`、`__MNOTE_DOCUMENT_PANES_BOOTSTRAP__`、`localFolderEventRegistry`、`__mnoteDebugDocumentSessions` 等当前实现标记
|
||||
> - 当前 pid=`491543` 的 `3000` 进程不能代表本轮最新代码,因此这一条仍需在用户允许处理 `3000` 常驻服务后补正式验收
|
||||
|
||||
## 16. 完成判定
|
||||
|
||||
满足以下条件才算完成这一阶段:
|
||||
|
||||
- “在右侧边栏打开”不再只是菜单文案,而是真实右侧可编辑 pane
|
||||
- 左侧切页时右侧保持不变
|
||||
- 同一文档允许左右同时打开
|
||||
- 左右编辑能即时同步,不依赖 reload
|
||||
- tree SSE 仍为单条
|
||||
- local-folder EventSource / notify watcher 不按 pane 数重复创建
|
||||
|
||||
## 17. 一句话收口
|
||||
|
||||
这一稿的关键不是“把页面再多开一个”,而是:
|
||||
|
||||
> **把 `mnote-web` 的文档页从单编辑器壳升级为共享 transport + 共享 session + 多 pane view 的正式宿主。**
|
||||
Reference in New Issue
Block a user