diff --git a/src/LLMProviders/brevilabsClient.ts b/src/LLMProviders/brevilabsClient.ts index e0e75842..9e2764b4 100644 --- a/src/LLMProviders/brevilabsClient.ts +++ b/src/LLMProviders/brevilabsClient.ts @@ -83,27 +83,6 @@ export interface AutocompleteResponse { elapsed_time_ms: number; } -export interface ComposerPromptResponse { - prompt: string; -} - -export interface ComposerApplyResponse { - content: string; -} - -// Define interface for the composerApply request -export interface ComposerApplyRequest { - target_note: { - title: string; - content: string; - }; - chat_history: Array<{ - role: string; - content: string; - }>; - markdown_block: string; -} - export class BrevilabsClient { private static instance: BrevilabsClient; private pluginVersion: string = "Unknown"; @@ -299,33 +278,4 @@ export class BrevilabsClient { } return data; } - - async composerPrompt(): Promise { - const { data, error } = await this.makeRequest( - "/composer/prompt", - {}, - "GET" - ); - if (error) { - throw error; - } - if (!data) { - throw new Error("No data returned from composerPrompt"); - } - return data; - } - - async composerApply(request: ComposerApplyRequest): Promise { - const { data, error } = await this.makeRequest( - "/composer/apply", - request - ); - if (error) { - throw error; - } - if (!data) { - throw new Error("No data returned from composerApply"); - } - return data; - } } diff --git a/src/LLMProviders/chainRunner.ts b/src/LLMProviders/chainRunner.ts index 579b88c7..9aff8f08 100644 --- a/src/LLMProviders/chainRunner.ts +++ b/src/LLMProviders/chainRunner.ts @@ -1,6 +1,7 @@ import { getCurrentProject } from "@/aiParams"; import { getStandaloneQuestion } from "@/chainUtils"; -import { getComposerSystemPrompt } from "@/composerUtils"; +import { Composer } from "./composer"; +import { getSystemPrompt } from "@/settings/model"; import { ABORT_REASON, AI_SENDER, @@ -88,10 +89,6 @@ abstract class BaseChainRunner implements ChainRunner { this.chainManager = chainManager; } - protected async getSystemPrompt(): Promise { - return getComposerSystemPrompt(); - } - abstract run( userMessage: ChatMessage, abortController: AbortController, @@ -437,7 +434,6 @@ class CopilotPlusChainRunner extends BaseChainRunner { const messages: any[] = []; // Add system message if available - let fullSystemMessage = await this.getSystemPrompt(); // Add chat history context to system message if exists @@ -458,9 +454,8 @@ class CopilotPlusChainRunner extends BaseChainRunner { } // Add chat history - for (const [human, ai] of chatHistory) { - messages.push({ role: "user", content: human }); - messages.push({ role: "assistant", content: ai }); + for (const entry of chatHistory) { + messages.push({ role: entry.role, content: entry.content }); } // Get the current chat model @@ -553,9 +548,9 @@ class CopilotPlusChainRunner extends BaseChainRunner { if (debug) console.log("==== Step 1: Analyzing intent ===="); let toolCalls; + // Use the original message for intent analysis + const messageForAnalysis = userMessage.originalMessage || userMessage.message; try { - // Use the original message for intent analysis - const messageForAnalysis = userMessage.originalMessage || userMessage.message; toolCalls = await IntentAnalyzer.analyzeIntent(messageForAnalysis); } catch (error: any) { return this.handleResponse( @@ -580,15 +575,15 @@ class CopilotPlusChainRunner extends BaseChainRunner { (output) => output.tool === "localSearch" && output.output && output.output.length > 0 ); + // Format chat history from memory + const memory = this.chainManager.memoryManager.getMemory(); + const memoryVariables = await memory.loadMemoryVariables({}); + const chatHistory = extractChatHistory(memoryVariables); + if (localSearchResult) { if (debug) console.log("==== Step 2: Processing local search results ===="); const documents = JSON.parse(localSearchResult.output); - // Format chat history from memory - const memory = this.chainManager.memoryManager.getMemory(); - const memoryVariables = await memory.loadMemoryVariables({}); - const chatHistory = extractChatHistory(memoryVariables); - if (debug) console.log("==== Step 3: Condensing Question ===="); const standaloneQuestion = await getStandaloneQuestion(cleanedUserMessage, chatHistory); if (debug) console.log("Condensed standalone question: ", standaloneQuestion); @@ -607,7 +602,7 @@ class CopilotPlusChainRunner extends BaseChainRunner { if (debug) console.log("==== Step 5: Invoking QA Chain ===="); const qaPrompt = await this.chainManager.promptManager.getQAPrompt({ question: enhancedQuestion, - context: context, + context, systemMessage: "", // System prompt is added separately in streamMultimodalResponse }); @@ -622,16 +617,23 @@ class CopilotPlusChainRunner extends BaseChainRunner { // Append sources to the response sources = this.getSources(documents); } else { - const enhancedUserMessage = this.prepareEnhancedUserMessage( - cleanedUserMessage, - toolOutputs - ); + // Enhance with tool outputs. + let enhancedUserMessage = this.prepareEnhancedUserMessage(cleanedUserMessage, toolOutputs); // If no results, default to LLM Chain if (debug) { console.log("No local search results. Using standard LLM Chain."); console.log("Enhanced user message:", enhancedUserMessage); } + // Enhance with composer output. + if (messageForAnalysis.includes("@composer")) { + enhancedUserMessage = await Composer.getInstance().prepareUserMessageWithComposerOutput( + messageForAnalysis, + enhancedUserMessage, + chatHistory, + debug + ); + } fullAIResponse = await this.streamMultimodalResponse( enhancedUserMessage, userMessage, @@ -765,6 +767,25 @@ class CopilotPlusChainRunner extends BaseChainRunner { ? `Local Search Result for ${timeExpression}:\n${formattedDocs}` : `Local Search Result:\n${formattedDocs}`; } + + protected async getSystemPrompt(): Promise { + // get current project + const projectConfig = getCurrentProject(); + + if (!projectConfig) { + return getSystemPrompt(); + } + + // Get cached context synchronously + const context = ProjectManager.instance.getProjectContext(projectConfig.id); + let finalPrompt = projectConfig.systemPrompt; + + if (context) { + finalPrompt = `${finalPrompt}\n\n${context}`; + } + + return finalPrompt; + } } class ProjectChainRunner extends CopilotPlusChainRunner { diff --git a/src/LLMProviders/composer.ts b/src/LLMProviders/composer.ts new file mode 100644 index 00000000..39f65e53 --- /dev/null +++ b/src/LLMProviders/composer.ts @@ -0,0 +1,251 @@ +import { ChatHistoryEntry } from "@/utils"; +import { BaseChatModelCallOptions } from "@langchain/core/language_models/chat_models"; +import ProjectManager from "./projectManager"; +import { Change, diffTrimmedLines } from "diff"; +import { App, TFile } from "obsidian"; +import { StructuredOutputParser } from "langchain/output_parsers"; +import { z } from "zod"; + +interface ComposerNote { + note_path: string; + note_content: string; +} + +interface ComposerResponse { + notes?: ComposerNote[]; + error?: string; +} + +export class Composer { + private static instance: Composer; + private static changesMap: { [key: string]: Change[] } = {}; + private constructor() {} + + public static getInstance(): Composer { + if (!Composer.instance) { + Composer.instance = new Composer(); + } + return Composer.instance; + } + + public static getChanges(notePath: string): Change[] { + return Composer.changesMap[notePath] || []; + } + + // Group changes into blocks for better UI presentation + public static getChangeBlocks(changes: Change[]): Change[][] { + const blocks: Change[][] = []; + let currentBlock: Change[] = []; + + changes.forEach((change) => { + if (change.added || change.removed) { + currentBlock.push(change); + } else { + if (currentBlock.length > 0) { + blocks.push(currentBlock); + currentBlock = []; + } + blocks.push([change]); + } + }); + if (currentBlock.length > 0) { + blocks.push(currentBlock); + } + return blocks; + } + + private async composerResponse( + content: string, + chatHistory: ChatHistoryEntry[], + debug: boolean + ): Promise { + const composerSystemPrompt = ` + You are a helpful assistant that creates markdown notes for obsidian users. + + # Task + Your task is to generate one or more markdown notes: + 1. For editing existing notes - Return the updated markdown note content and the original note path. + 2. For creating new notes - Return the new markdown note content and the new note path based on user's request. + 3. If user's request is not clear, such as the note path is not provided, return an error message. + 4. Do no include the title to the note content. + 5. You can return multiple notes if the user's request involves creating multiple notes. + + # Important JSON Formatting Rules + 1. All newlines in string values must be escaped as \\n + 2. The response must be valid JSON that can be parsed + + Below is the chat history the user and the previous assistant have had. You should continue the conversation if necessary but respond in a different format. + + ${chatHistory.map((entry) => `${entry.role}: ${entry.content}`).join("\n")} + `; + + // Define the schema for the output + const schema = z.object({ + notes: z + .array( + z.object({ + note_path: z.string().refine((val) => val.endsWith(".md"), { + message: "note_path must end with .md", + }), + note_content: z.string(), + }) + ) + .optional(), + error: z.string().optional(), + }); + + // Create the output parser + const parser = StructuredOutputParser.fromZodSchema(schema); + + const messages: any[] = [ + { + role: "system", + content: composerSystemPrompt + "\n\n" + parser.getFormatInstructions(), + }, + ]; + + // Get the current chat model + const chatModel = ProjectManager.instance + .getCurrentChainManager() + .chatModelManager.getChatModel() + .bind({ + temperature: 0, + maxTokens: 16000, + } as BaseChatModelCallOptions); + + // Add current user message + messages.push({ + role: "user", + content: content, + }); + + if (debug) { + console.log("==== Composer Request ====\n", messages); + } + + try { + const response = await chatModel.invoke(messages); + const responseContent = + typeof response.content === "string" ? response.content : JSON.stringify(response.content); + console.log("==== Composer Response ====\n", responseContent); + return await parser.parse(responseContent); + } catch (error) { + console.error("Error parsing composer response:", error); + return { + error: `Error parsing composer response: ${error.message}`, + }; + } + } + + public async prepareUserMessageWithComposerOutput( + originalMessage: string, + messageWithContext: string, + chatHistory: ChatHistoryEntry[], + debug: boolean + ): Promise { + const composerOutput = await this.composerResponse(messageWithContext, chatHistory, debug); + if (debug) { + console.log("==== Composer Output ====\n", composerOutput); + } + if (!composerOutput.error) { + const notes = composerOutput.notes || []; + const changesMarkdownPromises = notes.map(async (note: ComposerNote) => { + const changesMarkdown = await this.getChangesMarkdown( + app, + note.note_path, + note.note_content, + debug + ); + return `\`\`\`markdown + +${changesMarkdown} +\`\`\``; + }); + const changesMarkdownBlocks = await Promise.all(changesMarkdownPromises); + + return `User message: ${originalMessage} + +The user is calling @composer tool to generate note content. Below are the markdown blocks representing the changes from the @composer output: + +${changesMarkdownBlocks.join("\n\n")} + +Return the markdown blocks above directly and add a brief summary of the changes at the end.`; + } else { + return `User message: ${originalMessage} + +The user is calling @composer tool to generate note content. However, the note content was not created successfully. Below is the output + +${composerOutput.error}`; + } + } + + // Break content into lines and wrap each line in ~~ + private strikeThrough(content: string): string { + const lines = content.trim().split("\n"); + return lines.map((line) => "- " + line).join("\n"); + } + + // Get relevant changes only and combine them into a single markdown block + private getRelevantChangesMarkdown(blocks: Change[][]): string { + const renderedChanges = blocks + .map((block) => { + const hasAddedChanges = block.some((change) => change.added); + const hasRemovedChanges = block.some((change) => change.removed); + let blockChange = ""; + if (hasAddedChanges) { + blockChange = block.map((change) => (change.added ? change.value : "")).join("\n"); + } else if (hasRemovedChanges) { + blockChange = block + .map((change) => (change.removed ? this.strikeThrough(change.value) : "")) + .join("\n"); + } else { + blockChange = "..."; + } + return blockChange; + }) + .join("\n"); + return renderedChanges; + } + + private async getChangesMarkdown( + app: App, + path: string, + newContent: string, + debug: boolean = false + ): Promise { + try { + // Get the file from the path + const file = app.vault.getAbstractFileByPath(path); + if (!file) { + // If the file does not exist, return the newContent directly + return newContent; + } + + if (!(file instanceof TFile)) { + throw new Error(`Path is not a file: ${path}`); + } + + // Read the original content + const originalContent = await app.vault.read(file); + // Get the diff + const changes = diffTrimmedLines(originalContent, newContent, { newlineIsToken: true }); + // Cache the changes to be used by the Apply view. + Composer.changesMap[path] = changes; + + if (debug) { + console.log("==== Changes ====\n", changes); + } + + // Group changes into blocks + const blocks = Composer.getChangeBlocks(changes); + + // Process blocks into markdown + const markdownChanges = this.getRelevantChangesMarkdown(blocks); + + return markdownChanges; + } catch (error) { + console.error("Error getting changes markdown:", error); + throw error; + } + } +} diff --git a/src/LLMProviders/intentAnalyzer.ts b/src/LLMProviders/intentAnalyzer.ts index 61165b49..1efd6bc4 100644 --- a/src/LLMProviders/intentAnalyzer.ts +++ b/src/LLMProviders/intentAnalyzer.ts @@ -15,7 +15,7 @@ import { Vault } from "obsidian"; import ProjectManager from "@/LLMProviders/projectManager"; // TODO: Add @index with explicit pdf files in chat context menu -export const COPILOT_TOOL_NAMES = ["@vault", "@web", "@youtube", "@pomodoro"]; +export const COPILOT_TOOL_NAMES = ["@vault", "@composer", "@web", "@youtube", "@pomodoro"]; type ToolCall = { tool: any; diff --git a/src/chainUtils.ts b/src/chainUtils.ts index 1ff7d096..f3029af1 100644 --- a/src/chainUtils.ts +++ b/src/chainUtils.ts @@ -1,10 +1,10 @@ -import { removeThinkTags } from "@/utils"; +import { removeThinkTags, ChatHistoryEntry } from "@/utils"; import { BaseChatModelCallOptions } from "@langchain/core/language_models/chat_models"; import ProjectManager from "@/LLMProviders/projectManager"; export async function getStandaloneQuestion( question: string, - chatHistory: [string, string][] + chatHistory: ChatHistoryEntry[] ): Promise { const condenseQuestionTemplate = `Given the following conversation and a follow up question, summarize the conversation as context and keep the follow up question unchanged, in its original language. @@ -19,7 +19,7 @@ export async function getStandaloneQuestion( Standalone question:`; const formattedChatHistory = chatHistory - .map(([human, ai]) => `Human: ${human}\nAssistant: ${ai}`) + .map(({ role, content }) => `${role}: ${content}`) .join("\n"); const chatModel = ProjectManager.instance diff --git a/src/components/Chat.tsx b/src/components/Chat.tsx index 6fa93c84..c3e1eca8 100644 --- a/src/components/Chat.tsx +++ b/src/components/Chat.tsx @@ -11,7 +11,6 @@ import { ChatControls } from "@/components/chat-components/ChatControls"; import ChatInput from "@/components/chat-components/ChatInput"; import ChatMessages from "@/components/chat-components/ChatMessages"; import { ProjectList } from "@/components/chat-components/ProjectList"; -import { resetComposerPromptCache } from "@/composerUtils"; import { ABORT_REASON, COMMAND_IDS, EVENT_NAMES, LOADING_MESSAGES, USER_SENDER } from "@/constants"; import { AppContext, EventTargetContext } from "@/context"; import { ContextProcessor } from "@/contextProcessor"; @@ -600,8 +599,6 @@ ${chatContent}`; } clearMessages(); chainManager.memoryManager.clearChatMemory(); - // Reset the composer prompt cache when starting a new chat - resetComposerPromptCache(); setCurrentAiMessage(""); setContextNotes([]); setIncludeActiveNote(false); diff --git a/src/components/chat-components/ChatSingleMessage.tsx b/src/components/chat-components/ChatSingleMessage.tsx index f292f322..fb2b9559 100644 --- a/src/components/chat-components/ChatSingleMessage.tsx +++ b/src/components/chat-components/ChatSingleMessage.tsx @@ -11,7 +11,7 @@ import { Bot, User } from "lucide-react"; import { App, Component, MarkdownRenderer, MarkdownView, TFile } from "obsidian"; import React, { useCallback, useEffect, useRef, useState } from "react"; import { createRoot, Root } from "react-dom/client"; -import { CodeBlock } from "./CodeBlock"; +import { ComposerCodeBlock } from "./ComposerCodeBlock"; function MessageContext({ context }: { context: ChatMessage["context"] }) { if (!context || (context.notes.length === 0 && context.urls.length === 0)) { @@ -255,13 +255,7 @@ const ChatSingleMessage: React.FC = ({ const root = createRoot(container); roots.push(root); if (!isUnmounting) { - root.render( - - ); + root.render(); } } }); diff --git a/src/components/chat-components/ComposerCodeBlock.tsx b/src/components/chat-components/ComposerCodeBlock.tsx new file mode 100644 index 00000000..e15d6c17 --- /dev/null +++ b/src/components/chat-components/ComposerCodeBlock.tsx @@ -0,0 +1,105 @@ +import React from "react"; +import { Button } from "@/components/ui/button"; +import { Check } from "lucide-react"; +import { APPLY_VIEW_TYPE } from "@/components/composer/ApplyView"; +import { Composer } from "@/LLMProviders/composer"; +import { logError } from "@/logger"; +import { Notice } from "obsidian"; +import { TFile } from "obsidian"; + +interface ComposerCodeBlockProps { + code: string; + path: string; +} + +export const ComposerCodeBlock: React.FC = ({ path, code }) => { + const handleApply = async () => { + if (!path) return; + + try { + const changes = Composer.getChanges(path); + let file = app.vault.getAbstractFileByPath(path); + + let isNewFile = false; + + // If file doesn't exist, create it + if (!file) { + try { + // Create the folder if it doesn't exist + if (path.includes("/")) { + const folderPath = path.split("/").slice(0, -1).join("/"); + const folder = app.vault.getAbstractFileByPath(folderPath); + if (!folder) { + await app.vault.createFolder(folderPath); + } + } + file = await app.vault.create(path, code); + if (file) { + new Notice(`Created new file: ${path}`); + isNewFile = true; + } else { + new Notice(`Failed to create file: ${path}`); + return; + } + + isNewFile = true; + } catch (createError) { + logError("Error creating file:", createError); + new Notice(`Failed to create file: ${createError.message}`); + return; + } + } + + if (!(file instanceof TFile)) { + new Notice(`Path is not a file: ${path}`); + return; + } + + // Check if the current active note is the same as the target note + const activeFile = app.workspace.getActiveFile(); + if (!activeFile || activeFile.path !== path) { + // If not, open the target file in the current leaf + await app.workspace.getLeaf().openFile(file); + new Notice(`Switched to ${file.name}`); + } + + // If the file is newly created, don't show the apply view + if (isNewFile) { + return; + } + + // Open the Apply View in a new leaf with the processed content + const leaf = app.workspace.getLeaf(true); + await leaf.setViewState({ + type: APPLY_VIEW_TYPE, + active: true, + state: { + changes, + path, + }, + }); + } catch (error) { + logError("Error calling composer apply:", error); + new Notice(`Error processing code: ${error.message}`); + } + }; + + return ( +
+ {path && ( +
+
{path}
+ { + + } +
+ )} +
+        {code}
+      
+
+ ); +}; diff --git a/src/components/composer/ApplyView.tsx b/src/components/composer/ApplyView.tsx index 99f096cf..05d87662 100644 --- a/src/components/composer/ApplyView.tsx +++ b/src/components/composer/ApplyView.tsx @@ -1,20 +1,19 @@ -import { ApplyChangesConfirmModal } from "@/components/modals/ApplyChangesConfirmModal"; import { cn } from "@/lib/utils"; import { logError } from "@/logger"; -import { Change, diffLines } from "diff"; +import { Change } from "diff"; import { Check, X as XIcon } from "lucide-react"; import { App, ItemView, Notice, TFile, WorkspaceLeaf } from "obsidian"; -import React, { useMemo, useRef, useState } from "react"; +import React, { useRef } from "react"; import { createRoot } from "react-dom/client"; import { Button } from "../ui/button"; +import { Composer } from "@/LLMProviders/composer"; +import { useState } from "react"; export const APPLY_VIEW_TYPE = "obsidian-copilot-apply-view"; export interface ApplyViewState { - file: TFile; - originalContent: string; - newContent: string; - path?: string; + changes: Change[]; + path: string; } // Extended Change interface to track user decisions @@ -80,64 +79,25 @@ interface ApplyViewRootProps { } const ApplyViewRoot: React.FC = ({ app, state, close }) => { - // Initialize diff with extended properties - moved before conditional const [diff, setDiff] = useState(() => { - if (!state?.originalContent || !state?.newContent) { - return []; - } - const initialDiff = diffLines(state.originalContent, state.newContent); - return initialDiff.map((change) => ({ + return state.changes.map((change) => ({ ...change, accepted: null, // Start with null (undecided) })); }); - const undecidedChanges = diff.filter( - (change) => (change.added || change.removed) && change.accepted === null - ); - const hasAnyDecidedChanges = diff.some((change) => change.accepted !== null); - // Group changes into blocks for better UI presentation - const changeBlocks = useMemo(() => { - if (!diff.length) return; - - const blocks: ExtendedChange[][] = []; - let currentBlock: ExtendedChange[] = []; - let inChangeBlock = false; - - diff.forEach((change) => { - if (change.added || change.removed) { - if (!inChangeBlock) { - inChangeBlock = true; - currentBlock = []; - } - currentBlock.push(change); - } else { - if (inChangeBlock) { - blocks.push([...currentBlock]); - currentBlock = []; - inChangeBlock = false; - } - blocks.push([change]); - } - }); - - if (currentBlock.length > 0) { - blocks.push(currentBlock); - } - - return blocks; - }, [diff]); + const changeBlocks = Composer.getChangeBlocks(diff); // Add refs to track change blocks const blockRefs = useRef<(HTMLDivElement | null)[]>([]); // Add defensive check for state after hooks - if (!state || !state.originalContent || !state.newContent) { + if (!state || !state.changes) { logError("Invalid state:", state); return (
-
Error: Invalid state - missing content
+
Error: Invalid state - missing changes
@@ -145,52 +105,15 @@ const ApplyViewRoot: React.FC = ({ app, state, close }) => { ); } - // Handle applying changes that have been marked as accepted - const handleApply = async () => { - try { - const applyChanges = async () => { - const newContent = diff - .filter((change) => { - if (change.added) return change.accepted === true; - else if (change.removed) return change.accepted === false; - return true; // Unchanged lines are always included - }) - .map((change) => change.value) - .join(""); - - await app.vault.modify(state.file, newContent); - new Notice("Changes applied successfully"); - close(); - }; - - if (undecidedChanges.length > 0) { - // Divide by 2 because each change has a pair of added and removed lines - const modal = new ApplyChangesConfirmModal(app, undecidedChanges.length / 2, () => { - applyChanges(); - }); - modal.open(); - return; - } - - applyChanges(); - } catch (error) { - logError("Error applying changes:", error); - new Notice(`Error applying changes: ${error.message}`); - } - }; - // Apply all changes regardless of whether they have been marked as accepted - const handleAcceptAll = async () => { + const handleAccept = async () => { try { - const newContent = diff - .filter((change) => { - return change.added || !change.removed; - }) - .map((change) => change.value) - .join(""); - await app.vault.modify(state.file, newContent); - new Notice("Changes applied successfully"); - close(); + // Mark all undecided changes as accepted + const updatedDiff = diff.map((change) => + change.accepted === null ? { ...change, accepted: true } : change + ); + + await applyDecidedChangesToFile(updatedDiff); } catch (error) { logError("Error applying changes:", error); new Notice(`Error applying changes: ${error.message}`); @@ -198,7 +121,41 @@ const ApplyViewRoot: React.FC = ({ app, state, close }) => { }; // Handle rejecting all changes - const handleReject = () => { + const handleReject = async () => { + try { + // Mark all undecided changes as rejected + const updatedDiff = diff.map((change) => + change.accepted === null ? { ...change, accepted: false } : change + ); + + await applyDecidedChangesToFile(updatedDiff); + } catch (error) { + logError("Error applying changes:", error); + new Notice(`Error applying changes: ${error.message}`); + } + }; + + // Shared function to apply changes to file + const applyDecidedChangesToFile = async (updatedDiff: ExtendedChange[]) => { + // Apply changes based on their accepted status + const newContent = updatedDiff + .filter((change) => { + if (change.added) return change.accepted === true; // Include if accepted + if (change.removed) return change.accepted === false; // Include if rejected + return true; // Keep unchanged lines + }) + .map((change) => change.value) + .join(""); + + const file = app.vault.getAbstractFileByPath(state.path); + if (!file || !(file instanceof TFile)) { + new Notice("File not found:" + state.path); + close(); + return; + } + + await app.vault.modify(file, newContent); + new Notice("Changes applied successfully"); close(); }; @@ -212,7 +169,7 @@ const ApplyViewRoot: React.FC = ({ app, state, close }) => { const block = changeBlocks[i]; const hasChanges = block.some((change) => change.added || change.removed); const isUndecided = block.some( - (change) => (change.added || change.removed) && change.accepted === null + (change) => (change.added || change.removed) && (change as ExtendedChange).accepted === null ); if (hasChanges && isUndecided) { @@ -284,23 +241,15 @@ const ApplyViewRoot: React.FC = ({ app, state, close }) => {
- -
- {state.path || state.file.path} + {state.path}
@@ -311,11 +260,14 @@ const ApplyViewRoot: React.FC = ({ app, state, close }) => { // Get the decision status for this block const blockStatus = hasChanges ? block.every( - (change) => (!change.added && !change.removed) || change.accepted === true + (change) => + (!change.added && !change.removed) || (change as ExtendedChange).accepted === true ) ? "accepted" : block.every( - (change) => (!change.added && !change.removed) || change.accepted === false + (change) => + (!change.added && !change.removed) || + (change as ExtendedChange).accepted === false ) ? "rejected" : "undecided" diff --git a/src/components/modals/ApplyChangesConfirmModal.tsx b/src/components/modals/ApplyChangesConfirmModal.tsx deleted file mode 100644 index 2dd0b1ce..00000000 --- a/src/components/modals/ApplyChangesConfirmModal.tsx +++ /dev/null @@ -1,15 +0,0 @@ -import { App } from "obsidian"; -import { ConfirmModal } from "./ConfirmModal"; - -export class ApplyChangesConfirmModal extends ConfirmModal { - constructor(app: App, unDecidedChanges: number, onConfirm: () => void) { - super( - app, - onConfirm, - `There are ${unDecidedChanges} changes that have not been decided. Are you sure you want to skip these changes?`, - "Apply Changes", - "Confirm", - "Back to Edit" - ); - } -} diff --git a/src/tools/toolManager.ts b/src/tools/toolManager.ts index 40e907f7..59b193e2 100644 --- a/src/tools/toolManager.ts +++ b/src/tools/toolManager.ts @@ -10,6 +10,8 @@ export const getToolDescription = (tool: string): string => { return "Get the transcript of a YouTube video. Example: @youtube "; case "@pomodoro": return "Start a pomodoro timer. Example: @pomodoro 25m"; + case "@composer": + return "Edit existing notes or create new notes."; default: return ""; } diff --git a/src/utils.ts b/src/utils.ts index 56dd1c3d..e02432fe 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -354,14 +354,23 @@ export function getSendChatContextNotesPrompt( ); } -export function extractChatHistory(memoryVariables: MemoryVariables): [string, string][] { - const chatHistory: [string, string][] = []; +export interface ChatHistoryEntry { + role: "user" | "assistant"; + content: string; +} + +export function extractChatHistory(memoryVariables: MemoryVariables): ChatHistoryEntry[] { + const chatHistory: ChatHistoryEntry[] = []; const { history } = memoryVariables; for (let i = 0; i < history.length; i += 2) { const userMessage = history[i]?.content || ""; const aiMessage = history[i + 1]?.content || ""; - chatHistory.push([userMessage, aiMessage]); + + chatHistory.push( + { role: "user", content: userMessage }, + { role: "assistant", content: aiMessage } + ); } return chatHistory;