1
0

feat: Introduce robust hashline anchor validation and new editing tools.

This commit is contained in:
2026-03-06 02:35:46 +01:00
parent 119e623f5a
commit 82ef63c731
3 changed files with 55 additions and 39 deletions

View File

@@ -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;
}

View File

@@ -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;
}
}
}
}

View File

@@ -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,7 +120,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.";
}
}
@@ -132,10 +132,10 @@ internal static partial class EditTools
[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,7 +162,7 @@ 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.";
}
}
}
@@ -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.";
}
}
@@ -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.";
}
}