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) if (lineNumber < 1 || lineNumber > lines.Length)
{ {
error = $"Anchor '{anchor}': line {lineNumber} is out of range " + error = $"Anchor '{anchor}': line {lineNumber} is out of range. Re-read the file ({lines.Length} lines).";
$"(file has {lines.Length} line(s)).";
return false; return false;
} }
string actualHash = HashlineEncoder.ComputeHash(lines[lineNumber - 1].AsSpan(), lineNumber); string actualHash = HashlineEncoder.ComputeHash(lines[lineNumber - 1].AsSpan(), lineNumber);
if (!string.Equals(actualHash, expectedHash, StringComparison.OrdinalIgnoreCase)) if (!string.Equals(actualHash, expectedHash, StringComparison.OrdinalIgnoreCase))
{ {
error = $"Anchor '{anchor}': hash mismatch at line {lineNumber} " + error = $"Anchor '{anchor}': hash mismatch at line {lineNumber}. The file has changed — re-read before editing.";
$"(expected '{expectedHash}', got '{actualHash}'). " +
$"The file has changed — re-read before editing.";
return false; return false;
} }
@@ -98,10 +95,7 @@ internal static class HashlineValidator
out int endIndex, out int endIndex,
out string error) out string error)
{ {
startIndex = -1;
endIndex = -1; endIndex = -1;
error = string.Empty;
if (!TryResolve(startAnchor, lines, out startIndex, out error)) if (!TryResolve(startAnchor, lines, out startIndex, out error))
return false; return false;
@@ -110,8 +104,7 @@ internal static class HashlineValidator
if (startIndex > endIndex) if (startIndex > endIndex)
{ {
error = $"Range error: start anchor '{startAnchor}' (line {startIndex + 1}) " + error = $"Range error: start anchor is after end anchor.";
$"is after end anchor '{endAnchor}' (line {endIndex + 1}).";
return false; return false;
} }

View File

@@ -216,6 +216,17 @@ internal sealed class ReplLoop
} }
AnsiConsole.WriteLine(); AnsiConsole.WriteLine();
} }
// Save session after each LLM turn completes
try
{
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) catch (OperationCanceledException)
{ {
@@ -248,5 +259,17 @@ internal sealed class ReplLoop
responseCts = null; responseCts = null;
} }
} }
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();
}
}
} }
} }

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"); Log($" Replacing {endAnchor.Split(':')[0]}-{startAnchor.Split(':')[0]} lines with {newLines.Length} new lines");
if (!File.Exists(path)) 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 try
{ {
@@ -74,7 +74,7 @@ internal static partial class EditTools
if (!HashlineValidator.TryResolveRange(startAnchor, endAnchor, lines, if (!HashlineValidator.TryResolveRange(startAnchor, endAnchor, lines,
out int startIdx, out int endIdx, out string error)) 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); var result = new List<string>(lines.Length - (endIdx - startIdx + 1) + newLines.Length);
result.AddRange(lines[..startIdx]); result.AddRange(lines[..startIdx]);
@@ -86,7 +86,7 @@ internal static partial class EditTools
} }
catch (Exception ex) 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) 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") [Description("Type of deletion: 'file' or 'dir'. Defaults to 'file'.")] string mode = "file")
{ {
path = FileTools.ResolvePath(path); path = FileTools.ResolvePath(path);
string targetType = mode.ToLower() == "dir" ? "directory" : "file"; string targetType = mode.Equals("dir", StringComparison.CurrentCultureIgnoreCase) ? "directory" : "file";
Log($"Deleting {targetType}: {path}"); Log($"Deleting {targetType}: {path}");
if (mode.ToLower() == "dir") if (mode.Equals("dir", StringComparison.CurrentCultureIgnoreCase))
{ {
if (!Directory.Exists(path)) if (!Directory.Exists(path))
return $"ERROR: Directory not found: {path}"; return $"ERROR: Directory not found: {path}";
@@ -147,7 +147,7 @@ internal static partial class EditTools
} }
catch (Exception ex) 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 else
@@ -162,7 +162,7 @@ internal static partial class EditTools
} }
catch (Exception ex) 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) 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) 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.";
} }
} }