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,7 +247,6 @@ export class OpenCodeManager {
const abortController = new AbortController();
this.abortControllers.set(conversationId, abortController);
try {
const modelId = conversation.model || 'claude-sonnet-4';
const provider = this.detectProvider(modelId);
@@ -268,6 +267,7 @@ export class OpenCodeManager {
let fullResponse = '';
const toolCallsCollected: Array<{ name: string; args: unknown }> = [];
try {
if (provider === 'anthropic') {
const result = await this.sendAnthropicMessage(
modelId,
@@ -288,8 +288,17 @@ export class OpenCodeManager {
);
fullResponse = result.content;
}
} catch (error) {
const isAborted = abortController.signal.aborted || (error as Error).message === 'Request cancelled';
if (!isAborted) {
throw error;
}
// On abort, keep whatever was streamed so far (already in fullResponse or empty)
} finally {
this.abortControllers.delete(conversationId);
}
// Save assistant response
// Save assistant response (including partial content from aborted requests)
if (fullResponse) {
await this.chatEngine.addMessage({
conversationId,
@@ -313,9 +322,6 @@ export class OpenCodeManager {
message: fullResponse,
toolCalls: toolCallsCollected.length > 0 ? toolCallsCollected : undefined,
};
} finally {
this.abortControllers.delete(conversationId);
}
} catch (error) {
console.error('[OpenCodeManager] Error sending message:', error);
return { success: false, error: (error as Error).message };
@@ -338,6 +344,7 @@ export class OpenCodeManager {
): Promise<{ content: string; toolCalls: Array<{ name: string; args: unknown }> }> {
const tools = this.getToolDefinitions();
const allToolCalls: Array<{ name: string; args: unknown }> = [];
let accumulatedText = '';
// Convert DB messages to Anthropic format
let messages = this.buildAnthropicMessages(dbMessages);
@@ -376,6 +383,8 @@ export class OpenCodeManager {
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) {
throw new Error('API response missing content field');
}
@@ -388,17 +397,22 @@ export class OpenCodeManager {
(b: AnthropicContentBlock) => b.type === 'text'
);
// Stream text content to frontend
// Accumulate and stream text content to frontend
for (const block of textBlocks) {
if (block.text && callbacks.onDelta) {
if (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') {
// No more tool calls - extract final text and return
const finalText = textBlocks.map((b: AnthropicContentBlock) => b.text || '').join('');
return { content: finalText, toolCalls: allToolCalls };
// No more tool calls - return all accumulated text
console.log('[OpenCodeManager] Returning accumulated text length:', accumulatedText.length);
return { content: accumulatedText, toolCalls: allToolCalls };
}
// Execute tool calls
@@ -438,7 +452,8 @@ export class OpenCodeManager {
}
// 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;
border-radius: 12px;
background-color: var(--vscode-input-background);
user-select: text;
cursor: text;
}
.chat-message.user .chat-message-text {

View File

@@ -132,14 +132,33 @@ export const ChatPanel: React.FC<ChatPanelProps> = ({ conversationId }) => {
try {
// 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
const msgs = await window.electronAPI?.chat.getHistory(conversationId);
if (msgs) setMessages(msgs);
// Use the streamed content we accumulated via onStreamDelta
const assistantContent = streamingRef.current;
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) {
console.error('Failed to send message:', error);
// Add error message
const errorMessage: ChatMessage = {
id: `error-${Date.now()}`,
conversationId,
@@ -167,6 +186,23 @@ export const ChatPanel: React.FC<ChatPanelProps> = ({ conversationId }) => {
await window.electronAPI?.chat.abortMessage(conversationId);
} catch (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>;
// 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>;
getHistory: (conversationId: string) => Promise<ChatMessage[]>;
clearMessages: (conversationId: string) => Promise<void>;