Skip to content
Closed
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 AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@
- Conventional commits enforced by hook: `type(scope): subject` ≤ 50 chars; types: `feat|fix|docs|dx|style|refactor|perf|test|workflow|build|ci|chore|types|wip|release`.
- Do not include AI co-authoring footers in commits.
- PRs: clear description, link issues (`Closes #123`), screenshots/GIFs for UI, pass lint/typecheck/tests. Keep changes focused.
- Default PR base is `dev`; use `gh pr create --base dev` for routine feature, bugfix, docs, test, and refactor branches. Target `main` only for `release/<version>` branches following `docs/release-flow.md`.
- UI changes: include BEFORE/AFTER ASCII layout blocks to communicate structure.

## Architecture Notes & Security
Expand Down
18 changes: 18 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,23 @@
# Changelog

## v1.0.5 (2026-06-05)
- Added scheduled tasks, agent progress todos, session transfer, session tape memory, and remote `/agent` commands for more persistent agent workflows
- Added OpenAI-compatible video generation, tool result image previews, remote image delivery, and richer TTS model routing controls
- Added provider configuration import for CC Switch and external tools, plus refreshed bundled provider data to 142 providers and 6,964 models
- Added encrypted SQLite database storage and a safer settings Danger Zone reset flow
- Added the workspace file tree sidebar, richer Git diff rendering, sidebar theme and chat shortcut controls, and a cleaner new-thread input transition
- Improved chat readability and performance with automatic activity collapsing, merged activity groups, content-visibility message windowing, and smoother streaming
- Improved ACP v1 and remote-control reliability with stronger session handling, diagnostics, alias resolution, media delivery, and working-directory errors
- Fixed macOS foreground identity, provider capability handling, browser recovery errors, startup warning noise, floating button persistence, and session list behavior
- 新增定时任务、Agent 进度 todo、会话转移、Session Tape Memory 和远程 `/agent` 命令,让 Agent 工作流更持久可控
- 新增 OpenAI 兼容视频生成、工具结果图片预览、远程图片投递和更完整的 TTS 模型路由控制
- 新增 CC Switch 与外部工具的 Provider 配置导入,并刷新内置 Provider 数据至 142 个 Provider、6,964 个模型
- 新增 SQLite 数据库加密存储,并让设置里的 Danger Zone 重置流程更安全
- 新增工作区文件树侧栏、更丰富的 Git diff 渲染、侧栏主题与聊天快捷键控制,以及更清爽的新会话输入框过渡
- 通过活动自动折叠、活动组合并、content-visibility 消息窗口和更平滑的流式渲染,提升聊天可读性与性能
- 提升 ACP v1 与远程控制可靠性,强化会话处理、诊断、别名解析、媒体投递和工作目录错误处理
- 修复 macOS 前台身份、Provider 能力处理、浏览器恢复错误、启动告警噪声、浮动按钮位置持久化和会话列表行为

## v1.0.5-beta.8 (2026-06-02)
- Added a collapsible workspace file tree sidebar and an animated theme toggle in the app sidebar
- Added automatic chat activity collapsing so completed reasoning and tool-call work stays easier to scan
Expand Down
143 changes: 143 additions & 0 deletions docs/architecture/chat-scroll-windowing/plan.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,143 @@
# Chat Scroll Windowing Plan

## Architecture Direction

Use CSS `content-visibility: auto` for browser-native render skipping, combined with a custom layout model for anchor positioning and future minimap support.

This approach was chosen over DOM-removal windowing because:
- All message DOM nodes remain present → stable anchors for minimap, search, trace jumps
- Browser handles render skipping natively → no rAF batching overhead, no streaming delays
- No spacer elements or virtual range calculations → simpler code, fewer bugs
- `contain-intrinsic-size` provides size hints → smooth scrollbar, no blank gaps

## Four Layers

```text
1. Message data layer
Loaded message records and stable display-message conversion.

2. Layout model layer (useMessageWindow)
Per-message estimated/measured height, logical top/bottom offsets.
Used for anchor jumps, scroll-to-entry, and future minimap coordinates.

3. CSS render optimization layer (MessageListRow)
content-visibility: auto + contain-intrinsic-size on each row.
Browser skips painting off-screen heavy content (markdown, code, mermaid).

4. Scroll state layer (ChatPage)
Initial bottom positioning, auto-follow, anchored-reading, and manual jump behavior.
```

## Scroll Modes

```ts
type ScrollMode =
| 'initial-bottom' // opening/switching session → always land at bottom
| 'auto-follow' // generation + autoScrollEnabled → follow bottom
| 'anchored-reading' // user scrolled away or autoScroll disabled → preserve position
| 'manual-jump' // search/trace/spotlight jump → scroll to target
```

### Transitions

- Session open → `initial-bottom` (always, regardless of `autoScrollEnabled`)
- `initial-bottom` + first message change → `auto-follow`
- `auto-follow` + user scrolls up → `anchored-reading`
- `anchored-reading` + user scrolls to bottom → `auto-follow` (if `autoScrollEnabled`)
- Any mode + search/trace jump → `manual-jump`
- `manual-jump` + scroll settles → `anchored-reading` or `auto-follow`

## CSS content-visibility Strategy

Each `MessageListRow` has:
```css
.message-list-row {
content-visibility: auto;
contain-intrinsic-size: auto 300px;
}
```

The browser uses `contain-intrinsic-size` as a placeholder height for off-screen rows. Once a row is rendered, the browser remembers its actual height. This means:
- Scrollbar thumb stays accurate
- No blank gaps during fast scrolling
- Heavy markdown/code/mermaid content is only painted when near the viewport

The streaming last row is forced visible:
```css
[data-generating='true'] .message-list-row:last-child {
content-visibility: visible;
}
```

## Layout Model (useMessageWindow)

The composable maintains a pure data model for every loaded message:
- `entries`: per-message `{ id, orderSeq, estimatedHeight, measuredHeight, top, bottom }`
- `totalHeight`: sum of all message heights
- `getEntry(messageId)`: lookup for jump targets
- `setMeasuredHeight(messageId, height)`: update from ResizeObserver measurements

This model is used for:
- Anchor jumps (scroll to estimated position, then refine after render)
- Line-of-sight preservation (capture anchor before height changes, restore after)
- Future minimap (consume `entries` for position mapping)

## Single-Track Streaming

Streaming output is folded into the persisted message record in place rather than
rendered as a separate trailing row, so the generating message and the finished
message share the same id and DOM node:

- `displayMessages`: the single render track for both persisted and streaming messages
- Live streaming blocks are merged into their message record via
`applyStreamingBlocksToMessage`, so updates mutate the existing record's content
- During streaming, the last row has `content-visibility: visible` forced for smooth painting
- A virtual streaming row is only appended when the record is not yet in the store
(`hasInlineStreamingTarget` guard), preventing the same content rendering twice
after a mid-stream `loadMessages`

When streaming completes:
1. `onStreamCompleted` swaps the record's content in place — no `clearStreamingState()` +
`loadMessages()` remount, so the DOM node stays stable (no completion flash / blank gap)
2. Measurement updates (`setMeasuredHeight` / ResizeObserver) apply to the same stable
DOM node rather than a swapped row

## Scroll-to-Bottom

Simple, state-machine-driven:
```ts
function scrollToBottom(force = false) {
if (force) {
markProgrammaticScroll(500)
scrollMode = 'initial-bottom'
shouldAutoFollow = true
} else if (!autoScrollEnabled || !shouldAutoFollow) {
return // respect user's reading position
}
nextTick(() => scrollDomToBottom())
}
```

No rAF batching in scrollToBottom itself — the browser's native scroll coalescing handles this. The `nextTick` ensures DOM is updated before scrolling.

## Line-of-Sight Preservation

When heights change and we're not in auto-follow mode:
1. `captureViewportAnchor()` → `{ messageId, viewportOffset }`
2. Apply height changes
3. `scheduleViewportAnchorRestore(anchor)` → rAF → adjust `scrollTop` to keep anchor at same viewport position

## Long Chat First Load

Bottom-first phased loading (unchanged from existing behavior):
1. Load latest 40 messages → render → position at bottom
2. Make input interactive immediately
3. Older history loads on scroll-to-top (infinite scroll)

## Compatibility

### Search and trace jumps
Use layout model `getEntry(messageId)` to estimate position, scroll there, then refine after render.

### Future minimap
Consume `useMessageWindow.entries` for `messageId → top/bottom/height/status/role` mapping. No DOM queries needed.
191 changes: 191 additions & 0 deletions docs/architecture/chat-scroll-windowing/spec.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,191 @@
# Chat Scroll Windowing Specification

## User Need

DeepChat's chat page must remain fast and smooth for long conversations while preserving reliable message anchors for future features such as a chat minimap. The solution must not use a fully opaque virtual list model that makes anchor scrolling, search jumps, trace jumps, or minimap positioning depend on whether a message currently exists in the DOM.

## Goal

Design a chat-specific windowed rendering and scroll model that provides virtual-list-like performance without sacrificing stable message addressing, bottom-first chat behavior, user-controlled auto-scroll behavior, or smooth streaming output.

## Current Context

The current chat page renders through this path:

```text
ChatTabView
-> ChatPage
-> MessageList
-> MessageListRow
-> MessageItemUser / MessageItemAssistant
-> MessageBlockContent
-> MarkdownRenderer
```

Relevant current files:

- `src/renderer/src/views/ChatTabView.vue`
- `src/renderer/src/pages/ChatPage.vue`
- `src/renderer/src/components/chat/MessageList.vue`
- `src/renderer/src/components/chat/MessageListRow.vue`
- `src/renderer/src/components/message/MessageItemAssistant.vue`
- `src/renderer/src/components/message/MessageBlockContent.vue`
- `src/renderer/src/components/markdown/MarkdownRenderer.vue`
- `src/renderer/src/stores/ui/message.ts`
- `src/renderer/src/stores/ui/stream.ts`
- `src/renderer/src/stores/uiSettingsStore.ts`

Important existing behavior and risks:

- `ChatPage` currently has a virtual-list path in `MessageList`, but virtualization is effectively disabled by `MESSAGE_VIRTUALIZATION_THRESHOLD = Number.POSITIVE_INFINITY`.
- Full DOM rendering causes long conversations, Markdown rendering, code blocks, Mermaid, artifact parsing, tool-call blocks, and layout reads/writes to accumulate cost.
- Streaming currently updates reactive stream state and also applies streaming blocks into the message cache, causing repeated conversion, parsing, markdown rendering, scroll updates, and layout work.
- The UI setting `autoScrollEnabled` exists in `useUiSettingsStore()` and must be respected by any new scroll model.

## Required Behavior

### 1. Bottom-first chat entry

When the user opens an existing chat session, the page should quickly show the latest part of the conversation and land at the bottom.

This initial bottom positioning is distinct from the auto-scroll setting:

- Opening a chat should default to the bottom so users can see the latest context.
- This behavior should not be disabled merely because `autoScrollEnabled` is false.

### 2. Respect auto-scroll setting during generation

The existing `autoScrollEnabled` setting controls generation-time following behavior.

When `autoScrollEnabled` is true:

- During generation/streaming, the chat view should follow the bottom.
- Streaming content growth should be coalesced into efficient bottom-follow updates.
- The user should see new output without manual scrolling.

When `autoScrollEnabled` is false:

- Streaming/generation must not pull the user to the bottom.
- The user's current reading position, or "line of sight", should remain stable.
- Streaming output may continue below the viewport, but the viewport should not jump.

### 3. Preserve line of sight

The scroll system must be able to identify and preserve the user's current viewport anchor when auto-follow is not active.

A viewport anchor should be based on stable message identity rather than raw DOM availability:

```ts
type ViewportAnchor = {
messageId: string
offsetWithinMessage: number
}
```

When message heights change because of streaming, Markdown hydration, artifact rendering, image load, code block rendering, or history insertion, the system should compensate scroll position to keep the anchor visually stable unless the active mode is bottom-follow.

### 4. Virtual-list-like performance without full virtual opacity

The implementation should avoid painting all heavy message DOM for long conversations, but should retain full logical addressability.

Use a chat-specific windowed rendering model based on CSS `content-visibility`
rather than spacer-based DOM windowing:

```text
complete loaded message data
-> stable layout model for every loaded message
-> all rows kept mounted (no DOM add/remove on scroll)
-> each row uses `content-visibility: auto` + `contain-intrinsic-size`
so the browser skips painting off-screen rows
-> the generating row is forced `content-visibility: visible`
```

Rows outside the viewport stay mounted but unpainted (the browser uses the
intrinsic-size placeholder), while each loaded message still has:

- stable `messageId`
- ordering information
- estimated height
- measured height when available (committed only once the row has been painted)
- logical top/bottom offsets

### 5. Future minimap compatibility

This change must not block a future minimap.

The future minimap should be able to rely on a logical layout model, not on querying every message DOM node. Therefore:

- Do not make a third-party virtual scroller the sole source of truth for item heights or positions.
- Do not require all message DOM nodes to exist for anchor scrolling.
- Keep message positions addressable by `messageId`.
- Search, trace jumps, and future minimap jumps should operate through a message layout model.

### 6. Smooth and continuous scrolling

Scrolling should feel continuous for both normal and long conversations.

Requirements:

- Normal scrolling should not stutter from excessive Markdown mount/unmount work.
- Large or fast scrolls should not show large blank gaps caused by under-rendered virtual ranges.
- Overscan should adapt to scroll velocity and generation state.
- Heavy content hydration may be delayed while fast scrolling, then completed after scroll settles.

### 7. Long chat first load must be fast

Long conversations should not require full history or full DOM hydration before the chat becomes usable.

Preferred behavior:

1. Load and render the latest page/window first.
2. Position at the bottom.
3. Make input and latest messages interactive quickly.
4. Defer older history loading, metadata preparation, measurement refinement, and optional pre-hydration.

### 8. Streaming must stay smooth

Generation smoothness is a first-class requirement.

Streaming updates should not force the entire message list to recompute or remount. The currently streaming assistant message should be treated as a live row or live layer that is isolated from stable historical rows as much as possible.

The scroll/layout system should batch work during streaming:

- Coalesce `scrollToBottom` operations with `requestAnimationFrame` or equivalent batching.
- Batch height measurement commits.
- Avoid synchronous full-list layout recalculation on every token/chunk.
- Apply dynamic throttling/debouncing to Markdown rendering for long streaming content.

## Acceptance Criteria

1. Opening a long chat renders quickly and lands at the latest/bottom content.
2. Long chats avoid full heavy DOM rendering for all loaded messages.
3. `autoScrollEnabled = true` causes generation to follow the bottom.
4. `autoScrollEnabled = false` prevents generation from forcing the viewport to the bottom.
5. With auto-scroll disabled, the user's current reading position remains stable while generation continues.
6. Fast scrolling through long chats does not show large blank areas.
7. Streaming output remains smooth and is not blocked by full-list recomputation or excessive layout work.
8. Search, trace jumps, and future minimap jumps can target messages by `messageId` even if the target is outside the current render window.
9. Loading older messages at the top preserves viewport position.
10. The design leaves a reusable message layout model for future minimap work.

## Non-Goals

- Implementing the minimap itself.
- Replacing all chat message rendering components.
- Changing LLM/provider streaming semantics.
- Removing the existing `autoScrollEnabled` setting.
- Requiring full conversation history to load before the chat becomes usable.
- Relying solely on a third-party virtual scroller as the long-term architecture.

## Constraints

- Use Vue 3 Composition API patterns already present in the renderer.
- Keep changes localized to chat rendering, message layout, and scroll behavior where possible.
- Do not weaken existing message actions, trace behavior, search behavior, or read-only session behavior.
- Do not introduce user-facing strings without i18n keys.
- Avoid synchronous expensive work during streaming.
- Keep future minimap support data-driven rather than DOM-driven.

## Review Notes

The preferred architecture is a dedicated chat windowing model instead of enabling a fully opaque virtual list. Existing `vue-virtual-scroller` usage may still be referenced or used temporarily if it can satisfy the anchor and line-of-sight requirements, but the layout model should remain owned by DeepChat so minimap and jump behavior have stable coordinates.
Loading
Loading