Skip to content

feat(tui): return to the scrolled-up position on messages_first - #53630

Closed
Nowaker wants to merge 2 commits into
anomalyco:devfrom
Nowaker:tui-home-return
Closed

Nowaker wants to merge 2 commits into
anomalyco:devfrom
Nowaker:tui-home-return

Conversation

@Nowaker

@Nowaker Nowaker commented Oct 6, 2026 •

Copy link
Copy Markdown

Note: I'm aware v1 is now the maintenance branch and new features go to v2 only. This PR is here for others who want to use it on v1. I will submit a v2 version once I've migrated my plugin setup to v2.

Issue for this PR

Closes #53629. Builds on #53333: this branch contains that PR's commit too. Review only the last commit, 78ce706 "return to the scrolled-up position on messages_first". It is stacked because it uses #53333's ctrl+home / ctrl+end bindings and drops its mark from that PR's navigation commands.

Type of change

  • Bug fix
  • New feature
  • Refactor / code improvement
  • Documentation

What does this PR do?

messages_last (end, ctrl+end) now remembers where a scrolled-up reader was. The next messages_first (home, ctrl+home) brings them back there instead of to the top, and a second messages_first goes to the top as before. So end then home is a round trip, and home home still reaches the top.

The mark is the renderable at the top of the viewport plus its offset from that edge. Restoring scrolls that renderable back to the same row, so the position stays correct when content above or below it changes. It is a renderable rather than a scroll offset or a part id: offsets drift as content changes, and apply_patch repeats its part id across one box per file.

The mark is dropped, and messages_first goes to the top, when:

  • it was taken in another session;
  • the view is no longer at the bottom when messages_first runs (dragged scrollbar, anything else that moved it);
  • any other navigation command ran in between (page, half page, line, prompt, landmark, block, last user message), or the wheel scrolled the transcript (onMouseScroll on the scrollbox);
  • its renderable is destroyed or no longer in the transcript, for example after a revert or when the oldest messages are trimmed.

messages_last pressed while already at the bottom keeps the mark it holds, so new output or a submitted prompt in between doesn't lose it. A view sitting at the bottom is following output, so restoring from there doesn't interact with sticky scroll, and new output arriving afterwards above the restored view is handled by the existing layout code like any other scroll-up.

The rules live in rememberScroll / recallScroll in src/routes/session/navigation.ts, generic over the anchor type so they are unit-testable without a renderer. The route keeps one scrollReturn variable.

How did you verify your code works?

  • test/routes/session/navigation.test.ts covers the state machine: remember on leaving a scrolled-up view, replace on leaving again, keep when already at the bottom, discard across sessions, recall only at the bottom and only in the same session. bun test and bun typecheck pass in packages/tui.
  • In a throwaway tmux socket with isolated XDG_* dirs and a 28-message imported session, sending keys with tmux send-keys (first viewport row after each key):
# scrolled up, end, home returns, home again goes to the top
C-S-Down | # Questions
C-End    | # Todos              <- bottom
C-Home   | # Questions          <- back
C-Home   | PROMPT 1: please do step 1

# end, then any navigation drops the mark
C-End    | # Todos
C-Up     | PROMPT 6: please do step 6
C-Home   | PROMPT 1: please do step 1

# end, line up and back down to the bottom: dropped
C-End    | # Todos
M-C-y    |
M-C-e x3 | # Todos
C-Home   | PROMPT 1: please do step 1

# end at the bottom keeps the mark; end, home toggles
C-End    | # Todos
C-End    | # Todos
C-Home   | FINAL 1: step 1 is done.
C-End    | # Todos
C-Home   | FINAL 1: step 1 is done.

With the view three rows into a response before ctrl+end, ctrl+home restores all 30 captured viewport rows byte for byte. On dev, home after end goes to the top.

Checklist

  • I have tested my changes locally
  • I have not included unrelated changes in this PR

This PR is AI generated, but under direct supervision and on request of @Nowaker.

A long session can only be moved through by line, page or the whole
transcript. messages_next/messages_previous exist but are unbound,
and they skip any prompt within 10 rows of the viewport top, because
they compare against a fixed threshold rather than the row they land
on. Assistant parts render without an id the scroll position could be
matched against.

Bind three levels of navigation by default, all overridable in
tui.json:

- messages_previous / messages_next (ctrl+up / ctrl+down) stop on
  every user prompt, now through the same picker as the two below.
- messages_landmark_previous / messages_landmark_next
  (ctrl+shift+up / ctrl+shift+down) stop on prompts, the last text
  part of each turn (its final response), and question, todowrite and
  task calls. Edits, reads and shell calls are left out because they
  are as dense as the blocks themselves.
- messages_block_previous / messages_block_next
  (ctrl+alt+shift+up / ctrl+alt+shift+down) stop on every prompt,
  text part, reasoning part with content, and tool call. Reasoning
  without content - an encrypted block or an empty "Thinking"
  placeholder, which GPT models emit on every step - renders as a
  bare header and is skipped.

Text, reasoning and tool boxes now carry their part id so the session
route can find them among the scrollbox children, as it already does
for user messages by message id. "Previous" and "next" are relative to
the row a navigated block lands on, one below the viewport top, so
repeated presses walk block by block; past either end the view goes
to the top or back to the bottom.

messages_first and messages_last also bind ctrl+home and ctrl+end.
Jumping to the bottom to read new output or type a prompt loses the
place a scrolled-up reader was at; the only way back is to scroll and
search for it.

messages_last (end, ctrl+end) now remembers the block at the top of a
scrolled-up viewport and its offset from that edge. The next
messages_first (home, ctrl+home) brings that block back to the same
row; a second messages_first goes to the top as before. Remembering the
renderable rather than a scroll offset keeps the position correct when
content above it changes, and avoids part ids, which apply_patch
repeats across its per-file boxes.

The mark is dropped, and messages_first goes to the top, when:

- it was taken in another session;
- the view is no longer at the bottom when messages_first runs;
- any other navigation command ran in between (page, half page, line,
  prompt, landmark, block, last user message) or the wheel scrolled
  the transcript;
- its block is no longer rendered, e.g. after a revert.

messages_last pressed at the bottom keeps the mark it already holds,
so a submitted prompt or new output in between does not lose it.
rememberScroll / recallScroll in src/routes/session/navigation.ts hold
the rules and are unit-tested.
@github-actions

github-actions Bot commented Oct 6, 2026

Copy link
Copy Markdown
Contributor

The following comment was made by an LLM, it may be inaccurate:

@thdxr

thdxr commented Oct 10, 2026

Copy link
Copy Markdown
Member

This branch has been upgraded to V2. We're no longer taking PRs for V1. If you think this is still relevant, please port it over and open it for V2.

— from 𝕺𝖕𝖊𝖓𝕮𝖔𝖉𝖊

@thdxr thdxr closed this Oct 10, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[FEATURE]: home returns to where end left a scrolled-up reader

2 participants