git.iko.soy/opencode-android
on master help sign in
UX/UI: checkmark for sent status, SSE banner on sessions, chevron on session rows, adaptive status bar

Bug fixes: reset generating on SelectSession, reset generating on loadMessages failure, clear streaming on SendMessage failure

commit3c09796fffb8d8c218fc81a9b79f6ddd7752b8d0 authoriko <mail@iko.soy> dateWed 8 Jul 2026, 21:51 (3 months ago) parent9ba5eec
4 files changed+51−2
app/src/main/java/soy/iko/opencode/MainActivity.kt+7−1 app/src/main/java/soy/iko/opencode/core/OpencodeCore.kt+4 app/src/main/java/soy/iko/opencode/ui/components/MessageBubble.kt+2−1 app/src/main/java/soy/iko/opencode/ui/screens/SessionsScreen.kt+38
app/src/main/java/soy/iko/opencode/MainActivity.kt+7−1
@@ -2,6 +2,7 @@ package soy.iko.opencode
22
33import android.os.Bundle
44import androidx.activity.ComponentActivity
5+import androidx.activity.SystemBarStyle
56import androidx.activity.compose.setContent
67import androidx.activity.enableEdgeToEdge
78import androidx.compose.runtime.getValue
@@ -12,7 +13,12 @@ import soy.iko.opencode.ui.theme.OpencodeTheme
1213
1314class MainActivity : ComponentActivity() {
1415 override fun onCreate(savedInstanceState: Bundle?) {
15− enableEdgeToEdge()
16+ enableEdgeToEdge(
17+ statusBarStyle = SystemBarStyle.auto(
18+ lightScrim = 0xFF0F1115.toInt(),
19+ darkScrim = 0xFF0F1115.toInt(),
20+ ),
21+ )
1622 super.onCreate(savedInstanceState)
1723 setContent {
1824 OpencodeTheme {
app/src/main/java/soy/iko/opencode/core/OpencodeCore.kt+4
@@ -79,6 +79,7 @@ class OpencodeCore(
7979 messages.clear()
8080 screen = Screen.Chat
8181 loading = true
82+ generating = false
8283 error = null
8384 emit()
8485 scope.launch { loadMessages(event.id) }
@@ -189,12 +190,14 @@ class OpencodeCore(
189190 } else {
190191 messages.firstOrNull { it.id == user.id }?.status = MessageStatus.Failed
191192 generating = false
193+ messages.forEach { it.streaming = false }
192194 error = "Server returned status ${resp.code}"
193195 }
194196 },
195197 onFailure = { e ->
196198 messages.firstOrNull { it.id == user.id }?.status = MessageStatus.Failed
197199 generating = false
200+ messages.forEach { it.streaming = false }
198201 error = "Request failed: ${e.message}"
199202 },
200203 )
@@ -294,6 +297,7 @@ class OpencodeCore(
294297 onFailure = { e ->
295298 if (!isCurrent(sessionId)) { loading = false; return }
296299 loading = false
300+ generating = false
297301 error = "Request failed: ${e.message}"
298302 emit()
299303 },
app/src/main/java/soy/iko/opencode/ui/components/MessageBubble.kt+2−1
@@ -17,6 +17,7 @@ import androidx.compose.foundation.layout.widthIn
1717import androidx.compose.foundation.shape.RoundedCornerShape
1818import androidx.compose.material.icons.Icons
1919import androidx.compose.material.icons.filled.Build
20+import androidx.compose.material.icons.filled.Check
2021import androidx.compose.material.icons.filled.ErrorOutline
2122import androidx.compose.material.icons.filled.ExpandLess
2223import androidx.compose.material.icons.filled.ExpandMore
@@ -201,7 +202,7 @@ private fun StatusLine(status: MessageStatus) {
201202 val (icon, label, tint) = when (status) {
202203 MessageStatus.Pending -> Triple(Icons.Filled.Schedule, "Sending…", MaterialTheme.colorScheme.onPrimaryContainer)
203204 MessageStatus.Failed -> Triple(Icons.Filled.ErrorOutline, "Failed to send", MaterialTheme.colorScheme.error)
204− MessageStatus.Sent -> Triple(Icons.Filled.Schedule, "", Color.Unspecified)
205+ MessageStatus.Sent -> Triple(Icons.Filled.Check, "", Color.Unspecified)
205206 }
206207 Row(
207208 verticalAlignment = Alignment.CenterVertically,
app/src/main/java/soy/iko/opencode/ui/screens/SessionsScreen.kt+38
@@ -19,9 +19,11 @@ import androidx.compose.material.icons.Icons
1919import androidx.compose.material.icons.automirrored.filled.ArrowBack
2020import androidx.compose.material.icons.filled.Add
2121import androidx.compose.material.icons.filled.ChatBubbleOutline
22+import androidx.compose.material.icons.filled.ChevronRight
2223import androidx.compose.material.icons.filled.DeleteOutline
2324import androidx.compose.material.icons.filled.Refresh
2425import androidx.compose.material3.AlertDialog
26+import androidx.compose.material3.CircularProgressIndicator
2527import androidx.compose.material3.ExperimentalMaterial3Api
2628import androidx.compose.material3.ExtendedFloatingActionButton
2729import androidx.compose.material3.HorizontalDivider
@@ -31,6 +33,7 @@ import androidx.compose.material3.LinearProgressIndicator
3133import androidx.compose.material3.MaterialTheme
3234import androidx.compose.material3.OutlinedTextField
3335import androidx.compose.material3.Scaffold
36+import androidx.compose.material3.Surface
3437import androidx.compose.material3.SwipeToDismissBox
3538import androidx.compose.material3.SwipeToDismissBoxValue
3639import androidx.compose.material3.Text
@@ -117,6 +120,9 @@ fun SessionsScreen(state: UiState, dispatch: (Event) -> Unit) {
117120 },
118121 ) { padding ->
119122 Column(Modifier.fillMaxSize().padding(padding)) {
123+ if (!state.sseConnected) {
124+ ReconnectingBanner()
125+ }
120126 if (state.loading && !isPullRefreshing) {
121127 LinearProgressIndicator(Modifier.fillMaxWidth())
122128 }
@@ -217,6 +223,12 @@ private fun SessionRow(session: SessionView, onOpen: () -> Unit, onDelete: () ->
217223 overflow = TextOverflow.Ellipsis,
218224 )
219225 }
226+ Icon(
227+ Icons.Filled.ChevronRight,
228+ contentDescription = null,
229+ tint = MaterialTheme.colorScheme.onSurfaceVariant,
230+ modifier = Modifier.padding(horizontal = 4.dp),
231+ )
220232 IconButton(onClick = onDelete) {
221233 Icon(
222234 Icons.Filled.DeleteOutline,
@@ -245,6 +257,32 @@ private fun SwipeDeleteBackground() {
245257 }
246258}
247259
260+/** Shown at the top of the list while the SSE stream is down, since session updates stall too. */
261+@Composable
262+private fun ReconnectingBanner() {
263+ Surface(
264+ modifier = Modifier.fillMaxWidth(),
265+ color = MaterialTheme.colorScheme.tertiaryContainer,
266+ ) {
267+ Row(
268+ modifier = Modifier.fillMaxWidth().padding(horizontal = 16.dp, vertical = 6.dp),
269+ verticalAlignment = Alignment.CenterVertically,
270+ horizontalArrangement = Arrangement.spacedBy(8.dp),
271+ ) {
272+ CircularProgressIndicator(
273+ modifier = Modifier.size(14.dp),
274+ strokeWidth = 2.dp,
275+ color = MaterialTheme.colorScheme.onTertiaryContainer,
276+ )
277+ Text(
278+ text = "Reconnecting…",
279+ style = MaterialTheme.typography.bodySmall,
280+ color = MaterialTheme.colorScheme.onTertiaryContainer,
281+ )
282+ }
283+ }
284+}
285+
248286/** Shorten a session ID for display: "abc12345…" instead of the full UUID. */
249287private fun shortSessionId(id: String): String =
250288 if (id.length > 12) id.take(8) + "…" else id