summaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
-rw-r--r--.rules/changelog/2026-03/28/05.md22
-rw-r--r--src/chat-view.ts5
-rw-r--r--src/ollama-client.ts34
-rw-r--r--src/tools.ts5
-rw-r--r--styles.css13
5 files changed, 78 insertions, 1 deletions
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
@@ -244,6 +244,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<string, unknown>): 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<string, unknown>;
+ 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
* text response or the iteration cap is reached.
@@ -304,7 +335,8 @@ async function chatAgentLoop(opts: AgentLoopOptions): Promise<string> {
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<string, unknown>): 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 {