From 82ef63c731448f23232b273f8a7cca76d19aafd4 Mon Sep 17 00:00:00 2001 From: Tomi Eckert Date: Fri, 6 Mar 2026 02:35:46 +0100 Subject: [PATCH] feat: Introduce robust hashline anchor validation and new editing tools. --- Hashline/HashlineValidator.cs | 13 ++------- ReplLoop.cs | 55 +++++++++++++++++++++++++---------- Tools/EditTools.cs | 26 ++++++++--------- 3 files changed, 55 insertions(+), 39 deletions(-) diff --git a/Hashline/HashlineValidator.cs b/Hashline/HashlineValidator.cs index 692a5e8..57dea5a 100644 --- a/Hashline/HashlineValidator.cs +++ b/Hashline/HashlineValidator.cs @@ -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; } diff --git a/ReplLoop.cs b/ReplLoop.cs index f4d0b94..c93f1e4 100644 --- a/ReplLoop.cs +++ b/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; - } } } } diff --git a/Tools/EditTools.cs b/Tools/EditTools.cs index c164be5..d434162 100644 --- a/Tools/EditTools.cs +++ b/Tools/EditTools.cs @@ -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(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."; } }