diff --git a/AIClasses/Claude/Claude.ts b/AIClasses/Claude/Claude.ts index baa3647..f104755 100644 --- a/AIClasses/Claude/Claude.ts +++ b/AIClasses/Claude/Claude.ts @@ -11,6 +11,7 @@ import type AIAgentPlugin from "main"; import type { AIFunctionDefinitions } from "AIClasses/FunctionDefinitions/AIFunctionDefinitions"; import { isValidJson } from "Helpers/Helpers"; import type { ConversationContent } from "Conversations/ConversationContent"; +import { Role } from "Enums/Role"; export class Claude implements IAIClass { @@ -153,11 +154,12 @@ export class Claude implements IAIClass { return conversationContent.filter(content => content.content.trim() !== "" || content.functionCall.trim() !== "") .map(content => { const contentBlocks: any[] = []; + const contentToExtract = content.role == Role.User ? content.promptContent : content.content; - if (content.content.trim() !== "" && !content.isFunctionCallResponse) { + if (contentToExtract.trim() !== "" && !content.isFunctionCallResponse) { contentBlocks.push({ type: "text", - text: content.content + text: contentToExtract }); } @@ -175,7 +177,7 @@ export class Claude implements IAIClass { } catch (error) { console.error("Failed to parse function call:", error); // Fall back to treating as text - if (content.content.trim() === "") { + if (contentToExtract.trim() === "") { contentBlocks.push({ type: "text", text: "Error parsing function call" @@ -185,7 +187,7 @@ export class Claude implements IAIClass { } else { console.error("Invalid JSON in functionCall field"); // Fall back to treating as text - if (content.content.trim() === "") { + if (contentToExtract.trim() === "") { contentBlocks.push({ type: "text", text: "Error parsing function call" @@ -195,10 +197,10 @@ export class Claude implements IAIClass { } // Add function response if present - if (content.isFunctionCallResponse && content.content.trim() !== "") { - if (isValidJson(content.content)) { + if (content.isFunctionCallResponse && contentToExtract.trim() !== "") { + if (isValidJson(contentToExtract)) { try { - const parsedContent = JSON.parse(content.content); + const parsedContent = JSON.parse(contentToExtract); contentBlocks.push({ type: "tool_result", tool_use_id: parsedContent.id, @@ -208,14 +210,14 @@ export class Claude implements IAIClass { console.error("Failed to parse function response:", error); contentBlocks.push({ type: "text", - text: content.content + text: contentToExtract }); } } else { console.error("Invalid JSON in function response content"); contentBlocks.push({ type: "text", - text: content.content + text: contentToExtract }); } } diff --git a/AIClasses/Gemini/Gemini.ts b/AIClasses/Gemini/Gemini.ts index 7f92f66..ec81c39 100644 --- a/AIClasses/Gemini/Gemini.ts +++ b/AIClasses/Gemini/Gemini.ts @@ -159,29 +159,30 @@ export class Gemini implements IAIClass { return conversationContent.filter(content => content.content.trim() !== "" || content.functionCall.trim() !== "") .map(content => { const parts: any[] = []; + const contentToExtract = content.role == Role.User ? content.promptContent : content.content; - if (content.content.trim() !== "") { + if (contentToExtract.trim() !== "") { if (content.isFunctionCallResponse) { - if (isValidJson(content.content)) { + if (isValidJson(contentToExtract)) { try { - const parsedContent = JSON.parse(content.content); + const parsedContent = JSON.parse(contentToExtract); if (parsedContent.functionResponse) { parts.push({ functionResponse: parsedContent.functionResponse }); } else { - parts.push({ text: content.content }); + parts.push({ text: contentToExtract }); } } catch (error) { console.error("Failed to parse function response:", error); - parts.push({ text: content.content }); + parts.push({ text: contentToExtract }); } } else { console.error("Invalid JSON in function response content"); - parts.push({ text: content.content }); + parts.push({ text: contentToExtract }); } } else { - parts.push({ text: content.content }); + parts.push({ text: contentToExtract }); } } diff --git a/AIClasses/OpenAI/OpenAI.ts b/AIClasses/OpenAI/OpenAI.ts index 79e3bf0..ed6cd3d 100644 --- a/AIClasses/OpenAI/OpenAI.ts +++ b/AIClasses/OpenAI/OpenAI.ts @@ -52,8 +52,9 @@ export class OpenAI implements IAIClass { content: systemPrompt }, ...conversation.contents - .filter(content => content.content.trim() !== "" || content.functionCall.trim() !== "") - .map(content => { + .filter(content => content.content.trim() !== "" || content.functionCall.trim() !== "") + .map(content => { + const contentToExtract = content.role == Role.User ? content.promptContent : content.content; // Handle function call if (content.isFunctionCall && content.functionCall.trim() !== "") { if (isValidJson(content.functionCall)) { @@ -61,7 +62,7 @@ export class OpenAI implements IAIClass { const parsedContent = JSON.parse(content.functionCall); return { role: content.role, - content: content.content.trim() !== "" ? content.content : null, + content: contentToExtract.trim() !== "" ? contentToExtract : null, tool_calls: [ { id: parsedContent.functionCall.id, @@ -78,23 +79,23 @@ export class OpenAI implements IAIClass { // Fall back to regular message return { role: content.role, - content: content.content || "Error parsing function call" + content: contentToExtract || "Error parsing function call" }; } } else { console.error("Invalid JSON in functionCall field"); return { role: content.role, - content: content.content || "Error parsing function call" + content: contentToExtract || "Error parsing function call" }; } } // Handle function response - if (content.isFunctionCallResponse && content.content.trim() !== "") { - if (isValidJson(content.content)) { + if (content.isFunctionCallResponse && contentToExtract.trim() !== "") { + if (isValidJson(contentToExtract)) { try { - const parsedContent = JSON.parse(content.content); + const parsedContent = JSON.parse(contentToExtract); return { role: "tool", tool_call_id: parsedContent.id, @@ -105,14 +106,14 @@ export class OpenAI implements IAIClass { // Fall back to regular message return { role: content.role, - content: content.content + content: contentToExtract }; } } else { console.error("Invalid JSON in function response content"); return { role: content.role, - content: content.content + content: contentToExtract }; } } @@ -120,7 +121,7 @@ export class OpenAI implements IAIClass { // Regular text message return { role: content.role, - content: content.content + content: contentToExtract }; }) ]; diff --git a/Components/ChatInput.svelte b/Components/ChatInput.svelte index f02a7bc..713a0a5 100644 --- a/Components/ChatInput.svelte +++ b/Components/ChatInput.svelte @@ -9,7 +9,6 @@ import ChatSearchResults from "./ChatSearchResults.svelte"; import type { Writable } from "svelte/store"; import type { InputService } from "Services/InputService"; - import { textAreaNode } from "happy-dom/lib/PropertySymbol"; export let hasNoApiKey: boolean; export let isSubmitting: boolean; @@ -88,7 +87,7 @@ } inputService.deleteTextRange(position - 1, position, textareaElement); - + return; } @@ -131,7 +130,6 @@ inputService.deleteTextRange($searchState.position, inputService.getCursorPosition(textareaElement), textareaElement); inputService.insertElementAtCursor(node, textareaElement); - inputService.insertTextAtCursor(" ", textareaElement); } searchStateStore.resetSearch(); return; diff --git a/Services/ChatService.ts b/Services/ChatService.ts index 085a51e..58d3637 100644 --- a/Services/ChatService.ts +++ b/Services/ChatService.ts @@ -121,7 +121,7 @@ export class ChatService { const inputMessages = conversation.contents .filter(message => message.role === Role.User && !message.isFunctionCallResponse) - .map(message => message.content) + .map(message => message.promptContent) .join("\n"); const outputMessages = conversation.contents @@ -143,8 +143,7 @@ export class ChatService { private async streamRequestResponse( conversation: Conversation, allowDestructiveActions: boolean, callbacks: IChatServiceCallbacks ): Promise<{ functionCall: AIFunctionCall | null, shouldContinue: boolean }> { - // this should never happen - if (!this.ai) { + if (!this.ai) { // this should never happen return { functionCall: null, shouldContinue: false };; } diff --git a/__tests__/AIClasses/Claude.test.ts b/__tests__/AIClasses/Claude.test.ts index d0793d3..d803cd7 100644 --- a/__tests__/AIClasses/Claude.test.ts +++ b/__tests__/AIClasses/Claude.test.ts @@ -255,7 +255,7 @@ describe('Claude', () => { describe('extractContents', () => { it('should convert simple text content to Claude message format', () => { const contents = [ - new ConversationContent(Role.User, 'Hello'), + new ConversationContent(Role.User, 'Hello', 'Hello'), // content, promptContent new ConversationContent(Role.Assistant, 'Hi there') ]; @@ -301,14 +301,16 @@ describe('Claude', () => { }); it('should convert function response to tool_result format', () => { + const responseContent = JSON.stringify({ + id: 'call_123', + functionResponse: { + response: ['file1.txt', 'file2.txt'] + } + }); const functionResponseContent = new ConversationContent( Role.User, - JSON.stringify({ - id: 'call_123', - functionResponse: { - response: ['file1.txt', 'file2.txt'] - } - }) + responseContent, + responseContent // promptContent should also be set for User role ); functionResponseContent.isFunctionCallResponse = true; @@ -351,7 +353,8 @@ describe('Claude', () => { const invalidContent = new ConversationContent( Role.User, - 'invalid json {' + 'invalid json {', + 'invalid json {' // promptContent for User role ); invalidContent.isFunctionCallResponse = true; @@ -369,9 +372,9 @@ describe('Claude', () => { it('should filter out empty content', () => { const contents = [ - new ConversationContent(Role.User, 'Hello'), + new ConversationContent(Role.User, 'Hello', 'Hello'), new ConversationContent(Role.Assistant, ''), // Empty - new ConversationContent(Role.User, 'World') + new ConversationContent(Role.User, 'World', 'World') ]; const result = (claude as any).extractContents(contents); diff --git a/__tests__/AIClasses/Gemini.test.ts b/__tests__/AIClasses/Gemini.test.ts index 15db36d..556d7b1 100644 --- a/__tests__/AIClasses/Gemini.test.ts +++ b/__tests__/AIClasses/Gemini.test.ts @@ -309,7 +309,7 @@ describe('Gemini', () => { it('should format system instruction as parts array', async () => { const conversation = new Conversation(); - conversation.contents.push(new ConversationContent(Role.User, 'Test')); + conversation.contents.push(new ConversationContent(Role.User, 'Test', 'Test')); mockStreamingService.streamRequest.mockImplementation(async function* () { yield { content: 'done', isComplete: true }; @@ -357,14 +357,16 @@ describe('Gemini', () => { }); it('should convert function response to Gemini format', () => { + const responseContent = JSON.stringify({ + functionResponse: { + name: 'search_files', + response: ['file1.txt', 'file2.txt'] + } + }); const functionResponseContent = new ConversationContent( Role.User, - JSON.stringify({ - functionResponse: { - name: 'search_files', - response: ['file1.txt', 'file2.txt'] - } - }) + responseContent, + responseContent // promptContent for User role ); functionResponseContent.isFunctionCallResponse = true; @@ -407,7 +409,8 @@ describe('Gemini', () => { const invalidContent = new ConversationContent( Role.User, - 'invalid json {' + 'invalid json {', + 'invalid json {' // promptContent for User role ); invalidContent.isFunctionCallResponse = true; @@ -424,9 +427,9 @@ describe('Gemini', () => { it('should filter out empty content', () => { const contents = [ - new ConversationContent(Role.User, 'Hello'), + new ConversationContent(Role.User, 'Hello', 'Hello'), new ConversationContent(Role.Assistant, ''), // Empty - new ConversationContent(Role.User, 'World') + new ConversationContent(Role.User, 'World', 'World') ]; const result = (gemini as any).extractContents(contents); diff --git a/__tests__/AIClasses/OpenAI.test.ts b/__tests__/AIClasses/OpenAI.test.ts index 1f70ea3..e1588b9 100644 --- a/__tests__/AIClasses/OpenAI.test.ts +++ b/__tests__/AIClasses/OpenAI.test.ts @@ -358,14 +358,16 @@ describe('OpenAI', () => { it('should convert function response to role:tool format', async () => { const conversation = new Conversation(); + const responseContent = JSON.stringify({ + id: 'call_123', + functionResponse: { + response: ['file1.txt', 'file2.txt'] + } + }); const functionResponseContent = new ConversationContent( Role.User, - JSON.stringify({ - id: 'call_123', - functionResponse: { - response: ['file1.txt', 'file2.txt'] - } - }) + responseContent, + responseContent // promptContent for User role ); functionResponseContent.isFunctionCallResponse = true; conversation.contents.push(functionResponseContent); @@ -425,7 +427,7 @@ describe('OpenAI', () => { const invalidContent = new ConversationContent( Role.User, 'invalid json {', - false, + 'invalid json {', // promptContent for User role '' ); invalidContent.isFunctionCallResponse = true; @@ -451,9 +453,9 @@ describe('OpenAI', () => { it('should filter out empty content', async () => { const conversation = new Conversation(); - conversation.contents.push(new ConversationContent(Role.User, 'Hello')); - conversation.contents.push(new ConversationContent(Role.Assistant, '', false, '')); - conversation.contents.push(new ConversationContent(Role.User, 'World')); + conversation.contents.push(new ConversationContent(Role.User, 'Hello', 'Hello')); + conversation.contents.push(new ConversationContent(Role.Assistant, '', '', '')); + conversation.contents.push(new ConversationContent(Role.User, 'World', 'World')); mockStreamingService.streamRequest.mockImplementation(async function* () { yield { content: 'done', isComplete: true };