fix: better session handling
This commit is contained in:
@@ -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 };
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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]);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
2
src/renderer/types/electron.d.ts
vendored
2
src/renderer/types/electron.d.ts
vendored
@@ -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>;
|
||||
|
||||
Reference in New Issue
Block a user