Add automatic folder creation for file operations and improve chat session handling

Implement `ensureFolderExists` in `ToolExecutor` to create parent directories when creating, moving, or renaming notes.
Update `ChatView` to properly persist agent mode and derive session titles from the first user message. Fix error
handling in tool execution to return structured failures instead of null, and include failure details in follow-up
messages.
This commit is contained in:
2026-05-21 20:35:05 +02:00
parent d330b94816
commit b7b3a185a0
6 changed files with 204 additions and 28 deletions
+73 -16
View File
@@ -8968,6 +8968,30 @@ var ToolExecutor = class {
const file = this.getFile(path);
await this.vault.modify(file, content);
}
getParentFolderPath(path) {
const parts = path.split("/").filter((part) => part.length > 0);
parts.pop();
return parts.join("/");
}
async ensureFolderExists(folderPath) {
const normalizedFolder = folderPath.replace(/\/$/, "").trim();
if (!normalizedFolder) {
return;
}
const parts = normalizedFolder.split("/").filter((part) => part.length > 0);
let currentPath = "";
for (const part of parts) {
currentPath = currentPath ? `${currentPath}/${part}` : part;
const existing = this.vault.getAbstractFileByPath(currentPath);
if (existing) {
if (!(existing instanceof import_obsidian.TFolder)) {
throw new Error(`Cannot create folder ${currentPath}: a file already exists at that path`);
}
continue;
}
await this.vault.createFolder(currentPath);
}
}
async handleToolCall(toolCall) {
const startTime = Date.now();
const toolName = toolCall.function?.name ?? "unknown";
@@ -9062,6 +9086,7 @@ var ToolExecutor = class {
throw new Error("Invalid file path detected");
}
try {
await this.ensureFolderExists(this.getParentFolderPath(path));
await this.vault.create(path, content);
return { success: true, message: "Note created successfully" };
} catch (error) {
@@ -9285,6 +9310,7 @@ ${lines.join("\n")}
throw new Error("Invalid file path detected");
}
const file = this.getFile(oldPath);
await this.ensureFolderExists(this.getParentFolderPath(newPath));
await this.vault.rename(file, newPath);
return { success: true, message: `Note renamed from ${oldPath} to ${newPath}` };
}
@@ -9307,6 +9333,7 @@ ${lines.join("\n")}
const file = this.getFile(path);
const fileName = file.name;
const newPath = normalizedFolder ? `${normalizedFolder}/${fileName}` : fileName;
await this.ensureFolderExists(normalizedFolder);
await this.vault.rename(file, newPath);
return { success: true, message: `Note moved to ${newPath}` };
}
@@ -11022,6 +11049,7 @@ var ChatView = class extends import_obsidian5.ItemView {
newSettings.agentModel ?? newSettings.model,
{ cacheConfig: newSettings.cacheConfig }
);
this.conversationStateManager.setSystemPrompt(getSystemPromptForMode(this.currentAgentMode));
void this.initializeClientCaches().catch(() => {
new import_obsidian5.Notice(
"Semantic cache unavailable: could not connect to ChromaDB. Check the ChromaDB URL in settings."
@@ -11171,7 +11199,7 @@ var ChatView = class extends import_obsidian5.ItemView {
}
}
this.modeSelectorEl.addEventListener("change", () => {
this.currentAgentMode = this.modeSelectorEl.value;
this.setAgentMode(this.modeSelectorEl.value);
});
} else {
newChatContainer.appendChild(this.modeSelectorEl);
@@ -11368,6 +11396,10 @@ var ChatView = class extends import_obsidian5.ItemView {
this.modeSelectorEl.value = mode;
}
this.conversationStateManager.setSystemPrompt(getSystemPromptForMode(mode));
const activeId = this.chatHistoryManager?.getActiveSessionId();
if (activeId) {
this.chatHistoryManager?.updateSession(activeId, { agentMode: mode });
}
}
clearConversation() {
this.saveCurrentSession();
@@ -11409,14 +11441,28 @@ var ChatView = class extends import_obsidian5.ItemView {
const activeId = this.chatHistoryManager.getActiveSessionId();
if (!activeId) return;
const nonStreamingMessages = this.messages.filter((msg) => !msg.isStreaming);
this.chatHistoryManager.updateSessionMessages(activeId, nonStreamingMessages);
this.chatHistoryManager.updateSession(activeId, {
messages: nonStreamingMessages,
agentMode: this.currentAgentMode,
title: this.deriveSessionTitle(nonStreamingMessages)
});
}
syncMessagesToSession() {
if (!this.chatHistoryManager) return;
const activeId = this.chatHistoryManager.getActiveSessionId();
if (!activeId) return;
const nonStreamingMessages = this.messages.filter((msg) => !msg.isStreaming);
this.chatHistoryManager.updateSessionMessages(activeId, nonStreamingMessages);
this.chatHistoryManager.updateSession(activeId, {
messages: nonStreamingMessages,
agentMode: this.currentAgentMode,
title: this.deriveSessionTitle(nonStreamingMessages)
});
}
deriveSessionTitle(messages) {
const firstUser = messages.find((m) => m.role === "user");
const text = firstUser?.content.trim() ?? "";
if (!text) return "New Chat";
return text.length > 40 ? text.slice(0, 40) + "\u2026" : text;
}
populateHistoryDropdown() {
if (!this.historySelectEl) return;
@@ -11785,7 +11831,7 @@ var ChatView = class extends import_obsidian5.ItemView {
if (writePreviews.length > 0 && modeRequiresPreview(this.currentAgentMode)) {
this.pendingActions = writePreviews;
this.pendingReadResults = readResults;
this.pendingFollowUpContext = { messages, tools, assistantMessageId };
this.pendingFollowUpContext = { messages, tools, assistantMessageId, allToolCalls: toolCalls, assistantText: fullResponse };
this.updateMessageById(assistantMessageId, {
content: `${fullResponse}
@@ -11800,17 +11846,21 @@ ${writePreviews.map((a) => `- ${a.description}`).join("\n")}`,
}
let writeResults = [];
if (writePreviews.length > 0 && !modeRequiresPreview(this.currentAgentMode)) {
writeResults = (await Promise.all(
writeResults = await Promise.all(
writePreviews.map(async (action) => {
try {
const toolResult = await this.toolExecutor.handleToolCall(action.toolCall);
return { ...toolResult, id: action.toolCall.id };
} catch (error) {
ErrorHandler.handleError(error, "ChatView.processToolCalls");
return null;
return {
success: false,
message: error instanceof Error ? error.message : String(error),
id: action.toolCall.id
};
}
})
)).filter((result) => result !== null);
);
}
const allResults = [...readResults, ...writeResults];
const followUpMessages = allResults.map((result) => ({
@@ -11868,19 +11918,24 @@ ${writePreviews.map((a) => `- ${a.description}`).join("\n")}`,
if (this.pendingActions.length === 0 || !this.pendingFollowUpContext) {
return;
}
const { messages, tools, assistantMessageId } = this.pendingFollowUpContext;
const writeResults = (await Promise.all(
const { messages, tools, assistantMessageId, allToolCalls, assistantText } = this.pendingFollowUpContext;
const writeResults = await Promise.all(
this.pendingActions.map(async (action) => {
try {
const toolResult = await this.toolExecutor.handleToolCall(action.toolCall);
return { ...toolResult, id: action.toolCall.id };
} catch (error) {
ErrorHandler.handleError(error, "ChatView.applyPendingActions");
return null;
return {
success: false,
message: error instanceof Error ? error.message : String(error),
id: action.toolCall.id
};
}
})
)).filter((result) => result !== null);
);
const allResults = [...this.pendingReadResults, ...writeResults];
const failedWrites = writeResults.filter((result) => !result.success);
const followUpMessages = allResults.map((result) => ({
role: "tool",
content: JSON.stringify(result),
@@ -11888,8 +11943,8 @@ ${writePreviews.map((a) => `- ${a.description}`).join("\n")}`,
}));
const followUp = {
role: "assistant",
content: "I have processed your request using the following tools. Here are the results:",
tool_calls: this.pendingActions.map((a) => a.toolCall)
content: assistantText,
tool_calls: allToolCalls
};
if (followUpMessages.length > 0) {
const finalMessages = [...messages, followUp, ...followUpMessages];
@@ -11920,14 +11975,16 @@ ${writePreviews.map((a) => `- ${a.description}`).join("\n")}`,
);
} else {
this.updateMessageById(assistantMessageId, {
content: followUpContent || "Actions applied successfully.",
content: followUpContent || (failedWrites.length > 0 ? `Some actions failed:
${failedWrites.map((r) => `- ${r.message}`).join("\n")}` : "Actions applied successfully."),
isStreaming: false,
isThinking: false
});
}
} else {
this.updateMessageById(assistantMessageId, {
content: "Actions applied successfully.",
content: failedWrites.length > 0 ? `Actions failed:
${failedWrites.map((r) => `- ${r.message}`).join("\n")}` : "Actions applied successfully.",
isStreaming: false,
isThinking: false
});
@@ -12477,7 +12534,7 @@ ${actualMessage}` : actualMessage;
return this.isAgenticMode(this.currentAgentMode) ? this.agentOllamaClient : this.ollamaClient;
}
isAgenticMode(mode) {
return mode === "edit" || mode === "organize" || mode === "workflow";
return mode === "edit" || mode === "organize" || mode === "research" || mode === "workflow";
}
renderLogEntry(entry, container) {
const row = container.createEl("div", { cls: "ollama-log-row" });