refactor: apply Single Responsibility Principle to Program.cs and ReplLoop.cs
extracted responsibilities from Program.cs (208→46 lines) and ReplLoop.cs (274→174 lines) into focused service classes: HeaderRenderer, SessionManager, ApplicationStartup, ResponseStreamer, SpinnerService, UsageDisplayer, and ContextCompactionService. Each class now has a single, well-defined responsibility, improving testability and maintainability.
This commit is contained in:
176
ApplicationStartup.cs
Normal file
176
ApplicationStartup.cs
Normal file
@@ -0,0 +1,176 @@
|
||||
using System.ClientModel;
|
||||
using AnchorCli.Commands;
|
||||
using AnchorCli.OpenRouter;
|
||||
using AnchorCli.Providers;
|
||||
using AnchorCli.Tools;
|
||||
using Microsoft.Extensions.AI;
|
||||
using OpenAI;
|
||||
using Spectre.Console;
|
||||
|
||||
namespace AnchorCli;
|
||||
|
||||
/// <summary>
|
||||
/// Encapsulates application startup logic, including configuration loading,
|
||||
/// API client creation, and component initialization.
|
||||
/// </summary>
|
||||
internal sealed class ApplicationStartup
|
||||
{
|
||||
private readonly string[] _args;
|
||||
private AnchorConfig? _config;
|
||||
private ITokenExtractor? _tokenExtractor;
|
||||
private ModelInfo? _modelInfo;
|
||||
private IChatClient? _chatClient;
|
||||
private TokenTracker? _tokenTracker;
|
||||
|
||||
public ApplicationStartup(string[] args)
|
||||
{
|
||||
_args = args;
|
||||
}
|
||||
|
||||
public AnchorConfig Config => _config ?? throw new InvalidOperationException("Run InitializeAsync first");
|
||||
public string ApiKey => _config?.ApiKey ?? throw new InvalidOperationException("API key not loaded");
|
||||
public string Model => _config?.Model ?? throw new InvalidOperationException("Model not loaded");
|
||||
public string Endpoint => _config?.Endpoint ?? "https://openrouter.ai/api/v1";
|
||||
public string ProviderName => _tokenExtractor?.ProviderName ?? "Unknown";
|
||||
public ITokenExtractor TokenExtractor => _tokenExtractor ?? throw new InvalidOperationException("Token extractor not initialized");
|
||||
public ModelInfo? ModelInfo => _modelInfo;
|
||||
public IChatClient ChatClient => _chatClient ?? throw new InvalidOperationException("Chat client not initialized");
|
||||
public TokenTracker TokenTracker => _tokenTracker ?? throw new InvalidOperationException("Token tracker not initialized");
|
||||
|
||||
/// <summary>
|
||||
/// Runs the setup TUI if the "setup" subcommand was passed. Returns true if setup was run.
|
||||
/// </summary>
|
||||
public bool HandleSetupSubcommand()
|
||||
{
|
||||
if (_args.Length > 0 && _args[0].Equals("setup", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
SetupTui.Run();
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initializes the application by loading configuration and creating the chat client.
|
||||
/// </summary>
|
||||
public async Task InitializeAsync()
|
||||
{
|
||||
// Load configuration
|
||||
_config = AnchorConfig.Load();
|
||||
|
||||
if (string.IsNullOrWhiteSpace(_config.ApiKey))
|
||||
{
|
||||
AnsiConsole.MarkupLine("[red]No API key configured. Run [bold]anchor setup[/] first.[/]");
|
||||
throw new InvalidOperationException("API key not configured");
|
||||
}
|
||||
|
||||
// Create token extractor
|
||||
_tokenExtractor = ProviderFactory.CreateTokenExtractorForEndpoint(Endpoint);
|
||||
|
||||
// Fetch model pricing (only for OpenRouter)
|
||||
if (ProviderFactory.IsOpenRouter(Endpoint))
|
||||
{
|
||||
await AnsiConsole.Status()
|
||||
.Spinner(Spinner.Known.BouncingBar)
|
||||
.SpinnerStyle(Style.Parse("cornflowerblue"))
|
||||
.StartAsync("Fetching model pricing...", async ctx =>
|
||||
{
|
||||
try
|
||||
{
|
||||
var pricingProvider = new OpenRouterProvider();
|
||||
_modelInfo = await pricingProvider.GetModelInfoAsync(Model);
|
||||
}
|
||||
catch
|
||||
{
|
||||
// Pricing is best-effort
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Create chat client
|
||||
var httpClient = new HttpClient();
|
||||
OpenRouterHeaders.ApplyTo(httpClient);
|
||||
|
||||
var openAiClient = new OpenAIClient(
|
||||
new ApiKeyCredential(ApiKey),
|
||||
new OpenAIClientOptions
|
||||
{
|
||||
Endpoint = new Uri(Endpoint),
|
||||
Transport = new System.ClientModel.Primitives.HttpClientPipelineTransport(httpClient)
|
||||
});
|
||||
|
||||
_chatClient = openAiClient.GetChatClient(Model).AsIChatClient();
|
||||
|
||||
// Initialize token tracker
|
||||
_tokenTracker = new TokenTracker(new ChatSession(_chatClient))
|
||||
{
|
||||
Provider = _tokenExtractor.ProviderName
|
||||
};
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
if (_modelInfo != null)
|
||||
{
|
||||
_tokenTracker.ContextLength = _modelInfo.ContextLength;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates a new ChatSession with the initialized chat client.
|
||||
/// </summary>
|
||||
public ChatSession CreateSession()
|
||||
{
|
||||
return new ChatSession(ChatClient);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Configures tool logging to use Spectre.Console.
|
||||
/// </summary>
|
||||
public void ConfigureToolLogging()
|
||||
{
|
||||
object consoleLock = new();
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates and populates a CommandRegistry with all available commands.
|
||||
/// </summary>
|
||||
public CommandRegistry CreateCommandRegistry(ChatSession session)
|
||||
{
|
||||
var registry = new CommandRegistry();
|
||||
registry.Register(new ExitCommand());
|
||||
registry.Register(new HelpCommand(registry));
|
||||
registry.Register(new ClearCommand());
|
||||
registry.Register(new StatusCommand(Model, Endpoint));
|
||||
registry.Register(new CompactCommand(session.Compactor, session.History));
|
||||
registry.Register(new SetupCommand());
|
||||
registry.Register(new ResetCommand(session, TokenTracker));
|
||||
return registry;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates a HeaderRenderer with the current configuration.
|
||||
/// </summary>
|
||||
public HeaderRenderer CreateHeaderRenderer()
|
||||
{
|
||||
return new HeaderRenderer(Model, Endpoint, ProviderName, _modelInfo, _tokenTracker);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user