Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
18 commits
Select commit Hold shift + click to select a range
f3b5c6a
feat: overhaul scroll system with useAutoScroll + @tanstack/react-vir…
SsparKluo Jun 24, 2026
85cd3c7
fix: reverse chat page order so oldest messages render first (top to …
SsparKluo Jun 25, 2026
8563b37
fix: auto-scroll to bottom when last page expands while user is at bo…
SsparKluo Jun 25, 2026
4cc782e
fix: auto-snap to bottom when user scrolls within threshold; always f…
SsparKluo Jun 25, 2026
b4e62b8
fix: debounce snap timer and use instant scroll to prevent canceling …
SsparKluo Jun 25, 2026
d89d815
refactor: mobile input collapse bar with CSS grid-rows transition; fi…
SsparKluo Jun 25, 2026
46a079d
fix: move MentionMenu/SlashCommandMenu outside overflow-hidden to pre…
SsparKluo Jun 26, 2026
1c5c18c
refactor: replace 250ms scroll gesture window with opencode markBound…
SsparKluo Jun 26, 2026
fce3f67
fix: re-snap to bottom when scroll container resizes
SsparKluo Jun 26, 2026
fb8c163
Revert "fix: re-snap to bottom when scroll container resizes"
SsparKluo Jun 27, 2026
e9bff81
fix: remove unnecessary textarea height reset on auto-resize
SsparKluo Jun 27, 2026
a28cac2
refactor: replace auto-collapse with manual collapse toggle on mobile
SsparKluo Jun 27, 2026
55c41ca
fix: restructure expanded track grid so collapse fully reclaims space
SsparKluo Jun 27, 2026
92fbf69
fix: use hidden mirror element to measure textarea content height
SsparKluo Jun 27, 2026
c5b3601
fix: use textarea mirror for stable height measurement, keep footer v…
SsparKluo Jun 27, 2026
9a33b19
fix: add touch-pan-y to ChatArea to allow horizontal pager gestures o…
SsparKluo Jun 27, 2026
bb4d50e
fix: propagate touch-pan-y up the DOM chain to enable horizontal page…
SsparKluo Jun 27, 2026
4072973
fix: delegate horizontal swipes from chat scroll root to mobile pager
SsparKluo Jun 27, 2026
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 package.json
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@
"@codemirror/view": "^6.41.1",
"@opencode-ai/sdk": "^1.16.0",
"@streamdown/math": "^1.0.2",
"@tanstack/react-virtual": "^3.14.3",
"@tauri-apps/api": "^2.10.1",
"@tauri-apps/plugin-dialog": "^2.6.0",
"@tauri-apps/plugin-fs": "^2.4.5",
Expand Down
14 changes: 13 additions & 1 deletion src/constants/ui.ts
Original file line number Diff line number Diff line change
@@ -1,2 +1,14 @@
/** 底部判断阈值 */
/**
* 底部判断阈值 —— 三个功能共用同一个值:
* 1. to-bottom 按钮:用户滚动超出该距离,按钮显示
* 2. auto-follow:新内容是否自动跟随(userScrolled 在该阈值内为 false)
* 3. auto-snap:内容增长时,接近底部是否自动吸附到底部
*/
export const AT_BOTTOM_THRESHOLD_PX = 60

/**
* Sub-pixel 容差 —— 用于"scrollTop 是否本质上等于标记的 bottom 位置",
* 区分"我们自己 scrollToBottom 触发的 scroll"和"用户的 scroll"。
* 与 AT_BOTTOM_THRESHOLD_PX 是不同概念,单独命名避免 magic number。
*/
export const BOTTOM_NUDGE_TOLERANCE_PX = 2
13 changes: 7 additions & 6 deletions src/features/chat/ChatArea.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -183,12 +183,12 @@ describe('buildChatPages', () => {

const pages = buildChatPages(messages, 2)

// render order: newest page first
// render order: oldest page first
expect(pages).toHaveLength(3)
expect(pages[0].messageIds).toEqual(['user-2', 'assistant-3'])
expect(pages[0].messageIds).toEqual(['user-1'])
// assistant-1 + assistant-2 应该保持在同一个 group/page,不被拆开
expect(pages[1].messageIds).toEqual(['assistant-1', 'assistant-2'])
expect(pages[2].messageIds).toEqual(['user-1'])
expect(pages[2].messageIds).toEqual(['user-2', 'assistant-3'])
})
})

Expand Down Expand Up @@ -441,7 +441,8 @@ describe('reconcileStableChatPages', () => {
]
const nextPages = reconcileStableChatPages({ currentPages, nextMessages, allocateKey: alloc, pageMessageCount: 2 })

expect(nextPages.slice(0, currentPages.length).map(page => page.key)).toEqual(currentPages.map(page => page.key))
// 老的页在 oldest-first 中应保持末尾(新消息 prepend 到开头)
expect(nextPages.slice(nextPages.length - currentPages.length).map(page => page.key)).toEqual(currentPages.map(page => page.key))
})

it('only rebuilds the newest page segment when appending new messages', () => {
Expand All @@ -456,8 +457,8 @@ describe('reconcileStableChatPages', () => {
const nextMessages = [...currentMessages, createUserMessage('user-3', 7), createAssistantMessage('assistant-3', [], 8, 9)]
const nextPages = reconcileStableChatPages({ currentPages, nextMessages, allocateKey: alloc, pageMessageCount: 2 })

// 老的第二页 key 应该保住,避免整段历史一起重切
expect(nextPages[nextPages.length - 1].key).toBe(currentPages[currentPages.length - 1].key)
// 最旧的页 key 应该保住,避免整段历史一起重切
expect(nextPages[0].key).toBe(currentPages[0].key)
})
})

Expand Down
Loading