feat: Introduce a /reset command to clear the chat session and token tracking, and update documentation.
This commit is contained in:
275
Program.cs
275
Program.cs
@@ -1,136 +1,139 @@
|
||||
using System.ClientModel;
|
||||
using Microsoft.Extensions.AI;
|
||||
using OpenAI;
|
||||
using AnchorCli;
|
||||
using AnchorCli.Tools;
|
||||
using AnchorCli.Commands;
|
||||
using AnchorCli.OpenRouter;
|
||||
using Spectre.Console;
|
||||
|
||||
// ── Setup subcommand ─────────────────────────────────────────────────────
|
||||
if (args.Length > 0 && args[0].Equals("setup", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
SetupTui.Run();
|
||||
return;
|
||||
}
|
||||
|
||||
// ── Config ──────────────────────────────────────────────────────────────
|
||||
const string endpoint = "https://openrouter.ai/api/v1";
|
||||
var cfg = AnchorConfig.Load();
|
||||
string apiKey = cfg.ApiKey;
|
||||
string model = cfg.Model;
|
||||
|
||||
if (string.IsNullOrWhiteSpace(apiKey))
|
||||
{
|
||||
AnsiConsole.MarkupLine("[red]No API key configured. Run [bold]anchor setup[/] first.[/]");
|
||||
return;
|
||||
}
|
||||
|
||||
// ── Fetch model pricing from OpenRouter ─────────────────────────────────
|
||||
var pricingProvider = new PricingProvider();
|
||||
var tokenTracker = new TokenTracker();
|
||||
|
||||
ModelInfo? modelInfo = null;
|
||||
await AnsiConsole.Status()
|
||||
.Spinner(Spinner.Known.BouncingBar)
|
||||
.SpinnerStyle(Style.Parse("cornflowerblue"))
|
||||
.StartAsync("Fetching model pricing...", async ctx =>
|
||||
{
|
||||
try
|
||||
{
|
||||
modelInfo = await pricingProvider.GetModelInfoAsync(model);
|
||||
if (modelInfo?.Pricing != null)
|
||||
{
|
||||
tokenTracker.InputPrice = PricingProvider.ParsePrice(modelInfo.Pricing.Prompt);
|
||||
tokenTracker.OutputPrice = PricingProvider.ParsePrice(modelInfo.Pricing.Completion);
|
||||
tokenTracker.RequestPrice = PricingProvider.ParsePrice(modelInfo.Pricing.Request);
|
||||
}
|
||||
}
|
||||
catch { /* pricing is best-effort */ }
|
||||
});
|
||||
|
||||
// ── Pretty header ───────────────────────────────────────────────────────
|
||||
AnsiConsole.Write(
|
||||
new FigletText("anchor")
|
||||
.Color(Color.CornflowerBlue));
|
||||
|
||||
AnsiConsole.Write(
|
||||
new Rule("[dim]AI-powered coding assistant[/]")
|
||||
.RuleStyle(Style.Parse("cornflowerblue dim"))
|
||||
.LeftJustified());
|
||||
|
||||
AnsiConsole.WriteLine();
|
||||
|
||||
var infoTable = new Table()
|
||||
.Border(TableBorder.Rounded)
|
||||
.BorderColor(Color.Grey)
|
||||
.AddColumn(new TableColumn("[dim]Setting[/]").NoWrap())
|
||||
.AddColumn(new TableColumn("[dim]Value[/]"));
|
||||
|
||||
infoTable.AddRow("[grey]Model[/]", $"[cyan]{Markup.Escape(modelInfo?.Name ?? model)}[/]");
|
||||
infoTable.AddRow("[grey]Endpoint[/]", $"[blue]OpenRouter[/]");
|
||||
infoTable.AddRow("[grey]CWD[/]", $"[green]{Markup.Escape(Environment.CurrentDirectory)}[/]");
|
||||
|
||||
if (modelInfo?.Pricing != null)
|
||||
{
|
||||
var inM = tokenTracker.InputPrice * 1_000_000m;
|
||||
var outM = tokenTracker.OutputPrice * 1_000_000m;
|
||||
infoTable.AddRow("[grey]Pricing[/]",
|
||||
$"[yellow]${inM:F2}[/][dim]/M in[/] [yellow]${outM:F2}[/][dim]/M out[/]");
|
||||
}
|
||||
if (modelInfo != null)
|
||||
{
|
||||
infoTable.AddRow("[grey]Context[/]",
|
||||
$"[dim]{modelInfo.ContextLength:N0} tokens[/]");
|
||||
}
|
||||
|
||||
AnsiConsole.Write(infoTable);
|
||||
AnsiConsole.WriteLine();
|
||||
|
||||
// ── Build the chat client with tool-calling support ─────────────────────
|
||||
var openAiClient = new OpenAIClient(new ApiKeyCredential(apiKey), new OpenAIClientOptions
|
||||
{
|
||||
Endpoint = new Uri(endpoint)
|
||||
});
|
||||
|
||||
IChatClient innerClient = openAiClient.GetChatClient(model).AsIChatClient();
|
||||
|
||||
// ── Tool call logging via Spectre ───────────────────────────────────────
|
||||
object consoleLock = new object();
|
||||
|
||||
void ToolLog(string message)
|
||||
{
|
||||
lock (consoleLock)
|
||||
{
|
||||
Console.Write("\r" + new string(' ', 40) + "\r");
|
||||
AnsiConsole.MarkupLine($"[dim grey] ● {Markup.Escape(message)}[/]");
|
||||
}
|
||||
}
|
||||
|
||||
CommandTool.Log =
|
||||
DirTools.Log =
|
||||
FileTools.Log =
|
||||
EditTools.Log = ToolLog;
|
||||
|
||||
// ── Instantiate Core Components ──────────────────────────────────────────
|
||||
|
||||
var session = new ChatSession(innerClient);
|
||||
if (modelInfo != null)
|
||||
{
|
||||
tokenTracker.ContextLength = modelInfo.ContextLength;
|
||||
}
|
||||
|
||||
var commandRegistry = new CommandRegistry();
|
||||
commandRegistry.Register(new ExitCommand());
|
||||
commandRegistry.Register(new HelpCommand(commandRegistry));
|
||||
commandRegistry.Register(new ClearCommand());
|
||||
commandRegistry.Register(new StatusCommand(model, endpoint));
|
||||
commandRegistry.Register(new CompactCommand(session.Compactor, session.History));
|
||||
commandRegistry.Register(new SetupCommand());
|
||||
|
||||
var commandDispatcher = new CommandDispatcher(commandRegistry);
|
||||
|
||||
// ── Run Repl ────────────────────────────────────────────────────────────
|
||||
|
||||
var repl = new ReplLoop(session, tokenTracker, commandDispatcher);
|
||||
await repl.RunAsync();
|
||||
using System.ClientModel;
|
||||
using Microsoft.Extensions.AI;
|
||||
using OpenAI;
|
||||
using AnchorCli;
|
||||
using AnchorCli.Tools;
|
||||
using AnchorCli.Commands;
|
||||
using AnchorCli.OpenRouter;
|
||||
using Spectre.Console;
|
||||
|
||||
// ── Setup subcommand ─────────────────────────────────────────────────────
|
||||
if (args.Length > 0 && args[0].Equals("setup", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
SetupTui.Run();
|
||||
return;
|
||||
}
|
||||
|
||||
// ── Config ──────────────────────────────────────────────────────────────
|
||||
const string endpoint = "https://openrouter.ai/api/v1";
|
||||
var cfg = AnchorConfig.Load();
|
||||
string apiKey = cfg.ApiKey;
|
||||
string model = cfg.Model;
|
||||
|
||||
if (string.IsNullOrWhiteSpace(apiKey))
|
||||
{
|
||||
AnsiConsole.MarkupLine("[red]No API key configured. Run [bold]anchor setup[/] first.[/]");
|
||||
return;
|
||||
}
|
||||
|
||||
// ── Fetch model pricing from OpenRouter ─────────────────────────────────
|
||||
var pricingProvider = new PricingProvider();
|
||||
var tokenTracker = new TokenTracker();
|
||||
|
||||
ModelInfo? modelInfo = null;
|
||||
await AnsiConsole.Status()
|
||||
.Spinner(Spinner.Known.BouncingBar)
|
||||
.SpinnerStyle(Style.Parse("cornflowerblue"))
|
||||
.StartAsync("Fetching model pricing...", async ctx =>
|
||||
{
|
||||
try
|
||||
{
|
||||
modelInfo = await pricingProvider.GetModelInfoAsync(model);
|
||||
if (modelInfo?.Pricing != null)
|
||||
{
|
||||
tokenTracker.InputPrice = PricingProvider.ParsePrice(modelInfo.Pricing.Prompt);
|
||||
tokenTracker.OutputPrice = PricingProvider.ParsePrice(modelInfo.Pricing.Completion);
|
||||
tokenTracker.RequestPrice = PricingProvider.ParsePrice(modelInfo.Pricing.Request);
|
||||
}
|
||||
}
|
||||
catch { /* pricing is best-effort */ }
|
||||
});
|
||||
|
||||
// ── Pretty header ───────────────────────────────────────────────────────
|
||||
AnsiConsole.Write(
|
||||
new FigletText("anchor")
|
||||
.Color(Color.CornflowerBlue));
|
||||
|
||||
AnsiConsole.Write(
|
||||
new Rule("[dim]AI-powered coding assistant[/]")
|
||||
.RuleStyle(Style.Parse("cornflowerblue dim"))
|
||||
.LeftJustified());
|
||||
|
||||
AnsiConsole.WriteLine();
|
||||
|
||||
var infoTable = new Table()
|
||||
.Border(TableBorder.Rounded)
|
||||
.BorderColor(Color.Grey)
|
||||
.AddColumn(new TableColumn("[dim]Setting[/]").NoWrap())
|
||||
.AddColumn(new TableColumn("[dim]Value[/]"));
|
||||
|
||||
infoTable.AddRow("[grey]Model[/]", $"[cyan]{Markup.Escape(modelInfo?.Name ?? model)}[/]");
|
||||
infoTable.AddRow("[grey]Endpoint[/]", $"[blue]OpenRouter[/]");
|
||||
infoTable.AddRow("[grey]CWD[/]", $"[green]{Markup.Escape(Environment.CurrentDirectory)}[/]");
|
||||
|
||||
if (modelInfo?.Pricing != null)
|
||||
{
|
||||
var inM = tokenTracker.InputPrice * 1_000_000m;
|
||||
var outM = tokenTracker.OutputPrice * 1_000_000m;
|
||||
infoTable.AddRow("[grey]Pricing[/]",
|
||||
$"[yellow]${inM:F2}[/][dim]/M in[/] [yellow]${outM:F2}[/][dim]/M out[/]");
|
||||
}
|
||||
if (modelInfo != null)
|
||||
{
|
||||
infoTable.AddRow("[grey]Context[/]",
|
||||
$"[dim]{modelInfo.ContextLength:N0} tokens[/]");
|
||||
}
|
||||
|
||||
AnsiConsole.Write(infoTable);
|
||||
AnsiConsole.WriteLine();
|
||||
|
||||
// ── Build the chat client with tool-calling support ─────────────────────
|
||||
var openAiClient = new OpenAIClient(new ApiKeyCredential(apiKey), new OpenAIClientOptions
|
||||
{
|
||||
Endpoint = new Uri(endpoint)
|
||||
});
|
||||
|
||||
IChatClient innerClient = openAiClient.GetChatClient(model).AsIChatClient();
|
||||
|
||||
// ── Tool call logging via Spectre ───────────────────────────────────────
|
||||
object consoleLock = new object();
|
||||
|
||||
void ToolLog(string message)
|
||||
{
|
||||
lock (consoleLock)
|
||||
{
|
||||
Console.Write("\r" + new string(' ', 40) + "\r");
|
||||
AnsiConsole.MarkupLine($"[dim grey] ● {Markup.Escape(message)}[/]");
|
||||
}
|
||||
}
|
||||
|
||||
CommandTool.Log =
|
||||
DirTools.Log =
|
||||
FileTools.Log =
|
||||
EditTools.Log = ToolLog;
|
||||
|
||||
// ── Instantiate Core Components ──────────────────────────────────────────
|
||||
|
||||
var session = new ChatSession(innerClient);
|
||||
if (modelInfo != null)
|
||||
{
|
||||
tokenTracker.ContextLength = modelInfo.ContextLength;
|
||||
}
|
||||
|
||||
var commandRegistry = new CommandRegistry();
|
||||
commandRegistry.Register(new ExitCommand());
|
||||
commandRegistry.Register(new HelpCommand(commandRegistry));
|
||||
commandRegistry.Register(new ClearCommand());
|
||||
commandRegistry.Register(new StatusCommand(model, endpoint));
|
||||
commandRegistry.Register(new CompactCommand(session.Compactor, session.History));
|
||||
commandRegistry.Register(new SetupCommand());
|
||||
commandRegistry.Register(new ResetCommand(session, tokenTracker));
|
||||
|
||||
|
||||
var commandDispatcher = new CommandDispatcher(commandRegistry);
|
||||
|
||||
// ── Run Repl ────────────────────────────────────────────────────────────
|
||||
|
||||
var repl = new ReplLoop(session, tokenTracker, commandDispatcher);
|
||||
await repl.RunAsync();
|
||||
|
||||
|
||||
Reference in New Issue
Block a user