feat(coding-agent): added automatic stripping of hashline prefixes from write tool output

- Added automatic stripping of hashline display prefixes (LINE#ID:) from write tool content when hashline edit mode is enabled, preventing models from accidentally copying display markers into files.
- Implemented stripHashlinePrefixes() utility to remove hashline-only prefixes without affecting diff markers.
- Added stripWriteContent() helper to conditionally clean write content based on file display mode configuration.
- Updated write tool result message to notify users when hashline prefixes were auto-stripped.
This commit is contained in:
can1357
2026-03-18 23:20:21 +01:00
parent 09b0bad87d
commit 8cab107a48
3 changed files with 49 additions and 2 deletions
+2
View File
@@ -1,8 +1,10 @@
# Changelog
## [Unreleased]
### Added
- Added automatic stripping of hashline display prefixes (LINE#ID:) from write tool content when hashline edit mode is enabled, preventing the model from accidentally copying display markers into files
- Added `mcpServerName` and `mcpToolName` optional properties to custom tools for MCP server discovery and search metadata
## [13.13.1] - 2026-03-18
+21
View File
@@ -136,6 +136,27 @@ export function stripNewLinePrefixes(lines: string[]): string[] {
});
}
/**
* Strip hashline display prefixes only (no diff markers).
*
* Unlike {@link stripNewLinePrefixes} which also handles `+` diff markers,
* this only strips `LINE#ID:` / `#ID:` prefixes. Used by the write tool
* where diff markers are not applicable.
*
* Returns the original array reference when no stripping is needed.
*/
export function stripHashlinePrefixes(lines: string[]): string[] {
let hashPrefixCount = 0;
let nonEmpty = 0;
for (const l of lines) {
if (l.length === 0) continue;
nonEmpty++;
if (HASHLINE_PREFIX_RE.test(l)) hashPrefixCount++;
}
if (nonEmpty === 0 || hashPrefixCount !== nonEmpty) return lines;
return lines.map(l => l.replace(HASHLINE_PREFIX_RE, ""));
}
export function hashlineParseText(edit: string[] | string | null): string[] {
if (edit === null) return [];
if (typeof edit === "string") {
+26 -2
View File
@@ -14,9 +14,11 @@ import { renderPromptTemplate } from "../config/prompt-templates";
import type { RenderResultOptions } from "../extensibility/custom-tools/types";
import { createLspWritethrough, type FileDiagnosticsResult, type WritethroughCallback, writethroughNoop } from "../lsp";
import { getLanguageFromPath, type Theme } from "../modes/theme/theme";
import { stripHashlinePrefixes } from "../patch";
import writeDescription from "../prompts/tools/write.md" with { type: "text" };
import type { ToolSession } from "../sdk";
import { Ellipsis, Hasher, type RenderCache, renderStatusLine, truncateToWidth } from "../tui";
import { resolveFileDisplayMode } from "../utils/file-display-mode";
import { checkAutoGeneratedFile } from "./auto-generated-guard";
import { invalidateFsScanAfterWrite } from "./fs-cache-invalidation";
import { type OutputMeta, outputMeta } from "./output-meta";
@@ -60,6 +62,22 @@ function getLspBatchRequest(toolCall: ToolCallContext | undefined): { id: string
return { id: toolCall.batchId, flush: !hasLaterWrites };
}
/**
* Strip hashline display prefixes from write content.
*
* Only active when hashline edit mode is enabled — the model sees `LINE#ID:`
* prefixes in read output and sometimes copies them into write content.
*/
function stripWriteContent(session: ToolSession, content: string): { text: string; stripped: boolean } {
if (!resolveFileDisplayMode(session).hashLines) {
return { text: content, stripped: false };
}
const lines = content.split("\n");
const cleaned = stripHashlinePrefixes(lines);
if (cleaned === lines) return { text: content, stripped: false };
return { text: cleaned.join("\n"), stripped: true };
}
// ═══════════════════════════════════════════════════════════════════════════
// Tool Class
// ═══════════════════════════════════════════════════════════════════════════
@@ -109,10 +127,16 @@ export class WriteTool implements AgentTool<typeof writeSchema, WriteToolDetails
await checkAutoGeneratedFile(absolutePath, path);
}
const diagnostics = await this.#writethrough(absolutePath, content, signal, undefined, batchRequest);
// Strip hashline display prefixes (LINE#ID:) if the model copied them from read output
const { text: cleanContent, stripped } = stripWriteContent(this.session, content);
const diagnostics = await this.#writethrough(absolutePath, cleanContent, signal, undefined, batchRequest);
invalidateFsScanAfterWrite(absolutePath);
const resultText = `Successfully wrote ${content.length} bytes to ${path}`;
let resultText = `Successfully wrote ${cleanContent.length} bytes to ${path}`;
if (stripped) {
resultText += `\nNote: auto-stripped hashline display prefixes from content before writing.`;
}
if (!diagnostics) {
return {
content: [{ type: "text", text: resultText }],