diff --git a/internal/config/config.go b/internal/config/config.go index 361a469..51dad81 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -91,6 +91,7 @@ type MessageConfig struct { type MessagesConfig struct { RecordingStarted MessageConfig `toml:"recording_started"` Transcribing MessageConfig `toml:"transcribing"` + LLMProcessing MessageConfig `toml:"llm_processing"` ConfigReloaded MessageConfig `toml:"config_reloaded"` OperationCancelled MessageConfig `toml:"operation_cancelled"` RecordingAborted MessageConfig `toml:"recording_aborted"` @@ -721,6 +722,9 @@ keywords = [] # [notifications.messages.transcribing] # title = "Hyprvoice" # body = "Recording Ended... Transcribing" + # [notifications.messages.llm_processing] + # title = "Hyprvoice" + # body = "Processing..." # [notifications.messages.config_reloaded] # title = "Hyprvoice" # body = "Config Reloaded" diff --git a/internal/daemon/daemon.go b/internal/daemon/daemon.go index 37ddf35..d93799c 100644 --- a/internal/daemon/daemon.go +++ b/internal/daemon/daemon.go @@ -190,6 +190,7 @@ func (d *Daemon) toggle() { go d.notifier.Send(notify.MsgRecordingStarted) go d.monitorPipelineErrors(p) + go d.monitorPipelineNotifications(p) case pipeline.Recording: d.stopPipeline() @@ -240,3 +241,15 @@ func (d *Daemon) monitorPipelineErrors(p pipeline.Pipeline) { } } } + +func (d *Daemon) monitorPipelineNotifications(p pipeline.Pipeline) { + notifyCh := p.GetNotifyCh() + for { + select { + case mt := <-notifyCh: + d.notifier.Send(mt) + case <-d.ctx.Done(): + return + } + } +} diff --git a/internal/daemon/daemon_test.go b/internal/daemon/daemon_test.go index 8c4740e..e06f2f8 100644 --- a/internal/daemon/daemon_test.go +++ b/internal/daemon/daemon_test.go @@ -9,6 +9,7 @@ import ( "testing" "time" + "github.com/leonardotrapani/hyprvoice/internal/notify" "github.com/leonardotrapani/hyprvoice/internal/pipeline" ) @@ -480,3 +481,6 @@ func (m *MockPipeline) GetErrorCh() <-chan pipeline.PipelineError { return make(chan pipeline.PipelineError) } func (m *MockPipeline) GetActionCh() chan<- pipeline.Action { return make(chan pipeline.Action) } +func (m *MockPipeline) GetNotifyCh() <-chan notify.MessageType { + return make(chan notify.MessageType) +} diff --git a/internal/notify/message.go b/internal/notify/message.go index a46caf9..e3012fb 100644 --- a/internal/notify/message.go +++ b/internal/notify/message.go @@ -6,6 +6,7 @@ type MessageType int const ( MsgRecordingStarted MessageType = iota MsgTranscribing + MsgLLMProcessing MsgConfigReloaded MsgOperationCancelled MsgRecordingAborted @@ -25,6 +26,7 @@ type MessageDef struct { var MessageDefs = []MessageDef{ {MsgRecordingStarted, "recording_started", "Hyprvoice", "Recording Started", false}, {MsgTranscribing, "transcribing", "Hyprvoice", "Recording Ended... Transcribing", false}, + {MsgLLMProcessing, "llm_processing", "Hyprvoice", "Processing...", false}, {MsgConfigReloaded, "config_reloaded", "Hyprvoice", "Config Reloaded", false}, {MsgOperationCancelled, "operation_cancelled", "Hyprvoice", "Operation Cancelled", false}, {MsgRecordingAborted, "recording_aborted", "", "Recording Aborted", true}, diff --git a/internal/notify/notify_test.go b/internal/notify/notify_test.go index ac3f5ff..5d120e4 100644 --- a/internal/notify/notify_test.go +++ b/internal/notify/notify_test.go @@ -98,8 +98,8 @@ func TestNotifierInterface(t *testing.T) { func TestMessageDefs(t *testing.T) { // Verify MessageDefs contains expected entries - if len(MessageDefs) != 6 { - t.Errorf("Expected 6 MessageDefs, got %d", len(MessageDefs)) + if len(MessageDefs) != 7 { + t.Errorf("Expected 7 MessageDefs, got %d", len(MessageDefs)) } // Verify each has required fields diff --git a/internal/pipeline/pipeline.go b/internal/pipeline/pipeline.go index 2a4cb9c..eb2d50a 100644 --- a/internal/pipeline/pipeline.go +++ b/internal/pipeline/pipeline.go @@ -9,6 +9,7 @@ import ( "github.com/leonardotrapani/hyprvoice/internal/config" "github.com/leonardotrapani/hyprvoice/internal/injection" "github.com/leonardotrapani/hyprvoice/internal/llm" + "github.com/leonardotrapani/hyprvoice/internal/notify" "github.com/leonardotrapani/hyprvoice/internal/recording" "github.com/leonardotrapani/hyprvoice/internal/transcriber" ) @@ -41,12 +42,14 @@ type Pipeline interface { Status() Status GetActionCh() chan<- Action GetErrorCh() <-chan PipelineError + GetNotifyCh() <-chan notify.MessageType } type pipeline struct { status Status actionCh chan Action errorCh chan PipelineError + notifyCh chan notify.MessageType config *config.Config mu sync.RWMutex @@ -61,6 +64,7 @@ func New(cfg *config.Config) Pipeline { return &pipeline{ actionCh: make(chan Action, 1), errorCh: make(chan PipelineError, 10), + notifyCh: make(chan notify.MessageType, 10), config: cfg, } } @@ -189,6 +193,12 @@ func (p *pipeline) GetErrorCh() <-chan PipelineError { return p.errorCh } +func (p *pipeline) GetNotifyCh() <-chan notify.MessageType { + p.mu.RLock() + defer p.mu.RUnlock() + return p.notifyCh +} + func (p *pipeline) sendError(title, message string, err error) { pipelineErr := PipelineError{ Title: title, @@ -203,6 +213,14 @@ func (p *pipeline) sendError(title, message string, err error) { } } +func (p *pipeline) sendNotify(mt notify.MessageType) { + select { + case p.notifyCh <- mt: + default: + log.Printf("Pipeline: Notify channel full, dropping notification") + } +} + func (p *pipeline) handleInjectAction(ctx context.Context, recorder *recording.Recorder, t transcriber.Transcriber) { status := p.Status() @@ -232,6 +250,7 @@ func (p *pipeline) handleInjectAction(ctx context.Context, recorder *recording.R textToInject := transcriptionText if p.config.IsLLMEnabled() { p.setStatus(Processing) + p.sendNotify(notify.MsgLLMProcessing) log.Printf("Pipeline: LLM post-processing enabled, processing text") llmCfg := p.config.ToLLMConfig() diff --git a/progress.txt b/progress.txt index d624fbe..105bbc4 100644 --- a/progress.txt +++ b/progress.txt @@ -189,3 +189,19 @@ Key decisions: - 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]` diff --git a/tasks/prd.jsonc b/tasks/prd.jsonc index 9a17782..8a80751 100644 --- a/tasks/prd.jsonc +++ b/tasks/prd.jsonc @@ -216,7 +216,7 @@ "Configurable in config", "Typecheck passes" ], - "passes": false + "passes": true }, { "title": "Update README documentation",