mirror of
https://github.com/Narcooo/inkos.git
synced 2026-09-01 15:08:51 +08:00
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:
@@ -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),
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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",
|
||||
|
||||
Reference in New Issue
Block a user