diff --git a/docs/features/markstream-chat-rendering-optimization/plan.md b/docs/features/markstream-chat-rendering-optimization/plan.md new file mode 100644 index 000000000..c0297d233 --- /dev/null +++ b/docs/features/markstream-chat-rendering-optimization/plan.md @@ -0,0 +1,98 @@ +# MarkStream Chat Rendering Optimization Plan + +## Chosen approach + +Use MarkStream's own chat-mode and performance presets inside each Markdown message, while keeping DeepChat's existing outer message windowing and row behavior. This gives the main performance gain where Markdown is expensive without breaking chat-level features that depend on the current DOM/scroll model. + +## Current path + +`ChatPage` builds display messages, `MessageList` renders rows, assistant rows render `MessageBlockContent`, and text parts render `MarkdownRenderer`, which wraps `markstream-vue`'s `NodeRenderer` with DeepChat custom components. + +## Rendering strategy + +### Streaming / live content + +- Keep `mode="chat"` stable. +- Pass `final=false` while the block or part is still loading. +- Use `smooth-streaming="auto"` unless the caller disables smooth streaming. +- Enable `typewriter` and `code-block-stream` for the generating message. +- Use incremental rendering by setting live node limit to `0` while streaming. +- Tune MarkStream batch props for small frame-friendly chunks: + - small initial/render batch sizes; + - short render delay; + - low per-frame budget; + - idle timeout for catch-up. +- Keep `fade=false` to avoid repeated opacity animation during streaming updates. + +### Completed / static content + +- Pass `final=true` after generation completes. +- Disable smooth-streaming/typewriter/code-block-stream. +- Use MarkStream node virtualization and viewport priority for completed long Markdown: + - `node-virtual="auto"` when allowed; + - `max-live-nodes` and `live-node-buffer` bounded; + - deferred offscreen nodes; + - viewport-priority rendering. +- Keep the custom code block, Mermaid, reference, and link mappings registered through scoped `custom-id`. + +### Compatibility guardrails + +- Inline chat search disables Markdown node virtualization for all rendered messages while search is open. +- Search-result messages always disable Markdown node virtualization. +- Streaming content disables Markdown node virtualization even if the caller allows virtualization. +- Message capture keeps using the complete display message list through `allMessagesForCapture`. +- Session switch clears message-window measurements before loading the new session. + +## Affected interfaces + +- `MarkdownRenderer.vue` + - `streaming?: boolean` + - `final?: boolean` + - `virtualizeNodes?: boolean` + - internal MarkStream tuning constants for streaming and completed content. +- `MessageBlockContent.vue` + - derives streaming/final state from block status and processed part loading; + - passes virtualization guard to MarkdownRenderer. +- `MessageItemAssistant.vue`, `MessageListRow.vue`, `MessageList.vue` + - pass through `disableMarkdownVirtualization`. +- `ChatPage.vue` + - disables Markdown virtualization while inline chat search is open; + - clears row measurements during session switch. + +## Test strategy + +Focused tests should assert both behavior and configuration: + +- `MarkdownRenderer.test.ts` + - static completed defaults; + - streaming configuration; + - smooth streaming opt-out; + - node virtualization opt-out; + - custom preview/reference behavior remains wired. +- `MessageBlockContent.test.ts` + - pending/loading parts pass streaming/final state; + - completed parts pass final/static state; + - search-result and parent disable flags disable node virtualization. +- `MessageList.test.ts` + - disable flag propagates through list rows. +- `ChatPage.test.ts` + - chat search disables markdown virtualization; + - session switch clears measurements. + +Project checks before handoff: + +- `pnpm run format` +- `pnpm run i18n` +- `pnpm run lint` +- `pnpm run typecheck:web` +- focused renderer/list/page Vitest command. + +## Self-review loop + +After implementation: + +1. Inspect `git diff` for accidental unrelated changes. +2. Re-read the render path and verify streaming/static/search/capture/jump invariants. +3. Run focused tests and project checks. +4. Fix any issues and repeat diff/test review until clean. +5. Push the branch and update the PR body with concrete behavior and validation. diff --git a/docs/features/markstream-chat-rendering-optimization/spec.md b/docs/features/markstream-chat-rendering-optimization/spec.md new file mode 100644 index 000000000..a13ebd2a6 --- /dev/null +++ b/docs/features/markstream-chat-rendering-optimization/spec.md @@ -0,0 +1,46 @@ +# MarkStream Chat Rendering Optimization Spec + +## User need + +DeepChat's assistant messages can stream long Markdown responses with code blocks, Mermaid diagrams, tables, references, artifacts, and search highlights. Rendering must feel smooth while content is generating and stay fast when browsing completed long conversations. + +## Goal + +Build a high-performance, high-experience Markdown rendering path using `markstream-vue` for both streaming and completed chat content while preserving DeepChat's current behavior. + +The optimized path remains: + +`ChatPage` → `MessageList` / `MessageListRow` → `MessageItemAssistant` → `MessageBlockContent` → `MarkdownRenderer` → `markstream-vue` `NodeRenderer` + +## Acceptance criteria + +- Streaming assistant text blocks pass explicit live state to `markstream-vue` (`mode="chat"`, `final=false`) and use MarkStream's streaming-friendly features. +- Completed assistant text blocks pass `final=true` and use MarkStream's completed-content virtualization/deferral features for long Markdown. +- Streaming Markdown uses incremental rendering settings that prioritize frame budget and smooth typing cadence instead of large per-token DOM commits. +- Static/completed Markdown uses a virtualized node window for long documents to preserve scrollback responsiveness and memory usage. +- Code blocks remain Monaco/custom-renderer backed and support DeepChat artifact preview behavior. +- Mermaid, references, links, artifact previews, tool calls, message capture, spotlight jump, scroll anchoring, and inline chat search remain compatible. +- Chat search and search-result messages keep Markdown DOM available by disabling node virtualization where DOM search/highlighting needs it. +- Session changes reset message height measurements so old session row heights cannot affect new session windowing. +- The implementation is explicit, documented, and covered by focused renderer/list/page tests. +- The PR description clearly explains the concrete performance/UX behavior, non-goals, and validation. + +## Constraints + +- Use `markstream-vue`; do not replace it with another renderer. +- Prefer focused changes in the existing render chain and avoid broad unrelated refactors. +- Do not replace the outer `MessageList` with `MarkstreamVirtualTimeline` in this slice; DeepChat's row-level tool/action/artifact/search/capture/jump/anchor behaviors need separate design before an outer virtual timeline can safely own them. +- Keep `fade=false` to avoid opacity restart flicker during high-frequency streaming. +- Preserve Monaco-backed code block rendering and artifact preview behavior. +- Do not claim benchmark numbers unless measured in this task. + +## Non-goals + +- Full outer chat-list virtualization. +- Redesign of DOM-based chat search/highlight. +- New performance instrumentation UI. +- Replacing DeepChat's custom code block/artifact preview components. + +## Open questions + +None for this implementation slice. diff --git a/docs/features/markstream-chat-rendering-optimization/tasks.md b/docs/features/markstream-chat-rendering-optimization/tasks.md new file mode 100644 index 000000000..c615bf899 --- /dev/null +++ b/docs/features/markstream-chat-rendering-optimization/tasks.md @@ -0,0 +1,17 @@ +# MarkStream Chat Rendering Optimization Tasks + +- [x] Create SDD spec, plan, and task documents. +- [x] Reconfirm MarkStream Vue props and existing focused tests. +- [x] Add streaming/final MarkStream option handling in `MarkdownRenderer.vue`. +- [x] Pass explicit streaming/final state from `MessageBlockContent.vue`. +- [x] Enable streaming code-block mode for live Markdown. +- [x] Add a Markdown node virtualization guard for chat search/search-result messages. +- [x] Clear message height measurements on session switch. +- [x] Update focused component tests for the first pass. +- [x] Tune MarkStream streaming batch settings for smoother live rendering. +- [x] Tune completed-node virtualization settings for responsive static scrollback. +- [x] Add/adjust tests for batch tuning props and compatibility guardrails. +- [x] Run self-review pass for render-path invariants and unrelated changes. +- [x] Run focused renderer/list/page tests. +- [x] Run format, i18n, lint, and typecheck:web. +- [x] Push branch and update PR description. diff --git a/docs/issues/chat-send-jitter/spec.md b/docs/issues/chat-send-jitter/spec.md new file mode 100644 index 000000000..3a580cc8d --- /dev/null +++ b/docs/issues/chat-send-jitter/spec.md @@ -0,0 +1,44 @@ +# Chat send jitter + +## Issue + +When the user sends a message while the chat is pinned to the bottom, the message list can visibly jump for one frame immediately after submit or flash when the first assistant chunk starts rendering. + +## Impact + +The MarkStream rendering work feels smoother during generation, but the first outgoing-turn UI updates can still feel unstable because the list scroll position is corrected twice and the pending assistant row can be replaced by a different keyed DOM node at stream start. + +## Suspected root cause + +`ChatPage` inserts both an optimistic user message and an empty pending assistant placeholder before the backend starts streaming. `useMessageWindow` estimates an empty pending assistant row as a normal assistant message (`ASSISTANT_BASE`, 136px), while the real DOM for that row is only the assistant header plus spinner. `MessageListRow` then reports the smaller measured height on the next animation frame, and `ChatPage.onMessageMeasure` scrolls to the bottom again, causing a visible submit-time adjustment. + +At stream start, `chat.stream.updated` can set `isStreaming` before the real assistant record is available. The old placeholder was hidden immediately on `isStreaming`, leaving a one-frame gap. When the real assistant message then appears, `MessageList` keyed it by the real message id instead of the pending placeholder id, unmounting the spinner row and mounting a new assistant row. + +## Fix plan + +- Teach `useMessageWindow` to estimate empty pending assistant placeholders close to their actual spinner row height. +- Keep the change narrowly scoped to synthetic pending assistant rows so real assistant messages keep their existing estimates. +- Keep the pending assistant placeholder visible until the first real streaming assistant content exists. +- Carry the pending placeholder render key onto the first real assistant row so Vue patches the existing row instead of replacing it. +- Add regression tests covering the placeholder estimate, measurement delta, and stream-start row transition. + +## Task checklist + +- [x] Link GitHub bug issue +- [x] Update placeholder height estimate +- [x] Keep placeholder stable through stream-start +- [x] Add regression coverage +- [x] Run focused tests and required checks +- [x] Commit and push the PR branch + +## Validation + +- `pnpm exec vitest --config vitest.config.renderer.ts test/renderer/composables/useMessageWindow.test.ts test/renderer/components/MessageListRow.test.ts test/renderer/components/ChatPage.test.ts` +- `pnpm run format` +- `pnpm run i18n` +- `pnpm run lint` +- `pnpm run typecheck:web` + +## Linked GitHub issue + +https://github.com/ThinkInAIXYZ/deepchat/issues/1897 diff --git a/package.json b/package.json index 0e9f97ae4..523a087f7 100644 --- a/package.json +++ b/package.json @@ -190,7 +190,7 @@ "electron-vite": "5.0.0", "jsdom": "^26.1.0", "katex": "^0.16.47", - "markstream-vue": "1.0.5-beta.2", + "markstream-vue": "^1.0.5", "mermaid": "^11.15.0", "minimatch": "^10.2.5", "monaco-editor": "^0.55.1", @@ -199,7 +199,7 @@ "picocolors": "^1.1.1", "pinia": "^3.0.4", "reka-ui": "^2.9.7", - "stream-monaco": "^0.0.45", + "stream-monaco": "^0.0.46", "tailwind-merge": "^3.6.0", "tailwind-scrollbar-hide": "^4.0.0", "tailwindcss": "^4.3.0", diff --git a/src/renderer/src/components/artifacts/MarkdownArtifact.vue b/src/renderer/src/components/artifacts/MarkdownArtifact.vue index 3e31f83fa..ea6009461 100644 --- a/src/renderer/src/components/artifacts/MarkdownArtifact.vue +++ b/src/renderer/src/components/artifacts/MarkdownArtifact.vue @@ -5,6 +5,9 @@ > diff --git a/src/renderer/src/components/chat/MessageList.vue b/src/renderer/src/components/chat/MessageList.vue index e9e9214d5..6f251df4f 100644 --- a/src/renderer/src/components/chat/MessageList.vue +++ b/src/renderer/src/components/chat/MessageList.vue @@ -8,12 +8,13 @@ />