From 7d7bac3cfaa57229c154a4f0a9804f248d9b31c2 Mon Sep 17 00:00:00 2001 From: Arne Berge Date: Sun, 1 Feb 2026 19:01:26 +0100 Subject: [PATCH 1/3] feat: Implement Trusted Mode and Extended Vault Tools - Added Trusted Mode (alwaysAllowReadWrite) to bypass tool confirmations.\n- Added fetch_url alias for web_fetch for agent compatibility.\n- Added UpdateFrontmatterTool and AppendContentTool for improved Bases integration.\n- Updated README.md.\n\nNote: This code was written with Gemini 3 thinking in Google Antigravity. --- README.md | 10 ++ src/main.ts | 24 ++++- src/tools/execution-engine.ts | 57 +++++++----- src/tools/vault-tools-extended.ts | 148 ++++++++++++++++++++++++++++++ src/tools/web-fetch-tool.ts | 5 + src/ui/settings.ts | 14 +++ 6 files changed, 233 insertions(+), 25 deletions(-) create mode 100644 src/tools/vault-tools-extended.ts diff --git a/README.md b/README.md index c8fd35c9..4e6cbcea 100644 --- a/README.md +++ b/README.md @@ -139,6 +139,16 @@ Let the AI actively work with your vault through tool calling capabilities. - **Context Files:** Add specific notes as persistent context - **Session Configuration:** Override model, temperature, and prompt per session - **Safety Features:** System folders are protected from modifications +- **Tool Permission System**: + - Granular control over which tools the agent can use + - **Trusted Mode**: Optional setting to allow file modifications without constant confirmation prompts (Use with caution!) +- **New Power Tools**: + - `update_frontmatter`: Safely modify note properties (status, tags, dates) without rewriting content. Critical for "Bases" and "Projects" workflows. + - `append_content`: Efficiently add text to the end of notes (great for logs and journals). + - `fetch_url`: Alias for Web Fetch, ensuring compatibility with standard agent prompts. +- **RAG (Retrieval Augmented Generation)**: + - Index your vault for semantic search + - Retrieve relevant context based on user queries **Example Commands:** diff --git a/src/main.ts b/src/main.ts index df6a356e..033ce3df 100644 --- a/src/main.ts +++ b/src/main.ts @@ -70,6 +70,8 @@ export interface ObsidianGeminiSettings { loopDetectionEnabled: boolean; loopDetectionThreshold: number; loopDetectionTimeWindowSeconds: number; + // Trusted Mode + alwaysAllowReadWrite: boolean; // V4 upgrade tracking hasSeenV4Welcome: boolean; // Version tracking for update notifications @@ -106,6 +108,8 @@ const DEFAULT_SETTINGS: ObsidianGeminiSettings = { loopDetectionEnabled: true, loopDetectionThreshold: 3, loopDetectionTimeWindowSeconds: 30, + // Trusted Mode + alwaysAllowReadWrite: false, // V4 upgrade tracking hasSeenV4Welcome: false, // Version tracking for update notifications @@ -532,11 +536,29 @@ export default class ObsidianGemini extends Plugin { this.toolRegistry.registerTool(tool); } + // Register extended vault tools (Frontmatter & Append) + // Dynamically import to avoid circular dependencies if any + const { UpdateFrontmatterTool, AppendContentTool } = await import( + './tools/vault-tools-extended' + ); + this.toolRegistry.registerTool(new UpdateFrontmatterTool()); + this.toolRegistry.registerTool(new AppendContentTool()); + // Register web tools (Google Search and Web Fetch) const { getWebTools } = await import('./tools/web-tools'); const webTools = getWebTools(); for (const tool of webTools) { this.toolRegistry.registerTool(tool); + // Register fetch_url alias for WebFetchTool + // This is required for compatibility with AGENTS.md which expects 'fetch_url' + if (tool.name === 'web_fetch') { + // We need to cast to any or instantiate a new class to set the name + // Since setToolName is not on the interface, we'll try to clone it or instantiate new + // Easier approach: Use the WebFetchTool class directly if possible, or just re-register + // However, getWebTools returns instances. + const { WebFetchTool } = await import('./tools/web-fetch-tool'); + this.toolRegistry.registerTool(new WebFetchTool(this).setName('fetch_url')); + } } // Register memory tools @@ -655,7 +677,7 @@ export default class ObsidianGemini extends Plugin { // Clean up partial initialization if (this.ragIndexing) { - await this.ragIndexing.destroy().catch(() => {}); + await this.ragIndexing.destroy().catch(() => { }); this.ragIndexing = null; } } diff --git a/src/tools/execution-engine.ts b/src/tools/execution-engine.ts index 4e7d9254..ae448b48 100644 --- a/src/tools/execution-engine.ts +++ b/src/tools/execution-engine.ts @@ -81,37 +81,46 @@ export class ToolExecutionEngine { // Check if confirmation is required const requiresConfirmation = this.registry.requiresConfirmation(toolCall.name, context); - if (requiresConfirmation) { - // Check if this tool is allowed without confirmation for this session - const isAllowedWithoutConfirmation = view?.isToolAllowedWithoutConfirmation?.(toolCall.name) || false; - - if (!isAllowedWithoutConfirmation) { - // Update progress to show waiting for confirmation - const toolDisplay = tool.displayName || tool.name; - const confirmationMessage = `Waiting for confirmation: ${toolDisplay}`; - view?.updateProgress?.(confirmationMessage, 'waiting'); - - const result = await this.requestUserConfirmation(tool, toolCall.arguments, view); - // Update progress back to tool execution - view?.updateProgress?.(`Executing: ${toolDisplay}`, 'tool'); - - if (!result.confirmed) { - return { - success: false, - error: 'User declined tool execution', - }; - } - // If user allowed this action without future confirmation - if (result.allowWithoutConfirmation && view) { - view.allowToolWithoutConfirmation(toolCall.name); + if (requiresConfirmation) { + // Check if Trusted Mode is enabled + if (this.plugin.settings.alwaysAllowReadWrite) { + context.plugin.logger.log( + `[Trusted Mode] Bypassing confirmation for ${toolCall.name}` + ); + } else { + // Check if this tool is allowed without confirmation for this session + const isAllowedWithoutConfirmation = + view?.isToolAllowedWithoutConfirmation?.(toolCall.name) || false; + + if (!isAllowedWithoutConfirmation) { + // Update progress to show waiting for confirmation + const toolDisplay = tool.displayName || tool.name; + const confirmationMessage = `Waiting for confirmation: ${toolDisplay}`; + view?.updateProgress?.(confirmationMessage, 'waiting'); + + const result = await this.requestUserConfirmation(tool, toolCall.arguments, view); + + // Update progress back to tool execution + view?.updateProgress?.(`Executing: ${toolDisplay}`, 'tool'); + + if (!result.confirmed) { + return { + success: false, + error: 'User declined tool execution', + }; + } + // If user allowed this action without future confirmation + if (result.allowWithoutConfirmation && view) { + view.allowToolWithoutConfirmation(toolCall.name); + } } } } // Show execution notification (disabled - now shown in chat UI) // const executionNotice = new Notice(`Executing ${tool.name}...`, 0); - const executionNotice = { hide: () => {} }; // Dummy object for compatibility + const executionNotice = { hide: () => { } }; // Dummy object for compatibility try { // Record the execution attempt diff --git a/src/tools/vault-tools-extended.ts b/src/tools/vault-tools-extended.ts new file mode 100644 index 00000000..a4da74c4 --- /dev/null +++ b/src/tools/vault-tools-extended.ts @@ -0,0 +1,148 @@ +import { Tool, ToolResult, ToolExecutionContext } from './types'; +import { ToolCategory } from '../types/agent'; +import { Notice, TFile } from 'obsidian'; +import type ObsidianGemini from '../main'; + +/** + * Tool to safely update YAML frontmatter without touching content + * Critical for integration with Obsidian Bases and other metadata-driven plugins + */ +export class UpdateFrontmatterTool implements Tool { + name = 'update_frontmatter'; + displayName = 'Update Frontmatter'; + category = ToolCategory.VAULT_OPERATIONS; + requiresConfirmation = true; + description = + 'Update a specific YAML frontmatter property in a file. ' + + 'This tool is safe to use as it only modifies metadata and preserves the note content. ' + + 'Use it to update status, tags, dates, or any other property.'; + + parameters = { + type: 'object' as const, + properties: { + path: { + type: 'string' as const, + description: 'Absolute path to the file to update', + }, + key: { + type: 'string' as const, + description: 'The property key to update', + }, + value: { + type: ['string', 'number', 'boolean', 'array'] as const, + description: 'The new value for the property', + }, + }, + required: ['path', 'key', 'value'], + }; + + async execute( + params: { path: string; key: string; value: any }, + context: ToolExecutionContext + ): Promise { + const plugin = context.plugin as InstanceType; + const { path, key, value } = params; + + try { + const file = plugin.app.vault.getAbstractFileByPath(path); + + if (!file || !(file instanceof TFile)) { + return { + success: false, + error: `File not found or is not a markdown file: ${path}`, + }; + } + + // Use Obsidian's native API for safe frontmatter updates + await plugin.app.fileManager.processFrontMatter(file, (frontmatter) => { + frontmatter[key] = value; + }); + + plugin.logger.log(`Updated frontmatter for ${path}: ${key} = ${value}`); + + return { + success: true, + output: `Successfully updated property "${key}" to "${value}" in ${path}`, + }; + } catch (error) { + const msg = error instanceof Error ? error.message : 'Unknown error'; + plugin.logger.error(`Failed to update frontmatter for ${path}: ${msg}`); + return { + success: false, + error: `Failed to update frontmatter: ${msg}`, + }; + } + } +} + +/** + * Tool to append content to the end of a file + * Useful for logging, journaling, or adding items to lists without rewriting the whole file + */ +export class AppendContentTool implements Tool { + name = 'append_content'; + displayName = 'Append Content'; + category = ToolCategory.VAULT_OPERATIONS; + requiresConfirmation = true; + description = + 'Append text to the end of a file. ' + + 'Useful for adding log entries, diary updates, or new sections without rewriting the entire file. ' + + 'If the file does not exist, an error is returned (use write_file to create new files).'; + + parameters = { + type: 'object' as const, + properties: { + path: { + type: 'string' as const, + description: 'Absolute path to the file', + }, + content: { + type: 'string' as const, + description: 'The text content to append (automatically adds newline if needed)', + }, + }, + required: ['path', 'content'], + }; + + async execute( + params: { path: string; content: string }, + context: ToolExecutionContext + ): Promise { + const plugin = context.plugin as InstanceType; + const { path, content } = params; + + try { + const file = plugin.app.vault.getAbstractFileByPath(path); + + if (!file || !(file instanceof TFile)) { + return { + success: false, + error: `File not found: ${path}`, + }; + } + + // Ensure content starts with newline if file is not empty + let contentToAppend = content; + const fileContent = await plugin.app.vault.read(file); + if (fileContent.length > 0 && !fileContent.endsWith('\n') && !content.startsWith('\n')) { + contentToAppend = '\n' + content; + } + + await plugin.app.vault.append(file, contentToAppend); + + plugin.logger.log(`Appended ${contentToAppend.length} chars to ${path}`); + + return { + success: true, + output: `Successfully appended content to ${path}`, + }; + } catch (error) { + const msg = error instanceof Error ? error.message : 'Unknown error'; + plugin.logger.error(`Failed to append content to ${path}: ${msg}`); + return { + success: false, + error: `Failed to append content: ${msg}`, + }; + } + } +} diff --git a/src/tools/web-fetch-tool.ts b/src/tools/web-fetch-tool.ts index 84dddd24..f75eb49e 100644 --- a/src/tools/web-fetch-tool.ts +++ b/src/tools/web-fetch-tool.ts @@ -18,6 +18,11 @@ export class WebFetchTool implements Tool { description = "Fetch and analyze content from a specific URL using Google's URL Context feature and AI. Provide a URL and a query describing what information to extract or questions to answer about the page content. The AI will read the page and provide a targeted analysis based on your query. Returns the analyzed content, URL metadata, and fetch timestamp. Falls back to direct HTTP fetch if URL Context fails. Use this to extract specific information from web pages, documentation, articles, or any publicly accessible URL."; + setName(name: string) { + this.name = name; + return this; + } + parameters = { type: 'object' as const, properties: { diff --git a/src/ui/settings.ts b/src/ui/settings.ts index 09f02fee..5e6502e7 100644 --- a/src/ui/settings.ts +++ b/src/ui/settings.ts @@ -448,6 +448,20 @@ export default class ObsidianGeminiSettingTab extends PluginSettingTab { }) ); + // Trusted Mode Setting + const trustedModeSetting = new Setting(containerEl) + .setName('Trusted Mode (Always Allow Read/Write)') + .setDesc('DANGEROUS: Allow the agent to create/edit/delete files without asking for confirmation.'); + + trustedModeSetting.descEl.style.color = 'var(--text-warning)'; + + trustedModeSetting.addToggle((toggle) => + toggle.setValue(this.plugin.settings.alwaysAllowReadWrite ?? false).onChange(async (value) => { + this.plugin.settings.alwaysAllowReadWrite = value; + await this.plugin.saveSettings(); + }) + ); + // Tool Loop Detection Settings new Setting(containerEl).setName('Tool Loop Detection').setHeading(); From 6254626e6e24286a3d6747a2b76d74b24539dd7a Mon Sep 17 00:00:00 2001 From: "google-labs-jules[bot]" <161369871+google-labs-jules[bot]@users.noreply.github.com> Date: Sun, 1 Feb 2026 21:15:26 +0000 Subject: [PATCH 2/3] Fix PR feedback: Trusted Mode modal, safety checks, and formatting Co-authored-by: ArnBdev <207385326+ArnBdev@users.noreply.github.com> --- README.md | 14 +- manifest.json | 2 +- src/main.ts | 8 +- src/tools/execution-engine.ts | 10 +- src/tools/vault-tools-extended.ts | 308 ++++++++++++++++-------------- src/tools/web-fetch-tool.ts | 5 +- src/ui/settings.ts | 18 +- src/ui/trusted-mode-modal.ts | 53 +++++ versions.json | 2 +- 9 files changed, 248 insertions(+), 172 deletions(-) create mode 100644 src/ui/trusted-mode-modal.ts diff --git a/README.md b/README.md index 4e6cbcea..832e5dc2 100644 --- a/README.md +++ b/README.md @@ -140,15 +140,15 @@ Let the AI actively work with your vault through tool calling capabilities. - **Session Configuration:** Override model, temperature, and prompt per session - **Safety Features:** System folders are protected from modifications - **Tool Permission System**: - - Granular control over which tools the agent can use - - **Trusted Mode**: Optional setting to allow file modifications without constant confirmation prompts (Use with caution!) + - Granular control over which tools the agent can use + - **Trusted Mode**: Optional setting to allow file modifications without constant confirmation prompts (Use with caution!) - **New Power Tools**: - - `update_frontmatter`: Safely modify note properties (status, tags, dates) without rewriting content. Critical for "Bases" and "Projects" workflows. - - `append_content`: Efficiently add text to the end of notes (great for logs and journals). - - `fetch_url`: Alias for Web Fetch, ensuring compatibility with standard agent prompts. + - `update_frontmatter`: Safely modify note properties (status, tags, dates) without rewriting content. Critical for "Bases" and "Projects" workflows. + - `append_content`: Efficiently add text to the end of notes (great for logs and journals). + - `fetch_url`: Alias for Web Fetch, ensuring compatibility with standard agent prompts. - **RAG (Retrieval Augmented Generation)**: - - Index your vault for semantic search - - Retrieve relevant context based on user queries + - Index your vault for semantic search + - Retrieve relevant context based on user queries **Example Commands:** diff --git a/manifest.json b/manifest.json index f7249c67..4c802716 100644 --- a/manifest.json +++ b/manifest.json @@ -7,4 +7,4 @@ "author": "Allen Hutchison", "authorUrl": "https://allen.hutchison.org", "isDesktopOnly": false -} \ No newline at end of file +} diff --git a/src/main.ts b/src/main.ts index 033ce3df..c25c9fb0 100644 --- a/src/main.ts +++ b/src/main.ts @@ -538,9 +538,7 @@ export default class ObsidianGemini extends Plugin { // Register extended vault tools (Frontmatter & Append) // Dynamically import to avoid circular dependencies if any - const { UpdateFrontmatterTool, AppendContentTool } = await import( - './tools/vault-tools-extended' - ); + const { UpdateFrontmatterTool, AppendContentTool } = await import('./tools/vault-tools-extended'); this.toolRegistry.registerTool(new UpdateFrontmatterTool()); this.toolRegistry.registerTool(new AppendContentTool()); @@ -557,7 +555,7 @@ export default class ObsidianGemini extends Plugin { // Easier approach: Use the WebFetchTool class directly if possible, or just re-register // However, getWebTools returns instances. const { WebFetchTool } = await import('./tools/web-fetch-tool'); - this.toolRegistry.registerTool(new WebFetchTool(this).setName('fetch_url')); + this.toolRegistry.registerTool(new WebFetchTool().setName('fetch_url')); } } @@ -677,7 +675,7 @@ export default class ObsidianGemini extends Plugin { // Clean up partial initialization if (this.ragIndexing) { - await this.ragIndexing.destroy().catch(() => { }); + await this.ragIndexing.destroy().catch(() => {}); this.ragIndexing = null; } } diff --git a/src/tools/execution-engine.ts b/src/tools/execution-engine.ts index ae448b48..ef0f0603 100644 --- a/src/tools/execution-engine.ts +++ b/src/tools/execution-engine.ts @@ -81,17 +81,13 @@ export class ToolExecutionEngine { // Check if confirmation is required const requiresConfirmation = this.registry.requiresConfirmation(toolCall.name, context); - if (requiresConfirmation) { // Check if Trusted Mode is enabled if (this.plugin.settings.alwaysAllowReadWrite) { - context.plugin.logger.log( - `[Trusted Mode] Bypassing confirmation for ${toolCall.name}` - ); + context.plugin.logger.log(`[Trusted Mode] Bypassing confirmation for ${toolCall.name}`); } else { // Check if this tool is allowed without confirmation for this session - const isAllowedWithoutConfirmation = - view?.isToolAllowedWithoutConfirmation?.(toolCall.name) || false; + const isAllowedWithoutConfirmation = view?.isToolAllowedWithoutConfirmation?.(toolCall.name) || false; if (!isAllowedWithoutConfirmation) { // Update progress to show waiting for confirmation @@ -120,7 +116,7 @@ export class ToolExecutionEngine { // Show execution notification (disabled - now shown in chat UI) // const executionNotice = new Notice(`Executing ${tool.name}...`, 0); - const executionNotice = { hide: () => { } }; // Dummy object for compatibility + const executionNotice = { hide: () => {} }; // Dummy object for compatibility try { // Record the execution attempt diff --git a/src/tools/vault-tools-extended.ts b/src/tools/vault-tools-extended.ts index a4da74c4..13dab902 100644 --- a/src/tools/vault-tools-extended.ts +++ b/src/tools/vault-tools-extended.ts @@ -1,148 +1,160 @@ -import { Tool, ToolResult, ToolExecutionContext } from './types'; -import { ToolCategory } from '../types/agent'; -import { Notice, TFile } from 'obsidian'; -import type ObsidianGemini from '../main'; - -/** - * Tool to safely update YAML frontmatter without touching content - * Critical for integration with Obsidian Bases and other metadata-driven plugins - */ -export class UpdateFrontmatterTool implements Tool { - name = 'update_frontmatter'; - displayName = 'Update Frontmatter'; - category = ToolCategory.VAULT_OPERATIONS; - requiresConfirmation = true; - description = - 'Update a specific YAML frontmatter property in a file. ' + - 'This tool is safe to use as it only modifies metadata and preserves the note content. ' + - 'Use it to update status, tags, dates, or any other property.'; - - parameters = { - type: 'object' as const, - properties: { - path: { - type: 'string' as const, - description: 'Absolute path to the file to update', - }, - key: { - type: 'string' as const, - description: 'The property key to update', - }, - value: { - type: ['string', 'number', 'boolean', 'array'] as const, - description: 'The new value for the property', - }, - }, - required: ['path', 'key', 'value'], - }; - - async execute( - params: { path: string; key: string; value: any }, - context: ToolExecutionContext - ): Promise { - const plugin = context.plugin as InstanceType; - const { path, key, value } = params; - - try { - const file = plugin.app.vault.getAbstractFileByPath(path); - - if (!file || !(file instanceof TFile)) { - return { - success: false, - error: `File not found or is not a markdown file: ${path}`, - }; - } - - // Use Obsidian's native API for safe frontmatter updates - await plugin.app.fileManager.processFrontMatter(file, (frontmatter) => { - frontmatter[key] = value; - }); - - plugin.logger.log(`Updated frontmatter for ${path}: ${key} = ${value}`); - - return { - success: true, - output: `Successfully updated property "${key}" to "${value}" in ${path}`, - }; - } catch (error) { - const msg = error instanceof Error ? error.message : 'Unknown error'; - plugin.logger.error(`Failed to update frontmatter for ${path}: ${msg}`); - return { - success: false, - error: `Failed to update frontmatter: ${msg}`, - }; - } - } -} - -/** - * Tool to append content to the end of a file - * Useful for logging, journaling, or adding items to lists without rewriting the whole file - */ -export class AppendContentTool implements Tool { - name = 'append_content'; - displayName = 'Append Content'; - category = ToolCategory.VAULT_OPERATIONS; - requiresConfirmation = true; - description = - 'Append text to the end of a file. ' + - 'Useful for adding log entries, diary updates, or new sections without rewriting the entire file. ' + - 'If the file does not exist, an error is returned (use write_file to create new files).'; - - parameters = { - type: 'object' as const, - properties: { - path: { - type: 'string' as const, - description: 'Absolute path to the file', - }, - content: { - type: 'string' as const, - description: 'The text content to append (automatically adds newline if needed)', - }, - }, - required: ['path', 'content'], - }; - - async execute( - params: { path: string; content: string }, - context: ToolExecutionContext - ): Promise { - const plugin = context.plugin as InstanceType; - const { path, content } = params; - - try { - const file = plugin.app.vault.getAbstractFileByPath(path); - - if (!file || !(file instanceof TFile)) { - return { - success: false, - error: `File not found: ${path}`, - }; - } - - // Ensure content starts with newline if file is not empty - let contentToAppend = content; - const fileContent = await plugin.app.vault.read(file); - if (fileContent.length > 0 && !fileContent.endsWith('\n') && !content.startsWith('\n')) { - contentToAppend = '\n' + content; - } - - await plugin.app.vault.append(file, contentToAppend); - - plugin.logger.log(`Appended ${contentToAppend.length} chars to ${path}`); - - return { - success: true, - output: `Successfully appended content to ${path}`, - }; - } catch (error) { - const msg = error instanceof Error ? error.message : 'Unknown error'; - plugin.logger.error(`Failed to append content to ${path}: ${msg}`); - return { - success: false, - error: `Failed to append content: ${msg}`, - }; - } - } -} +import { Tool, ToolResult, ToolExecutionContext } from './types'; +import { ToolCategory } from '../types/agent'; +import { TFile } from 'obsidian'; +import type ObsidianGemini from '../main'; + +/** + * Tool to safely update YAML frontmatter without touching content + * Critical for integration with Obsidian Bases and other metadata-driven plugins + */ +export class UpdateFrontmatterTool implements Tool { + name = 'update_frontmatter'; + displayName = 'Update Frontmatter'; + category = ToolCategory.VAULT_OPERATIONS; + requiresConfirmation = true; + description = + 'Update a specific YAML frontmatter property in a file. ' + + 'This tool is safe to use as it only modifies metadata and preserves the note content. ' + + 'Use it to update status, tags, dates, or any other property.'; + + parameters = { + type: 'object' as const, + properties: { + path: { + type: 'string' as const, + description: 'Absolute path to the file to update', + }, + key: { + type: 'string' as const, + description: 'The property key to update', + }, + value: { + type: ['string', 'number', 'boolean', 'array'] as const, + description: 'The new value for the property', + }, + }, + required: ['path', 'key', 'value'], + }; + + async execute(params: { path: string; key: string; value: any }, context: ToolExecutionContext): Promise { + const plugin = context.plugin as InstanceType; + const { path, key, value } = params; + + // Check for system folder protection + const historyFolder = plugin.settings.historyFolder; + if (path.startsWith(historyFolder + '/') || path.startsWith('.obsidian/')) { + return { + success: false, + error: `Cannot modify files in protected system folder: ${path}`, + }; + } + + try { + const file = plugin.app.vault.getAbstractFileByPath(path); + + if (!file || !(file instanceof TFile)) { + return { + success: false, + error: `File not found or is not a markdown file: ${path}`, + }; + } + + // Use Obsidian's native API for safe frontmatter updates + await plugin.app.fileManager.processFrontMatter(file, (frontmatter) => { + frontmatter[key] = value; + }); + + plugin.logger.log(`Updated frontmatter for ${path}: ${key} = ${value}`); + + return { + success: true, + output: `Successfully updated property "${key}" to "${value}" in ${path}`, + }; + } catch (error) { + const msg = error instanceof Error ? error.message : 'Unknown error'; + plugin.logger.error(`Failed to update frontmatter for ${path}: ${msg}`); + return { + success: false, + error: `Failed to update frontmatter: ${msg}`, + }; + } + } +} + +/** + * Tool to append content to the end of a file + * Useful for logging, journaling, or adding items to lists without rewriting the whole file + */ +export class AppendContentTool implements Tool { + name = 'append_content'; + displayName = 'Append Content'; + category = ToolCategory.VAULT_OPERATIONS; + requiresConfirmation = true; + description = + 'Append text to the end of a file. ' + + 'Useful for adding log entries, diary updates, or new sections without rewriting the entire file. ' + + 'If the file does not exist, an error is returned (use write_file to create new files).'; + + parameters = { + type: 'object' as const, + properties: { + path: { + type: 'string' as const, + description: 'Absolute path to the file', + }, + content: { + type: 'string' as const, + description: 'The text content to append (automatically adds newline if needed)', + }, + }, + required: ['path', 'content'], + }; + + async execute(params: { path: string; content: string }, context: ToolExecutionContext): Promise { + const plugin = context.plugin as InstanceType; + const { path, content } = params; + + // Check for system folder protection + const historyFolder = plugin.settings.historyFolder; + if (path.startsWith(historyFolder + '/') || path.startsWith('.obsidian/')) { + return { + success: false, + error: `Cannot modify files in protected system folder: ${path}`, + }; + } + + try { + const file = plugin.app.vault.getAbstractFileByPath(path); + + if (!file || !(file instanceof TFile)) { + return { + success: false, + error: `File not found: ${path}`, + }; + } + + // Ensure content starts with newline if file is not empty + let contentToAppend = content; + const fileContent = await plugin.app.vault.read(file); + if (fileContent.length > 0 && !fileContent.endsWith('\n') && !content.startsWith('\n')) { + contentToAppend = '\n' + content; + } + + await plugin.app.vault.append(file, contentToAppend); + + plugin.logger.log(`Appended ${contentToAppend.length} chars to ${path}`); + + return { + success: true, + output: `Successfully appended content to ${path}`, + }; + } catch (error) { + const msg = error instanceof Error ? error.message : 'Unknown error'; + plugin.logger.error(`Failed to append content to ${path}: ${msg}`); + return { + success: false, + error: `Failed to append content: ${msg}`, + }; + } + } +} diff --git a/src/tools/web-fetch-tool.ts b/src/tools/web-fetch-tool.ts index f75eb49e..b3f329a9 100644 --- a/src/tools/web-fetch-tool.ts +++ b/src/tools/web-fetch-tool.ts @@ -18,7 +18,10 @@ export class WebFetchTool implements Tool { description = "Fetch and analyze content from a specific URL using Google's URL Context feature and AI. Provide a URL and a query describing what information to extract or questions to answer about the page content. The AI will read the page and provide a targeted analysis based on your query. Returns the analyzed content, URL metadata, and fetch timestamp. Falls back to direct HTTP fetch if URL Context fails. Use this to extract specific information from web pages, documentation, articles, or any publicly accessible URL."; - setName(name: string) { + setName(name: string): this { + if (!name || name.trim().length === 0) { + throw new Error('Tool name cannot be empty'); + } this.name = name; return this; } diff --git a/src/ui/settings.ts b/src/ui/settings.ts index 5e6502e7..6f9db406 100644 --- a/src/ui/settings.ts +++ b/src/ui/settings.ts @@ -457,8 +457,22 @@ export default class ObsidianGeminiSettingTab extends PluginSettingTab { trustedModeSetting.addToggle((toggle) => toggle.setValue(this.plugin.settings.alwaysAllowReadWrite ?? false).onChange(async (value) => { - this.plugin.settings.alwaysAllowReadWrite = value; - await this.plugin.saveSettings(); + if (value) { + // Revert toggle until user confirms + toggle.setValue(false); + const { TrustedModeConfirmationModal } = await import('./trusted-mode-modal'); + const modal = new TrustedModeConfirmationModal(this.app, async (confirmed) => { + if (confirmed) { + toggle.setValue(true); + this.plugin.settings.alwaysAllowReadWrite = true; + await this.plugin.saveSettings(); + } + }); + modal.open(); + } else { + this.plugin.settings.alwaysAllowReadWrite = value; + await this.plugin.saveSettings(); + } }) ); diff --git a/src/ui/trusted-mode-modal.ts b/src/ui/trusted-mode-modal.ts new file mode 100644 index 00000000..b645d8bc --- /dev/null +++ b/src/ui/trusted-mode-modal.ts @@ -0,0 +1,53 @@ +import { App, Modal, Setting } from 'obsidian'; + +/** + * Modal shown when user enables Trusted Mode to confirm they understand the risks + */ +export class TrustedModeConfirmationModal extends Modal { + private onConfirm: (confirmed: boolean) => void; + + constructor(app: App, onConfirm: (confirmed: boolean) => void) { + super(app); + this.onConfirm = onConfirm; + } + + onOpen() { + const { contentEl } = this; + contentEl.empty(); + + contentEl.createEl('h2', { text: 'Enable Trusted Mode?' }); + + const container = contentEl.createEl('div'); + + container.createEl('p', { + text: 'Trusted Mode allows the AI agent to create, edit, and delete files in your vault without asking for confirmation.', + }); + + const warningEl = container.createEl('p', { + text: '⚠️ This grants the AI full write access to your vault. While convenient, it carries risks if the model hallucinates or makes mistakes.', + }); + warningEl.style.color = 'var(--text-warning)'; + + new Setting(contentEl) + .addButton((btn) => + btn.setButtonText('Cancel').onClick(() => { + this.close(); + this.onConfirm(false); + }) + ) + .addButton((btn) => + btn + .setButtonText('Enable Trusted Mode') + .setWarning() + .onClick(() => { + this.close(); + this.onConfirm(true); + }) + ); + } + + onClose() { + const { contentEl } = this; + contentEl.empty(); + } +} diff --git a/versions.json b/versions.json index 28a884dd..6317b322 100644 --- a/versions.json +++ b/versions.json @@ -42,4 +42,4 @@ "4.2.1": "1.7.5", "4.3.0": "1.7.5", "4.3.1": "1.7.5" -} \ No newline at end of file +} From 4ca39d37d4ab6777ade961c02e60b3a4d7e6f8c7 Mon Sep 17 00:00:00 2001 From: "google-labs-jules[bot]" <161369871+google-labs-jules[bot]@users.noreply.github.com> Date: Thu, 5 Feb 2026 18:41:39 +0000 Subject: [PATCH 3/3] =?UTF-8?q?=F0=9F=8E=A8=20Palette:=20Improve=20accessi?= =?UTF-8?q?bility=20of=20Agent=20View=20buttons?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: ArnBdev <207385326+ArnBdev@users.noreply.github.com> --- src/ui/agent-view/agent-view-ui.ts | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/src/ui/agent-view/agent-view-ui.ts b/src/ui/agent-view/agent-view-ui.ts index f57779e1..5c82cbe4 100644 --- a/src/ui/agent-view/agent-view-ui.ts +++ b/src/ui/agent-view/agent-view-ui.ts @@ -123,6 +123,10 @@ export class AgentViewUI { const toggleBtn = leftSection.createEl('button', { cls: 'gemini-agent-toggle-btn', title: 'Toggle context panel', + attr: { + 'aria-label': 'Toggle context panel', + 'aria-expanded': 'false', + }, }); setIcon(toggleBtn, 'chevron-down'); @@ -131,9 +135,11 @@ export class AgentViewUI { if (isCollapsed) { contextPanel.removeClass('gemini-agent-context-panel-collapsed'); setIcon(toggleBtn, 'chevron-up'); + toggleBtn.setAttribute('aria-expanded', 'true'); } else { contextPanel.addClass('gemini-agent-context-panel-collapsed'); setIcon(toggleBtn, 'chevron-down'); + toggleBtn.setAttribute('aria-expanded', 'false'); } }); @@ -262,6 +268,7 @@ export class AgentViewUI { const settingsBtn = rightSection.createEl('button', { cls: 'gemini-agent-btn gemini-agent-btn-icon', title: 'Session Settings', + attr: { 'aria-label': 'Session Settings' }, }); setIcon(settingsBtn, 'settings'); settingsBtn.addEventListener('click', () => callbacks.showSessionSettings()); @@ -269,6 +276,7 @@ export class AgentViewUI { const newSessionBtn = rightSection.createEl('button', { cls: 'gemini-agent-btn gemini-agent-btn-icon', title: 'New Session', + attr: { 'aria-label': 'New Session' }, }); setIcon(newSessionBtn, 'plus'); newSessionBtn.addEventListener('click', () => callbacks.createNewSession()); @@ -276,6 +284,7 @@ export class AgentViewUI { const listSessionsBtn = rightSection.createEl('button', { cls: 'gemini-agent-btn gemini-agent-btn-icon', title: 'Browse Sessions', + attr: { 'aria-label': 'Browse Sessions' }, }); setIcon(listSessionsBtn, 'list'); listSessionsBtn.addEventListener('click', () => callbacks.showSessionList()); @@ -640,6 +649,7 @@ export class AgentViewUI { text: '×', cls: 'gemini-agent-remove-btn', title: 'Remove file', + attr: { 'aria-label': `Remove ${file.basename}` }, }); removeBtn.addEventListener('click', () => {