Repository navigation
docs(rust-porting): adopt diff/edit/js-testing/highlight rows and add word-diff A/B adapter #6194
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
b07dbce
55b3bdf
6b508ee
78844a8
a7856d8
54b908f
d4ce6d0
e6f1f3d
30b61e6
851ed50
4b914ca
dd340e7
64a946e
08783be
65a80cf
da455cb
ca850a2
0833153
4d23fac
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Large diffs are not rendered by default.
Large diffs are not rendered by default.
Large diffs are not rendered by default.
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,8 +1,8 @@ | ||
| /** | ||
| * Fuzzy matching utilities for the edit tool. | ||
| * | ||
| * Provides both character-level and line-level fuzzy matching with progressive | ||
| * fallback strategies for finding text in files. | ||
| * Provides both character-level and line-level fuzzy matching with staged | ||
| * strategies for finding text in files. | ||
| */ | ||
| import type { AgentToolResult } from "@gajae-code/agent-core"; | ||
| import { markDesignedError } from "@gajae-code/utils/error-classification"; | ||
|
|
@@ -430,7 +430,7 @@ export function findContextLine( | |
| lines: string[], | ||
| context: string, | ||
| startFrom: number, | ||
| options?: { allowFuzzy?: boolean; skipFunctionFallback?: boolean }, | ||
| options?: { allowFuzzy?: boolean; skipParenRetry?: boolean }, | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Useful? React with 👍 / 👎. |
||
| ): ContextLineResult { | ||
| const allowFuzzy = options?.allowFuzzy ?? true; | ||
| const trimmedContext = context.trim(); | ||
|
|
@@ -570,14 +570,14 @@ export function findContextLine( | |
| }; | ||
| } | ||
|
|
||
| if (!options?.skipFunctionFallback && trimmedContext.endsWith("()")) { | ||
| if (!options?.skipParenRetry && trimmedContext.endsWith("()")) { | ||
| const withParen = trimmedContext.replace(/\(\)\s*$/u, "("); | ||
| const withoutParen = trimmedContext.replace(/\(\)\s*$/u, ""); | ||
| const parenResult = findContextLine(lines, withParen, startFrom, { allowFuzzy, skipFunctionFallback: true }); | ||
| const parenResult = findContextLine(lines, withParen, startFrom, { allowFuzzy, skipParenRetry: true }); | ||
| if (parenResult.index !== undefined || (parenResult.matchCount ?? 0) > 0) { | ||
| return parenResult; | ||
| } | ||
| return findContextLine(lines, withoutParen, startFrom, { allowFuzzy, skipFunctionFallback: true }); | ||
| return findContextLine(lines, withoutParen, startFrom, { allowFuzzy, skipParenRetry: true }); | ||
| } | ||
|
|
||
| return { index: undefined, confidence: bestScore }; | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -7,7 +7,7 @@ | |
| * - compute unified diff previews for the in-flight args | ||
| * (`computeDiffPreview`), and | ||
| * - render a text placeholder while no diff exists yet | ||
| * (`renderStreamingFallback`). | ||
| * (`renderStreamingPlaceholder`). | ||
| * | ||
| * The shared renderer / `ToolExecutionComponent` consult the strategy via | ||
| * the injected `editMode` rather than probing argument shape. | ||
|
|
@@ -72,7 +72,7 @@ export interface EditStreamingStrategy<Args = unknown> { | |
| * Rendered inline while the diff hasn't been computed yet (or when the | ||
| * compute returned `null` because args are still too partial). | ||
| */ | ||
| renderStreamingFallback(args: Args, uiTheme: Theme): string; | ||
| renderStreamingPlaceholder(args: Args, uiTheme: Theme): string; | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Useful? React with 👍 / 👎. |
||
| } | ||
| export interface EditRequestTargetInventory { | ||
| paths: string[]; | ||
|
|
@@ -169,7 +169,7 @@ export function getEditRequestTargetInventory( | |
| if (editMode === "hashline") { | ||
| const input = typeof values.input === "string" ? values.input : ""; | ||
| const headerPaths = getHashlineTargetPaths(input); | ||
| // `path` is only a fallback for headerless input; explicit §PATH headers are the full target set. | ||
| // `path` is only the target for headerless input; explicit §PATH headers are the full target set. | ||
| if (headerPaths.length > 0) return { paths: orderedDistinctPaths(headerPaths) }; | ||
| return { paths: orderedDistinctPaths([topLevelPath]) }; | ||
| } | ||
|
|
@@ -197,8 +197,8 @@ export function getEditRequestTargetInventory( | |
| return { paths: orderedDistinctPaths([topLevelPath, ...editPaths, partialPath]) }; | ||
| } | ||
|
|
||
| const STREAMING_FALLBACK_LINES = 12; | ||
| const STREAMING_FALLBACK_WIDTH = 80; | ||
| const STREAMING_PLACEHOLDER_LINES = 12; | ||
| const STREAMING_PLACEHOLDER_WIDTH = 80; | ||
|
|
||
| function isHashlineHeaderLine(line: string): boolean { | ||
| return line.trimEnd().startsWith(HL_FILE_PREFIX); | ||
|
|
@@ -236,15 +236,15 @@ function trimHashlineStreamingSyntax(lines: string[]): string[] { | |
| return lines.slice(index).filter(line => !isHashlineEnvelopeMarkerLine(line)); | ||
| } | ||
|
|
||
| function renderHashlineInputFallback(input: string, uiTheme: Theme): string { | ||
| function renderHashlineInputPlaceholder(input: string, uiTheme: Theme): string { | ||
| const lines = trimHashlineStreamingSyntax(sanitizeText(input).split("\n")); | ||
| if (!lines.some(line => line.trim().length > 0)) return ""; | ||
|
|
||
| const displayLines = lines.slice(-STREAMING_FALLBACK_LINES); | ||
| const displayLines = lines.slice(-STREAMING_PLACEHOLDER_LINES); | ||
| const hidden = lines.length - displayLines.length; | ||
| let text = "\n\n"; | ||
| text += displayLines | ||
| .map(line => uiTheme.fg("toolOutput", truncateToWidth(replaceTabs(line), STREAMING_FALLBACK_WIDTH))) | ||
| .map(line => uiTheme.fg("toolOutput", truncateToWidth(replaceTabs(line), STREAMING_PLACEHOLDER_WIDTH))) | ||
| .join("\n"); | ||
| if (hidden > 0) { | ||
| text += uiTheme.fg("dim", `\n… (streaming +${hidden} lines)`); | ||
|
|
@@ -376,7 +376,7 @@ const replaceStrategy: EditStreamingStrategy<ReplaceArgs> = { | |
| ctx.signal.throwIfAborted(); | ||
| return [toPerFilePreview(args.path, result)]; | ||
| }, | ||
| renderStreamingFallback() { | ||
| renderStreamingPlaceholder() { | ||
| return ""; | ||
| }, | ||
| }; | ||
|
|
@@ -405,7 +405,7 @@ const patchStrategy: EditStreamingStrategy<PatchArgs> = { | |
| ctx.signal.throwIfAborted(); | ||
| return [toPerFilePreview(args.path, result)]; | ||
| }, | ||
| renderStreamingFallback() { | ||
| renderStreamingPlaceholder() { | ||
| return ""; | ||
| }, | ||
| }; | ||
|
|
@@ -552,7 +552,7 @@ const hashlineStrategy: EditStreamingStrategy<HashlineArgs> = { | |
| try { | ||
| sections = splitHashlineInputs(input, { cwd: ctx.cwd, path: args.path }); | ||
| } catch { | ||
| // Single-section fallback keeps the original error rendering for the | ||
| // Single-section handling keeps the original error rendering for the | ||
| // "haven't typed `§ PATH` yet" case. | ||
| const result = await computeHashlineDiff({ input, path: args.path }, ctx.cwd, { | ||
| autoDropPureInsertDuplicates: ctx.hashlineAutoDropPureInsertDuplicates, | ||
|
|
@@ -590,8 +590,8 @@ const hashlineStrategy: EditStreamingStrategy<HashlineArgs> = { | |
| } | ||
| return previews.length > 0 ? previews : null; | ||
| }, | ||
| renderStreamingFallback(args, uiTheme) { | ||
| return typeof args.input === "string" ? renderHashlineInputFallback(args.input, uiTheme) : ""; | ||
| renderStreamingPlaceholder(args, uiTheme) { | ||
| return typeof args.input === "string" ? renderHashlineInputPlaceholder(args.input, uiTheme) : ""; | ||
| }, | ||
| }; | ||
|
|
||
|
|
@@ -642,7 +642,7 @@ const applyPatchStrategy: EditStreamingStrategy<ApplyPatchArgs> = { | |
| } | ||
| return previews.length > 0 ? previews : null; | ||
| }, | ||
| renderStreamingFallback() { | ||
| renderStreamingPlaceholder() { | ||
| return ""; | ||
| }, | ||
| }; | ||
|
|
@@ -656,7 +656,7 @@ const vimStrategy: EditStreamingStrategy<unknown> = { | |
| async computeDiffPreview() { | ||
| return null; | ||
| }, | ||
| renderStreamingFallback() { | ||
| renderStreamingPlaceholder() { | ||
| return ""; | ||
| }, | ||
| }; | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,48 @@ | ||
| // A/B adapter for word-level intra-line diff rendering. | ||
| // | ||
| // The A/B runner installs this file into the base worktree too, so both sides | ||
| // time the same fixtures through the same public entrypoint (`renderDiff`), | ||
| // which computes a word diff for every adjacent removed/added line pair. Only | ||
| // the implementation behind it differs (jsdiff on base, native on head). | ||
| import { Settings } from "../../coding-agent/src/config/settings"; | ||
| import { renderDiff } from "../../coding-agent/src/modes/components/diff"; | ||
| import { initTheme } from "../../coding-agent/src/modes/theme/theme"; | ||
| import { runAbSuite } from "./ab-adapter"; | ||
|
|
||
| await initTheme("dark"); | ||
| await Settings.init({ inMemory: true, cwd: process.cwd() }); | ||
|
|
||
| interface Fixture { | ||
| id: string; | ||
| diffText: string; | ||
| } | ||
|
|
||
| function makeFixture(id: string, pairCount: number): Fixture { | ||
| const lines: string[] = []; | ||
| for (let index = 0; index < pairCount; index++) { | ||
| const base = `const record${index} = compute(alpha, amber, gamma, delta, epsilon, zeta, eta, theta, iota, kappa, ${index});`; | ||
| const edited = base.replace("amber", "violet").replace("theta", `theta${index % 7}`); | ||
| const lineNo = String(index + 1).padStart(4, " "); | ||
| lines.push(`-${lineNo}|${base}`); | ||
| lines.push(`+${lineNo}|${edited}`); | ||
| } | ||
| return { id, diffText: lines.join("\n") }; | ||
| } | ||
|
|
||
| const fixtures = [makeFixture("D01", 10), makeFixture("D02", 100), makeFixture("D03", 500)]; | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
When this suite is used to mark A-PI-DIFF/B-DIFF adopted, it does not clear the documented H04 gate: all three fixtures merely repeat one synthetic line shape, and the only output validation is a length check rather than a base/head byte comparison. Useful? React with 👍 / 👎. |
||
|
|
||
| for (const fixture of fixtures) { | ||
| const rendered = await renderDiff(fixture.diffText); | ||
| if (typeof rendered !== "string" || rendered.length < fixture.diffText.length) { | ||
| throw new Error(`${fixture.id}: renderDiff produced unexpectedly short output`); | ||
| } | ||
| } | ||
|
|
||
| await runAbSuite( | ||
| "word-diff", | ||
| fixtures.map(fixture => ({ | ||
| id: fixture.id, | ||
| run: () => renderDiff(fixture.diffText), | ||
| })), | ||
| 20, | ||
| ); | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
For the W01 workload—one normal 40×120
ProcessTerminalrepaint—the committeddocs/rust-porting/evidence/native-ab-tty-write.jsonreports this direct N-API UTF-8 rewrite as a FAIL, with a p50 ratio CI of 1.0697–1.1114. That is a measured 7–11% regression in full-frame delivery, and the inventory consequently still leaves B-TTY-WRITER awaiting an owner decision; retain the previous conversion or resolve and explicitly accept this regression before switching the production path.Useful? React with 👍 / 👎.