Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions docs/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
| --- | --- |
| [ARCHITECTURE.md](./ARCHITECTURE.md) | main 进程模块、所有权、生命周期和依赖方向 |
| [FLOWS.md](./FLOWS.md) | 启动、Session、Agent、Tool、Remote、Scheduler、Sync 和退出流程 |
| [design-system.md](./design-system.md) | 当前产品、界面与视觉设计风格基线 |
| [architecture/agent-system.md](./architecture/agent-system.md) | DeepChat / ACP backend、Run、权限和 Subagent 合同 |
| [architecture/session-management.md](./architecture/session-management.md) | Session 数据、binding、恢复、删除和 transfer |
| [architecture/tool-system.md](./architecture/tool-system.md) | Tool、MCP、Skill、Plugin 和权限边界 |
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,10 @@
"file": "src/renderer/settings/App.vue",
"specifier": "../src/composables/useFontManager"
},
{
"file": "src/renderer/settings/App.vue",
"specifier": "../src/foundation/appearance/documentAppearance"
},
{
"file": "src/renderer/settings/App.vue",
"specifier": "../src/lib/iconLoader"
Expand Down Expand Up @@ -723,5 +727,5 @@
"specifier": "@/i18n/bootstrap"
}
],
"settingsToChatAppImportCount": 170
"settingsToChatAppImportCount": 171
}
134 changes: 134 additions & 0 deletions docs/architecture/chat-markstream-rendering-pipeline/plan.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,134 @@
# 实施计划

## 1. 建立单一流式节拍 owner

`MarkdownRenderer` 保留非流式 fast/slow debounce,但为 chat streaming 增加同步旁路:

```text
content update
-> streaming now or streaming just ended?
yes -> invalidate older debounce revision and commit immediately
no -> keep existing 32/96 ms static debounce
-> NodeRenderer
-> Markstream smooth stream / parse scheduler
```

监听 source 同时包含 `content` 与 `isStreaming`。完成态转换必须根据 previous streaming state
同步提交,以覆盖父组件同一次 patch 中 content/status 一起改变的情况。旧 timer 继续用 revision
guard no-op,不新增 timer cleanup 或第二份派生状态。

## 2. 解耦 node virtualization 与重节点优先级

保留两类派生值:

- `shouldVirtualizeNodes = virtualizeNodes && !isStreaming`:只决定 Markdown node DOM window;
- `shouldUseViewportPriority = virtualizeNodes`:继续决定 heavy node 是否接近视口后才启动。

保持 `codeRenderer="monaco"` 贯穿同一个 `NodeRenderer`,不要以 phase key 重建它。当前 Markstream 将
该兼容名称映射到由 `stream-diffs` 支持的 `CodeBlockNode`:流式期间它保留自身的 `<pre>` fallback,代码块
完成且可见后再把同一宿主原子升级为单一 File/FileDiff surface。`deferNodesUntilVisible` 与
`viewportPriority` 继续使用第二个值,保留既有 Mermaid/KaTeX 等重节点的调度策略。搜索/截图传入
`virtualizeNodes=false` 时,所有内容仍立即可用。

`MessageBlockContent` 只在搜索、截图等完整 DOM 消费者出现时传 `virtualizeNodes=false`。streaming 的
node window 仍由 `MarkdownRenderer` 内部的 `virtualizeNodes && !isStreaming` 负责关闭。

chat streaming 的 typewriter 使用 `simple` 模式。它保留尾部 CSS 光标和 smooth streaming 的 auto
eligibility,但不使用 precise 模式每次提交的 Range/getClientRects 光标定位。

## 3. 使用 Markstream 内建 code renderer

- 增加 `stream-diffs` direct dependency,使 Markstream 的动态 runtime import 可解析;
- live chat 与 completed/static 均显式传 `codeRenderer="monaco"`;live 传 `codeBlockStream=true`,完成时传 false 并同步更新 `final=true`。不重建 NodeRenderer,也不直接调用 `stream-diffs`;
- 删除 generic `code_block` custom mapping,恢复 `CodeBlockNode` 的内建 `<pre>` fallback 与增强 surface handoff;
- 通过 `codeBlockProps` 传 themes 和工具栏选项,通过 `codeBlockMonacoOptions` 传字体/换行,通过 `@handle-artifact-click` 接收预览;
- 保留 Mermaid strict props、`break-words` prose root 和可收缩的 flex host,但不覆盖 `stream-diffs` gutter/content 内部几何。

## 4. 消息顺序与流式窗口

后续 `renderer-state-ownership-hardening` 已移除 stable/tail layout 快路径。`useDisplayMessages`
始终按 `messageIds` 产生完整顺序的 display list,未变化记录通过转换缓存复用;`useMessageWindow`
据此建立 geometry,虚拟窗口限制实际挂载的行数。本地 optimistic user 和首次 stream placeholder 的
`orderSeq` 使用当前缓存最大有限值加一,而不是 `messageIds.length + 1`。这样只加载长会话尾页时,
新消息仍保持真实尾序,不会因一次全量排序被移到历史前方。该扫描只发生在本地消息首次插入,不在
token snapshot 热路径。

## 5. 缩短 full snapshot 的 renderer 热路径

- stream store 使用 shallow ref,因为每个 snapshot 都是整数组替换,内部没有嵌套 mutation contract;
- `applyStreamingBlocksToMessage` 在写入 JSON record 的同时,用同一份已校验 blocks 预填 parsed cache;
- stable block identity 继续通过 `reuseStableAssistantBlocks` 复用,metadata cache 在内容更新时保留。

main 的 JSON normalization + Zod clone 继续保留,因为真实 `extra` payload 含有 undefined 值,直接
schema parse 会改变兼容语义甚至拒绝 snapshot。preload 与 renderer event contract 校验也继续保留。
full snapshot transport 仍会随累计长度重复校验和跨进程复制;delta protocol 需要独立设计和
profile,不在本次变更中暗改。

## 6. 移除 custom registry 热路径

MarkdownRenderer 改用 NodeRenderer 内建 link/reference/Mermaid 组件:

- `mermaidProps.isStrict=true` 保持安全模式;
- 根级 click 委托识别 anchor,继续调用 `navigateLink`;
- 根级 mouse/click 委托识别 `.reference-node`,继续加载搜索结果与显示引用浮层;
- code preview 通过公开 `@handle-artifact-click` event 处理。

因此不再调用 `setCustomComponents/removeCustomComponents`,同消息多个 text part 不会互相覆盖,
Markstream 也可以启用 append-only parse 和 stable top-level node reuse。`customId` 仍包含实例 token,
用于隔离 NodeRenderer 自身的虚拟化/测量 identity。

## 7. 与 PR #2000 的边界

不改变 `useMessageWindow` 或 `useMessageVirtualization` 的 contract。本次补齐
`useDisplayMessages` 对真实尾部 inline stream 的分类,并保证 tail 到 Markstream 的交接不再重复
pacing、重复 JSON parse 或因 registry 关闭增量节点复用。

完成态继续依赖现有 message id / renderKey handoff:

```text
pending placeholder -> real stream message -> persisted final message
same outer row identity same MarkdownRenderer instance
```

## 8. 测试策略

- MarkdownRenderer component test:
- 后续 streaming snapshot 同步转交,不只覆盖首 chunk;
- final transition 同步带上最终 content,且旧 timer 不能回写;
- streaming 到 final 始终选择同一个 enhanced renderer,并把 `codeBlockStream` 从 true 切到 false;
- explicit `virtualizeNodes=false` 同时关闭三类延迟;
- `stream-diffs` handoff、无 generic override、preview 和 Mermaid strict 保持。
- useDisplayMessages:
- 尾部 inline stream 返回 segments,连续 snapshot 保持 stable identity;
- 中间位置 inline stream 返回 null,完整消息顺序不变。
- message/echo:
- schema parse 保持深拷贝;
- snapshot blocks 预填 parsed cache,并保持 settled block identity。
- 复跑 MessageBlockContent、MessageList、MessageItemAssistant、useDisplayMessages、
useMessageWindow、useMessageVirtualization 和 ChatPage 相关 suite。
- 执行 typecheck 与 production renderer build,验证 Markstream optional peer 的真实解析路径。

## 9. 验证命令

```bash
pnpm exec vitest --config vitest.config.renderer.ts test/renderer/components/MarkdownRenderer.test.ts test/renderer/components/message/MessageBlockContent.test.ts
pnpm exec vitest --config vitest.config.renderer.ts test/renderer/components/MessageList.test.ts test/renderer/components/message/MessageItemAssistant.test.ts test/renderer/composables/useMessageWindow.test.ts test/renderer/composables/useMessageVirtualization.test.ts
pnpm run format
pnpm run i18n
pnpm run lint
pnpm run typecheck
pnpm run test:renderer
pnpm run build
```

## 风险与缓解

| 风险 | 缓解 |
| --- | --- |
| final 与 content 同 patch 时顺序不确定 | 同时 watch 两个 source,并用 previous streaming 状态同步提交 |
| streaming heavy node 延迟后高度变化影响滚动 | 使用 Markstream 自带 lifecycle/ResizeObserver,外层继续按 PR #2000 批量测量 |
| 搜索或截图拿不到离屏 DOM | `virtualizeNodes=false` 同时关闭 node window 与 viewport deferral |
| stream-diffs optional peer 缺失 | direct dependency + Markstream 内建 pre fallback + production build 验证 |
| 旧静态 debounce 晚到覆盖 stream | 每次同步旁路递增同一个 revision,使旧 callback no-op |
| 事件委托误拦截普通节点 | 只处理 closest anchor / `.reference-node`,其他 click/mouse event 原样忽略 |
| 中间 stream 被错误追加到尾部 | 仅以排序后的 `messageIds` 最后一个 id 精确匹配,其他情况返回 null |
120 changes: 120 additions & 0 deletions docs/architecture/chat-markstream-rendering-pipeline/spec.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
# Chat 与 Markstream 流式渲染管线

## 背景

PR #2000 已把高频流式消息从稳定历史列表中拆出,并为消息窗口增加 append-only tail
快路径、批量测量和显式行级 streaming 状态。与此同时,聊天正文仍经过 DeepChat 自己的
内容防抖,再交给当前 `markstream-vue@1.0.7-beta.4` 的 smooth streaming、解析合并、节点批量挂载和重节点延迟机制。增强代码块使用其 `stream-diffs@0.0.2` peer。

当前端到端链路如下:

```text
provider token events
-> main accumulator
-> echo renderer throttle (120 ms, full block snapshot)
-> chat.stream.updated typed event
-> messageIpc request/session ordering gate
-> stream store + folded message record
-> stable display history + active streaming tail
-> outer message window / scroll arbitration
-> MessageBlockContent ant-tag projection
-> MarkdownRenderer stream handoff
-> Markstream smooth streaming (up to 20 commits/s)
-> incremental Markdown parse + node batching
-> Monaco / Mermaid / KaTeX viewport deferral
-> DOM measurement -> outer message window / scroll follow
```

主进程的 120 ms 合帧控制跨进程吞吐,Markstream 的 smooth streaming 控制用户实际看到的
文本节奏,PR #2000 的消息窗口控制历史行重算和滚动。集成层不应再成为第三个流式节拍 owner。

## 问题

1. 聊天流内容在进入 Markstream 前再经过 32 ms 或 96 ms 防抖。它不能减少 main snapshot
数量,却增加首段之外每个 snapshot 的延迟,并可能让 `final` 先于最新防抖内容生效。
2. 旧集成曾把 `codeRenderer="monaco"` 误当作流式期间即时创建 Monaco 的开关,并因此尝试在
应用层把 renderer 从 `pre` 重建为 `monaco`。当前 Markstream 的该兼容名称实际选择由
`stream-diffs` 驱动的增强 `CodeBlockNode`:它在 block streaming 时保留内建 `<pre>`,仅在
block 完成且可见后才升级同一宿主。因此外部 remount 会破坏受支持的 handoff,并可能造成完成闪烁或瞬时几何。
3. generic `code_block` 自定义映射会绕过 Markstream 内建的 renderer selection、异步 fallback 和
viewport-deferred `stream-diffs` 路径。仅安装 optional peer 或直接导入其 controller 都不能替代该路径。
4. 该阶段曾为尾部 inline stream 引入 stable/tail layout segments;后续
`renderer-state-ownership-hardening` 已移除这条私有路径,统一以完整 display-list 建立消息 geometry。
5. 每个累计 snapshot 在 main 中先 JSON round-trip 再 Zod clone,renderer 写入消息后又立即把
同一份 blocks JSON.parse 回来;长回复会放大全量 snapshot 协议本身的 O(L^2) 累计成本。
6. 每个 MarkdownRenderer 通过全局 custom component registry 注册纯渲染 wrapper。同一消息被
artifact 分段时 registry id 冲突,且任意非空 custom map 都会让 Markstream 禁用稳定顶层节点复用。
7. `codeBlockStream=false` 在 loading code node 上显示 skeleton,而不是实时代码;它不负责选择
Monaco。对于聊天流,轻量 `<pre>` 比 skeleton 更连续,enhanced runtime 仍应等节点闭合且可见。
8. 外层消息窗口与 Markstream 内层节点窗口职责相邻。两者必须各自只处理自己的粒度:外层按
message 行裁剪,内层按 Markdown node 裁剪,搜索/截图需要完整 DOM 时同时显式退出延迟行为。

## 目标

1. 让 main 进程负责 snapshot 合帧,Markstream 负责文本 pacing;聊天集成层同步转交流式内容。
2. 从 streaming 到 final 的同一消息、MarkdownRenderer、NodeRenderer 与内建 CodeBlockNode 都保持身份稳定;最终内容不会被旧的防抖回调覆盖。
3. 整个生命周期保持 Markstream `codeRenderer="monaco"`,并仅以 `codeBlockStream` / `final` 表达流式状态;内建 `CodeBlockNode` 在 streaming 时呈现 `<pre>`,完成且可见后才挂载 `stream-diffs` 表面。内层 node virtualization 继续在 streaming 阶段关闭,避免 typewriter 尾部被节点窗口裁掉。
4. completed 历史消息继续使用 Markstream node virtualization 及可见性延迟;聊天搜索、截图和其他要求
完整 DOM 的路径仍可通过 `virtualizeNodes=false` 同时关闭虚拟化与视口延迟。
5. ordinary fenced code 使用 Markstream 内建 `stream-diffs` 路径;streaming 时先显示轻量 fallback,
final 且接近视口后再加载 enhanced File/FileDiff surface。Mermaid 保持 strict 处理。
6. 保留非流式、可高频编辑的 docs/artifact surface 的现有内容防抖,避免把聊天优化扩散到不同
交互语义的页面。
7. inline stream 与其他消息统一由完整 display-list 驱动;未变化记录仍复用 display-message
转换缓存,虚拟窗口负责限制已挂载的行数。
8. 复用已通过 IPC schema 校验的 blocks,减少 renderer 内部重复 JSON parse,同时不削弱
main/preload/renderer 边界校验,也不改变持久化 JSON 格式。
9. 使用 Markstream 内建 link/reference/Mermaid 节点和 renderer 级事件委托,避免全局 registry
写入及其 parser reuse 失效;链接导航、搜索引用预览和 strict Mermaid 行为保持不变。
10. 高频 chat stream 使用 Markstream 的 simple CSS typewriter cursor,避免 precise cursor 每次可见
提交都通过 Range/getClientRects 触发布局读取。

## 非目标

- 不修改 `chat.stream.updated` snapshot contract、main 120 ms renderer 合帧或 600 ms DB 合帧。
- 不改 Markstream、stream-diffs 或 DeepChat 独立 editor surfaces 所用 stream-monaco 的上游实现,不维护本地 fork。
- 不重写 `useArtifacts` 的 ant-tag parser、消息 JSON 持久化格式或 message store 的折叠协议。
- 不在本次工作中把 full snapshot IPC 改为 delta transport;它仍是超长输出的剩余架构瓶颈。
- 不替换 PR #2000 的 outer message window、scroll controller、search highlight 或 capture owner。
- 不在本次工作中改变 markdown worker 的全局生命周期或引入新的用户设置。

## 验收标准

1. streaming 状态下,首个及后续 `content` snapshot 都在同一 Vue 更新周期传给 NodeRenderer,
不等待 DeepChat 的 32/96 ms timer。
2. streaming -> final 时,NodeRenderer 同步收到最终 content 和 `final=true`;任何较早的静态防抖
任务都不能回写旧内容。
3. 非流式内容更新继续走已有 fast/slow debounce;现有 docs/artifact 行为不变。
4. 正常 streaming chat 配置为 `nodeVirtual=false`、`maxLiveNodes=0`、`codeRenderer="monaco"`、`codeBlockStream=true`;完成态在同一 NodeRenderer 上设为 `final=true` 与 `codeBlockStream=false`。Markstream 在流式阶段展示内建 `<pre>`,完成且可见后才升级 `stream-diffs` surface。`viewportPriority` 与 `deferNodesUntilVisible` 仍仅由 `virtualizeNodes` 控制。
5. `virtualizeNodes=false` 时,node virtualization、viewport priority 和 visible deferral 均关闭,
保证搜索、截图和完整 DOM 消费者可用。
6. NodeRenderer 在 live chat 和 completed/static 内容均保持 `codeRenderer="monaco"`,只通过 `codeBlockStream` 与 `final` 交给 Markstream 管理单一代码块 handoff;preview event 与 strict Mermaid 仍可用。
7. MarkdownRenderer 不写入 Markstream 全局 custom component registry;内建 link/reference 事件经
根级委托保持 DeepChat 导航和引用交互,同消息多个 text part 互不覆盖。
8. inline stream 在完整 display-list 中保持 `messageIds` 顺序;未变化记录的转换缓存、单行
streaming 状态和 completion node reuse 回归测试继续通过。
9. renderer 已校验 blocks 直接预填 parsed cache,不在同一个 snapshot 写入后立即 JSON.parse。
12. format、i18n、lint、typecheck、targeted renderer tests 和 renderer production build 通过。

## 性能与交互预算

- 跨进程 snapshot 频率仍由 main 限制为最多约 8.3 次/秒。
- 屏幕文本提交频率由 Markstream 限制为最多 20 次/秒;集成层不再叠加聊天内容 timer。
- 单个 stream snapshot 的 display-message 转换复用未变化记录;外层 geometry 仍以完整 display-list
计算,并由虚拟窗口限制实际挂载的行数。
- 已校验 blocks 不得在 renderer 同步热路径再次 JSON.parse。
- 重节点在接近视口前不得启动 Monaco/Mermaid/KaTeX 重工作;完成态的 fallback 到 enhanced
切换不得替换外层 message row。
- 沿用 chat scroll ownership 的每帧最多一次 scroll write、1 px anchor 误差和无新增 >50 ms
long task 预算;jsdom 测试覆盖可自动化的调度和 handoff 回归。

## 兼容性与回滚

变化位于 main snapshot clone 与 renderer 集成层,不改变持久化数据、IPC schema 或用户设置。
若需回滚,可以分别恢复内容路由、parsed cache 预填、事件委托和内建 code renderer 选择;
`renderer-state-ownership-hardening` 的完整 display-list 消息窗口仍可独立工作。

## GitHub Issue

未请求或创建 GitHub issue。当前工作区的 `docs/issues/markstream-code-block-rendering/spec.md`
记录 ordinary code block 的具体回归,本架构目标覆盖其集成层根因。
Loading
Loading