返回 DeepSeek-Reasonix
scroll_state.go
根目录 / internal / cli / scroll_state.go
1 package cli
2
3 // scrollFollowMode is an explicit transcript tail-follow state machine.
4 //
5 // Previously the post-Update path used a single wasAtBottom snapshot taken
6 // before the message was applied. Opening an approval/chooser/todo overlay
7 // shrinks the viewport (bottomRows grows) without any user scroll, which made
8 // AtBottom() flip false and permanently disabled tail-follow until the user
9 // manually returned to the bottom (#6430). Long sessions with frequent
10 // height-changing panels then looked like "scroll is broken" (#6978).
11 //
12 // Rules:
13 // - Default is followTail: new content pins the viewport to the bottom.
14 // - Explicit user scroll away from the bottom → userScrolled (wheel-up,
15 // PgUp, scrollbar drag, Ctrl+Home, …).
16 // - Returning to the bottom (wheel/page to end, empty Enter, Ctrl+End,
17 // forceGotoBottom / session switch) restores followTail.
18 // - Modal overlays that only change bottomRows never flip the mode.
19 type scrollFollowMode uint8
20
21 const (
22 scrollFollowTail scrollFollowMode = iota
23 scrollUserScrolled
24 )
25
26 // markUserScrolled records that the user intentionally left the live tail.
27 func (m *chatTUI) markUserScrolled() {
28 m.scrollMode = scrollUserScrolled
29 }
30
31 // markFollowTail restores live tail-follow after the user (or a session
32 // switch) returns to the newest output.
33 func (m *chatTUI) markFollowTail() {
34 m.scrollMode = scrollFollowTail
35 }
36
37 // shouldFollowTail reports whether new transcript content should pin the
38 // viewport to the bottom. forceGotoBottom always wins for one frame.
39 func (m chatTUI) shouldFollowTail() bool {
40 return m.scrollMode == scrollFollowTail || m.forceGotoBottom
41 }
42
43 // modalOpen is true while a panel that steals bottom rows (or the whole main
44 // area) is active. Used so height-only layout changes never mark userScrolled.
45 func (m chatTUI) modalOpen() bool {
46 if m.pendingApproval != nil || m.chooser != nil || m.rewind != nil {
47 return true
48 }
49 if m.mcp != nil || m.clearConfirm != nil || m.mcpImport != nil {
50 return true
51 }
52 if m.skillPick != nil || m.resumePick != nil || m.quickPick != nil || m.copyPick != nil {
53 return true
54 }
55 return false
56 }
57
58 // syncScrollModeAfterGesture updates follow state from the current viewport
59 // position after an intentional scroll command. Landing on the bottom restores
60 // followTail; any other offset marks userScrolled.
61 func (m *chatTUI) syncScrollModeAfterGesture() {
62 if m.viewport.AtBottom() {
63 m.markFollowTail()
64 return
65 }
66 m.markUserScrolled()
67 }
68
68 lines GO