Skip to content

refactor(chat): decompose ChatPage into composables#1989

Merged
zhangmo8 merged 16 commits into
devfrom
refactor/chatpage-decomposition
Jul 17, 2026
Merged

refactor(chat): decompose ChatPage into composables#1989
zhangmo8 merged 16 commits into
devfrom
refactor/chatpage-decomposition

Conversation

@zhangmo8

@zhangmo8 zhangmo8 commented Jul 17, 2026

Copy link
Copy Markdown
Collaborator

Summary

  • Decompose the 3050-line ChatPage.vue <script setup> into seven focused composables under src/renderer/src/pages/chat-page/, each owning one concern and its own race-guard epoch/token. Main file drops to ~1710 lines (−44%).
  • Replace scattered module-level race tokens (sessionRestoreRequestId, attachmentFilterToken, isChatPageActive) with self-contained closures inside the owning composable.
  • Delete confirmed dead code: composables/message/useMessageScroll.ts (339 lines, zero references, old vue-virtual-scroller path) + its orphan test + the ScrollInfo type.
  • Behavior zero-drift: no logic change. Every concern is a line-for-line move; the four submit paths (onSubmit / onQueueSubmit / onSteer / onCommandSubmit) had their duplicated guards collapsed into a single canSubmitNow() with identical semantics.

Composables extracted

composable concern race state it now owns
useSessionRestore session switch, write gate, restore scheduling restoreRequestId epoch, canWriteSessionView, deferred startup restore
useDisplayMessages record→DisplayMessage, stable/streaming split, pending-assistant placeholder state machine displayMessageCache, assistantRenderKeyByMessageId, pendingAssistantPlaceholder
useMessageVirtualization window range, measure batching, anchor compensation pendingMeasureQueue, rAF flush
useListGestures wheel/touch/pointer/keyboard gestures → controller ~15 handlers, isListScrolling
usePlanFloatLifecycle plan-snapshot cross-session lifecycle + deferred clear planSnapshotClearTimers
useChatSearch in-conversation search (wraps lib/chatSearch) search rAF, highlight scheduling
useComposerSubmit send / queue / steer / command / compaction post-gate submit paths, attachmentFilterToken

Notable notes

  • The planned useAssistantPlaceholder was merged into useDisplayMessages rather than split out: its renderKey handoff writes the same assistantRenderKeyByMessageId map the conversion cache reads, and its show/hide depends on hasFirstStreamingContent / ephemeralRateLimitBlock. A single owner keeps the boundary clean. Decision recorded in the spec.
  • An adversarial line-by-line review against the base commit caught one reactivity regression introduced mid-branch — the plan-float linger flag had been downgraded from a ref to a plain object, so the float would vanish instantly instead of lingering 1200ms after a session completes. Fixed in fix(chat): restore plan float linger reactivity; everything else verified drift-free.
  • The scroll-arbitration family (useChatScrollController, chatScrollState, chatScrollOperationArbiter, useMessageWindow) is unchanged — only ChatPage's glue calls were regrouped.
  • No i18n keys added or changed; all user-facing strings reuse existing keys (verified present with real translations across all 21 locales via pnpm run i18n).

Test plan

  • pnpm run typecheck (node + web)
  • pnpm run format:check / pnpm run lint / pnpm run i18n
  • ChatPage.test.ts — 82/82 pass
  • Full test:renderer — the 18 failures are pre-existing on dev (Settings/Provider deeplink + sidepanel), reproduced on a clean origin/dev checkout; unrelated to this branch.

Architecture

See docs/architecture/chatpage-decomposition/spec.md, plan.md, tasks.md.

Summary by CodeRabbit

  • Architecture

    • Refactored chat-page responsibilities into dedicated, feature-focused composables (message rendering, in-chat search, gestures, virtualization, session restore, plan snapshot float, and composer submission).
    • Improved scrolling stability during message updates and virtualization.
  • Bug Fixes

    • Prevented stale async restores/submissions from impacting the currently active conversation.
    • Improved cleanup and cancellation behavior when leaving the chat page.
  • Documentation

    • Added detailed architecture/spec, task checklist, and rollback guidance for the chat-page improvements.
  • Tests

    • Updated scrolling-architecture test coverage and removed obsolete message-scroll tests.

@coderabbitai

coderabbitai Bot commented Jul 17, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 2bf2c92f-06e2-4ade-b3d2-0779ce152e4d

📥 Commits

Reviewing files that changed from the base of the PR and between 62d8639 and 5d055fb.

📒 Files selected for processing (2)
  • docs/architecture/chatpage-decomposition/plan.md
  • docs/architecture/chatpage-decomposition/spec.md
🚧 Files skipped from review as they are similar to previous changes (1)
  • docs/architecture/chatpage-decomposition/plan.md

📝 Walkthrough

Walkthrough

ChatPage is decomposed into composables for message display, search, gestures, virtualization, session restoration, plan snapshots, and composer submission. Obsolete scroll code and related declarations/tests are removed, while async operations gain epoch, token, and session-view guards.

Changes

ChatPage decomposition

Layer / File(s) Summary
Scope and scroll ownership cleanup
docs/architecture/chatpage-decomposition/*, src/renderer/src/composables/message/types.ts, test/renderer/composables/chat/chatScrollArchitecture.test.ts
Architecture plans document the decomposition and race-token rules; obsolete scroll types and scroll composable test allowances are removed.
Display, input, and viewport composables
src/renderer/src/pages/chat-page/useDisplayMessages.ts, useChatSearch.ts, useListGestures.ts, useMessageVirtualization.ts, src/renderer/src/pages/ChatPage.vue
Message rendering, placeholder lifecycles, search highlighting, gesture intent, virtualization, measurement batching, and anchor restoration are extracted and wired into ChatPage.
Session restore and plan lifecycles
src/renderer/src/pages/chat-page/useSessionRestore.ts, usePlanFloatLifecycle.ts, src/renderer/src/pages/ChatPage.vue
Session restore uses request epochs and active-view checks; plan snapshots use per-session linger state and delayed clearing.
Composer submission and async guards
src/renderer/src/pages/chat-page/useComposerSubmit.ts, src/renderer/src/pages/ChatPage.vue
Send, queue, steer, compaction, attachment filtering, optimistic feedback, and composer state clearing are centralized with session-view and attachment tokens.

Estimated code review effort: 4 (Complex) | ~60 minutes

Sequence Diagram(s)

sequenceDiagram
  participant ChatPage
  participant useSessionRestore
  participant SessionStore
  participant MessageStore
  ChatPage->>useSessionRestore: schedule session restore
  useSessionRestore->>SessionStore: apply session summary
  useSessionRestore->>MessageStore: load messages
  useSessionRestore->>useSessionRestore: validate epoch and active view
  useSessionRestore-->>ChatPage: expose restored state
Loading

Possibly related PRs

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 16.92% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly summarizes the main change: refactoring ChatPage by decomposing it into composables.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch refactor/chatpage-decomposition

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 5

🧹 Nitpick comments (1)
src/renderer/src/pages/chat-page/useDisplayMessages.ts (1)

45-45: 📐 Maintainability & Code Quality | 🔵 Trivial | 🏗️ Heavy lift

Align the new feature-local composables with the renderer module-location rule.

All seven modules are introduced under pages/chat-page, outside the listed responsibility directories. Relocate them to an approved directory, or update the repository rule if feature-local page modules are intentional.

  • src/renderer/src/pages/chat-page/useDisplayMessages.ts#L45-L45: relocate useDisplayMessages.
  • src/renderer/src/pages/chat-page/useChatSearch.ts#L31-L31: relocate useChatSearch.
  • src/renderer/src/pages/chat-page/useListGestures.ts#L31-L31: relocate useListGestures.
  • src/renderer/src/pages/chat-page/useMessageVirtualization.ts#L38-L38: relocate useMessageVirtualization.
  • src/renderer/src/pages/chat-page/useSessionRestore.ts#L29-L29: relocate useSessionRestore.
  • src/renderer/src/pages/chat-page/usePlanFloatLifecycle.ts#L32-L32: relocate usePlanFloatLifecycle.
  • src/renderer/src/pages/chat-page/useComposerSubmit.ts#L84-L84: relocate useComposerSubmit.

As per coding guidelines, “In renderer app code, organize modules under components/, stores/, views/, i18n/, and lib/ according to their responsibility.”

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/renderer/src/pages/chat-page/useDisplayMessages.ts` at line 45,
Feature-local composables are placed outside the renderer’s approved
responsibility directories. Relocate useDisplayMessages in
src/renderer/src/pages/chat-page/useDisplayMessages.ts#L45-L45, useChatSearch in
src/renderer/src/pages/chat-page/useChatSearch.ts#L31-L31, useListGestures in
src/renderer/src/pages/chat-page/useListGestures.ts#L31-L31,
useMessageVirtualization in
src/renderer/src/pages/chat-page/useMessageVirtualization.ts#L38-L38,
useSessionRestore in
src/renderer/src/pages/chat-page/useSessionRestore.ts#L29-L29,
usePlanFloatLifecycle in
src/renderer/src/pages/chat-page/usePlanFloatLifecycle.ts#L32-L32, and
useComposerSubmit in
src/renderer/src/pages/chat-page/useComposerSubmit.ts#L84-L84 into appropriate
components/, stores/, views/, i18n/, or lib/ directories according to
responsibility, updating all imports; alternatively, update the repository rule
only if feature-local page modules are intentionally supported.

Source: Coding guidelines

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@docs/architecture/chatpage-decomposition/plan.md`:
- Around line 31-32: 更新该架构拆分计划中的回滚说明,移除“任一步回滚不影响其他步骤”的无条件表述;明确 composable
提取提交存在依赖关系,必须按提交的逆序回滚,或同时回滚所有依赖该 composable 的后续提交,避免破坏导入和编排。

In `@src/renderer/src/pages/chat-page/useComposerSubmit.ts`:
- Around line 154-176: Update prepareFilesForCurrentModel to capture the active
provider/model identity before awaiting getCapabilities, then recheck it against
the current selection after the await and before filtering or notifying. If the
selection changed, discard the stale capability result and retry using the
current selection, preserving the existing behavior for unchanged selections and
failures.

In `@src/renderer/src/pages/chat-page/useDisplayMessages.ts`:
- Around line 413-440: The message-building flow should not append an assistant
placeholder after adding the fallback streaming row. Track whether the `else if`
branch that pushes `toStreamingMessage` for `messageStore.streamingBlocks` ran,
and only evaluate the pending or generating placeholder branches when it did
not.

In `@src/renderer/src/pages/chat-page/useListGestures.ts`:
- Around line 219-223: Update resetIntentForSessionChange to reset the complete
gesture lifecycle, including cancelling or clearing the old session’s timer and
resetting isListGestureActive and isListScrolling, in addition to the existing
intent fields. Ensure no pending old-session callback can affect the new
scroll-controller session.

In `@src/renderer/src/pages/chat-page/useSessionRestore.ts`:
- Around line 54-70: Update restoreSessionMessages to handle rejections from
both loadMessagesForSession and pendingInputStore.loadPendingInputs. Catch
failures from the primary restore without leaving runRestore’s promise rejected,
and add rejection handling to the pendingInputsPromise chain while preserving
the requestId guard for onSecondaryStateReady and reporting or recovering from
each failure.

---

Nitpick comments:
In `@src/renderer/src/pages/chat-page/useDisplayMessages.ts`:
- Line 45: Feature-local composables are placed outside the renderer’s approved
responsibility directories. Relocate useDisplayMessages in
src/renderer/src/pages/chat-page/useDisplayMessages.ts#L45-L45, useChatSearch in
src/renderer/src/pages/chat-page/useChatSearch.ts#L31-L31, useListGestures in
src/renderer/src/pages/chat-page/useListGestures.ts#L31-L31,
useMessageVirtualization in
src/renderer/src/pages/chat-page/useMessageVirtualization.ts#L38-L38,
useSessionRestore in
src/renderer/src/pages/chat-page/useSessionRestore.ts#L29-L29,
usePlanFloatLifecycle in
src/renderer/src/pages/chat-page/usePlanFloatLifecycle.ts#L32-L32, and
useComposerSubmit in
src/renderer/src/pages/chat-page/useComposerSubmit.ts#L84-L84 into appropriate
components/, stores/, views/, i18n/, or lib/ directories according to
responsibility, updating all imports; alternatively, update the repository rule
only if feature-local page modules are intentionally supported.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: b4475df8-033a-449a-91a2-43f7667319f5

📥 Commits

Reviewing files that changed from the base of the PR and between b6625d2 and 62d8639.

📒 Files selected for processing (15)
  • docs/architecture/chatpage-decomposition/plan.md
  • docs/architecture/chatpage-decomposition/spec.md
  • docs/architecture/chatpage-decomposition/tasks.md
  • src/renderer/src/composables/message/types.ts
  • src/renderer/src/composables/message/useMessageScroll.ts
  • src/renderer/src/pages/ChatPage.vue
  • src/renderer/src/pages/chat-page/useChatSearch.ts
  • src/renderer/src/pages/chat-page/useComposerSubmit.ts
  • src/renderer/src/pages/chat-page/useDisplayMessages.ts
  • src/renderer/src/pages/chat-page/useListGestures.ts
  • src/renderer/src/pages/chat-page/useMessageVirtualization.ts
  • src/renderer/src/pages/chat-page/usePlanFloatLifecycle.ts
  • src/renderer/src/pages/chat-page/useSessionRestore.ts
  • test/renderer/composables/chat/chatScrollArchitecture.test.ts
  • test/renderer/composables/useMessageScroll.test.ts
💤 Files with no reviewable changes (4)
  • test/renderer/composables/useMessageScroll.test.ts
  • src/renderer/src/composables/message/useMessageScroll.ts
  • src/renderer/src/composables/message/types.ts
  • test/renderer/composables/chat/chatScrollArchitecture.test.ts

Comment thread docs/architecture/chatpage-decomposition/plan.md Outdated
Comment thread src/renderer/src/pages/chat-page/useComposerSubmit.ts
Comment thread src/renderer/src/pages/chat-page/useDisplayMessages.ts
Comment thread src/renderer/src/pages/chat-page/useListGestures.ts
Comment thread src/renderer/src/pages/chat-page/useSessionRestore.ts
@zhangmo8

Copy link
Copy Markdown
Collaborator Author

Review response

This PR is a behavior-zero-drift decomposition — every concern was moved line-for-line out of ChatPage.vue into a composable, with no logic change. I verified each finding against the base commit (8b121ede8) before acting. Summary of dispositions:

Actionable comments — verified as pre-existing, intentionally preserved (no change)

All five describe behavior that already existed on the base and was faithfully carried over. "Fixing" any of them here would introduce the exact behavior drift this refactor is meant to avoid; they're better raised as separate hardening PRs.

  1. useComposerSubmitprepareFilesForCurrentModel doesn't re-check selection after await getCapabilities (L154-176). Confirmed identical to base ChatPage.vue:2371-2390 — same await, no post-await re-check. Not introduced here.
  2. useDisplayMessages — placeholder appended after fallback streaming row (L413-440). Byte-for-byte equal to base streamingDisplayTail (ChatPage.vue:1456-1491). The two placeholder branches are independently gated by shouldShowPendingAssistantPlaceholder / shouldShowGeneratingAssistantPlaceholder; the fallback-row branch and the placeholder branches are guarded by disjoint conditions (ephemeralRateLimitBlock, hasInlineStreamingTarget). No behavior change.
  3. useListGesturesresetIntentForSessionChange doesn't reset the full gesture lifecycle (L219-223). Base resets exactly these three intent fields on session change (pendingHistoryLoadAtIdle, hasUpwardPaginationIntent, lastGestureClientY) and intentionally does not touch isListGestureActive or timers — the method name says "intent", not "lifecycle". Faithful move.
  4. useSessionRestore — unhandled rejections from loadMessages / loadPendingInputs (L54-70). Base restoreSessionMessages (ChatPage.vue:395-412) has no try/catch and no .catch() on the pending-inputs .then() either. Identical.

Doc / structure comments — addressed

  1. plan.md rollback wording — valid catch, my wording was too absolute. Updated: composable-extraction commits have ordering dependencies, so rollback must go in reverse commit order (or revert dependents together), not step-independently. (commit 5d055fb)
  2. Nitpick: composables live under pages/chat-page/ rather than lib//components/ — intentional feature-local layout. These seven modules are ChatPage.vue-private, tightly coupled to page props and page-level store composition, and not meant for reuse; co-locating makes ownership obvious and prevents other pages from importing them as if they were generic utilities. lint:architecture passes (no hard rule on this layout). Documented the decision in spec.md; will promote to lib/ if a second page ever needs one. (commit 5d055fb)

Validation

typecheck (node+web), lint, format:check, i18n all green; ChatPage.test.ts 82/82. An independent line-by-line review against the base also caught and fixed one genuine reactivity regression that had crept in mid-branch (plan-float linger flag downgraded from ref to a plain object → float vanished instead of lingering 1200ms), already fixed in d9fa944c.

@zhangmo8
zhangmo8 merged commit 0ab4d57 into dev Jul 17, 2026
4 checks passed
@zhangmo8
zhangmo8 deleted the refactor/chatpage-decomposition branch July 17, 2026 01:26
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant