Skip to content

feat: add replace_lines tool - token-efficient line-based editing#385

Open
hl9020 wants to merge 2 commits intowonderwhy-er:mainfrom
hl9020:feature/replace-lines
Open

feat: add replace_lines tool - token-efficient line-based editing#385
hl9020 wants to merge 2 commits intowonderwhy-er:mainfrom
hl9020:feature/replace-lines

Conversation

@hl9020
Copy link
Copy Markdown

@hl9020 hl9020 commented Mar 19, 2026

Summary

Adds a new replace_lines tool with configurable editMode scaffolding, directly addressing #68 and incorporating @wonderwhy-er's feedback on configurable tool sets.

Problem

edit_block requires sending both old_string + new_string, which doubles token usage for edits where the position is already known from a previous read_file call.

Solution

1. replace_lines tool

replace_lines(path, startLine, endLine, newContent) - when line numbers are known from read_file output, only line numbers + new content are needed. Roughly 50% fewer tokens per edit.

Parameter Type Description
path string File path
startLine number First line to replace (1-based, from read_file output)
endLine number Last line to replace (1-based, inclusive)
newContent string Replacement text (can be more or fewer lines). Empty string = deletion.

Response includes context and shift warnings:

Replaced lines 3-5 (3 lines) with 5 lines in app.ts

WARNING: Line count changed by +2. All line numbers after line 7 have shifted. Re-read before further edits.

Context (lines 1-10, + = new content):
     1  import { foo } from './bar';
     2  
+    3  // New implementation
+    4  export function handler() {
+    5    return processData();
+    6  }
+    7  
     8  export default app;

2. editMode config option

Per @wonderwhy-er's suggestion, a new config value controls which editing tools are registered:

Value Tools registered Description
"string-replace" (default) edit_block only Current behavior, lean system prompt
"line-replace" replace_lines only Line-based editing only
"both" Both tools User opts in to both

Set via set_config_value("editMode", "both"), takes effect after restart. Invalid values are rejected with a clear error message. Naturally extensible for a future "diff" mode.

Design decisions

  • Opt-in by default - replace_lines not registered unless explicitly enabled, keeping the default context lean
  • Complements, not replaces edit_block
  • 1-based line numbers matching read_file output
  • Empty newContent = true deletion (no leftover blank line)
  • Preserves line endings (handles CRLF, LF, and standalone CR)
  • Context in response with + markers and line-shift warnings
  • Input validation on editMode values

Changes

6 files:

  • src/config-field-definitions.ts - editMode field definition for settings panel
  • src/config-manager.ts - editMode in ServerConfig interface + input validation
  • src/tools/schemas.ts - ReplaceLinesArgsSchema with Zod validation
  • src/tools/edit.ts - handleReplaceLines implementation with context output
  • src/handlers/edit-search-handlers.ts - Export new handler
  • src/server.ts - shouldIncludeTool with async editMode check, tool definition, switch case

Testing

24 test scenarios across 4 categories, all passing:

Config (A1-A8): All three editMode values tested with real restarts - correct tools registered/hidden in each mode.

Functionality (B1-B8): Equal line count, expansion (+4 shift), shrink (-4 shift), deletion (true removal), boundary cases (first/last line), error handling (out of bounds, invalid range).

Compatibility (C1): edit_block works correctly in "both" mode.

Validation (E1-E4): Invalid editMode values rejected, config persists on disk.

Closes #68

Summary by CodeRabbit

  • New Features
    • Added a new line-based file editing tool for replacing specific line ranges in files.
    • Added editMode configuration option to control which editing tools are available: string-replace, line-replace, or both.

@codeant-ai
Copy link
Copy Markdown
Contributor

codeant-ai Bot commented Mar 19, 2026

CodeAnt AI is reviewing your PR.


Thanks for using CodeAnt! 🎉

We're free for open-source projects. if you're enjoying it, help us grow by sharing.

Share on X ·
Reddit ·
LinkedIn

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Mar 19, 2026

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: cebac65b-a53b-4132-a811-1204151484e0

📥 Commits

Reviewing files that changed from the base of the PR and between e2b086f and 8de629b.

📒 Files selected for processing (6)
  • src/config-field-definitions.ts
  • src/config-manager.ts
  • src/handlers/edit-search-handlers.ts
  • src/server.ts
  • src/tools/edit.ts
  • src/tools/schemas.ts
🚧 Files skipped from review as they are similar to previous changes (4)
  • src/handlers/edit-search-handlers.ts
  • src/config-manager.ts
  • src/tools/schemas.ts
  • src/config-field-definitions.ts

📝 Walkthrough

Walkthrough

A new replace_lines MCP tool is added for line-based file editing, registered with mode-based availability control in the server, backed by configuration support and corresponding schemas and handlers.

Changes

Cohort / File(s) Summary
Replace Lines Tool Implementation
src/tools/schemas.ts, src/tools/edit.ts
Introduces ReplaceLinesArgsSchema and new handleReplaceLines handler that parses arguments, validates line ranges, detects file line endings, performs inclusive line-range replacement, writes updates to disk, and emits telemetry with 3-line context windows.
Server Tool Registration & Dispatch
src/server.ts
Registers replace_lines tool with mode-based filtering; makes shouldIncludeTool async to gate tools by editMode config (exclude replace_lines when "string-replace", exclude edit_block when "line-replace", include both when "both"); adds tool invocation case for replace_lines.
Handler Re-export
src/handlers/edit-search-handlers.ts
Re-exports handleReplaceLines alongside existing handleEditBlock.
Configuration Support
src/config-field-definitions.ts, src/config-manager.ts
Adds editMode config field with string type and optional values 'string-replace' | 'line-replace' | 'both'; includes validation logic to sanitize invalid persisted values and reject invalid assignments.

Sequence Diagram(s)

sequenceDiagram
    participant Client
    participant Server as MCP Server
    participant Handler as handleReplaceLines
    participant FS as File System
    participant Telemetry

    Client->>Server: CallToolRequest (replace_lines)
    Server->>Handler: invoke with args
    Handler->>Handler: parse & validate args
    Handler->>FS: read file as text
    FS-->>Handler: file content
    Handler->>Handler: detect line endings
    Handler->>Handler: validate line range
    Handler->>Handler: split into lines
    Handler->>Handler: replace line range
    Handler->>Handler: rejoin with original separator
    Handler->>FS: write updated content
    FS-->>Handler: success
    Handler->>Telemetry: capture (removed/inserted lines, extension)
    Handler-->>Server: ServerResult with context
    Server-->>Client: response
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

  • PR #86: Modifies edit handler exports and tool registration in similar edit-related code paths.
  • PR #103: Adds line-ending detection and normalization logic to src/tools/edit.ts affecting edit behavior.
  • PR #341: Refactors editing tool surface and schemas; related through concurrent editing tool changes.

Suggested reviewers

  • serg33v
  • dmitry-ottic-ai
  • scutuatua-crypto

Poem

🐰 A rabbit hops with glee today,
New line-replace tools come to play!
No more repeating blocks of code,
Just line numbers on the road!
Tokens saved, the LLM sings,
Efficient editing—what joy it brings! ✨

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 44.44% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'feat: add replace_lines tool - token-efficient line-based editing' accurately reflects the main change: introducing a new replace_lines tool with token efficiency as a key benefit.
Linked Issues check ✅ Passed The PR fully addresses issue #68's objectives: reduces token consumption via line-indexed replacement, avoids repeating original block content, preserves string-based edit_block, and includes mitigations (context, shift warnings) for LLM accuracy concerns.
Out of Scope Changes check ✅ Passed All changes are scoped to implementing replace_lines, its configuration scaffolding, and integration. No unrelated modifications detected.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@codeant-ai codeant-ai Bot added the size:L This PR changes 100-499 lines, ignoring generated files label Mar 19, 2026
@codeant-ai
Copy link
Copy Markdown
Contributor

codeant-ai Bot commented Mar 19, 2026

Sequence Diagram

This PR adds a new replace_lines tool that lets clients update files by line number range instead of sending old and new blocks. The core flow is a read to obtain line numbers, then a targeted line replacement that rewrites the file and returns an edit summary.

sequenceDiagram
    participant Client
    participant Server
    participant FileSystem

    Client->>Server: Read file to get numbered lines
    Server-->>Client: Return file content with line numbers
    Client->>Server: Call replace_lines with path and line range
    Server->>FileSystem: Read file and replace requested lines
    FileSystem-->>Server: Save updated file content
    Server-->>Client: Return replaced line range summary
Loading

Generated by CodeAnt AI

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (2)
src/tools/edit.ts (2)

475-490: Standalone \r line endings not fully handled.

The split regex /\r?\n/ handles \n and \r\n but not standalone \r (classic Mac format). If detectLineEnding returns '\r', the file won't split into lines correctly, and the separator fallback on line 490 would use '\n' instead, potentially corrupting the file.

This is a rare edge case (pre-OS X Mac format), but for consistency with detectLineEnding's contract, consider:

♻️ Suggested handling for standalone \r
-    const lines = content.split(/\r?\n/);
+    // Split on any line ending style the file uses
+    const lines = content.split(/\r\n|\r|\n/);
     const totalLines = lines.length;
     ...
-    const sep = fileLineEnding === '\r\n' ? '\r\n' : '\n';
+    const sep = fileLineEnding;
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/tools/edit.ts` around lines 475 - 490, The code assumes only '\n' or
'\r\n' in detectLineEnding and uses content.split(/\r?\n/) and sep fallback to
'\n', which breaks when detectLineEnding returns '\r'; update splitting to
handle standalone '\r' (use content.split(/\r\n|\r|\n/) or equivalent) and
ensure newLines = parsed.newContent.split(/\r\n|\r|\n/), and set sep to
fileLineEnding (handle '\r' explicitly) so fileLineEnding, detectLineEnding,
content.split, parsed.newContent.split, and sep consistently handle '\r', '\n',
and '\r\n'.

486-491: Empty newContent inserts a blank line rather than deleting.

When newContent is an empty string, "".split(/\r?\n/) returns [""] (array with one empty string), so the result inserts a blank line rather than truly deleting lines 5-8 as the tool description suggests.

Consider whether this is the intended behavior. If true deletion is expected:

♻️ Handle empty content as true deletion
-    const newLines = parsed.newContent.split(/\r?\n/);
+    const newLines = parsed.newContent === '' ? [] : parsed.newContent.split(/\r?\n/);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/tools/edit.ts` around lines 486 - 491, The code uses
parsed.newContent.split(...) which turns an empty string into [""] and inserts a
blank line instead of deleting; update the logic around parsed.newContent (used
to build newLines/result/newContent) to treat an empty string as an empty array
(i.e., true deletion) — e.g., when parsed.newContent === '' set newLines = []
(otherwise use the existing split), then rebuild result and newContent as before
using fileLineEnding to join.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@src/tools/edit.ts`:
- Around line 475-490: The code assumes only '\n' or '\r\n' in detectLineEnding
and uses content.split(/\r?\n/) and sep fallback to '\n', which breaks when
detectLineEnding returns '\r'; update splitting to handle standalone '\r' (use
content.split(/\r\n|\r|\n/) or equivalent) and ensure newLines =
parsed.newContent.split(/\r\n|\r|\n/), and set sep to fileLineEnding (handle
'\r' explicitly) so fileLineEnding, detectLineEnding, content.split,
parsed.newContent.split, and sep consistently handle '\r', '\n', and '\r\n'.
- Around line 486-491: The code uses parsed.newContent.split(...) which turns an
empty string into [""] and inserts a blank line instead of deleting; update the
logic around parsed.newContent (used to build newLines/result/newContent) to
treat an empty string as an empty array (i.e., true deletion) — e.g., when
parsed.newContent === '' set newLines = [] (otherwise use the existing split),
then rebuild result and newContent as before using fileLineEnding to join.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 3a04241a-67da-4fd7-b210-e6ba86f3b8b4

📥 Commits

Reviewing files that changed from the base of the PR and between d854870 and 6b0615b.

📒 Files selected for processing (4)
  • src/handlers/edit-search-handlers.ts
  • src/server.ts
  • src/tools/edit.ts
  • src/tools/schemas.ts

Comment thread src/tools/edit.ts Outdated

const before = lines.slice(0, parsed.startLine - 1);
const after = lines.slice(parsed.endLine);
const newLines = parsed.newContent.split(/\r?\n/);
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggestion: Splitting newContent directly turns an empty string into [''], so a delete operation (newContent: "") inserts one blank line instead of removing the target range. Treat empty replacement content as zero lines to preserve the documented delete behavior. [logic error]

Severity Level: Major ⚠️
- ❌ replace_lines delete mode leaves unintended blank line.
- ⚠️ Line numbers shift, subsequent read_file-based edits misalign.
- ⚠️ Deletion telemetry reports inserted line count as one.
Suggested change
const newLines = parsed.newContent.split(/\r?\n/);
const newLines = parsed.newContent === '' ? [] : parsed.newContent.split(/\r?\n/);
Steps of Reproduction ✅
1. Trigger MCP tool execution through `server.setRequestHandler(CallToolRequestSchema,
...)` at `src/server.ts:1201` with tool name `replace_lines`.

2. The request is routed by the switch case `case "replace_lines"` at
`src/server.ts:1452-1453` into `handlers.handleReplaceLines(args)`.

3. Use the documented delete flow shown in the tool description at `src/server.ts:809`:
`startLine=5, endLine=8, newContent=""` (empty string). This input is valid because schema
allows any string via `newContent: z.string()` at `src/tools/schemas.ts:212`.

4. In `handleReplaceLines` (`src/tools/edit.ts:466`), line splitting at
`src/tools/edit.ts:488` executes `''.split(/\r?\n/)`, producing `['']` (one empty line),
then `result = [...before, ...newLines, ...after]` at `src/tools/edit.ts:489` keeps one
blank line instead of removing the range.

5. File is written with the unintended blank line at `src/tools/edit.ts:493`, and
response/telemetry report `insertedCount = newLines.length` (`src/tools/edit.ts:496`) as
`1`, contradicting expected delete semantics.
Prompt for AI Agent 🤖
This is a comment left during a code review.

**Path:** src/tools/edit.ts
**Line:** 488:488
**Comment:**
	*Logic Error: Splitting `newContent` directly turns an empty string into `['']`, so a delete operation (`newContent: ""`) inserts one blank line instead of removing the target range. Treat empty replacement content as zero lines to preserve the documented delete behavior.

Validate the correctness of the flagged issue. If correct, How can I resolve this? If you propose a fix, implement it and please make it concise.
👍 | 👎

@codeant-ai
Copy link
Copy Markdown
Contributor

codeant-ai Bot commented Mar 19, 2026

CodeAnt AI finished reviewing your PR.

@wonderwhy-er
Copy link
Copy Markdown
Owner

Hi @hl9020

Thanks for this contribution! The core idea is solid and I want to merge something here, but I have a few thoughts before we do.

✅ On merging: I like that this is designed to complement rather than replace edit_block — that framing is right. I'm in favor of landing this in some form.


⚠️ On the token problem it creates

There's an irony worth calling out — we added a tool to save tokens, but every registered tool expands the system prompt/context sent to the model on every request. So the net win is smaller than it appears, and for light usage it might be a net loss.

Proposed solution — configurable tool sets: I'd like to introduce a config option (e.g. editMode: "string-replace" | "line-replace" | "both", defaulting to "string-replace") that controls which editing tools are registered. Users who want replace_lines can opt in, keeping the default context lean.


📖 Some historical context

Line-based replacement is actually where DC started, and I moved away from it deliberately. Two failure modes I kept hitting:

  1. Wrong line numbers — models occasionally get them wrong, especially in longer files
  2. Stale line numbers — if the file changed between read_file and the edit, you silently replace the wrong lines

String-based matching fails loudly in both cases — it either finds the string or it doesn't. That "false positive prevention" is a meaningful reliability improvement, not just a style preference.


🔭 Looking ahead — diff-based editing

I've been thinking about revisiting unified diff support. I tested it a few years ago and models made a lot of mistakes, but modern models handle diffs much better. It sits in an interesting middle ground:

Method Token cost Speed Error-proneness
String replace Highest Slowest Lowest — fails loudly on mismatch
Diff Medium Fast Medium — depends on model quality
Line replace Lowest Fastest Highest — silent wrong replacements

Longer term I'd like DC to support all three via a single config value, and potentially auto-select based on the connected client/model.

Would you be open to building this PR on top of that config scaffolding so it slots naturally into that future design?

@hl9020
Copy link
Copy Markdown
Author

hl9020 commented Mar 22, 2026

Hi @wonderwhy-er, thanks for the detailed feedback! Really appreciate the historical context - knowing DC started with line-based editing and you moved away deliberately is exactly the kind of insight we needed.

Your points are well taken:

On the token irony - completely valid. Every registered tool adds to the system prompt, so a tool meant to save tokens could be a net loss for light usage. The configurable editMode approach makes sense.

On reliability - we actually ran into the "stale line numbers" problem ourselves while developing this. That's why we added context lines in the response (3 lines before/after with + markers) and explicit line-shift warnings when the line count changes. These mitigate the problem but don't eliminate it - your point that string-based matching "fails loudly" vs line-based "fails silently" is a meaningful distinction.

On the config scaffolding - happy to build this. We'll add editMode to configManager and wire it into shouldIncludeTool() so replace_lines is opt-in (default "string-replace"). That keeps the default context lean and is naturally extensible for a future "diff" mode.

We'll look at the implementation details and come back with a concrete update to this PR.

@hl9020 hl9020 force-pushed the feature/replace-lines branch from 5e65cf2 to e2b086f Compare March 26, 2026 16:22
@codeant-ai
Copy link
Copy Markdown
Contributor

codeant-ai Bot commented Mar 26, 2026

CodeAnt AI is running Incremental review


Thanks for using CodeAnt! 🎉

We're free for open-source projects. if you're enjoying it, help us grow by sharing.

Share on X ·
Reddit ·
LinkedIn

@codeant-ai codeant-ai Bot added size:L This PR changes 100-499 lines, ignoring generated files and removed size:L This PR changes 100-499 lines, ignoring generated files labels Mar 26, 2026
@hl9020
Copy link
Copy Markdown
Author

hl9020 commented Mar 26, 2026

Hi @wonderwhy-er, we've reworked the PR based on your feedback. Here's what changed:

editMode config scaffolding - added as you suggested:

  • New config option editMode: "string-replace" (default) | "line-replace" | "both"
  • Wired into shouldIncludeTool() so replace_lines is only registered when opted in
  • Default behavior unchanged - only edit_block is registered out of the box
  • Input validation rejects invalid values with a clear error message
  • Naturally extensible for a future "diff" mode (just add the value + handler)

Config field definition added to config-field-definitions.ts so it appears in the settings panel.

CodeRabbit fixes incorporated:

  • Empty newContent now performs true deletion (no leftover blank line)
  • Line splitting handles standalone \r (classic Mac format)

Tested across 24 scenarios including all three editMode values with real restarts, boundary cases, deletion, expansion, shrink, error handling, and validation.

Changes: 6 files (config-field-definitions.ts, config-manager.ts, schemas.ts, edit.ts, edit-search-handlers.ts, server.ts)

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 6

🧹 Nitpick comments (1)
src/config-field-definitions.ts (1)

41-45: Expose allowed editMode values in field metadata (not just runtime).

valueType: 'string' works, but it doesn’t communicate valid options (string-replace | line-replace | both) to config consumers/UI. Consider adding constrained options in ConfigFieldDefinition so invalid values are blocked before submission.

♻️ Proposed metadata extension
 export type ConfigFieldDefinition = {
   label: string;
   description: string;
   valueType: ConfigFieldValueType;
+  allowedValues?: readonly string[];
 };
   editMode: {
     label: 'Edit Mode',
     description: 'Controls which file editing tools are registered. "string-replace" (default) uses edit_block with fuzzy matching. "line-replace" uses replace_lines with line numbers. "both" registers both tools. Fewer tools means a leaner system prompt.',
     valueType: 'string',
+    allowedValues: ['string-replace', 'line-replace', 'both'],
   },
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/config-field-definitions.ts` around lines 41 - 45, The editMode field
currently only sets valueType: 'string' and should include explicit allowed
options so UIs and validators can surface/block invalid values; update the
editMode entry in src/config-field-definitions.ts to add a constrained-values
metadata property (e.g., allowedValues, enum, options — whichever the existing
ConfigFieldDefinition type uses or extend ConfigFieldDefinition to include it)
listing 'string-replace', 'line-replace', and 'both', and update the
ConfigFieldDefinition type (or its consumer) to accept this property so runtime
validation and UI pickers can use it.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/config-manager.ts`:
- Around line 215-221: The editMode check in setValue() is insufficient because
updateConfig() and init() can accept invalid values; add a single shared
validation routine (e.g., validateEditMode or validateConfigEntry) that enforces
the allowed values ['string-replace','line-replace','both'] and use it from
setValue(), updateConfig(), and wherever init() loads on-disk config; when
invalid, either throw a clear Error or normalize to a safe default and log the
rejection so invalid editMode cannot slip into runtime config or tool
registration in src/server.ts.

In `@src/terminal-manager.ts`:
- Around line 48-67: The buildWindowsPath function currently rebuilds PATH only
from Machine/User registry values and nodeDir, dropping any live process PATH
entries; update buildWindowsPath to include process.env.PATH (the live PATH)
when constructing merged (e.g., merge [...new Set([nodeDir, ...sys.split(';'),
...user.split(';'), ...existing.split(';')].filter(Boolean))]) so
wrapper/launch-time injected entries are preserved, and ensure the code that
forces the WINDOWS_PATH into spawned child processes continues to use this
merged value so runtime PATH entries are not lost.
- Around line 34-45: The current resolveShellPath function replaces any string
whose basename matches known shells, which incorrectly rewrites explicit paths
like "C:\tools\pwsh.exe" or "./powershell.exe"; change the logic to only apply
the PS5_PATH/PS7_PATH/CMD_PATH replacements when the caller passed a plain shell
name (no directory separators and not a relative path). In resolveShellPath,
detect explicit paths by checking for path separators or a leading
"."/path.isAbsolute and if the input contains one return shellName unchanged;
only when the input is just a basename (e.g. path.basename(shellName) ===
shellName) perform the existing existence checks against PS5_PATH, PS7_PATH,
CMD_PATH and return the mapped path.
- Around line 136-140: The PowerShell invocation currently passes the
-NonInteractive flag in the branches that handle shellName === 'pwsh' /
'pwsh.exe' and shellName === 'powershell' / 'powershell.exe' which prevents
interactive prompts from working; remove the '-NonInteractive' arg from the args
arrays returned by those branches in the terminal manager (leave '-NoProfile',
'-Command', command and useShellOption: false intact) so sendInputToProcess()
can satisfy Read-Host/confirmation/credential prompts.

In `@src/tools/edit.ts`:
- Line 493: The write uses parsed.path while the file was read using validPath,
causing possible read/write drift; change the write to use the same resolved
validated path (validPath) instead of parsed.path so the writeFile call writes
newContent to validPath (replace the reference to parsed.path in the writeFile
invocation), ensuring both read and write operate on the same validated path.
- Line 523: The warning message uses insertEnd as the anchor and can produce
"after line 0" when a deletion starts at the top of the file; update the string
construction around msg (the line that uses lineDelta and insertEnd) to
special-case insertEnd === 0 and substitute a clearer anchor such as "start of
file" (or "before line 1") instead of "after line 0". Locate the code building
msg (references: msg, lineDelta, insertEnd) and change only the wording logic so
it selects the normal "after line ${insertEnd}" text for insertEnd > 0 and the
clearer anchor text for insertEnd === 0.

---

Nitpick comments:
In `@src/config-field-definitions.ts`:
- Around line 41-45: The editMode field currently only sets valueType: 'string'
and should include explicit allowed options so UIs and validators can
surface/block invalid values; update the editMode entry in
src/config-field-definitions.ts to add a constrained-values metadata property
(e.g., allowedValues, enum, options — whichever the existing
ConfigFieldDefinition type uses or extend ConfigFieldDefinition to include it)
listing 'string-replace', 'line-replace', and 'both', and update the
ConfigFieldDefinition type (or its consumer) to accept this property so runtime
validation and UI pickers can use it.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 2fd4cfca-dc2d-422e-8f84-09094b73393c

📥 Commits

Reviewing files that changed from the base of the PR and between 5e65cf2 and e2b086f.

⛔ Files ignored due to path filters (1)
  • package-lock.json is excluded by !**/package-lock.json
📒 Files selected for processing (7)
  • src/config-field-definitions.ts
  • src/config-manager.ts
  • src/handlers/edit-search-handlers.ts
  • src/server.ts
  • src/terminal-manager.ts
  • src/tools/edit.ts
  • src/tools/schemas.ts
🚧 Files skipped from review as they are similar to previous changes (3)
  • src/handlers/edit-search-handlers.ts
  • src/tools/schemas.ts
  • src/server.ts

Comment thread src/config-manager.ts
Comment thread src/terminal-manager.ts Outdated
Comment on lines +34 to +45
function resolveShellPath(shellName: string): string {
const n = path.basename(shellName).toLowerCase();
if (n === 'powershell.exe' || n === 'powershell') {
if (fs.existsSync(PS5_PATH)) return PS5_PATH;
}
if (n === 'pwsh.exe' || n === 'pwsh') {
if (fs.existsSync(PS7_PATH)) return PS7_PATH;
}
if (n === 'cmd.exe' || n === 'cmd') {
if (fs.existsSync(CMD_PATH)) return CMD_PATH;
}
return shellName;
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Don't rewrite explicit shell paths.

Lines 34-45 key off path.basename(), so a configured value like C:\tools\pwsh.exe or .\powershell.exe gets replaced with the hardcoded default install path whenever that default exists. That ignores the caller's explicit shell selection and can silently bypass wrappers or shims.

Suggested fix
 function resolveShellPath(shellName: string): string {
-  const n = path.basename(shellName).toLowerCase();
+  if (path.isAbsolute(shellName) || /[\\/]/.test(shellName)) {
+    return shellName;
+  }
+  const n = shellName.toLowerCase();
   if (n === 'powershell.exe' || n === 'powershell') {
     if (fs.existsSync(PS5_PATH)) return PS5_PATH;
   }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
function resolveShellPath(shellName: string): string {
const n = path.basename(shellName).toLowerCase();
if (n === 'powershell.exe' || n === 'powershell') {
if (fs.existsSync(PS5_PATH)) return PS5_PATH;
}
if (n === 'pwsh.exe' || n === 'pwsh') {
if (fs.existsSync(PS7_PATH)) return PS7_PATH;
}
if (n === 'cmd.exe' || n === 'cmd') {
if (fs.existsSync(CMD_PATH)) return CMD_PATH;
}
return shellName;
function resolveShellPath(shellName: string): string {
if (path.isAbsolute(shellName) || /[\\/]/.test(shellName)) {
return shellName;
}
const n = shellName.toLowerCase();
if (n === 'powershell.exe' || n === 'powershell') {
if (fs.existsSync(PS5_PATH)) return PS5_PATH;
}
if (n === 'pwsh.exe' || n === 'pwsh') {
if (fs.existsSync(PS7_PATH)) return PS7_PATH;
}
if (n === 'cmd.exe' || n === 'cmd') {
if (fs.existsSync(CMD_PATH)) return CMD_PATH;
}
return shellName;
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/terminal-manager.ts` around lines 34 - 45, The current resolveShellPath
function replaces any string whose basename matches known shells, which
incorrectly rewrites explicit paths like "C:\tools\pwsh.exe" or
"./powershell.exe"; change the logic to only apply the
PS5_PATH/PS7_PATH/CMD_PATH replacements when the caller passed a plain shell
name (no directory separators and not a relative path). In resolveShellPath,
detect explicit paths by checking for path separators or a leading
"."/path.isAbsolute and if the input contains one return shellName unchanged;
only when the input is just a basename (e.g. path.basename(shellName) ===
shellName) perform the existing existence checks against PS5_PATH, PS7_PATH,
CMD_PATH and return the mapped path.

Comment thread src/terminal-manager.ts Outdated
Comment on lines +48 to +67
function buildWindowsPath(): string {
if (process.platform !== 'win32') return process.env.PATH ?? '';
try {
const sys = execSync(
`${PS5_PATH} -NoProfile -NonInteractive -Command "[System.Environment]::GetEnvironmentVariable('Path','Machine')"`,
{ encoding: 'utf8', timeout: 3000 }
).replace(/\r?\n|\r|"/g, '').trim();
const user = execSync(
`${PS5_PATH} -NoProfile -NonInteractive -Command "[System.Environment]::GetEnvironmentVariable('Path','User')"`,
{ encoding: 'utf8', timeout: 3000 }
).replace(/\r?\n|\r|"/g, '').trim();
// Also include the directory of the current node executable
const nodeDir = path.dirname(process.execPath);
const merged = [...new Set([nodeDir, ...sys.split(';'), ...user.split(';')].filter(Boolean))];
return merged.join(';');
} catch {
const nodeDir = path.dirname(process.execPath);
const existing = process.env.PATH ?? '';
return `${nodeDir};${existing}`;
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Preserve the live process PATH when building WINDOWS_PATH.

Lines 59-62 rebuild Path from Machine/User registry values plus nodeDir, and Lines 112-113 then force that value into every Windows child process. That drops any launch-time or wrapper-injected entries that exist only in the current process, so commands can work in the server and then stop resolving once spawned.

Suggested fix
 function buildWindowsPath(): string {
   if (process.platform !== 'win32') return process.env.PATH ?? '';
   try {
@@
     // Also include the directory of the current node executable
     const nodeDir = path.dirname(process.execPath);
-    const merged = [...new Set([nodeDir, ...sys.split(';'), ...user.split(';')].filter(Boolean))];
+    const current = process.env.Path ?? process.env.PATH ?? '';
+    const merged = [...new Set([
+      ...current.split(';'),
+      nodeDir,
+      ...sys.split(';'),
+      ...user.split(';'),
+    ].filter(Boolean))];
     return merged.join(';');
   } catch {
     const nodeDir = path.dirname(process.execPath);
-    const existing = process.env.PATH ?? '';
+    const existing = process.env.Path ?? process.env.PATH ?? '';
     return `${nodeDir};${existing}`;
   }
 }

Also applies to: 112-113

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/terminal-manager.ts` around lines 48 - 67, The buildWindowsPath function
currently rebuilds PATH only from Machine/User registry values and nodeDir,
dropping any live process PATH entries; update buildWindowsPath to include
process.env.PATH (the live PATH) when constructing merged (e.g., merge [...new
Set([nodeDir, ...sys.split(';'), ...user.split(';'),
...existing.split(';')].filter(Boolean))]) so wrapper/launch-time injected
entries are preserved, and ensure the code that forces the WINDOWS_PATH into
spawned child processes continues to use this merged value so runtime PATH
entries are not lost.

Comment thread src/terminal-manager.ts Outdated
Comment on lines +136 to +140
if (shellName === 'pwsh' || shellName === 'pwsh.exe') {
return {
executable: shellPath,
args: ['-Login', '-Command', command],
useShellOption: false
};
return { executable: resolved, args: ['-NoProfile', '-NonInteractive', '-Command', command], useShellOption: false };
}

// Windows PowerShell 5.1 (no login flag support)
if (shellName === 'powershell' || shellName === 'powershell.exe') {
return {
executable: shellPath,
args: ['-Command', command],
useShellOption: false
};
return { executable: resolved, args: ['-NoProfile', '-NonInteractive', '-Command', command], useShellOption: false };
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🌐 Web query:

PowerShell CLI: does -NonInteractivedisableRead-Host, confirmation prompts, or other interactive prompts when invoking powershell.exe -Command ...orpwsh -Command ...?

💡 Result:

Yes, the -NonInteractive switch for both powershell.exe and pwsh.exe disables interactive prompts including Read-Host and confirmation prompts when used with -Command (or -File). Instead of hanging waiting for input, these attempts result in statement-terminating errors such as "Windows PowerShell is in NonInteractive mode. Read and Prompt functionality is not available." or equivalent. This behavior is explicitly documented in the official Microsoft documentation for both Windows PowerShell (about_PowerShell_exe) and PowerShell 7+ (about_Pwsh): - "-NonInteractive | -noni: This switch is used to create sessions that shouldn't require user input. This is useful for scripts that run in scheduled tasks or CI/CD pipelines. Any attempts to use interactive features, like Read-Host or confirmation prompts, result in statement terminating errors rather than hanging." Practical examples confirm this: - powershell -NonInteractive -Command "Read-Host" throws the error. - Confirmation prompts from cmdlets (e.g., those using ShouldProcess/ShouldContinue) also fail similarly. Note: If -NonInteractive is used without -Command/-File (e.g., pwsh -NonInteractive alone), it still starts an interactive shell, but interactive features within it will error out.

Citations:


Remove -NonInteractive from PowerShell invocations.

Hardcoding -NonInteractive will cause Read-Host, confirmation prompts, and credential prompts to fail immediately instead of allowing sendInputToProcess() to provide input. The -NonInteractive flag explicitly disables interactive features in PowerShell, converting them to statement-terminating errors. Since the class is designed to support blocked sessions and follow-up input, and lines 270-271 already handle prompt detection with multiline anchoring, removing this flag allows interactive commands to work as intended.

Suggested fix
   if (shellName === 'pwsh' || shellName === 'pwsh.exe') {
-    return { executable: resolved, args: ['-NoProfile', '-NonInteractive', '-Command', command], useShellOption: false };
+    return { executable: resolved, args: ['-NoProfile', '-Command', command], useShellOption: false };
   }
   if (shellName === 'powershell' || shellName === 'powershell.exe') {
-    return { executable: resolved, args: ['-NoProfile', '-NonInteractive', '-Command', command], useShellOption: false };
+    return { executable: resolved, args: ['-NoProfile', '-Command', command], useShellOption: false };
   }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
if (shellName === 'pwsh' || shellName === 'pwsh.exe') {
return {
executable: shellPath,
args: ['-Login', '-Command', command],
useShellOption: false
};
return { executable: resolved, args: ['-NoProfile', '-NonInteractive', '-Command', command], useShellOption: false };
}
// Windows PowerShell 5.1 (no login flag support)
if (shellName === 'powershell' || shellName === 'powershell.exe') {
return {
executable: shellPath,
args: ['-Command', command],
useShellOption: false
};
return { executable: resolved, args: ['-NoProfile', '-NonInteractive', '-Command', command], useShellOption: false };
if (shellName === 'pwsh' || shellName === 'pwsh.exe') {
return { executable: resolved, args: ['-NoProfile', '-Command', command], useShellOption: false };
}
if (shellName === 'powershell' || shellName === 'powershell.exe') {
return { executable: resolved, args: ['-NoProfile', '-Command', command], useShellOption: false };
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/terminal-manager.ts` around lines 136 - 140, The PowerShell invocation
currently passes the -NonInteractive flag in the branches that handle shellName
=== 'pwsh' / 'pwsh.exe' and shellName === 'powershell' / 'powershell.exe' which
prevents interactive prompts from working; remove the '-NonInteractive' arg from
the args arrays returned by those branches in the terminal manager (leave
'-NoProfile', '-Command', command and useShellOption: false intact) so
sendInputToProcess() can satisfy Read-Host/confirmation/credential prompts.

Comment thread src/tools/edit.ts Outdated
Comment thread src/tools/edit.ts Outdated
let msg = `Replaced lines ${parsed.startLine}-${parsed.endLine} (${removedCount} lines) with ${insertedCount} lines in ${parsed.path}`;

if (lineDelta !== 0) {
msg += `\n\nWARNING: Line count changed by ${lineDelta > 0 ? '+' : ''}${lineDelta}. All line numbers after line ${insertEnd} have shifted. Re-read before further edits.`;
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Avoid “after line 0” in shift warnings for top-of-file deletions.

At Line 523, deleting from Line 1 with zero inserted lines produces a confusing warning (“after line 0”). Use a clearer anchor for this edge case.

Proposed fix
-    if (lineDelta !== 0) {
-        msg += `\n\nWARNING: Line count changed by ${lineDelta > 0 ? '+' : ''}${lineDelta}. All line numbers after line ${insertEnd} have shifted. Re-read before further edits.`;
-    }
+    if (lineDelta !== 0) {
+        const shiftAnchorText =
+            insertEnd <= 0
+                ? 'from line 1 onward'
+                : `after line ${insertEnd}`;
+        msg += `\n\nWARNING: Line count changed by ${lineDelta > 0 ? '+' : ''}${lineDelta}. All line numbers ${shiftAnchorText} have shifted. Re-read before further edits.`;
+    }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
msg += `\n\nWARNING: Line count changed by ${lineDelta > 0 ? '+' : ''}${lineDelta}. All line numbers after line ${insertEnd} have shifted. Re-read before further edits.`;
if (lineDelta !== 0) {
const shiftAnchorText =
insertEnd <= 0
? 'from line 1 onward'
: `after line ${insertEnd}`;
msg += `\n\nWARNING: Line count changed by ${lineDelta > 0 ? '+' : ''}${lineDelta}. All line numbers ${shiftAnchorText} have shifted. Re-read before further edits.`;
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/tools/edit.ts` at line 523, The warning message uses insertEnd as the
anchor and can produce "after line 0" when a deletion starts at the top of the
file; update the string construction around msg (the line that uses lineDelta
and insertEnd) to special-case insertEnd === 0 and substitute a clearer anchor
such as "start of file" (or "before line 1") instead of "after line 0". Locate
the code building msg (references: msg, lineDelta, insertEnd) and change only
the wording logic so it selects the normal "after line ${insertEnd}" text for
insertEnd > 0 and the clearer anchor text for insertEnd === 0.

@codeant-ai
Copy link
Copy Markdown
Contributor

codeant-ai Bot commented Mar 26, 2026

CodeAnt AI Incremental review completed.

@hl9020 hl9020 force-pushed the feature/replace-lines branch from e2b086f to 8de629b Compare March 26, 2026 16:42
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size:L This PR changes 100-499 lines, ignoring generated files

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Observation: edit block takes a lot of token to repeat code that needs to be replaced

2 participants