fix: better session handling

This commit is contained in:
2026-02-11 19:35:33 +01:00
parent 498bda542f
commit 898a90b864
4 changed files with 110 additions and 57 deletions

View File

@@ -247,27 +247,27 @@ export class OpenCodeManager {
const abortController = new AbortController(); const abortController = new AbortController();
this.abortControllers.set(conversationId, abortController); this.abortControllers.set(conversationId, abortController);
const modelId = conversation.model || 'claude-sonnet-4';
const provider = this.detectProvider(modelId);
// Get system prompt
const systemMessage = conversation.messages.find(m => m.role === 'system');
const systemPrompt = systemMessage?.content || await this.chatEngine.getDefaultSystemPrompt();
// Build message history from DB (excluding system messages)
const dbMessages = conversation.messages.filter(m => m.role !== 'system');
// Add the new user message
dbMessages.push({
conversationId,
role: 'user',
content: userMessage,
createdAt: new Date(),
});
let fullResponse = '';
const toolCallsCollected: Array<{ name: string; args: unknown }> = [];
try { try {
const modelId = conversation.model || 'claude-sonnet-4';
const provider = this.detectProvider(modelId);
// Get system prompt
const systemMessage = conversation.messages.find(m => m.role === 'system');
const systemPrompt = systemMessage?.content || await this.chatEngine.getDefaultSystemPrompt();
// Build message history from DB (excluding system messages)
const dbMessages = conversation.messages.filter(m => m.role !== 'system');
// Add the new user message
dbMessages.push({
conversationId,
role: 'user',
content: userMessage,
createdAt: new Date(),
});
let fullResponse = '';
const toolCallsCollected: Array<{ name: string; args: unknown }> = [];
if (provider === 'anthropic') { if (provider === 'anthropic') {
const result = await this.sendAnthropicMessage( const result = await this.sendAnthropicMessage(
modelId, modelId,
@@ -288,34 +288,40 @@ export class OpenCodeManager {
); );
fullResponse = result.content; fullResponse = result.content;
} }
} catch (error) {
// Save assistant response const isAborted = abortController.signal.aborted || (error as Error).message === 'Request cancelled';
if (fullResponse) { if (!isAborted) {
await this.chatEngine.addMessage({ throw error;
conversationId,
role: 'assistant',
content: fullResponse,
toolCalls: toolCallsCollected.length > 0 ? JSON.stringify(toolCallsCollected) : undefined,
createdAt: new Date(),
});
} }
// On abort, keep whatever was streamed so far (already in fullResponse or empty)
// Generate title after first exchange
const userMsgCount = conversation.messages.filter(m => m.role === 'user').length;
if (userMsgCount === 0 && fullResponse) {
this.generateConversationTitle(conversationId, userMessage, fullResponse).catch(err =>
console.error('[OpenCodeManager] Error generating title:', err)
);
}
return {
success: true,
message: fullResponse,
toolCalls: toolCallsCollected.length > 0 ? toolCallsCollected : undefined,
};
} finally { } finally {
this.abortControllers.delete(conversationId); this.abortControllers.delete(conversationId);
} }
// Save assistant response (including partial content from aborted requests)
if (fullResponse) {
await this.chatEngine.addMessage({
conversationId,
role: 'assistant',
content: fullResponse,
toolCalls: toolCallsCollected.length > 0 ? JSON.stringify(toolCallsCollected) : undefined,
createdAt: new Date(),
});
}
// Generate title after first exchange
const userMsgCount = conversation.messages.filter(m => m.role === 'user').length;
if (userMsgCount === 0 && fullResponse) {
this.generateConversationTitle(conversationId, userMessage, fullResponse).catch(err =>
console.error('[OpenCodeManager] Error generating title:', err)
);
}
return {
success: true,
message: fullResponse,
toolCalls: toolCallsCollected.length > 0 ? toolCallsCollected : undefined,
};
} catch (error) { } catch (error) {
console.error('[OpenCodeManager] Error sending message:', error); console.error('[OpenCodeManager] Error sending message:', error);
return { success: false, error: (error as Error).message }; return { success: false, error: (error as Error).message };
@@ -338,6 +344,7 @@ export class OpenCodeManager {
): Promise<{ content: string; toolCalls: Array<{ name: string; args: unknown }> }> { ): Promise<{ content: string; toolCalls: Array<{ name: string; args: unknown }> }> {
const tools = this.getToolDefinitions(); const tools = this.getToolDefinitions();
const allToolCalls: Array<{ name: string; args: unknown }> = []; const allToolCalls: Array<{ name: string; args: unknown }> = [];
let accumulatedText = '';
// Convert DB messages to Anthropic format // Convert DB messages to Anthropic format
let messages = this.buildAnthropicMessages(dbMessages); let messages = this.buildAnthropicMessages(dbMessages);
@@ -376,6 +383,8 @@ export class OpenCodeManager {
const data = JSON.parse(response.body); const data = JSON.parse(response.body);
console.log('[OpenCodeManager] Round', round, 'stop_reason:', data.stop_reason, 'content blocks:', JSON.stringify(data.content?.map((b: AnthropicContentBlock) => ({ type: b.type, textLen: b.text?.length, name: b.name }))));
if (!data.content) { if (!data.content) {
throw new Error('API response missing content field'); throw new Error('API response missing content field');
} }
@@ -388,17 +397,22 @@ export class OpenCodeManager {
(b: AnthropicContentBlock) => b.type === 'text' (b: AnthropicContentBlock) => b.type === 'text'
); );
// Stream text content to frontend // Accumulate and stream text content to frontend
for (const block of textBlocks) { for (const block of textBlocks) {
if (block.text && callbacks.onDelta) { if (block.text) {
callbacks.onDelta(block.text); accumulatedText += block.text;
if (callbacks.onDelta) {
callbacks.onDelta(block.text);
}
} }
} }
console.log('[OpenCodeManager] Round', round, 'accumulatedText length:', accumulatedText.length, 'toolUseBlocks:', toolUseBlocks.length);
if (toolUseBlocks.length === 0 || data.stop_reason !== 'tool_use') { if (toolUseBlocks.length === 0 || data.stop_reason !== 'tool_use') {
// No more tool calls - extract final text and return // No more tool calls - return all accumulated text
const finalText = textBlocks.map((b: AnthropicContentBlock) => b.text || '').join(''); console.log('[OpenCodeManager] Returning accumulated text length:', accumulatedText.length);
return { content: finalText, toolCalls: allToolCalls }; return { content: accumulatedText, toolCalls: allToolCalls };
} }
// Execute tool calls // Execute tool calls
@@ -438,7 +452,8 @@ export class OpenCodeManager {
} }
// If we hit max rounds, return whatever we have // If we hit max rounds, return whatever we have
return { content: 'I reached the maximum number of tool calls. Please try again.', toolCalls: allToolCalls }; const fallbackText = accumulatedText || 'I reached the maximum number of tool calls. Please try again.';
return { content: fallbackText, toolCalls: allToolCalls };
} }
/** /**

View File

@@ -207,6 +207,8 @@
padding: 10px 14px; padding: 10px 14px;
border-radius: 12px; border-radius: 12px;
background-color: var(--vscode-input-background); background-color: var(--vscode-input-background);
user-select: text;
cursor: text;
} }
.chat-message.user .chat-message-text { .chat-message.user .chat-message-text {

View File

@@ -132,14 +132,33 @@ export const ChatPanel: React.FC<ChatPanelProps> = ({ conversationId }) => {
try { try {
// Send message and wait for complete response // Send message and wait for complete response
await window.electronAPI?.chat.sendMessage(conversationId, message); const result = await window.electronAPI?.chat.sendMessage(conversationId, message);
// Reload messages to get the saved assistant response // Use the streamed content we accumulated via onStreamDelta
const msgs = await window.electronAPI?.chat.getHistory(conversationId); const assistantContent = streamingRef.current;
if (msgs) setMessages(msgs);
if (assistantContent) {
const assistantMessage: ChatMessage = {
id: `assistant-${Date.now()}`,
conversationId,
role: 'assistant',
content: assistantContent,
createdAt: new Date().toISOString()
};
setMessages(prev => [...prev, assistantMessage]);
} else if (result && !result.success) {
// Backend returned an error (API failure, model unavailable, etc.)
const errorMessage: ChatMessage = {
id: `error-${Date.now()}`,
conversationId,
role: 'assistant',
content: `Error: ${result.error || 'Failed to get a response. Please try again.'}`,
createdAt: new Date().toISOString()
};
setMessages(prev => [...prev, errorMessage]);
}
} catch (error) { } catch (error) {
console.error('Failed to send message:', error); console.error('Failed to send message:', error);
// Add error message
const errorMessage: ChatMessage = { const errorMessage: ChatMessage = {
id: `error-${Date.now()}`, id: `error-${Date.now()}`,
conversationId, conversationId,
@@ -167,6 +186,23 @@ export const ChatPanel: React.FC<ChatPanelProps> = ({ conversationId }) => {
await window.electronAPI?.chat.abortMessage(conversationId); await window.electronAPI?.chat.abortMessage(conversationId);
} catch (error) { } catch (error) {
console.error('Failed to abort:', error); console.error('Failed to abort:', error);
} finally {
// Keep any streamed content as a visible message
const partialContent = streamingRef.current;
setIsStreaming(false);
setStreamingContent('');
streamingRef.current = '';
if (partialContent) {
const partialMessage: ChatMessage = {
id: `partial-${Date.now()}`,
conversationId,
role: 'assistant',
content: partialContent + '\n\n*(cancelled)*',
createdAt: new Date().toISOString()
};
setMessages(prev => [...prev, partialMessage]);
}
} }
}; };

View File

@@ -354,7 +354,7 @@ export interface ElectronAPI {
deleteConversation: (id: string) => Promise<boolean>; deleteConversation: (id: string) => Promise<boolean>;
// Messaging // Messaging
sendMessage: (conversationId: string, message: string) => Promise<string>; sendMessage: (conversationId: string, message: string) => Promise<{ success: boolean; message?: string; error?: string }>;
abortMessage: (conversationId: string) => Promise<void>; abortMessage: (conversationId: string) => Promise<void>;
getHistory: (conversationId: string) => Promise<ChatMessage[]>; getHistory: (conversationId: string) => Promise<ChatMessage[]>;
clearMessages: (conversationId: string) => Promise<void>; clearMessages: (conversationId: string) => Promise<void>;