detached 4b2bce3

interactive: snap to bottom after /sessions load

Loading a session via the /sessions dialog used to park the viewport
on the last user turn, intended to show "where you left off". In
practice users open /sessions to resume work and expect to land at
the live tail of the conversation, not somewhere mid-scroll.

Replace the scrollToLastTurn call in applySessionSelection with
scrollToBottom so a /sessions resume snaps to the latest message.
The CLI startup paths (--continue, --resume, --session) still park
on the last turn since they're a different entry point with a
different mental model (boot into a saved session vs. switch
sessions during an active run).
commit
4b2bce3fb243358f44e8d226aaa33bd997e22ffb
author
patriceckhart · · 2026-04-27 13:31 UTC
parent
8e92c62
signature
unsigned
refs
tag: v0.1.27
1 file · +3 -15
M internal/agent/modes/interactive.go +3 -15
@@ -2264,15 +2264,8 @@ func totalTurnsLocked(msgs []provider.Message) int {
22642264 }
22652265
22662266 // applySessionSelection loads the given session via the cli-provided
2267-// callback and parks the viewport on the last turn so the user lands
2268-// looking at where the conversation left off (their last prompt at the
2269-// top of the chat, the assistant's last reply right below). Older
2270-// history is one scroll up; pgdn or end snaps to the current tail.
2271-//
2272-// Without this, scrollOffset stayed at 0 (pinned to the live tail),
2273-// which on a long resumed session showed only the last few rows of
2274-// the final assistant message — the user read that as "only one liner
2275-// happened, the resume didn't work".
2267+// callback and snaps the viewport to the bottom (the latest message)
2268+// so the user lands at the live tail of the resumed conversation.
22762269 func (i *Interactive) applySessionSelection(path string) {
22772270 if i.cfg.LoadSession == nil {
22782271 i.mu.Lock()
@@ -2293,17 +2286,12 @@ func (i *Interactive) applySessionSelection(path string) {
22932286 i.parkedTurn = 0
22942287 i.parkedTotal = 0
22952288 i.view.InvalidateRenderCache()
2296- // Pull the freshly-loaded transcript into the view so the anchor
2297- // math below sees the post-resume messages, not the empty pre-load
2298- // state. redraw() does the same on its next pass; we just front-run
2299- // it here to compute the scroll target.
23002289 if i.agent != nil {
23012290 i.view.Messages = i.agent.Messages()
23022291 }
2303- msgs := i.view.Messages
23042292 i.mu.Unlock()
23052293
2306- i.scrollToLastTurn(msgs)
2294+ i.scrollToBottom()
23072295 }
23082296
23092297 // scrollToLastTurn parks the viewport at the most recent user turn,
@@ -2264,15 +2264,8 @@ func totalTurnsLocked(msgs []provider.Message) int {
2264 }2264 }
2265 2265
2266 // applySessionSelection loads the given session via the cli-provided2266 // applySessionSelection loads the given session via the cli-provided
2267-// callback and parks the viewport on the last turn so the user lands2267+// callback and snaps the viewport to the bottom (the latest message)
2268-// looking at where the conversation left off (their last prompt at the2268+// so the user lands at the live tail of the resumed conversation.
2269-// top of the chat, the assistant's last reply right below). Older
2270-// history is one scroll up; pgdn or end snaps to the current tail.
2271-//
2272-// Without this, scrollOffset stayed at 0 (pinned to the live tail),
2273-// which on a long resumed session showed only the last few rows of
2274-// the final assistant message — the user read that as "only one liner
2275-// happened, the resume didn't work".
2276 func (i *Interactive) applySessionSelection(path string) {2269 func (i *Interactive) applySessionSelection(path string) {
2277 if i.cfg.LoadSession == nil {2270 if i.cfg.LoadSession == nil {
2278 i.mu.Lock()2271 i.mu.Lock()
@@ -2293,17 +2286,12 @@ func (i *Interactive) applySessionSelection(path string) {
2293 i.parkedTurn = 02286 i.parkedTurn = 0
2294 i.parkedTotal = 02287 i.parkedTotal = 0
2295 i.view.InvalidateRenderCache()2288 i.view.InvalidateRenderCache()
2296- // Pull the freshly-loaded transcript into the view so the anchor
2297- // math below sees the post-resume messages, not the empty pre-load
2298- // state. redraw() does the same on its next pass; we just front-run
2299- // it here to compute the scroll target.
2300 if i.agent != nil {2289 if i.agent != nil {
2301 i.view.Messages = i.agent.Messages()2290 i.view.Messages = i.agent.Messages()
2302 }2291 }
2303- msgs := i.view.Messages
2304 i.mu.Unlock()2292 i.mu.Unlock()
2305 2293
2306- i.scrollToLastTurn(msgs)2294+ i.scrollToBottom()
2307 }2295 }
2308 2296
2309 // scrollToLastTurn parks the viewport at the most recent user turn,2297 // scrollToLastTurn parks the viewport at the most recent user turn,

Keyboard shortcuts

Single-key shortcuts are on. ctrl k and esc work whatever this says.

ctrl k :
go to a section, branch, tag, file or repository
?
this list
j k
move the row cursor down / up
enter
open the row under the cursor; on Files, a directory opens or closes in place
↑ ↓ ← → home end
in the file tree (focus on a row): previous / next row, close / open a directory or go to its parent / first entry, first / last row. They never open a file
l h
on Files: open a directory or a file / close a directory or go up to its parent, then to the parent page. Elsewhere: into the side panel (on a merge request, its files) / back to the content; there j k move between its controls
g g G
first / last row
/
filter the list; on Files, find a file anywhere in the repository (esc clears, enter opens the best match)
b
switch branch or tag
g s g f g c g b g m g i g r g a
tabs: summary, files, commits, branches & tags, merge requests, issues, releases, actions (when available)
g h
home
w
wide mode: use the whole width of the screen, or not (remembered)
y
copy a permalink pinned to the commit
d u x
diff: split / unified / collapse or expand all
e E
diff: the whole file in view / every file, with the lines the hunks hide
n
merge request: next note in the diff
esc
close a dialog, leave a field

Branches 1

Tags 476

↑↓ moveenter openesc close