# Ralph Progress Log Started: Sat Jan 31 08:30:51 PM CET 2026 --- ## Task 1: Create Provider interface and registry - COMPLETE Created internal/provider package with: - Provider interface with all required methods - ProviderConfig struct for API key storage - 4 provider implementations: OpenAI, Groq, Mistral, ElevenLabs - Registry with GetProvider(), ListProviders(), ListProvidersWithLLM(), ListProvidersWithTranscription() - Comprehensive tests (all passing) Key decisions: - OpenAI and Groq support both transcription + LLM - Mistral and ElevenLabs are transcription-only - ValidateAPIKey checks prefix for OpenAI (sk-) and Groq (gsk_), accepts any non-empty for others ## Task 2: Refactor config to unified provider structure - COMPLETE Added to internal/config/config.go: - `Providers map[string]ProviderConfig` for centralized API key storage - `Keywords []string` at config root level - `LLMConfig` with Enabled, Provider, Model, PostProcessing, CustomPrompt - `LLMPostProcessingConfig` with RemoveStutters, AddPunctuation, FixGrammar, RemoveFillerWords (all default true) - `LLMCustomPromptConfig` with Enabled, Prompt - `LLMAdapterConfig` struct for passing to LLM adapters - `ToLLMConfig()` method - `IsLLMEnabled()` helper - `resolveAPIKeyForProvider()` - unified API key resolution: providers map -> legacy transcription.api_key -> env var - `resolveAPIKeyForLLMProvider()` - same for LLM - `migrateTranscriptionAPIKey()` - auto-migrates old config format - `applyLLMDefaults()` - sets post-processing options to true if all are zero Migration: - Old configs with `transcription.api_key` auto-migrate to `providers` map on Load() - Logs warning: "Run 'hyprvoice configure' to update config format" - Both old and new config formats work (backward compatible) Validation: - LLM validation only runs when `llm.enabled = true` - Checks provider is openai or groq - Checks API key is available for LLM provider Key decisions: - API key resolution order: providers.X.api_key -> transcription.api_key -> ENV_VAR - LLM provider names are "openai" and "groq" (not "groq-transcription") - PostProcessing defaults to all true only if ALL options are false (zero values) - Keywords at root level (global), used by both transcription and LLM ## Task 3: Create LLM adapter interface and implementations - COMPLETE Created internal/llm package with: - `Adapter` interface: `Process(ctx, text) (string, error)` - `Config` struct mirroring config.LLMAdapterConfig - `prompt.go` with `BuildSystemPrompt(opts, keywords)` and `BuildUserPrompt(text, customPrompt)` - `OpenAIAdapter` using go-openai chat completions API - `GroqAdapter` using Groq's OpenAI-compatible API (baseURL override) - `NewAdapter(config)` factory function Key decisions: - Low temperature (0.3) for consistent text cleanup - Default models: gpt-4o-mini (OpenAI), llama-3.3-70b-versatile (Groq) - System prompt builds dynamically based on enabled options - Keywords included in system prompt for correct spelling hints - Custom prompt prepended to user prompt if enabled ## Task 4: Integrate LLM phase into pipeline - COMPLETE Updated internal/pipeline/pipeline.go: - Added `Processing` status for LLM post-processing phase - After transcription, checks `config.IsLLMEnabled()` before LLM processing - Creates LLM adapter using config.ToLLMConfig() - Processes text with adapter, uses result for injection - Graceful fallback: logs warning and uses raw transcription text on any error Key decisions: - LLM processing happens between transcription and injection - Adapter creation and processing errors are logged but don't fail the pipeline - Sets status to Processing during LLM phase, then back to Injecting ## Task 5: Pass keywords to transcription adapters - COMPLETE Added Keywords support to transcription adapters: - Added `Keywords []string` to transcriber.Config struct - Updated `ToTranscriberConfig()` to pass keywords from config - OpenAI adapter uses keywords in `Prompt` field (initial_prompt parameter) - Groq transcription adapter uses keywords in `Prompt` field - Groq translation adapter uses keywords in `Prompt` field - Mistral and ElevenLabs adapters ignore keywords (APIs don't support initial_prompt) Key decisions: - Keywords joined with ", " to form a single string for the Prompt field - Whisper uses this as "initial_prompt" to help with spelling/terminology - Only added to adapters that clearly support it (OpenAI/Groq via go-openai lib) ## Task 6: Add TUI dependencies and base components - COMPLETE Added Charmbracelet TUI stack: - bubbletea v1.3.10, lipgloss v1.1.0, huh v0.8.0 - Created internal/tui package Files created: - `internal/tui/theme.go` - color palette (purple primary, cyan secondary, status colors) - `internal/tui/styles.go` - lipgloss styles (header, label, success, error, muted, highlight, selected, box styles) - `Logo()` function for ASCII branding Key decisions: - Purple (#7C3AED) as primary accent, matches hyprvoice "voice" theme - Dark slate backgrounds for terminal aesthetics - Box styles with rounded borders for form containers ## Task 7: Create TUI configure - fresh install flow - COMPLETE Created internal/tui/configure.go with full TUI wizard: - `Run(existingConfig)` entry point returning ConfigureResult - `runFreshInstall()` - linear flow through all configuration steps - `selectProviders()` - multi-select for OpenAI, Groq, Mistral, ElevenLabs - `inputAPIKey()` - password-masked input with validation per provider - `configureTranscription()` - provider dropdown (only configured+capable), model dropdown, language input - `configureLLM()` - enable confirm (defaults YES, labeled "Recommended"), provider, model, post-processing toggles, custom prompt - `inputKeywords()` - comma-separated input - `selectBackends()` - multi-select with descriptions - `configureNotifications()` - enable toggle - `showSummary()` - displays all config, confirm button - `getTheme()` - applies hyprvoice color scheme to huh forms Key decisions: - Linear flow for fresh installs, all steps required - Transcription providers mapped: groq -> groq-transcription + groq-translation options - LLM enabled by default, "Yes (Recommended)" as affirmative text - Post-processing options all default to true - Uses huh library forms with custom theme matching styles.go colors ## Task 8: Create TUI configure - edit existing flow - COMPLETE Added edit flow for existing configs in internal/tui/configure.go: - `hasUserChanges()` detects if config has been modified (providers configured or legacy api_key set) - `runEditExisting()` - section-based edit flow instead of full wizard - `selectSections()` - multi-select for Providers, Transcription, LLM, Keywords, Injection, Notifications, Full Setup - `editProviders()` - add/update API keys for selected providers - `editTranscription()` - configure speech-to-text with smart provider detection - `editLLM()` - configure post-processing with smart provider detection - `getUnconfiguredTranscriptionOptions()` / `getUnconfiguredLLMOptions()` - show options for providers without keys - `ensureProviderConfigured()` - prompts for API key when user selects unconfigured provider Key decisions: - "Full Setup" option runs the fresh install flow - Configured providers show "(configured)" label in providers section - Unconfigured providers show "(needs API key)" in transcription/LLM sections - When user picks unconfigured provider, immediately prompts for API key - Unedited sections preserved - only touched sections are modified - Config struct passed by reference, changes accumulate ## Task 9: Replace old configure with TUI - COMPLETE Replaced old interactive config in cmd/hyprvoice/main.go: - `configureCmd` now calls `tui.Run()` instead of `runInteractiveConfig()` - Removed all old functions: `runInteractiveConfig`, `maskAPIKey`, `formatBackends`, old `saveConfig` - New `saveConfig()` writes proper TOML with new structure: - `keywords = [...]` at top (before any tables) - `[providers.X]` sections with `api_key` - `[llm]` with `[llm.post_processing]` and `[llm.custom_prompt]` subsections - No more `transcription.api_key` in saved configs - Added `showNextSteps()` helper for post-save instructions - Added `runConfigure()` that wraps TUI flow with validation and save Key decisions: - Keywords written before any TOML table definitions (TOML requirement) - Config saved only if user confirms in TUI summary - Validation runs before save, errors displayed cleanly - Next steps shown after successful save ## Task 10: Update default config template - COMPLETE Updated SaveDefaultConfig() in internal/config/config.go: - Added `keywords = []` at top level (before any TOML tables) - Added `[providers.openai]` and `[providers.groq]` sections with api_key - Added `[llm]` section with enabled = true, provider = "openai", model = "gpt-4o-mini" - Added `[llm.post_processing]` with all 4 options = true - Added `[llm.custom_prompt]` with enabled = false - Added MIGRATION NOTE in header about old format upgrade - Reorganized with clear section headers (box-drawing chars) - Removed `transcription.api_key` from default (uses providers map now) - Simplified and consolidated reference docs at bottom Key decisions: - LLM enabled by default with OpenAI gpt-4o-mini (best cost/quality) - Providers section at top for visibility - Keywords before any table definitions (TOML syntax requirement) - Concise comments, full reference at bottom ## Task 11: Add LLM processing notification - COMPLETE Added notification when LLM post-processing starts: - Added `MsgLLMProcessing` to `notify/message.go` (default: "Hyprvoice", "Processing...") - Added `LLMProcessing` field to `MessagesConfig` in config.go (toml: `llm_processing`) - Added notification channel to pipeline (`GetNotifyCh()` method) - Pipeline sends `MsgLLMProcessing` when entering Processing status - Daemon monitors `notifyCh` via `monitorPipelineNotifications` goroutine - Updated default config template with `llm_processing` message example - Fixed tests: MockPipeline implements `GetNotifyCh`, notify test expects 7 MessageDefs Key decisions: - Notification channel approach (vs direct notifier access) keeps pipeline decoupled - Notification sent at same time status changes to Processing - Configurable like all other notifications via `[notifications.messages.llm_processing]`