Add configurable agent modes with per-mode tool filtering

Introduces `ask`, `edit`, `organize`, `research`, and `workflow` modes.
Each mode defines its own system prompt, allowed tools, and preview
requirements. The active mode can be switched via a dropdown in the
chat UI and defaults can be set in plugin settings. Mode selection
affects which tools are exposed to the LLM and whether write actions
require user preview before execution.
This commit is contained in:
2026-05-20 21:49:41 +02:00
parent 2f739c6f21
commit 2db7c34920
8 changed files with 461 additions and 10 deletions
+167
View File
@@ -0,0 +1,167 @@
import {
AgentMode,
ALL_AGENT_MODES,
DEFAULT_AGENT_MODE,
AGENT_MODE_CONFIGS,
getAgentModeLabel,
modeRequiresPreview,
getSystemPromptForMode,
filterToolsForMode,
} from '../src/agent-modes';
import { OllamaTool } from '../src/types';
describe('Agent Modes', () => {
const allTools: OllamaTool[] = [
{
type: 'function',
function: {
name: 'read_vault_file',
description: 'Read',
parameters: { type: 'object', properties: {} },
},
},
{
type: 'function',
function: {
name: 'search_vault_files',
description: 'Search',
parameters: { type: 'object', properties: {} },
},
},
{
type: 'function',
function: {
name: 'create_note',
description: 'Create',
parameters: { type: 'object', properties: {} },
},
},
{
type: 'function',
function: {
name: 'update_frontmatter',
description: 'Update frontmatter',
parameters: { type: 'object', properties: {} },
},
},
{
type: 'function',
function: {
name: 'delete_note',
description: 'Delete',
parameters: { type: 'object', properties: {} },
},
},
];
describe('constants', () => {
it('should define all expected modes', () => {
expect(ALL_AGENT_MODES).toEqual(['ask', 'edit', 'organize', 'research', 'workflow']);
});
it('should have default mode ask', () => {
expect(DEFAULT_AGENT_MODE).toBe('ask');
});
});
describe('getAgentModeLabel', () => {
it('should return labels for known modes', () => {
expect(getAgentModeLabel('ask')).toBe('Ask');
expect(getAgentModeLabel('edit')).toBe('Edit');
expect(getAgentModeLabel('organize')).toBe('Organize');
expect(getAgentModeLabel('research')).toBe('Research');
expect(getAgentModeLabel('workflow')).toBe('Workflow');
});
it('should fallback to raw mode name for unknown modes', () => {
expect(getAgentModeLabel('unknown' as AgentMode)).toBe('unknown');
});
});
describe('modeRequiresPreview', () => {
it('should require preview for edit and organize', () => {
expect(modeRequiresPreview('edit')).toBe(true);
expect(modeRequiresPreview('organize')).toBe(true);
});
it('should not require preview for ask, research, workflow', () => {
expect(modeRequiresPreview('ask')).toBe(false);
expect(modeRequiresPreview('research')).toBe(false);
expect(modeRequiresPreview('workflow')).toBe(false);
});
});
describe('getSystemPromptForMode', () => {
it('should return a non-empty prompt for each mode', () => {
for (const mode of ALL_AGENT_MODES) {
const prompt = getSystemPromptForMode(mode);
expect(typeof prompt).toBe('string');
expect(prompt.length).toBeGreaterThan(0);
}
});
it('should fallback to ask prompt for unknown mode', () => {
const askPrompt = getSystemPromptForMode('ask');
const fallback = getSystemPromptForMode('unknown' as AgentMode);
expect(fallback).toBe(askPrompt);
});
});
describe('filterToolsForMode', () => {
it('ask mode should only allow read/search tools', () => {
const filtered = filterToolsForMode(allTools, 'ask');
const names = filtered.map((t) => t.function.name);
expect(names).toContain('read_vault_file');
expect(names).toContain('search_vault_files');
expect(names).not.toContain('create_note');
expect(names).not.toContain('delete_note');
});
it('edit mode should allow all tools', () => {
const filtered = filterToolsForMode(allTools, 'edit');
const names = filtered.map((t) => t.function.name);
expect(names).toContain('read_vault_file');
expect(names).toContain('create_note');
expect(names).toContain('delete_note');
});
it('organize mode should allow organize tools but not delete', () => {
const filtered = filterToolsForMode(allTools, 'organize');
const names = filtered.map((t) => t.function.name);
expect(names).toContain('read_vault_file');
expect(names).toContain('search_vault_files');
expect(names).toContain('update_frontmatter');
expect(names).not.toContain('delete_note');
expect(names).not.toContain('create_note');
});
it('research mode should only allow read/search tools', () => {
const filtered = filterToolsForMode(allTools, 'research');
const names = filtered.map((t) => t.function.name);
expect(names).toContain('read_vault_file');
expect(names).toContain('search_vault_files');
expect(names).not.toContain('create_note');
});
it('workflow mode should return no tools', () => {
const filtered = filterToolsForMode(allTools, 'workflow');
expect(filtered).toHaveLength(0);
});
it('unknown mode should return all tools', () => {
const filtered = filterToolsForMode(allTools, 'unknown' as AgentMode);
expect(filtered).toEqual(allTools);
});
});
describe('AGENT_MODE_CONFIGS', () => {
it('should have a config for every mode', () => {
for (const mode of ALL_AGENT_MODES) {
expect(AGENT_MODE_CONFIGS[mode]).toBeDefined();
expect(AGENT_MODE_CONFIGS[mode].label).toBeDefined();
expect(AGENT_MODE_CONFIGS[mode].description).toBeDefined();
expect(AGENT_MODE_CONFIGS[mode].systemPrompt).toBeDefined();
}
});
});
});
+5
View File
@@ -38,6 +38,7 @@ const mockSettings: PluginSettings = {
maxMessageHistory: 50,
maxContextLength: 8000,
lastIndexTime: 0,
agentMode: 'ask',
cacheConfig: {
enabled: false,
similarityThreshold: 0.9,
@@ -412,6 +413,7 @@ describe('ChatView', () => {
});
it('should show preview for write tool calls and defer follow-up', async () => {
view.setAgentMode('edit');
view['sendButton'] = document.createElement('button');
view['inputEl'] = document.createElement('textarea');
(view['inputEl'] as HTMLTextAreaElement).value = 'test';
@@ -464,6 +466,7 @@ describe('ChatView', () => {
});
it('should apply pending actions and trigger follow-up', async () => {
view.setAgentMode('edit');
view['sendButton'] = document.createElement('button');
view['inputEl'] = document.createElement('textarea');
(view['inputEl'] as HTMLTextAreaElement).value = 'test';
@@ -524,6 +527,7 @@ describe('ChatView', () => {
});
it('should cancel pending actions', async () => {
view.setAgentMode('edit');
view['sendButton'] = document.createElement('button');
view['inputEl'] = document.createElement('textarea');
(view['inputEl'] as HTMLTextAreaElement).value = 'test';
@@ -567,6 +571,7 @@ describe('ChatView', () => {
});
it('should handle tool call errors gracefully and continue with partial results', async () => {
view.setAgentMode('edit');
view['sendButton'] = document.createElement('button');
view['inputEl'] = document.createElement('textarea');
(view['inputEl'] as HTMLTextAreaElement).value = 'test';