# 7-18 [process] local-first agent 文件编辑控制面 v1

> 创建时间：2026-05-22
>
> 当前状态：`PROCESS`
>
> 上位依据：
> - `/mnt/Data1T/mnote/ARCHITECTURE.md`
> - `/mnt/Data1T/mnote/CURRENT_ARCHITECTURE.md`
> - `/mnt/Data1T/mnote/design/01-tree-first-graph-kernel/process/1-8-mvp-post-process-execution-order-v1.md`
> - `/mnt/Data1T/mnote/design/05-editor-mainline/done/5-6-page-aggregate-alignment-checklist-v1.md`
> - `/mnt/Data1T/mnote/design/07-ai/done/7-15-page-ai-acp-agent-runtime-unified-layer-v1.md`
> - `/mnt/Data1T/mnote/design/07-ai/reference/7-12-page-ai-hermes-tool-routing-and-review-surface-v1.md`
>
> 覆盖旧稿：`/mnt/Data1T/mnote/design/old/07-ai/process/7-14-local-first-ai-markdown-editing-convergence-v1.md`
>
> 口径说明：旧 `7-14` 仍受 CLI Main / Lark Doc / tool-first 写入模型影响。CLI Main 可以参考 agent workflow 纪律，但不作为 MNote local-first Markdown 编辑架构主参考。

## 1. 第一结论

MNote 不提供普通 Markdown 正文编辑工具。

MNote 在 local-first 下只提供：

- 页面和文件定位。
- agent target 选择与冻结。
- `AiAccessScope`、allowed roots、allowed files、readonly / dirty / permission context。
- selection、当前光标附近上下文、相关资源引用。
- agent runtime session、审计、changed files / diff 回收。
- watcher、BufferStore、Page Aggregate、ProseMirror 前台同步。

Hermes / Reasonix 是已知 agent，使用自身文件 read / write / patch / diff 能力在授权目录内完成普通 `.md` 编辑。

## 2. 明确退役项

### 2.1 `mnote.doc.markdown_edit`

`mnote.doc.markdown_edit` 不再作为：

- local-first 普通 Markdown 编辑默认入口。
- local-first 普通 Markdown 编辑 fallback。
- remote fallback。
- 新 agent runtime 的推荐工具。
- 新 checklist 的验收目标。

已有代码和 smoke 若仍覆盖 `markdown_edit`，只代表历史在线 / compat 路径的回归证据；不得再由它反推出“local-first 应继续提供 Markdown 编辑工具”。

### 2.2 `mnote.doc.fetch`

`mnote.doc.fetch` 不作为普通正文读取主入口。

local-first 默认路径中，agent 直接读取授权文件。MNote 可以把 selection、文件摘要、block projection 或结构化上下文放进 run input，但不要求 agent 通过 `mnote.doc.fetch` 再读正文。

### 2.3 `page_ai_workflow` / `mnote.block.*` / `mnote.page.save`

- `page_ai_workflow` 只保留为 debug / 历史兼容门面，不进入 local-first 普通编辑主路径。
- `mnote.block.*` 只保留为复杂结构辅助，例如拖拽排序、精确块删除、非 Markdown 结构资源辅助。
- `mnote.page.save` 只保留为页面级兜底写入工具，不作为精确 Markdown 编辑入口。

## 3. 主路径数据流

```text
Page / active editor
  -> Agent Target Resolver freezes selected target package
  -> resolve canonical .md file / resource file
  -> build AiAccessScope
  -> freeze allowed roots / allowed files / readonly / dirty / selection
  -> start Hermes / Reasonix run with file refs and context
  -> agent native patch / diff / write inside allowed roots
  -> MNote collects changed files / diff / audit
  -> watcher observes file changes
  -> BufferStore arbitrates clean / dirty / conflict state
  -> Page Aggregate rebuilds projection
  -> ProseMirror / tiptap island refreshes visible document
```

关键点：

- MNote 只给位置、权限和上下文，不给普通 Markdown 编辑工具。
- 多工作区、多 tab、多资源同时打开时，默认 target 只能来自最后获得编辑焦点的 editor / resource tab；不允许用“第一个 tab”“树选中项”或 URL documentId 静默猜测。
- 写入动作发生在 agent 自身文件能力内，并受 allowed roots / allowed files 限制。
- 前台同步由 watcher / BufferStore / Page Aggregate 接回，不由 agent 直接推 UI 状态。
- dirty buffer 时不得静默覆盖，必须进入冲突或 review 状态。

## 4. Agent Target Resolver

Agent Target Resolver 是 MNote 发送给 agent 前的唯一目标解析层。它的职责是把当前 UI 状态冻结为一份明确的 `targetPackage`，而不是让 agent 或 API 自行猜“当前页”。

### 4.1 选择原则

- **workspace 是权限边界**：默认一次 run 只属于一个 workspace；跨 workspace 必须由用户显式多选，并生成多个 `allowedRoots`。
- **tab 是视图，不是真相**：tab 只帮助判断用户正在看的对象；最终 target 必须落到 `objectIdentity`、`resourceKind` 和 canonical path。
- **last focused editor 优先**：默认 target 是最后获得编辑焦点的 editor / resource tab。
- **selection 覆盖 whole file**：存在真实选区时，默认 scope 为 selection；没有选区时才是 whole file / current resource。
- **树选中项只作为候选**：FileTree / PageTree selection 不自动成为 agent target，除非用户在 target picker 中选择“树选中项”。
- **歧义时不发送**：如果焦点不明确、target dirty 且会被写入、或跨 workspace 未显式确认，发送按钮进入需要选择状态。

### 4.2 targetPackage 合同

```json
{
  "schema": "mnote.agent_target_package.v1",
  "primaryTargetId": "target-1",
  "targets": [
    {
      "targetId": "target-1",
      "workspaceId": "local:design",
      "workspaceSource": "local_folder",
      "workspaceRoot": "/workspace/design",
      "editorGroupId": "main",
      "tabId": "tab-design-md",
      "focusRank": 1,
      "objectIdentity": "local_folder:/workspace/design/design.md",
      "resourceKind": "markdown_page",
      "title": "design.md",
      "canonicalPath": "/workspace/design/design.md",
      "uri": "file:///workspace/design/design.md",
      "version": "mtime:size:hash",
      "scope": "selection",
      "selection": {
        "text": "当前选区文本",
        "anchor": "pm:block:p_1:0..12"
      },
      "dirty": false,
      "readonly": false,
      "ownerPage": null,
      "relatedResources": []
    }
  ],
  "allowedRoots": ["/workspace/design"],
  "allowedFiles": ["/workspace/design/design.md"],
  "policy": {
    "writeMode": "agent_native_file_patch",
    "requiresConfirmation": false,
    "crossWorkspace": false
  }
}
```

### 4.3 resourceKind

`resourceKind` 至少需要区分：

- `markdown_page`：普通 `.md` 页面正文。
- `mindmap`：mindmap 资源文件；可带 `ownerPage`，但不伪装成正文。
- `office`：OnlyOffice 资源；默认只读上下文，除非明确进入 office 编辑 agent flow。
- `raw_file`：普通文件。
- `folder`：目录范围；只在用户显式选择“把这个文件夹作为上下文”时出现。

## 5. UI 设计

当前右侧页面 AI 抽屉已有 runtime / profile / context scope，但“上下文：当前页/选区/块/页面设置”不够表达多工作区、多 tab 和资源 tab。需要把它升级为明确的 target UI。

### 5.1 默认展示

AI composer 上方固定显示 target chip，不放到设置页深处：

```text
[工作区: design] [design.md] [当前选区] [可写]
```

如果当前焦点是截图中的右侧 mindmap，则显示：

```text
[工作区: local] [新页面165037 / KMIND] [mindmap] [可写]
```

chip 必须可点击，打开 target picker。chip 内容优先显示用户能识别的名称，不优先显示内部 id；hover / detail 再显示 path、workspace root、object identity。

### 5.2 target picker

点击 target chip 后打开轻量 popover，不跳转页面。结构为：

```text
发送给 Agent

当前焦点
  ● design.md                  markdown_page   当前选区   /workspace/design/design.md

打开的 Tab
  ○ 新页面223322               markdown_page   全文
  ○ design.md                  markdown_page   当前选区
  ○ 新页面165037 / KMIND       mindmap         当前资源

树选中项
  ○ 3 个文件                   只读上下文

范围
  ( ) 当前选区   ( ) 当前文件   ( ) 当前资源   ( ) 多选 tab   ( ) 文件夹

[取消] [使用这个目标]
```

交互规则：

- 单选是默认；多选必须用户点击“多选 tab”或勾选多个候选。
- 多 workspace 候选默认折叠在各自 workspace 组下，跨 workspace 多选时出现权限提示。
- dirty target 显示 `未保存` 标记；如果 agent 可能写入，发送前需要确认。
- readonly target 显示 `只读`，发送按钮文案变成“只读提问”或禁用写入型 intent。
- resource target 不能显示成“当前页”；必须显示 `mindmap` / `office` / `raw file`。

### 5.3 composer 中的 context scope

原 `context scope` select 不应继续承担 target 选择职责。它只控制 primary target 内部范围：

- `selection`：当前选区。
- `file`：当前文件。
- `resource`：当前资源文件。
- `related`：当前页面引用的相关资源，只读附加上下文。

不再把 `page / block / options` 混成同一层 target 选择。页面设置属于 target 的辅助上下文，不是普通 Markdown 编辑范围。

### 5.4 发送前状态

发送按钮按 targetPackage 状态变化：

- `发送`：单 target、clean、可写或只读问答。
- `选择目标`：没有明确 last focused target，或当前焦点和 URL / active tab 冲突。
- `确认发送`：跨 workspace、多 target、dirty write、folder scope。
- `只读提问`：readonly target 或用户选择只读上下文。

发送后第一条用户消息旁边显示冻结的 target 摘要，避免 run 过程中用户切 tab 后误以为 agent 改了新 tab：

```text
目标：design.md / 当前选区 / local_folder / 12:31:05 冻结
```

## 6. SQLite control-plane 职责

Rust SQLite control-plane 是默认控制面，负责：

- auth / session / membership。
- share grants / sync state。
- AI policy、workspace policy、allowed roots / allowed files。
- ACP / Hermes / Reasonix runtime session。
- targetPackage 审计与用户确认记录。
- permission decision、approval decision、tool / file access audit。
- changed files / diff / command context 记录。

它不持有普通 Markdown 正文真相。本地 `.md` 文件仍是正文真相。

## 7. 运行时输入合同

每次从页面触发 agent 编辑时，run input 至少应包含：

```json
{
  "workspaceSource": "local_folder",
  "targetPackage": {
    "schema": "mnote.agent_target_package.v1",
    "primaryTargetId": "target-1",
    "targets": []
  },
  "currentFile": {
    "path": "/workspace/page.md",
    "uri": "file:///workspace/page.md",
    "version": "mtime:size:hash"
  },
  "allowedRoots": ["/workspace"],
  "allowedFiles": ["/workspace/page.md"],
  "selection": {
    "text": "当前选区文本",
    "anchor": "可选的 editor/block/offset 引用"
  },
  "context": {
    "readonly": false,
    "dirty": false,
    "resourceRefs": []
  }
}
```

后续可以补充 page id、object identity、resource refs、outline、nearby context，但不得把普通正文写入重新包装成 `mnote.doc.markdown_edit`。

## 8. 验收清单

### Phase A0：target selection 与 UI

- [x] Page AI composer 可见 target chip，显示 workspace、target title、resourceKind、scope、readonly / dirty 状态。
- [x] 点击 target chip 打开 target picker，可在当前焦点、打开 tabs、树选中项之间选择。
- [x] 默认 target 来自 last focused editor / resource tab，不来自第一个 tab、URL documentId 或树选中项。
- [x] mindmap / office / raw file target 显示真实 resourceKind，不伪装成 markdown page。
- [ ] 多 tab / 跨 workspace / dirty write 时发送前需要显式确认。
- [ ] 发送后消息记录冻结 target 摘要，切换 tab 不影响已启动 run。

### Phase A：运行时输入与权限边界

- [ ] 从文档页发起 Hermes / Reasonix run 时，request body 包含 `currentFile`、`allowedRoots`、`allowedFiles`、selection 和 dirty / readonly context。
- [ ] run body 包含 `targetPackage`，且 `allowedRoots` / `allowedFiles` 从 targetPackage 推导。
- [x] local-first 普通 Markdown 编辑 run 不包含 `mnote.doc.markdown_edit` 推荐工具。
- [ ] allowed roots 外文件写入被 agent runtime 或 MNote 审计层拒绝。
- [x] readonly 页面不会启动可写 run，或只启动只读问答 run。

2026-06-01 进展：已补第一版 `mnote.agent_target_package.v1` 运行时输入。前端 Page AI run body 会从当前 `editorTarget` / `WorkspacePath` 生成 `targetPackage`；后端会 sanitize 该包，并把 local-first `aiAccessScope.allowedFiles` / `allowedFilePaths` 与 `agentRunEnvelope.allowedFiles` 从该包派生。`task502-page-ai-agent-selector-context-smoke.js` 已覆盖 page / mindmap / OnlyOffice target picker 与 payload 冻结；`task520-page-ai-raw-resource-target-smoke.js` 已覆盖真实 raw local resource tab，并断言 `objectIdentity` 不退化为 `[object Object]`。当前仍未完成跨 workspace 多选确认和真实 agent 写入回收 smoke，因此本文继续保持 `process`。

### Phase B：写入回收与前台同步

- [x] agent 修改当前 `.md` 后，MNote 能回收 changed files / diff。
- [x] clean buffer 下 watcher -> BufferStore -> Page Aggregate -> ProseMirror 可见更新通过 browser smoke。
- [x] dirty buffer 下外部 agent 写入不会静默覆盖，必须出现冲突或 review 状态。
- [x] changed files / diff 与 session / run / actor 关联写入审计。

2026-06-06 执行拆分：Phase B/C 的真实 smoke 与审计收口已拆到 `design/07-ai/process/7-18-agent-edit-clean-dirty-smoke-checklist-v1.md`。该拆分只覆盖 local-first 普通 Markdown agent 原生文件编辑，不碰 LightRAG / Knowledge RAG / OCR / evidence index。

2026-06-07 进展：`task535` 已证明 clean buffer 下 agent 原生文件 patch 写入真实 `.md` 后，receipt 触发当前文档与 filetree 刷新，前台 tiptap 无需 reload 即可见更新；`task536` 已证明 dirty buffer 在启动可写 run 前阻塞且磁盘不变；`local_agent_audit` targeted tests 已覆盖 allowedFiles 优先、folder context 不扫全 root、超限 auditScope 可见。

### Phase C：旧工具退役

- [x] local-first 普通 Markdown 编辑 smoke 断言未调用 `/api/documents/save`。
- [x] local-first 普通 Markdown 编辑 smoke 断言未调用 `mnote.doc.markdown_edit`。
- [x] Hermes / Reasonix manifest 或 system prompt 不再鼓励普通 Markdown 编辑调用 `mnote.doc.markdown_edit`。
- [x] 旧 `markdown_edit` 测试只保留为历史 online / compat 回归，并在文档中标明不指导新实现。

2026-06-07 进展：Page AI / Hermes run guidance 明确禁止 local-first 普通 Markdown 编辑调用 `mnote_doc_markdown_edit` 或 `mnote_page_save`；`mnote.doc.markdown_edit` manifest 描述已收口为 compat / remote / cloud fallback 或结构校验辅助；Reasonix ACP wrapper selftest 已断言 prompt 包含禁止使用 MNote doc/page write tools 的口径；`task535` 同时断言未调用 `/api/documents/save`、`mnote.doc.markdown_edit`、`mnote.page.save`。

### Phase D：结构辅助边界

- [ ] `mnote.block.*` 只在复杂结构辅助场景出现，例如结构块排序、非 Markdown 资源辅助。
- [x] `mnote.page.save` 只作为页面级兜底写入，不作为默认精确编辑入口。
- [ ] `mnote.doc.fetch` 不作为 local-first 正文读取主入口；需要结构化上下文时由 MNote 在 run input 中提供摘要或 projection。

## 9. 不做事项

- 不参考 CLI Main 的 Lark Doc 在线文档写入架构来设计 local-first 普通 Markdown 编辑。
- 不新增 MNote 普通 Markdown 编辑工具。
- 不把 Convex 恢复为默认正文或 AI 会话全文主存储。
- 不实现 Phase C Review Mode / StreamApplyController / GhostTextOverlay。
- 不让前端维护第二份正文真相。
- 不把所有打开 tab 默认发送给 agent。
- 不把跨 workspace 自动合并为一个隐式上下文。

## 10. 归档条件

满足以下条件后，本稿可归档到 `design/07-ai/done/`：

- Phase A0 target UI 有真实浏览器 smoke 证据。
- Phase A / B / C 至少各有一条真实浏览器或 targeted test 证据。
- local-first agent 文件编辑主路径已能在 clean buffer 下完成真实 `.md` 修改并同步回 tiptap。
- dirty conflict 不静默覆盖。
- 文档和 manifest 不再把 `mnote.doc.markdown_edit` 描述为 local-first fallback。
