diff --git a/plugin/views/ai-chat/chat.tsx b/plugin/views/ai-chat/chat.tsx index 2bf64014..9644a7bc 100644 --- a/plugin/views/ai-chat/chat.tsx +++ b/plugin/views/ai-chat/chat.tsx @@ -74,7 +74,7 @@ export const ChatComponent: React.FC = ({ logger.debug("contextString", contextString); const chatBody = { - currentDatetime: moment().format("YYYY-MM-DDTHH:mm:ssZ"), + currentDatetime: window.moment().format("YYYY-MM-DDTHH:mm:ssZ"), enableScreenpipe: plugin.settings.enableScreenpipe, newUnifiedContext: contextString, }; diff --git a/plugin/views/ai-chat/tool-handlers/move-files-handler.tsx b/plugin/views/ai-chat/tool-handlers/move-files-handler.tsx new file mode 100644 index 00000000..e67df096 --- /dev/null +++ b/plugin/views/ai-chat/tool-handlers/move-files-handler.tsx @@ -0,0 +1,167 @@ +import React, { useState } from "react"; +import { TFile } from "obsidian"; +import { usePlugin } from "../../organizer/provider"; +import { ToolHandlerProps } from "./types"; + +interface FilePattern { + namePattern?: string; + extension?: string; +} + +interface MoveOperation { + sourcePath: string; + destinationPath: string; + pattern?: FilePattern; +} + +export function MoveFilesHandler({ + toolInvocation, + handleAddResult, +}: ToolHandlerProps) { + const plugin = usePlugin(); + const [isValidated, setIsValidated] = useState(false); + const [moveResults, setMoveResults] = useState([]); + const [filesToMove, setFilesToMove] = useState([]); + + // Simplified pattern matching without isRoot + const matchesPattern = (file: TFile, pattern?: FilePattern): boolean => { + if (!pattern) return true; + + const { namePattern, extension } = pattern; + + // Check file name pattern + if (namePattern) { + const regex = new RegExp(namePattern.replace("*", ".*")); + if (!regex.test(file.basename)) { + return false; + } + } + + // Check extension + if (extension && !file.extension.toLowerCase().includes(extension.toLowerCase())) { + return false; + } + + return true; + }; + + // Simplified file matching using sourcePath + const getMatchingFiles = (moveOp: MoveOperation): TFile[] => { + const allFiles = plugin.app.vault.getMarkdownFiles(); + + return allFiles.filter(file => { + // For root path, only match files directly in root + if (moveOp.sourcePath === "/") { + return !file.path.includes("/") && matchesPattern(file, moveOp.pattern); + } + + // For specific source paths + return file.path.startsWith(moveOp.sourcePath) && matchesPattern(file, moveOp.pattern); + }); + }; + + React.useEffect(() => { + if (!isValidated && !filesToMove.length) { + const { moves } = toolInvocation.args; + const matchedFiles = moves.flatMap(move => getMatchingFiles(move)); + setFilesToMove(matchedFiles); + } + }, [toolInvocation.args, isValidated]); + + const handleMoveFiles = async () => { + const { moves } = toolInvocation.args; + const results: string[] = []; + + for (const move of moves) { + try { + // Get matching files for this move operation + const matchingFiles = getMatchingFiles(move); + + // Create destination folder if it doesn't exist + await plugin.app.vault.createFolder(move.destinationPath).catch(() => {}); + + // Move each matching file + for (const file of matchingFiles) { + const newPath = `${move.destinationPath}/${file.name}`; + await plugin.app.fileManager.renameFile(file, newPath); + results.push(`✅ Moved: ${file.path} → ${newPath}`); + } + + if (matchingFiles.length === 0) { + results.push(`ℹ️ No files found matching criteria for ${move.sourcePath}`); + } + } catch (error) { + results.push(`❌ Error: ${error.message}`); + } + } + + setMoveResults(results); + setIsValidated(true); + handleAddResult(JSON.stringify({ success: true, results })); + }; + + return ( +
+
+ {toolInvocation.args.message || "Ready to move files"} +
+ + {!isValidated && filesToMove.length > 0 && ( +
+ Found {filesToMove.length} files to move: +
    + {filesToMove.slice(0, 5).map((file, i) => ( +
  • {file.path}
  • + ))} + {filesToMove.length > 5 && ( +
  • ...and {filesToMove.length - 5} more
  • + )} +
+
+ )} + + {moveResults.length > 0 && ( +
+ {moveResults.map((result, i) => ( +
+ {result} +
+ ))} +
+ )} + + {!isValidated && ( +
+ + +
+ )} +
+ ); +} \ No newline at end of file diff --git a/plugin/views/ai-chat/tool-handlers/onboard-handler.tsx b/plugin/views/ai-chat/tool-handlers/onboard-handler.tsx index 6aaf7d9f..aac62860 100644 --- a/plugin/views/ai-chat/tool-handlers/onboard-handler.tsx +++ b/plugin/views/ai-chat/tool-handlers/onboard-handler.tsx @@ -1,6 +1,7 @@ import React, { useState } from "react"; -import { TFolder } from "obsidian"; +import { TFile, TFolder } from "obsidian"; import { ToolHandlerProps } from "./types"; +import { addFileContext } from "../use-context-items"; interface FolderStructure { name: string; @@ -23,35 +24,74 @@ export function OnboardHandler({ const [isAnalyzing, setIsAnalyzing] = useState(false); const [isValidated, setIsValidated] = useState(false); - const analyzeFolderStructure = ( - folder: TFolder, + const getFilesFromPath = (path: string): TFile[] => { + if (path === "/") { + return app.vault.getMarkdownFiles(); + } + + const folder = app.vault.getAbstractFileByPath(path); + if (!folder || !(folder instanceof TFolder)) { + return []; + } + + const files: TFile[] = []; + folder.children.forEach(child => { + if (child instanceof TFile) { + files.push(child); + } + }); + return files; + }; + + const analyzeFolderStructure = async ( + path: string, depth = 0, - maxDepth = 3 - ): FolderStructure => { - if (depth >= maxDepth) return null; + maxDepth = 3, + shouldAddToContext = false + ) => { + const files = getFilesFromPath(path); + const structure = { + path, + files: await Promise.all(files.map(async file => { + const fileData = { + name: file.name, + path: file.path, + content: await app.vault.read(file), + type: "file" as const, + depth: depth + 1, + }; + + // Add to context if requested + if (shouldAddToContext) { + addFileContext({ + path: file.path, + title: file.basename, + content: fileData.content, + }); + } - const structure: FolderStructure = { - name: folder.name, - type: "folder", - children: [], + return fileData; + })), + subfolders: [], depth, }; - // Get immediate children - folder.children.forEach((child) => { - if (child instanceof TFolder) { - const subStructure = analyzeFolderStructure(child, depth + 1, maxDepth); - if (subStructure) { - structure.children.push(subStructure); + if (depth < maxDepth && path !== "/") { + const folder = app.vault.getAbstractFileByPath(path) as TFolder; + if (folder && folder instanceof TFolder) { + for (const child of folder.children) { + if (child instanceof TFolder) { + const subStructure = await analyzeFolderStructure( + child.path, + depth + 1, + maxDepth, + shouldAddToContext + ); + structure.subfolders.push(subStructure); + } } - } else { - structure.children.push({ - name: child.name, - type: "file", - depth: depth + 1, - }); } - }); + } return structure; }; @@ -59,23 +99,16 @@ export function OnboardHandler({ const handleAnalyze = async () => { setIsAnalyzing(true); try { - const rootFolder = app.vault.getRoot(); - const structure = analyzeFolderStructure(rootFolder); + const { path = "/", maxDepth = 3, addToContext = false } = toolInvocation.args; + const structure = await analyzeFolderStructure(path, 0, maxDepth, addToContext); - // Filter out system folders and files - const filteredStructure = { - ...structure, - children: structure.children.filter( - (child) => !child.name.startsWith(".") - ), - }; - setIsValidated(true); handleAddResult( JSON.stringify({ success: true, - structure: filteredStructure, - message: "Vault structure analyzed successfully", + structure, + analyzedPath: path, + message: `Vault structure analyzed successfully for path: ${path}`, }) ); } catch (error) { diff --git a/plugin/views/ai-chat/tool-handlers/tool-invocation-handler.tsx b/plugin/views/ai-chat/tool-handlers/tool-invocation-handler.tsx index abcb829f..20a06274 100644 --- a/plugin/views/ai-chat/tool-handlers/tool-invocation-handler.tsx +++ b/plugin/views/ai-chat/tool-handlers/tool-invocation-handler.tsx @@ -9,6 +9,7 @@ import { ScreenpipeHandler } from "./screenpipe-handler"; import { SettingsUpdateHandler } from "./settings-update-handler"; import { AppendContentHandler } from "./append-content-handler"; import { OnboardHandler } from "./onboard-handler"; +import { MoveFilesHandler } from "./move-files-handler"; interface ToolInvocationHandlerProps { toolInvocation: any; @@ -37,6 +38,7 @@ function ToolInvocationHandler({ generateSettings: "Settings Update", appendContentToFile: "Append Content", analyzeVaultStructure: "Vault Analysis", + moveFiles: "Moving Files", }; return toolTitles[toolName] || "Tool Invocation"; }; @@ -95,6 +97,13 @@ function ToolInvocationHandler({ app={app} /> ), + moveFiles: () => ( + + ), }; return handlers[toolInvocation.toolName]?.() || null; diff --git a/web/app/api/(newai)/chat/route.ts b/web/app/api/(newai)/chat/route.ts index b1a0c9e2..7d3c1b4e 100644 --- a/web/app/api/(newai)/chat/route.ts +++ b/web/app/api/(newai)/chat/route.ts @@ -95,7 +95,9 @@ export async function POST(req: NextRequest) { analyzeVaultStructure: { description: "Analyze vault structure to suggest organization improvements", parameters: z.object({ + path: z.string().describe("Path to analyze. Use '/' for all files or specific folder path"), maxDepth: z.number().optional().describe("Maximum depth to analyze"), + addToContext: z.boolean().optional().describe("Whether to add analyzed files to context") }), }, getScreenpipeDailySummary: { @@ -105,6 +107,20 @@ export async function POST(req: NextRequest) { endTime: z.string().optional().describe("End time in ISO format"), }), }, + moveFiles: { + description: "Move files to their designated folders", + parameters: z.object({ + moves: z.array(z.object({ + sourcePath: z.string().describe("Source path (e.g., '/' for root, or specific folder path)"), + destinationPath: z.string().describe("Destination folder path"), + pattern: z.object({ + namePattern: z.string().optional().describe("File name pattern to match (e.g., 'untitled-*')"), + extension: z.string().optional().describe("File extension to match") + }).optional() + })), + message: z.string().describe("Confirmation message to show user") + }), + }, }, onFinish: async ({ usage }) => { console.log("Token usage:", usage);