Fix conversation reset during streaming

This commit is contained in:
dwrz
2026-07-05 14:14:04 +00:00
parent b1b708dd45
commit c4730eb8ce

View File

@@ -77,6 +77,8 @@ class Odidere {
this.isScrollPending = false; this.isScrollPending = false;
this.isStopPending = false; this.isStopPending = false;
this.pendingDeleteId = null; this.pendingDeleteId = null;
// Promise for the currently in-flight runTurn, so reset() can await it.
this._currentRun = null;
this.currentController = null; this.currentController = null;
this.totalTokens = 0; this.totalTokens = 0;
@@ -128,13 +130,26 @@ class Odidere {
this.wakeLock.release(); this.wakeLock.release();
} }
reset() { /**
* Reset stops any in-flight turn, waits for it to settle, then
* clears the conversation, DOM, attachments, location, and input.
* The abort and await pattern ensures message streaming in runTurn completes
* before conversation.clear().
*/
async reset() {
this.tts.stop(); this.tts.stop();
if (this.currentController) { if (this.currentController) {
this.currentController.abort(); this.currentController.abort();
this.currentController = null; this.currentController = null;
} }
this.setLoadingState(false); this.setLoadingState(false);
// Wait for the current run to settle before clearing.
if (this._currentRun) {
try {
await this._currentRun;
} catch {}
}
this.pendingDeleteId = null; this.pendingDeleteId = null;
this.conversation.clear(); this.conversation.clear();
this.renderer.setAutoScroll(true); this.renderer.setAutoScroll(true);
@@ -523,7 +538,14 @@ class Odidere {
} }
/** /**
* runTurn sends a chat request to the streaming SSE endpoint. * Build and send a chat request, then consume the streaming SSE response.
*
* The body executes inside an async IIFE stored as this._currentRun so
* reset() can abort the stream and await the IIFE before clearing state.
* On interruption (stop or reset), the catch block persists any partially-
* received assistant message or pending tool calls; on reset, clear() runs
* afterward, so the persisted data is immediately discarded.
*
* @param {Object} options * @param {Object} options
* @param {string} [options.text] * @param {string} [options.text]
* @param {string} [options.detectedLanguage] * @param {string} [options.detectedLanguage]
@@ -534,13 +556,13 @@ class Odidere {
this.renderer.setAutoScroll(true); this.renderer.setAutoScroll(true);
this.currentController = new AbortController(); this.currentController = new AbortController();
this._currentRun = (async () => {
let streamingBubbles = { let streamingBubbles = {
assistantId: null, assistantId: null,
message: null, message: null,
reasoning: null, reasoning: null,
text: null, text: null,
}; };
let done = false;
let pendingTools = null; let pendingTools = null;
const streamingMeta = (usage, timings) => { const streamingMeta = (usage, timings) => {
@@ -737,7 +759,11 @@ class Odidere {
} }
// Collect tool results. // Collect tool results.
if (message.role === 'tool' && pendingTools && pendingTools.assistant) { if (
message.role === 'tool' &&
pendingTools &&
pendingTools.assistant
) {
const appended = this.conversation.append([message]); const appended = this.conversation.append([message]);
pendingTools.results.push(appended[0]); pendingTools.results.push(appended[0]);
@@ -758,7 +784,6 @@ class Odidere {
} }
} }
done = true;
this.setLoadingState(false); this.setLoadingState(false);
} catch (e) { } catch (e) {
if (e.name !== 'AbortError') { if (e.name !== 'AbortError') {
@@ -766,11 +791,9 @@ class Odidere {
this.renderer.renderError(`Error: ${e.message}`); this.renderer.renderError(`Error: ${e.message}`);
} }
this.setLoadingState(false); this.setLoadingState(false);
} finally {
this.currentController = null;
// Persist a streaming message interrupted by stop or error. // Persist a streaming message interrupted by stop or error.
if (!done && streamingBubbles.message) { if (streamingBubbles.message) {
const meta = streamingMeta(null, null); const meta = streamingMeta(null, null);
const finalMessage = streamingBubbles.message; const finalMessage = streamingBubbles.message;
const hasContent = !!extractText(finalMessage.content); const hasContent = !!extractText(finalMessage.content);
@@ -799,7 +822,7 @@ class Odidere {
} }
// Finalize pending tool calls interrupted by stop or error. // Finalize pending tool calls interrupted by stop or error.
if (!done && pendingTools) { if (pendingTools) {
if (pendingTools.$el) { if (pendingTools.$el) {
this.renderer.finalizeStreamingText( this.renderer.finalizeStreamingText(
pendingTools.$el, pendingTools.$el,
@@ -813,6 +836,15 @@ class Odidere {
]); ]);
pendingTools = null; pendingTools = null;
} }
} finally {
this.currentController = null;
}
})();
try {
await this._currentRun;
} finally {
this._currentRun = null;
} }
} }
@@ -1058,7 +1090,7 @@ class Odidere {
this.$location.classList.add('compose__action-btn--location', 'active'); this.$location.classList.add('compose__action-btn--location', 'active');
this.updateSendButtonState(); this.updateSendButtonState();
}, },
() => { }, () => {},
GeolocationOptions, GeolocationOptions,
); );
} }
@@ -1226,6 +1258,7 @@ class Odidere {
if ($btn) $btn.classList.remove('pending'); if ($btn) $btn.classList.remove('pending');
} }
this.pendingDeleteId = null; this.pendingDeleteId = null;
this._currentRun = null;
} }
_cancelAllPending() { _cancelAllPending() {