Repository navigation
Conversation
The TUI keeps the newest 100 messages of a session loaded and drops the
oldest one for every new message past that, silently. Scrolling to the
top of a long session lands somewhere in the middle of the work, the
first prompt is the first thing to go, and nothing brings older
messages back.
Wherever messages are hidden the transcript now shows a divider: a
horizontal rule with the hidden count centered on it, and below it
"load 50 above", "load 50 below" and "load all" (just "load all N" for
a gap of 50 or fewer). The actions are clickable and are palette
commands ("Load 50 hidden messages above/below the divider", "Load all
hidden messages", /load-hidden) with keybinds messages_hidden_above,
messages_hidden_below and messages_hidden_all, unbound by default.
"Above" fills the gap from its older edge, "below" from its newer
edge. Loading grows the window by what it loaded so new output doesn't
immediately drop it again, and "load all" stops dropping messages for
that session. When the view is scrolled away from the bottom, the block
at the top of the viewport is put back on the same row after a load.
tui.json gains transcript.max_messages (default 100, the old constant)
and transcript.keep_first_prompt (default true), which keeps the
session's first prompt above the divider when older messages are
hidden.
The sync store tracks each session's gap as head (messages before it),
count (hidden) and limit (newest messages kept). A new message past the
limit hides the oldest message after the head, and an update to a
message inside the gap is ignored instead of reinserted.
GET /session/:id/message pages backwards only, so it gains after (a
cursor or message id; newer messages, oldest first), order=asc (page
from the oldest message), message ids for before as well as cursors,
and X-Total-Count on paged responses. The TUI anchors on message ids it
already holds, because a cursor goes stale once the message it points
at has been dropped. The first prompt is requested only when the
newest page doesn't cover the whole session.
AI-Tool: opencode 1.18.34-vt-126-907b3bc518
AI-Model: anthropic/claude-opus-5-5
AI-Variant: high
AI-Platform: linux
AI-Harness: Vibeterm 52e9db3
AI-Session-ID: ses_eec5b71ddffe4A47jCysfkQh6l
Contributor
|
The following comment was made by an LLM, it may be inaccurate: |
This branch has not been deployed
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Issue for this PR
Closes #53642
Type of change
What does this PR do?
The TUI keeps a session's newest 100 messages loaded and silently drops the oldest one for every new message past that. The limit counts messages only (all parts of a kept message stay loaded), and nothing else limits what the transcript renders. This PR makes the hidden part visible and loadable, and the limit configurable.
Divider. Wherever messages are hidden, the transcript shows a horizontal rule with the count centered on it and the actions below:
A gap of 50 or fewer shows only
load all 23. "Above" fills the gap from its older edge (right after the first prompt, or the session's first messages when no prompt is pinned); "below" fills it from its newer edge, extending the recent messages backwards. The actions are clickable and are palette commands: "Load 50 hidden messages above the divider", "... below the divider", "Load all hidden messages" (/load-hidden), with keybindsmessages_hidden_above,messages_hidden_below,messages_hidden_all, unbound by default. Loading grows the window by what it loaded, so new output doesn't drop it again straight away, and "load all" stops dropping messages for that session. When the view is scrolled away from the bottom, the block at the top of the viewport is put back on the same row after a load, so loading into an off-screen gap doesn't slide the transcript.Settings (
tui.json):transcript.max_messages- newest messages kept loaded, default100(the old constant).transcript.keep_first_prompt- keep the session's first prompt above the divider when older messages are hidden, defaulttrue. This changes what a long session looks like today (the first prompt appears at the top instead of being dropped). Happy to flip the default tofalseif you'd rather keep the current look by default.Store.
sync.tsxtracks each session's gap ashead(messages before it),count(hidden) andlimit(newest messages kept). A new message past the limit hides the oldest message after the head instead ofshift()ing index 0; an update to a message inside the gap is ignored rather than reinserted (today such an update is inserted and immediately shifted off again).API.
GET /session/:id/messageonly pages backwards, so the same route gains:after- messages strictly newer than a cursor or message ID, oldest first;X-Next-Cursor/Linkcontinue forward;order=asc- without an anchor, page from the oldest message;beforealso accepts a message ID, not just a cursor;X-Total-Counton paged responses.Conflicting combinations (
before+after,after+order=desc,before+order=asc) are 400. Unpaged requests are unchanged. The TUI anchors on message IDs it already has, because a cursor goes stale once the message it points at has been dropped. It asks for the first prompt (limit=1&order=asc) only when the newest page doesn't cover the session, so short sessions make exactly one request as today. The experimental/api/session/:id/messageroute already pages both ways but reads thesession_messageprojection, which sessions predating the event tables don't have.Memory and CPU by limit
One real session of 7,152 messages (part JSON for the newest 50 / 100 / 200 messages: 336 KB / 959 KB / 2.47 MB), opened with
-sin an isolated TUI (scratchXDG_*, throwaway tmux socket, 160x50) on Linux, then left idle on the session. RSS is the TUI process (VmRSS) 90 s after start and after 10 idle minutes; idle CPU isutime+stimeper minute averaged over idle minutes 4-10 (minutes 1-3 still settle). One run per row, strictly one at a time, on a busy desktop (load average around 50).transcript.max_messagesdevHalving the limit saves about 40 MB of settled RSS on this session; doubling it costs about 50 MB. Idle CPU does not depend on the limit: an idle TUI showing the session spends about 2% of a core whatever the limit. At the default the PR settles at the same RSS as
dev(610 vs 614 MB). Its after-load figure is higher, by the pinned first prompt (here a 300+ line prompt) and run-to-run noise.How did you verify your code works?
packages/opencode:test/session/messages-pagination.test.ts(forward pages, message-ID anchors, missing anchor) andtest/server/session-messages.test.ts(asc +after,X-Total-Count,Link, 400s) - 54 + 6 pass.packages/tui:test/cli/cmd/tui/sync-hidden-messages.test.tsxcovers the first prompt pinned above the gap,keep_first_prompt: false, an uncropped session making one request, loading below then above until the gap closes, "above" without a pinned prompt starting from the oldest message, live messages hiding the oldest recent message but never the pinned prompt, a hidden message's update staying hidden, an uncropped session starting to pin once it outgrows the limit, and "load all" keeping later messages.bun testandbun typecheckpass.XDG_*dirs, throwaway tmux socket, a copy of a real 7,152-message session:Checklist