@@ -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