# 5-30 Sidebar 星标置顶快捷入口与 Scoped Explorer 设计 v1

> 状态：process
> Owner：05-editor-mainline / 03-rust-web / control-plane
> 背景：星标置顶需要对齐 Wolai 的 Sidebar 顶部快速访问体验，但 MNote 还需要支持把高频本地文件夹固定成 Explorer scoped root，避免每次进入大 workspace 时加载完整根目录。

## 目标

星标置顶不是文件或文件夹自身的属性，而是用户在 Sidebar 固定的一组快捷入口。

- 当前页面 `.md` 可以加入星标置顶，点击后直接打开该页面。
- FileTree 文件夹右键可以加入星标置顶，点击后切到 Explorer，并把该文件夹作为临时 scoped root 显示。
- 星标置顶按登录用户持久化到 SQLite control-plane；同一用户在不同设备登录时应看到同一组快捷入口。
- 顶栏“全网公开”不能默认误导显示；没有真实公开分享状态时隐藏。
- scoped Explorer 必须避免扫描完整大 workspace，例如 `/mnt/Data1T/mnote` 下只打开 `design/` 时只加载 `design/` 子树。

## 非目标

- 不把星标写入 Markdown frontmatter。
- 不写入 `.mnote/starred-pins.json` 作为长期真相。
- 不把文件夹变成 Resource Tree 的业务属性。
- 不在前端 `localStorage` 中持久化最终星标真相；前端最多做加载态/乐观态缓存。
- 不在本阶段实现跨设备文件内容同步；本设计只定义用户快捷入口在 control-plane 的一致性。

## 用户模型

星标置顶等价于“我的 Sidebar 快捷入口”：

- 页面快捷入口：`kind=page`，目标是某个 workspace 下的页面 identity。
- 文件夹快捷入口：`kind=folder`，目标是某个 workspace 下的相对路径 scope。
- 快捷入口属于用户，不属于文件夹，不影响其他用户。
- 如果某台设备没有对应 workspace 或没有该 folder 相对路径，星标项保留但显示失效态，允许移除或重新绑定。

## SQLite 数据模型

新增 control-plane 表：`sidebar_shortcuts`。

建议字段：

```sql
CREATE TABLE sidebar_shortcuts (
  id TEXT PRIMARY KEY,
  user_id TEXT NOT NULL REFERENCES users(id) ON DELETE CASCADE,
  workspace_id TEXT NOT NULL REFERENCES workspaces(id) ON DELETE CASCADE,
  root_uri TEXT,
  kind TEXT NOT NULL, -- page | folder
  source_kind TEXT NOT NULL DEFAULT 'local_folder',
  target_id TEXT NOT NULL,
  relative_path TEXT,
  document_id TEXT,
  title TEXT NOT NULL,
  icon TEXT,
  sort_order INTEGER NOT NULL DEFAULT 0,
  status TEXT NOT NULL DEFAULT 'active',
  metadata_json TEXT NOT NULL DEFAULT '{}',
  created_at TEXT NOT NULL,
  updated_at TEXT NOT NULL,
  revision INTEGER NOT NULL DEFAULT 1,
  UNIQUE(user_id, workspace_id, kind, target_id)
);
```

`target_id` 规范：

- 页面：优先 `document_id`，例如 `local-md:design%2FREADME.md`。
- 文件夹：`local-dir:<encoded relative path>` 或稳定的 `local:folder:<relative path>` 归一化值。

`relative_path` 用于 scoped Explorer 与跨设备路径映射，必须保存 workspace 内相对路径，不保存绝对本机路径作为主键。

`root_uri` 必须作为显式字段保存和返回，而不是只塞在 `metadata_json` 中。原因：

- 星标文件夹点击必须直接消费保存时的 workspace root 上下文，不能从 `workspace_id` 字符串反推本机路径。
- local workspace id 是派生标识，不是路径编码协议；用它反推 `file:///...` 会把 UI fallback 变成事实源。
- `metadata_json.rootUri` 只作为兼容冗余字段，不能作为唯一来源。

长期跨设备映射仍以 `workspace_id + relative_path` 为主；当前设备打开本地 workspace 时必须把实际 `root_uri` 一起写入 shortcut，缺失时应显示失效/需重绑定状态，而不是自动猜测路径。

## API / 命令面

新增最小 API：

- `GET /api/sidebar/shortcuts?workspaceId=...`
  - 返回当前用户当前 workspace 的星标快捷入口。
- `POST /api/sidebar/shortcuts`
  - upsert 快捷入口。
  - body 包含 `workspaceId/rootUri/kind/sourceKind/targetId/relativePath/documentId/title/icon`。
- `DELETE /api/sidebar/shortcuts/:id`
  - 移除当前用户自己的快捷入口。

实现边界：

- API 只操作 control-plane 用户快捷入口表。
- API 不写本地文件，不改 Markdown，不改 `.mnote` 元数据。
- 写操作要求真实登录用户；dev fallback 不应静默创建跨用户快捷入口。
- 每次写入追加 audit：`sidebar.shortcut.created` / `sidebar.shortcut.removed`。
- 后续需要实时同步时，通过 `outbox_events` 或现有 WS delta 广播 `sidebar.shortcut.*`。

## Projection 与 Sidebar 渲染

`WorkspaceShellProjection.starred_items` 需要从 `documents[].is_starred` 迁移为 control-plane `sidebar_shortcuts` 投影。

投影项扩展：

- `id`
- `title`
- `kind: page | folder`
- `href`
- `workspaceId`
- `sourceKind`
- `targetId`
- `relativePath`
- `rootUri`
- `documentId`
- `active`
- `unavailable`

页面快捷入口：

- `href` 指向当前页面打开 URL。
- 点击后走现有 document open 主链。

文件夹快捷入口：

- `href` 可为当前页面 URL 加 query，例如 `?treeView=filetree&filetreeScope=design`。
- 更推荐 runtime 拦截点击，调用 `openScopedFileTree(relativePath)`，避免不必要的页面跳转。
- 点击后切换到 Explorer tab，并显示 scoped Explorer。

## Scoped Explorer 行为

文件夹星标点击后：

1. Sidebar 切到 Explorer tab。
2. Explorer 标题显示 scoped 状态，例如 `Explorer / design`。
3. FileTree root 只渲染该文件夹下的 children。
4. 子目录继续按需加载。
5. 提供“返回工作区根”入口；点击后恢复 root Explorer。

数据读取：

- scoped root 为 `relativePath=design` 时，调用现有 `load_local_folder_file_tree_children_snapshot(rootUri, "design")` 或对应 API。
- 不调用完整 root snapshot。
- 不预加载 scoped root 之外的兄弟目录。

URL / 状态：

- scoped 状态可以写入 URL query：`treeView=filetree&filetreeScope=design`。
- 刷新页面后恢复 scoped Explorer。
- 切回“我的页面”不清除 scope；点击“返回工作区根”才清除 scope。

## 顶栏星标与公开状态

顶栏星标按钮：

- 未星标：空心星；tooltip 对齐 Wolai：“点击 加入星标置顶，可在左侧边栏顶部快速访问”。
- 已星标：高亮星；tooltip “取消星标置顶”。
- 点击当前页面星标只操作 `sidebar_shortcuts`。

顶栏公开状态：

- `wolai-public-pill` 只有当当前页面存在 active share link / public permission 时显示。
- 没有公开状态时隐藏，不显示“全网公开”。
- 公开分享弹窗仍属于 share link / permission 领域，不与星标快捷入口混用。

## FileTree 右键菜单

文件夹行增加：

- 未星标：`加入星标置顶`
- 已星标：`取消星标置顶`

启用条件：

- `sourceKind == local_folder`
- `rowKind == folder | directory | index`
- 当前用户对 workspace 有至少 read access；写入快捷入口只需要用户偏好写权限，不要求文件系统写权限。

不支持项：

- 普通附件文件暂不加入星标置顶。
- 多选批量星标暂不做。

## 失效与重命名处理

MVP 处理：

- 文件夹被删除：快捷入口保留，显示失效态；点击提示“文件夹不存在”，允许移除。
- 文件夹重命名：如果重命名由 MNote 发起，可同步更新对应 `relative_path/title/target_id`；如果外部重命名，先显示失效态。
- 页面被删除：页面快捷入口显示失效态，允许移除。

长期可选：

- 引入 local object stable id 后，星标目标可由 stable object id 跟随重命名。

## 验收标准

- 无公开分享状态时，顶栏不显示“全网公开”。
- 当前页面点击星标后，Sidebar 星标置顶出现该页面；再次点击移除。
- 右键 `design/` 文件夹可加入星标置顶。
- 点击 `design/` 星标后，切到 Explorer scoped root，只加载并显示 `design/` 下内容，不显示 workspace root 的其他大目录。
- 刷新页面后，`filetreeScope=design` 能恢复 scoped Explorer。
- 同一用户重新登录后，星标快捷入口仍存在。
- 不同用户登录同一 workspace 时，星标列表互不污染。
- 文件夹不存在时，星标项显示失效态并可移除。

## 测试计划

Rust/control-plane：

- migration 创建 `sidebar_shortcuts`。
- store 可 upsert/list/delete 当前用户快捷入口。
- unique 约束防止同一用户同一 workspace 重复固定同一目标。
- 不同用户同一目标互不影响。

mnote-web：

- API 鉴权：只能读写当前用户自己的快捷入口。
- `WorkspaceShellProjection` 从 shortcuts 投影 starred rows。
- local folder scoped snapshot 不扫描完整 root。

浏览器 smoke：

- 顶栏公开 pill 隐藏断言。
- 顶栏星标页面 add/remove。
- FileTree 文件夹右键 add/remove。
- 点击星标文件夹进入 scoped Explorer，并断言 root siblings 不渲染。
- 刷新恢复 scoped Explorer。

## 实施顺序

1. control-plane migration / model / store：新增 `sidebar_shortcuts`。
2. mnote-web API：list/upsert/delete shortcuts。
3. workspace shell projection：星标区改读 shortcuts，并保留历史 `is_starred` fixture 兼容测试。
4. 顶栏：星标按钮接 API；公开 pill 改为条件显示。
5. FileTree 右键菜单：文件夹加入/取消星标置顶。
6. Scoped Explorer runtime：支持 `filetreeScope`，只加载 scoped children，并提供返回 root。
7. smoke 与 Rust 测试补齐。

## 决策记录

- 2026-05-26：星标置顶确定为用户级 Sidebar 快捷入口，不是文件夹/页面自身属性。
- 2026-05-26：星标快捷入口必须写入 SQLite control-plane，满足同一用户跨设备一致显示。
- 2026-05-26：文件夹星标点击进入 scoped Explorer，避免大 workspace root 扫描。
