Skip to content
Merged
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
34 changes: 34 additions & 0 deletions docs/architecture/chatpage-decomposition/plan.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
# Plan: ChatPage 分解与竞态治理

## 实施顺序

按"每抽一个 composable 即 typecheck + 测试"的节奏,从耦合最少的关注点开始,
逐步收编模块级令牌;滚动仲裁族(`useChatScrollController` 等)保持不动,只归拢胶水调用。

1. **死代码清理** — 删除零引用的 `composables/message/useMessageScroll.ts`(339 行,
旧 vue-virtual-scroller 实现)及其孤儿测试、`ScrollInfo` 类型。
2. **useDisplayMessages + usePlanFloatLifecycle** — 记录→DisplayMessage 转换、
稳定/流式分离、占位符四态机(见 spec 决策记录)、Plan 快照生命周期。
3. **useChatSearch** — 会话内搜索,包装 `lib/chatSearch` 的 rAF/highlight 调度。
4. **useListGestures + useMessageVirtualization** — 手势原子与虚拟化原子,
再组合接入主文件;composable 声明顺序移到会话 watch 之前。
5. **useComposerSubmit** — 发送/排队/steer/命令/compaction 统一提交路径,
自持 `attachmentFilterToken`;四个提交入口的重复守卫收敛为 `canSubmitNow()`。
6. **useSessionRestore** — 会话恢复 epoch(`restoreRequestId`)、`canWriteSessionView`
写 gate、启动延迟恢复调度、`deactivate()` 卸载语义;主文件通过
`currentRestoreRequestId()` 读取最新令牌。

## 验证策略

- 每步:`pnpm run typecheck:web` + `ChatPage.test.ts`(82 用例)。
- 收尾:`pnpm run format` / `lint` / `i18n` / `test:renderer` 全量,
与 origin/dev 对照隔离既有失败。
- 行为零漂移由测试 + 逐行 diff 审查共同保证;竞态令牌语义(闭包内最新值 vs 快照)
单独走一轮对抗性 review。

## 回滚

每个 composable 一个独立 commit。因抽取提交之间存在依赖(后一个 composable 的接入
依赖前面已建立的装配结构与导入),回滚须按提交的**逆序**进行,或连同依赖它的后续
提交一并回滚,不能孤立 revert 中间某一步。滚动仲裁契约(`useChatScrollController`
及其协作模块)全程未动,整体回滚到基线不影响该子系统。
64 changes: 64 additions & 0 deletions docs/architecture/chatpage-decomposition/spec.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
# ChatPage 分解与竞态治理

## 背景

`src/renderer/src/pages/ChatPage.vue` 已膨胀到 3050 行,单个 `<script setup>` 承载 10+ 个关注点:
会话恢复、消息记录转换、虚拟窗口、测量批处理、手势/滚动、占位符状态机、Plan 快照生命周期、
会话内搜索、语音输入、发送/排队/steer、消息操作。竞态治理靠散落的手写令牌
(`sessionRestoreRequestId`、`voiceInputConfigToken`、`attachmentFilterToken`、
`chatScrollSessionEpoch`、`pendingAssistantPlaceholderSeq`)和大量模块级 `let`,理解成本极高。

## 现状边界(已良好分层,不动)

滚动仲裁已是成熟架构,本次仅把 ChatPage 内的胶水调用归拢,不改其内部时序:

- `useChatScrollController` — 独占滚动通道、状态机驱动、rAF commit/verify。
- `chatScrollState` — 纯 reducer 状态机(mode/userOwned/nearBottom/activeGesture)。
- `chatScrollOperationArbiter` — 物理滚动条独占所有权 + 优先级抢占。
- `chatScrollRequestQueue` — 单槽优先级队列。
- `useMessageWindow` — 虚拟列表布局、测量高度、快照捕获/恢复。
- `recentMessageMeasurementCache` — 最近会话测量 LRU 缓存。

## 目标

1. 主文件收缩到 ~400 行,只做装配 + 模板。
2. 每类竞态由一个自持 epoch/token 的 composable 独立治理,边界清晰。
3. 删除确认的死代码。
4. 行为零漂移:每抽一个 composable 即 typecheck;最终 lint/test/真实应用验证。

## 死代码(确认零引用)

- `composables/message/useMessageScroll.ts`(339 行)— 全项目零引用,旧 vue-virtual-scroller 实现。
- `composables/message/types.ts` 中的 `ScrollInfo` 接口 — 仅被上文件引用(`CaptureOptions` 仍在用,保留)。

## 分解目标(8 个关注点 → 7 个 composable)

| composable | 职责 | 收编的状态/竞态 |
|---|---|---|
| `useSessionRestore` | 会话切换、令牌 gate、恢复 | `sessionRestoreRequestId`、`canWriteSessionView`、启动延迟恢复调度 |
| `useDisplayMessages` | 记录→DisplayMessage、稳定/流式分离、缓存;**并含占位符四态机** | `displayMessageCache`、`assistantRenderKeyByMessageId`、`pendingAssistantPlaceholder` 全套 |
| `useMessageVirtualization` | 窗口范围、测量批处理、锚点补偿、几何观测 | `pendingMeasureQueue`、rAF flush |
| `useListGestures` | wheel/touch/pointer/键盘手势 → 控制器 | ~15 handler、`isListScrolling` |
| `usePlanFloatLifecycle` | Plan 快照跨会话生命周期 + 延迟清除 | `planSnapshotClearTimers`、3 lifecycleKey |
| `useChatSearch` | 会话内搜索(包 `lib/chatSearch`) | 搜索 rAF、highlight 调度 |
| `useComposerSubmit` | 发送/排队/steer/命令/compaction | 统一 gate 后的提交路径、`attachmentFilterToken` |

> 决策记录:原计划独立的 `useAssistantPlaceholder` 并入 `useDisplayMessages`。占位符的
> renderKey 交接直接写入消息转换缓存读取的 `assistantRenderKeyByMessageId`,显隐判定依赖
> `hasFirstStreamingContent` / `ephemeralRateLimitBlock`,占位符行本身注入流式尾部组装;
> 强拆会造成两个 composable 双向共享三份可变状态,不如单一所有者边界清晰。

## 约束

- 每个 composable 自持一个防竞态 epoch/token,替代散落的模块级令牌。
- 不改 `useChatScrollController` 及其协作模块的对外时序契约。
- 优先 shadcn-vue / VueUse(`useEventListener`、`useRafFn` 等)替代手写 rAF/监听器。
- i18n key 不新增;用户可见字符串沿用现有 key。

## 模块位置决策

7 个 composable 放在 `pages/chat-page/` 而非全局 `lib/` / `components/`,是有意的
feature-local 布局:它们是 `ChatPage.vue` 独占的私有逻辑,强耦合页面 props 与页面级
store 组合,不面向复用。就近放置能让读者一眼看出归属、避免误当作通用工具被其他页面引用。
`lint:architecture` guard 通过(未对该布局设硬约束)。若后续有第二个页面需要复用其中某个
composable,再将其上提到 `lib/` 并补通用化改造。
19 changes: 19 additions & 0 deletions docs/architecture/chatpage-decomposition/tasks.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
# Tasks: ChatPage 分解与竞态治理

- [x] 写 `spec.md` / `plan.md` / `tasks.md`
- [x] 删除死代码 `useMessageScroll.ts`(339 行)+ 孤儿测试 + `ScrollInfo` 类型
- [x] 抽取 `useDisplayMessages`(含占位符四态机,见 spec 决策记录)
- [x] 抽取 `usePlanFloatLifecycle`
- [x] 抽取 `useChatSearch`
- [x] 抽取 `useListGestures` / `useMessageVirtualization` 原子并组合接入
- [x] composable 声明顺序移到会话 watch 之前
- [x] 抽取 `useComposerSubmit`(自持 `attachmentFilterToken`,守卫收敛 `canSubmitNow()`)
- [x] 抽取 `useSessionRestore`(自持 `restoreRequestId` epoch + `deactivate()` 卸载语义)
- [x] `useAssistantPlaceholder` 决策:并入 `useDisplayMessages`,不单独成文件(spec 有记录)
- [x] 全量验证:typecheck / lint / format / i18n / `ChatPage.test.ts` 82 用例
- [x] 对抗性 review(逐行对照基线 `8b121ede8`):发现并修复 `usePlanFloatLifecycle`
中 linger 状态从 `ref` 降级为普通对象导致的响应性回归(完成后浮窗不再驻留 1200ms);
其余确认零漂移
- [x] 合并 origin/dev 并复核(Settings/Provider/sidepanel 的 18 个失败为 dev 既有,与本分支无关)
- [ ] 后续(可选):主文件继续向 ~400 行收缩(语音输入、消息操作 handler、
工具交互 respond 等仍在主文件;当前 ~1710 行,较 3050 行已收缩 44%)
17 changes: 0 additions & 17 deletions src/renderer/src/composables/message/types.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,3 @@
export interface ScrollInfo {
viewportHeight: number
contentHeight: number
scrollTop: number
}

export interface CaptureOptions {
messageId: string
parentId?: string
Expand All @@ -13,14 +7,3 @@ export interface CaptureOptions {
model_provider: string
}
}

export interface WatermarkConfig {
isDark: boolean
version: string
texts: {
brand: string
tip: string
model?: string
provider?: string
}
}
Loading
Loading