fix: better session handling
This commit is contained in:
@@ -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 };
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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]);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
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>;
|
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>;
|
||||||
|
|||||||
Reference in New Issue
Block a user