add generation cancellation middleware and refactor chat/message workflows to support HTTP client disconnections
CI / check (22) (push) Failing after 4m54s
CI / check (24) (push) Failing after 4m54s

This commit is contained in:
2026-10-05 23:21:42 +02:00
parent c48e1b17c4
commit 1c0f06716e
17 changed files with 620 additions and 172 deletions
+48 -42
View File
@@ -40,54 +40,60 @@ export function createConversationController({
async function setActiveChat(id, {preserveComposer = false, matchMessageId = null} = {}) {
const openRequest = ++state.chatOpenRequest;
if (!preserveComposer && id !== state.activeChatId) {
if (state.activeChatId != null) {
state.chatDrafts.set(state.activeChatId, msgInput.value);
attachmentComposer.saveDraft(state.activeChatId);
msgInput.value = state.chatDrafts.get(id) || "";
attachmentComposer.restoreDraft(id);
resizeComposerInput();
} else if (!msgInput.value && !attachmentComposer.pending && !attachmentComposer.attachments.length) {
msgInput.value = state.chatDrafts.get(id) || "";
attachmentComposer.restoreDraft(id);
resizeComposerInput();
}
}
if (isMobileLayout()) closeMobileSidebar();
setChatLoading(true, "Opening chat...");
setChatActivity(true, {
eyebrow: "Opening chat",
title: "Loading conversation",
eyebrow: "Opening chat", title: "Loading conversation",
detail: "Fetching messages and restoring the current scene."
});
state.activeChatId = id;
history.replaceState(null, "", `/app/chats/${id}`);
state.messageRetryState = new Map();
updateWorkspaceCopy(); renderChatList(); updateChatActionState();
let res = await get(`/chats/${id}/messages`);
if (openRequest !== state.chatOpenRequest) { setChatLoading(false); setChatActivity(false); return; }
if (res.error) {
setChatLoading(false);
setChatActivity(false);
return setNotice(res.error, "error");
try {
let res = await get(`/chats/${id}/messages`);
if (openRequest !== state.chatOpenRequest) return;
if (res.error) return setNotice(res.error, "error");
if (matchMessageId && !res.messages?.some(message => message.id === matchMessageId)) {
const activated = await post(`/chats/${id}/branches/${matchMessageId}/activate`, {includeDescendants: true});
if (openRequest !== state.chatOpenRequest) return;
if (activated.error) return setNotice(activated.error, "error");
res = await get(`/chats/${id}/messages`);
if (openRequest !== state.chatOpenRequest) return;
if (res.error) return setNotice(res.error, "error");
}
// Commit identity, content and drafts together, only for the latest successful load.
if (!preserveComposer && id !== state.activeChatId) {
if (state.activeChatId != null) {
state.chatDrafts.set(state.activeChatId, msgInput.value);
attachmentComposer.saveDraft(state.activeChatId);
msgInput.value = state.chatDrafts.get(id) || "";
attachmentComposer.restoreDraft(id);
resizeComposerInput();
} else if (!msgInput.value && !attachmentComposer.pending && !attachmentComposer.attachments.length) {
msgInput.value = state.chatDrafts.get(id) || "";
attachmentComposer.restoreDraft(id);
resizeComposerInput();
}
}
const existing = getChatById(id);
if (res.chat && existing) Object.assign(existing, res.chat);
state.activeChatId = id;
state.currentMessages = res.messages || [];
history.replaceState(null, "", `/app/chats/${id}`);
state.messageRetryState = new Map();
updateWorkspaceCopy(); renderChatList(); updateChatActionState();
populateGenerationSettings(res.chat || existing);
renderMessages();
if (matchMessageId) requestAnimationFrame(() => {
if (openRequest !== state.chatOpenRequest) return;
const target = [...messagesDiv.querySelectorAll("[data-message-id]")]
.find(node => Number(node.dataset.messageId) === Number(matchMessageId));
if (target) { target.scrollIntoView({block: "center"}); updateJumpToLatest(); }
});
updateSendState();
} finally {
if (openRequest === state.chatOpenRequest) {
setChatLoading(false);
setChatActivity(false);
}
}
if (matchMessageId && !res.messages?.some(message => message.id === matchMessageId)) {
const activated = await post(`/chats/${id}/branches/${matchMessageId}/activate`, {includeDescendants: true});
if (!activated.error) res = await get(`/chats/${id}/messages`);
if (openRequest !== state.chatOpenRequest) { setChatLoading(false); setChatActivity(false); return; }
}
const existing = getChatById(id);
if (res.chat && existing) Object.assign(existing, res.chat);
populateGenerationSettings(res.chat || existing);
state.currentMessages = res.messages || []; renderMessages();
if (matchMessageId) requestAnimationFrame(() => {
const target = [...messagesDiv.querySelectorAll("[data-message-id]")]
.find(node => Number(node.dataset.messageId) === Number(matchMessageId));
if (target) { target.scrollIntoView({block: "center"}); updateJumpToLatest(); }
});
updateSendState();
setChatLoading(false);
setChatActivity(false);
}
async function loadChatSessions() {