Skip to content

Commit 8687ce2

Browse files
committed
feat: Improve logic for continuing the assistant message
1 parent 95d8c65 commit 8687ce2

File tree

1 file changed

+86
-5
lines changed

1 file changed

+86
-5
lines changed

tools/server/webui/src/lib/stores/chat.svelte.ts

Lines changed: 86 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1457,6 +1457,10 @@ class ChatStore {
14571457
timestamp: Date.now()
14581458
});
14591459

1460+
// Ensure currNode points to the edited message to maintain correct path
1461+
await DatabaseStore.updateCurrentNode(this.activeConversation.id, messageToEdit.id);
1462+
this.activeConversation.currNode = messageToEdit.id;
1463+
14601464
this.updateMessageAtIndex(messageIndex, {
14611465
content: newContent,
14621466
timestamp: Date.now()
@@ -1473,6 +1477,16 @@ class ChatStore {
14731477
/**
14741478
* Edits a user message and preserves all responses below
14751479
* Updates the message content in-place without deleting or regenerating responses
1480+
*
1481+
* **Use Case**: When you want to fix a typo or rephrase a question without losing the assistant's response
1482+
*
1483+
* **Important Behavior:**
1484+
* - Does NOT create a branch (unlike editMessageWithBranching)
1485+
* - Does NOT regenerate assistant responses
1486+
* - Only updates the user message content in the database
1487+
* - Preserves the entire conversation tree below the edited message
1488+
* - Updates conversation title if this is the first user message
1489+
*
14761490
* @param messageId - The ID of the user message to edit
14771491
* @param newContent - The new content for the message
14781492
*/
@@ -1720,6 +1734,19 @@ class ChatStore {
17201734
/**
17211735
* Continues generation for an existing assistant message
17221736
* Appends new content to the existing message without branching
1737+
*
1738+
* **Important Implementation Details:**
1739+
* - Sends a synthetic "continue" prompt to the API that is NOT persisted to the database
1740+
* - This creates intentional divergence: API sees the prompt, database does not
1741+
* - The synthetic prompt instructs the model to continue from where it left off
1742+
* - Original message content is fetched from database to ensure accuracy after stops/edits
1743+
* - New content is appended to the original message in-place (no branching)
1744+
*
1745+
* **Data Consistency Note:**
1746+
* The conversation history in the database will not include the synthetic "continue" prompt.
1747+
* This is by design - the prompt is a UI affordance, not part of the conversation content.
1748+
* Export/import will preserve the actual conversation without synthetic prompts.
1749+
*
17231750
* @param messageId - The ID of the assistant message to continue
17241751
*/
17251752
async continueAssistantMessage(messageId: string): Promise<void> {
@@ -1738,14 +1765,33 @@ class ChatStore {
17381765
return;
17391766
}
17401767

1768+
// Race condition protection: Check if this specific conversation is already loading
1769+
// This prevents multiple rapid clicks on "Continue" from creating concurrent operations
1770+
if (this.isConversationLoading(this.activeConversation.id)) {
1771+
console.warn('Continuation already in progress for this conversation');
1772+
return;
1773+
}
1774+
17411775
this.errorDialogState = null;
17421776
this.setConversationLoading(this.activeConversation.id, true);
17431777
this.clearConversationStreaming(this.activeConversation.id);
17441778

1745-
// Get current content (includes any edits made to the message)
1746-
// This comes from activeMessages which is kept in sync with the database
1747-
const originalContent = messageToContinue.content;
1748-
const originalThinking = messageToContinue.thinking || '';
1779+
// IMPORTANT: Fetch the latest content from the database to ensure we have
1780+
// the most up-to-date content, especially after a stopped generation
1781+
// This prevents issues where the in-memory state might be stale
1782+
const allMessages = await DatabaseStore.getConversationMessages(this.activeConversation.id);
1783+
const dbMessage = allMessages.find((m) => m.id === messageId);
1784+
1785+
if (!dbMessage) {
1786+
console.error('Message not found in database for continuation');
1787+
this.setConversationLoading(this.activeConversation.id, false);
1788+
1789+
return;
1790+
}
1791+
1792+
// Use content from database as the source of truth
1793+
const originalContent = dbMessage.content;
1794+
const originalThinking = dbMessage.thinking || '';
17491795

17501796
// Get conversation context up to (but not including) the message to continue
17511797
const conversationContext = this.activeMessages.slice(0, messageIndex);
@@ -1780,13 +1826,15 @@ class ChatStore {
17801826

17811827
let appendedContent = '';
17821828
let appendedThinking = '';
1829+
let hasReceivedContent = false;
17831830

17841831
await chatService.sendMessage(
17851832
contextWithContinue,
17861833
{
17871834
...this.getApiOptions(),
17881835

17891836
onChunk: (chunk: string) => {
1837+
hasReceivedContent = true;
17901838
appendedContent += chunk;
17911839
// Preserve originalContent exactly as-is, including any trailing whitespace
17921840
// The concatenation naturally preserves any whitespace at the end of originalContent
@@ -1803,6 +1851,7 @@ class ChatStore {
18031851
},
18041852

18051853
onReasoningChunk: (reasoningChunk: string) => {
1854+
hasReceivedContent = true;
18061855
appendedThinking += reasoningChunk;
18071856
const fullThinking = originalThinking + appendedThinking;
18081857

@@ -1842,15 +1891,47 @@ class ChatStore {
18421891
slotsService.clearConversationState(messageToContinue.convId);
18431892
},
18441893

1845-
onError: (error: Error) => {
1894+
onError: async (error: Error) => {
18461895
if (this.isAbortError(error)) {
1896+
// User cancelled - save partial continuation if any content was received
1897+
if (hasReceivedContent && appendedContent) {
1898+
const partialContent = originalContent + appendedContent;
1899+
const partialThinking = originalThinking + appendedThinking;
1900+
1901+
await DatabaseStore.updateMessage(messageToContinue.id, {
1902+
content: partialContent,
1903+
thinking: partialThinking,
1904+
timestamp: Date.now()
1905+
});
1906+
1907+
this.updateMessageAtIndex(messageIndex, {
1908+
content: partialContent,
1909+
thinking: partialThinking,
1910+
timestamp: Date.now()
1911+
});
1912+
}
1913+
18471914
this.setConversationLoading(messageToContinue.convId, false);
18481915
this.clearConversationStreaming(messageToContinue.convId);
18491916
slotsService.clearConversationState(messageToContinue.convId);
18501917
return;
18511918
}
18521919

1920+
// Non-abort error - rollback to original content
18531921
console.error('Continue generation error:', error);
1922+
1923+
// Rollback: Restore original content in UI
1924+
this.updateMessageAtIndex(messageIndex, {
1925+
content: originalContent,
1926+
thinking: originalThinking
1927+
});
1928+
1929+
// Ensure database has original content (in case of partial writes)
1930+
await DatabaseStore.updateMessage(messageToContinue.id, {
1931+
content: originalContent,
1932+
thinking: originalThinking
1933+
});
1934+
18541935
this.setConversationLoading(messageToContinue.convId, false);
18551936
this.clearConversationStreaming(messageToContinue.convId);
18561937
slotsService.clearConversationState(messageToContinue.convId);

0 commit comments

Comments
 (0)