Add support for message history pagination, preserve scroll in rendering, and improve draft/message workflows.
CI / check (22) (push) Successful in 9m34s
CI / check (24) (push) Successful in 9m34s

This commit is contained in:
2026-10-06 00:32:18 +02:00
parent 1c0f06716e
commit 387c0ac2e7
40 changed files with 2374 additions and 1314 deletions
+27 -14
View File
@@ -74,7 +74,12 @@ export function createComposerController({
const {signal} = sendController;
const visible = () => state.activeChatId === chatId;
let userCard, loadingMsg, rendering, completedReply = null;
let completedThinking = "", completedSources = null;
const userMessage = {role: "user", content: message, attachments};
const clearSentDraft = () => {
if (state.chatDrafts.get(chatId)?.trim() === message) state.chatDrafts.delete(chatId);
attachmentComposer.clearSentDraft(chatId, attachments.map(item => item.id));
};
try {
setLoadingState(true, false);
if (retryMessage && !msgInput.value.trim()) {
@@ -115,11 +120,11 @@ export function createComposerController({
if (follow) scrollMessagesToBottom();
};
rendering = createStreamRenderer({
onThinking: text => render(() => {
onThinking: text => { completedThinking = text; render(() => {
if (!reasoning) return;
updateReasoning(loadingMsg, text, true);
loadingShell?.classList.toggle("hidden", Boolean(text));
}),
}); },
onReply: text => render(() => {
if (text) finishReasoning(loadingMsg);
loadingShell?.classList.toggle("hidden", Boolean(text));
@@ -136,24 +141,36 @@ export function createComposerController({
: searchState === "no_relevant_results" ? "No sufficiently relevant results. Generating response..."
: sourceCount === 0 ? "No web results found. Generating response..." : "Generating response...";
},
onDone: ({reply}) => { completedReply = reply; }
onDone: ({reply, sources}) => { completedReply = reply; completedSources = sources; }
}, {signal});
rendering.flush();
rendering.dispose();
loadingMsg?.remove();
// Keep a completed answer in both state and the DOM until history replaces it.
if (visible()) {
const reply = {role: "bot", content: completedReply, modelName: model,
...(completedThinking ? {thinking: completedThinking} : {}), ...(completedSources ? {sources: completedSources} : {})};
state.currentMessages.push(reply);
if (loadingMsg?.isConnected) loadingMsg.replaceWith(addMessage(reply, false, false, {detached: true}));
else addMessage(reply);
} else loadingMsg?.remove();
webSearchToggle.reset(); reasoningToggle.reset();
if (visible()) {
const followOutput = messagesDiv.scrollHeight - messagesDiv.scrollTop - messagesDiv.clientHeight < 100;
const previousScrollTop = messagesDiv.scrollTop;
if (msgInput.value.trim() === message) { msgInput.value = ""; resizeComposerInput(); }
const currentAttachments = attachmentComposer.attachments;
if (currentAttachments.length === attachments.length && currentAttachments.every((item, index) => item.id === attachments[index].id)) attachmentComposer.clear();
await setActiveChat(chatId);
const refreshed = await setActiveChat(chatId);
if (refreshed?.error) throw new Error(refreshed.error);
if (visible()) {
if (msgInput.value.trim() === message) { msgInput.value = ""; resizeComposerInput(); }
if (msgInput.value) state.chatDrafts.set(chatId, msgInput.value);
else state.chatDrafts.delete(chatId);
const currentAttachments = attachmentComposer.attachments;
if (currentAttachments.length === attachments.length && currentAttachments.every((item, index) => item.id === attachments[index].id)) attachmentComposer.clear();
} else clearSentDraft();
if (!followOutput && visible()) requestAnimationFrame(() => {
if (visible()) messagesDiv.scrollTop = previousScrollTop;
});
} else {
if (state.chatDrafts.get(chatId)?.trim() === message) state.chatDrafts.delete(chatId);
attachmentComposer.clearSentDraft(chatId, attachments.map(item => item.id));
clearSentDraft();
}
await loadSummary();
markOnboardingCompleted(); maybeShowPersonaNudge();
@@ -162,10 +179,6 @@ export function createComposerController({
loadingMsg?.remove();
const messageText = signal.aborted ? "Generation stopped." : error?.message || "Unable to send message.";
if (completedReply !== null) {
if (visible() && userCard?.isConnected) {
state.currentMessages.push({role: "bot", content: completedReply});
addMessage(completedReply);
}
setNotice(`Reply sent, but the conversation could not be refreshed: ${messageText}`, "error");
} else {
if (visible()) {