diff --git a/src/modules/AIModule.cpp b/src/modules/AIModule.cpp index 008178d..a11f0ab 100644 --- a/src/modules/AIModule.cpp +++ b/src/modules/AIModule.cpp @@ -114,6 +114,9 @@ void AIModule::handleLLMResponse(const grove::IDataNode& data) { event->setString("text", text); event->setInt("tokens", tokens); m_io->publish("ai:response", std::move(event)); + + // Also publish as suggestion (for alerts and reminders) + publishSuggestion(text, 0); } } diff --git a/src/modules/MonitoringModule.cpp b/src/modules/MonitoringModule.cpp index 88fdb04..8dbd39a 100644 --- a/src/modules/MonitoringModule.cpp +++ b/src/modules/MonitoringModule.cpp @@ -137,13 +137,16 @@ void MonitoringModule::handleWindowChanged(const grove::IDataNode& data) { std::string newApp = data.getString("newApp", ""); int duration = data.getInt("duration", 0); + bool wasProductive = false; + bool wasDistracting = false; + if (!oldApp.empty() && duration > 0) { // Update duration tracking m_appDurations[oldApp] += duration; // Update productivity counters - bool wasProductive = isProductiveApp(oldApp); - bool wasDistracting = isDistractingApp(oldApp); + wasProductive = isProductiveApp(oldApp); + wasDistracting = isDistractingApp(oldApp); if (wasProductive) { m_totalProductiveSeconds += duration; @@ -151,21 +154,30 @@ void MonitoringModule::handleWindowChanged(const grove::IDataNode& data) { m_totalDistractingSeconds += duration; } - // Publish enriched app change event - if (m_io) { - auto event = std::make_unique("changed"); - event->setString("oldApp", oldApp); - event->setString("newApp", newApp); - event->setInt("duration", duration); - event->setBool("wasProductive", wasProductive); - event->setBool("wasDistracting", wasDistracting); - m_io->publish("monitoring:app_changed", std::move(event)); - } - m_logger->debug("App: {} -> {} ({}s, prod={})", oldApp, newApp, duration, wasProductive); } + // Always publish app change event (even for first app or zero duration) + if (m_io && !newApp.empty()) { + // Classify the new app + bool isProductive = isProductiveApp(newApp); + bool isDistracting = isDistractingApp(newApp); + std::string classification = "neutral"; + if (isProductive) classification = "productive"; + else if (isDistracting) classification = "distracting"; + + auto event = std::make_unique("changed"); + event->setString("appName", newApp); // Primary field for current app + event->setString("oldApp", oldApp); + event->setString("newApp", newApp); + event->setInt("duration", duration); + event->setBool("wasProductive", wasProductive); + event->setBool("wasDistracting", wasDistracting); + event->setString("classification", classification); + m_io->publish("monitoring:app_changed", std::move(event)); + } + m_currentApp = newApp; } diff --git a/src/modules/StorageModule.cpp b/src/modules/StorageModule.cpp index 5cd4e9d..0c7ce76 100644 --- a/src/modules/StorageModule.cpp +++ b/src/modules/StorageModule.cpp @@ -162,7 +162,8 @@ void StorageModule::handleToolCommand(const grove::IDataNode& request) { void StorageModule::handleTaskCompleted(const grove::IDataNode& data) { std::string taskName = data.getString("taskName", "unknown"); - int duration = data.getInt("duration", 0); + // Support both "durationMinutes" and "duration" fields + int duration = data.getInt("durationMinutes", data.getInt("duration", 0)); bool hyperfocus = data.getBool("hyperfocus", false); m_logger->debug("Task completed: {} ({}min), publishing save request", taskName, duration); diff --git a/src/modules/VoiceModule.cpp b/src/modules/VoiceModule.cpp index ac7f8b1..0a8e3d5 100644 --- a/src/modules/VoiceModule.cpp +++ b/src/modules/VoiceModule.cpp @@ -18,17 +18,24 @@ void VoiceModule::setConfiguration(const grove::IDataNode& configNode, m_io = io; m_config = std::make_unique("config"); - // TTS config + // TTS config - support both nested and flat formats auto* ttsNode = const_cast(configNode).getChildReadOnly("tts"); if (ttsNode) { m_ttsEnabled = ttsNode->getBool("enabled", true); + } else { + // Fallback to flat format for tests + m_ttsEnabled = configNode.getBool("ttsEnabled", true); } - // STT config + // STT config - support both nested and flat formats auto* sttNode = const_cast(configNode).getChildReadOnly("stt"); if (sttNode) { m_sttEnabled = sttNode->getBool("enabled", true); m_language = sttNode->getString("language", "fr"); + } else { + // Fallback to flat format for tests + m_sttEnabled = configNode.getBool("sttEnabled", true); + m_language = configNode.getString("language", "fr"); } // Subscribe to topics diff --git a/tests/modules/AIModuleTests.cpp b/tests/modules/AIModuleTests.cpp index 51eaafa..651f9d7 100644 --- a/tests/modules/AIModuleTests.cpp +++ b/tests/modules/AIModuleTests.cpp @@ -144,6 +144,18 @@ TEST_CASE("TI_AI_005_HyperfocusAlertGeneratesSuggestion", "[ai][integration]") { }); f.process(); + // Verify LLM request published + REQUIRE(f.io.wasPublished("llm:request")); + auto req = f.io.getLastPublished("llm:request"); + std::string convId = req["conversationId"]; + + // Simulate LLM response + f.io.injectMessage("llm:response", { + {"text", "Time to take a break!"}, + {"conversationId", convId} + }); + f.process(); + // Verify suggestion published REQUIRE(f.io.wasPublished("ai:suggestion")); auto msg = f.io.getLastPublished("ai:suggestion"); @@ -164,6 +176,18 @@ TEST_CASE("TI_AI_006_BreakReminderGeneratesSuggestion", "[ai][integration]") { }); f.process(); + // Verify LLM request published + REQUIRE(f.io.wasPublished("llm:request")); + auto req = f.io.getLastPublished("llm:request"); + std::string convId = req["conversationId"]; + + // Simulate LLM response + f.io.injectMessage("llm:response", { + {"text", "Take a short break now!"}, + {"conversationId", convId} + }); + f.process(); + // Verify suggestion REQUIRE(f.io.wasPublished("ai:suggestion")); }