From ff4fd494811d326986cfe0305f35b89e37dd493c Mon Sep 17 00:00:00 2001 From: Adam Malczewski Date: Sat, 28 Mar 2026 05:15:49 +0900 Subject: no=op validation, and red tool error messages --- .rules/changelog/2026-03/28/05.md | 22 ++++++++++++++++++++++ src/chat-view.ts | 5 +++++ src/ollama-client.ts | 34 +++++++++++++++++++++++++++++++++- src/tools.ts | 5 +++++ styles.css | 13 +++++++++++++ 5 files changed, 78 insertions(+), 1 deletion(-) create mode 100644 .rules/changelog/2026-03/28/05.md diff --git a/.rules/changelog/2026-03/28/05.md b/.rules/changelog/2026-03/28/05.md new file mode 100644 index 0000000..e0cdf17 --- /dev/null +++ b/.rules/changelog/2026-03/28/05.md @@ -0,0 +1,22 @@ +# Changelog — 2026-03-28 / 05 + +## No-op edit validation and tool error styling + +### No-op edit detection (`src/tools.ts`, `src/ollama-client.ts`) + +- `executeEditFile` now rejects calls where `old_text === new_text` immediately with an error, preventing unnecessary file operations. +- Added `isNoOpEdit()` helper in `ollama-client.ts` that detects identical `old_text`/`new_text` for both `edit_file` and `batch_edit_file`. +- The agent loop skips the user approval prompt for no-op edits — the tool executes directly and returns the error without interrupting the user. + +### Red accents for tool errors (`src/chat-view.ts`, `styles.css`) + +- `appendToolCall` now detects when `event.result` starts with `"Error"` and adds the `ai-pulse-tool-call-error` CSS class to the tool call container. +- Added CSS rules for `.ai-pulse-tool-call-error` that change the border-left, header text, and result summary text from the default purple accent to red (`--text-error`). +- Applies universally to all tool errors across every tool type. + +### Files changed + +- `src/tools.ts` — Added `oldText === newText` guard in `executeEditFile`. +- `src/ollama-client.ts` — Added `isNoOpEdit()` function; updated approval condition in `chatAgentLoop`. +- `src/chat-view.ts` — Added error class detection in `appendToolCall`. +- `styles.css` — Added `.ai-pulse-tool-call-error` styling rules. diff --git a/src/chat-view.ts b/src/chat-view.ts index 7c7f110..b673d26 100644 --- a/src/chat-view.ts +++ b/src/chat-view.ts @@ -440,6 +440,11 @@ export class ChatView extends ItemView { header.createSpan({ text: event.friendlyName, cls: "ai-pulse-tool-call-name" }); container.createDiv({ text: event.summary, cls: "ai-pulse-tool-call-summary" }); + const isError = event.result.startsWith("Error"); + if (isError) { + container.addClass("ai-pulse-tool-call-error"); + } + container.createDiv({ text: event.resultSummary, cls: "ai-pulse-tool-call-result-summary" }); // DaisyUI-style collapse with checkbox diff --git a/src/ollama-client.ts b/src/ollama-client.ts index 4a184f6..abbbd10 100644 --- a/src/ollama-client.ts +++ b/src/ollama-client.ts @@ -243,6 +243,37 @@ function buildToolSystemPrompt(): string { const TOOL_SYSTEM_PROMPT = buildToolSystemPrompt(); +/** + * Detect whether an edit tool call is a no-op (old_text === new_text). + * Returns true for edit_file and batch_edit_file when no actual changes + * would occur, so the approval prompt can be skipped. + */ +function isNoOpEdit(toolName: string, args: Record): boolean { + if (toolName === "edit_file") { + const oldText = typeof args.old_text === "string" ? args.old_text : ""; + const newText = typeof args.new_text === "string" ? args.new_text : ""; + return oldText === newText; + } + if (toolName === "batch_edit_file") { + let operations: unknown[] = []; + if (Array.isArray(args.operations)) { + operations = args.operations; + } else if (typeof args.operations === "string") { + try { operations = JSON.parse(args.operations) as unknown[]; } catch { return false; } + } + // No-op if every operation has identical old_text and new_text + if (operations.length === 0) return false; + return operations.every((op) => { + if (typeof op !== "object" || op === null) return false; + const o = op as Record; + const oldText = typeof o.old_text === "string" ? o.old_text : ""; + const newText = typeof o.new_text === "string" ? o.new_text : ""; + return oldText === newText; + }); + } + return false; +} + /** * Shared agent loop: injects the system prompt, calls the strategy for each * iteration, executes tool calls, and loops until the model returns a final @@ -304,7 +335,8 @@ async function chatAgentLoop(opts: AgentLoopOptions): Promise { let result: string; if (toolEntry === undefined) { result = `Error: Unknown tool "${fnName}".`; - } else if (toolEntry.requiresApproval) { + } else if (toolEntry.requiresApproval && !isNoOpEdit(fnName, fnArgs)) { + // Requires approval — but skip the prompt for no-op edits let approved = false; if (onApprovalRequest !== undefined) { const message = toolEntry.approvalMessage !== undefined diff --git a/src/tools.ts b/src/tools.ts index a4eb16e..57e21c1 100644 --- a/src/tools.ts +++ b/src/tools.ts @@ -338,6 +338,11 @@ async function executeEditFile(app: App, args: Record): Promise const oldText = typeof args.old_text === "string" ? args.old_text : ""; const newText = typeof args.new_text === "string" ? args.new_text : ""; + // Reject no-op edits where old_text and new_text are identical + if (oldText === newText) { + return `Error: old_text and new_text are identical — no change would occur. Provide different text for new_text, or skip this edit.`; + } + const file = app.vault.getAbstractFileByPath(filePath); if (file === null || !(file instanceof TFile)) { return `Error: File not found at path "${filePath}".`; diff --git a/styles.css b/styles.css index 6d22413..871380d 100644 --- a/styles.css +++ b/styles.css @@ -268,6 +268,19 @@ font-size: 0.9em; } +/* Tool call error state */ +.ai-pulse-tool-call.ai-pulse-tool-call-error { + border-left-color: var(--text-error); +} + +.ai-pulse-tool-call.ai-pulse-tool-call-error .ai-pulse-tool-call-header { + color: var(--text-error); +} + +.ai-pulse-tool-call.ai-pulse-tool-call-error .ai-pulse-tool-call-result-summary { + color: var(--text-error); +} + /* ===== DaisyUI-inspired Collapse ===== */ .ai-pulse-collapse { -- cgit v1.2.3