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,20 +216,48 @@ internal sealed class ReplLoop
} }
AnsiConsole.WriteLine(); 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) catch (Exception ex)
{ {
@@ -242,11 +270,6 @@ internal sealed class ReplLoop
.Padding(1, 0)); .Padding(1, 0));
AnsiConsole.WriteLine(); 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"); 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,22 +120,22 @@ 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.";
} }
} }
[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( public static string Delete(
[Description("Path to the file or directory to delete.")] string path, [Description("Path to the file or directory to delete.")] string path,
[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,12 +162,12 @@ 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.";
} }
} }
} }
[Description("Move or copy a file to a new location.")] [Description("Move or copy a file to a new location.")]
public static string MoveFile( public static string MoveFile(
[Description("Current path to the file.")] string sourcePath, [Description("Current path to the file.")] string sourcePath,
[Description("New path for the file.")] string destinationPath, [Description("New path for the file.")] string destinationPath,
@@ -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.";
} }
} }
@@ -241,7 +241,7 @@ internal static partial class EditTools
File.WriteAllText(path, ""); File.WriteAllText(path, "");
Log($" (created new file)"); Log($" (created new file)");
} }
using (var writer = new System.IO.StreamWriter(path, true)) using (var writer = new System.IO.StreamWriter(path, true))
{ {
foreach (var line in content) foreach (var line in content)
@@ -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.";
} }
} }