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:
@@ -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
|
||||
|
||||
@@ -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") {
|
||||
|
||||
@@ -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 }],
|
||||
|
||||
Reference in New Issue
Block a user