From a6c7df0a711be5237486f0b98909a07d7f93c57a Mon Sep 17 00:00:00 2001 From: TomiEckert Date: Sun, 1 Mar 2026 21:05:35 +0100 Subject: [PATCH] refactor: modernize code, improve performance, and clean up various components. --- Api/Models/OpenAiModels.cs | 6 +- Api/OpenAiCompatibleClient.cs | 30 +- Audio/AudioRecorder.cs | 26 +- Audio/FfmpegAudioRecorder.cs | 76 +++--- Commands/ConfigUpdaterCommand.cs | 12 +- Commands/DiscardCommand.cs | 2 - Commands/HistoryCommand.cs | 18 +- Commands/LatencyTestCommand.cs | 20 +- Commands/OnboardCommand.cs | 7 +- Commands/ShowCommand.cs | 3 +- Commands/SkillCommand.cs | 7 +- Commands/StartCommand.cs | 2 - Commands/StatsCommand.cs | 11 +- Commands/StatusCommand.cs | 9 +- Commands/StopCommand.cs | 4 +- Commands/ToggleCommand.cs | 4 +- Configuration/ConfigManager.cs | 20 +- Configuration/ToakConfig.cs | 6 +- Core/Constants.cs | 3 - Core/DaemonService.cs | 30 +- Core/HistoryEntry.cs | 2 - Core/HistoryManager.cs | 36 +-- Core/Interfaces/ITranscriptionOrchestrator.cs | 1 - Core/Interfaces/Interfaces.cs | 9 +- Core/PromptBuilder.cs | 1 - Core/Skills/DynamicSkill.cs | 10 +- Core/Skills/SkillDefinition.cs | 2 +- Core/Skills/SkillRegistry.cs | 62 +++-- Core/StateTracker.cs | 18 +- Core/TranscriptionOrchestrator.cs | 70 ++--- IO/ClipboardManager.cs | 26 +- IO/Notifications.cs | 5 +- IO/TextInjector.cs | 33 +-- Program.cs | 3 +- Serialization/AppJsonSerializerContext.cs | 6 +- Toak.csproj | 30 +- qodana_problems.md | 257 ------------------ 37 files changed, 240 insertions(+), 627 deletions(-) delete mode 100644 qodana_problems.md diff --git a/Api/Models/OpenAiModels.cs b/Api/Models/OpenAiModels.cs index 5eaef58..df046d1 100644 --- a/Api/Models/OpenAiModels.cs +++ b/Api/Models/OpenAiModels.cs @@ -15,7 +15,7 @@ public class OpenAiRequest [JsonPropertyName("model")] public string Model { get; set; } = "llama-3.1-8b-instant"; [JsonPropertyName("messages")] - public OpenAiRequestMessage[] Messages { get; set; } = Array.Empty(); + public OpenAiRequestMessage[] Messages { get; set; } = []; [JsonPropertyName("temperature")] public double Temperature { get; set; } = 0.0; [JsonPropertyName("stream")] @@ -27,7 +27,7 @@ public class OpenAiRequest public class OpenAiResponse { [JsonPropertyName("choices")] - public OpenAiChoice[] Choices { get; set; } = Array.Empty(); + public OpenAiChoice[] Choices { get; set; } = []; } public class OpenAiChoice @@ -39,7 +39,7 @@ public class OpenAiChoice public class OpenAiStreamResponse { [JsonPropertyName("choices")] - public OpenAiStreamChoice[] Choices { get; set; } = Array.Empty(); + public OpenAiStreamChoice[] Choices { get; set; } = []; } public class OpenAiStreamChoice diff --git a/Api/OpenAiCompatibleClient.cs b/Api/OpenAiCompatibleClient.cs index e373381..720ae02 100644 --- a/Api/OpenAiCompatibleClient.cs +++ b/Api/OpenAiCompatibleClient.cs @@ -1,7 +1,5 @@ using System.Net.Http.Headers; using System.Text.Json; -using System.Text.Json.Serialization; - using Toak.Api.Models; using Toak.Serialization; using Toak.Core; @@ -22,17 +20,17 @@ public class OpenAiCompatibleClient : ISpeechClient, ILlmClient _reasoningEffort = reasoningEffort == "none" ? null : reasoningEffort; } - public async Task TranscribeAsync(string filePath, string language = "", string model = Toak.Core.Constants.Defaults.WhisperModel) + public async Task TranscribeAsync(string filePath, string language = "", string model = Constants.Defaults.WhisperModel) { // ... (TranscribeAsync content remains same except maybe some internal comments or contexts) using var content = new MultipartFormDataContent(); - using var fileStream = File.OpenRead(filePath); + await using var fileStream = File.OpenRead(filePath); using var streamContent = new StreamContent(fileStream); streamContent.Headers.ContentType = new MediaTypeHeaderValue("audio/wav"); // or mpeg content.Add(streamContent, "file", Path.GetFileName(filePath)); - string modelToUse = string.IsNullOrWhiteSpace(model) ? Toak.Core.Constants.Defaults.WhisperModel : model; + var modelToUse = string.IsNullOrWhiteSpace(model) ? Constants.Defaults.WhisperModel : model; content.Add(new StringContent(modelToUse), "model"); @@ -57,18 +55,18 @@ public class OpenAiCompatibleClient : ISpeechClient, ILlmClient return result?.Text ?? string.Empty; } - public async Task RefineTextAsync(string rawTranscript, string systemPrompt, string model = Toak.Core.Constants.Defaults.LlmModel) + public async Task RefineTextAsync(string rawTranscript, string systemPrompt, string model = Constants.Defaults.LlmModel) { var requestBody = new OpenAiRequest { - Model = string.IsNullOrWhiteSpace(model) ? Toak.Core.Constants.Defaults.LlmModel : model, + Model = string.IsNullOrWhiteSpace(model) ? Constants.Defaults.LlmModel : model, Temperature = 0.0, ReasoningEffort = _reasoningEffort, - Messages = new[] - { + Messages = + [ new OpenAiRequestMessage { Role = "system", Content = systemPrompt }, new OpenAiRequestMessage { Role = "user", Content = $"{rawTranscript}" } - } + ] }; var jsonContent = new StringContent(JsonSerializer.Serialize(requestBody, AppJsonSerializerContext.Default.OpenAiRequest), System.Text.Encoding.UTF8, "application/json"); @@ -89,19 +87,19 @@ public class OpenAiCompatibleClient : ISpeechClient, ILlmClient return result?.Choices?.FirstOrDefault()?.Message?.Content ?? string.Empty; } - public async IAsyncEnumerable RefineTextStreamAsync(string rawTranscript, string systemPrompt, string model = Toak.Core.Constants.Defaults.LlmModel) + public async IAsyncEnumerable RefineTextStreamAsync(string rawTranscript, string systemPrompt, string model = Constants.Defaults.LlmModel) { var requestBody = new OpenAiRequest { - Model = string.IsNullOrWhiteSpace(model) ? Toak.Core.Constants.Defaults.LlmModel : model, + Model = string.IsNullOrWhiteSpace(model) ? Constants.Defaults.LlmModel : model, Temperature = 0.0, Stream = true, ReasoningEffort = _reasoningEffort, - Messages = new[] - { + Messages = + [ new OpenAiRequestMessage { Role = "system", Content = systemPrompt }, new OpenAiRequestMessage { Role = "user", Content = $"{rawTranscript}" } - } + ] }; var jsonContent = new StringContent(JsonSerializer.Serialize(requestBody, AppJsonSerializerContext.Default.OpenAiRequest), System.Text.Encoding.UTF8, "application/json"); @@ -120,7 +118,7 @@ public class OpenAiCompatibleClient : ISpeechClient, ILlmClient throw new Exception($"OpenAi API Error: {response.StatusCode} - {error}"); } - using var stream = await response.Content.ReadAsStreamAsync(); + await using var stream = await response.Content.ReadAsStreamAsync(); using var reader = new StreamReader(stream); string? line; diff --git a/Audio/AudioRecorder.cs b/Audio/AudioRecorder.cs index df7c385..16f4b0e 100644 --- a/Audio/AudioRecorder.cs +++ b/Audio/AudioRecorder.cs @@ -1,32 +1,24 @@ using System.Diagnostics; using Toak.Core; -using Toak.IO; - using Toak.Core.Interfaces; namespace Toak.Audio; -public class AudioRecorder : IAudioRecorder +public class AudioRecorder(IRecordingStateTracker stateTracker, INotifications notifications) : IAudioRecorder { - private readonly string WavPath = Constants.Paths.RecordingWavFile; - private readonly IRecordingStateTracker _stateTracker; - private readonly INotifications _notifications; + private readonly string _wavPath = Constants.Paths.RecordingWavFile; + private readonly IRecordingStateTracker _stateTracker = stateTracker; + private readonly INotifications _notifications = notifications; - public AudioRecorder(IRecordingStateTracker stateTracker, INotifications notifications) - { - _stateTracker = stateTracker; - _notifications = notifications; - } - - public string GetWavPath() => WavPath; + public string GetWavPath() => _wavPath; public void StartRecording() { - if (File.Exists(WavPath)) + if (File.Exists(_wavPath)) { - Logger.LogDebug($"Deleting old audio file: {WavPath}"); - File.Delete(WavPath); + Logger.LogDebug($"Deleting old audio file: {_wavPath}"); + File.Delete(_wavPath); } Logger.LogDebug("Starting pw-record to record audio..."); @@ -34,7 +26,7 @@ public class AudioRecorder : IAudioRecorder var pInfo = new ProcessStartInfo { FileName = Constants.Commands.AudioRecord, - Arguments = $"--rate=16000 --channels=1 --format=s16 \"{WavPath}\"", + Arguments = $"--rate=16000 --channels=1 --format=s16 \"{_wavPath}\"", UseShellExecute = false, CreateNoWindow = true, RedirectStandardOutput = true, diff --git a/Audio/FfmpegAudioRecorder.cs b/Audio/FfmpegAudioRecorder.cs index d7fed73..a1505c4 100644 --- a/Audio/FfmpegAudioRecorder.cs +++ b/Audio/FfmpegAudioRecorder.cs @@ -1,33 +1,23 @@ -using System; using System.Diagnostics; -using System.IO; - using Toak.Core; -using Toak.IO; using Toak.Core.Interfaces; namespace Toak.Audio; -public class FfmpegAudioRecorder : IAudioRecorder +public class FfmpegAudioRecorder(IRecordingStateTracker stateTracker, INotifications notifications) : IAudioRecorder { - private readonly string WavPath = Constants.Paths.RecordingWavFile; - private readonly IRecordingStateTracker _stateTracker; - private readonly INotifications _notifications; + private readonly string _wavPath = Constants.Paths.RecordingWavFile; + private readonly IRecordingStateTracker _stateTracker = stateTracker; + private readonly INotifications _notifications = notifications; - public FfmpegAudioRecorder(IRecordingStateTracker stateTracker, INotifications notifications) - { - _stateTracker = stateTracker; - _notifications = notifications; - } - - public string GetWavPath() => WavPath; + public string GetWavPath() => _wavPath; public void StartRecording() { - if (File.Exists(WavPath)) + if (File.Exists(_wavPath)) { - Logger.LogDebug($"Deleting old audio file: {WavPath}"); - File.Delete(WavPath); + Logger.LogDebug($"Deleting old audio file: {_wavPath}"); + File.Delete(_wavPath); } Logger.LogDebug("Starting ffmpeg to record audio..."); @@ -35,7 +25,7 @@ public class FfmpegAudioRecorder : IAudioRecorder var pInfo = new ProcessStartInfo { FileName = Constants.Commands.AudioFfmpeg, - Arguments = $"-f pulse -i default -ac 1 -ar 16000 \"{WavPath}\"", + Arguments = $"-f pulse -i default -ac 1 -ar 16000 \"{_wavPath}\"", UseShellExecute = false, CreateNoWindow = true, RedirectStandardOutput = true, @@ -53,35 +43,31 @@ public class FfmpegAudioRecorder : IAudioRecorder public void StopRecording() { var pid = _stateTracker.GetRecordingPid(); - if (pid.HasValue) + if (!pid.HasValue) return; + Logger.LogDebug($"Found active ffmpeg process with PID {pid.Value}. Attempting to stop..."); + try { - Logger.LogDebug($"Found active ffmpeg process with PID {pid.Value}. Attempting to stop..."); - try + var process = Process.GetProcessById(pid.Value); + if (process.HasExited) return; + // Gracefully stop ffmpeg using SIGINT to ensure WAV headers are finalizing cleanly + Process.Start(new ProcessStartInfo { - var process = Process.GetProcessById(pid.Value); - if (!process.HasExited) - { - // Gracefully stop ffmpeg using SIGINT to ensure WAV headers are finalizing cleanly - Process.Start(new ProcessStartInfo - { - FileName = Constants.Commands.ProcessKill, - Arguments = $"-INT {pid.Value}", - CreateNoWindow = true, - UseShellExecute = false - })?.WaitForExit(); + FileName = Constants.Commands.ProcessKill, + Arguments = $"-INT {pid.Value}", + CreateNoWindow = true, + UseShellExecute = false + })?.WaitForExit(); - process.WaitForExit(2000); // give it a moment to flush - } - } - catch (Exception ex) - { - // Process might already be dead - Console.WriteLine($"[FfmpegAudioRecorder] Error stopping ffmpeg: {ex.Message}"); - } - finally - { - _stateTracker.ClearRecording(); - } + process.WaitForExit(2000); // give it a moment to flush + } + catch (Exception ex) + { + // Process might already be dead + Console.WriteLine($"[FfmpegAudioRecorder] Error stopping ffmpeg: {ex.Message}"); + } + finally + { + _stateTracker.ClearRecording(); } } } diff --git a/Commands/ConfigUpdaterCommand.cs b/Commands/ConfigUpdaterCommand.cs index 3ed962f..090ce1e 100644 --- a/Commands/ConfigUpdaterCommand.cs +++ b/Commands/ConfigUpdaterCommand.cs @@ -1,4 +1,3 @@ -using System.Threading.Tasks; using Spectre.Console; using Toak.Configuration; @@ -6,9 +5,9 @@ namespace Toak.Commands; public static class ConfigUpdaterCommand { - public static async Task ExecuteAsync(string key, string val, bool verbose) + public static Task ExecuteAsync(string key, string val, bool verbose) { - Toak.Core.Logger.Verbose = verbose; + Core.Logger.Verbose = verbose; var configManager = new ConfigManager(); var config = configManager.LoadConfig(); key = key.ToLowerInvariant(); @@ -23,18 +22,19 @@ public static class ConfigUpdaterCommand case "backend": config.TypingBackend = val; break; case "punctuation": if (bool.TryParse(val, out var p)) { config.ModulePunctuation = p; } - else { AnsiConsole.MarkupLine("[red]Invalid value. Use true or false.[/]"); return; } + else { AnsiConsole.MarkupLine("[red]Invalid value. Use true or false.[/]"); return Task.CompletedTask; } break; case "tech": if (bool.TryParse(val, out var t)) { config.ModuleTechnicalSanitization = t; } - else { AnsiConsole.MarkupLine("[red]Invalid value. Use true or false.[/]"); return; } + else { AnsiConsole.MarkupLine("[red]Invalid value. Use true or false.[/]"); return Task.CompletedTask; } break; default: AnsiConsole.MarkupLine($"[red]Unknown config key: {key}[/]"); - return; + return Task.CompletedTask; } configManager.SaveConfig(config); AnsiConsole.MarkupLine($"[green]Successfully[/] set {key} to [blue]{val}[/]."); + return Task.CompletedTask; } } diff --git a/Commands/DiscardCommand.cs b/Commands/DiscardCommand.cs index 46ea1ec..b8a4f41 100644 --- a/Commands/DiscardCommand.cs +++ b/Commands/DiscardCommand.cs @@ -1,6 +1,4 @@ -using System; using System.Net.Sockets; -using System.Threading.Tasks; using Spectre.Console; using Toak.Core; diff --git a/Commands/HistoryCommand.cs b/Commands/HistoryCommand.cs index 50705a8..e6ee348 100644 --- a/Commands/HistoryCommand.cs +++ b/Commands/HistoryCommand.cs @@ -1,9 +1,3 @@ -using System; -using System.Collections.Generic; -using System.IO; -using System.Linq; -using System.Threading.Tasks; -using System.CommandLine; using Spectre.Console; using Toak.Core; @@ -54,15 +48,15 @@ public static class HistoryCommand { try { - using var writer = new StreamWriter(export); - writer.WriteLine($"# Toak Transcriptions - {DateTime.Now:yyyy-MM-dd}"); - writer.WriteLine(); + await using var writer = new StreamWriter(export); + await writer.WriteLineAsync($"# Toak Transcriptions - {DateTime.Now:yyyy-MM-dd}"); + await writer.WriteLineAsync(); foreach (var entry in entries) { - writer.WriteLine($"## {entry.Timestamp.ToLocalTime():HH:mm:ss}"); - writer.WriteLine(entry.RefinedText); - writer.WriteLine(); + await writer.WriteLineAsync($"## {entry.Timestamp.ToLocalTime():HH:mm:ss}"); + await writer.WriteLineAsync(entry.RefinedText); + await writer.WriteLineAsync(); } AnsiConsole.MarkupLine($"[green]Successfully exported {entries.Count} entries to {export}[/]"); diff --git a/Commands/LatencyTestCommand.cs b/Commands/LatencyTestCommand.cs index 382939c..49f0a20 100644 --- a/Commands/LatencyTestCommand.cs +++ b/Commands/LatencyTestCommand.cs @@ -1,7 +1,4 @@ -using System; using System.Diagnostics; -using System.IO; -using System.Threading.Tasks; using Spectre.Console; using Toak.Api; using Toak.Configuration; @@ -34,7 +31,7 @@ public static class LatencyTestCommand RedirectStandardOutput = true }; var proc = Process.Start(pInfo); - proc?.WaitForExit(); + if (proc != null) await proc.WaitForExitAsync(); if (!File.Exists(testWavPath)) { @@ -51,13 +48,13 @@ public static class LatencyTestCommand { ctx.Status("Testing STT (Whisper)..."); var sttWatch = Stopwatch.StartNew(); - var transcript = await client.TranscribeAsync(testWavPath, config.WhisperLanguage, config.WhisperModel); + await client.TranscribeAsync(testWavPath, config.WhisperLanguage, config.WhisperModel); sttWatch.Stop(); ctx.Status("Testing LLM (Llama)..."); var systemPrompt = PromptBuilder.BuildPrompt(config); var llmWatch = Stopwatch.StartNew(); - var refinedText = await client.RefineTextAsync("Hello world, this is a latency test.", systemPrompt, config.LlmModel); + await client.RefineTextAsync("Hello world, this is a latency test.", systemPrompt, config.LlmModel); llmWatch.Stop(); var total = sttWatch.ElapsedMilliseconds + llmWatch.ElapsedMilliseconds; @@ -73,14 +70,9 @@ public static class LatencyTestCommand AnsiConsole.Write(table); - if (total < 1500) - { - AnsiConsole.MarkupLine($"[green]Status: OK (under 1.5s target). Total time: {(total / 1000.0):0.0}s.[/]"); - } - else - { - AnsiConsole.MarkupLine($"[yellow]Status: SLOW (over 1.5s target). Total time: {(total / 1000.0):0.0}s.[/]"); - } + AnsiConsole.MarkupLine(total < 1500 + ? $"[green]Status: OK (under 1.5s target). Total time: {(total / 1000.0):0.0}s.[/]" + : $"[yellow]Status: SLOW (over 1.5s target). Total time: {(total / 1000.0):0.0}s.[/]"); }); } catch (Exception ex) diff --git a/Commands/OnboardCommand.cs b/Commands/OnboardCommand.cs index e46c090..f1713a9 100644 --- a/Commands/OnboardCommand.cs +++ b/Commands/OnboardCommand.cs @@ -1,7 +1,4 @@ -using System; using System.Diagnostics; -using System.Linq; -using System.Threading.Tasks; using Spectre.Console; using Toak.Configuration; using Toak.Core.Skills; @@ -12,7 +9,7 @@ public static class OnboardCommand { public static async Task ExecuteAsync(bool verbose) { - Toak.Core.Logger.Verbose = verbose; + Core.Logger.Verbose = verbose; var configManager = new ConfigManager(); var config = configManager.LoadConfig(); @@ -121,7 +118,7 @@ public static class OnboardCommand } catch (Exception ex) { - Toak.Core.Logger.LogDebug($"Failed to restart toak service: {ex.Message}"); + Core.Logger.LogDebug($"Failed to restart toak service: {ex.Message}"); } } } diff --git a/Commands/ShowCommand.cs b/Commands/ShowCommand.cs index 0a124ab..9fb5828 100644 --- a/Commands/ShowCommand.cs +++ b/Commands/ShowCommand.cs @@ -1,4 +1,3 @@ -using System.Threading.Tasks; using Spectre.Console; using Toak.Configuration; @@ -8,7 +7,7 @@ public static class ShowCommand { public static async Task ExecuteAsync(bool verbose) { - Toak.Core.Logger.Verbose = verbose; + Core.Logger.Verbose = verbose; var config = new ConfigManager().LoadConfig(); var table = new Table(); diff --git a/Commands/SkillCommand.cs b/Commands/SkillCommand.cs index 20804e0..2d117f7 100644 --- a/Commands/SkillCommand.cs +++ b/Commands/SkillCommand.cs @@ -1,8 +1,5 @@ -using System; using System.CommandLine; -using System.IO; using System.Text.Json; -using System.Threading.Tasks; using Spectre.Console; using Toak.Core.Skills; using Toak.Serialization; @@ -100,8 +97,8 @@ public static class SkillCommand }; SkillRegistry.Initialize(); // ensure dir exists - string filename = Path.Combine(SkillRegistry.SkillsDirectory, $"{name.ToLowerInvariant()}.json"); - string json = JsonSerializer.Serialize(def, AppJsonSerializerContext.Default.SkillDefinition); + var filename = Path.Combine(SkillRegistry.SkillsDirectory, $"{name.ToLowerInvariant()}.json"); + var json = JsonSerializer.Serialize(def, AppJsonSerializerContext.Default.SkillDefinition); File.WriteAllText(filename, json); AnsiConsole.MarkupLine($"[bold green]Success![/] Skill '{name}' saved to {filename}"); diff --git a/Commands/StartCommand.cs b/Commands/StartCommand.cs index 4dcbfc2..1e6706b 100644 --- a/Commands/StartCommand.cs +++ b/Commands/StartCommand.cs @@ -1,6 +1,4 @@ -using System; using System.Net.Sockets; -using System.Threading.Tasks; using Spectre.Console; using Toak.Core; diff --git a/Commands/StatsCommand.cs b/Commands/StatsCommand.cs index 26589b2..f81f5ed 100644 --- a/Commands/StatsCommand.cs +++ b/Commands/StatsCommand.cs @@ -1,7 +1,3 @@ -using System; -using System.Linq; -using System.Threading.Tasks; -using System.CommandLine; using Spectre.Console; using Toak.Core; @@ -9,7 +5,7 @@ namespace Toak.Commands; public static class StatsCommand { - public static async Task ExecuteAsync(bool verbose) + public static Task ExecuteAsync(bool verbose) { Logger.Verbose = verbose; @@ -17,7 +13,7 @@ public static class StatsCommand if (entries.Count == 0) { AnsiConsole.MarkupLine("[yellow]No history found. Cannot generate statistics.[/]"); - return; + return Task.CompletedTask; } var totalCount = entries.Count; @@ -30,7 +26,7 @@ public static class StatsCommand .FirstOrDefault(); var topWords = entries - .SelectMany(e => e.RefinedText.Split(new[] { ' ', '.', ',', '!', '?' }, StringSplitOptions.RemoveEmptyEntries)) + .SelectMany(e => e.RefinedText.Split([' ', '.', ',', '!', '?'], StringSplitOptions.RemoveEmptyEntries)) .Where(w => w.Length > 3) // Exclude short common words .GroupBy(w => w.ToLowerInvariant()) .OrderByDescending(g => g.Count()) @@ -52,5 +48,6 @@ public static class StatsCommand { AnsiConsole.MarkupLine($"[dim]Top spoken words (>3 chars):[/] {string.Join(", ", topWords)}"); } + return Task.CompletedTask; } } diff --git a/Commands/StatusCommand.cs b/Commands/StatusCommand.cs index a9d1733..3e1f6ce 100644 --- a/Commands/StatusCommand.cs +++ b/Commands/StatusCommand.cs @@ -1,6 +1,4 @@ -using System; using System.Net.Sockets; -using System.Threading.Tasks; using Spectre.Console; using Toak.Core; @@ -24,7 +22,7 @@ public static class StatusCommand await socket.SendAsync(msg, SocketFlags.None); var responseBuffer = new byte[4096]; - int received = await socket.ReceiveAsync(responseBuffer, SocketFlags.None); + var received = await socket.ReceiveAsync(responseBuffer, SocketFlags.None); if (received > 0) { var text = System.Text.Encoding.UTF8.GetString(responseBuffer, 0, received); @@ -33,10 +31,7 @@ public static class StatusCommand } catch (SocketException) { - if (json) - Console.WriteLine("{\"state\": \"Offline\"}"); - else - Console.WriteLine("Offline"); + Console.WriteLine(json ? "{\"state\": \"Offline\"}" : "Offline"); } catch (Exception ex) { diff --git a/Commands/StopCommand.cs b/Commands/StopCommand.cs index d68c614..774fd22 100644 --- a/Commands/StopCommand.cs +++ b/Commands/StopCommand.cs @@ -1,6 +1,4 @@ -using System; using System.Net.Sockets; -using System.Threading.Tasks; using Spectre.Console; using Toak.Core; @@ -25,7 +23,7 @@ public static class StopCommand var responseBuffer = new byte[4096]; while (true) { - int received = await socket.ReceiveAsync(responseBuffer, SocketFlags.None); + var received = await socket.ReceiveAsync(responseBuffer, SocketFlags.None); if (received == 0) break; if (pipeToStdout) { diff --git a/Commands/ToggleCommand.cs b/Commands/ToggleCommand.cs index 3c30f46..01054e2 100644 --- a/Commands/ToggleCommand.cs +++ b/Commands/ToggleCommand.cs @@ -1,6 +1,4 @@ -using System; using System.Net.Sockets; -using System.Threading.Tasks; using Spectre.Console; using Toak.Core; @@ -34,7 +32,7 @@ public static class ToggleCommand var responseBuffer = new byte[4096]; while (true) { - int received = await socket.ReceiveAsync(responseBuffer, SocketFlags.None); + var received = await socket.ReceiveAsync(responseBuffer, SocketFlags.None); if (received == 0) break; // socket closed by daemon if (pipeToStdout) diff --git a/Configuration/ConfigManager.cs b/Configuration/ConfigManager.cs index 3838e11..01cc949 100644 --- a/Configuration/ConfigManager.cs +++ b/Configuration/ConfigManager.cs @@ -1,6 +1,4 @@ using System.Text.Json; -using System.Text.Json.Serialization; - using Toak.Core; using Toak.Serialization; using Toak.Core.Interfaces; @@ -9,23 +7,19 @@ namespace Toak.Configuration; public class ConfigManager : IConfigProvider { - private readonly string ConfigDir = Constants.Paths.ConfigDir; - private readonly string ConfigPath = Constants.Paths.ConfigFile; - - public ConfigManager() - { - } + private readonly string _configDir = Constants.Paths.ConfigDir; + private readonly string _configPath = Constants.Paths.ConfigFile; public ToakConfig LoadConfig() { - if (!File.Exists(ConfigPath)) + if (!File.Exists(_configPath)) { return new ToakConfig(); } try { - var json = File.ReadAllText(ConfigPath); + var json = File.ReadAllText(_configPath); return JsonSerializer.Deserialize(json, AppJsonSerializerContext.Default.ToakConfig) ?? new ToakConfig(); } catch (Exception) @@ -36,12 +30,12 @@ public class ConfigManager : IConfigProvider public void SaveConfig(ToakConfig config) { - if (!Directory.Exists(ConfigDir)) + if (!Directory.Exists(_configDir)) { - Directory.CreateDirectory(ConfigDir); + Directory.CreateDirectory(_configDir); } var json = JsonSerializer.Serialize(config, AppJsonSerializerContext.Default.ToakConfig); - File.WriteAllText(ConfigPath, json); + File.WriteAllText(_configPath, json); } } diff --git a/Configuration/ToakConfig.cs b/Configuration/ToakConfig.cs index b17fb8b..60ab6cc 100644 --- a/Configuration/ToakConfig.cs +++ b/Configuration/ToakConfig.cs @@ -12,10 +12,10 @@ public class ToakConfig public int MinRecordingDuration { get; set; } = 500; public string WhisperLanguage { get; set; } = string.Empty; - public string LlmModel { get; set; } = Toak.Core.Constants.Defaults.LlmModel; + public string LlmModel { get; set; } = Core.Constants.Defaults.LlmModel; public string ReasoningEffort { get; set; } = "none"; // none or low - public string WhisperModel { get; set; } = Toak.Core.Constants.Defaults.WhisperModel; + public string WhisperModel { get; set; } = Core.Constants.Defaults.WhisperModel; public string StartSoundPath { get; set; } = "Assets/Audio/beep.wav"; public string StopSoundPath { get; set; } = "Assets/Audio/beep.wav"; - public List ActiveSkills { get; set; } = new List { "Terminal", "Translate" }; + public List ActiveSkills { get; set; } = ["Terminal", "Translate"]; } diff --git a/Core/Constants.cs b/Core/Constants.cs index f266248..0c06848 100644 --- a/Core/Constants.cs +++ b/Core/Constants.cs @@ -1,6 +1,3 @@ -using System; -using System.IO; - namespace Toak.Core; public static class Constants diff --git a/Core/DaemonService.cs b/Core/DaemonService.cs index 7f5ea26..78fe737 100644 --- a/Core/DaemonService.cs +++ b/Core/DaemonService.cs @@ -1,8 +1,4 @@ -using System; -using System.Diagnostics; -using System.IO; using System.Net.Sockets; -using System.Threading.Tasks; using Toak.Configuration; using Toak.Api; using Toak.Core.Interfaces; @@ -54,15 +50,9 @@ public static class DaemonService var notifications = new Notifications(); var speechClient = new OpenAiCompatibleClient(config.GroqApiKey); - ILlmClient llmClient; - if (config.LlmProvider == "together") - { - llmClient = new OpenAiCompatibleClient(config.TogetherApiKey, "https://api.together.xyz/v1/", config.ReasoningEffort); - } - else - { - llmClient = new OpenAiCompatibleClient(config.GroqApiKey, "https://api.groq.com/openai/v1/", config.ReasoningEffort); - } + ILlmClient llmClient = config.LlmProvider == "together" + ? new OpenAiCompatibleClient(config.TogetherApiKey, "https://api.together.xyz/v1/", config.ReasoningEffort) + : new OpenAiCompatibleClient(config.GroqApiKey, "https://api.groq.com/openai/v1/", config.ReasoningEffort); IAudioRecorder recorder = config.AudioBackend == "ffmpeg" ? new FfmpegAudioRecorder(stateTracker, notifications) @@ -114,12 +104,12 @@ public static class DaemonService try { var buffer = new byte[3]; - int bytesRead = await client.ReceiveAsync(buffer, SocketFlags.None); + var bytesRead = await client.ReceiveAsync(buffer, SocketFlags.None); if (bytesRead > 0) { - byte cmd = buffer[0]; - bool pipeToStdout = bytesRead > 1 && buffer[1] == 1; - bool copyToClipboard = bytesRead > 2 && buffer[2] == 1; + var cmd = buffer[0]; + var pipeToStdout = bytesRead > 1 && buffer[1] == 1; + var copyToClipboard = bytesRead > 2 && buffer[2] == 1; if (cmd == 1) // START { @@ -142,9 +132,9 @@ public static class DaemonService } else if (cmd == 5) // STATUS { - bool json = pipeToStdout; // buffer[1] == 1 is json - bool isRecording = stateTracker.IsRecording(); - string stateStr = isRecording ? "Recording" : "Idle"; + var json = pipeToStdout; // buffer[1] == 1 is json + var isRecording = stateTracker.IsRecording(); + var stateStr = isRecording ? "Recording" : "Idle"; if (json) { diff --git a/Core/HistoryEntry.cs b/Core/HistoryEntry.cs index 799379f..346b93f 100644 --- a/Core/HistoryEntry.cs +++ b/Core/HistoryEntry.cs @@ -1,5 +1,3 @@ -using System; - namespace Toak.Core; public class HistoryEntry diff --git a/Core/HistoryManager.cs b/Core/HistoryManager.cs index cdd65a9..76de918 100644 --- a/Core/HistoryManager.cs +++ b/Core/HistoryManager.cs @@ -1,8 +1,4 @@ -using System; -using System.Collections.Generic; -using System.IO; using System.Text.Json; -using System.Threading.Tasks; using Toak.Serialization; using Toak.Core.Interfaces; @@ -10,20 +6,16 @@ namespace Toak.Core; public class HistoryManager : IHistoryManager { - private readonly string HistoryDir = Constants.Paths.AppDataDir; - private readonly string HistoryFile = Constants.Paths.HistoryFile; - - public HistoryManager() - { - } + private readonly string _historyDir = Constants.Paths.AppDataDir; + private readonly string _historyFile = Constants.Paths.HistoryFile; public void SaveEntry(string rawTranscript, string refinedText, string? skillName, long durationMs) { try { - if (!Directory.Exists(HistoryDir)) + if (!Directory.Exists(_historyDir)) { - Directory.CreateDirectory(HistoryDir); + Directory.CreateDirectory(_historyDir); } var entry = new HistoryEntry @@ -38,9 +30,9 @@ public class HistoryManager : IHistoryManager var json = JsonSerializer.Serialize(entry, CompactJsonSerializerContext.Default.HistoryEntry); // Thread-safe append - lock (HistoryFile) + lock (_historyFile) { - File.AppendAllLines(HistoryFile, new[] { json }); + File.AppendAllLines(_historyFile, [json]); } } catch (Exception ex) @@ -52,14 +44,14 @@ public class HistoryManager : IHistoryManager public List LoadHistory() { var entries = new List(); - if (!File.Exists(HistoryFile)) return entries; + if (!File.Exists(_historyFile)) return entries; try { string[] lines; - lock (HistoryFile) + lock (_historyFile) { - lines = File.ReadAllLines(HistoryFile); + lines = File.ReadAllLines(_historyFile); } foreach (var line in lines) @@ -92,20 +84,20 @@ public class HistoryManager : IHistoryManager public void ClearHistory() { - if (File.Exists(HistoryFile)) + if (File.Exists(_historyFile)) { try { - lock (HistoryFile) + lock (_historyFile) { // Securely delete - var len = new FileInfo(HistoryFile).Length; - using (var fs = new FileStream(HistoryFile, FileMode.Open, FileAccess.Write)) + var len = new FileInfo(_historyFile).Length; + using (var fs = new FileStream(_historyFile, FileMode.Open, FileAccess.Write)) { var blank = new byte[len]; fs.Write(blank, 0, blank.Length); } - File.Delete(HistoryFile); + File.Delete(_historyFile); } } catch (Exception ex) diff --git a/Core/Interfaces/ITranscriptionOrchestrator.cs b/Core/Interfaces/ITranscriptionOrchestrator.cs index f4a2e29..e41331b 100644 --- a/Core/Interfaces/ITranscriptionOrchestrator.cs +++ b/Core/Interfaces/ITranscriptionOrchestrator.cs @@ -1,5 +1,4 @@ using System.Net.Sockets; -using System.Threading.Tasks; namespace Toak.Core.Interfaces; diff --git a/Core/Interfaces/Interfaces.cs b/Core/Interfaces/Interfaces.cs index 528af5a..c8166cf 100644 --- a/Core/Interfaces/Interfaces.cs +++ b/Core/Interfaces/Interfaces.cs @@ -1,6 +1,3 @@ -using System; -using System.Collections.Generic; -using System.Threading.Tasks; using Toak.Configuration; namespace Toak.Core.Interfaces; @@ -13,13 +10,13 @@ public interface IConfigProvider public interface ISpeechClient { - Task TranscribeAsync(string filePath, string language = "", string model = Toak.Core.Constants.Defaults.WhisperModel); + Task TranscribeAsync(string filePath, string language = "", string model = Constants.Defaults.WhisperModel); } public interface ILlmClient { - Task RefineTextAsync(string rawTranscript, string systemPrompt, string model = Toak.Core.Constants.Defaults.LlmModel); - IAsyncEnumerable RefineTextStreamAsync(string rawTranscript, string systemPrompt, string model = Toak.Core.Constants.Defaults.LlmModel); + Task RefineTextAsync(string rawTranscript, string systemPrompt, string model = Constants.Defaults.LlmModel); + IAsyncEnumerable RefineTextStreamAsync(string rawTranscript, string systemPrompt, string model = Constants.Defaults.LlmModel); } public interface IAudioRecorder diff --git a/Core/PromptBuilder.cs b/Core/PromptBuilder.cs index 3b45aec..5629e03 100644 --- a/Core/PromptBuilder.cs +++ b/Core/PromptBuilder.cs @@ -1,5 +1,4 @@ using System.Text; - using Toak.Configuration; namespace Toak.Core; diff --git a/Core/Skills/DynamicSkill.cs b/Core/Skills/DynamicSkill.cs index 809a8a9..2cd131e 100644 --- a/Core/Skills/DynamicSkill.cs +++ b/Core/Skills/DynamicSkill.cs @@ -1,11 +1,10 @@ -using System; using System.Diagnostics; namespace Toak.Core.Skills; -public class DynamicSkill : ISkill +public class DynamicSkill(SkillDefinition def) : ISkill { - private readonly SkillDefinition _def; + private readonly SkillDefinition _def = def; public string Name => _def.Name; public string Description => _def.Description; @@ -13,11 +12,6 @@ public class DynamicSkill : ISkill public bool HandlesExecution => _def.Action.ToLowerInvariant() == "script"; - public DynamicSkill(SkillDefinition def) - { - _def = def; - } - public string GetSystemPrompt(string rawTranscript) { return _def.SystemPrompt.Replace("{transcript}", rawTranscript); diff --git a/Core/Skills/SkillDefinition.cs b/Core/Skills/SkillDefinition.cs index 4c850c5..f4599c7 100644 --- a/Core/Skills/SkillDefinition.cs +++ b/Core/Skills/SkillDefinition.cs @@ -4,7 +4,7 @@ public class SkillDefinition { public string Name { get; set; } = ""; public string Description { get; set; } = ""; - public string[] Hotwords { get; set; } = System.Array.Empty(); + public string[] Hotwords { get; set; } = []; public string Action { get; set; } = "type"; // "type" or "script" public string SystemPrompt { get; set; } = ""; public string? ScriptPath { get; set; } diff --git a/Core/Skills/SkillRegistry.cs b/Core/Skills/SkillRegistry.cs index 037adc7..392a6b5 100644 --- a/Core/Skills/SkillRegistry.cs +++ b/Core/Skills/SkillRegistry.cs @@ -1,7 +1,3 @@ -using System; -using System.Collections.Generic; -using System.IO; -using System.Linq; using System.Text.Json; using Toak.Serialization; @@ -9,7 +5,7 @@ namespace Toak.Core.Skills; public static class SkillRegistry { - public static List AllSkills = new List(); + public static List AllSkills = []; public static string SkillsDirectory => Path.Combine( Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), @@ -28,7 +24,7 @@ public static class SkillRegistry { try { - string json = File.ReadAllText(file); + var json = File.ReadAllText(file); var def = JsonSerializer.Deserialize(json, AppJsonSerializerContext.Default.SkillDefinition); if (def != null) { @@ -47,7 +43,7 @@ public static class SkillRegistry if (AllSkills.Count == 0) Initialize(); var activeSkills = AllSkills.Where(s => activeSkillNames.Contains(s.Name, StringComparer.OrdinalIgnoreCase)).ToList(); - string normalizedTranscript = transcript.Trim(); + var normalizedTranscript = transcript.Trim(); foreach (var skill in activeSkills) { @@ -72,9 +68,11 @@ public static class SkillRegistry Description = "Translates the spoken command into a bash command and types it.", Hotwords = ["System terminal", "System run", "System execute"], Action = "type", - SystemPrompt = @"You are a Linux terminal expert. -Translate the user's request into a single, valid bash command. -Output ONLY the raw command, no formatting, no markdown." + SystemPrompt = """ + You are a Linux terminal expert. + Translate the user's request into a single, valid bash command. + Output ONLY the raw command, no formatting, no markdown. + """ }, new SkillDefinition { @@ -82,10 +80,12 @@ Output ONLY the raw command, no formatting, no markdown." Description = "Translates the spoken text into another language on the fly.", Hotwords = ["System translate to", "System translate into"], Action = "type", - SystemPrompt = @"You are an expert translator. The user wants to translate the following text. -The first few words identify the target language (e.g. 'Translate to Spanish:', 'Translate into Hungarian:'). -Translate the REST of the transcript into that target language. -Output ONLY the final translated text. Do not include markdown, explanations, or quotes." + SystemPrompt = """ + You are an expert translator. The user wants to translate the following text. + The first few words identify the target language (e.g. 'Translate to Spanish:', 'Translate into Hungarian:'). + Translate the REST of the transcript into that target language. + Output ONLY the final translated text. Do not include markdown, explanations, or quotes. + """ }, new SkillDefinition { @@ -93,13 +93,15 @@ Output ONLY the final translated text. Do not include markdown, explanations, or Description = "Rewrites text into a formal, articulate tone.", Hotwords = ["System professional", "System formalize", "System formal"], Action = "type", - SystemPrompt = @"Rewrite the following text to be articulate and formal. -The text will start with 'System professional', 'System formalize', or 'System formal', -or something along the lines of that. You can ignore those words. -Do not add any conversational filler. -Make sure to preserve the meaning of the original text. -Output ONLY the final professional text. -Text: {transcript}" + SystemPrompt = """ + Rewrite the following text to be articulate and formal. + The text will start with 'System professional', 'System formalize', or 'System formal', + or something along the lines of that. You can ignore those words. + Do not add any conversational filler. + Make sure to preserve the meaning of the original text. + Output ONLY the final professional text. + Text: {transcript} + """ }, new SkillDefinition { @@ -107,19 +109,21 @@ Text: {transcript}" Description = "Provides a direct, crisp summary of the dictation.", Hotwords = ["System summary", "System concise", "System summarize"], Action = "type", - SystemPrompt = @"Summarize the following text to be as concise -and direct as possible. -The text will start with 'System summary', 'System concise', or 'System summarize', -and you shoul ignore that part of the text. -Output ONLY the final summary text. -Text: {transcript}" + SystemPrompt = """ + Summarize the following text to be as concise + and direct as possible. + The text will start with 'System summary', 'System concise', or 'System summarize', + and you shoul ignore that part of the text. + Output ONLY the final summary text. + Text: {transcript} + """ } }; foreach (var def in defaults) { - string filename = Path.Combine(SkillsDirectory, $"{def.Name.ToLowerInvariant()}.json"); - string json = JsonSerializer.Serialize(def, AppJsonSerializerContext.Default.SkillDefinition); + var filename = Path.Combine(SkillsDirectory, $"{def.Name.ToLowerInvariant()}.json"); + var json = JsonSerializer.Serialize(def, AppJsonSerializerContext.Default.SkillDefinition); File.WriteAllText(filename, json); } diff --git a/Core/StateTracker.cs b/Core/StateTracker.cs index 2332b08..36aff94 100644 --- a/Core/StateTracker.cs +++ b/Core/StateTracker.cs @@ -4,24 +4,24 @@ namespace Toak.Core; public class StateTracker : IRecordingStateTracker { - private readonly string StateFilePath = Constants.Paths.StateFile; + private readonly string _stateFilePath = Constants.Paths.StateFile; public bool IsRecording() { - return File.Exists(StateFilePath); + return File.Exists(_stateFilePath); } public void SetRecording(int ffmpegPid) { Logger.LogDebug($"Setting recording state with PID {ffmpegPid}"); - File.WriteAllText(StateFilePath, $"{ffmpegPid}\n{DateTime.UtcNow.Ticks}"); + File.WriteAllText(_stateFilePath, $"{ffmpegPid}\n{DateTime.UtcNow.Ticks}"); } public int? GetRecordingPid() { - if (File.Exists(StateFilePath)) + if (File.Exists(_stateFilePath)) { - var lines = File.ReadAllLines(StateFilePath); + var lines = File.ReadAllLines(_stateFilePath); if (lines.Length > 0 && int.TryParse(lines[0], out var pid)) { Logger.LogDebug($"Read recording PID {pid} from state file"); @@ -33,9 +33,9 @@ public class StateTracker : IRecordingStateTracker public DateTime? GetRecordingStartTime() { - if (File.Exists(StateFilePath)) + if (File.Exists(_stateFilePath)) { - var lines = File.ReadAllLines(StateFilePath); + var lines = File.ReadAllLines(_stateFilePath); if (lines.Length > 1 && long.TryParse(lines[1], out var ticks)) { return new DateTime(ticks, DateTimeKind.Utc); @@ -46,10 +46,10 @@ public class StateTracker : IRecordingStateTracker public void ClearRecording() { - if (File.Exists(StateFilePath)) + if (File.Exists(_stateFilePath)) { Logger.LogDebug("Clearing recording state file"); - File.Delete(StateFilePath); + File.Delete(_stateFilePath); } } } diff --git a/Core/TranscriptionOrchestrator.cs b/Core/TranscriptionOrchestrator.cs index ec686ce..053ba39 100644 --- a/Core/TranscriptionOrchestrator.cs +++ b/Core/TranscriptionOrchestrator.cs @@ -1,55 +1,39 @@ -using System; using System.Diagnostics; -using System.IO; using System.Net.Sockets; -using System.Threading.Tasks; using Toak.Core.Interfaces; -using Toak.Configuration; namespace Toak.Core; -public class TranscriptionOrchestrator : ITranscriptionOrchestrator +public class TranscriptionOrchestrator( + ISpeechClient speechClient, + ILlmClient llmClient, + IConfigProvider configProvider, + IAudioRecorder audioRecorder, + INotifications notifications, + ITextInjector textInjector, + IHistoryManager historyManager, + IClipboardManager clipboardManager, + IRecordingStateTracker stateTracker) : ITranscriptionOrchestrator { - private readonly ISpeechClient _speechClient; - private readonly ILlmClient _llmClient; - private readonly IConfigProvider _configProvider; - private readonly IAudioRecorder _audioRecorder; - private readonly INotifications _notifications; - private readonly ITextInjector _textInjector; - private readonly IHistoryManager _historyManager; - private readonly IClipboardManager _clipboardManager; - private readonly IRecordingStateTracker _stateTracker; + private readonly ISpeechClient _speechClient = speechClient; + private readonly ILlmClient _llmClient = llmClient; + private readonly IConfigProvider _configProvider = configProvider; + private readonly IAudioRecorder _audioRecorder = audioRecorder; + private readonly INotifications _notifications = notifications; + private readonly ITextInjector _textInjector = textInjector; + private readonly IHistoryManager _historyManager = historyManager; + private readonly IClipboardManager _clipboardManager = clipboardManager; + private readonly IRecordingStateTracker _stateTracker = stateTracker; - public TranscriptionOrchestrator( - ISpeechClient speechClient, - ILlmClient llmClient, - IConfigProvider configProvider, - IAudioRecorder audioRecorder, - INotifications notifications, - ITextInjector textInjector, - IHistoryManager historyManager, - IClipboardManager clipboardManager, - IRecordingStateTracker stateTracker) + public Task ProcessStartRecordingAsync() { - _speechClient = speechClient; - _llmClient = llmClient; - _configProvider = configProvider; - _audioRecorder = audioRecorder; - _notifications = notifications; - _textInjector = textInjector; - _historyManager = historyManager; - _clipboardManager = clipboardManager; - _stateTracker = stateTracker; - } - - public async Task ProcessStartRecordingAsync() - { - if (_stateTracker.IsRecording()) return; + if (_stateTracker.IsRecording()) return Task.CompletedTask; Logger.LogDebug("Received START command"); var config = _configProvider.LoadConfig(); _notifications.PlaySound(config.StartSoundPath); _audioRecorder.StartRecording(); + return Task.CompletedTask; } public async Task ProcessStopRecordingAsync(Socket client, bool pipeToStdout, bool copyToClipboard) @@ -96,9 +80,9 @@ public class TranscriptionOrchestrator : ITranscriptionOrchestrator return; } - var detectedSkill = Toak.Core.Skills.SkillRegistry.DetectSkill(transcript, config.ActiveSkills); - string systemPrompt = detectedSkill != null ? detectedSkill.GetSystemPrompt(transcript) : PromptBuilder.BuildPrompt(config); - bool isExecutionSkill = detectedSkill != null && detectedSkill.HandlesExecution; + var detectedSkill = Skills.SkillRegistry.DetectSkill(transcript, config.ActiveSkills); + var systemPrompt = detectedSkill != null ? detectedSkill.GetSystemPrompt(transcript) : PromptBuilder.BuildPrompt(config); + var isExecutionSkill = detectedSkill != null && detectedSkill.HandlesExecution; if (isExecutionSkill) { @@ -118,7 +102,7 @@ public class TranscriptionOrchestrator : ITranscriptionOrchestrator if (pipeToStdout || copyToClipboard) { - string fullText = ""; + var fullText = ""; await foreach (var token in tokenStream) { fullText += token; @@ -137,7 +121,7 @@ public class TranscriptionOrchestrator : ITranscriptionOrchestrator } else { - string fullText = await _textInjector.InjectStreamAsync(tokenStream, config.TypingBackend); + var fullText = await _textInjector.InjectStreamAsync(tokenStream, config.TypingBackend); stopWatch.Stop(); _historyManager.SaveEntry(transcript, fullText, detectedSkill?.Name, stopWatch.ElapsedMilliseconds); _notifications.Notify("Toak", $"Done in {stopWatch.ElapsedMilliseconds}ms"); diff --git a/IO/ClipboardManager.cs b/IO/ClipboardManager.cs index 04a7f8c..f3d0985 100644 --- a/IO/ClipboardManager.cs +++ b/IO/ClipboardManager.cs @@ -1,31 +1,25 @@ using System.Diagnostics; - using Toak.Core.Interfaces; namespace Toak.IO; -public class ClipboardManager : IClipboardManager +public class ClipboardManager(INotifications notifications) : IClipboardManager { - private readonly INotifications _notifications; - - public ClipboardManager(INotifications notifications) - { - _notifications = notifications; - } + private readonly INotifications _notifications = notifications; public void Copy(string text) { if (string.IsNullOrWhiteSpace(text)) return; try { - string sessionType = Environment.GetEnvironmentVariable("XDG_SESSION_TYPE")?.ToLowerInvariant() ?? ""; + var sessionType = Environment.GetEnvironmentVariable("XDG_SESSION_TYPE")?.ToLowerInvariant() ?? ""; ProcessStartInfo pInfo; if (sessionType == "wayland") { pInfo = new ProcessStartInfo { - FileName = Toak.Core.Constants.Commands.ClipboardWayland, + FileName = Core.Constants.Commands.ClipboardWayland, UseShellExecute = false, CreateNoWindow = true, RedirectStandardInput = true @@ -35,7 +29,7 @@ public class ClipboardManager : IClipboardManager { pInfo = new ProcessStartInfo { - FileName = Toak.Core.Constants.Commands.ClipboardX11, + FileName = Core.Constants.Commands.ClipboardX11, Arguments = "-selection clipboard", UseShellExecute = false, CreateNoWindow = true, @@ -44,14 +38,12 @@ public class ClipboardManager : IClipboardManager } var process = Process.Start(pInfo); - if (process != null) + if (process == null) return; + using (var sw = process.StandardInput) { - using (var sw = process.StandardInput) - { - sw.Write(text); - } - process.WaitForExit(); + sw.Write(text); } + process.WaitForExit(); } catch (Exception ex) { diff --git a/IO/Notifications.cs b/IO/Notifications.cs index 98cd111..8c58b61 100644 --- a/IO/Notifications.cs +++ b/IO/Notifications.cs @@ -1,5 +1,4 @@ using System.Diagnostics; - using Toak.Core.Interfaces; namespace Toak.IO; @@ -16,7 +15,7 @@ public class Notifications : INotifications { var pInfo = new ProcessStartInfo { - FileName = Toak.Core.Constants.Commands.Notify, + FileName = Core.Constants.Commands.Notify, Arguments = $"-a \"Toak\" \"{summary}\" \"{body}\"", UseShellExecute = false, CreateNoWindow = true @@ -66,7 +65,7 @@ public class Notifications : INotifications var pInfo = new ProcessStartInfo { - FileName = Toak.Core.Constants.Commands.PlaySound, + FileName = Core.Constants.Commands.PlaySound, Arguments = $"\"{absolutePath}\"", UseShellExecute = false, CreateNoWindow = true diff --git a/IO/TextInjector.cs b/IO/TextInjector.cs index adb0aa4..5caa422 100644 --- a/IO/TextInjector.cs +++ b/IO/TextInjector.cs @@ -1,19 +1,12 @@ using System.Diagnostics; - using Toak.Core; - using Toak.Core.Interfaces; namespace Toak.IO; -public class TextInjector : ITextInjector +public class TextInjector(INotifications notifications) : ITextInjector { - private readonly INotifications _notifications; - - public TextInjector(INotifications notifications) - { - _notifications = notifications; - } + private readonly INotifications _notifications = notifications; public Task InjectTextAsync(string text, string backend = "xdotool") { @@ -28,8 +21,8 @@ public class TextInjector : ITextInjector Logger.LogDebug($"Injecting text using wtype..."); pInfo = new ProcessStartInfo { - FileName = Toak.Core.Constants.Commands.TypeWayland, - Arguments = $"-d {Toak.Core.Constants.Defaults.DefaultTypeDelayMs} \"{text.Replace("\"", "\\\"")}\"", + FileName = Constants.Commands.TypeWayland, + Arguments = $"-d {Constants.Defaults.DefaultTypeDelayMs} \"{text.Replace("\"", "\\\"")}\"", UseShellExecute = false, CreateNoWindow = true }; @@ -39,7 +32,7 @@ public class TextInjector : ITextInjector Logger.LogDebug($"Injecting text using ydotool..."); pInfo = new ProcessStartInfo { - FileName = Toak.Core.Constants.Commands.TypeYdotool, + FileName = Constants.Commands.TypeYdotool, Arguments = $"type \"{text.Replace("\"", "\\\"")}\"", UseShellExecute = false, CreateNoWindow = true @@ -50,8 +43,8 @@ public class TextInjector : ITextInjector Logger.LogDebug($"Injecting text using xdotool..."); pInfo = new ProcessStartInfo { - FileName = Toak.Core.Constants.Commands.TypeX11, - Arguments = $"type --clearmodifiers --delay {Toak.Core.Constants.Defaults.DefaultTypeDelayMs} \"{text.Replace("\"", "\\\"")}\"", + FileName = Constants.Commands.TypeX11, + Arguments = $"type --clearmodifiers --delay {Constants.Defaults.DefaultTypeDelayMs} \"{text.Replace("\"", "\\\"")}\"", UseShellExecute = false, CreateNoWindow = true }; @@ -69,7 +62,7 @@ public class TextInjector : ITextInjector public async Task InjectStreamAsync(IAsyncEnumerable tokenStream, string backend) { - string fullText = string.Empty; + var fullText = string.Empty; try { ProcessStartInfo pInfo; @@ -78,8 +71,8 @@ public class TextInjector : ITextInjector Logger.LogDebug($"Setting up stream injection using wtype..."); pInfo = new ProcessStartInfo { - FileName = Toak.Core.Constants.Commands.TypeWayland, - Arguments = $"-d {Toak.Core.Constants.Defaults.DefaultTypeDelayMs} -", + FileName = Constants.Commands.TypeWayland, + Arguments = $"-d {Constants.Defaults.DefaultTypeDelayMs} -", UseShellExecute = false, CreateNoWindow = true, RedirectStandardInput = true @@ -94,7 +87,7 @@ public class TextInjector : ITextInjector fullText += token; var chunkInfo = new ProcessStartInfo { - FileName = Toak.Core.Constants.Commands.TypeYdotool, + FileName = Constants.Commands.TypeYdotool, Arguments = $"type \"{token.Replace("\"", "\\\"")}\"", UseShellExecute = false, CreateNoWindow = true @@ -109,8 +102,8 @@ public class TextInjector : ITextInjector Logger.LogDebug($"Setting up stream injection using xdotool..."); pInfo = new ProcessStartInfo { - FileName = Toak.Core.Constants.Commands.TypeX11, - Arguments = $"type --clearmodifiers --delay {Toak.Core.Constants.Defaults.DefaultTypeDelayMs} --file -", + FileName = Constants.Commands.TypeX11, + Arguments = $"type --clearmodifiers --delay {Constants.Defaults.DefaultTypeDelayMs} --file -", UseShellExecute = false, CreateNoWindow = true, RedirectStandardInput = true diff --git a/Program.cs b/Program.cs index cfb5fb2..0104a0b 100644 --- a/Program.cs +++ b/Program.cs @@ -1,5 +1,4 @@ using System.CommandLine; -using System.Threading.Tasks; using Toak.Commands; namespace Toak; @@ -44,7 +43,7 @@ public class Program // Daemon Command var daemonCmd = new Command("daemon", "Starts the background service"); - daemonCmd.SetHandler(Toak.Core.DaemonService.StartAsync, verboseOption); + daemonCmd.SetHandler(Core.DaemonService.StartAsync, verboseOption); rootCommand.AddCommand(daemonCmd); // Discard Command diff --git a/Serialization/AppJsonSerializerContext.cs b/Serialization/AppJsonSerializerContext.cs index fc425d4..05a8e33 100644 --- a/Serialization/AppJsonSerializerContext.cs +++ b/Serialization/AppJsonSerializerContext.cs @@ -18,14 +18,14 @@ namespace Toak.Serialization; [JsonSerializable(typeof(OpenAiStreamChoice))] [JsonSerializable(typeof(OpenAiStreamDelta))] [JsonSerializable(typeof(OpenAiStreamChoice[]))] -[JsonSerializable(typeof(Toak.Core.Skills.SkillDefinition))] -[JsonSerializable(typeof(Toak.Core.HistoryEntry))] +[JsonSerializable(typeof(Core.Skills.SkillDefinition))] +[JsonSerializable(typeof(Core.HistoryEntry))] internal partial class AppJsonSerializerContext : JsonSerializerContext { } [JsonSourceGenerationOptions(WriteIndented = false, DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull)] -[JsonSerializable(typeof(Toak.Core.HistoryEntry))] +[JsonSerializable(typeof(Core.HistoryEntry))] internal partial class CompactJsonSerializerContext : JsonSerializerContext { } diff --git a/Toak.csproj b/Toak.csproj index c806a24..5454695 100644 --- a/Toak.csproj +++ b/Toak.csproj @@ -1,20 +1,20 @@  - - Exe - net10.0 - enable - enable - true - + + Exe + net10.0 + enable + enable + true + - - - + + + - - - - + + + + - + \ No newline at end of file diff --git a/qodana_problems.md b/qodana_problems.md deleted file mode 100644 index 38e0a1b..0000000 --- a/qodana_problems.md +++ /dev/null @@ -1,257 +0,0 @@ -# Qodana Inspection Results - -This document outlines the problems detected in the codebase by the Qodana scan. - -## ArrangeObjectCreationWhenTypeEvident -Found 6 occurrences. - -- **Configuration/ToakConfig.cs** (Line 20): Redundant type specification [note] -- **Core/Skills/SkillRegistry.cs** (Line 12): Redundant type specification [note] -- **Core/Skills/SkillRegistry.cs** (Line 69): Redundant type specification [note] -- **Core/Skills/SkillRegistry.cs** (Line 79): Redundant type specification [note] -- **Core/Skills/SkillRegistry.cs** (Line 90): Redundant type specification [note] -- **Core/Skills/SkillRegistry.cs** (Line 104): Redundant type specification [note] - -## AsyncMethodWithoutAwait -Found 9 occurrences. - -- **Commands/ConfigUpdaterCommand.cs** (Line 9): This async method lacks 'await' operators and will run synchronously. Consider using the 'await' operator to await non-blocking API calls or drop 'async' to avoid generating state machine for this method [note] -- **Commands/HistoryCommand.cs** (Line 14): This async method lacks 'await' operators and will run synchronously. Consider using the 'await' operator to await non-blocking API calls or drop 'async' to avoid generating state machine for this method [note] -- **Commands/OnboardCommand.cs** (Line 13): This async method lacks 'await' operators and will run synchronously. Consider using the 'await' operator to await non-blocking API calls or drop 'async' to avoid generating state machine for this method [note] -- **Commands/ShowCommand.cs** (Line 9): This async method lacks 'await' operators and will run synchronously. Consider using the 'await' operator to await non-blocking API calls or drop 'async' to avoid generating state machine for this method [note] -- **Commands/SkillCommand.cs** (Line 37): This async method lacks 'await' operators and will run synchronously. Consider using the 'await' operator to await non-blocking API calls or drop 'async' to avoid generating state machine for this method [note] -- **Commands/SkillCommand.cs** (Line 68): This async method lacks 'await' operators and will run synchronously. Consider using the 'await' operator to await non-blocking API calls or drop 'async' to avoid generating state machine for this method [note] -- **Commands/SkillCommand.cs** (Line 110): This async method lacks 'await' operators and will run synchronously. Consider using the 'await' operator to await non-blocking API calls or drop 'async' to avoid generating state machine for this method [note] -- **Commands/StatsCommand.cs** (Line 12): This async method lacks 'await' operators and will run synchronously. Consider using the 'await' operator to await non-blocking API calls or drop 'async' to avoid generating state machine for this method [note] -- **Core/TranscriptionOrchestrator.cs** (Line 45): This async method lacks 'await' operators and will run synchronously. Consider using the 'await' operator to await non-blocking API calls or drop 'async' to avoid generating state machine for this method [note] - -## ClassNeverInstantiated.Global -Found 1 occurrences. - -- **Program.cs** (Line 7): Class 'Program' is never instantiated [note] - -## ConditionalAccessQualifierIsNonNullableAccordingToAPIContract -Found 4 occurrences. - -- **Api/OpenAiCompatibleClient.cs** (Line 89): Conditional access qualifier expression is never null according to nullable reference types' annotations [warning] -- **Api/OpenAiCompatibleClient.cs** (Line 89): Conditional access qualifier expression is never null according to nullable reference types' annotations [warning] -- **Api/OpenAiCompatibleClient.cs** (Line 135): Conditional access qualifier expression is never null according to nullable reference types' annotations [warning] -- **Api/OpenAiCompatibleClient.cs** (Line 135): Conditional access qualifier expression is never null according to nullable reference types' annotations [warning] - -## ConvertClosureToMethodGroup -Found 1 occurrences. - -- **Commands/SkillCommand.cs** (Line 31): Convert into method group [note] - -## ConvertIfStatementToConditionalTernaryExpression -Found 3 occurrences. - -- **Core/DaemonService.cs** (Line 58): Convert into '?:' expression [note] -- **Commands/LatencyTestCommand.cs** (Line 76): Convert into method call with '?:' expression inside [note] -- **Commands/StatusCommand.cs** (Line 36): Convert into method call with '?:' expression inside [note] - -## ConvertToPrimaryConstructor -Found 6 occurrences. - -- **Audio/AudioRecorder.cs** (Line 16): Convert into primary constructor [note] -- **Audio/FfmpegAudioRecorder.cs** (Line 17): Convert into primary constructor [note] -- **Core/Skills/DynamicSkill.cs** (Line 16): Convert into primary constructor [note] -- **Core/TranscriptionOrchestrator.cs** (Line 23): Convert into primary constructor [note] -- **IO/ClipboardManager.cs** (Line 11): Convert into primary constructor [note] -- **IO/TextInjector.cs** (Line 13): Convert into primary constructor [note] - -## EmptyConstructor -Found 2 occurrences. - -- **Configuration/ConfigManager.cs** (Line 15): Empty constructor is redundant. The compiler generates the same by default. [warning] -- **Core/HistoryManager.cs** (Line 16): Empty constructor is redundant. The compiler generates the same by default. [warning] - -## EmptyGeneralCatchClause -Found 1 occurrences. - -- **Core/DaemonService.cs** (Line 42): Empty general catch clause suppresses any errors [warning] - -## FieldCanBeMadeReadOnly.Global -Found 1 occurrences. - -- **Core/Skills/SkillRegistry.cs** (Line 12): Field can be made readonly [note] - -## InconsistentNaming -Found 7 occurrences. - -- **Configuration/ConfigManager.cs** (Line 12): Name 'ConfigDir' does not match rule 'Instance fields (private)'. Suggested name is '_configDir'. [warning] -- **Configuration/ConfigManager.cs** (Line 13): Name 'ConfigPath' does not match rule 'Instance fields (private)'. Suggested name is '_configPath'. [warning] -- **Core/HistoryManager.cs** (Line 13): Name 'HistoryDir' does not match rule 'Instance fields (private)'. Suggested name is '_historyDir'. [warning] -- **Core/HistoryManager.cs** (Line 14): Name 'HistoryFile' does not match rule 'Instance fields (private)'. Suggested name is '_historyFile'. [warning] -- **Core/StateTracker.cs** (Line 7): Name 'StateFilePath' does not match rule 'Instance fields (private)'. Suggested name is '_stateFilePath'. [warning] -- **Audio/AudioRecorder.cs** (Line 12): Name 'WavPath' does not match rule 'Instance fields (private)'. Suggested name is '_wavPath'. [warning] -- **Audio/FfmpegAudioRecorder.cs** (Line 13): Name 'WavPath' does not match rule 'Instance fields (private)'. Suggested name is '_wavPath'. [warning] - -## MemberCanBePrivate.Global -Found 1 occurrences. - -- **Core/Constants.cs** (Line 8): Constant 'AppName' can be made private [note] - -## MergeIntoPattern -Found 1 occurrences. - -- **Core/TranscriptionOrchestrator.cs** (Line 101): Merge into pattern [note] - -## MethodHasAsyncOverload -Found 9 occurrences. - -- **Commands/HistoryCommand.cs** (Line 58): Method has async overload [note] -- **Commands/HistoryCommand.cs** (Line 59): Method has async overload [note] -- **Commands/HistoryCommand.cs** (Line 63): Method has async overload [note] -- **Commands/HistoryCommand.cs** (Line 64): Method has async overload [note] -- **Commands/HistoryCommand.cs** (Line 65): Method has async overload [note] -- **Commands/LatencyTestCommand.cs** (Line 37): Method has async overload [note] -- **Commands/OnboardCommand.cs** (Line 119): Method has async overload [note] -- **Commands/SkillCommand.cs** (Line 51): Method has async overload [note] -- **Commands/SkillCommand.cs** (Line 105): Method has async overload [note] - -## MoveVariableDeclarationInsideLoopCondition -Found 1 occurrences. - -- **Api/OpenAiCompatibleClient.cs** (Line 125): Variable 'line' can be declared inside loop condition [note] - -## NotAccessedField.Local -Found 1 occurrences. - -- **Core/DaemonService.cs** (Line 21): Field '_lockFile' is assigned but its value is never used [warning] - -## RedundantDefaultMemberInitializer -Found 2 occurrences. - -- **Api/Models/OpenAiModels.cs** (Line 20): Initializing property by default value is redundant [warning] -- **Core/Logger.cs** (Line 5): Initializing property by default value is redundant [warning] - -## RedundantExplicitParamsArrayCreation -Found 7 occurrences. - -- **Commands/OnboardCommand.cs** (Line 31): Redundant explicit collection creation in argument of 'params' parameter [note] -- **Commands/OnboardCommand.cs** (Line 44): Redundant explicit collection creation in argument of 'params' parameter [note] -- **Commands/OnboardCommand.cs** (Line 51): Redundant explicit collection creation in argument of 'params' parameter [note] -- **Commands/OnboardCommand.cs** (Line 60): Redundant explicit collection creation in argument of 'params' parameter [note] -- **Commands/OnboardCommand.cs** (Line 66): Redundant explicit collection creation in argument of 'params' parameter [note] -- **Commands/OnboardCommand.cs** (Line 93): Redundant explicit collection creation in argument of 'params' parameter [note] -- **Commands/SkillCommand.cs** (Line 81): Redundant explicit collection creation in argument of 'params' parameter [note] - -## RedundantNameQualifier -Found 35 occurrences. - -- **Api/OpenAiCompatibleClient.cs** (Line 25): Qualifier is redundant [warning] -- **Api/OpenAiCompatibleClient.cs** (Line 35): Qualifier is redundant [warning] -- **Api/OpenAiCompatibleClient.cs** (Line 60): Qualifier is redundant [warning] -- **Api/OpenAiCompatibleClient.cs** (Line 64): Qualifier is redundant [warning] -- **Api/OpenAiCompatibleClient.cs** (Line 92): Qualifier is redundant [warning] -- **Api/OpenAiCompatibleClient.cs** (Line 96): Qualifier is redundant [warning] -- **Commands/ConfigUpdaterCommand.cs** (Line 11): Qualifier is redundant [warning] -- **Commands/OnboardCommand.cs** (Line 15): Qualifier is redundant [warning] -- **Commands/OnboardCommand.cs** (Line 124): Qualifier is redundant [warning] -- **Commands/ShowCommand.cs** (Line 11): Qualifier is redundant [warning] -- **Configuration/ToakConfig.cs** (Line 15): Qualifier is redundant [warning] -- **Configuration/ToakConfig.cs** (Line 17): Qualifier is redundant [warning] -- **Core/Interfaces/Interfaces.cs** (Line 16): Qualifier is redundant [warning] -- **Core/Interfaces/Interfaces.cs** (Line 21): Qualifier is redundant [warning] -- **Core/Interfaces/Interfaces.cs** (Line 22): Qualifier is redundant [warning] -- **Core/Skills/SkillDefinition.cs** (Line 7): Qualifier is redundant [warning] -- **Core/TranscriptionOrchestrator.cs** (Line 99): Qualifier is redundant [warning] -- **IO/ClipboardManager.cs** (Line 28): Qualifier is redundant [warning] -- **IO/ClipboardManager.cs** (Line 38): Qualifier is redundant [warning] -- **IO/Notifications.cs** (Line 19): Qualifier is redundant [warning] -- ... and 15 more occurrences. - -## RedundantStringInterpolation -Found 6 occurrences. - -- **IO/TextInjector.cs** (Line 28): Redundant string interpolation [note] -- **IO/TextInjector.cs** (Line 39): Redundant string interpolation [note] -- **IO/TextInjector.cs** (Line 50): Redundant string interpolation [note] -- **IO/TextInjector.cs** (Line 78): Redundant string interpolation [note] -- **IO/TextInjector.cs** (Line 90): Redundant string interpolation [note] -- **IO/TextInjector.cs** (Line 109): Redundant string interpolation [note] - -## RedundantTypeDeclarationBody -Found 2 occurrences. - -- **Serialization/AppJsonSerializerContext.cs** (Line 24): Redundant empty class declaration body [note] -- **Serialization/AppJsonSerializerContext.cs** (Line 30): Redundant empty class declaration body [note] - -## RedundantUsingDirective -Found 62 occurrences. - -- **Api/OpenAiCompatibleClient.cs** (Line 3): Using directive is not required by the code and can be safely removed [warning] -- **Audio/AudioRecorder.cs** (Line 4): Using directive is not required by the code and can be safely removed [warning] -- **Audio/FfmpegAudioRecorder.cs** (Line 1): Using directive is not required by the code and can be safely removed [warning] -- **Audio/FfmpegAudioRecorder.cs** (Line 3): Using directive is not required by the code and can be safely removed [warning] -- **Audio/FfmpegAudioRecorder.cs** (Line 6): Using directive is not required by the code and can be safely removed [warning] -- **Commands/ConfigUpdaterCommand.cs** (Line 1): Using directive is not required by the code and can be safely removed [warning] -- **Commands/DiscardCommand.cs** (Line 1): Using directive is not required by the code and can be safely removed [warning] -- **Commands/DiscardCommand.cs** (Line 3): Using directive is not required by the code and can be safely removed [warning] -- **Commands/HistoryCommand.cs** (Line 1): Using directive is not required by the code and can be safely removed [warning] -- **Commands/HistoryCommand.cs** (Line 2): Using directive is not required by the code and can be safely removed [warning] -- **Commands/HistoryCommand.cs** (Line 3): Using directive is not required by the code and can be safely removed [warning] -- **Commands/HistoryCommand.cs** (Line 4): Using directive is not required by the code and can be safely removed [warning] -- **Commands/HistoryCommand.cs** (Line 5): Using directive is not required by the code and can be safely removed [warning] -- **Commands/HistoryCommand.cs** (Line 6): Using directive is not required by the code and can be safely removed [warning] -- **Commands/LatencyTestCommand.cs** (Line 1): Using directive is not required by the code and can be safely removed [warning] -- **Commands/LatencyTestCommand.cs** (Line 3): Using directive is not required by the code and can be safely removed [warning] -- **Commands/LatencyTestCommand.cs** (Line 4): Using directive is not required by the code and can be safely removed [warning] -- **Commands/OnboardCommand.cs** (Line 1): Using directive is not required by the code and can be safely removed [warning] -- **Commands/OnboardCommand.cs** (Line 3): Using directive is not required by the code and can be safely removed [warning] -- **Commands/OnboardCommand.cs** (Line 4): Using directive is not required by the code and can be safely removed [warning] -- ... and 42 more occurrences. - -## UnusedMember.Global -Found 2 occurrences. - -- **Core/Interfaces/Interfaces.cs** (Line 41): Method 'InjectTextAsync' is never used [note] -- **Core/Skills/ISkill.cs** (Line 6): Property 'Description' is never used [note] - -## UnusedMemberInSuper.Global -Found 3 occurrences. - -- **Core/Interfaces/Interfaces.cs** (Line 48): Only implementations of method 'ClearHistory' are used [note] -- **Core/Interfaces/Interfaces.cs** (Line 47): Only implementations of method 'LoadHistory' are used [note] -- **Core/Interfaces/Interfaces.cs** (Line 11): Only implementations of method 'SaveConfig' are used [note] - -## UnusedParameter.Global -Found 1 occurrences. - -- **Commands/SkillCommand.cs** (Line 14): Parameter 'verboseOption' is never used [note] - -## UnusedVariable -Found 2 occurrences. - -- **Commands/LatencyTestCommand.cs** (Line 60): Local variable 'refinedText' is never used [warning] -- **Commands/LatencyTestCommand.cs** (Line 54): Local variable 'transcript' is never used [warning] - -## UseAwaitUsing -Found 3 occurrences. - -- **Api/OpenAiCompatibleClient.cs** (Line 29): Use 'await using' [note] -- **Api/OpenAiCompatibleClient.cs** (Line 122): Use 'await using' [note] -- **Commands/HistoryCommand.cs** (Line 57): Use 'await using' [note] - -## UseCollectionExpression -Found 12 occurrences. - -- **Api/OpenAiCompatibleClient.cs** (Line 67): Use collection expression [note] -- **Api/OpenAiCompatibleClient.cs** (Line 100): Use collection expression [note] -- **Commands/StatsCommand.cs** (Line 33): Use collection expression [note] -- **Configuration/ToakConfig.cs** (Line 20): Use collection expression [note] -- **Core/HistoryManager.cs** (Line 43): Use collection expression [note] -- **Core/Skills/SkillRegistry.cs** (Line 73): Use collection expression [note] -- **Core/Skills/SkillRegistry.cs** (Line 83): Use collection expression [note] -- **Core/Skills/SkillRegistry.cs** (Line 94): Use collection expression [note] -- **Core/Skills/SkillRegistry.cs** (Line 108): Use collection expression [note] -- **Program.cs** (Line 13): Use collection expression [note] -- **Program.cs** (Line 15): Use collection expression [note] -- **Program.cs** (Line 87): Use collection expression [note] - -## UsingStatementResourceInitialization -Found 1 occurrences. - -- **Api/OpenAiCompatibleClient.cs** (Line 109): Initialize object properties inside the 'using' statement to ensure that the object is disposed if an exception is thrown during initialization [warning] -