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:
@@ -234,6 +234,18 @@ describe('ChatView', () => {
|
||||
const messages = view.contentEl.querySelectorAll('.ollama-message');
|
||||
expect(messages.length).toBe(1);
|
||||
});
|
||||
|
||||
it('should update the system prompt when mode selector changes', async () => {
|
||||
await view.render();
|
||||
const selector = view.contentEl.querySelector('.ollama-mode-selector') as HTMLSelectElement;
|
||||
selector.appendChild(new Option('Edit', 'edit'));
|
||||
selector.value = 'edit';
|
||||
selector.dispatchEvent(new Event('change'));
|
||||
|
||||
const systemPrompt = view['conversationStateManager'].getLongTermContext()[0].content;
|
||||
expect(view.getAgentMode()).toBe('edit');
|
||||
expect(systemPrompt).toContain('helps edit and manage notes');
|
||||
});
|
||||
});
|
||||
|
||||
describe('handleUserInput', () => {
|
||||
@@ -600,6 +612,8 @@ describe('ChatView', () => {
|
||||
messages: [],
|
||||
tools: [],
|
||||
assistantMessageId,
|
||||
allToolCalls: [],
|
||||
assistantText: 'test',
|
||||
};
|
||||
|
||||
const followUpSpy = jest
|
||||
@@ -653,6 +667,8 @@ describe('ChatView', () => {
|
||||
messages: [],
|
||||
tools: [],
|
||||
assistantMessageId,
|
||||
allToolCalls: [],
|
||||
assistantText: 'Proposed actions...',
|
||||
};
|
||||
|
||||
view.cancelPendingActions();
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { ToolExecutor } from '../src/tool-executor';
|
||||
import { TFile } from 'obsidian';
|
||||
import { TFile, TFolder } from 'obsidian';
|
||||
import { ToolCall, ToolResult } from '../src/types';
|
||||
import { ErrorHandler } from '../src/error-handler';
|
||||
import { TelemetryManager } from '../src/tool-telemetry';
|
||||
@@ -7,6 +7,7 @@ import { TelemetryManager } from '../src/tool-telemetry';
|
||||
// Mock Obsidian types
|
||||
interface MockVault {
|
||||
create: (path: string, content: string) => Promise<any>;
|
||||
createFolder: (path: string) => Promise<any>;
|
||||
getAbstractFileByPath: (path: string) => any;
|
||||
cachedRead: (file: any) => Promise<string>;
|
||||
getMarkdownFiles: () => any[];
|
||||
@@ -32,6 +33,7 @@ jest.mock('obsidian', () => {
|
||||
App: jest.fn(),
|
||||
Notice: jest.fn(),
|
||||
TFile,
|
||||
TFolder: class TFolder {},
|
||||
};
|
||||
});
|
||||
|
||||
@@ -50,6 +52,7 @@ describe('ToolExecutor', () => {
|
||||
beforeEach(() => {
|
||||
mockVault = {
|
||||
create: jest.fn().mockResolvedValue(null),
|
||||
createFolder: jest.fn().mockResolvedValue(null),
|
||||
getAbstractFileByPath: jest.fn(),
|
||||
cachedRead: jest.fn().mockResolvedValue(''),
|
||||
getMarkdownFiles: jest.fn().mockReturnValue([]),
|
||||
@@ -103,6 +106,7 @@ describe('ToolExecutor', () => {
|
||||
});
|
||||
|
||||
it('should successfully create a file in a subdirectory', async () => {
|
||||
mockVault.getAbstractFileByPath = jest.fn().mockReturnValue(null);
|
||||
const call: ToolCall = {
|
||||
id: 'call_3',
|
||||
type: 'function',
|
||||
@@ -116,12 +120,32 @@ describe('ToolExecutor', () => {
|
||||
};
|
||||
const result = await executor.handleToolCall(call);
|
||||
expect(result).toEqual({ success: true, message: 'Note created successfully' });
|
||||
expect(mockVault.createFolder).toHaveBeenCalledWith('subdirectory');
|
||||
expect(mockVault.create).toHaveBeenCalledWith(
|
||||
'subdirectory/test-file.md',
|
||||
'Subdir content'
|
||||
);
|
||||
});
|
||||
|
||||
it('should not recreate existing parent folders', async () => {
|
||||
mockVault.getAbstractFileByPath = jest.fn().mockReturnValue(new TFolder());
|
||||
const call: ToolCall = {
|
||||
id: 'call_existing_folder',
|
||||
type: 'function',
|
||||
function: {
|
||||
name: 'create_file',
|
||||
arguments: JSON.stringify({
|
||||
path: 'existing/test-file.md',
|
||||
content: 'Subdir content',
|
||||
}),
|
||||
},
|
||||
};
|
||||
const result = await executor.handleToolCall(call);
|
||||
expect(result).toEqual({ success: true, message: 'Note created successfully' });
|
||||
expect(mockVault.createFolder).not.toHaveBeenCalled();
|
||||
expect(mockVault.create).toHaveBeenCalledWith('existing/test-file.md', 'Subdir content');
|
||||
});
|
||||
|
||||
it('should handle multiple slashes gracefully by normalizing path', async () => {
|
||||
const call: ToolCall = {
|
||||
id: 'call_4',
|
||||
@@ -1170,7 +1194,9 @@ describe('ToolExecutor', () => {
|
||||
}
|
||||
}
|
||||
const file = new MockTFile('Projects/old.md');
|
||||
mockVault.getAbstractFileByPath = jest.fn().mockReturnValue(file);
|
||||
mockVault.getAbstractFileByPath = jest
|
||||
.fn()
|
||||
.mockImplementation((path: string) => (path === 'Projects/old.md' ? file : null));
|
||||
|
||||
const call: ToolCall = {
|
||||
id: 'call_mn1',
|
||||
@@ -1185,6 +1211,7 @@ describe('ToolExecutor', () => {
|
||||
};
|
||||
const result = await executor.handleToolCall(call);
|
||||
expect(result.success).toBe(true);
|
||||
expect(mockVault.createFolder).toHaveBeenCalledWith('Archive');
|
||||
expect(mockVault.rename).toHaveBeenCalledWith(file, 'Archive/old.md');
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user