108 lines
4.4 KiB
C#
108 lines
4.4 KiB
C#
using Microsoft.Extensions.AI;
|
|
using System.Text.Json;
|
|
|
|
namespace AnchorCli;
|
|
|
|
internal sealed class ChatSession
|
|
{
|
|
private readonly IChatClient _agent;
|
|
public ContextCompactor Compactor { get; }
|
|
public List<ChatMessage> History { get; }
|
|
|
|
public ChatSession(IChatClient innerClient)
|
|
{
|
|
Compactor = new ContextCompactor(innerClient);
|
|
|
|
var tools = ToolRegistry.GetTools();
|
|
_agent = new ChatClientBuilder(innerClient)
|
|
.UseFunctionInvocation()
|
|
.Build();
|
|
|
|
History = new List<ChatMessage>
|
|
{
|
|
new(ChatRole.System, $$"""
|
|
You are anchor, a coding assistant that edits files using the Hashline technique.
|
|
|
|
## Reading files
|
|
When you read a file, lines are returned in the format: lineNumber:hash|content
|
|
The "lineNumber:hash|" prefix is METADATA for anchoring — it is NOT part of the file.
|
|
|
|
## Editing files
|
|
To edit, reference anchors as "lineNumber:hash" in startAnchor/endAnchor parameters.
|
|
The newLines/initialLines parameter must contain RAW SOURCE CODE ONLY.
|
|
❌ WRONG: ["5:a3| public void Foo()"]
|
|
✅ RIGHT: [" public void Foo()"]
|
|
Never include the "lineNumber:hash|" prefix in content you write — it will corrupt the file.
|
|
|
|
## Workflow
|
|
1. ALWAYS call GrepFile before ReadFile on any source file.
|
|
Use patterns like "public|func|function|class|interface|enum|def|fn " to get a structural
|
|
outline of the file and identify the exact line numbers of the section you need.
|
|
Only then call ReadFile with a targeted startLine/endLine range.
|
|
❌ WRONG: ReadFile("Foo.cs") — reads blindly without knowing the structure.
|
|
✅ RIGHT: GrepFile("Foo.cs", "public|class|interface") → ReadFile("Foo.cs", 42, 90)
|
|
2. After reading, edit the file before verifying the returned fingerprint.
|
|
3. Edit from bottom to top so line numbers don't shift.
|
|
4. If an anchor fails validation, re-read the relevant range to get fresh anchors.
|
|
|
|
Keep responses concise. You have access to the current working directory.
|
|
You are running on: {{System.Runtime.InteropServices.RuntimeInformation.OSDescription}}
|
|
""")
|
|
};
|
|
}
|
|
public void Reset()
|
|
{
|
|
// Keep only the system message
|
|
var systemMessage = History[0];
|
|
History.Clear();
|
|
History.Add(systemMessage);
|
|
}
|
|
|
|
|
|
public async IAsyncEnumerable<ChatResponseUpdate> GetStreamingResponseAsync(
|
|
[System.Runtime.CompilerServices.EnumeratorCancellation] CancellationToken cancellationToken = default)
|
|
{
|
|
var options = new ChatOptions { Tools = ToolRegistry.GetTools() };
|
|
var stream = _agent.GetStreamingResponseAsync(History, options, cancellationToken);
|
|
|
|
await foreach (var update in stream.WithCancellation(cancellationToken))
|
|
{
|
|
yield return update;
|
|
}
|
|
}
|
|
|
|
public async Task SaveAsync(string filePath, CancellationToken cancellationToken = default)
|
|
{
|
|
// Skip the system message when saving (it will be recreated on load)
|
|
var messagesToSave = History.Skip(1).ToList();
|
|
|
|
var options = new JsonSerializerOptions
|
|
{
|
|
PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
|
|
WriteIndented = true
|
|
};
|
|
|
|
var json = JsonSerializer.Serialize(messagesToSave, AppJsonContext.Default.ListChatMessage);
|
|
await File.WriteAllTextAsync(filePath, json, cancellationToken);
|
|
}
|
|
|
|
public async Task LoadAsync(string filePath, CancellationToken cancellationToken = default)
|
|
{
|
|
var json = await File.ReadAllTextAsync(filePath, cancellationToken);
|
|
|
|
var options = new JsonSerializerOptions
|
|
{
|
|
PropertyNamingPolicy = JsonNamingPolicy.CamelCase
|
|
};
|
|
|
|
var messages = JsonSerializer.Deserialize<List<ChatMessage>>(json, AppJsonContext.Default.ListChatMessage)
|
|
?? new List<ChatMessage>();
|
|
|
|
// Keep the system message and append loaded messages
|
|
var systemMessage = History[0];
|
|
History.Clear();
|
|
History.Add(systemMessage);
|
|
History.AddRange(messages);
|
|
}
|
|
}
|