feat: Introduce robust hashline anchor validation and new editing tools.
This commit is contained in:
@@ -62,17 +62,14 @@ internal static class HashlineValidator
|
||||
|
||||
if (lineNumber < 1 || lineNumber > lines.Length)
|
||||
{
|
||||
error = $"Anchor '{anchor}': line {lineNumber} is out of range " +
|
||||
$"(file has {lines.Length} line(s)).";
|
||||
error = $"Anchor '{anchor}': line {lineNumber} is out of range. Re-read the file ({lines.Length} lines).";
|
||||
return false;
|
||||
}
|
||||
|
||||
string actualHash = HashlineEncoder.ComputeHash(lines[lineNumber - 1].AsSpan(), lineNumber);
|
||||
if (!string.Equals(actualHash, expectedHash, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
error = $"Anchor '{anchor}': hash mismatch at line {lineNumber} " +
|
||||
$"(expected '{expectedHash}', got '{actualHash}'). " +
|
||||
$"The file has changed — re-read before editing.";
|
||||
error = $"Anchor '{anchor}': hash mismatch at line {lineNumber}. The file has changed — re-read before editing.";
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -98,10 +95,7 @@ internal static class HashlineValidator
|
||||
out int endIndex,
|
||||
out string error)
|
||||
{
|
||||
startIndex = -1;
|
||||
endIndex = -1;
|
||||
error = string.Empty;
|
||||
|
||||
if (!TryResolve(startAnchor, lines, out startIndex, out error))
|
||||
return false;
|
||||
|
||||
@@ -110,8 +104,7 @@ internal static class HashlineValidator
|
||||
|
||||
if (startIndex > endIndex)
|
||||
{
|
||||
error = $"Range error: start anchor '{startAnchor}' (line {startIndex + 1}) " +
|
||||
$"is after end anchor '{endAnchor}' (line {endIndex + 1}).";
|
||||
error = $"Range error: start anchor is after end anchor.";
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
55
ReplLoop.cs
55
ReplLoop.cs
@@ -216,20 +216,48 @@ internal sealed class ReplLoop
|
||||
}
|
||||
AnsiConsole.WriteLine();
|
||||
}
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
AnsiConsole.WriteLine();
|
||||
AnsiConsole.MarkupLine("[dim grey] ✗ Cancelled[/]");
|
||||
AnsiConsole.Write(new Rule().RuleStyle(Style.Parse("grey dim")));
|
||||
AnsiConsole.WriteLine();
|
||||
|
||||
if (!string.IsNullOrEmpty(fullResponse))
|
||||
// Save session after each LLM turn completes
|
||||
try
|
||||
{
|
||||
_session.History.Add(new ChatMessage(ChatRole.Assistant, fullResponse));
|
||||
const string sessionPath = ".anchor/session.json";
|
||||
var directory = Path.GetDirectoryName(sessionPath);
|
||||
if (!string.IsNullOrEmpty(directory) && !Directory.Exists(directory))
|
||||
{
|
||||
Directory.CreateDirectory(directory);
|
||||
}
|
||||
await _session.SaveAsync(sessionPath, default);
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
AnsiConsole.WriteLine();
|
||||
AnsiConsole.MarkupLine("[dim grey] ✗ Cancelled[/]");
|
||||
AnsiConsole.Write(new Rule().RuleStyle(Style.Parse("grey dim")));
|
||||
AnsiConsole.WriteLine();
|
||||
|
||||
if (!string.IsNullOrEmpty(fullResponse))
|
||||
{
|
||||
_session.History.Add(new ChatMessage(ChatRole.Assistant, fullResponse));
|
||||
}
|
||||
_session.History.Add(new ChatMessage(ChatRole.User,
|
||||
"[Response cancelled by user. Acknowledge briefly and wait for the next instruction. Do not repeat what was already said.]"));
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
AnsiConsole.WriteLine();
|
||||
AnsiConsole.Write(
|
||||
new Panel($"[red]{Markup.Escape(ex.Message)}[/]")
|
||||
.Header("[bold red] Error [/]")
|
||||
.BorderColor(Color.Red)
|
||||
.RoundedBorder()
|
||||
.Padding(1, 0));
|
||||
AnsiConsole.WriteLine();
|
||||
}
|
||||
finally
|
||||
{
|
||||
responseCts?.Dispose();
|
||||
responseCts = null;
|
||||
}
|
||||
_session.History.Add(new ChatMessage(ChatRole.User,
|
||||
"[Response cancelled by user. Acknowledge briefly and wait for the next instruction. Do not repeat what was already said.]"));
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
@@ -242,11 +270,6 @@ internal sealed class ReplLoop
|
||||
.Padding(1, 0));
|
||||
AnsiConsole.WriteLine();
|
||||
}
|
||||
finally
|
||||
{
|
||||
responseCts?.Dispose();
|
||||
responseCts = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -66,7 +66,7 @@ internal static partial class EditTools
|
||||
Log($" Replacing {endAnchor.Split(':')[0]}-{startAnchor.Split(':')[0]} lines with {newLines.Length} new lines");
|
||||
|
||||
if (!File.Exists(path))
|
||||
return $"ERROR: File not found: {path}";
|
||||
return $"ERROR: File not found: {path}\n Check the correct path and try again.";
|
||||
|
||||
try
|
||||
{
|
||||
@@ -74,7 +74,7 @@ internal static partial class EditTools
|
||||
|
||||
if (!HashlineValidator.TryResolveRange(startAnchor, endAnchor, lines,
|
||||
out int startIdx, out int endIdx, out string error))
|
||||
return $"ERROR: {error}";
|
||||
return $"ERROR: Anchor validation failed\n{error}";
|
||||
|
||||
var result = new List<string>(lines.Length - (endIdx - startIdx + 1) + newLines.Length);
|
||||
result.AddRange(lines[..startIdx]);
|
||||
@@ -86,7 +86,7 @@ internal static partial class EditTools
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
return $"ERROR modifying '{path}': {ex.Message}";
|
||||
return $"ERROR modifying '{path}': {ex.Message}.\nThis is a bug. Tell the user about it.";
|
||||
}
|
||||
}
|
||||
|
||||
@@ -120,22 +120,22 @@ internal static partial class EditTools
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
return $"ERROR modifying '{path}': {ex.Message}";
|
||||
return $"ERROR modifying '{path}': {ex.Message}\nThis is a bug. Tell the user about it.";
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
[Description("Delete a file or directory. Use mode='file' to delete a file, mode='dir' to delete a directory.")]
|
||||
[Description("Delete a file or directory. Use mode='file' to delete a file, mode='dir' to delete a directory.")]
|
||||
public static string Delete(
|
||||
[Description("Path to the file or directory to delete.")] string path,
|
||||
[Description("Type of deletion: 'file' or 'dir'. Defaults to 'file'.")] string mode = "file")
|
||||
{
|
||||
path = FileTools.ResolvePath(path);
|
||||
string targetType = mode.ToLower() == "dir" ? "directory" : "file";
|
||||
string targetType = mode.Equals("dir", StringComparison.CurrentCultureIgnoreCase) ? "directory" : "file";
|
||||
Log($"Deleting {targetType}: {path}");
|
||||
|
||||
if (mode.ToLower() == "dir")
|
||||
if (mode.Equals("dir", StringComparison.CurrentCultureIgnoreCase))
|
||||
{
|
||||
if (!Directory.Exists(path))
|
||||
return $"ERROR: Directory not found: {path}";
|
||||
@@ -147,7 +147,7 @@ internal static partial class EditTools
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
return $"ERROR deleting directory '{path}': {ex.Message}";
|
||||
return $"ERROR deleting directory '{path}': {ex.Message}\nThis is a bug. Tell the user about it.";
|
||||
}
|
||||
}
|
||||
else
|
||||
@@ -162,12 +162,12 @@ internal static partial class EditTools
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
return $"ERROR deleting '{path}': {ex.Message}";
|
||||
return $"ERROR deleting '{path}': {ex.Message}\nThis is a bug. Tell the user about it.";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
[Description("Move or copy a file to a new location.")]
|
||||
[Description("Move or copy a file to a new location.")]
|
||||
public static string MoveFile(
|
||||
[Description("Current path to the file.")] string sourcePath,
|
||||
[Description("New path for the file.")] string destinationPath,
|
||||
@@ -198,7 +198,7 @@ internal static partial class EditTools
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
return $"ERROR {action.ToLower()} file: {ex.Message}";
|
||||
return $"ERROR {action.ToLower()} file: {ex.Message}\nThis is a bug. Tell the user about it.";
|
||||
}
|
||||
}
|
||||
|
||||
@@ -241,7 +241,7 @@ internal static partial class EditTools
|
||||
File.WriteAllText(path, "");
|
||||
Log($" (created new file)");
|
||||
}
|
||||
|
||||
|
||||
using (var writer = new System.IO.StreamWriter(path, true))
|
||||
{
|
||||
foreach (var line in content)
|
||||
@@ -279,7 +279,7 @@ internal static partial class EditTools
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
return $"ERROR writing to '{path}': {ex.Message}";
|
||||
return $"ERROR writing to '{path}': {ex.Message}\nThis is a bug. Tell the user about it.";
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user