tui: add transcript paging controls
This commit is contained in:
parent
06845a1974
commit
a3e64a1fba
@ -73,4 +73,25 @@ describe("ChatLog", () => {
|
||||
expect(rendered).not.toContain("BTW: what is 17 * 19?");
|
||||
expect(chatLog.hasVisibleBtw()).toBe(false);
|
||||
});
|
||||
|
||||
it("supports paging back through transcript history", () => {
|
||||
const chatLog = new ChatLog(80);
|
||||
for (let i = 1; i <= 12; i++) {
|
||||
chatLog.addSystem(`system-${i}`);
|
||||
}
|
||||
chatLog.setViewportHeight(6);
|
||||
|
||||
let rendered = chatLog.render(120).join("\n");
|
||||
expect(rendered).toContain("system-12");
|
||||
expect(rendered).not.toContain("system-7");
|
||||
|
||||
chatLog.scrollPageUp();
|
||||
rendered = chatLog.render(120).join("\n");
|
||||
expect(rendered).toContain("system-7");
|
||||
expect(rendered).not.toContain("system-12");
|
||||
|
||||
chatLog.scrollToLatest();
|
||||
rendered = chatLog.render(120).join("\n");
|
||||
expect(rendered).toContain("system-12");
|
||||
});
|
||||
});
|
||||
|
||||
@ -8,6 +8,8 @@ import { UserMessageComponent } from "./user-message.js";
|
||||
|
||||
export class ChatLog extends Container {
|
||||
private readonly maxComponents: number;
|
||||
private viewportHeight: number | null = null;
|
||||
private scrollOffset = 0;
|
||||
private toolById = new Map<string, ToolExecutionComponent>();
|
||||
private streamingRuns = new Map<string, AssistantMessageComponent>();
|
||||
private btwMessage: BtwInlineMessage | null = null;
|
||||
@ -46,8 +48,53 @@ export class ChatLog extends Container {
|
||||
}
|
||||
|
||||
private append(component: Component) {
|
||||
const wasAtLatest = this.scrollOffset === 0;
|
||||
this.addChild(component);
|
||||
this.pruneOverflow();
|
||||
if (wasAtLatest) {
|
||||
this.scrollToLatest();
|
||||
}
|
||||
}
|
||||
|
||||
setViewportHeight(height: number | null) {
|
||||
if (height === null || Number.isNaN(height)) {
|
||||
this.viewportHeight = null;
|
||||
this.clampScrollOffset();
|
||||
return;
|
||||
}
|
||||
this.viewportHeight = Math.max(1, Math.floor(height));
|
||||
this.clampScrollOffset();
|
||||
}
|
||||
|
||||
scrollPageUp() {
|
||||
const page = this.viewportHeight ?? 10;
|
||||
this.scrollOffset += page;
|
||||
this.clampScrollOffset();
|
||||
}
|
||||
|
||||
scrollPageDown() {
|
||||
const page = this.viewportHeight ?? 10;
|
||||
this.scrollOffset -= page;
|
||||
this.clampScrollOffset();
|
||||
}
|
||||
|
||||
scrollToLatest() {
|
||||
this.scrollOffset = 0;
|
||||
}
|
||||
|
||||
private getRenderedLineCount(width: number) {
|
||||
return super.render(width).length;
|
||||
}
|
||||
|
||||
private getMaxScrollOffset(width: number) {
|
||||
if (!this.viewportHeight) {
|
||||
return 0;
|
||||
}
|
||||
return Math.max(0, this.getRenderedLineCount(width) - this.viewportHeight);
|
||||
}
|
||||
|
||||
private clampScrollOffset(width = 120) {
|
||||
this.scrollOffset = Math.max(0, Math.min(this.scrollOffset, this.getMaxScrollOffset(width)));
|
||||
}
|
||||
|
||||
clearAll() {
|
||||
@ -186,4 +233,17 @@ export class ChatLog extends Container {
|
||||
tool.setExpanded(expanded);
|
||||
}
|
||||
}
|
||||
|
||||
override render(width: number) {
|
||||
const lines = super.render(width);
|
||||
if (!this.viewportHeight || lines.length <= this.viewportHeight) {
|
||||
this.scrollOffset = 0;
|
||||
return lines;
|
||||
}
|
||||
|
||||
this.clampScrollOffset(width);
|
||||
const start = Math.max(0, lines.length - this.viewportHeight - this.scrollOffset);
|
||||
const end = start + this.viewportHeight;
|
||||
return lines.slice(start, end);
|
||||
}
|
||||
}
|
||||
|
||||
@ -11,6 +11,8 @@ export class CustomEditor extends Editor {
|
||||
onCtrlT?: () => void;
|
||||
onShiftTab?: () => void;
|
||||
onAltEnter?: () => void;
|
||||
onPageUp?: () => void;
|
||||
onPageDown?: () => void;
|
||||
|
||||
handleInput(data: string): void {
|
||||
if (matchesKey(data, Key.alt("enter")) && this.onAltEnter) {
|
||||
@ -37,6 +39,14 @@ export class CustomEditor extends Editor {
|
||||
this.onCtrlT();
|
||||
return;
|
||||
}
|
||||
if (matchesKey(data, Key.pageUp) && this.onPageUp) {
|
||||
this.onPageUp();
|
||||
return;
|
||||
}
|
||||
if (matchesKey(data, Key.pageDown) && this.onPageDown) {
|
||||
this.onPageDown();
|
||||
return;
|
||||
}
|
||||
if (matchesKey(data, Key.shift("tab")) && this.onShiftTab) {
|
||||
this.onShiftTab();
|
||||
return;
|
||||
|
||||
@ -542,12 +542,20 @@ export async function runTui(opts: TuiOptions) {
|
||||
const footer = new Text("", 1, 0);
|
||||
const chatLog = new ChatLog();
|
||||
const editor = new CustomEditor(tui, editorTheme);
|
||||
const root = new Container();
|
||||
root.addChild(header);
|
||||
root.addChild(chatLog);
|
||||
root.addChild(statusContainer);
|
||||
root.addChild(footer);
|
||||
root.addChild(editor);
|
||||
const root = new (class extends Container {
|
||||
override render(width: number) {
|
||||
const headerLines = header.render(width);
|
||||
const statusLines = statusContainer.render(width);
|
||||
const footerLines = footer.render(width);
|
||||
const editorLines = editor.render(width);
|
||||
const reserved =
|
||||
headerLines.length + statusLines.length + footerLines.length + editorLines.length;
|
||||
const available = Math.max(1, tui.terminal.rows - reserved);
|
||||
chatLog.setViewportHeight(available);
|
||||
const chatLines = chatLog.render(width);
|
||||
return [...headerLines, ...chatLines, ...statusLines, ...footerLines, ...editorLines];
|
||||
}
|
||||
})();
|
||||
|
||||
const updateAutocompleteProvider = () => {
|
||||
editor.setAutocompleteProvider(
|
||||
@ -564,6 +572,14 @@ export async function runTui(opts: TuiOptions) {
|
||||
|
||||
tui.addChild(root);
|
||||
tui.setFocus(editor);
|
||||
editor.onPageUp = () => {
|
||||
chatLog.scrollPageUp();
|
||||
tui.requestRender();
|
||||
};
|
||||
editor.onPageDown = () => {
|
||||
chatLog.scrollPageDown();
|
||||
tui.requestRender();
|
||||
};
|
||||
|
||||
const formatSessionKey = (key: string) => {
|
||||
if (key === "global" || key === "unknown") {
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user