feat(tui): add conversation scroll with Shift+Up/Down

Add scrollOffset to the conversation area so users can scroll back
through message history. Shift+Up scrolls up 3 messages, Shift+Down
scrolls back down. Scroll resets to bottom on new message submit.

Also reverts the Terminal.app light palette color change (keep
original yellow/blue).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
fanghanjun
2026-04-13 01:53:49 -07:00
parent e80a41def7
commit 9c1388bbdd
3 changed files with 24 additions and 5 deletions
+8 -3
View File
@@ -49,6 +49,7 @@ export interface BuildDashboardViewModelParams {
readonly lastError?: string;
readonly sinceTimestamp?: number;
readonly terminalRows?: number;
readonly scrollOffset?: number;
}
export function buildDashboardViewModel(params: BuildDashboardViewModelParams): DashboardViewModel {
@@ -62,9 +63,13 @@ export function buildDashboardViewModel(params: BuildDashboardViewModelParams):
const terminalRows = params.terminalRows ?? process.stdout.rows ?? 24;
const conversationLimit = Math.max(4, terminalRows - 10);
const messageRows = params.session.messages
.filter((message) => message.timestamp >= sinceTimestamp)
.slice(-conversationLimit)
const filteredMessages = params.session.messages
.filter((message) => message.timestamp >= sinceTimestamp);
const scrollOffset = params.scrollOffset ?? 0;
const endIndex = filteredMessages.length - scrollOffset;
const startIndex = Math.max(0, endIndex - conversationLimit);
const messageRows = filteredMessages
.slice(startIndex, endIndex > 0 ? endIndex : undefined)
.map((message, index) => ({
key: `${message.timestamp}-${index}`,
label: roleLabel(message.role, params.copy),
+14
View File
@@ -46,6 +46,7 @@ export interface InkTuiDashboardProps {
readonly slashSuggestions?: ReadonlyArray<string>;
readonly selectedSlashIndex?: number;
readonly showComposerCursor?: boolean;
readonly scrollOffset?: number;
readonly onInputChange?: (value: string) => void;
readonly onSubmit?: (value: string) => void;
}
@@ -78,6 +79,7 @@ export function InkTuiDashboard(props: InkTuiDashboardProps): React.JSX.Element
isSubmitting: props.isSubmitting,
lastError: props.lastError,
sinceTimestamp: props.sinceTimestamp,
scrollOffset: props.scrollOffset,
});
const activeAccent = props.isSubmitting ? WARM_ACCENT : statusColor(model.executionStatus);
const composer = renderComposerDisplay(props.inputValue, model.composerPlaceholder, props.showComposerCursor ?? false);
@@ -170,6 +172,7 @@ export function InkTuiApp(props: InkTuiAppProps): React.JSX.Element {
const [lastError, setLastError] = useState<string | undefined>();
const [sinceTimestamp, setSinceTimestamp] = useState<number | undefined>();
const [selectedSlashIndex, setSelectedSlashIndex] = useState(0);
const [scrollOffset, setScrollOffset] = useState(0);
const [historyState, setHistoryState] = useState<{ cursor: number | null; draft: string }>({
cursor: null,
draft: "",
@@ -251,6 +254,10 @@ export function InkTuiApp(props: InkTuiAppProps): React.JSX.Element {
}
if (key.downArrow) {
if (key.shift) {
setScrollOffset((cur) => Math.max(0, cur - 3));
return;
}
const next = moveHistoryCursor(inputHistory, historyState, inputValue, "down");
setHistoryState(next.state);
setInputValue(next.value);
@@ -258,6 +265,11 @@ export function InkTuiApp(props: InkTuiAppProps): React.JSX.Element {
}
if (key.upArrow) {
if (key.shift) {
const maxOffset = Math.max(0, session.messages.length - 4);
setScrollOffset((cur) => Math.min(maxOffset, cur + 3));
return;
}
const next = moveHistoryCursor(inputHistory, historyState, inputValue, "up");
setHistoryState(next.state);
setInputValue(next.value);
@@ -348,6 +360,7 @@ export function InkTuiApp(props: InkTuiAppProps): React.JSX.Element {
setIsSubmitting(true);
setLastError(undefined);
setInputValue("");
setScrollOffset(0);
setHistoryState({ cursor: null, draft: "" });
setSession((current) => createOptimisticUserMessageSession(current, input, userTimestamp));
@@ -412,6 +425,7 @@ export function InkTuiApp(props: InkTuiAppProps): React.JSX.Element {
slashSuggestions={slashSuggestions}
selectedSlashIndex={selectedSlashIndex}
showComposerCursor={composerCaret.visible}
scrollOffset={scrollOffset}
onInputChange={(value) => {
setInputValue(value);
setSelectedSlashIndex(0);
+2 -2
View File
@@ -96,9 +96,9 @@ const appleTerminalDarkPalette: ThemePalette = {
};
const appleTerminalLightPalette: ThemePalette = {
warmAccent: "gray",
warmAccent: "yellow",
warmMuted: "gray",
warmReply: "black",
warmReply: "blue",
warmBorder: "gray",
statusSuccess: "green",
statusError: "red",