pgup/pgdown in transcript
This commit is contained in:
@@ -37,6 +37,7 @@ Compatibility aliases:
|
||||
- `Tab` / `Shift+Tab`: move focus between sidebar, transcript, and composer
|
||||
- `Esc` (in composer): exit input mode and focus sidebar
|
||||
- `Up` / `Down` (in sidebar): move highlight
|
||||
- `Page Up` / `Page Down` (in transcript): scroll the transcript by one page
|
||||
- `Enter` in sidebar: load highlighted conversation/search
|
||||
- `Enter` in composer: send message/search
|
||||
- `n`: new chat draft
|
||||
|
||||
@@ -323,6 +323,35 @@ async function main() {
|
||||
|
||||
const focusables = [sidebar, transcript, composer] as const;
|
||||
|
||||
function getTranscriptViewportHeight() {
|
||||
const lpos = transcript.lpos;
|
||||
if (lpos) {
|
||||
return Math.max(1, lpos.yl - lpos.yi - Number(transcript.iheight ?? 0));
|
||||
}
|
||||
|
||||
return Math.max(1, Number(transcript.height ?? 1) - Number(transcript.iheight ?? 0));
|
||||
}
|
||||
|
||||
function isTranscriptNearBottom() {
|
||||
const viewportHeight = getTranscriptViewportHeight();
|
||||
const maxScroll = Math.max(0, transcript.getScrollHeight() - viewportHeight);
|
||||
if (maxScroll === 0) return true;
|
||||
return maxScroll - transcript.getScroll() <= 1;
|
||||
}
|
||||
|
||||
function queueTranscriptScrollToBottomIfFollowing() {
|
||||
if (isTranscriptNearBottom()) {
|
||||
forceScrollToBottom = true;
|
||||
}
|
||||
}
|
||||
|
||||
function scrollTranscriptByPage(direction: 1 | -1) {
|
||||
forceScrollToBottom = false;
|
||||
const pageSize = Math.max(1, getTranscriptViewportHeight() - 1);
|
||||
transcript.scroll(direction * pageSize);
|
||||
screen.render();
|
||||
}
|
||||
|
||||
function withSuppressedSidebarSelectEvents(fn: () => void) {
|
||||
suppressedSidebarSelectEvents += 1;
|
||||
try {
|
||||
@@ -684,7 +713,7 @@ async function main() {
|
||||
return items.some((item) => item.kind === selection.kind && item.id === selection.id);
|
||||
}
|
||||
|
||||
async function loadSelection(selection: SidebarSelection | null) {
|
||||
async function loadSelection(selection: SidebarSelection | null, options?: { scrollToBottom?: boolean | undefined }) {
|
||||
if (!selection) {
|
||||
selectedChat = null;
|
||||
selectedSearch = null;
|
||||
@@ -714,12 +743,18 @@ async function main() {
|
||||
if (selectionKey(selectedItem) === requestedSelectionKey) {
|
||||
isLoadingSelection = false;
|
||||
}
|
||||
forceScrollToBottom = true;
|
||||
if (options?.scrollToBottom) {
|
||||
forceScrollToBottom = true;
|
||||
}
|
||||
updateUI();
|
||||
}
|
||||
}
|
||||
|
||||
async function refreshCollections(options?: { preferredSelection?: SidebarSelection; loadSelection?: boolean }) {
|
||||
async function refreshCollections(options?: {
|
||||
preferredSelection?: SidebarSelection;
|
||||
loadSelection?: boolean;
|
||||
scrollToBottomOnLoad?: boolean | undefined;
|
||||
}) {
|
||||
isLoadingCollections = true;
|
||||
updateUI();
|
||||
|
||||
@@ -748,7 +783,7 @@ async function main() {
|
||||
}
|
||||
|
||||
if (options?.loadSelection) {
|
||||
await loadSelection(selectedItem);
|
||||
await loadSelection(selectedItem, { scrollToBottom: options?.scrollToBottomOnLoad });
|
||||
}
|
||||
}
|
||||
|
||||
@@ -797,7 +832,7 @@ async function main() {
|
||||
errorMessage = null;
|
||||
forceScrollToBottom = true;
|
||||
updateUI();
|
||||
await loadSelection(selectedItem);
|
||||
await loadSelection(selectedItem, { scrollToBottom: true });
|
||||
}
|
||||
|
||||
function handleCreateChat() {
|
||||
@@ -966,7 +1001,7 @@ async function main() {
|
||||
};
|
||||
}
|
||||
|
||||
forceScrollToBottom = true;
|
||||
queueTranscriptScrollToBottomIfFollowing();
|
||||
updateUI();
|
||||
},
|
||||
onDelta: (payload) => {
|
||||
@@ -982,7 +1017,7 @@ async function main() {
|
||||
|
||||
if (!updated) return;
|
||||
pendingChatState = { ...pendingChatState, messages: nextMessages };
|
||||
forceScrollToBottom = true;
|
||||
queueTranscriptScrollToBottomIfFollowing();
|
||||
updateUI();
|
||||
},
|
||||
onDone: (payload) => {
|
||||
@@ -998,7 +1033,7 @@ async function main() {
|
||||
|
||||
if (!updated) return;
|
||||
pendingChatState = { ...pendingChatState, messages: nextMessages };
|
||||
forceScrollToBottom = true;
|
||||
queueTranscriptScrollToBottomIfFollowing();
|
||||
updateUI();
|
||||
},
|
||||
onError: (payload) => {
|
||||
@@ -1011,15 +1046,17 @@ async function main() {
|
||||
throw new Error(streamErrorMessage);
|
||||
}
|
||||
|
||||
const shouldFollowTranscript = isTranscriptNearBottom();
|
||||
|
||||
await refreshCollections({ preferredSelection: { kind: "chat", id: chatId }, loadSelection: false });
|
||||
|
||||
const currentSelection = selectedItem;
|
||||
if (currentSelection?.kind === "chat" && currentSelection.id === chatId) {
|
||||
await loadSelection(currentSelection);
|
||||
await loadSelection(currentSelection, { scrollToBottom: shouldFollowTranscript });
|
||||
}
|
||||
|
||||
pendingChatState = null;
|
||||
forceScrollToBottom = true;
|
||||
forceScrollToBottom = shouldFollowTranscript;
|
||||
updateUI();
|
||||
}
|
||||
|
||||
@@ -1103,7 +1140,7 @@ async function main() {
|
||||
error: null,
|
||||
results: payload.results,
|
||||
};
|
||||
forceScrollToBottom = true;
|
||||
queueTranscriptScrollToBottomIfFollowing();
|
||||
updateUI();
|
||||
},
|
||||
onSearchError: (payload) => {
|
||||
@@ -1122,7 +1159,7 @@ async function main() {
|
||||
answerCitations: payload.answerCitations,
|
||||
answerError: null,
|
||||
};
|
||||
forceScrollToBottom = true;
|
||||
queueTranscriptScrollToBottomIfFollowing();
|
||||
updateUI();
|
||||
},
|
||||
onAnswerError: (payload) => {
|
||||
@@ -1135,7 +1172,7 @@ async function main() {
|
||||
if (runId !== searchRunCounter) return;
|
||||
selectedSearch = payload.search;
|
||||
selectedChat = null;
|
||||
forceScrollToBottom = true;
|
||||
queueTranscriptScrollToBottomIfFollowing();
|
||||
updateUI();
|
||||
},
|
||||
onError: (payload) => {
|
||||
@@ -1154,11 +1191,13 @@ async function main() {
|
||||
}
|
||||
}
|
||||
|
||||
const shouldFollowTranscript = isTranscriptNearBottom();
|
||||
|
||||
await refreshCollections({ preferredSelection: { kind: "search", id: searchId }, loadSelection: false });
|
||||
|
||||
const currentSelection = selectedItem;
|
||||
if (currentSelection?.kind === "search" && currentSelection.id === searchId) {
|
||||
await loadSelection(currentSelection);
|
||||
await loadSelection(currentSelection, { scrollToBottom: shouldFollowTranscript });
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1186,7 +1225,7 @@ async function main() {
|
||||
}
|
||||
|
||||
if (selectedItem) {
|
||||
await loadSelection(selectedItem);
|
||||
await loadSelection(selectedItem, { scrollToBottom: true });
|
||||
}
|
||||
} finally {
|
||||
isSending = false;
|
||||
@@ -1214,7 +1253,7 @@ async function main() {
|
||||
selectedSearch = null;
|
||||
}
|
||||
|
||||
await refreshCollections({ loadSelection: true });
|
||||
await refreshCollections({ loadSelection: true, scrollToBottomOnLoad: true });
|
||||
}
|
||||
|
||||
function cycleProvider() {
|
||||
@@ -1262,6 +1301,14 @@ async function main() {
|
||||
updateUI();
|
||||
});
|
||||
|
||||
transcript.key(["pageup"], () => {
|
||||
scrollTranscriptByPage(-1);
|
||||
});
|
||||
|
||||
transcript.key(["pagedown"], () => {
|
||||
scrollTranscriptByPage(1);
|
||||
});
|
||||
|
||||
sidebar.on("select item", (_item, index) => {
|
||||
if (suppressedSidebarSelectEvents > 0) return;
|
||||
const highlightedItem = getSidebarItems()[index];
|
||||
@@ -1389,7 +1436,7 @@ async function main() {
|
||||
}
|
||||
|
||||
await runAction(async () => {
|
||||
await Promise.all([refreshCollections({ loadSelection: true }), refreshModels()]);
|
||||
await Promise.all([refreshCollections({ loadSelection: true, scrollToBottomOnLoad: true }), refreshModels()]);
|
||||
});
|
||||
|
||||
focusComposer();
|
||||
|
||||
Reference in New Issue
Block a user