Skip to content

Commit 952dd39

Browse files
committed
Polish ClawWatch speech, status bubbles, icon, and fix UI synchronization bugs
1 parent 0df915d commit 952dd39

File tree

9 files changed

+883
-199
lines changed

9 files changed

+883
-199
lines changed

app/src/main/AndroidManifest.xml

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,8 @@
1414
<application
1515
android:allowBackup="false"
1616
android:label="ClawWatch"
17-
android:icon="@mipmap/ic_launcher"
17+
android:icon="@drawable/ic_clawwatch_launcher"
18+
android:roundIcon="@drawable/ic_clawwatch_launcher"
1819
android:extractNativeLibs="true"
1920
android:networkSecurityConfig="@xml/network_security_config"
2021
android:theme="@style/Theme.AppCompat.NoActionBar">

app/src/main/java/com/thinkoff/clawwatch/ClawRunner.kt

Lines changed: 66 additions & 61 deletions
Original file line numberDiff line numberDiff line change
@@ -51,11 +51,16 @@ class ClawRunner(private val context: Context) {
5151
)
5252

5353
private const val DEFAULT_MODEL = "claude-opus-4-6"
54-
private const val DEFAULT_MAX_TOKENS = 150
55-
private const val DEFAULT_SYSTEM_PROMPT =
54+
private const val LEGACY_DEFAULT_SYSTEM_PROMPT =
5655
"You are ClawWatch, a smart and relaxed voice presence on a watch. " +
5756
"Respond in 1-3 short sentences maximum. No markdown, no lists, no bullet points. " +
5857
"Use plain spoken language. Be natural, helpful, and a little playful when it fits, without sounding formal, salesy, or robotic."
58+
private const val DEFAULT_MAX_TOKENS = 150
59+
private const val DEFAULT_SYSTEM_PROMPT =
60+
"You are ClawWatch, an intelligent and relaxed companion living on a watch. " +
61+
"Reply in 1 to 3 short spoken sentences. No markdown, no bullet points, no stiff assistant phrasing. " +
62+
"Sound natural, grounded, and warm. You can help with the wearer's body, their team rooms, timers, and live information. " +
63+
"Do not keep reminding people that you are a smartwatch unless they ask."
5964

6065
private const val MAX_CONTEXT_MESSAGES = 10
6166
private const val MAX_CONTEXT_CHARS_PER_MESSAGE = 600
@@ -95,22 +100,13 @@ class ClawRunner(private val context: Context) {
95100
fun saveMaxTokens(n: Int) = prefs.edit().putInt(PREF_MAX_TOKENS, n).apply()
96101
fun saveRagMode(mode: String) = prefs.edit().putString(PREF_RAG_MODE, mode).apply()
97102

98-
private fun getStringSetting(key: String, default: String? = null): String? {
99-
val secure = prefs.getString(key, null)
100-
if (!secure.isNullOrBlank()) return secure
101-
val legacy = context
102-
.getSharedPreferences("clawwatch_prefs", Context.MODE_PRIVATE)
103-
.getString(key, null)
104-
return legacy ?: default
105-
}
106-
107103
fun hasApiKey(): Boolean = prefs.getString(PREF_API_KEY, null)?.isNotBlank() == true
108104
private fun getApiKey(): String? = prefs.getString(PREF_API_KEY, null)
109105
private fun getBraveKey(): String? = prefs.getString(PREF_BRAVE_KEY, null)
110106
private fun getTavilyKey(): String? = prefs.getString(PREF_TAVILY_KEY, null)
111-
private fun getAntFarmKey(): String? = getStringSetting(PREF_ANTFARM_KEY)
107+
private fun getAntFarmKey(): String? = prefs.getString(PREF_ANTFARM_KEY, null)
112108
private fun getAntFarmRooms(): List<String> =
113-
(getStringSetting(PREF_ANTFARM_ROOMS, DEFAULT_FAMILY_ROOMS) ?: DEFAULT_FAMILY_ROOMS)
109+
(prefs.getString(PREF_ANTFARM_ROOMS, DEFAULT_FAMILY_ROOMS) ?: DEFAULT_FAMILY_ROOMS)
114110
.split(',')
115111
.map { it.trim() }
116112
.filter { it.isNotBlank() }
@@ -133,9 +129,17 @@ class ClawRunner(private val context: Context) {
133129
context.assets.open(assetName).use { it.copyTo(configFile.outputStream()) }
134130
}
135131
buildCaBundle()
132+
migrateDefaultPromptIfNeeded()
136133
Log.i(TAG, "ClawRunner ready, home=${homeDir.absolutePath}, rag=${getRagMode()}")
137134
}
138135

136+
private fun migrateDefaultPromptIfNeeded() {
137+
val current = prefs.getString(PREF_SYSTEM_PROMPT, null)?.trim()
138+
if (current.isNullOrEmpty() || current == LEGACY_DEFAULT_SYSTEM_PROMPT) {
139+
prefs.edit().putString(PREF_SYSTEM_PROMPT, DEFAULT_SYSTEM_PROMPT).apply()
140+
}
141+
}
142+
139143
private fun buildCaBundle() {
140144
if (caBundleFile.exists()) return
141145
val certDirs = listOf("/apex/com.android.conscrypt/cacerts", "/system/etc/security/cacerts")
@@ -436,58 +440,59 @@ class ClawRunner(private val context: Context) {
436440
)
437441
}
438442

439-
suspend fun postMessageToRoom(room: String, message: String): Result<String> = withContext(Dispatchers.IO) {
440-
val antFarmKey = getAntFarmKey()
441-
?: return@withContext Result.failure(
442-
RuntimeException("room access key is missing")
443-
)
444-
445-
val targetRoom = room.trim()
446-
.ifBlank { getAntFarmRooms().firstOrNull().orEmpty() }
447-
.ifBlank {
448-
return@withContext Result.failure(RuntimeException("room name is missing"))
449-
}
450-
451-
val cleanBody = message
452-
.trim()
453-
.replace(Regex("\\s+"), " ")
454-
.take(500)
455-
if (cleanBody.isBlank()) {
456-
return@withContext Result.failure(RuntimeException("message was empty"))
457-
}
443+
suspend fun postRoomMessage(message: String, requestedRoom: String? = null): Result<String> =
444+
withContext(Dispatchers.IO) {
445+
val antFarmKey = getAntFarmKey()
446+
?: return@withContext Result.failure(
447+
RuntimeException("I don't have room posting access configured yet.")
448+
)
449+
val room = resolveTargetRoom(requestedRoom)
450+
?: return@withContext Result.failure(
451+
RuntimeException("I couldn't find a room to post to.")
452+
)
458453

459-
return@withContext try {
460-
val encodedRoom = URLEncoder.encode(targetRoom, "UTF-8")
461-
val url = URL("https://antfarm.world/api/v1/rooms/$encodedRoom/messages")
462-
val conn = url.openConnection() as HttpURLConnection
463-
conn.requestMethod = "POST"
464-
conn.setRequestProperty("Accept", "application/json")
465-
conn.setRequestProperty("Authorization", "Bearer $antFarmKey")
466-
conn.setRequestProperty("Content-Type", "application/json")
467-
conn.connectTimeout = 10_000
468-
conn.readTimeout = 10_000
469-
conn.doOutput = true
470-
val payload = JSONObject().apply { put("body", cleanBody) }.toString()
471-
OutputStreamWriter(conn.outputStream).use { it.write(payload) }
454+
return@withContext try {
455+
val encodedRoom = URLEncoder.encode(room, "UTF-8")
456+
val url = URL("https://antfarm.world/api/v1/rooms/$encodedRoom/messages")
457+
val conn = url.openConnection() as HttpURLConnection
458+
conn.requestMethod = "POST"
459+
conn.setRequestProperty("Content-Type", "application/json")
460+
conn.setRequestProperty("Accept", "application/json")
461+
conn.setRequestProperty("X-API-Key", antFarmKey)
462+
conn.connectTimeout = 10_000
463+
conn.readTimeout = 10_000
464+
conn.doOutput = true
465+
466+
val payload = JSONObject().apply {
467+
put("body", message.trim())
468+
}.toString()
469+
OutputStreamWriter(conn.outputStream).use { it.write(payload) }
470+
471+
val code = conn.responseCode
472+
if (code !in 200..299) {
473+
val errorBody = conn.errorStream?.bufferedReader()?.use { it.readText() }
474+
Log.w(TAG, "Ant Farm room post failed for $room: $code $errorBody")
475+
return@withContext Result.failure(
476+
RuntimeException("I couldn't post to $room right now.")
477+
)
478+
}
472479

473-
val code = conn.responseCode
474-
val responseText = if (code in 200..299) {
475-
conn.inputStream.bufferedReader().readText()
476-
} else {
477-
conn.errorStream?.bufferedReader()?.readText() ?: "HTTP $code"
480+
Result.success(room)
481+
} catch (e: Exception) {
482+
Log.w(TAG, "Ant Farm room post failed for $room: ${e.message}")
483+
Result.failure(RuntimeException("I couldn't post to $room right now."))
478484
}
485+
}
479486

480-
if (code !in 200..299) {
481-
Log.w(TAG, "Ant Farm room post failed for $targetRoom: $code $responseText")
482-
Result.failure(RuntimeException("room post failed ($code)"))
483-
} else {
484-
Log.i(TAG, "Posted room message to $targetRoom")
485-
Result.success("Posted to $targetRoom.")
486-
}
487-
} catch (e: Exception) {
488-
Log.w(TAG, "Ant Farm room post failed for $targetRoom: ${e.message}")
489-
Result.failure(RuntimeException("network error while posting to room"))
487+
private fun resolveTargetRoom(requestedRoom: String?): String? {
488+
val configuredRooms = getAntFarmRooms()
489+
if (requestedRoom.isNullOrBlank()) {
490+
return configuredRooms.firstOrNull()
490491
}
492+
493+
val normalized = requestedRoom.trim().lowercase()
494+
return configuredRooms.firstOrNull { it.equals(normalized, ignoreCase = true) }
495+
?: requestedRoom.trim()
491496
}
492497

493498
// ── Mode 1: Direct (no RAG) ───────────────────────────────────────────────
@@ -521,7 +526,7 @@ class ClawRunner(private val context: Context) {
521526
val conn = url.openConnection() as HttpURLConnection
522527
conn.requestMethod = "GET"
523528
conn.setRequestProperty("Accept", "application/json")
524-
conn.setRequestProperty("Authorization", "Bearer $apiKey")
529+
conn.setRequestProperty("X-API-Key", apiKey)
525530
conn.connectTimeout = 10_000
526531
conn.readTimeout = 10_000
527532

0 commit comments

Comments
 (0)