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
150 changes: 150 additions & 0 deletions docs/issues/markdown-codeblock-scrollbar-jitter/plan.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,150 @@
# Markdown Codeblock Scrollbar Jitter Plan

## Current Layout

```text
Before

Chat viewport
- message-list-container
- stable vertical gutter already exists
|
+-- assistant message
|
+-- MarkdownRenderer .prose
|
+-- markstream-vue code block
|
+-- code-editor-container / code-pre-fallback / pre
- overflow: auto
- wrapping may be implicit
- internal scrollbar can appear/disappear while settling
- code content area shifts on Windows
```

```text
After

Chat viewport
- unchanged
|
+-- assistant message
|
+-- MarkdownRenderer .prose
|
+-- markstream-vue code block
|
+-- stable internal scrollport
- scrollbar space is reserved or otherwise kept stable
- code wrapping is explicit for Monaco rendering
- fallback/enhanced renderer swaps do not move content
```

## Design

Implement the first fix as a scoped renderer code block stabilization in the app-owned Markdown
renderer.

Recommended target file:

- `src/renderer/src/components/markdown/MarkdownRenderer.vue`

Candidate targets:

- `.markstream-vue [data-markstream-code-block='1']`
- `.markstream-vue [data-markstream-code-block='1'] .code-editor-container`
- `.markstream-vue [data-markstream-code-block='1'] .code-pre-fallback`
- `.markstream-vue pre[class^='language-']`
- `.markstream-vue pre[class*=' language-']`

Preferred first increment:

1. Pass `wordWrap: 'on'` explicitly to Markdown code block Monaco rendering so the app does not
rely on an implicit dependency default.
2. Add `scrollbar-gutter: stable;` to the inner code block scrollports that can create vertical
classic scrollbars.
3. Keep `overflow: auto` so scrollbars are still demand-driven.
4. Do not force `overflow-x: scroll`; default code blocks should wrap.
5. Validate the issue video scenario on Windows.
6. If a horizontal scrollbar still appears in default wrapped mode, identify which renderer path is
bypassing wrap before adding any scrollbar fallback.

Avoid these first:

- Do not add another outer `scrollbar-gutter` to `ChatPage.vue`; it already exists.
- Do not set global `overflow-y: scroll`.
- Do not force permanent horizontal scrollbars in default wrapped code blocks.
- Do not apply broad scrollbar rules to all `.prose` content.

## Compatibility

- The reported runtime is Electron/Chromium on Windows. Modern Chromium supports
`scrollbar-gutter`.
- macOS overlay scrollbars should not show meaningful visual changes because overlay scrollbars do
not consume layout gutter space.
- Linux classic scrollbar behavior should benefit from the same scoped stabilization.

## Risk Areas

- Horizontal scrollbar appearance in default wrapped mode would indicate a renderer path bypassing
wrapping; it must be verified on Windows with classic scrollbars.
- `markstream-vue` uses scoped compiled CSS, so app overrides need enough selector specificity.
- Code block fallback and enhanced editor layers overlap during streaming; a fix must avoid clipping
either layer.
- Permanent horizontal scrollbars would be noisy and conflict with the expected default wrapping
behavior.

## Test Strategy

Automated checks:

- Add or update a renderer test around `MarkdownRenderer` if implementation introduces a stable app
class or data attribute that can be asserted in jsdom.
- Add a browser-level regression check when practical:
- mount a Markdown response with a streaming code block;
- append content until the code block crosses the overflow threshold;
- measure the code block rect and first code line rect before and after the scrollbar transition;
- fail if position changes by more than 1 px.

Manual Windows validation:

1. Run the app on Windows 11.
2. Open a chat and stream a response containing a long fenced code block with enough lines to make
the block overflow.
3. Watch the code block during streaming and completion.
4. Confirm normal long code wraps by default and does not show a permanent horizontal scrollbar.
5. Confirm the outer chat scrollbar, sticky input, and auto-follow behavior are unchanged.
6. Repeat once in dark theme.

Quality gates after implementation:

- `pnpm run format`
- `pnpm run i18n`
- `pnpm run lint`
- `pnpm run typecheck`
- Targeted renderer test, for example `pnpm test:renderer -- MarkdownRenderer`

## Validation Fixture

Use Markdown content shaped like the issue video:

````markdown
### Comparison

```java
String sql = "SELECT * FROM users WHERE id = ?";
PreparedStatement ps = conn.prepareStatement(sql);
ps.setInt(1, 1);
ResultSet rs = ps.executeQuery();
while (rs.next()) {
String name = rs.getString("name");
int age = rs.getInt("age");
}

// Repeat enough long lines to exercise wrapping and code block overflow on Windows classic scrollbars.
String veryLongLine = "SELECT first_name, last_name, email, created_at FROM users WHERE account_status = 'active' ORDER BY created_at DESC";
```
````

Expected behavior: the long code line wraps by default. If a real scrollbar is needed for vertical
overflow, the code content and block geometry should not jump while the scrollbar state changes.
98 changes: 98 additions & 0 deletions docs/issues/markdown-codeblock-scrollbar-jitter/spec.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
# Markdown Codeblock Scrollbar Jitter

## Source

- GitHub issue: https://github.com/ThinkInAIXYZ/deepchat/issues/1763
- Reported environment: Windows 11 Home Chinese, DeepChat v1.0.6-beta.5.
- Symptom: assistant Markdown rendering shows visible scrollbar jitter while generated content settles.
- Attached video: `20260612_174024.mp4`, linked from the issue body.
- Related comments:
- https://github.com/ThinkInAIXYZ/deepchat/issues/1763#issuecomment-4706327016
- https://github.com/ThinkInAIXYZ/deepchat/issues/1763#issuecomment-4714279781

## Problem Determination

The issue is most likely not caused by the outer chat viewport scrollbar.

Evidence:

- The issue video shows the visible jump inside a rendered Markdown code block. A horizontal
scrollbar is visible during settling, but default code block rendering is expected to wrap, so the
scrollbar should not be treated as the desired steady state.
- The outer chat container already reserves vertical scrollbar space in `ChatPage.vue` via
`.message-list-container { scrollbar-gutter: stable both-edges; }`.
- `MarkdownRenderer.vue` wraps `markstream-vue`'s `NodeRenderer` in a `.prose` container, but does
not add any scrollbar stability rule for inner Markdown scrollports.
- `markstream-vue@1.0.1-beta.4` renders code blocks through `CodeBlockNode`, including
`.code-block-container`, `.code-editor-container`, `.code-pre-fallback`, and
`pre[class^=language-]` nodes that use `overflow: auto`.
- `markstream-vue` defaults code block word wrapping to enabled when `monacoOptions.wordWrap` is not
set to `off`, and its fallback `.code-pre-fallback.is-wrap` CSS uses wrapping rules.
- On Windows classic scrollbars consume layout space. When an inner code block switches between
fallback and enhanced rendering, or crosses an overflow threshold during streaming, those
internal scrollbars can change the code block's available content area and cause visible jitter.

## Comment Assessment

The comment suggesting `scrollbar-gutter: stable` is directionally reasonable, but it must be
applied to the scrollable Markdown/code block nodes that actually jitter. Applying it only to the
outer chat page would likely be ineffective because the outer container already has a stable gutter.

`overflow-y: scroll` is a weaker fit for this report:

- The captured symptom is inside a Markdown code block, and the most visible scrollbar in the video
is horizontal.
- Forcing vertical scrollbars globally would add permanent scrollbar chrome to many non-problem
containers.
- Forcing a horizontal scrollbar would normalize an unreasonable default state; the first fix should
preserve automatic wrapping and only stabilize real scroll containers.

MDN documents `scrollbar-gutter` as a way to reserve classic scrollbar space for scroll containers,
with `both-edges` mirroring the gutter on inline edges. This supports using it for vertical
scrollbar reflow, but horizontal scrollbar behavior still needs explicit Windows validation.

## User Need

As a Windows user reading a streaming assistant response, I need Markdown code blocks to remain
visually stable while content is rendered, so the response does not look like it is shaking or
reflowing around scrollbars.

## Goals

- Stabilize Markdown code block scrollports on Windows classic scrollbar environments.
- Preserve default automatic wrapping for Markdown code blocks.
- Keep the existing outer chat scroll behavior unchanged.
- Avoid introducing permanent horizontal scrollbars for normal wrapped code.
- Preserve code block rendering in chat messages, artifacts, and workspace Markdown preview.
- Keep the fix scoped to renderer styling unless validation proves the issue is in
`markstream-vue` behavior.

## Acceptance Criteria

- A streaming Markdown response containing a long code block does not visibly jitter when the code
block crosses an overflow threshold.
- The code block container width, block height, and first rendered code line position remain stable
within 1 px while fallback/enhanced code rendering settles.
- Long code lines wrap by default and do not gain a permanent horizontal scrollbar in normal chat
rendering.
- Normal Markdown paragraphs do not gain unwanted permanent scrollbars or extra spacing.
- Chat auto-follow, manual scroll-away, and session restore behavior remain unchanged.
- Markdown artifacts and workspace Markdown previews render without clipped code blocks.
- Light and dark themes keep the current code block visual style.

## Non-goals

- Redesign the Markdown renderer.
- Replace `markstream-vue`.
- Rewrite chat scrolling or virtualization.
- Add a user-facing setting.
- Hide scrollbars with `scrollbar-width: none`, because that weakens discoverability and
accessibility.

## Constraints

- Keep the implementation local to renderer Markdown/code block styling if possible.
- Do not patch files under `node_modules`.
- Prefer CSS overrides in app-owned files over dependency forks.
- Do not force permanent horizontal scrollbars as a default-mode fallback.
- No new runtime dependencies.
34 changes: 34 additions & 0 deletions docs/issues/markdown-codeblock-scrollbar-jitter/tasks.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
# Markdown Codeblock Scrollbar Jitter Tasks

## Investigation

- [x] Read GitHub issue 1763 and current comments.
- [x] Download and inspect the attached issue video.
- [x] Confirm the visible jitter is inside a Markdown code block, not the outer chat viewport.
- [x] Check local chat container styling and confirm the outer viewport already uses
`scrollbar-gutter: stable both-edges`.
- [x] Check `MarkdownRenderer.vue` and `markstream-vue` code block output for inner
`overflow: auto` scrollports.
- [x] Assess the comment recommendation and document the scope correction.

## Implementation

- [x] Make Markdown code block Monaco word wrapping explicit.
- [x] Add scoped CSS stabilization for Markdown code block scrollports.
- [x] Keep normal Markdown paragraphs and non-scrollable blocks unaffected.
- [x] Avoid forcing permanent horizontal scrollbars in default wrapped code blocks.
- [ ] Validate whether `scrollbar-gutter: stable` is enough for the Windows issue video scenario.
- [x] Add or update a focused renderer/browser regression check where practical.

## Verification

- [ ] Run the original issue-style Markdown fixture on Windows 11.
- [ ] Verify chat message code blocks during streaming and after completion.
- [ ] Verify Markdown artifacts.
- [ ] Verify workspace Markdown preview.
- [ ] Verify light and dark themes.
- [x] Run `pnpm run format`.
- [x] Run `pnpm run i18n`.
- [x] Run `pnpm run lint`.
- [x] Run `pnpm run typecheck`.
- [x] Run targeted renderer tests for `MarkdownRenderer`.
15 changes: 13 additions & 2 deletions src/renderer/src/components/markdown/MarkdownRenderer.vue
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
<template>
<div class="prose prose-zinc prose-sm dark:prose-invert w-full max-w-none break-all">
<div
class="markdown-renderer-root prose prose-zinc prose-sm dark:prose-invert w-full max-w-none break-all"
>
<NodeRenderer
:content="debouncedContent"
:custom-id="customRendererId"
Expand Down Expand Up @@ -86,7 +88,8 @@ const codeBlockThemes = ['vitesse-dark', 'vitesse-light'] as const
const codeBlockDarkTheme = codeBlockThemes[0]
const codeBlockLightTheme = codeBlockThemes[1]
const codeBlockMonacoOption = computed(() => ({
fontFamily: uiSettingsStore.formattedCodeFontFamily
fontFamily: uiSettingsStore.formattedCodeFontFamily,
wordWrap: 'on' as const
}))
const { navigateLink } = useMarkdownLinkNavigation({
linkContext: effectiveLinkContext
Expand Down Expand Up @@ -304,6 +307,14 @@ defineEmits(['copy'])
contain: layout style paint;
}

.markstream-vue [data-markstream-code-block='1'],
.markstream-vue [data-markstream-code-block='1'] .code-editor-container,
.markstream-vue [data-markstream-code-block='1'] .code-pre-fallback,
.markstream-vue pre[class^='language-'],
.markstream-vue pre[class*=' language-'] {
scrollbar-gutter: stable;
}

table {
@apply py-0 my-0;
border-collapse: collapse;
Expand Down
6 changes: 6 additions & 0 deletions test/renderer/components/MarkdownRenderer.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -223,6 +223,12 @@ describe('MarkdownRenderer', () => {
)
})

it('marks the root for scoped code block scrollbar stabilization', async () => {
const { wrapper } = await setup()

expect(wrapper.classes()).toContain('markdown-renderer-root')
})

it('passes explicit smooth streaming to NodeRenderer', async () => {
const { wrapper } = await setup({
smoothStreaming: true
Expand Down