@@ -75,6 +75,10 @@ class MainActivity : AppCompatActivity() {
7575 val totalSeconds : Int ,
7676 val spokenDuration : String
7777 )
78+ private data class RoomMessageCommand (
79+ val room : String ,
80+ val body : String
81+ )
7882 private data class PendingVitalsCommand (
7983 val type : LocalCommandType ,
8084 val token : Int
@@ -479,6 +483,10 @@ class MainActivity : AppCompatActivity() {
479483 }
480484
481485 private fun handleLocalCommand (prompt : String , token : Int ): Boolean {
486+ parseRoomMessageCommand(prompt)?.let { command ->
487+ launchRoomMessageCommand(command, token)
488+ return true
489+ }
482490 parseVitalsCommand(prompt)?.let { command ->
483491 launchVitalsCommand(command, token)
484492 return true
@@ -491,6 +499,54 @@ class MainActivity : AppCompatActivity() {
491499 return launchSystemTimer(timer, token)
492500 }
493501
502+ private fun parseRoomMessageCommand (prompt : String ): RoomMessageCommand ? {
503+ val raw = prompt.trim()
504+ if (raw.isBlank()) return null
505+ val normalized = raw.lowercase().replace(Regex (" \\ s+" ), " " )
506+ val likelyPostIntent =
507+ normalized.contains(" send" ) ||
508+ normalized.contains(" post" ) ||
509+ normalized.contains(" write" ) ||
510+ normalized.contains(" tell" )
511+ if (! likelyPostIntent || ! normalized.contains(" room" )) return null
512+
513+ val toRoomPattern = Regex (
514+ """ \b(?:send|post|write|tell)\b\s+(.+?)\s+\bto\s+(?:the\s+)?room\s+([a-zA-Z0-9._-]+)\b\s*$""" ,
515+ RegexOption .IGNORE_CASE
516+ )
517+ toRoomPattern.find(raw)?.let { match ->
518+ val body = cleanRoomMessageBody(match.groupValues[1 ])
519+ val room = match.groupValues[2 ].trim()
520+ if (body.isNotBlank() && room.isNotBlank()) {
521+ return RoomMessageCommand (room = room, body = body)
522+ }
523+ }
524+
525+ val inRoomPattern = Regex (
526+ """ \b(?:send|post|write|tell)\b\s+(?:a\s+)?(?:message\s+)?(?:to|in)\s+(?:the\s+)?room\s+([a-zA-Z0-9._-]+)\b(?:\s*(?:saying|that|:)\s*|\s+)(.+)$""" ,
527+ RegexOption .IGNORE_CASE
528+ )
529+ inRoomPattern.find(raw)?.let { match ->
530+ val room = match.groupValues[1 ].trim()
531+ val body = cleanRoomMessageBody(match.groupValues[2 ])
532+ if (body.isNotBlank() && room.isNotBlank()) {
533+ return RoomMessageCommand (room = room, body = body)
534+ }
535+ }
536+
537+ return null
538+ }
539+
540+ private fun cleanRoomMessageBody (raw : String ): String {
541+ return raw
542+ .trim()
543+ .trim(' "' , ' \' ' , ' “' , ' ”' )
544+ .replace(Regex (" ^message\\ s+" , RegexOption .IGNORE_CASE ), " " )
545+ .replace(Regex (" \\ s+" ), " " )
546+ .trim()
547+ .take(500 )
548+ }
549+
494550 private fun parseVitalsCommand (prompt : String ): LocalCommandType ? {
495551 val normalized = prompt.lowercase()
496552 if (
@@ -687,6 +743,31 @@ class MainActivity : AppCompatActivity() {
687743 }
688744 }
689745
746+ private fun launchRoomMessageCommand (command : RoomMessageCommand , token : Int ) {
747+ queryJob?.cancel()
748+ setState(State .THINKING )
749+ setStatus(" Sending message…" )
750+ queryJob = lifecycleScope.launch {
751+ val result = clawRunner.postMessageToRoom(
752+ room = command.room,
753+ message = command.body
754+ )
755+ if (token != interactionToken) return @launch
756+ result.fold(
757+ onSuccess = { response ->
758+ binding.responseText.text = response
759+ speakLocalResponse(response, token)
760+ },
761+ onFailure = { err ->
762+ val reason = err.message?.take(120 ) ? : " unknown error"
763+ val response = " I couldn't send that room message. $reason "
764+ binding.responseText.text = response
765+ speakLocalResponse(response, token)
766+ }
767+ )
768+ }
769+ }
770+
690771 private fun buildHeartRateSummary (snapshot : VitalsReader .Snapshot ): String {
691772 val bpm = snapshot.heartRateBpm
692773 ? : return " I couldn't get a clean pulse reading just now. Keep the watch snug and hold still for a moment, then ask again."
0 commit comments